diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..a0bd05f47ec8 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +# https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/README.md +ARG VARIANT=3.13-bookworm +FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} +COPY requirements.txt /tmp/pip-tmp/ +RUN python3 -m pip install --upgrade pip \ + && python3 -m pip install --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ + && pipx install pre-commit ruff \ + && pre-commit install diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000000..ec3cdb61de7a --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1 @@ +https://code.visualstudio.com/docs/devcontainers/tutorial diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..e23263f5b9de --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.11, 3.10, 3.9, 3.8 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.13-bookworm", + } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b68ee8533a61..000000000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -max-line-length = 88 -# max-complexity should be 10 -max-complexity = 19 -extend-ignore = - # Formatting style for `black` - # E203 is whitespace before ':' - E203, - # W503 is line break occurred before a binary operator - W503 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index abf99ab227be..3cc25d1bae1c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,9 +7,7 @@ # Order is important. The last matching pattern has the most precedence. -/.* @cclauss @dhruvmanila - -# /arithmetic_analysis/ +/.* @cclauss # /backtracking/ @@ -21,15 +19,15 @@ # /cellular_automata/ -# /ciphers/ @cclauss # TODO: Uncomment this line after Hacktoberfest +# /ciphers/ # /compression/ # /computer_vision/ -# /conversions/ @cclauss # TODO: Uncomment this line after Hacktoberfest +# /conversions/ -# /data_structures/ @cclauss # TODO: Uncomment this line after Hacktoberfest +# /data_structures/ # /digital_image_processing/ @@ -67,9 +65,9 @@ # /neural_network/ -# /other/ @cclauss # TODO: Uncomment this line after Hacktoberfest +# /other/ -/project_euler/ @dhruvmanila +# /project_euler/ # /quantum/ @@ -81,7 +79,7 @@ # /sorts/ -# /strings/ @cclauss # TODO: Uncomment this line after Hacktoberfest +# /strings/ # /traversals/ diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 09a159b2193e..20823bd58ab1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -6,6 +6,7 @@ body: attributes: value: > Before requesting please search [existing issues](https://github.com/TheAlgorithms/Python/labels/enhancement). + Do not create issues to implement new algorithms as these will be closed. Usage questions such as "How do I...?" belong on the [Discord](https://discord.gg/c7MnfGFGa6) and will be closed. @@ -13,7 +14,6 @@ body: attributes: label: "Feature description" description: > - This could be new algorithms, data structures or improving any existing - implementations. + This could include new topics or improving any existing implementations. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/other.yml b/.github/ISSUE_TEMPLATE/other.yml new file mode 100644 index 000000000000..44d6ff541506 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.yml @@ -0,0 +1,19 @@ +name: Other +description: Use this for any other issues. PLEASE do not create blank issues +labels: ["awaiting triage"] +body: + - type: textarea + id: issuedescription + attributes: + label: What would you like to share? + description: Provide a clear and concise explanation of your issue. + validations: + required: true + + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this issue? + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..15e494ec867e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +# Keep GitHub Actions up to date with Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b3ba8baf9c34..e2ae0966cda5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,6 +4,7 @@ * [ ] Add an algorithm? * [ ] Fix a bug or typo in an existing algorithm? +* [ ] Add or change doctests? -- Note: Please avoid changing both code and tests in a single pull request. * [ ] Documentation change? ### Checklist: @@ -17,4 +18,4 @@ * [ ] All function parameters and return values are annotated with Python [type hints](https://docs.python.org/3/library/typing.html). * [ ] All functions have [doctests](https://docs.python.org/3/library/doctest.html) that pass the automated testing. * [ ] All new algorithms include at least one URL that points to Wikipedia or another similar explanation. -* [ ] If this pull request resolves one or more open issues then the commit message contains `Fixes: #{$ISSUE_NO}`. +* [ ] If this pull request resolves one or more open issues then the description above includes the issue number(s) with a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue): "Fixes #ISSUE-NUMBER". diff --git a/.github/stale.yml b/.github/stale.yml index 36ca56266b26..0939e1f223ff 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -45,7 +45,7 @@ pulls: closeComment: > Please reopen this pull request once you commit the changes requested or make improvements on the code. If this is not the case and you need - some help, feel free to seek help from our [Gitter](https://gitter.im/TheAlgorithms) + some help, feel free to seek help from our [Gitter](https://gitter.im/TheAlgorithms/community) or ping one of the reviewers. Thank you for your contributions! issues: @@ -59,5 +59,5 @@ issues: closeComment: > Please reopen this issue once you add more information and updates here. If this is not the case and you need some help, feel free to seek help - from our [Gitter](https://gitter.im/TheAlgorithms) or ping one of the + from our [Gitter](https://gitter.im/TheAlgorithms/community) or ping one of the reviewers. Thank you for your contributions! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b9cc890b6af..62829b2b45a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,27 +9,28 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 with: - python-version: 3.11 - - uses: actions/cache@v3 + enable-cache: true + cache-dependency-glob: uv.lock + - uses: actions/setup-python@v5 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip setuptools six wheel - python -m pip install pytest-cov -r requirements.txt + python-version: 3.13 + allow-prereleases: true + - run: uv sync --group=test - name: Run tests - # See: #6591 for re-enabling tests on Python v3.11 - run: pytest - --ignore=computer_vision/cnn_classification.py - --ignore=machine_learning/lstm/lstm_prediction.py - --ignore=quantum/ - --ignore=project_euler/ - --ignore=scripts/validate_solutions.py - --cov-report=term-missing:skip-covered - --cov=. . + # TODO: #8818 Re-enable quantum tests + run: uv run pytest + --ignore=computer_vision/cnn_classification.py + --ignore=docs/conf.py + --ignore=dynamic_programming/k_means_clustering_tensorflow.py + --ignore=machine_learning/lstm/lstm_prediction.py + --ignore=neural_network/input_data.py + --ignore=project_euler/ + --ignore=quantum/q_fourier_transform.py + --ignore=scripts/validate_solutions.py + --cov-report=term-missing:skip-covered + --cov=. . - if: ${{ success() }} run: scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index 331962cef11e..55d89f455a25 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -6,15 +6,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 # v1, NOT v2 or v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 with: python-version: 3.x - name: Write DIRECTORY.md run: | scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md - git config --global user.name github-actions - git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY - name: Update DIRECTORY.md run: | diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index 460938219c14..8d51ad8850cf 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -14,26 +14,22 @@ jobs: project-euler: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - uses: actions/setup-python@v5 with: python-version: 3.x - - name: Install pytest and pytest-cov - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade pytest pytest-cov - - run: pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/ + - run: uv sync --group=euler-validate --group=test + - run: uv run pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/ validate-solutions: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - uses: actions/setup-python@v5 with: python-version: 3.x - - name: Install pytest and requests - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade pytest requests - - run: pytest scripts/validate_solutions.py + - run: uv sync --group=euler-validate --group=test + - run: uv run pytest scripts/validate_solutions.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 000000000000..cfe127b3521f --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,16 @@ +# https://beta.ruff.rs +name: ruff +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml new file mode 100644 index 000000000000..16ff284a74f2 --- /dev/null +++ b/.github/workflows/sphinx.yml @@ -0,0 +1,50 @@ +name: sphinx + +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + push: + branches: ["master"] + pull_request: + branches: ["master"] + # Or manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build_docs: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - uses: actions/setup-python@v5 + with: + python-version: 3.13 + allow-prereleases: true + - run: uv sync --group=docs + - uses: actions/configure-pages@v5 + - run: uv run sphinx-build -c docs . docs/_build/html + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html + + deploy_docs: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + if: github.event_name != 'pull_request' + needs: build_docs + runs-on: ubuntu-latest + steps: + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1496984f950..c4480f47faa1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,9 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-executables-have-shebangs + - id: check-toml - id: check-yaml - id: end-of-file-fixer types: [python] @@ -10,85 +11,54 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/MarcoGorelli/auto-walrus - rev: v0.2.2 + rev: 0.3.4 hooks: - - id: auto-walrus + - id: auto-walrus - - repo: https://github.com/psf/black - rev: 23.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.2 hooks: - - id: black + - id: ruff + - id: ruff-format - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 hooks: - - id: isort - args: - - --profile=black + - id: codespell + additional_dependencies: + - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.8.0" + rev: "v2.5.0" hooks: - id: pyproject-fmt - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.12.1 - hooks: - - id: validate-pyproject - - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: - - --py311-plus - - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: local hooks: - - id: flake8 # See .flake8 for args - additional_dependencies: &flake8-plugins - - flake8-bugbear - - flake8-builtins - # - flake8-broken-line - - flake8-comprehensions - - pep8-naming + - id: validate-filenames + name: Validate filenames + entry: ./scripts/validate_filenames.py + language: script + pass_filenames: false - - repo: https://github.com/asottile/yesqa - rev: v1.4.0 + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.23 hooks: - - id: yesqa - additional_dependencies: - *flake8-plugins + - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.14.1 hooks: - id: mypy args: + - --explicit-package-bases - --ignore-missing-imports - --install-types # See mirrors-mypy README.md - --non-interactive additional_dependencies: [types-requests] - - repo: https://github.com/codespell-project/codespell - rev: v2.2.2 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v4.0.0-alpha.8" hooks: - - id: codespell - args: - - --ignore-words-list=ans,crate,damon,fo,followings,hist,iff,mater,secant,som,sur,tim,zar - exclude: | - (?x)^( - ciphers/prehistoric_men.txt | - strings/dictionary.txt | - strings/words.txt | - project_euler/problem_022/p022_names.txt - )$ - - - repo: local - hooks: - - id: validate-filenames - name: Validate filenames - entry: ./scripts/validate_filenames.py - language: script - pass_filenames: false + - id: prettier + types_or: [toml, yaml] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..ef16fa1aa7ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "master" + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ce5bd1edf68..3df39f95b784 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,20 +2,20 @@ ## Before contributing -Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before sending your pull requests, make sure that you __read the whole guidelines__. If you have any doubt on the contributing guide, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community in [Gitter](https://gitter.im/TheAlgorithms). +Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before submitting your pull requests, please ensure that you __read the whole guidelines__. If you have any doubts about the contributing guide, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community on [Gitter](https://gitter.im/TheAlgorithms/community). ## Contributing ### Contributor -We are very happy that you are considering implementing algorithms and data structures for others! This repository is referenced and used by learners from all over the globe. Being one of our contributors, you agree and confirm that: +We are delighted that you are considering implementing algorithms and data structures for others! This repository is referenced and used by learners from all over the globe. By being one of our contributors, you agree and confirm that: -- You did your work - no plagiarism allowed +- You did your work - no plagiarism allowed. - Any plagiarized work will not be merged. -- Your work will be distributed under [MIT License](LICENSE.md) once your pull request is merged -- Your submitted work fulfils or mostly fulfils our styles and standards +- Your work will be distributed under [MIT License](LICENSE.md) once your pull request is merged. +- Your submitted work fulfills or mostly fulfills our styles and standards. -__New implementation__ is welcome! For example, new solutions for a problem, different representations for a graph data structure or algorithm designs with different complexity but __identical implementation__ of an existing implementation is not allowed. Please check whether the solution is already implemented or not before submitting your pull request. +__New implementation__ is welcome! For example, new solutions for a problem, different representations for a graph data structure or algorithm designs with different complexity, but __identical implementation__ of an existing implementation is not allowed. Please check whether the solution is already implemented or not before submitting your pull request. __Improving comments__ and __writing proper tests__ are also highly welcome. @@ -23,9 +23,20 @@ __Improving comments__ and __writing proper tests__ are also highly welcome. We appreciate any contribution, from fixing a grammar mistake in a comment to implementing complex algorithms. Please read this section if you are contributing your work. -Your contribution will be tested by our [automated testing on GitHub Actions](https://github.com/TheAlgorithms/Python/actions) to save time and mental energy. After you have submitted your pull request, you should see the GitHub Actions tests start to run at the bottom of your submission page. If those tests fail, then click on the ___details___ button try to read through the GitHub Actions output to understand the failure. If you do not understand, please leave a comment on your submission page and a community member will try to help. +Your contribution will be tested by our [automated testing on GitHub Actions](https://github.com/TheAlgorithms/Python/actions) to save time and mental energy. After you have submitted your pull request, you should see the GitHub Actions tests start to run at the bottom of your submission page. If those tests fail, then click on the ___details___ button to read through the GitHub Actions output to understand the failure. If you do not understand, please leave a comment on your submission page and a community member will try to help. -Please help us keep our issue list small by adding fixes: #{$ISSUE_NO} to the commit message of pull requests that resolve open issues. GitHub will use this tag to auto-close the issue when the PR is merged. +#### Issues + +If you are interested in resolving an [open issue](https://github.com/TheAlgorithms/Python/issues), simply make a pull request with your proposed fix. __We do not assign issues in this repo__ so please do not ask for permission to work on an issue. + +__Do not__ create an issue to contribute an algorithm. Please submit a pull request instead. + +Please help us keep our issue list small by adding `Fixes #{$ISSUE_NUMBER}` to the description of pull requests that resolve open issues. +For example, if your pull request fixes issue #10, then please add the following to its description: +``` +Fixes #10 +``` +GitHub will use this tag to [auto-close the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if and when the PR is merged. #### What is an Algorithm? @@ -47,7 +58,7 @@ Algorithms should: * contain doctests that test both valid and erroneous input values * return all calculation results instead of printing or plotting them -Algorithms in this repo should not be how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. Those calculations or manipulations can use data types, classes, or functions of existing Python packages but each algorithm in this repo should add unique value. +Algorithms in this repo should not be how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. Those calculations or manipulations can use data types, classes, or functions of existing Python packages but each algorithm in this repo should add unique value. #### Pre-commit plugin Use [pre-commit](https://pre-commit.com/#installation) to automatically format your code to match our coding style: @@ -66,7 +77,7 @@ pre-commit run --all-files --show-diff-on-failure We want your work to be readable by others; therefore, we encourage you to note the following: -- Please write in Python 3.11+. For instance: `print()` is a function in Python 3 so `print "Hello"` will *not* work but `print("Hello")` will. +- Please write in Python 3.13+. For instance: `print()` is a function in Python 3 so `print "Hello"` will *not* work but `print("Hello")` will. - Please focus hard on the naming of functions, classes, and variables. Help your reader by using __descriptive names__ that can help you to remove redundant comments. - Single letter variable names are *old school* so please avoid them unless their life only spans a few lines. - Expand acronyms because `gcd()` is hard to understand but `greatest_common_divisor()` is not. @@ -81,11 +92,11 @@ We want your work to be readable by others; therefore, we encourage you to note black . ``` -- All submissions will need to pass the test `flake8 . --ignore=E203,W503 --max-line-length=88` before they will be accepted so if possible, try this test locally on your Python file(s) before submitting your pull request. +- All submissions will need to pass the test `ruff .` before they will be accepted so if possible, try this test locally on your Python file(s) before submitting your pull request. ```bash - python3 -m pip install flake8 # only required the first time - flake8 . --ignore=E203,W503 --max-line-length=88 --show-source + python3 -m pip install ruff # only required the first time + ruff check ``` - Original code submission require docstrings or comments to describe your work. @@ -134,7 +145,7 @@ We want your work to be readable by others; therefore, we encourage you to note python3 -m doctest -v my_submission.py ``` - The use of the Python builtin `input()` function is __not__ encouraged: + The use of the Python built-in `input()` function is __not__ encouraged: ```python input('Enter your input:') @@ -176,7 +187,7 @@ We want your work to be readable by others; therefore, we encourage you to note - Most importantly, - __Be consistent in the use of these guidelines when submitting.__ - - __Join__ us on [Discord](https://discord.com/invite/c7MnfGFGa6) and [Gitter](https://gitter.im/TheAlgorithms) __now!__ + - __Join__ us on [Discord](https://discord.com/invite/c7MnfGFGa6) and [Gitter](https://gitter.im/TheAlgorithms/community) __now!__ - Happy coding! Writer [@poyea](https://github.com/poyea), Jun 2019. diff --git a/DIRECTORY.md b/DIRECTORY.md index a8786cc2591f..941e30dfe721 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1,17 +1,4 @@ -## Arithmetic Analysis - * [Bisection](arithmetic_analysis/bisection.py) - * [Gaussian Elimination](arithmetic_analysis/gaussian_elimination.py) - * [In Static Equilibrium](arithmetic_analysis/in_static_equilibrium.py) - * [Intersection](arithmetic_analysis/intersection.py) - * [Jacobi Iteration Method](arithmetic_analysis/jacobi_iteration_method.py) - * [Lu Decomposition](arithmetic_analysis/lu_decomposition.py) - * [Newton Forward Interpolation](arithmetic_analysis/newton_forward_interpolation.py) - * [Newton Method](arithmetic_analysis/newton_method.py) - * [Newton Raphson](arithmetic_analysis/newton_raphson.py) - * [Newton Raphson New](arithmetic_analysis/newton_raphson_new.py) - * [Secant Method](arithmetic_analysis/secant_method.py) - ## Audio Filters * [Butterworth Filter](audio_filters/butterworth_filter.py) * [Iir Filter](audio_filters/iir_filter.py) @@ -23,45 +10,60 @@ * [All Subsequences](backtracking/all_subsequences.py) * [Coloring](backtracking/coloring.py) * [Combination Sum](backtracking/combination_sum.py) + * [Crossword Puzzle Solver](backtracking/crossword_puzzle_solver.py) + * [Generate Parentheses](backtracking/generate_parentheses.py) * [Hamiltonian Cycle](backtracking/hamiltonian_cycle.py) * [Knight Tour](backtracking/knight_tour.py) + * [Match Word Pattern](backtracking/match_word_pattern.py) * [Minimax](backtracking/minimax.py) - * [Minmax](backtracking/minmax.py) * [N Queens](backtracking/n_queens.py) * [N Queens Math](backtracking/n_queens_math.py) + * [Power Sum](backtracking/power_sum.py) * [Rat In Maze](backtracking/rat_in_maze.py) * [Sudoku](backtracking/sudoku.py) * [Sum Of Subsets](backtracking/sum_of_subsets.py) + * [Word Break](backtracking/word_break.py) + * [Word Ladder](backtracking/word_ladder.py) * [Word Search](backtracking/word_search.py) ## Bit Manipulation * [Binary And Operator](bit_manipulation/binary_and_operator.py) + * [Binary Coded Decimal](bit_manipulation/binary_coded_decimal.py) * [Binary Count Setbits](bit_manipulation/binary_count_setbits.py) * [Binary Count Trailing Zeros](bit_manipulation/binary_count_trailing_zeros.py) * [Binary Or Operator](bit_manipulation/binary_or_operator.py) * [Binary Shifts](bit_manipulation/binary_shifts.py) * [Binary Twos Complement](bit_manipulation/binary_twos_complement.py) * [Binary Xor Operator](bit_manipulation/binary_xor_operator.py) + * [Bitwise Addition Recursive](bit_manipulation/bitwise_addition_recursive.py) * [Count 1S Brian Kernighan Method](bit_manipulation/count_1s_brian_kernighan_method.py) * [Count Number Of One Bits](bit_manipulation/count_number_of_one_bits.py) + * [Excess 3 Code](bit_manipulation/excess_3_code.py) + * [Find Previous Power Of Two](bit_manipulation/find_previous_power_of_two.py) * [Gray Code Sequence](bit_manipulation/gray_code_sequence.py) * [Highest Set Bit](bit_manipulation/highest_set_bit.py) * [Index Of Rightmost Set Bit](bit_manipulation/index_of_rightmost_set_bit.py) * [Is Even](bit_manipulation/is_even.py) * [Is Power Of Two](bit_manipulation/is_power_of_two.py) + * [Largest Pow Of Two Le Num](bit_manipulation/largest_pow_of_two_le_num.py) + * [Missing Number](bit_manipulation/missing_number.py) * [Numbers Different Signs](bit_manipulation/numbers_different_signs.py) + * [Power Of 4](bit_manipulation/power_of_4.py) * [Reverse Bits](bit_manipulation/reverse_bits.py) * [Single Bit Manipulation Operations](bit_manipulation/single_bit_manipulation_operations.py) + * [Swap All Odd And Even Bits](bit_manipulation/swap_all_odd_and_even_bits.py) ## Blockchain - * [Chinese Remainder Theorem](blockchain/chinese_remainder_theorem.py) * [Diophantine Equation](blockchain/diophantine_equation.py) - * [Modular Division](blockchain/modular_division.py) ## Boolean Algebra * [And Gate](boolean_algebra/and_gate.py) + * [Imply Gate](boolean_algebra/imply_gate.py) + * [Karnaugh Map Simplification](boolean_algebra/karnaugh_map_simplification.py) + * [Multiplexer](boolean_algebra/multiplexer.py) * [Nand Gate](boolean_algebra/nand_gate.py) - * [Norgate](boolean_algebra/norgate.py) + * [Nimply Gate](boolean_algebra/nimply_gate.py) + * [Nor Gate](boolean_algebra/nor_gate.py) * [Not Gate](boolean_algebra/not_gate.py) * [Or Gate](boolean_algebra/or_gate.py) * [Quine Mc Cluskey](boolean_algebra/quine_mc_cluskey.py) @@ -71,8 +73,10 @@ ## Cellular Automata * [Conways Game Of Life](cellular_automata/conways_game_of_life.py) * [Game Of Life](cellular_automata/game_of_life.py) + * [Langtons Ant](cellular_automata/langtons_ant.py) * [Nagel Schrekenberg](cellular_automata/nagel_schrekenberg.py) * [One Dimensional](cellular_automata/one_dimensional.py) + * [Wa Tor](cellular_automata/wa_tor.py) ## Ciphers * [A1Z26](ciphers/a1z26.py) @@ -82,7 +86,7 @@ * [Baconian Cipher](ciphers/baconian_cipher.py) * [Base16](ciphers/base16.py) * [Base32](ciphers/base32.py) - * [Base64](ciphers/base64.py) + * [Base64 Cipher](ciphers/base64_cipher.py) * [Base85](ciphers/base85.py) * [Beaufort Cipher](ciphers/beaufort_cipher.py) * [Bifid](ciphers/bifid.py) @@ -95,11 +99,14 @@ * [Diffie Hellman](ciphers/diffie_hellman.py) * [Elgamal Key Generator](ciphers/elgamal_key_generator.py) * [Enigma Machine2](ciphers/enigma_machine2.py) + * [Fractionated Morse Cipher](ciphers/fractionated_morse_cipher.py) + * [Gronsfeld Cipher](ciphers/gronsfeld_cipher.py) * [Hill Cipher](ciphers/hill_cipher.py) * [Mixed Keyword Cypher](ciphers/mixed_keyword_cypher.py) * [Mono Alphabetic Ciphers](ciphers/mono_alphabetic_ciphers.py) * [Morse Code](ciphers/morse_code.py) * [Onepad Cipher](ciphers/onepad_cipher.py) + * [Permutation Cipher](ciphers/permutation_cipher.py) * [Playfair Cipher](ciphers/playfair_cipher.py) * [Polybius](ciphers/polybius.py) * [Porta Cipher](ciphers/porta_cipher.py) @@ -109,12 +116,14 @@ * [Rsa Cipher](ciphers/rsa_cipher.py) * [Rsa Factorization](ciphers/rsa_factorization.py) * [Rsa Key Generator](ciphers/rsa_key_generator.py) + * [Running Key Cipher](ciphers/running_key_cipher.py) * [Shuffled Shift Cipher](ciphers/shuffled_shift_cipher.py) * [Simple Keyword Cypher](ciphers/simple_keyword_cypher.py) * [Simple Substitution Cipher](ciphers/simple_substitution_cipher.py) - * [Trafid Cipher](ciphers/trafid_cipher.py) * [Transposition Cipher](ciphers/transposition_cipher.py) * [Transposition Cipher Encrypt Decrypt File](ciphers/transposition_cipher_encrypt_decrypt_file.py) + * [Trifid Cipher](ciphers/trifid_cipher.py) + * [Vernam Cipher](ciphers/vernam_cipher.py) * [Vigenere Cipher](ciphers/vigenere_cipher.py) * [Xor Cipher](ciphers/xor_cipher.py) @@ -130,8 +139,10 @@ ## Computer Vision * [Cnn Classification](computer_vision/cnn_classification.py) * [Flip Augmentation](computer_vision/flip_augmentation.py) + * [Haralick Descriptors](computer_vision/haralick_descriptors.py) * [Harris Corner](computer_vision/harris_corner.py) * [Horn Schunck](computer_vision/horn_schunck.py) + * [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py) * [Mean Threshold](computer_vision/mean_threshold.py) * [Mosaic Augmentation](computer_vision/mosaic_augmentation.py) * [Pooling Functions](computer_vision/pooling_functions.py) @@ -141,31 +152,48 @@ * [Binary To Decimal](conversions/binary_to_decimal.py) * [Binary To Hexadecimal](conversions/binary_to_hexadecimal.py) * [Binary To Octal](conversions/binary_to_octal.py) + * [Convert Number To Words](conversions/convert_number_to_words.py) * [Decimal To Any](conversions/decimal_to_any.py) * [Decimal To Binary](conversions/decimal_to_binary.py) - * [Decimal To Binary Recursion](conversions/decimal_to_binary_recursion.py) * [Decimal To Hexadecimal](conversions/decimal_to_hexadecimal.py) * [Decimal To Octal](conversions/decimal_to_octal.py) + * [Energy Conversions](conversions/energy_conversions.py) * [Excel Title To Column](conversions/excel_title_to_column.py) * [Hex To Bin](conversions/hex_to_bin.py) * [Hexadecimal To Decimal](conversions/hexadecimal_to_decimal.py) + * [Ipv4 Conversion](conversions/ipv4_conversion.py) * [Length Conversion](conversions/length_conversion.py) * [Molecular Chemistry](conversions/molecular_chemistry.py) + * [Octal To Binary](conversions/octal_to_binary.py) * [Octal To Decimal](conversions/octal_to_decimal.py) + * [Octal To Hexadecimal](conversions/octal_to_hexadecimal.py) * [Prefix Conversions](conversions/prefix_conversions.py) * [Prefix Conversions String](conversions/prefix_conversions_string.py) * [Pressure Conversions](conversions/pressure_conversions.py) + * [Rectangular To Polar](conversions/rectangular_to_polar.py) + * [Rgb Cmyk Conversion](conversions/rgb_cmyk_conversion.py) * [Rgb Hsv Conversion](conversions/rgb_hsv_conversion.py) * [Roman Numerals](conversions/roman_numerals.py) * [Speed Conversions](conversions/speed_conversions.py) * [Temperature Conversions](conversions/temperature_conversions.py) + * [Time Conversions](conversions/time_conversions.py) * [Volume Conversions](conversions/volume_conversions.py) * [Weight Conversion](conversions/weight_conversion.py) ## Data Structures * Arrays + * [Equilibrium Index In Array](data_structures/arrays/equilibrium_index_in_array.py) + * [Find Triplets With 0 Sum](data_structures/arrays/find_triplets_with_0_sum.py) + * [Index 2D Array In 1D](data_structures/arrays/index_2d_array_in_1d.py) + * [Kth Largest Element](data_structures/arrays/kth_largest_element.py) + * [Median Two Array](data_structures/arrays/median_two_array.py) + * [Monotonic Array](data_structures/arrays/monotonic_array.py) + * [Pairs With Given Sum](data_structures/arrays/pairs_with_given_sum.py) * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) + * [Product Sum](data_structures/arrays/product_sum.py) + * [Sparse Table](data_structures/arrays/sparse_table.py) + * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree * [Avl Tree](data_structures/binary_tree/avl_tree.py) * [Basic Binary Tree](data_structures/binary_tree/basic_binary_tree.py) @@ -175,32 +203,44 @@ * [Binary Tree Node Sum](data_structures/binary_tree/binary_tree_node_sum.py) * [Binary Tree Path Sum](data_structures/binary_tree/binary_tree_path_sum.py) * [Binary Tree Traversals](data_structures/binary_tree/binary_tree_traversals.py) + * [Diameter Of Binary Tree](data_structures/binary_tree/diameter_of_binary_tree.py) * [Diff Views Of Binary Tree](data_structures/binary_tree/diff_views_of_binary_tree.py) * [Distribute Coins](data_structures/binary_tree/distribute_coins.py) * [Fenwick Tree](data_structures/binary_tree/fenwick_tree.py) + * [Flatten Binarytree To Linkedlist](data_structures/binary_tree/flatten_binarytree_to_linkedlist.py) + * [Floor And Ceiling](data_structures/binary_tree/floor_and_ceiling.py) * [Inorder Tree Traversal 2022](data_structures/binary_tree/inorder_tree_traversal_2022.py) - * [Is Bst](data_structures/binary_tree/is_bst.py) + * [Is Sorted](data_structures/binary_tree/is_sorted.py) + * [Is Sum Tree](data_structures/binary_tree/is_sum_tree.py) * [Lazy Segment Tree](data_structures/binary_tree/lazy_segment_tree.py) * [Lowest Common Ancestor](data_structures/binary_tree/lowest_common_ancestor.py) * [Maximum Fenwick Tree](data_structures/binary_tree/maximum_fenwick_tree.py) + * [Maximum Sum Bst](data_structures/binary_tree/maximum_sum_bst.py) * [Merge Two Binary Trees](data_structures/binary_tree/merge_two_binary_trees.py) + * [Mirror Binary Tree](data_structures/binary_tree/mirror_binary_tree.py) * [Non Recursive Segment Tree](data_structures/binary_tree/non_recursive_segment_tree.py) * [Number Of Possible Binary Trees](data_structures/binary_tree/number_of_possible_binary_trees.py) * [Red Black Tree](data_structures/binary_tree/red_black_tree.py) * [Segment Tree](data_structures/binary_tree/segment_tree.py) * [Segment Tree Other](data_structures/binary_tree/segment_tree_other.py) + * [Serialize Deserialize Binary Tree](data_structures/binary_tree/serialize_deserialize_binary_tree.py) + * [Symmetric Tree](data_structures/binary_tree/symmetric_tree.py) * [Treap](data_structures/binary_tree/treap.py) * [Wavelet Tree](data_structures/binary_tree/wavelet_tree.py) * Disjoint Set * [Alternate Disjoint Set](data_structures/disjoint_set/alternate_disjoint_set.py) * [Disjoint Set](data_structures/disjoint_set/disjoint_set.py) * Hashing + * [Bloom Filter](data_structures/hashing/bloom_filter.py) * [Double Hash](data_structures/hashing/double_hash.py) + * [Hash Map](data_structures/hashing/hash_map.py) * [Hash Table](data_structures/hashing/hash_table.py) * [Hash Table With Linked List](data_structures/hashing/hash_table_with_linked_list.py) * Number Theory * [Prime Numbers](data_structures/hashing/number_theory/prime_numbers.py) * [Quadratic Probing](data_structures/hashing/quadratic_probing.py) + * Tests + * [Test Hash Map](data_structures/hashing/tests/test_hash_map.py) * Heap * [Binomial Heap](data_structures/heap/binomial_heap.py) * [Heap](data_structures/heap/heap.py) @@ -209,17 +249,29 @@ * [Min Heap](data_structures/heap/min_heap.py) * [Randomized Heap](data_structures/heap/randomized_heap.py) * [Skew Heap](data_structures/heap/skew_heap.py) + * Kd Tree + * [Build Kdtree](data_structures/kd_tree/build_kdtree.py) + * Example + * [Example Usage](data_structures/kd_tree/example/example_usage.py) + * [Hypercube Points](data_structures/kd_tree/example/hypercube_points.py) + * [Kd Node](data_structures/kd_tree/kd_node.py) + * [Nearest Neighbour Search](data_structures/kd_tree/nearest_neighbour_search.py) + * Tests + * [Test Kdtree](data_structures/kd_tree/tests/test_kdtree.py) * Linked List * [Circular Linked List](data_structures/linked_list/circular_linked_list.py) * [Deque Doubly](data_structures/linked_list/deque_doubly.py) * [Doubly Linked List](data_structures/linked_list/doubly_linked_list.py) * [Doubly Linked List Two](data_structures/linked_list/doubly_linked_list_two.py) + * [Floyds Cycle Detection](data_structures/linked_list/floyds_cycle_detection.py) * [From Sequence](data_structures/linked_list/from_sequence.py) * [Has Loop](data_structures/linked_list/has_loop.py) * [Is Palindrome](data_structures/linked_list/is_palindrome.py) * [Merge Two Lists](data_structures/linked_list/merge_two_lists.py) * [Middle Element Of Linked List](data_structures/linked_list/middle_element_of_linked_list.py) * [Print Reverse](data_structures/linked_list/print_reverse.py) + * [Reverse K Group](data_structures/linked_list/reverse_k_group.py) + * [Rotate To The Right](data_structures/linked_list/rotate_to_the_right.py) * [Singly Linked List](data_structures/linked_list/singly_linked_list.py) * [Skip List](data_structures/linked_list/skip_list.py) * [Swap Nodes](data_structures/linked_list/swap_nodes.py) @@ -229,21 +281,30 @@ * [Double Ended Queue](data_structures/queue/double_ended_queue.py) * [Linked Queue](data_structures/queue/linked_queue.py) * [Priority Queue Using List](data_structures/queue/priority_queue_using_list.py) - * [Queue On List](data_structures/queue/queue_on_list.py) + * [Queue By List](data_structures/queue/queue_by_list.py) + * [Queue By Two Stacks](data_structures/queue/queue_by_two_stacks.py) * [Queue On Pseudo Stack](data_structures/queue/queue_on_pseudo_stack.py) * Stacks * [Balanced Parentheses](data_structures/stacks/balanced_parentheses.py) * [Dijkstras Two Stack Algorithm](data_structures/stacks/dijkstras_two_stack_algorithm.py) - * [Evaluate Postfix Notations](data_structures/stacks/evaluate_postfix_notations.py) * [Infix To Postfix Conversion](data_structures/stacks/infix_to_postfix_conversion.py) * [Infix To Prefix Conversion](data_structures/stacks/infix_to_prefix_conversion.py) + * [Lexicographical Numbers](data_structures/stacks/lexicographical_numbers.py) * [Next Greater Element](data_structures/stacks/next_greater_element.py) * [Postfix Evaluation](data_structures/stacks/postfix_evaluation.py) * [Prefix Evaluation](data_structures/stacks/prefix_evaluation.py) * [Stack](data_structures/stacks/stack.py) + * [Stack Using Two Queues](data_structures/stacks/stack_using_two_queues.py) * [Stack With Doubly Linked List](data_structures/stacks/stack_with_doubly_linked_list.py) * [Stack With Singly Linked List](data_structures/stacks/stack_with_singly_linked_list.py) * [Stock Span Problem](data_structures/stacks/stock_span_problem.py) + * Suffix Tree + * Example + * [Example Usage](data_structures/suffix_tree/example/example_usage.py) + * [Suffix Tree](data_structures/suffix_tree/suffix_tree.py) + * [Suffix Tree Node](data_structures/suffix_tree/suffix_tree_node.py) + * Tests + * [Test Suffix Tree](data_structures/suffix_tree/tests/test_suffix_tree.py) * Trie * [Radix Tree](data_structures/trie/radix_tree.py) * [Trie](data_structures/trie/trie.py) @@ -261,6 +322,7 @@ * [Convolve](digital_image_processing/filters/convolve.py) * [Gabor Filter](digital_image_processing/filters/gabor_filter.py) * [Gaussian Filter](digital_image_processing/filters/gaussian_filter.py) + * [Laplacian Filter](digital_image_processing/filters/laplacian_filter.py) * [Local Binary Pattern](digital_image_processing/filters/local_binary_pattern.py) * [Median Filter](digital_image_processing/filters/median_filter.py) * [Sobel Filter](digital_image_processing/filters/sobel_filter.py) @@ -285,12 +347,15 @@ * [Inversions](divide_and_conquer/inversions.py) * [Kth Order Statistic](divide_and_conquer/kth_order_statistic.py) * [Max Difference Pair](divide_and_conquer/max_difference_pair.py) - * [Max Subarray Sum](divide_and_conquer/max_subarray_sum.py) + * [Max Subarray](divide_and_conquer/max_subarray.py) * [Mergesort](divide_and_conquer/mergesort.py) * [Peak](divide_and_conquer/peak.py) * [Power](divide_and_conquer/power.py) * [Strassen Matrix Multiplication](divide_and_conquer/strassen_matrix_multiplication.py) +## Docs + * [Conf](docs/conf.py) + ## Dynamic Programming * [Abbreviation](dynamic_programming/abbreviation.py) * [All Construct](dynamic_programming/all_construct.py) @@ -306,42 +371,61 @@ * [Floyd Warshall](dynamic_programming/floyd_warshall.py) * [Integer Partition](dynamic_programming/integer_partition.py) * [Iterating Through Submasks](dynamic_programming/iterating_through_submasks.py) + * [K Means Clustering Tensorflow](dynamic_programming/k_means_clustering_tensorflow.py) * [Knapsack](dynamic_programming/knapsack.py) + * [Largest Divisible Subset](dynamic_programming/largest_divisible_subset.py) * [Longest Common Subsequence](dynamic_programming/longest_common_subsequence.py) * [Longest Common Substring](dynamic_programming/longest_common_substring.py) * [Longest Increasing Subsequence](dynamic_programming/longest_increasing_subsequence.py) - * [Longest Increasing Subsequence O(Nlogn)](dynamic_programming/longest_increasing_subsequence_o(nlogn).py) - * [Longest Sub Array](dynamic_programming/longest_sub_array.py) + * [Longest Increasing Subsequence Iterative](dynamic_programming/longest_increasing_subsequence_iterative.py) + * [Longest Increasing Subsequence O Nlogn](dynamic_programming/longest_increasing_subsequence_o_nlogn.py) + * [Longest Palindromic Subsequence](dynamic_programming/longest_palindromic_subsequence.py) + * [Matrix Chain Multiplication](dynamic_programming/matrix_chain_multiplication.py) * [Matrix Chain Order](dynamic_programming/matrix_chain_order.py) * [Max Non Adjacent Sum](dynamic_programming/max_non_adjacent_sum.py) - * [Max Sub Array](dynamic_programming/max_sub_array.py) - * [Max Sum Contiguous Subsequence](dynamic_programming/max_sum_contiguous_subsequence.py) + * [Max Product Subarray](dynamic_programming/max_product_subarray.py) + * [Max Subarray Sum](dynamic_programming/max_subarray_sum.py) * [Min Distance Up Bottom](dynamic_programming/min_distance_up_bottom.py) * [Minimum Coin Change](dynamic_programming/minimum_coin_change.py) * [Minimum Cost Path](dynamic_programming/minimum_cost_path.py) * [Minimum Partition](dynamic_programming/minimum_partition.py) + * [Minimum Size Subarray Sum](dynamic_programming/minimum_size_subarray_sum.py) * [Minimum Squares To Represent A Number](dynamic_programming/minimum_squares_to_represent_a_number.py) * [Minimum Steps To One](dynamic_programming/minimum_steps_to_one.py) * [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py) * [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py) * [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py) + * [Regex Match](dynamic_programming/regex_match.py) * [Rod Cutting](dynamic_programming/rod_cutting.py) + * [Smith Waterman](dynamic_programming/smith_waterman.py) * [Subset Generation](dynamic_programming/subset_generation.py) * [Sum Of Subset](dynamic_programming/sum_of_subset.py) + * [Trapped Water](dynamic_programming/trapped_water.py) + * [Tribonacci](dynamic_programming/tribonacci.py) * [Viterbi](dynamic_programming/viterbi.py) + * [Wildcard Matching](dynamic_programming/wildcard_matching.py) * [Word Break](dynamic_programming/word_break.py) ## Electronics + * [Apparent Power](electronics/apparent_power.py) * [Builtin Voltage](electronics/builtin_voltage.py) + * [Capacitor Equivalence](electronics/capacitor_equivalence.py) * [Carrier Concentration](electronics/carrier_concentration.py) + * [Charging Capacitor](electronics/charging_capacitor.py) + * [Charging Inductor](electronics/charging_inductor.py) + * [Circular Convolution](electronics/circular_convolution.py) * [Coulombs Law](electronics/coulombs_law.py) * [Electric Conductivity](electronics/electric_conductivity.py) * [Electric Power](electronics/electric_power.py) * [Electrical Impedance](electronics/electrical_impedance.py) + * [Ic 555 Timer](electronics/ic_555_timer.py) * [Ind Reactance](electronics/ind_reactance.py) * [Ohms Law](electronics/ohms_law.py) + * [Real And Reactive Power](electronics/real_and_reactive_power.py) + * [Resistor Color Code](electronics/resistor_color_code.py) * [Resistor Equivalence](electronics/resistor_equivalence.py) * [Resonant Frequency](electronics/resonant_frequency.py) + * [Wheatstone Bridge](electronics/wheatstone_bridge.py) ## File Transfer * [Receive File](file_transfer/receive_file.py) @@ -351,14 +435,18 @@ ## Financial * [Equated Monthly Installments](financial/equated_monthly_installments.py) + * [Exponential Moving Average](financial/exponential_moving_average.py) * [Interest](financial/interest.py) + * [Present Value](financial/present_value.py) * [Price Plus Tax](financial/price_plus_tax.py) + * [Simple Moving Average](financial/simple_moving_average.py) ## Fractals * [Julia Sets](fractals/julia_sets.py) * [Koch Snowflake](fractals/koch_snowflake.py) * [Mandelbrot](fractals/mandelbrot.py) * [Sierpinski Triangle](fractals/sierpinski_triangle.py) + * [Vicsek](fractals/vicsek.py) ## Fuzzy Logic * [Fuzzy Operations](fuzzy_logic/fuzzy_operations.py) @@ -370,12 +458,18 @@ * [Haversine Distance](geodesy/haversine_distance.py) * [Lamberts Ellipsoidal Distance](geodesy/lamberts_ellipsoidal_distance.py) +## Geometry + * [Geometry](geometry/geometry.py) + ## Graphics * [Bezier Curve](graphics/bezier_curve.py) + * [Butterfly Pattern](graphics/butterfly_pattern.py) + * [Digital Differential Analyzer Line](graphics/digital_differential_analyzer_line.py) * [Vector3 For 2D Rendering](graphics/vector3_for_2d_rendering.py) ## Graphs * [A Star](graphs/a_star.py) + * [Ant Colony Optimization Algorithms](graphs/ant_colony_optimization_algorithms.py) * [Articulation Points](graphs/articulation_points.py) * [Basic Graphs](graphs/basic_graphs.py) * [Bellman Ford](graphs/bellman_ford.py) @@ -388,18 +482,19 @@ * [Breadth First Search Shortest Path](graphs/breadth_first_search_shortest_path.py) * [Breadth First Search Shortest Path 2](graphs/breadth_first_search_shortest_path_2.py) * [Breadth First Search Zero One Shortest Path](graphs/breadth_first_search_zero_one_shortest_path.py) - * [Check Bipartite Graph Bfs](graphs/check_bipartite_graph_bfs.py) - * [Check Bipartite Graph Dfs](graphs/check_bipartite_graph_dfs.py) + * [Check Bipatrite](graphs/check_bipatrite.py) * [Check Cycle](graphs/check_cycle.py) * [Connected Components](graphs/connected_components.py) + * [Deep Clone Graph](graphs/deep_clone_graph.py) * [Depth First Search](graphs/depth_first_search.py) * [Depth First Search 2](graphs/depth_first_search_2.py) * [Dijkstra](graphs/dijkstra.py) * [Dijkstra 2](graphs/dijkstra_2.py) * [Dijkstra Algorithm](graphs/dijkstra_algorithm.py) * [Dijkstra Alternate](graphs/dijkstra_alternate.py) + * [Dijkstra Binary Grid](graphs/dijkstra_binary_grid.py) * [Dinic](graphs/dinic.py) - * [Directed And Undirected (Weighted) Graph](graphs/directed_and_undirected_(weighted)_graph.py) + * [Directed And Undirected Weighted Graph](graphs/directed_and_undirected_weighted_graph.py) * [Edmonds Karp Multiple Source And Sink](graphs/edmonds_karp_multiple_source_and_sink.py) * [Eulerian Path And Circuit For Undirected Graph](graphs/eulerian_path_and_circuit_for_undirected_graph.py) * [Even Tree](graphs/even_tree.py) @@ -407,14 +502,16 @@ * [Frequent Pattern Graph Miner](graphs/frequent_pattern_graph_miner.py) * [G Topological Sort](graphs/g_topological_sort.py) * [Gale Shapley Bigraph](graphs/gale_shapley_bigraph.py) + * [Graph Adjacency List](graphs/graph_adjacency_list.py) + * [Graph Adjacency Matrix](graphs/graph_adjacency_matrix.py) * [Graph List](graphs/graph_list.py) - * [Graph Matrix](graphs/graph_matrix.py) * [Graphs Floyd Warshall](graphs/graphs_floyd_warshall.py) * [Greedy Best First](graphs/greedy_best_first.py) * [Greedy Min Vertex Cover](graphs/greedy_min_vertex_cover.py) * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py) * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py) * [Karger](graphs/karger.py) + * [Lanczos Eigenvectors](graphs/lanczos_eigenvectors.py) * [Markov Chain](graphs/markov_chain.py) * [Matching Min Vertex Cover](graphs/matching_min_vertex_cover.py) * [Minimum Path Sum](graphs/minimum_path_sum.py) @@ -435,9 +532,15 @@ * [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py) ## Greedy Methods + * [Best Time To Buy And Sell Stock](greedy_methods/best_time_to_buy_and_sell_stock.py) + * [Fractional Cover Problem](greedy_methods/fractional_cover_problem.py) * [Fractional Knapsack](greedy_methods/fractional_knapsack.py) * [Fractional Knapsack 2](greedy_methods/fractional_knapsack_2.py) + * [Gas Station](greedy_methods/gas_station.py) + * [Minimum Coin Change](greedy_methods/minimum_coin_change.py) + * [Minimum Waiting Time](greedy_methods/minimum_waiting_time.py) * [Optimal Merge Pattern](greedy_methods/optimal_merge_pattern.py) + * [Smallest Range](greedy_methods/smallest_range.py) ## Hashes * [Adler32](hashes/adler32.py) @@ -445,6 +548,7 @@ * [Djb2](hashes/djb2.py) * [Elf](hashes/elf.py) * [Enigma Machine](hashes/enigma_machine.py) + * [Fletcher16](hashes/fletcher16.py) * [Hamming Code](hashes/hamming_code.py) * [Luhn](hashes/luhn.py) * [Md5](hashes/md5.py) @@ -461,35 +565,49 @@ * [Test Knapsack](knapsack/tests/test_knapsack.py) ## Linear Algebra + * [Gaussian Elimination](linear_algebra/gaussian_elimination.py) + * [Jacobi Iteration Method](linear_algebra/jacobi_iteration_method.py) + * [Lu Decomposition](linear_algebra/lu_decomposition.py) * Src * [Conjugate Gradient](linear_algebra/src/conjugate_gradient.py) + * [Gaussian Elimination Pivoting](linear_algebra/src/gaussian_elimination_pivoting.py) * [Lib](linear_algebra/src/lib.py) * [Polynom For Points](linear_algebra/src/polynom_for_points.py) * [Power Iteration](linear_algebra/src/power_iteration.py) + * [Rank Of Matrix](linear_algebra/src/rank_of_matrix.py) * [Rayleigh Quotient](linear_algebra/src/rayleigh_quotient.py) * [Schur Complement](linear_algebra/src/schur_complement.py) * [Test Linear Algebra](linear_algebra/src/test_linear_algebra.py) * [Transformations 2D](linear_algebra/src/transformations_2d.py) +## Linear Programming + * [Simplex](linear_programming/simplex.py) + ## Machine Learning + * [Apriori Algorithm](machine_learning/apriori_algorithm.py) * [Astar](machine_learning/astar.py) + * [Automatic Differentiation](machine_learning/automatic_differentiation.py) * [Data Transformations](machine_learning/data_transformations.py) * [Decision Tree](machine_learning/decision_tree.py) + * [Dimensionality Reduction](machine_learning/dimensionality_reduction.py) * Forecasting * [Run](machine_learning/forecasting/run.py) + * [Frequent Pattern Growth](machine_learning/frequent_pattern_growth.py) + * [Gradient Boosting Classifier](machine_learning/gradient_boosting_classifier.py) * [Gradient Descent](machine_learning/gradient_descent.py) * [K Means Clust](machine_learning/k_means_clust.py) * [K Nearest Neighbours](machine_learning/k_nearest_neighbours.py) - * [Knn Sklearn](machine_learning/knn_sklearn.py) * [Linear Discriminant Analysis](machine_learning/linear_discriminant_analysis.py) * [Linear Regression](machine_learning/linear_regression.py) * Local Weighted Learning * [Local Weighted Learning](machine_learning/local_weighted_learning/local_weighted_learning.py) * [Logistic Regression](machine_learning/logistic_regression.py) + * [Loss Functions](machine_learning/loss_functions.py) * Lstm * [Lstm Prediction](machine_learning/lstm/lstm_prediction.py) + * [Mfcc](machine_learning/mfcc.py) * [Multilayer Perceptron Classifier](machine_learning/multilayer_perceptron_classifier.py) - * [Polymonial Regression](machine_learning/polymonial_regression.py) + * [Polynomial Regression](machine_learning/polynomial_regression.py) * [Scoring Functions](machine_learning/scoring_functions.py) * [Self Organizing Map](machine_learning/self_organizing_map.py) * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py) @@ -500,79 +618,70 @@ * [Xgboost Regressor](machine_learning/xgboost_regressor.py) ## Maths - * [3N Plus 1](maths/3n_plus_1.py) * [Abs](maths/abs.py) - * [Add](maths/add.py) * [Addition Without Arithmetic](maths/addition_without_arithmetic.py) * [Aliquot Sum](maths/aliquot_sum.py) * [Allocation Number](maths/allocation_number.py) * [Arc Length](maths/arc_length.py) * [Area](maths/area.py) * [Area Under Curve](maths/area_under_curve.py) - * [Armstrong Numbers](maths/armstrong_numbers.py) - * [Automorphic Number](maths/automorphic_number.py) * [Average Absolute Deviation](maths/average_absolute_deviation.py) * [Average Mean](maths/average_mean.py) * [Average Median](maths/average_median.py) * [Average Mode](maths/average_mode.py) * [Bailey Borwein Plouffe](maths/bailey_borwein_plouffe.py) + * [Base Neg2 Conversion](maths/base_neg2_conversion.py) * [Basic Maths](maths/basic_maths.py) - * [Binary Exp Mod](maths/binary_exp_mod.py) * [Binary Exponentiation](maths/binary_exponentiation.py) - * [Binary Exponentiation 2](maths/binary_exponentiation_2.py) - * [Binary Exponentiation 3](maths/binary_exponentiation_3.py) + * [Binary Multiplication](maths/binary_multiplication.py) * [Binomial Coefficient](maths/binomial_coefficient.py) * [Binomial Distribution](maths/binomial_distribution.py) - * [Bisection](maths/bisection.py) - * [Carmichael Number](maths/carmichael_number.py) - * [Catalan Number](maths/catalan_number.py) * [Ceil](maths/ceil.py) + * [Chebyshev Distance](maths/chebyshev_distance.py) * [Check Polygon](maths/check_polygon.py) + * [Chinese Remainder Theorem](maths/chinese_remainder_theorem.py) * [Chudnovsky Algorithm](maths/chudnovsky_algorithm.py) * [Collatz Sequence](maths/collatz_sequence.py) * [Combinations](maths/combinations.py) + * [Continued Fraction](maths/continued_fraction.py) * [Decimal Isolate](maths/decimal_isolate.py) * [Decimal To Fraction](maths/decimal_to_fraction.py) * [Dodecahedron](maths/dodecahedron.py) - * [Double Factorial Iterative](maths/double_factorial_iterative.py) - * [Double Factorial Recursive](maths/double_factorial_recursive.py) + * [Double Factorial](maths/double_factorial.py) + * [Dual Number Automatic Differentiation](maths/dual_number_automatic_differentiation.py) * [Entropy](maths/entropy.py) * [Euclidean Distance](maths/euclidean_distance.py) - * [Euclidean Gcd](maths/euclidean_gcd.py) * [Euler Method](maths/euler_method.py) * [Euler Modified](maths/euler_modified.py) * [Eulers Totient](maths/eulers_totient.py) * [Extended Euclidean Algorithm](maths/extended_euclidean_algorithm.py) * [Factorial](maths/factorial.py) * [Factors](maths/factors.py) + * [Fast Inverse Sqrt](maths/fast_inverse_sqrt.py) * [Fermat Little Theorem](maths/fermat_little_theorem.py) * [Fibonacci](maths/fibonacci.py) * [Find Max](maths/find_max.py) - * [Find Max Recursion](maths/find_max_recursion.py) * [Find Min](maths/find_min.py) - * [Find Min Recursion](maths/find_min_recursion.py) * [Floor](maths/floor.py) * [Gamma](maths/gamma.py) - * [Gamma Recursive](maths/gamma_recursive.py) * [Gaussian](maths/gaussian.py) - * [Gaussian Error Linear Unit](maths/gaussian_error_linear_unit.py) * [Gcd Of N Numbers](maths/gcd_of_n_numbers.py) + * [Geometric Mean](maths/geometric_mean.py) + * [Germain Primes](maths/germain_primes.py) * [Greatest Common Divisor](maths/greatest_common_divisor.py) - * [Greedy Coin Change](maths/greedy_coin_change.py) - * [Hamming Numbers](maths/hamming_numbers.py) * [Hardy Ramanujanalgo](maths/hardy_ramanujanalgo.py) - * [Hexagonal Number](maths/hexagonal_number.py) - * [Integration By Simpson Approx](maths/integration_by_simpson_approx.py) + * [Integer Square Root](maths/integer_square_root.py) + * [Interquartile Range](maths/interquartile_range.py) + * [Is Int Palindrome](maths/is_int_palindrome.py) * [Is Ip V4 Address Valid](maths/is_ip_v4_address_valid.py) * [Is Square Free](maths/is_square_free.py) * [Jaccard Similarity](maths/jaccard_similarity.py) + * [Joint Probability Distribution](maths/joint_probability_distribution.py) + * [Josephus Problem](maths/josephus_problem.py) * [Juggler Sequence](maths/juggler_sequence.py) - * [Kadanes](maths/kadanes.py) * [Karatsuba](maths/karatsuba.py) - * [Krishnamurthy Number](maths/krishnamurthy_number.py) * [Kth Lexicographic Permutation](maths/kth_lexicographic_permutation.py) * [Largest Of Very Large Numbers](maths/largest_of_very_large_numbers.py) - * [Largest Subarray Sum](maths/largest_subarray_sum.py) * [Least Common Multiple](maths/least_common_multiple.py) * [Line Length](maths/line_length.py) * [Liouville Lambda](maths/liouville_lambda.py) @@ -582,20 +691,36 @@ * [Manhattan Distance](maths/manhattan_distance.py) * [Matrix Exponentiation](maths/matrix_exponentiation.py) * [Max Sum Sliding Window](maths/max_sum_sliding_window.py) - * [Median Of Two Arrays](maths/median_of_two_arrays.py) - * [Miller Rabin](maths/miller_rabin.py) + * [Minkowski Distance](maths/minkowski_distance.py) * [Mobius Function](maths/mobius_function.py) + * [Modular Division](maths/modular_division.py) * [Modular Exponential](maths/modular_exponential.py) * [Monte Carlo](maths/monte_carlo.py) * [Monte Carlo Dice](maths/monte_carlo_dice.py) - * [Nevilles Method](maths/nevilles_method.py) - * [Newton Raphson](maths/newton_raphson.py) * [Number Of Digits](maths/number_of_digits.py) - * [Numerical Integration](maths/numerical_integration.py) + * Numerical Analysis + * [Adams Bashforth](maths/numerical_analysis/adams_bashforth.py) + * [Bisection](maths/numerical_analysis/bisection.py) + * [Bisection 2](maths/numerical_analysis/bisection_2.py) + * [Integration By Simpson Approx](maths/numerical_analysis/integration_by_simpson_approx.py) + * [Intersection](maths/numerical_analysis/intersection.py) + * [Nevilles Method](maths/numerical_analysis/nevilles_method.py) + * [Newton Forward Interpolation](maths/numerical_analysis/newton_forward_interpolation.py) + * [Newton Raphson](maths/numerical_analysis/newton_raphson.py) + * [Numerical Integration](maths/numerical_analysis/numerical_integration.py) + * [Proper Fractions](maths/numerical_analysis/proper_fractions.py) + * [Runge Kutta](maths/numerical_analysis/runge_kutta.py) + * [Runge Kutta Fehlberg 45](maths/numerical_analysis/runge_kutta_fehlberg_45.py) + * [Runge Kutta Gills](maths/numerical_analysis/runge_kutta_gills.py) + * [Secant Method](maths/numerical_analysis/secant_method.py) + * [Simpson Rule](maths/numerical_analysis/simpson_rule.py) + * [Square Root](maths/numerical_analysis/square_root.py) + * [Odd Sieve](maths/odd_sieve.py) * [Perfect Cube](maths/perfect_cube.py) * [Perfect Number](maths/perfect_number.py) * [Perfect Square](maths/perfect_square.py) * [Persistence](maths/persistence.py) + * [Pi Generator](maths/pi_generator.py) * [Pi Monte Carlo Estimation](maths/pi_monte_carlo_estimation.py) * [Points Are Collinear 3D](maths/points_are_collinear_3d.py) * [Pollard Rho](maths/pollard_rho.py) @@ -609,15 +734,12 @@ * [Prime Sieve Eratosthenes](maths/prime_sieve_eratosthenes.py) * [Primelib](maths/primelib.py) * [Print Multiplication Table](maths/print_multiplication_table.py) - * [Pronic Number](maths/pronic_number.py) - * [Proth Number](maths/proth_number.py) * [Pythagoras](maths/pythagoras.py) * [Qr Decomposition](maths/qr_decomposition.py) * [Quadratic Equations Complex Numbers](maths/quadratic_equations_complex_numbers.py) * [Radians](maths/radians.py) * [Radix2 Fft](maths/radix2_fft.py) - * [Relu](maths/relu.py) - * [Runge Kutta](maths/runge_kutta.py) + * [Remove Digit](maths/remove_digit.py) * [Segmented Sieve](maths/segmented_sieve.py) * Series * [Arithmetic](maths/series/arithmetic.py) @@ -629,40 +751,63 @@ * [P Series](maths/series/p_series.py) * [Sieve Of Eratosthenes](maths/sieve_of_eratosthenes.py) * [Sigmoid](maths/sigmoid.py) - * [Sigmoid Linear Unit](maths/sigmoid_linear_unit.py) * [Signum](maths/signum.py) - * [Simpson Rule](maths/simpson_rule.py) + * [Simultaneous Linear Equation Solver](maths/simultaneous_linear_equation_solver.py) * [Sin](maths/sin.py) * [Sock Merchant](maths/sock_merchant.py) * [Softmax](maths/softmax.py) - * [Square Root](maths/square_root.py) + * [Solovay Strassen Primality Test](maths/solovay_strassen_primality_test.py) + * [Spearman Rank Correlation Coefficient](maths/spearman_rank_correlation_coefficient.py) + * Special Numbers + * [Armstrong Numbers](maths/special_numbers/armstrong_numbers.py) + * [Automorphic Number](maths/special_numbers/automorphic_number.py) + * [Bell Numbers](maths/special_numbers/bell_numbers.py) + * [Carmichael Number](maths/special_numbers/carmichael_number.py) + * [Catalan Number](maths/special_numbers/catalan_number.py) + * [Hamming Numbers](maths/special_numbers/hamming_numbers.py) + * [Happy Number](maths/special_numbers/happy_number.py) + * [Harshad Numbers](maths/special_numbers/harshad_numbers.py) + * [Hexagonal Number](maths/special_numbers/hexagonal_number.py) + * [Krishnamurthy Number](maths/special_numbers/krishnamurthy_number.py) + * [Perfect Number](maths/special_numbers/perfect_number.py) + * [Polygonal Numbers](maths/special_numbers/polygonal_numbers.py) + * [Pronic Number](maths/special_numbers/pronic_number.py) + * [Proth Number](maths/special_numbers/proth_number.py) + * [Triangular Numbers](maths/special_numbers/triangular_numbers.py) + * [Ugly Numbers](maths/special_numbers/ugly_numbers.py) + * [Weird Number](maths/special_numbers/weird_number.py) * [Sum Of Arithmetic Series](maths/sum_of_arithmetic_series.py) * [Sum Of Digits](maths/sum_of_digits.py) * [Sum Of Geometric Progression](maths/sum_of_geometric_progression.py) * [Sum Of Harmonic Series](maths/sum_of_harmonic_series.py) * [Sumset](maths/sumset.py) * [Sylvester Sequence](maths/sylvester_sequence.py) + * [Tanh](maths/tanh.py) * [Test Prime Check](maths/test_prime_check.py) + * [Three Sum](maths/three_sum.py) * [Trapezoidal Rule](maths/trapezoidal_rule.py) * [Triplet Sum](maths/triplet_sum.py) * [Twin Prime](maths/twin_prime.py) * [Two Pointer](maths/two_pointer.py) * [Two Sum](maths/two_sum.py) - * [Ugly Numbers](maths/ugly_numbers.py) * [Volume](maths/volume.py) - * [Weird Number](maths/weird_number.py) * [Zellers Congruence](maths/zellers_congruence.py) ## Matrix * [Binary Search Matrix](matrix/binary_search_matrix.py) * [Count Islands In Matrix](matrix/count_islands_in_matrix.py) + * [Count Negative Numbers In Sorted Matrix](matrix/count_negative_numbers_in_sorted_matrix.py) * [Count Paths](matrix/count_paths.py) * [Cramers Rule 2X2](matrix/cramers_rule_2x2.py) * [Inverse Of Matrix](matrix/inverse_of_matrix.py) * [Largest Square Area In Matrix](matrix/largest_square_area_in_matrix.py) + * [Matrix Based Game](matrix/matrix_based_game.py) * [Matrix Class](matrix/matrix_class.py) + * [Matrix Equalization](matrix/matrix_equalization.py) + * [Matrix Multiplication Recursion](matrix/matrix_multiplication_recursion.py) * [Matrix Operation](matrix/matrix_operation.py) * [Max Area Of Island](matrix/max_area_of_island.py) + * [Median Matrix](matrix/median_matrix.py) * [Nth Fibonacci Using Matrix Exponentiation](matrix/nth_fibonacci_using_matrix_exponentiation.py) * [Pascal Triangle](matrix/pascal_triangle.py) * [Rotate Matrix](matrix/rotate_matrix.py) @@ -671,57 +816,92 @@ * [Spiral Print](matrix/spiral_print.py) * Tests * [Test Matrix Operation](matrix/tests/test_matrix_operation.py) + * [Validate Sudoku Board](matrix/validate_sudoku_board.py) ## Networking Flow * [Ford Fulkerson](networking_flow/ford_fulkerson.py) * [Minimum Cut](networking_flow/minimum_cut.py) ## Neural Network - * [2 Hidden Layers Neural Network](neural_network/2_hidden_layers_neural_network.py) + * Activation Functions + * [Binary Step](neural_network/activation_functions/binary_step.py) + * [Exponential Linear Unit](neural_network/activation_functions/exponential_linear_unit.py) + * [Gaussian Error Linear Unit](neural_network/activation_functions/gaussian_error_linear_unit.py) + * [Leaky Rectified Linear Unit](neural_network/activation_functions/leaky_rectified_linear_unit.py) + * [Mish](neural_network/activation_functions/mish.py) + * [Rectified Linear Unit](neural_network/activation_functions/rectified_linear_unit.py) + * [Scaled Exponential Linear Unit](neural_network/activation_functions/scaled_exponential_linear_unit.py) + * [Soboleva Modified Hyperbolic Tangent](neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py) + * [Softplus](neural_network/activation_functions/softplus.py) + * [Squareplus](neural_network/activation_functions/squareplus.py) + * [Swish](neural_network/activation_functions/swish.py) * [Back Propagation Neural Network](neural_network/back_propagation_neural_network.py) * [Convolution Neural Network](neural_network/convolution_neural_network.py) - * [Perceptron](neural_network/perceptron.py) + * [Input Data](neural_network/input_data.py) * [Simple Neural Network](neural_network/simple_neural_network.py) + * [Two Hidden Layers Neural Network](neural_network/two_hidden_layers_neural_network.py) ## Other * [Activity Selection](other/activity_selection.py) * [Alternative List Arrange](other/alternative_list_arrange.py) - * [Davisb Putnamb Logemannb Loveland](other/davisb_putnamb_logemannb_loveland.py) - * [Dijkstra Bankers Algorithm](other/dijkstra_bankers_algorithm.py) + * [Bankers Algorithm](other/bankers_algorithm.py) + * [Davis Putnam Logemann Loveland](other/davis_putnam_logemann_loveland.py) * [Doomsday](other/doomsday.py) * [Fischer Yates Shuffle](other/fischer_yates_shuffle.py) * [Gauss Easter](other/gauss_easter.py) * [Graham Scan](other/graham_scan.py) * [Greedy](other/greedy.py) + * [Guess The Number Search](other/guess_the_number_search.py) + * [H Index](other/h_index.py) * [Least Recently Used](other/least_recently_used.py) * [Lfu Cache](other/lfu_cache.py) * [Linear Congruential Generator](other/linear_congruential_generator.py) * [Lru Cache](other/lru_cache.py) * [Magicdiamondpattern](other/magicdiamondpattern.py) - * [Maximum Subarray](other/maximum_subarray.py) + * [Majority Vote Algorithm](other/majority_vote_algorithm.py) + * [Maximum Subsequence](other/maximum_subsequence.py) * [Nested Brackets](other/nested_brackets.py) + * [Number Container System](other/number_container_system.py) * [Password](other/password.py) * [Quine](other/quine.py) * [Scoring Algorithm](other/scoring_algorithm.py) * [Sdes](other/sdes.py) * [Tower Of Hanoi](other/tower_of_hanoi.py) + * [Word Search](other/word_search.py) ## Physics - * [Archimedes Principle](physics/archimedes_principle.py) + * [Altitude Pressure](physics/altitude_pressure.py) + * [Archimedes Principle Of Buoyant Force](physics/archimedes_principle_of_buoyant_force.py) + * [Basic Orbital Capture](physics/basic_orbital_capture.py) * [Casimir Effect](physics/casimir_effect.py) + * [Center Of Mass](physics/center_of_mass.py) * [Centripetal Force](physics/centripetal_force.py) + * [Coulombs Law](physics/coulombs_law.py) + * [Doppler Frequency](physics/doppler_frequency.py) + * [Grahams Law](physics/grahams_law.py) * [Horizontal Projectile Motion](physics/horizontal_projectile_motion.py) * [Hubble Parameter](physics/hubble_parameter.py) * [Ideal Gas Law](physics/ideal_gas_law.py) + * [In Static Equilibrium](physics/in_static_equilibrium.py) * [Kinetic Energy](physics/kinetic_energy.py) + * [Lens Formulae](physics/lens_formulae.py) * [Lorentz Transformation Four Vector](physics/lorentz_transformation_four_vector.py) * [Malus Law](physics/malus_law.py) + * [Mass Energy Equivalence](physics/mass_energy_equivalence.py) + * [Mirror Formulae](physics/mirror_formulae.py) * [N Body Simulation](physics/n_body_simulation.py) * [Newtons Law Of Gravitation](physics/newtons_law_of_gravitation.py) * [Newtons Second Law Of Motion](physics/newtons_second_law_of_motion.py) + * [Period Of Pendulum](physics/period_of_pendulum.py) + * [Photoelectric Effect](physics/photoelectric_effect.py) * [Potential Energy](physics/potential_energy.py) + * [Rainfall Intensity](physics/rainfall_intensity.py) + * [Reynolds Number](physics/reynolds_number.py) * [Rms Speed Of Molecule](physics/rms_speed_of_molecule.py) * [Shear Stress](physics/shear_stress.py) + * [Speed Of Sound](physics/speed_of_sound.py) + * [Speeds Of Gas Molecules](physics/speeds_of_gas_molecules.py) + * [Terminal Velocity](physics/terminal_velocity.py) ## Project Euler * Problem 001 @@ -914,10 +1094,14 @@ * [Sol1](project_euler/problem_077/sol1.py) * Problem 078 * [Sol1](project_euler/problem_078/sol1.py) + * Problem 079 + * [Sol1](project_euler/problem_079/sol1.py) * Problem 080 * [Sol1](project_euler/problem_080/sol1.py) * Problem 081 * [Sol1](project_euler/problem_081/sol1.py) + * Problem 082 + * [Sol1](project_euler/problem_082/sol1.py) * Problem 085 * [Sol1](project_euler/problem_085/sol1.py) * Problem 086 @@ -930,10 +1114,14 @@ * [Sol1](project_euler/problem_091/sol1.py) * Problem 092 * [Sol1](project_euler/problem_092/sol1.py) + * Problem 094 + * [Sol1](project_euler/problem_094/sol1.py) * Problem 097 * [Sol1](project_euler/problem_097/sol1.py) * Problem 099 * [Sol1](project_euler/problem_099/sol1.py) + * Problem 100 + * [Sol1](project_euler/problem_100/sol1.py) * Problem 101 * [Sol1](project_euler/problem_101/sol1.py) * Problem 102 @@ -954,6 +1142,8 @@ * [Sol1](project_euler/problem_115/sol1.py) * Problem 116 * [Sol1](project_euler/problem_116/sol1.py) + * Problem 117 + * [Sol1](project_euler/problem_117/sol1.py) * Problem 119 * [Sol1](project_euler/problem_119/sol1.py) * Problem 120 @@ -966,6 +1156,8 @@ * [Sol1](project_euler/problem_125/sol1.py) * Problem 129 * [Sol1](project_euler/problem_129/sol1.py) + * Problem 131 + * [Sol1](project_euler/problem_131/sol1.py) * Problem 135 * [Sol1](project_euler/problem_135/sol1.py) * Problem 144 @@ -978,6 +1170,8 @@ * [Sol1](project_euler/problem_174/sol1.py) * Problem 180 * [Sol1](project_euler/problem_180/sol1.py) + * Problem 187 + * [Sol1](project_euler/problem_187/sol1.py) * Problem 188 * [Sol1](project_euler/problem_188/sol1.py) * Problem 191 @@ -1002,23 +1196,16 @@ * [Sol1](project_euler/problem_587/sol1.py) * Problem 686 * [Sol1](project_euler/problem_686/sol1.py) + * Problem 800 + * [Sol1](project_euler/problem_800/sol1.py) ## Quantum - * [Bb84](quantum/bb84.py) - * [Deutsch Jozsa](quantum/deutsch_jozsa.py) - * [Half Adder](quantum/half_adder.py) - * [Not Gate](quantum/not_gate.py) * [Q Fourier Transform](quantum/q_fourier_transform.py) - * [Q Full Adder](quantum/q_full_adder.py) - * [Quantum Entanglement](quantum/quantum_entanglement.py) - * [Quantum Teleportation](quantum/quantum_teleportation.py) - * [Ripple Adder Classic](quantum/ripple_adder_classic.py) - * [Single Qubit Measure](quantum/single_qubit_measure.py) - * [Superdense Coding](quantum/superdense_coding.py) ## Scheduling * [First Come First Served](scheduling/first_come_first_served.py) * [Highest Response Ratio Next](scheduling/highest_response_ratio_next.py) + * [Job Sequence With Deadline](scheduling/job_sequence_with_deadline.py) * [Job Sequencing With Deadline](scheduling/job_sequencing_with_deadline.py) * [Multi Level Feedback Queue](scheduling/multi_level_feedback_queue.py) * [Non Preemptive Shortest Job First](scheduling/non_preemptive_shortest_job_first.py) @@ -1030,11 +1217,13 @@ * [Binary Tree Traversal](searches/binary_tree_traversal.py) * [Double Linear Search](searches/double_linear_search.py) * [Double Linear Search Recursion](searches/double_linear_search_recursion.py) + * [Exponential Search](searches/exponential_search.py) * [Fibonacci Search](searches/fibonacci_search.py) * [Hill Climbing](searches/hill_climbing.py) * [Interpolation Search](searches/interpolation_search.py) * [Jump Search](searches/jump_search.py) * [Linear Search](searches/linear_search.py) + * [Median Of Medians](searches/median_of_medians.py) * [Quick Select](searches/quick_select.py) * [Sentinel Linear Search](searches/sentinel_linear_search.py) * [Simple Binary Search](searches/simple_binary_search.py) @@ -1044,6 +1233,7 @@ ## Sorts * [Bead Sort](sorts/bead_sort.py) + * [Binary Insertion Sort](sorts/binary_insertion_sort.py) * [Bitonic Sort](sorts/bitonic_sort.py) * [Bogo Sort](sorts/bogo_sort.py) * [Bubble Sort](sorts/bubble_sort.py) @@ -1076,9 +1266,6 @@ * [Quick Sort](sorts/quick_sort.py) * [Quick Sort 3 Partition](sorts/quick_sort_3_partition.py) * [Radix Sort](sorts/radix_sort.py) - * [Random Normal Distribution Quicksort](sorts/random_normal_distribution_quicksort.py) - * [Random Pivot Quick Sort](sorts/random_pivot_quick_sort.py) - * [Recursive Bubble Sort](sorts/recursive_bubble_sort.py) * [Recursive Insertion Sort](sorts/recursive_insertion_sort.py) * [Recursive Mergesort Array](sorts/recursive_mergesort_array.py) * [Recursive Quick Sort](sorts/recursive_quick_sort.py) @@ -1100,22 +1287,28 @@ * [Anagrams](strings/anagrams.py) * [Autocomplete Using Trie](strings/autocomplete_using_trie.py) * [Barcode Validator](strings/barcode_validator.py) + * [Bitap String Match](strings/bitap_string_match.py) * [Boyer Moore Search](strings/boyer_moore_search.py) + * [Camel Case To Snake Case](strings/camel_case_to_snake_case.py) * [Can String Be Rearranged As Palindrome](strings/can_string_be_rearranged_as_palindrome.py) * [Capitalize](strings/capitalize.py) * [Check Anagrams](strings/check_anagrams.py) + * [Count Vowels](strings/count_vowels.py) * [Credit Card Validator](strings/credit_card_validator.py) + * [Damerau Levenshtein Distance](strings/damerau_levenshtein_distance.py) * [Detecting English Programmatically](strings/detecting_english_programmatically.py) * [Dna](strings/dna.py) + * [Edit Distance](strings/edit_distance.py) * [Frequency Finder](strings/frequency_finder.py) * [Hamming Distance](strings/hamming_distance.py) * [Indian Phone Validator](strings/indian_phone_validator.py) * [Is Contains Unique Chars](strings/is_contains_unique_chars.py) * [Is Isogram](strings/is_isogram.py) - * [Is Palindrome](strings/is_palindrome.py) * [Is Pangram](strings/is_pangram.py) + * [Is Polish National Id](strings/is_polish_national_id.py) * [Is Spain National Id](strings/is_spain_national_id.py) * [Is Srilankan Phone Number](strings/is_srilankan_phone_number.py) + * [Is Valid Email Address](strings/is_valid_email_address.py) * [Jaro Winkler](strings/jaro_winkler.py) * [Join](strings/join.py) * [Knuth Morris Pratt](strings/knuth_morris_pratt.py) @@ -1126,17 +1319,21 @@ * [Naive String Search](strings/naive_string_search.py) * [Ngram](strings/ngram.py) * [Palindrome](strings/palindrome.py) + * [Pig Latin](strings/pig_latin.py) * [Prefix Function](strings/prefix_function.py) * [Rabin Karp](strings/rabin_karp.py) * [Remove Duplicate](strings/remove_duplicate.py) * [Reverse Letters](strings/reverse_letters.py) - * [Reverse Long Words](strings/reverse_long_words.py) * [Reverse Words](strings/reverse_words.py) * [Snake Case To Camel Pascal Case](strings/snake_case_to_camel_pascal_case.py) * [Split](strings/split.py) + * [String Switch Case](strings/string_switch_case.py) + * [Strip](strings/strip.py) * [Text Justification](strings/text_justification.py) + * [Title](strings/title.py) + * [Top K Frequent Words](strings/top_k_frequent_words.py) * [Upper](strings/upper.py) - * [Wave](strings/wave.py) + * [Wave String](strings/wave_string.py) * [Wildcard Pattern Matching](strings/wildcard_pattern_matching.py) * [Word Occurrence](strings/word_occurrence.py) * [Word Patterns](strings/word_patterns.py) @@ -1144,7 +1341,6 @@ ## Web Programming * [Co2 Emission](web_programming/co2_emission.py) - * [Convert Number To Words](web_programming/convert_number_to_words.py) * [Covid Stats Via Xpath](web_programming/covid_stats_via_xpath.py) * [Crawl Google Results](web_programming/crawl_google_results.py) * [Crawl Google Scholar Citation](web_programming/crawl_google_scholar_citation.py) @@ -1162,10 +1358,9 @@ * [Fetch Well Rx Price](web_programming/fetch_well_rx_price.py) * [Get Amazon Product Data](web_programming/get_amazon_product_data.py) * [Get Imdb Top 250 Movies Csv](web_programming/get_imdb_top_250_movies_csv.py) - * [Get Imdbtop](web_programming/get_imdbtop.py) + * [Get Ip Geolocation](web_programming/get_ip_geolocation.py) * [Get Top Billionaires](web_programming/get_top_billionaires.py) * [Get Top Hn Posts](web_programming/get_top_hn_posts.py) - * [Get User Tweets](web_programming/get_user_tweets.py) * [Giphy](web_programming/giphy.py) * [Instagram Crawler](web_programming/instagram_crawler.py) * [Instagram Pic](web_programming/instagram_pic.py) diff --git a/LICENSE.md b/LICENSE.md index 2897d02e2a01..de631c3ef333 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -MIT License +## MIT License Copyright (c) 2016-2022 TheAlgorithms and contributors diff --git a/README.md b/README.md index 68a6e5e6fbce..d8eba4e016fa 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Contributions Welcome - + Discord chat - + Gitter chat @@ -42,7 +42,7 @@ Read through our [Contribution Guidelines](CONTRIBUTING.md) before you contribut ## Community Channels -We are on [Discord](https://discord.gg/c7MnfGFGa6) and [Gitter](https://gitter.im/TheAlgorithms)! Community channels are a great way for you to ask questions and get help. Please join us! +We are on [Discord](https://the-algorithms.com/discord) and [Gitter](https://gitter.im/TheAlgorithms/community)! Community channels are a great way for you to ask questions and get help. Please join us! ## List of Algorithms diff --git a/arithmetic_analysis/README.md b/arithmetic_analysis/README.md deleted file mode 100644 index 45cf321eb6ad..000000000000 --- a/arithmetic_analysis/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Arithmetic analysis - -Arithmetic analysis is a branch of mathematics that deals with solving linear equations. - -* -* -* diff --git a/arithmetic_analysis/lu_decomposition.py b/arithmetic_analysis/lu_decomposition.py deleted file mode 100644 index 217719cf4da1..000000000000 --- a/arithmetic_analysis/lu_decomposition.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Lower-Upper (LU) Decomposition. - -Reference: -- https://en.wikipedia.org/wiki/LU_decomposition -""" -from __future__ import annotations - -import numpy as np -from numpy import float64 -from numpy.typing import ArrayLike - - -def lower_upper_decomposition( - table: ArrayLike[float64], -) -> tuple[ArrayLike[float64], ArrayLike[float64]]: - """Lower-Upper (LU) Decomposition - - Example: - - >>> matrix = np.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]]) - >>> outcome = lower_upper_decomposition(matrix) - >>> outcome[0] - array([[1. , 0. , 0. ], - [0. , 1. , 0. ], - [2.5, 8. , 1. ]]) - >>> outcome[1] - array([[ 2. , -2. , 1. ], - [ 0. , 1. , 2. ], - [ 0. , 0. , -17.5]]) - - >>> matrix = np.array([[2, -2, 1], [0, 1, 2]]) - >>> lower_upper_decomposition(matrix) - Traceback (most recent call last): - ... - ValueError: 'table' has to be of square shaped array but got a 2x3 array: - [[ 2 -2 1] - [ 0 1 2]] - """ - # Table that contains our data - # Table has to be a square array so we need to check first - rows, columns = np.shape(table) - if rows != columns: - raise ValueError( - f"'table' has to be of square shaped array but got a {rows}x{columns} " - + f"array:\n{table}" - ) - lower = np.zeros((rows, columns)) - upper = np.zeros((rows, columns)) - for i in range(columns): - for j in range(i): - total = 0 - for k in range(j): - total += lower[i][k] * upper[k][j] - lower[i][j] = (table[i][j] - total) / upper[j][j] - lower[i][i] = 1 - for j in range(i, columns): - total = 0 - for k in range(i): - total += lower[i][k] * upper[k][j] - upper[i][j] = table[i][j] - total - return lower, upper - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/arithmetic_analysis/newton_method.py b/arithmetic_analysis/newton_method.py deleted file mode 100644 index 5127bfcafd9a..000000000000 --- a/arithmetic_analysis/newton_method.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Newton's Method.""" - -# Newton's Method - https://en.wikipedia.org/wiki/Newton%27s_method -from collections.abc import Callable - -RealFunc = Callable[[float], float] # type alias for a real -> real function - - -# function is the f(x) and derivative is the f'(x) -def newton( - function: RealFunc, - derivative: RealFunc, - starting_int: int, -) -> float: - """ - >>> newton(lambda x: x ** 3 - 2 * x - 5, lambda x: 3 * x ** 2 - 2, 3) - 2.0945514815423474 - >>> newton(lambda x: x ** 3 - 1, lambda x: 3 * x ** 2, -2) - 1.0 - >>> newton(lambda x: x ** 3 - 1, lambda x: 3 * x ** 2, -4) - 1.0000000000000102 - >>> import math - >>> newton(math.sin, math.cos, 1) - 0.0 - >>> newton(math.sin, math.cos, 2) - 3.141592653589793 - >>> newton(math.cos, lambda x: -math.sin(x), 2) - 1.5707963267948966 - >>> newton(math.cos, lambda x: -math.sin(x), 0) - Traceback (most recent call last): - ... - ZeroDivisionError: Could not find root - """ - prev_guess = float(starting_int) - while True: - try: - next_guess = prev_guess - function(prev_guess) / derivative(prev_guess) - except ZeroDivisionError: - raise ZeroDivisionError("Could not find root") from None - if abs(prev_guess - next_guess) < 10**-5: - return next_guess - prev_guess = next_guess - - -def f(x: float) -> float: - return (x**3) - (2 * x) - 5 - - -def f1(x: float) -> float: - return 3 * (x**2) - 2 - - -if __name__ == "__main__": - print(newton(f, f1, 3)) diff --git a/arithmetic_analysis/newton_raphson.py b/arithmetic_analysis/newton_raphson.py deleted file mode 100644 index 86ff9d350dde..000000000000 --- a/arithmetic_analysis/newton_raphson.py +++ /dev/null @@ -1,44 +0,0 @@ -# Implementing Newton Raphson method in Python -# Author: Syed Haseeb Shah (github.com/QuantumNovice) -# The Newton-Raphson method (also known as Newton's method) is a way to -# quickly find a good approximation for the root of a real-valued function -from __future__ import annotations - -from decimal import Decimal -from math import * # noqa: F401, F403 - -from sympy import diff - - -def newton_raphson( - func: str, a: float | Decimal, precision: float = 10**-10 -) -> float: - """Finds root from the point 'a' onwards by Newton-Raphson method - >>> newton_raphson("sin(x)", 2) - 3.1415926536808043 - >>> newton_raphson("x**2 - 5*x +2", 0.4) - 0.4384471871911695 - >>> newton_raphson("x**2 - 5", 0.1) - 2.23606797749979 - >>> newton_raphson("log(x)- 1", 2) - 2.718281828458938 - """ - x = a - while True: - x = Decimal(x) - (Decimal(eval(func)) / Decimal(eval(str(diff(func))))) - # This number dictates the accuracy of the answer - if abs(eval(func)) < precision: - return float(x) - - -# Let's Execute -if __name__ == "__main__": - # Find root of trigonometric function - # Find value of pi - print(f"The root of sin(x) = 0 is {newton_raphson('sin(x)', 2)}") - # Find root of polynomial - print(f"The root of x**2 - 5*x + 2 = 0 is {newton_raphson('x**2 - 5*x + 2', 0.4)}") - # Find Square Root of 5 - print(f"The root of log(x) - 1 = 0 is {newton_raphson('log(x) - 1', 2)}") - # Exponential Roots - print(f"The root of exp(x) - 1 = 0 is {newton_raphson('exp(x) - 1', 0)}") diff --git a/arithmetic_analysis/newton_raphson_new.py b/arithmetic_analysis/newton_raphson_new.py deleted file mode 100644 index 472cb5b5ac54..000000000000 --- a/arithmetic_analysis/newton_raphson_new.py +++ /dev/null @@ -1,83 +0,0 @@ -# Implementing Newton Raphson method in Python -# Author: Saksham Gupta -# -# The Newton-Raphson method (also known as Newton's method) is a way to -# quickly find a good approximation for the root of a functreal-valued ion -# The method can also be extended to complex functions -# -# Newton's Method - https://en.wikipedia.org/wiki/Newton's_method - -from sympy import diff, lambdify, symbols -from sympy.functions import * # noqa: F401, F403 - - -def newton_raphson( - function: str, - starting_point: complex, - variable: str = "x", - precision: float = 10**-10, - multiplicity: int = 1, -) -> complex: - """Finds root from the 'starting_point' onwards by Newton-Raphson method - Refer to https://docs.sympy.org/latest/modules/functions/index.html - for usable mathematical functions - - >>> newton_raphson("sin(x)", 2) - 3.141592653589793 - >>> newton_raphson("x**4 -5", 0.4 + 5j) - (-7.52316384526264e-37+1.4953487812212207j) - >>> newton_raphson('log(y) - 1', 2, variable='y') - 2.7182818284590455 - >>> newton_raphson('exp(x) - 1', 10, precision=0.005) - 1.2186556186174883e-10 - >>> newton_raphson('cos(x)', 0) - Traceback (most recent call last): - ... - ZeroDivisionError: Could not find root - """ - - x = symbols(variable) - func = lambdify(x, function) - diff_function = lambdify(x, diff(function, x)) - - prev_guess = starting_point - - while True: - if diff_function(prev_guess) != 0: - next_guess = prev_guess - multiplicity * func(prev_guess) / diff_function( - prev_guess - ) - else: - raise ZeroDivisionError("Could not find root") from None - - # Precision is checked by comparing the difference of consecutive guesses - if abs(next_guess - prev_guess) < precision: - return next_guess - - prev_guess = next_guess - - -# Let's Execute -if __name__ == "__main__": - # Find root of trigonometric function - # Find value of pi - print(f"The root of sin(x) = 0 is {newton_raphson('sin(x)', 2)}") - - # Find root of polynomial - # Find fourth Root of 5 - print(f"The root of x**4 - 5 = 0 is {newton_raphson('x**4 -5', 0.4 +5j)}") - - # Find value of e - print( - "The root of log(y) - 1 = 0 is ", - f"{newton_raphson('log(y) - 1', 2, variable='y')}", - ) - - # Exponential Roots - print( - "The root of exp(x) - 1 = 0 is", - f"{newton_raphson('exp(x) - 1', 10, precision=0.005)}", - ) - - # Find root of cos(x) - print(f"The root of cos(x) = 0 is {newton_raphson('cos(x)', 0)}") diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index cffedb7a68fd..4e6ea1b18fb4 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -11,7 +11,9 @@ def make_lowpass( - frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) # noqa: B008 + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a low-pass filter @@ -39,7 +41,9 @@ def make_lowpass( def make_highpass( - frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) # noqa: B008 + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a high-pass filter @@ -67,7 +71,9 @@ def make_highpass( def make_bandpass( - frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) # noqa: B008 + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a band-pass filter @@ -96,7 +102,9 @@ def make_bandpass( def make_allpass( - frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) # noqa: B008 + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates an all-pass filter @@ -124,7 +132,7 @@ def make_peak( frequency: int, samplerate: int, gain_db: float, - q_factor: float = 1 / sqrt(2), # noqa: B008 + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a peak filter @@ -156,7 +164,7 @@ def make_lowshelf( frequency: int, samplerate: int, gain_db: float, - q_factor: float = 1 / sqrt(2), # noqa: B008 + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a low-shelf filter @@ -193,7 +201,7 @@ def make_highshelf( frequency: int, samplerate: int, gain_db: float, - q_factor: float = 1 / sqrt(2), # noqa: B008 + q_factor: float = 1 / sqrt(2), ) -> IIRFilter: """ Creates a high-shelf filter diff --git a/audio_filters/equal_loudness_filter.py.broken.txt b/audio_filters/equal_loudness_filter.py.broken.txt index b9a3c50e1c33..88cba8533cf7 100644 --- a/audio_filters/equal_loudness_filter.py.broken.txt +++ b/audio_filters/equal_loudness_filter.py.broken.txt @@ -20,7 +20,7 @@ class EqualLoudnessFilter: samplerate, use with caution. Code based on matlab implementation at https://bit.ly/3eqh2HU - (url shortened for flake8) + (url shortened for ruff) Target curve: https://i.imgur.com/3g2VfaM.png Yulewalk response: https://i.imgur.com/J9LnJ4C.png diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index aae320365012..fa3e6c54b33f 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -10,13 +10,17 @@ class IIRFilter: Implementation details: Based on the 2nd-order function from - https://en.wikipedia.org/wiki/Digital_biquad_filter, + https://en.wikipedia.org/wiki/Digital_biquad_filter, this generalized N-order function was made. Using the following transfer function - H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}{a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}} + .. math:: H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}} + {a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}} + we can rewrite this to - y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) + .. math:: y[n]={\frac{1}{a_{0}}} + \left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)- + \left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) """ def __init__(self, order: int) -> None: @@ -34,39 +38,43 @@ def __init__(self, order: int) -> None: def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None: """ - Set the coefficients for the IIR filter. These should both be of size order + 1. - a_0 may be left out, and it will use 1.0 as default value. + Set the coefficients for the IIR filter. + These should both be of size `order` + 1. + :math:`a_0` may be left out, and it will use 1.0 as default value. This method works well with scipy's filter design functions - >>> # Make a 2nd-order 1000Hz butterworth lowpass filter - >>> import scipy.signal - >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, - ... btype='lowpass', - ... fs=48000) - >>> filt = IIRFilter(2) - >>> filt.set_coefficients(a_coeffs, b_coeffs) + + >>> # Make a 2nd-order 1000Hz butterworth lowpass filter + >>> import scipy.signal + >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, + ... btype='lowpass', + ... fs=48000) + >>> filt = IIRFilter(2) + >>> filt.set_coefficients(a_coeffs, b_coeffs) """ if len(a_coeffs) < self.order: - a_coeffs = [1.0] + a_coeffs + a_coeffs = [1.0, *a_coeffs] if len(a_coeffs) != self.order + 1: - raise ValueError( - f"Expected a_coeffs to have {self.order + 1} elements for {self.order}" - f"-order filter, got {len(a_coeffs)}" + msg = ( + f"Expected a_coeffs to have {self.order + 1} elements " + f"for {self.order}-order filter, got {len(a_coeffs)}" ) + raise ValueError(msg) if len(b_coeffs) != self.order + 1: - raise ValueError( - f"Expected b_coeffs to have {self.order + 1} elements for {self.order}" - f"-order filter, got {len(a_coeffs)}" + msg = ( + f"Expected b_coeffs to have {self.order + 1} elements " + f"for {self.order}-order filter, got {len(a_coeffs)}" ) + raise ValueError(msg) self.a_coeffs = a_coeffs self.b_coeffs = b_coeffs def process(self, sample: float) -> float: """ - Calculate y[n] + Calculate :math:`y[n]` >>> filt = IIRFilter(2) >>> filt.process(0) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 097b8152b4e6..f9c9537c047c 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import abstractmethod from math import pi from typing import Protocol @@ -8,6 +9,7 @@ class FilterType(Protocol): + @abstractmethod def process(self, sample: float) -> float: """ Calculate y[n] @@ -15,7 +17,6 @@ def process(self, sample: float) -> float: >>> issubclass(FilterType, Protocol) True """ - return 0.0 def get_bounds( diff --git a/backtracking/all_combinations.py b/backtracking/all_combinations.py index bde60f0328ba..1d15c6263e14 100644 --- a/backtracking/all_combinations.py +++ b/backtracking/all_combinations.py @@ -1,16 +1,60 @@ """ - In this problem, we want to determine all possible combinations of k - numbers out of 1 ... n. We use backtracking to solve this problem. - Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))) +In this problem, we want to determine all possible combinations of k +numbers out of 1 ... n. We use backtracking to solve this problem. + +Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))), """ + from __future__ import annotations +from itertools import combinations + + +def combination_lists(n: int, k: int) -> list[list[int]]: + """ + Generates all possible combinations of k numbers out of 1 ... n using itertools. + + >>> combination_lists(n=4, k=2) + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + """ + return [list(x) for x in combinations(range(1, n + 1), k)] + def generate_all_combinations(n: int, k: int) -> list[list[int]]: """ + Generates all possible combinations of k numbers out of 1 ... n using backtracking. + >>> generate_all_combinations(n=4, k=2) [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + >>> generate_all_combinations(n=0, k=0) + [[]] + >>> generate_all_combinations(n=10, k=-1) + Traceback (most recent call last): + ... + ValueError: k must not be negative + >>> generate_all_combinations(n=-1, k=10) + Traceback (most recent call last): + ... + ValueError: n must not be negative + >>> generate_all_combinations(n=5, k=4) + [[1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 4, 5], [1, 3, 4, 5], [2, 3, 4, 5]] + >>> generate_all_combinations(n=3, k=3) + [[1, 2, 3]] + >>> generate_all_combinations(n=3, k=1) + [[1], [2], [3]] + >>> generate_all_combinations(n=1, k=0) + [[]] + >>> generate_all_combinations(n=1, k=1) + [[1]] + >>> from itertools import combinations + >>> all(generate_all_combinations(n, k) == combination_lists(n, k) + ... for n in range(1, 6) for k in range(1, 6)) + True """ + if k < 0: + raise ValueError("k must not be negative") + if n < 0: + raise ValueError("n must not be negative") result: list[list[int]] = [] create_all_state(1, n, k, [], result) @@ -24,6 +68,28 @@ def create_all_state( current_list: list[int], total_list: list[list[int]], ) -> None: + """ + Helper function to recursively build all combinations. + + >>> create_all_state(1, 4, 2, [], result := []) + >>> result + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + >>> create_all_state(1, 3, 3, [], result := []) + >>> result + [[1, 2, 3]] + >>> create_all_state(2, 2, 1, [1], result := []) + >>> result + [[1, 2]] + >>> create_all_state(1, 0, 0, [], result := []) + >>> result + [[]] + >>> create_all_state(1, 4, 0, [1, 2], result := []) + >>> result + [[1, 2]] + >>> create_all_state(5, 4, 2, [1, 2], result := []) + >>> result + [] + """ if level == 0: total_list.append(current_list[:]) return @@ -34,13 +100,17 @@ def create_all_state( current_list.pop() -def print_all_state(total_list: list[list[int]]) -> None: - for i in total_list: - print(*i) +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(generate_all_combinations(n=4, k=2)) + tests = ((n, k) for n in range(1, 5) for k in range(1, 5)) + for n, k in tests: + print(n, k, generate_all_combinations(n, k) == combination_lists(n, k)) + print("Benchmark:") + from timeit import timeit -if __name__ == "__main__": - n = 4 - k = 2 - total_list = generate_all_combinations(n, k) - print_all_state(total_list) + for func in ("combination_lists", "generate_all_combinations"): + print(f"{func:>25}(): {timeit(f'{func}(n=4, k = 2)', globals=globals())}") diff --git a/backtracking/all_permutations.py b/backtracking/all_permutations.py index ff8a53e0dd0e..f376e6fa0945 100644 --- a/backtracking/all_permutations.py +++ b/backtracking/all_permutations.py @@ -1,10 +1,11 @@ """ - In this problem, we want to determine all possible permutations - of the given sequence. We use backtracking to solve this problem. +In this problem, we want to determine all possible permutations +of the given sequence. We use backtracking to solve this problem. - Time complexity: O(n! * n), - where n denotes the length of the given sequence. +Time complexity: O(n! * n), +where n denotes the length of the given sequence. """ + from __future__ import annotations @@ -22,6 +23,42 @@ def create_state_space_tree( Creates a state space tree to iterate through each branch using DFS. We know that each state has exactly len(sequence) - index children. It terminates when it reaches the end of the given sequence. + + :param sequence: The input sequence for which permutations are generated. + :param current_sequence: The current permutation being built. + :param index: The current index in the sequence. + :param index_used: list to track which elements are used in permutation. + + Example 1: + >>> sequence = [1, 2, 3] + >>> current_sequence = [] + >>> index_used = [False, False, False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + [1, 2, 3] + [1, 3, 2] + [2, 1, 3] + [2, 3, 1] + [3, 1, 2] + [3, 2, 1] + + Example 2: + >>> sequence = ["A", "B", "C"] + >>> current_sequence = [] + >>> index_used = [False, False, False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + ['A', 'B', 'C'] + ['A', 'C', 'B'] + ['B', 'A', 'C'] + ['B', 'C', 'A'] + ['C', 'A', 'B'] + ['C', 'B', 'A'] + + Example 3: + >>> sequence = [1] + >>> current_sequence = [] + >>> index_used = [False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + [1] """ if index == len(sequence): diff --git a/backtracking/all_subsequences.py b/backtracking/all_subsequences.py index c465fc542407..18696054eb7e 100644 --- a/backtracking/all_subsequences.py +++ b/backtracking/all_subsequences.py @@ -5,6 +5,7 @@ Time complexity: O(2^n), where n denotes the length of the given sequence. """ + from __future__ import annotations from typing import Any @@ -21,6 +22,56 @@ def create_state_space_tree( Creates a state space tree to iterate through each branch using DFS. We know that each state has exactly two children. It terminates when it reaches the end of the given sequence. + + :param sequence: The input sequence for which subsequences are generated. + :param current_subsequence: The current subsequence being built. + :param index: The current index in the sequence. + + Example: + >>> sequence = [3, 2, 1] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + [1] + [2] + [2, 1] + [3] + [3, 1] + [3, 2] + [3, 2, 1] + + >>> sequence = ["A", "B"] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + ['B'] + ['A'] + ['A', 'B'] + + >>> sequence = [] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + + >>> sequence = [1, 2, 3, 4] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + [4] + [3] + [3, 4] + [2] + [2, 4] + [2, 3] + [2, 3, 4] + [1] + [1, 4] + [1, 3] + [1, 3, 4] + [1, 2] + [1, 2, 4] + [1, 2, 3] + [1, 2, 3, 4] """ if index == len(sequence): @@ -34,7 +85,7 @@ def create_state_space_tree( if __name__ == "__main__": - seq: list[Any] = [3, 1, 2, 4] + seq: list[Any] = [1, 2, 3] generate_all_subsequences(seq) seq.clear() diff --git a/backtracking/coloring.py b/backtracking/coloring.py index 9d539de8a3c4..f10cdbcf9d26 100644 --- a/backtracking/coloring.py +++ b/backtracking/coloring.py @@ -1,9 +1,9 @@ """ - Graph Coloring also called "m coloring problem" - consists of coloring a given graph with at most m colors - such that no adjacent vertices are assigned the same color +Graph Coloring also called "m coloring problem" +consists of coloring a given graph with at most m colors +such that no adjacent vertices are assigned the same color - Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring +Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring """ diff --git a/backtracking/combination_sum.py b/backtracking/combination_sum.py index f555adb751d0..3c6ed81f44f0 100644 --- a/backtracking/combination_sum.py +++ b/backtracking/combination_sum.py @@ -47,7 +47,7 @@ def combination_sum(candidates: list, target: int) -> list: >>> combination_sum([-8, 2.3, 0], 1) Traceback (most recent call last): ... - RecursionError: maximum recursion depth exceeded in comparison + RecursionError: maximum recursion depth exceeded """ path = [] # type: list[int] answer = [] # type: list[int] diff --git a/backtracking/crossword_puzzle_solver.py b/backtracking/crossword_puzzle_solver.py new file mode 100644 index 000000000000..e702c7e52153 --- /dev/null +++ b/backtracking/crossword_puzzle_solver.py @@ -0,0 +1,131 @@ +# https://www.geeksforgeeks.org/solve-crossword-puzzle/ + + +def is_valid( + puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool +) -> bool: + """ + Check if a word can be placed at the given position. + + >>> puzzle = [ + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''] + ... ] + >>> is_valid(puzzle, 'word', 0, 0, True) + True + >>> puzzle = [ + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''] + ... ] + >>> is_valid(puzzle, 'word', 0, 0, False) + True + """ + for i in range(len(word)): + if vertical: + if row + i >= len(puzzle) or puzzle[row + i][col] != "": + return False + elif col + i >= len(puzzle[0]) or puzzle[row][col + i] != "": + return False + return True + + +def place_word( + puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool +) -> None: + """ + Place a word at the given position. + + >>> puzzle = [ + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''] + ... ] + >>> place_word(puzzle, 'word', 0, 0, True) + >>> puzzle + [['w', '', '', ''], ['o', '', '', ''], ['r', '', '', ''], ['d', '', '', '']] + """ + for i, char in enumerate(word): + if vertical: + puzzle[row + i][col] = char + else: + puzzle[row][col + i] = char + + +def remove_word( + puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool +) -> None: + """ + Remove a word from the given position. + + >>> puzzle = [ + ... ['w', '', '', ''], + ... ['o', '', '', ''], + ... ['r', '', '', ''], + ... ['d', '', '', ''] + ... ] + >>> remove_word(puzzle, 'word', 0, 0, True) + >>> puzzle + [['', '', '', ''], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']] + """ + for i in range(len(word)): + if vertical: + puzzle[row + i][col] = "" + else: + puzzle[row][col + i] = "" + + +def solve_crossword(puzzle: list[list[str]], words: list[str]) -> bool: + """ + Solve the crossword puzzle using backtracking. + + >>> puzzle = [ + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''] + ... ] + + >>> words = ['word', 'four', 'more', 'last'] + >>> solve_crossword(puzzle, words) + True + >>> puzzle = [ + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''], + ... ['', '', '', ''] + ... ] + >>> words = ['word', 'four', 'more', 'paragraphs'] + >>> solve_crossword(puzzle, words) + False + """ + for row in range(len(puzzle)): + for col in range(len(puzzle[0])): + if puzzle[row][col] == "": + for word in words: + for vertical in [True, False]: + if is_valid(puzzle, word, row, col, vertical): + place_word(puzzle, word, row, col, vertical) + words.remove(word) + if solve_crossword(puzzle, words): + return True + words.append(word) + remove_word(puzzle, word, row, col, vertical) + return False + return True + + +if __name__ == "__main__": + PUZZLE = [[""] * 3 for _ in range(3)] + WORDS = ["cat", "dog", "car"] + + if solve_crossword(PUZZLE, WORDS): + print("Solution found:") + for row in PUZZLE: + print(" ".join(row)) + else: + print("No solution found:") diff --git a/backtracking/generate_parentheses.py b/backtracking/generate_parentheses.py new file mode 100644 index 000000000000..18c21e2a9b51 --- /dev/null +++ b/backtracking/generate_parentheses.py @@ -0,0 +1,77 @@ +""" +author: Aayush Soni +Given n pairs of parentheses, write a function to generate all +combinations of well-formed parentheses. +Input: n = 2 +Output: ["(())","()()"] +Leetcode link: https://leetcode.com/problems/generate-parentheses/description/ +""" + + +def backtrack( + partial: str, open_count: int, close_count: int, n: int, result: list[str] +) -> None: + """ + Generate valid combinations of balanced parentheses using recursion. + + :param partial: A string representing the current combination. + :param open_count: An integer representing the count of open parentheses. + :param close_count: An integer representing the count of close parentheses. + :param n: An integer representing the total number of pairs. + :param result: A list to store valid combinations. + :return: None + + This function uses recursion to explore all possible combinations, + ensuring that at each step, the parentheses remain balanced. + + Example: + >>> result = [] + >>> backtrack("", 0, 0, 2, result) + >>> result + ['(())', '()()'] + """ + if len(partial) == 2 * n: + # When the combination is complete, add it to the result. + result.append(partial) + return + + if open_count < n: + # If we can add an open parenthesis, do so, and recurse. + backtrack(partial + "(", open_count + 1, close_count, n, result) + + if close_count < open_count: + # If we can add a close parenthesis (it won't make the combination invalid), + # do so, and recurse. + backtrack(partial + ")", open_count, close_count + 1, n, result) + + +def generate_parenthesis(n: int) -> list[str]: + """ + Generate valid combinations of balanced parentheses for a given n. + + :param n: An integer representing the number of pairs of parentheses. + :return: A list of strings with valid combinations. + + This function uses a recursive approach to generate the combinations. + + Time Complexity: O(2^(2n)) - In the worst case, we have 2^(2n) combinations. + Space Complexity: O(n) - where 'n' is the number of pairs. + + Example 1: + >>> generate_parenthesis(3) + ['((()))', '(()())', '(())()', '()(())', '()()()'] + + Example 2: + >>> generate_parenthesis(1) + ['()'] + """ + + result: list[str] = [] + backtrack("", 0, 0, n, result) + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 4a4156d70b32..f6e4212e47f4 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -1,10 +1,10 @@ """ - A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle - through a graph that visits each node exactly once. - Determining whether such paths and cycles exist in graphs - is the 'Hamiltonian path problem', which is NP-complete. +A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle +through a graph that visits each node exactly once. +Determining whether such paths and cycles exist in graphs +is the 'Hamiltonian path problem', which is NP-complete. - Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path +Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path """ @@ -95,7 +95,7 @@ def util_hamilton_cycle(graph: list[list[int]], path: list[int], curr_ind: int) return graph[path[curr_ind - 1]][path[0]] == 1 # Recursive Step - for next_ver in range(0, len(graph)): + for next_ver in range(len(graph)): if valid_connection(graph, next_ver, curr_ind, path): # Insert current vertex into path as next transition path[curr_ind] = next_ver diff --git a/backtracking/knight_tour.py b/backtracking/knight_tour.py index bb650ece3f5e..8906aaa1094c 100644 --- a/backtracking/knight_tour.py +++ b/backtracking/knight_tour.py @@ -24,10 +24,10 @@ def get_valid_pos(position: tuple[int, int], n: int) -> list[tuple[int, int]]: ] permissible_positions = [] - for position in positions: - y_test, x_test = position + for inner_position in positions: + y_test, x_test = inner_position if 0 <= y_test < n and 0 <= x_test < n: - permissible_positions.append(position) + permissible_positions.append(inner_position) return permissible_positions @@ -79,7 +79,7 @@ def open_knight_tour(n: int) -> list[list[int]]: >>> open_knight_tour(2) Traceback (most recent call last): ... - ValueError: Open Kight Tour cannot be performed on a board of size 2 + ValueError: Open Knight Tour cannot be performed on a board of size 2 """ board = [[0 for i in range(n)] for j in range(n)] @@ -91,7 +91,8 @@ def open_knight_tour(n: int) -> list[list[int]]: return board board[i][j] = 0 - raise ValueError(f"Open Kight Tour cannot be performed on a board of size {n}") + msg = f"Open Knight Tour cannot be performed on a board of size {n}" + raise ValueError(msg) if __name__ == "__main__": diff --git a/backtracking/match_word_pattern.py b/backtracking/match_word_pattern.py new file mode 100644 index 000000000000..bfa9b1354d51 --- /dev/null +++ b/backtracking/match_word_pattern.py @@ -0,0 +1,61 @@ +def match_word_pattern(pattern: str, input_string: str) -> bool: + """ + Determine if a given pattern matches a string using backtracking. + + pattern: The pattern to match. + input_string: The string to match against the pattern. + return: True if the pattern matches the string, False otherwise. + + >>> match_word_pattern("aba", "GraphTreesGraph") + True + + >>> match_word_pattern("xyx", "PythonRubyPython") + True + + >>> match_word_pattern("GG", "PythonJavaPython") + False + """ + + def backtrack(pattern_index: int, str_index: int) -> bool: + """ + >>> backtrack(0, 0) + True + + >>> backtrack(0, 1) + True + + >>> backtrack(0, 4) + False + """ + if pattern_index == len(pattern) and str_index == len(input_string): + return True + if pattern_index == len(pattern) or str_index == len(input_string): + return False + char = pattern[pattern_index] + if char in pattern_map: + mapped_str = pattern_map[char] + if input_string.startswith(mapped_str, str_index): + return backtrack(pattern_index + 1, str_index + len(mapped_str)) + else: + return False + for end in range(str_index + 1, len(input_string) + 1): + substr = input_string[str_index:end] + if substr in str_map: + continue + pattern_map[char] = substr + str_map[substr] = char + if backtrack(pattern_index + 1, end): + return True + del pattern_map[char] + del str_map[substr] + return False + + pattern_map: dict[str, str] = {} + str_map: dict[str, str] = {} + return backtrack(0, 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/minimax.py b/backtracking/minimax.py index 6e310131e069..4eef90b75483 100644 --- a/backtracking/minimax.py +++ b/backtracking/minimax.py @@ -7,6 +7,7 @@ leaves of game tree is stored in scores[] height is maximum height of Game tree """ + from __future__ import annotations import math @@ -16,6 +17,22 @@ def minimax( depth: int, node_index: int, is_max: bool, scores: list[int], height: float ) -> int: """ + This function implements the minimax algorithm, which helps achieve the optimal + score for a player in a two-player game by checking all possible moves. + If the player is the maximizer, then the score is maximized. + If the player is the minimizer, then the score is minimized. + + Parameters: + - depth: Current depth in the game tree. + - node_index: Index of the current node in the scores list. + - is_max: A boolean indicating whether the current move + is for the maximizer (True) or minimizer (False). + - scores: A list containing the scores of the leaves of the game tree. + - height: The maximum height of the game tree. + + Returns: + - An integer representing the optimal score for the current player. + >>> import math >>> scores = [90, 23, 6, 33, 21, 65, 123, 34423] >>> height = math.log(len(scores), 2) @@ -37,19 +54,24 @@ def minimax( if depth < 0: raise ValueError("Depth cannot be less than 0") - if len(scores) == 0: raise ValueError("Scores cannot be empty") + # Base case: If the current depth equals the height of the tree, + # return the score of the current node. if depth == height: return scores[node_index] + # If it's the maximizer's turn, choose the maximum score + # between the two possible moves. if is_max: return max( minimax(depth + 1, node_index * 2, False, scores, height), minimax(depth + 1, node_index * 2 + 1, False, scores, height), ) + # If it's the minimizer's turn, choose the minimum score + # between the two possible moves. return min( minimax(depth + 1, node_index * 2, True, scores, height), minimax(depth + 1, node_index * 2 + 1, True, scores, height), @@ -57,8 +79,11 @@ def minimax( def main() -> None: + # Sample scores and height calculation scores = [90, 23, 6, 33, 21, 65, 123, 34423] height = math.log(len(scores), 2) + + # Calculate and print the optimal value using the minimax algorithm print("Optimal value : ", end="") print(minimax(0, 0, True, scores, height)) diff --git a/backtracking/minmax.py b/backtracking/minmax.py deleted file mode 100644 index 9b87183cfdb7..000000000000 --- a/backtracking/minmax.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Minimax helps to achieve maximum score in a game by checking all possible moves. - -""" -from __future__ import annotations - -import math - - -def minimax( - depth: int, node_index: int, is_max: bool, scores: list[int], height: float -) -> int: - """ - depth is current depth in game tree. - node_index is index of current node in scores[]. - scores[] contains the leaves of game tree. - height is maximum height of game tree. - - >>> scores = [90, 23, 6, 33, 21, 65, 123, 34423] - >>> height = math.log(len(scores), 2) - >>> minimax(0, 0, True, scores, height) - 65 - >>> minimax(-1, 0, True, scores, height) - Traceback (most recent call last): - ... - ValueError: Depth cannot be less than 0 - >>> minimax(0, 0, True, [], 2) - Traceback (most recent call last): - ... - ValueError: Scores cannot be empty - >>> scores = [3, 5, 2, 9, 12, 5, 23, 23] - >>> height = math.log(len(scores), 2) - >>> minimax(0, 0, True, scores, height) - 12 - """ - - if depth < 0: - raise ValueError("Depth cannot be less than 0") - - if not scores: - raise ValueError("Scores cannot be empty") - - if depth == height: - return scores[node_index] - - return ( - max( - minimax(depth + 1, node_index * 2, False, scores, height), - minimax(depth + 1, node_index * 2 + 1, False, scores, height), - ) - if is_max - else min( - minimax(depth + 1, node_index * 2, True, scores, height), - minimax(depth + 1, node_index * 2 + 1, True, scores, height), - ) - ) - - -def main() -> None: - scores = [90, 23, 6, 33, 21, 65, 123, 34423] - height = math.log(len(scores), 2) - print(f"Optimal value : {minimax(0, 0, True, scores, height)}") - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - main() diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py index bbf0ce44f91c..81668b17a0ac 100644 --- a/backtracking/n_queens.py +++ b/backtracking/n_queens.py @@ -1,12 +1,13 @@ """ - The nqueens problem is of placing N queens on a N * N - chess board such that no queen can attack any other queens placed - on that chess board. - This means that one queen cannot have any other queen on its horizontal, vertical and - diagonal lines. +The nqueens problem is of placing N queens on a N * N +chess board such that no queen can attack any other queens placed +on that chess board. +This means that one queen cannot have any other queen on its horizontal, vertical and +diagonal lines. """ + from __future__ import annotations solution = [] @@ -17,40 +18,43 @@ def is_safe(board: list[list[int]], row: int, column: int) -> bool: This function returns a boolean value True if it is safe to place a queen there considering the current state of the board. - Parameters : - board(2D matrix) : board - row ,column : coordinates of the cell on a board + Parameters: + board (2D matrix): The chessboard + row, column: Coordinates of the cell on the board - Returns : + Returns: Boolean Value + >>> is_safe([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + True + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + False """ - for i in range(len(board)): - if board[row][i] == 1: - return False - for i in range(len(board)): - if board[i][column] == 1: - return False - for i, j in zip(range(row, -1, -1), range(column, -1, -1)): - if board[i][j] == 1: - return False - for i, j in zip(range(row, -1, -1), range(column, len(board))): - if board[i][j] == 1: - return False - return True + + n = len(board) # Size of the board + + # Check if there is any queen in the same row, column, + # left upper diagonal, and right upper diagonal + return ( + all(board[i][j] != 1 for i, j in zip(range(row, -1, -1), range(column, n))) + and all( + board[i][j] != 1 for i, j in zip(range(row, -1, -1), range(column, -1, -1)) + ) + and all(board[i][j] != 1 for i, j in zip(range(row, n), range(column, n))) + and all(board[i][j] != 1 for i, j in zip(range(row, n), range(column, -1, -1))) + ) def solve(board: list[list[int]], row: int) -> bool: """ - It creates a state space tree and calls the safe function until it receives a - False Boolean and terminates that branch and backtracks to the next + This function creates a state space tree and calls the safe function until it + receives a False Boolean and terminates that branch and backtracks to the next possible solution branch. """ if row >= len(board): """ - If the row number exceeds N we have board with a successful combination + If the row number exceeds N, we have a board with a successful combination and that combination is appended to the solution list and the board is printed. - """ solution.append(board) printboard(board) @@ -58,9 +62,9 @@ def solve(board: list[list[int]], row: int) -> bool: return True for i in range(len(board)): """ - For every row it iterates through each column to check if it is feasible to + For every row, it iterates through each column to check if it is feasible to place a queen there. - If all the combinations for that particular branch are successful the board is + If all the combinations for that particular branch are successful, the board is reinitialized for the next possible combination. """ if is_safe(board, row, i): @@ -77,14 +81,14 @@ def printboard(board: list[list[int]]) -> None: for i in range(len(board)): for j in range(len(board)): if board[i][j] == 1: - print("Q", end=" ") + print("Q", end=" ") # Queen is present else: - print(".", end=" ") + print(".", end=" ") # Empty cell print() -# n=int(input("The no. of queens")) +# Number of queens (e.g., n=8 for an 8x8 board) n = 8 board = [[0 for i in range(n)] for j in range(n)] solve(board, 0) -print("The total no. of solutions are :", len(solution)) +print("The total number of solutions are:", len(solution)) diff --git a/backtracking/n_queens_math.py b/backtracking/n_queens_math.py index 23bd1590618b..287d1f090373 100644 --- a/backtracking/n_queens_math.py +++ b/backtracking/n_queens_math.py @@ -75,6 +75,7 @@ for another one or vice versa. """ + from __future__ import annotations @@ -129,9 +130,9 @@ def depth_first_search( # If it is False we call dfs function again and we update the inputs depth_first_search( - possible_board + [col], - diagonal_right_collisions + [row - col], - diagonal_left_collisions + [row + col], + [*possible_board, col], + [*diagonal_right_collisions, row - col], + [*diagonal_left_collisions, row + col], boards, n, ) diff --git a/backtracking/power_sum.py b/backtracking/power_sum.py new file mode 100644 index 000000000000..ee2eac426ec7 --- /dev/null +++ b/backtracking/power_sum.py @@ -0,0 +1,91 @@ +""" +Problem source: https://www.hackerrank.com/challenges/the-power-sum/problem +Find the number of ways that a given integer X, can be expressed as the sum +of the Nth powers of unique, natural numbers. For example, if X=13 and N=2. +We have to find all combinations of unique squares adding up to 13. +The only solution is 2^2+3^2. Constraints: 1<=X<=1000, 2<=N<=10. +""" + + +def backtrack( + needed_sum: int, + power: int, + current_number: int, + current_sum: int, + solutions_count: int, +) -> tuple[int, int]: + """ + >>> backtrack(13, 2, 1, 0, 0) + (0, 1) + >>> backtrack(10, 2, 1, 0, 0) + (0, 1) + >>> backtrack(10, 3, 1, 0, 0) + (0, 0) + >>> backtrack(20, 2, 1, 0, 0) + (0, 1) + >>> backtrack(15, 10, 1, 0, 0) + (0, 0) + >>> backtrack(16, 2, 1, 0, 0) + (0, 1) + >>> backtrack(20, 1, 1, 0, 0) + (0, 64) + """ + if current_sum == needed_sum: + # If the sum of the powers is equal to needed_sum, then we have a solution. + solutions_count += 1 + return current_sum, solutions_count + + i_to_n = current_number**power + if current_sum + i_to_n <= needed_sum: + # If the sum of the powers is less than needed_sum, then continue adding powers. + current_sum += i_to_n + current_sum, solutions_count = backtrack( + needed_sum, power, current_number + 1, current_sum, solutions_count + ) + current_sum -= i_to_n + if i_to_n < needed_sum: + # If the power of i is less than needed_sum, then try with the next power. + current_sum, solutions_count = backtrack( + needed_sum, power, current_number + 1, current_sum, solutions_count + ) + return current_sum, solutions_count + + +def solve(needed_sum: int, power: int) -> int: + """ + >>> solve(13, 2) + 1 + >>> solve(10, 2) + 1 + >>> solve(10, 3) + 0 + >>> solve(20, 2) + 1 + >>> solve(15, 10) + 0 + >>> solve(16, 2) + 1 + >>> solve(20, 1) + Traceback (most recent call last): + ... + ValueError: Invalid input + needed_sum must be between 1 and 1000, power between 2 and 10. + >>> solve(-10, 5) + Traceback (most recent call last): + ... + ValueError: Invalid input + needed_sum must be between 1 and 1000, power between 2 and 10. + """ + if not (1 <= needed_sum <= 1000 and 2 <= power <= 10): + raise ValueError( + "Invalid input\n" + "needed_sum must be between 1 and 1000, power between 2 and 10." + ) + + return backtrack(needed_sum, power, 1, 0, 0)[1] # Return the solutions_count + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/rat_in_maze.py b/backtracking/rat_in_maze.py index 7bde886dd558..626c83cb4a15 100644 --- a/backtracking/rat_in_maze.py +++ b/backtracking/rat_in_maze.py @@ -1,91 +1,164 @@ from __future__ import annotations -def solve_maze(maze: list[list[int]]) -> bool: +def solve_maze( + maze: list[list[int]], + source_row: int, + source_column: int, + destination_row: int, + destination_column: int, +) -> list[list[int]]: """ This method solves the "rat in maze" problem. - In this problem we have some n by n matrix, a start point and an end point. - We want to go from the start to the end. In this matrix zeroes represent walls - and ones paths we can use. Parameters : - maze(2D matrix) : maze + - maze: A two dimensional matrix of zeros and ones. + - source_row: The row index of the starting point. + - source_column: The column index of the starting point. + - destination_row: The row index of the destination point. + - destination_column: The column index of the destination point. Returns: - Return: True if the maze has a solution or False if it does not. + - solution: A 2D matrix representing the solution path if it exists. + Raises: + - ValueError: If no solution exists or if the source or + destination coordinates are invalid. + Description: + This method navigates through a maze represented as an n by n matrix, + starting from a specified source cell and + aiming to reach a destination cell. + The maze consists of walls (1s) and open paths (0s). + By providing custom row and column values, the source and destination + cells can be adjusted. >>> maze = [[0, 1, 0, 1, 1], ... [0, 0, 0, 0, 0], ... [1, 0, 1, 0, 1], ... [0, 0, 1, 0, 0], ... [1, 0, 0, 1, 0]] - >>> solve_maze(maze) - [1, 0, 0, 0, 0] - [1, 1, 1, 1, 0] - [0, 0, 0, 1, 0] - [0, 0, 0, 1, 1] - [0, 0, 0, 0, 1] - True + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 1, 1, 1, 1], + [0, 0, 0, 0, 1], + [1, 1, 1, 0, 1], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 0]] + + Note: + In the output maze, the zeros (0s) represent one of the possible + paths from the source to the destination. >>> maze = [[0, 1, 0, 1, 1], ... [0, 0, 0, 0, 0], ... [0, 0, 0, 0, 1], ... [0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0]] - >>> solve_maze(maze) - [1, 0, 0, 0, 0] - [1, 0, 0, 0, 0] - [1, 0, 0, 0, 0] - [1, 0, 0, 0, 0] - [1, 1, 1, 1, 1] - True + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 0, 0, 0, 0]] >>> maze = [[0, 0, 0], ... [0, 1, 0], ... [1, 0, 0]] - >>> solve_maze(maze) - [1, 1, 1] - [0, 0, 1] - [0, 0, 1] - True + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 0, 0], + [1, 1, 0], + [1, 1, 0]] - >>> maze = [[0, 1, 0], + >>> maze = [[1, 0, 0], ... [0, 1, 0], ... [1, 0, 0]] - >>> solve_maze(maze) - No solution exists! - False + >>> solve_maze(maze,0,1,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[1, 0, 0], + [1, 1, 0], + [1, 1, 0]] + + >>> maze = [[1, 1, 0, 0, 1, 0, 0, 1], + ... [1, 0, 1, 0, 0, 1, 1, 1], + ... [0, 1, 0, 1, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0, 1, 0, 1], + ... [0, 1, 0, 0, 1, 0, 1, 1], + ... [0, 0, 0, 1, 1, 1, 0, 1], + ... [0, 1, 0, 1, 0, 1, 1, 1], + ... [1, 1, 0, 0, 0, 0, 0, 1]] + >>> solve_maze(maze,0,2,len(maze)-1,2) # doctest: +NORMALIZE_WHITESPACE + [[1, 1, 0, 0, 1, 1, 1, 1], + [1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1]] + >>> maze = [[1, 0, 0], + ... [0, 1, 1], + ... [1, 0, 1]] + >>> solve_maze(maze,0,1,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: No solution exists! + + >>> maze = [[0, 0], + ... [1, 1]] + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: No solution exists! >>> maze = [[0, 1], ... [1, 0]] - >>> solve_maze(maze) - No solution exists! - False + >>> solve_maze(maze,2,0,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: Invalid source or destination coordinates + + >>> maze = [[1, 0, 0], + ... [0, 1, 0], + ... [1, 0, 0]] + >>> solve_maze(maze,0,1,len(maze),len(maze)-1) + Traceback (most recent call last): + ... + ValueError: Invalid source or destination coordinates """ size = len(maze) + # Check if source and destination coordinates are Invalid. + if not (0 <= source_row <= size - 1 and 0 <= source_column <= size - 1) or ( + not (0 <= destination_row <= size - 1 and 0 <= destination_column <= size - 1) + ): + raise ValueError("Invalid source or destination coordinates") # We need to create solution object to save path. - solutions = [[0 for _ in range(size)] for _ in range(size)] - solved = run_maze(maze, 0, 0, solutions) + solutions = [[1 for _ in range(size)] for _ in range(size)] + solved = run_maze( + maze, source_row, source_column, destination_row, destination_column, solutions + ) if solved: - print("\n".join(str(row) for row in solutions)) + return solutions else: - print("No solution exists!") - return solved + raise ValueError("No solution exists!") -def run_maze(maze: list[list[int]], i: int, j: int, solutions: list[list[int]]) -> bool: +def run_maze( + maze: list[list[int]], + i: int, + j: int, + destination_row: int, + destination_column: int, + solutions: list[list[int]], +) -> bool: """ This method is recursive starting from (i, j) and going in one of four directions: up, down, left, right. If a path is found to destination it returns True otherwise it returns False. - Parameters: - maze(2D matrix) : maze + Parameters + maze: A two dimensional matrix of zeros and ones. i, j : coordinates of matrix - solutions(2D matrix) : solutions + solutions: A two dimensional matrix of solutions. Returns: Boolean if path is found True, Otherwise False. """ size = len(maze) # Final check point. - if i == j == (size - 1): - solutions[i][j] = 1 + if i == destination_row and j == destination_column and maze[i][j] == 0: + solutions[i][j] = 0 return True lower_flag = (not i < 0) and (not j < 0) # Check lower bounds @@ -93,21 +166,27 @@ def run_maze(maze: list[list[int]], i: int, j: int, solutions: list[list[int]]) if lower_flag and upper_flag: # check for already visited and block points. - block_flag = (not solutions[i][j]) and (not maze[i][j]) + block_flag = (solutions[i][j]) and (not maze[i][j]) if block_flag: # check visited - solutions[i][j] = 1 + solutions[i][j] = 0 # check for directions if ( - run_maze(maze, i + 1, j, solutions) - or run_maze(maze, i, j + 1, solutions) - or run_maze(maze, i - 1, j, solutions) - or run_maze(maze, i, j - 1, solutions) + run_maze(maze, i + 1, j, destination_row, destination_column, solutions) + or run_maze( + maze, i, j + 1, destination_row, destination_column, solutions + ) + or run_maze( + maze, i - 1, j, destination_row, destination_column, solutions + ) + or run_maze( + maze, i, j - 1, destination_row, destination_column, solutions + ) ): return True - solutions[i][j] = 0 + solutions[i][j] = 1 return False return False @@ -115,4 +194,4 @@ def run_maze(maze: list[list[int]], i: int, j: int, solutions: list[list[int]]) if __name__ == "__main__": import doctest - doctest.testmod() + doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE) diff --git a/backtracking/sudoku.py b/backtracking/sudoku.py index 698dedcc2125..cabeebb90433 100644 --- a/backtracking/sudoku.py +++ b/backtracking/sudoku.py @@ -1,7 +1,7 @@ """ -Given a partially filled 9×9 2D array, the objective is to fill a 9×9 +Given a partially filled 9x9 2D array, the objective is to fill a 9x9 square grid with digits numbered 1 to 9, so that every row, column, and -and each of the nine 3×3 sub-grids contains all of the digits. +and each of the nine 3x3 sub-grids contains all of the digits. This can be solved using Backtracking and is similar to n-queens. We check to see if a cell is safe or not and recursively call the @@ -9,6 +9,7 @@ have solved the puzzle. else, we backtrack and place another number in that cell and repeat this process. """ + from __future__ import annotations Matrix = list[list[int]] @@ -48,7 +49,7 @@ def is_safe(grid: Matrix, row: int, column: int, n: int) -> bool: is found) else returns True if it is 'safe' """ for i in range(9): - if grid[row][i] == n or grid[i][column] == n: + if n in {grid[row][i], grid[i][column]}: return False for i in range(3): diff --git a/backtracking/sum_of_subsets.py b/backtracking/sum_of_subsets.py index 128e290718cd..f34d3ca34339 100644 --- a/backtracking/sum_of_subsets.py +++ b/backtracking/sum_of_subsets.py @@ -1,11 +1,12 @@ """ - The sum-of-subsetsproblem states that a set of non-negative integers, and a - value M, determine all possible subsets of the given set whose summation sum - equal to given M. +The sum-of-subsetsproblem states that a set of non-negative integers, and a +value M, determine all possible subsets of the given set whose summation sum +equal to given M. - Summation of the chosen numbers must be equal to given number M and one number - can be used only once. +Summation of the chosen numbers must be equal to given number M and one number +can be used only once. """ + from __future__ import annotations @@ -44,7 +45,7 @@ def create_state_space_tree( nums, max_sum, index + 1, - path + [nums[index]], + [*path, nums[index]], result, remaining_nums_sum - nums[index], ) diff --git a/backtracking/word_break.py b/backtracking/word_break.py new file mode 100644 index 000000000000..1f2ab073f499 --- /dev/null +++ b/backtracking/word_break.py @@ -0,0 +1,71 @@ +""" +Word Break Problem is a well-known problem in computer science. +Given a string and a dictionary of words, the task is to determine if +the string can be segmented into a sequence of one or more dictionary words. + +Wikipedia: https://en.wikipedia.org/wiki/Word_break_problem +""" + + +def backtrack(input_string: str, word_dict: set[str], start: int) -> bool: + """ + Helper function that uses backtracking to determine if a valid + word segmentation is possible starting from index 'start'. + + Parameters: + input_string (str): The input string to be segmented. + word_dict (set[str]): A set of valid dictionary words. + start (int): The starting index of the substring to be checked. + + Returns: + bool: True if a valid segmentation is possible, otherwise False. + + Example: + >>> backtrack("leetcode", {"leet", "code"}, 0) + True + + >>> backtrack("applepenapple", {"apple", "pen"}, 0) + True + + >>> backtrack("catsandog", {"cats", "dog", "sand", "and", "cat"}, 0) + False + """ + + # Base case: if the starting index has reached the end of the string + if start == len(input_string): + return True + + # Try every possible substring from 'start' to 'end' + for end in range(start + 1, len(input_string) + 1): + if input_string[start:end] in word_dict and backtrack( + input_string, word_dict, end + ): + return True + + return False + + +def word_break(input_string: str, word_dict: set[str]) -> bool: + """ + Determines if the input string can be segmented into a sequence of + valid dictionary words using backtracking. + + Parameters: + input_string (str): The input string to segment. + word_dict (set[str]): The set of valid words. + + Returns: + bool: True if the string can be segmented into valid words, otherwise False. + + Example: + >>> word_break("leetcode", {"leet", "code"}) + True + + >>> word_break("applepenapple", {"apple", "pen"}) + True + + >>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"}) + False + """ + + return backtrack(input_string, word_dict, 0) diff --git a/backtracking/word_ladder.py b/backtracking/word_ladder.py new file mode 100644 index 000000000000..7d9fd00f6669 --- /dev/null +++ b/backtracking/word_ladder.py @@ -0,0 +1,100 @@ +""" +Word Ladder is a classic problem in computer science. +The problem is to transform a start word into an end word +by changing one letter at a time. +Each intermediate word must be a valid word from a given list of words. +The goal is to find a transformation sequence +from the start word to the end word. + +Wikipedia: https://en.wikipedia.org/wiki/Word_ladder +""" + +import string + + +def backtrack( + current_word: str, path: list[str], end_word: str, word_set: set[str] +) -> list[str]: + """ + Helper function to perform backtracking to find the transformation + from the current_word to the end_word. + + Parameters: + current_word (str): The current word in the transformation sequence. + path (list[str]): The list of transformations from begin_word to current_word. + end_word (str): The target word for transformation. + word_set (set[str]): The set of valid words for transformation. + + Returns: + list[str]: The list of transformations from begin_word to end_word. + Returns an empty list if there is no valid + transformation from current_word to end_word. + + Example: + >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log", "cog"}) + ['hit', 'hot', 'dot', 'lot', 'log', 'cog'] + + >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log"}) + [] + + >>> backtrack("lead", ["lead"], "gold", {"load", "goad", "gold", "lead", "lord"}) + ['lead', 'lead', 'load', 'goad', 'gold'] + + >>> backtrack("game", ["game"], "code", {"came", "cage", "code", "cade", "gave"}) + ['game', 'came', 'cade', 'code'] + """ + + # Base case: If the current word is the end word, return the path + if current_word == end_word: + return path + + # Try all possible single-letter transformations + for i in range(len(current_word)): + for c in string.ascii_lowercase: # Try changing each letter + transformed_word = current_word[:i] + c + current_word[i + 1 :] + if transformed_word in word_set: + word_set.remove(transformed_word) + # Recur with the new word added to the path + result = backtrack( + transformed_word, [*path, transformed_word], end_word, word_set + ) + if result: # valid transformation found + return result + word_set.add(transformed_word) # backtrack + + return [] # No valid transformation found + + +def word_ladder(begin_word: str, end_word: str, word_set: set[str]) -> list[str]: + """ + Solve the Word Ladder problem using Backtracking and return + the list of transformations from begin_word to end_word. + + Parameters: + begin_word (str): The word from which the transformation starts. + end_word (str): The target word for transformation. + word_list (list[str]): The list of valid words for transformation. + + Returns: + list[str]: The list of transformations from begin_word to end_word. + Returns an empty list if there is no valid transformation. + + Example: + >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]) + ['hit', 'hot', 'dot', 'lot', 'log', 'cog'] + + >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log"]) + [] + + >>> word_ladder("lead", "gold", ["load", "goad", "gold", "lead", "lord"]) + ['lead', 'lead', 'load', 'goad', 'gold'] + + >>> word_ladder("game", "code", ["came", "cage", "code", "cade", "gave"]) + ['game', 'came', 'cade', 'code'] + """ + + if end_word not in word_set: # no valid transformation possible + return [] + + # Perform backtracking starting from the begin_word + return backtrack(begin_word, [begin_word], end_word, word_set) diff --git a/backtracking/word_search.py b/backtracking/word_search.py index 25d1436be36e..8a9b2f1b5359 100644 --- a/backtracking/word_search.py +++ b/backtracking/word_search.py @@ -33,6 +33,61 @@ """ +def get_point_key(len_board: int, len_board_column: int, row: int, column: int) -> int: + """ + Returns the hash key of matrix indexes. + + >>> get_point_key(10, 20, 1, 0) + 200 + """ + + return len_board * len_board_column * row + column + + +def exits_word( + board: list[list[str]], + word: str, + row: int, + column: int, + word_index: int, + visited_points_set: set[int], +) -> bool: + """ + Return True if it's possible to search the word suffix + starting from the word_index. + + >>> exits_word([["A"]], "B", 0, 0, 0, set()) + False + """ + + if board[row][column] != word[word_index]: + return False + + if word_index == len(word) - 1: + return True + + traverts_directions = [(0, 1), (0, -1), (-1, 0), (1, 0)] + len_board = len(board) + len_board_column = len(board[0]) + for direction in traverts_directions: + next_i = row + direction[0] + next_j = column + direction[1] + if not (0 <= next_i < len_board and 0 <= next_j < len_board_column): + continue + + key = get_point_key(len_board, len_board_column, next_i, next_j) + if key in visited_points_set: + continue + + visited_points_set.add(key) + if exits_word(board, word, next_i, next_j, word_index + 1, visited_points_set): + return True + + visited_points_set.remove(key) + + return False + + def word_exists(board: list[list[str]], word: str) -> bool: """ >>> word_exists([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], "ABCCED") @@ -43,13 +98,7 @@ def word_exists(board: list[list[str]], word: str) -> bool: False >>> word_exists([["A"]], "A") True - >>> word_exists([["A","A","A","A","A","A"], - ... ["A","A","A","A","A","A"], - ... ["A","A","A","A","A","A"], - ... ["A","A","A","A","A","A"], - ... ["A","A","A","A","A","B"], - ... ["A","A","A","A","B","A"]], - ... "AAAAAAAAAAAAABB") + >>> word_exists([["B", "A", "A"], ["A", "A", "A"], ["A", "B", "A"]], "ABB") False >>> word_exists([["A"]], 123) Traceback (most recent call last): @@ -77,6 +126,8 @@ def word_exists(board: list[list[str]], word: str) -> bool: board_error_message = ( "The board should be a non empty matrix of single chars strings." ) + + len_board = len(board) if not isinstance(board, list) or len(board) == 0: raise ValueError(board_error_message) @@ -94,61 +145,12 @@ def word_exists(board: list[list[str]], word: str) -> bool: "The word parameter should be a string of length greater than 0." ) - traverts_directions = [(0, 1), (0, -1), (-1, 0), (1, 0)] - len_word = len(word) - len_board = len(board) len_board_column = len(board[0]) - - # Returns the hash key of matrix indexes. - def get_point_key(row: int, column: int) -> int: - """ - >>> len_board=10 - >>> len_board_column=20 - >>> get_point_key(0, 0) - 200 - """ - - return len_board * len_board_column * row + column - - # Return True if it's possible to search the word suffix - # starting from the word_index. - def exits_word( - row: int, column: int, word_index: int, visited_points_set: set[int] - ) -> bool: - """ - >>> board=[["A"]] - >>> word="B" - >>> exits_word(0, 0, 0, set()) - False - """ - - if board[row][column] != word[word_index]: - return False - - if word_index == len_word - 1: - return True - - for direction in traverts_directions: - next_i = row + direction[0] - next_j = column + direction[1] - if not (0 <= next_i < len_board and 0 <= next_j < len_board_column): - continue - - key = get_point_key(next_i, next_j) - if key in visited_points_set: - continue - - visited_points_set.add(key) - if exits_word(next_i, next_j, word_index + 1, visited_points_set): - return True - - visited_points_set.remove(key) - - return False - for i in range(len_board): for j in range(len_board_column): - if exits_word(i, j, 0, {get_point_key(i, j)}): + if exits_word( + board, word, i, j, 0, {get_point_key(len_board, len_board_column, i, j)} + ): return True return False diff --git a/bit_manipulation/binary_and_operator.py b/bit_manipulation/binary_and_operator.py index 36f6c668d9b3..f33b8b1c0ab4 100644 --- a/bit_manipulation/binary_and_operator.py +++ b/bit_manipulation/binary_and_operator.py @@ -26,7 +26,7 @@ def binary_and(a: int, b: int) -> str: >>> binary_and(0, 1.1) Traceback (most recent call last): ... - TypeError: 'float' object cannot be interpreted as an integer + ValueError: Unknown format code 'b' for object of type 'float' >>> binary_and("0", "1") Traceback (most recent call last): ... @@ -35,8 +35,8 @@ def binary_and(a: int, b: int) -> str: if a < 0 or b < 0: raise ValueError("the value of both inputs must be positive") - a_binary = str(bin(a))[2:] # remove the leading "0b" - b_binary = str(bin(b))[2:] # remove the leading "0b" + a_binary = format(a, "b") + b_binary = format(b, "b") max_len = max(len(a_binary), len(b_binary)) diff --git a/bit_manipulation/binary_coded_decimal.py b/bit_manipulation/binary_coded_decimal.py new file mode 100644 index 000000000000..676fd6d54fc5 --- /dev/null +++ b/bit_manipulation/binary_coded_decimal.py @@ -0,0 +1,29 @@ +def binary_coded_decimal(number: int) -> str: + """ + Find binary coded decimal (bcd) of integer base 10. + Each digit of the number is represented by a 4-bit binary. + Example: + >>> binary_coded_decimal(-2) + '0b0000' + >>> binary_coded_decimal(-1) + '0b0000' + >>> binary_coded_decimal(0) + '0b0000' + >>> binary_coded_decimal(3) + '0b0011' + >>> binary_coded_decimal(2) + '0b0010' + >>> binary_coded_decimal(12) + '0b00010010' + >>> binary_coded_decimal(987) + '0b100110000111' + """ + return "0b" + "".join( + str(bin(int(digit)))[2:].zfill(4) for digit in str(max(0, number)) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/bitwise_addition_recursive.py b/bit_manipulation/bitwise_addition_recursive.py new file mode 100644 index 000000000000..70eaf6887b64 --- /dev/null +++ b/bit_manipulation/bitwise_addition_recursive.py @@ -0,0 +1,55 @@ +""" +Calculates the sum of two non-negative integers using bitwise operators +Wikipedia explanation: https://en.wikipedia.org/wiki/Binary_number +""" + + +def bitwise_addition_recursive(number: int, other_number: int) -> int: + """ + >>> bitwise_addition_recursive(4, 5) + 9 + >>> bitwise_addition_recursive(8, 9) + 17 + >>> bitwise_addition_recursive(0, 4) + 4 + >>> bitwise_addition_recursive(4.5, 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive('4', 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive('4.5', 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive(-1, 9) + Traceback (most recent call last): + ... + ValueError: Both arguments MUST be non-negative! + >>> bitwise_addition_recursive(1, -9) + Traceback (most recent call last): + ... + ValueError: Both arguments MUST be non-negative! + """ + + if not isinstance(number, int) or not isinstance(other_number, int): + raise TypeError("Both arguments MUST be integers!") + + if number < 0 or other_number < 0: + raise ValueError("Both arguments MUST be non-negative!") + + bitwise_sum = number ^ other_number + carry = number & other_number + + if carry == 0: + return bitwise_sum + + return bitwise_addition_recursive(bitwise_sum, carry << 1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/count_number_of_one_bits.py b/bit_manipulation/count_number_of_one_bits.py index a1687503a383..f0c9f927620a 100644 --- a/bit_manipulation/count_number_of_one_bits.py +++ b/bit_manipulation/count_number_of_one_bits.py @@ -70,11 +70,13 @@ def do_benchmark(number: int) -> None: setup = "import __main__ as z" print(f"Benchmark when {number = }:") print(f"{get_set_bits_count_using_modulo_operator(number) = }") - timing = timeit("z.get_set_bits_count_using_modulo_operator(25)", setup=setup) + timing = timeit( + f"z.get_set_bits_count_using_modulo_operator({number})", setup=setup + ) print(f"timeit() runs in {timing} seconds") print(f"{get_set_bits_count_using_brian_kernighans_algorithm(number) = }") timing = timeit( - "z.get_set_bits_count_using_brian_kernighans_algorithm(25)", + f"z.get_set_bits_count_using_brian_kernighans_algorithm({number})", setup=setup, ) print(f"timeit() runs in {timing} seconds") diff --git a/bit_manipulation/excess_3_code.py b/bit_manipulation/excess_3_code.py new file mode 100644 index 000000000000..7beaabd90e8a --- /dev/null +++ b/bit_manipulation/excess_3_code.py @@ -0,0 +1,27 @@ +def excess_3_code(number: int) -> str: + """ + Find excess-3 code of integer base 10. + Add 3 to all digits in a decimal number then convert to a binary-coded decimal. + https://en.wikipedia.org/wiki/Excess-3 + + >>> excess_3_code(0) + '0b0011' + >>> excess_3_code(3) + '0b0110' + >>> excess_3_code(2) + '0b0101' + >>> excess_3_code(20) + '0b01010011' + >>> excess_3_code(120) + '0b010001010011' + """ + num = "" + for digit in str(max(0, number)): + num += str(bin(int(digit) + 3))[2:].zfill(4) + return "0b" + num + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/find_previous_power_of_two.py b/bit_manipulation/find_previous_power_of_two.py new file mode 100644 index 000000000000..8ac74ac98478 --- /dev/null +++ b/bit_manipulation/find_previous_power_of_two.py @@ -0,0 +1,30 @@ +def find_previous_power_of_two(number: int) -> int: + """ + Find the largest power of two that is less than or equal to a given integer. + https://stackoverflow.com/questions/1322510 + + >>> [find_previous_power_of_two(i) for i in range(18)] + [0, 1, 2, 2, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 16, 16] + >>> find_previous_power_of_two(-5) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> find_previous_power_of_two(10.5) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + """ + if not isinstance(number, int) or number < 0: + raise ValueError("Input must be a non-negative integer") + if number == 0: + return 0 + power = 1 + while power <= number: + power <<= 1 # Equivalent to multiplying by 2 + return power >> 1 if number > 1 else 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/is_even.py b/bit_manipulation/is_even.py index ba036f35aa1e..6f95a1160797 100644 --- a/bit_manipulation/is_even.py +++ b/bit_manipulation/is_even.py @@ -1,7 +1,7 @@ def is_even(number: int) -> bool: """ return true if the input integer is even - Explanation: Lets take a look at the following deicmal to binary conversions + Explanation: Lets take a look at the following decimal to binary conversions 2 => 10 14 => 1110 100 => 1100100 diff --git a/bit_manipulation/largest_pow_of_two_le_num.py b/bit_manipulation/largest_pow_of_two_le_num.py new file mode 100644 index 000000000000..6ef827312199 --- /dev/null +++ b/bit_manipulation/largest_pow_of_two_le_num.py @@ -0,0 +1,60 @@ +""" +Author : Naman Sharma +Date : October 2, 2023 + +Task: +To Find the largest power of 2 less than or equal to a given number. + +Implementation notes: Use bit manipulation. +We start from 1 & left shift the set bit to check if (res<<1)<=number. +Each left bit shift represents a pow of 2. + +For example: +number: 15 +res: 1 0b1 + 2 0b10 + 4 0b100 + 8 0b1000 + 16 0b10000 (Exit) +""" + + +def largest_pow_of_two_le_num(number: int) -> int: + """ + Return the largest power of two less than or equal to a number. + + >>> largest_pow_of_two_le_num(0) + 0 + >>> largest_pow_of_two_le_num(1) + 1 + >>> largest_pow_of_two_le_num(-1) + 0 + >>> largest_pow_of_two_le_num(3) + 2 + >>> largest_pow_of_two_le_num(15) + 8 + >>> largest_pow_of_two_le_num(99) + 64 + >>> largest_pow_of_two_le_num(178) + 128 + >>> largest_pow_of_two_le_num(999999) + 524288 + >>> largest_pow_of_two_le_num(99.9) + Traceback (most recent call last): + ... + TypeError: Input value must be a 'int' type + """ + if isinstance(number, float): + raise TypeError("Input value must be a 'int' type") + if number <= 0: + return 0 + res = 1 + while (res << 1) <= number: + res <<= 1 + return res + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/missing_number.py b/bit_manipulation/missing_number.py new file mode 100644 index 000000000000..554887b17562 --- /dev/null +++ b/bit_manipulation/missing_number.py @@ -0,0 +1,40 @@ +def find_missing_number(nums: list[int]) -> int: + """ + Finds the missing number in a list of consecutive integers. + + Args: + nums: A list of integers. + + Returns: + The missing number. + + Example: + >>> find_missing_number([0, 1, 3, 4]) + 2 + >>> find_missing_number([4, 3, 1, 0]) + 2 + >>> find_missing_number([-4, -3, -1, 0]) + -2 + >>> find_missing_number([-2, 2, 1, 3, 0]) + -1 + >>> find_missing_number([1, 3, 4, 5, 6]) + 2 + >>> find_missing_number([6, 5, 4, 2, 1]) + 3 + >>> find_missing_number([6, 1, 5, 3, 4]) + 2 + """ + low = min(nums) + high = max(nums) + missing_number = high + + for i in range(low, high): + missing_number ^= i ^ nums[i - low] + + return missing_number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/power_of_4.py b/bit_manipulation/power_of_4.py new file mode 100644 index 000000000000..09e6e28621df --- /dev/null +++ b/bit_manipulation/power_of_4.py @@ -0,0 +1,67 @@ +""" + +Task: +Given a positive int number. Return True if this number is power of 4 +or False otherwise. + +Implementation notes: Use bit manipulation. +For example if the number is the power of 2 it's bits representation: +n = 0..100..00 +n - 1 = 0..011..11 + +n & (n - 1) - no intersections = 0 +If the number is a power of 4 then it should be a power of 2 +and the set bit should be at an odd position. +""" + + +def power_of_4(number: int) -> bool: + """ + Return True if this number is power of 4 or False otherwise. + + >>> power_of_4(0) + Traceback (most recent call last): + ... + ValueError: number must be positive + >>> power_of_4(1) + True + >>> power_of_4(2) + False + >>> power_of_4(4) + True + >>> power_of_4(6) + False + >>> power_of_4(8) + False + >>> power_of_4(17) + False + >>> power_of_4(64) + True + >>> power_of_4(-1) + Traceback (most recent call last): + ... + ValueError: number must be positive + >>> power_of_4(1.2) + Traceback (most recent call last): + ... + TypeError: number must be an integer + + """ + if not isinstance(number, int): + raise TypeError("number must be an integer") + if number <= 0: + raise ValueError("number must be positive") + if number & (number - 1) == 0: + c = 0 + while number: + c += 1 + number >>= 1 + return c % 2 == 1 + else: + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/reverse_bits.py b/bit_manipulation/reverse_bits.py index 55608ae12908..74b4f2563234 100644 --- a/bit_manipulation/reverse_bits.py +++ b/bit_manipulation/reverse_bits.py @@ -14,12 +14,13 @@ def get_reverse_bit_string(number: int) -> str: TypeError: operation can not be conducted on a object of type str """ if not isinstance(number, int): - raise TypeError( + msg = ( "operation can not be conducted on a object of type " f"{type(number).__name__}" ) + raise TypeError(msg) bit_string = "" - for _ in range(0, 32): + for _ in range(32): bit_string += str(number % 2) number = number >> 1 return bit_string diff --git a/bit_manipulation/single_bit_manipulation_operations.py b/bit_manipulation/single_bit_manipulation_operations.py index b43ff07b776f..fcbf033ccb24 100644 --- a/bit_manipulation/single_bit_manipulation_operations.py +++ b/bit_manipulation/single_bit_manipulation_operations.py @@ -8,8 +8,8 @@ def set_bit(number: int, position: int) -> int: Set the bit at position to 1. Details: perform bitwise or for given number and X. - Where X is a number with all the bits – zeroes and bit on given - position – one. + Where X is a number with all the bits - zeroes and bit on given + position - one. >>> set_bit(0b1101, 1) # 0b1111 15 @@ -26,8 +26,8 @@ def clear_bit(number: int, position: int) -> int: Set the bit at position to 0. Details: perform bitwise and for given number and X. - Where X is a number with all the bits – ones and bit on given - position – zero. + Where X is a number with all the bits - ones and bit on given + position - zero. >>> clear_bit(0b10010, 1) # 0b10000 16 @@ -42,8 +42,8 @@ def flip_bit(number: int, position: int) -> int: Flip the bit at position. Details: perform bitwise xor for given number and X. - Where X is a number with all the bits – zeroes and bit on given - position – one. + Where X is a number with all the bits - zeroes and bit on given + position - one. >>> flip_bit(0b101, 1) # 0b111 7 @@ -79,7 +79,7 @@ def get_bit(number: int, position: int) -> int: Get the bit at the given position Details: perform bitwise and for the given number and X, - Where X is a number with all the bits – zeroes and bit on given position – one. + Where X is a number with all the bits - zeroes and bit on given position - one. If the result is not equal to 0, then the bit on the given position is 1, else 0. >>> get_bit(0b1010, 0) diff --git a/bit_manipulation/swap_all_odd_and_even_bits.py b/bit_manipulation/swap_all_odd_and_even_bits.py new file mode 100644 index 000000000000..5ec84417bea6 --- /dev/null +++ b/bit_manipulation/swap_all_odd_and_even_bits.py @@ -0,0 +1,58 @@ +def show_bits(before: int, after: int) -> str: + """ + >>> print(show_bits(0, 0xFFFF)) + 0: 00000000 + 65535: 1111111111111111 + """ + return f"{before:>5}: {before:08b}\n{after:>5}: {after:08b}" + + +def swap_odd_even_bits(num: int) -> int: + """ + 1. We use bitwise AND operations to separate the even bits (0, 2, 4, 6, etc.) and + odd bits (1, 3, 5, 7, etc.) in the input number. + 2. We then right-shift the even bits by 1 position and left-shift the odd bits by + 1 position to swap them. + 3. Finally, we combine the swapped even and odd bits using a bitwise OR operation + to obtain the final result. + >>> print(show_bits(0, swap_odd_even_bits(0))) + 0: 00000000 + 0: 00000000 + >>> print(show_bits(1, swap_odd_even_bits(1))) + 1: 00000001 + 2: 00000010 + >>> print(show_bits(2, swap_odd_even_bits(2))) + 2: 00000010 + 1: 00000001 + >>> print(show_bits(3, swap_odd_even_bits(3))) + 3: 00000011 + 3: 00000011 + >>> print(show_bits(4, swap_odd_even_bits(4))) + 4: 00000100 + 8: 00001000 + >>> print(show_bits(5, swap_odd_even_bits(5))) + 5: 00000101 + 10: 00001010 + >>> print(show_bits(6, swap_odd_even_bits(6))) + 6: 00000110 + 9: 00001001 + >>> print(show_bits(23, swap_odd_even_bits(23))) + 23: 00010111 + 43: 00101011 + """ + # Get all even bits - 0xAAAAAAAA is a 32-bit number with all even bits set to 1 + even_bits = num & 0xAAAAAAAA + + # Get all odd bits - 0x55555555 is a 32-bit number with all odd bits set to 1 + odd_bits = num & 0x55555555 + + # Right shift even bits and left shift odd bits and swap them + return even_bits >> 1 | odd_bits << 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + for i in (-1, 0, 1, 2, 3, 4, 23, 24): + print(show_bits(i, swap_odd_even_bits(i)), "\n") diff --git a/blockchain/diophantine_equation.py b/blockchain/diophantine_equation.py index 22b0cad75c63..ae6a145d2922 100644 --- a/blockchain/diophantine_equation.py +++ b/blockchain/diophantine_equation.py @@ -1,11 +1,13 @@ from __future__ import annotations +from maths.greatest_common_divisor import greatest_common_divisor + def diophantine(a: int, b: int, c: int) -> tuple[float, float]: """ Diophantine Equation : Given integers a,b,c ( at least one of a and b != 0), the diophantine equation a*x + b*y = c has a solution (where x and y are integers) - iff gcd(a,b) divides c. + iff greatest_common_divisor(a,b) divides c. GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) @@ -22,7 +24,7 @@ def diophantine(a: int, b: int, c: int) -> tuple[float, float]: assert ( c % greatest_common_divisor(a, b) == 0 - ) # greatest_common_divisor(a,b) function implemented below + ) # greatest_common_divisor(a,b) is in maths directory (d, x, y) = extended_gcd(a, b) # extended_gcd(a,b) function implemented below r = c / d return (r * x, r * y) @@ -69,32 +71,6 @@ def diophantine_all_soln(a: int, b: int, c: int, n: int = 2) -> None: print(x, y) -def greatest_common_divisor(a: int, b: int) -> int: - """ - Euclid's Lemma : d divides a and b, if and only if d divides a-b and b - - Euclid's Algorithm - - >>> greatest_common_divisor(7,5) - 1 - - Note : In number theory, two integers a and b are said to be relatively prime, - mutually prime, or co-prime if the only positive integer (factor) that - divides both of them is 1 i.e., gcd(a,b) = 1. - - >>> greatest_common_divisor(121, 11) - 11 - - """ - if a < b: - a, b = b, a - - while a % b != 0: - a, b = b, a % b - - return b - - def extended_gcd(a: int, b: int) -> tuple[int, int, int]: """ Extended Euclid's Algorithm : If d divides a and b and d = a*x + b*y for integers @@ -107,7 +83,8 @@ def extended_gcd(a: int, b: int) -> tuple[int, int, int]: (1, -2, 3) """ - assert a >= 0 and b >= 0 + assert a >= 0 + assert b >= 0 if b == 0: d, x, y = a, 1, 0 @@ -116,7 +93,8 @@ def extended_gcd(a: int, b: int) -> tuple[int, int, int]: x = q y = p - q * (a // b) - assert a % d == 0 and b % d == 0 + assert a % d == 0 + assert b % d == 0 assert d == a * x + b * y return (d, x, y) diff --git a/boolean_algebra/and_gate.py b/boolean_algebra/and_gate.py index cbbcfde79f33..6ae66b5b0a77 100644 --- a/boolean_algebra/and_gate.py +++ b/boolean_algebra/and_gate.py @@ -29,20 +29,10 @@ def and_gate(input_1: int, input_2: int) -> int: >>> and_gate(1, 1) 1 """ - return int((input_1, input_2).count(0) == 0) - - -def test_and_gate() -> None: - """ - Tests the and_gate function - """ - assert and_gate(0, 0) == 0 - assert and_gate(0, 1) == 0 - assert and_gate(1, 0) == 0 - assert and_gate(1, 1) == 1 + return int(input_1 and input_2) if __name__ == "__main__": - print(and_gate(0, 0)) - print(and_gate(0, 1)) - print(and_gate(1, 1)) + import doctest + + doctest.testmod() diff --git a/boolean_algebra/imply_gate.py b/boolean_algebra/imply_gate.py new file mode 100644 index 000000000000..b64ebaceb306 --- /dev/null +++ b/boolean_algebra/imply_gate.py @@ -0,0 +1,39 @@ +""" +An IMPLY Gate is a logic gate in boolean algebra which results to 1 if +either input 1 is 0, or if input 1 is 1, then the output is 1 only if input 2 is 1. +It is true if input 1 implies input 2. + +Following is the truth table of an IMPLY Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 1 | + | 0 | 1 | 1 | + | 1 | 0 | 0 | + | 1 | 1 | 1 | + ------------------------------ + +Refer - https://en.wikipedia.org/wiki/IMPLY_gate +""" + + +def imply_gate(input_1: int, input_2: int) -> int: + """ + Calculate IMPLY of the input values + + >>> imply_gate(0, 0) + 1 + >>> imply_gate(0, 1) + 1 + >>> imply_gate(1, 0) + 0 + >>> imply_gate(1, 1) + 1 + """ + return int(input_1 == 0 or input_2 == 1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/karnaugh_map_simplification.py b/boolean_algebra/karnaugh_map_simplification.py new file mode 100644 index 000000000000..c7f2d4c6b897 --- /dev/null +++ b/boolean_algebra/karnaugh_map_simplification.py @@ -0,0 +1,55 @@ +""" +https://en.wikipedia.org/wiki/Karnaugh_map +https://www.allaboutcircuits.com/technical-articles/karnaugh-map-boolean-algebraic-simplification-technique +""" + + +def simplify_kmap(kmap: list[list[int]]) -> str: + """ + Simplify the Karnaugh map. + >>> simplify_kmap(kmap=[[0, 1], [1, 1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 0], [0, 0]]) + '' + >>> simplify_kmap(kmap=[[0, 1], [1, -1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 2]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 1.1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 'a']]) + "A'B + AB' + AB" + """ + simplified_f = [] + for a, row in enumerate(kmap): + for b, item in enumerate(row): + if item: + term = ("A" if a else "A'") + ("B" if b else "B'") + simplified_f.append(term) + return " + ".join(simplified_f) + + +def main() -> None: + """ + Main function to create and simplify a K-Map. + + >>> main() + [0, 1] + [1, 1] + Simplified Expression: + A'B + AB' + AB + """ + kmap = [[0, 1], [1, 1]] + + # Manually generate the product of [0, 1] and [0, 1] + + for row in kmap: + print(row) + + print("Simplified Expression:") + print(simplify_kmap(kmap)) + + +if __name__ == "__main__": + main() + print(f"{simplify_kmap(kmap=[[0, 1], [1, 1]]) = }") diff --git a/boolean_algebra/multiplexer.py b/boolean_algebra/multiplexer.py new file mode 100644 index 000000000000..7e65c785c829 --- /dev/null +++ b/boolean_algebra/multiplexer.py @@ -0,0 +1,42 @@ +def mux(input0: int, input1: int, select: int) -> int: + """ + Implement a 2-to-1 Multiplexer. + + :param input0: The first input value (0 or 1). + :param input1: The second input value (0 or 1). + :param select: The select signal (0 or 1) to choose between input0 and input1. + :return: The output based on the select signal. input1 if select else input0. + + https://www.electrically4u.com/solved-problems-on-multiplexer + https://en.wikipedia.org/wiki/Multiplexer + + >>> mux(0, 1, 0) + 0 + >>> mux(0, 1, 1) + 1 + >>> mux(1, 0, 0) + 1 + >>> mux(1, 0, 1) + 0 + >>> mux(2, 1, 0) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + >>> mux(0, -1, 0) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + >>> mux(0, 1, 1.1) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + """ + if all(i in (0, 1) for i in (input0, input1, select)): + return input1 if select else input0 + raise ValueError("Inputs and select signal must be 0 or 1") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nand_gate.py b/boolean_algebra/nand_gate.py index ea3303d16b25..ea7a6815dcc9 100644 --- a/boolean_algebra/nand_gate.py +++ b/boolean_algebra/nand_gate.py @@ -27,21 +27,10 @@ def nand_gate(input_1: int, input_2: int) -> int: >>> nand_gate(1, 1) 0 """ - return int((input_1, input_2).count(0) != 0) - - -def test_nand_gate() -> None: - """ - Tests the nand_gate function - """ - assert nand_gate(0, 0) == 1 - assert nand_gate(0, 1) == 1 - assert nand_gate(1, 0) == 1 - assert nand_gate(1, 1) == 0 + return int(not (input_1 and input_2)) if __name__ == "__main__": - print(nand_gate(0, 0)) - print(nand_gate(0, 1)) - print(nand_gate(1, 0)) - print(nand_gate(1, 1)) + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nimply_gate.py b/boolean_algebra/nimply_gate.py new file mode 100644 index 000000000000..68e82c8db8d9 --- /dev/null +++ b/boolean_algebra/nimply_gate.py @@ -0,0 +1,39 @@ +""" +An NIMPLY Gate is a logic gate in boolean algebra which results to 0 if +either input 1 is 0, or if input 1 is 1, then it is 0 only if input 2 is 1. +It is false if input 1 implies input 2. It is the negated form of imply + +Following is the truth table of an NIMPLY Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 0 | + | 0 | 1 | 0 | + | 1 | 0 | 1 | + | 1 | 1 | 0 | + ------------------------------ + +Refer - https://en.wikipedia.org/wiki/NIMPLY_gate +""" + + +def nimply_gate(input_1: int, input_2: int) -> int: + """ + Calculate NIMPLY of the input values + + >>> nimply_gate(0, 0) + 0 + >>> nimply_gate(0, 1) + 0 + >>> nimply_gate(1, 0) + 1 + >>> nimply_gate(1, 1) + 0 + """ + return int(input_1 == 1 and input_2 == 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nor_gate.py b/boolean_algebra/nor_gate.py new file mode 100644 index 000000000000..d4d6f0da23ea --- /dev/null +++ b/boolean_algebra/nor_gate.py @@ -0,0 +1,68 @@ +""" +A NOR Gate is a logic gate in boolean algebra which results in false(0) if any of the +inputs is 1, and True(1) if all inputs are 0. +Following is the truth table of a NOR Gate: + Truth Table of NOR Gate: + | Input 1 | Input 2 | Output | + | 0 | 0 | 1 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 0 | + + Code provided by Akshaj Vishwanathan +https://www.geeksforgeeks.org/logic-gates-in-python +""" + +from collections.abc import Callable + + +def nor_gate(input_1: int, input_2: int) -> int: + """ + >>> nor_gate(0, 0) + 1 + >>> nor_gate(0, 1) + 0 + >>> nor_gate(1, 0) + 0 + >>> nor_gate(1, 1) + 0 + >>> nor_gate(0.0, 0.0) + 1 + >>> nor_gate(0, -7) + 0 + """ + return int(input_1 == input_2 == 0) + + +def truth_table(func: Callable) -> str: + """ + >>> print(truth_table(nor_gate)) + Truth Table of NOR Gate: + | Input 1 | Input 2 | Output | + | 0 | 0 | 1 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 0 | + """ + + def make_table_row(items: list | tuple) -> str: + """ + >>> make_table_row(("One", "Two", "Three")) + '| One | Two | Three |' + """ + return f"| {' | '.join(f'{item:^8}' for item in items)} |" + + return "\n".join( + ( + "Truth Table of NOR Gate:", + make_table_row(("Input 1", "Input 2", "Output")), + *[make_table_row((i, j, func(i, j))) for i in (0, 1) for j in (0, 1)], + ) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(truth_table(nor_gate)) diff --git a/boolean_algebra/norgate.py b/boolean_algebra/norgate.py deleted file mode 100644 index 2c27b80afdbe..000000000000 --- a/boolean_algebra/norgate.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -A NOR Gate is a logic gate in boolean algebra which results to false(0) -if any of the input is 1, and True(1) if both the inputs are 0. -Following is the truth table of a NOR Gate: - | Input 1 | Input 2 | Output | - | 0 | 0 | 1 | - | 0 | 1 | 0 | - | 1 | 0 | 0 | - | 1 | 1 | 0 | - -Following is the code implementation of the NOR Gate -""" - - -def nor_gate(input_1: int, input_2: int) -> int: - """ - >>> nor_gate(0, 0) - 1 - >>> nor_gate(0, 1) - 0 - >>> nor_gate(1, 0) - 0 - >>> nor_gate(1, 1) - 0 - >>> nor_gate(0.0, 0.0) - 1 - >>> nor_gate(0, -7) - 0 - """ - return int(input_1 == input_2 == 0) - - -def main() -> None: - print("Truth Table of NOR Gate:") - print("| Input 1 | Input 2 | Output |") - print(f"| 0 | 0 | {nor_gate(0, 0)} |") - print(f"| 0 | 1 | {nor_gate(0, 1)} |") - print(f"| 1 | 0 | {nor_gate(1, 0)} |") - print(f"| 1 | 1 | {nor_gate(1, 1)} |") - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - main() -"""Code provided by Akshaj Vishwanathan""" -"""Reference: https://www.geeksforgeeks.org/logic-gates-in-python/""" diff --git a/boolean_algebra/not_gate.py b/boolean_algebra/not_gate.py index eb85e9e44cd3..cfa74cf42204 100644 --- a/boolean_algebra/not_gate.py +++ b/boolean_algebra/not_gate.py @@ -24,14 +24,7 @@ def not_gate(input_1: int) -> int: return 1 if input_1 == 0 else 0 -def test_not_gate() -> None: - """ - Tests the not_gate function - """ - assert not_gate(0) == 1 - assert not_gate(1) == 0 - - if __name__ == "__main__": - print(not_gate(0)) - print(not_gate(1)) + import doctest + + doctest.testmod() diff --git a/boolean_algebra/or_gate.py b/boolean_algebra/or_gate.py index aa7e6645e33f..0fd4e5a5dc18 100644 --- a/boolean_algebra/or_gate.py +++ b/boolean_algebra/or_gate.py @@ -29,18 +29,7 @@ def or_gate(input_1: int, input_2: int) -> int: return int((input_1, input_2).count(1) != 0) -def test_or_gate() -> None: - """ - Tests the or_gate function - """ - assert or_gate(0, 0) == 0 - assert or_gate(0, 1) == 1 - assert or_gate(1, 0) == 1 - assert or_gate(1, 1) == 1 - - if __name__ == "__main__": - print(or_gate(0, 1)) - print(or_gate(1, 0)) - print(or_gate(0, 0)) - print(or_gate(1, 1)) + import doctest + + doctest.testmod() diff --git a/boolean_algebra/quine_mc_cluskey.py b/boolean_algebra/quine_mc_cluskey.py index 6788dfb28ba1..8e22e66726d4 100644 --- a/boolean_algebra/quine_mc_cluskey.py +++ b/boolean_algebra/quine_mc_cluskey.py @@ -74,10 +74,7 @@ def is_for_table(string1: str, string2: str, count: int) -> bool: """ list1 = list(string1) list2 = list(string2) - count_n = 0 - for i in range(len(list1)): - if list1[i] != list2[i]: - count_n += 1 + count_n = sum(item1 != item2 for item1, item2 in zip(list1, list2)) return count_n == count @@ -92,40 +89,34 @@ def selection(chart: list[list[int]], prime_implicants: list[str]) -> list[str]: temp = [] select = [0] * len(chart) for i in range(len(chart[0])): - count = 0 - rem = -1 - for j in range(len(chart)): - if chart[j][i] == 1: - count += 1 - rem = j + count = sum(row[i] == 1 for row in chart) if count == 1: + rem = max(j for j, row in enumerate(chart) if row[i] == 1) select[rem] = 1 - for i in range(len(select)): - if select[i] == 1: - for j in range(len(chart[0])): - if chart[i][j] == 1: - for k in range(len(chart)): - chart[k][j] = 0 - temp.append(prime_implicants[i]) + for i, item in enumerate(select): + if item != 1: + continue + for j in range(len(chart[0])): + if chart[i][j] != 1: + continue + for row in chart: + row[j] = 0 + temp.append(prime_implicants[i]) while True: - max_n = 0 - rem = -1 - count_n = 0 - for i in range(len(chart)): - count_n = chart[i].count(1) - if count_n > max_n: - max_n = count_n - rem = i + counts = [chart[i].count(1) for i in range(len(chart))] + max_n = max(counts) + rem = counts.index(max_n) if max_n == 0: return temp temp.append(prime_implicants[rem]) - for i in range(len(chart[0])): - if chart[rem][i] == 1: - for j in range(len(chart)): - chart[j][i] = 0 + for j in range(len(chart[0])): + if chart[rem][j] != 1: + continue + for i in range(len(chart)): + chart[i][j] = 0 def prime_implicant_chart( diff --git a/boolean_algebra/xnor_gate.py b/boolean_algebra/xnor_gate.py index 45ab2700ec35..05b756da2960 100644 --- a/boolean_algebra/xnor_gate.py +++ b/boolean_algebra/xnor_gate.py @@ -31,18 +31,7 @@ def xnor_gate(input_1: int, input_2: int) -> int: return 1 if input_1 == input_2 else 0 -def test_xnor_gate() -> None: - """ - Tests the xnor_gate function - """ - assert xnor_gate(0, 0) == 1 - assert xnor_gate(0, 1) == 0 - assert xnor_gate(1, 0) == 0 - assert xnor_gate(1, 1) == 1 - - if __name__ == "__main__": - print(xnor_gate(0, 0)) - print(xnor_gate(0, 1)) - print(xnor_gate(1, 0)) - print(xnor_gate(1, 1)) + import doctest + + doctest.testmod() diff --git a/boolean_algebra/xor_gate.py b/boolean_algebra/xor_gate.py index db4f5b45c3c6..f3922e426e3d 100644 --- a/boolean_algebra/xor_gate.py +++ b/boolean_algebra/xor_gate.py @@ -31,16 +31,7 @@ def xor_gate(input_1: int, input_2: int) -> int: return (input_1, input_2).count(0) % 2 -def test_xor_gate() -> None: - """ - Tests the xor_gate function - """ - assert xor_gate(0, 0) == 0 - assert xor_gate(0, 1) == 1 - assert xor_gate(1, 0) == 1 - assert xor_gate(1, 1) == 0 - - if __name__ == "__main__": - print(xor_gate(0, 0)) - print(xor_gate(0, 1)) + import doctest + + doctest.testmod() diff --git a/cellular_automata/conways_game_of_life.py b/cellular_automata/conways_game_of_life.py index 84f4d5be40da..485f0d47bd8b 100644 --- a/cellular_automata/conways_game_of_life.py +++ b/cellular_automata/conways_game_of_life.py @@ -2,6 +2,7 @@ Conway's Game of Life implemented in Python. https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life """ + from __future__ import annotations from PIL import Image @@ -57,10 +58,8 @@ def new_generation(cells: list[list[int]]) -> list[list[int]]: # 3. All other live cells die in the next generation. # Similarly, all other dead cells stay dead. alive = cells[i][j] == 1 - if ( - (alive and 2 <= neighbour_count <= 3) - or not alive - and neighbour_count == 3 + if (alive and 2 <= neighbour_count <= 3) or ( + not alive and neighbour_count == 3 ): next_generation_row.append(1) else: diff --git a/cellular_automata/game_of_life.py b/cellular_automata/game_of_life.py index 8e54702519b9..76276b272d65 100644 --- a/cellular_automata/game_of_life.py +++ b/cellular_automata/game_of_life.py @@ -10,7 +10,7 @@ - 3.5 Usage: - - $python3 game_o_life + - $python3 game_of_life Game-Of-Life Rules: @@ -26,7 +26,8 @@ 4. Any dead cell with exactly three live neighbours be- comes a live cell, as if by reproduction. - """ +""" + import random import sys @@ -34,7 +35,7 @@ from matplotlib import pyplot as plt from matplotlib.colors import ListedColormap -usage_doc = "Usage of script: script_nama " +usage_doc = "Usage of script: script_name " choice = [0] * 100 + [1] * 10 random.shuffle(choice) @@ -52,7 +53,8 @@ def seed(canvas: list[list[bool]]) -> None: def run(canvas: list[list[bool]]) -> list[list[bool]]: - """This function runs the rules of game through all points, and changes their + """ + This function runs the rules of game through all points, and changes their status accordingly.(in the same canvas) @Args: -- @@ -60,7 +62,7 @@ def run(canvas: list[list[bool]]) -> list[list[bool]]: @returns: -- - None + canvas of population after one step """ current_canvas = np.array(canvas) next_gen_canvas = np.array(create_canvas(current_canvas.shape[0])) @@ -70,10 +72,7 @@ def run(canvas: list[list[bool]]) -> list[list[bool]]: pt, current_canvas[r - 1 : r + 2, c - 1 : c + 2] ) - current_canvas = next_gen_canvas - del next_gen_canvas # cleaning memory as we move on. - return_canvas: list[list[bool]] = current_canvas.tolist() - return return_canvas + return next_gen_canvas.tolist() def __judge_point(pt: bool, neighbours: list[list[bool]]) -> bool: @@ -98,13 +97,12 @@ def __judge_point(pt: bool, neighbours: list[list[bool]]) -> bool: if pt: if alive < 2: state = False - elif alive == 2 or alive == 3: + elif alive in {2, 3}: state = True elif alive > 3: state = False - else: - if alive == 3: - state = True + elif alive == 3: + state = True return state diff --git a/cellular_automata/langtons_ant.py b/cellular_automata/langtons_ant.py new file mode 100644 index 000000000000..9847c50a5c3e --- /dev/null +++ b/cellular_automata/langtons_ant.py @@ -0,0 +1,106 @@ +""" +Langton's ant + +@ https://en.wikipedia.org/wiki/Langton%27s_ant +@ https://upload.wikimedia.org/wikipedia/commons/0/09/LangtonsAntAnimated.gif +""" + +from functools import partial + +from matplotlib import pyplot as plt +from matplotlib.animation import FuncAnimation + +WIDTH = 80 +HEIGHT = 80 + + +class LangtonsAnt: + """ + Represents the main LangonsAnt algorithm. + + >>> la = LangtonsAnt(2, 2) + >>> la.board + [[True, True], [True, True]] + >>> la.ant_position + (1, 1) + """ + + def __init__(self, width: int, height: int) -> None: + # Each square is either True or False where True is white and False is black + self.board = [[True] * width for _ in range(height)] + self.ant_position: tuple[int, int] = (width // 2, height // 2) + + # Initially pointing left (similar to the wikipedia image) + # (0 = 0° | 1 = 90° | 2 = 180 ° | 3 = 270°) + self.ant_direction: int = 3 + + def move_ant(self, axes: plt.Axes | None, display: bool, _frame: int) -> None: + """ + Performs three tasks: + 1. The ant turns either clockwise or anti-clockwise according to the colour + of the square that it is currently on. If the square is white, the ant + turns clockwise, and if the square is black the ant turns anti-clockwise + 2. The ant moves one square in the direction that it is currently facing + 3. The square the ant was previously on is inverted (White -> Black and + Black -> White) + + If display is True, the board will also be displayed on the axes + + >>> la = LangtonsAnt(2, 2) + >>> la.move_ant(None, True, 0) + >>> la.board + [[True, True], [True, False]] + >>> la.move_ant(None, True, 0) + >>> la.board + [[True, False], [True, False]] + """ + directions = { + 0: (-1, 0), # 0° + 1: (0, 1), # 90° + 2: (1, 0), # 180° + 3: (0, -1), # 270° + } + x, y = self.ant_position + + # Turn clockwise or anti-clockwise according to colour of square + if self.board[x][y] is True: + # The square is white so turn 90° clockwise + self.ant_direction = (self.ant_direction + 1) % 4 + else: + # The square is black so turn 90° anti-clockwise + self.ant_direction = (self.ant_direction - 1) % 4 + + # Move ant + move_x, move_y = directions[self.ant_direction] + self.ant_position = (x + move_x, y + move_y) + + # Flip colour of square + self.board[x][y] = not self.board[x][y] + + if display and axes: + # Display the board on the axes + axes.get_xaxis().set_ticks([]) + axes.get_yaxis().set_ticks([]) + axes.imshow(self.board, cmap="gray", interpolation="nearest") + + def display(self, frames: int = 100_000) -> None: + """ + Displays the board without delay in a matplotlib plot + to visually understand and track the ant. + + >>> _ = LangtonsAnt(WIDTH, HEIGHT) + """ + fig, ax = plt.subplots() + # Assign animation to a variable to prevent it from getting garbage collected + self.animation = FuncAnimation( + fig, partial(self.move_ant, ax, True), frames=frames, interval=1 + ) + plt.show() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + LangtonsAnt(WIDTH, HEIGHT).display() diff --git a/cellular_automata/nagel_schrekenberg.py b/cellular_automata/nagel_schrekenberg.py index 3fd6afca0153..bcdca902afee 100644 --- a/cellular_automata/nagel_schrekenberg.py +++ b/cellular_automata/nagel_schrekenberg.py @@ -24,6 +24,7 @@ >>> simulate(construct_highway(5, 2, -2), 3, 0, 2) [[0, -1, 0, -1, 0], [0, -1, 0, -1, -1], [0, -1, -1, 1, -1], [-1, 1, -1, 0, -1]] """ + from random import randint, random diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py new file mode 100644 index 000000000000..29f7ea510bfe --- /dev/null +++ b/cellular_automata/wa_tor.py @@ -0,0 +1,548 @@ +""" +Wa-Tor algorithm (1984) + +| @ https://en.wikipedia.org/wiki/Wa-Tor +| @ https://beltoforion.de/en/wator/ +| @ https://beltoforion.de/en/wator/images/wator_medium.webm + +This solution aims to completely remove any systematic approach +to the Wa-Tor planet, and utilise fully random methods. + +The constants are a working set that allows the Wa-Tor planet +to result in one of the three possible results. +""" + +from collections.abc import Callable +from random import randint, shuffle +from time import sleep +from typing import Literal + +WIDTH = 50 # Width of the Wa-Tor planet +HEIGHT = 50 # Height of the Wa-Tor planet + +PREY_INITIAL_COUNT = 30 # The initial number of prey entities +PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing + +PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities +# The initial energy value of predator entities +PREDATOR_INITIAL_ENERGY_VALUE = 15 +# The energy value provided when consuming prey +PREDATOR_FOOD_VALUE = 5 +PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing + +MAX_ENTITIES = 500 # The max number of organisms on the board +# The number of entities to delete from the unbalanced side +DELETE_UNBALANCED_ENTITIES = 50 + + +class Entity: + """ + Represents an entity (either prey or predator). + + >>> e = Entity(True, coords=(0, 0)) + >>> e.prey + True + >>> e.coords + (0, 0) + >>> e.alive + True + """ + + def __init__(self, prey: bool, coords: tuple[int, int]) -> None: + self.prey = prey + # The (row, col) pos of the entity + self.coords = coords + + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if prey else PREDATOR_REPRODUCTION_TIME + ) + self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE + self.alive = True + + def reset_reproduction_time(self) -> None: + """ + >>> e = Entity(True, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME + True + >>> e = Entity(False, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME + True + """ + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME + ) + + def __repr__(self) -> str: + """ + >>> Entity(prey=True, coords=(1, 1)) + Entity(prey=True, coords=(1, 1), remaining_reproduction_time=5) + >>> Entity(prey=False, coords=(2, 1)) # doctest: +NORMALIZE_WHITESPACE + Entity(prey=False, coords=(2, 1), + remaining_reproduction_time=20, energy_value=15) + """ + repr_ = ( + f"Entity(prey={self.prey}, coords={self.coords}, " + f"remaining_reproduction_time={self.remaining_reproduction_time}" + ) + if self.energy_value is not None: + repr_ += f", energy_value={self.energy_value}" + return f"{repr_})" + + +class WaTor: + """ + Represents the main Wa-Tor algorithm. + + :attr time_passed: A function that is called every time + time passes (a chronon) in order to visually display + the new Wa-Tor planet. The `time_passed` function can block + using ``time.sleep`` to slow the algorithm progression. + + >>> wt = WaTor(10, 15) + >>> wt.width + 10 + >>> wt.height + 15 + >>> len(wt.planet) + 15 + >>> len(wt.planet[0]) + 10 + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + + time_passed: Callable[["WaTor", int], None] | None + + def __init__(self, width: int, height: int) -> None: + self.width = width + self.height = height + self.time_passed = None + + self.planet: list[list[Entity | None]] = [[None] * width for _ in range(height)] + + # Populate planet with predators and prey randomly + for _ in range(PREY_INITIAL_COUNT): + self.add_entity(prey=True) + for _ in range(PREDATOR_INITIAL_COUNT): + self.add_entity(prey=False) + self.set_planet(self.planet) + + def set_planet(self, planet: list[list[Entity | None]]) -> None: + """ + Ease of access for testing + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None] + ... ] + >>> wt.set_planet(planet) + >>> wt.planet == planet + True + >>> wt.width + 3 + >>> wt.height + 2 + """ + self.planet = planet + self.width = len(planet[0]) + self.height = len(planet) + + def add_entity(self, prey: bool) -> None: + """ + Adds an entity, making sure the entity does + not override another entity + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[None, None], [None, None]]) + >>> wt.add_entity(True) + >>> len(wt.get_entities()) + 1 + >>> wt.add_entity(False) + >>> len(wt.get_entities()) + 2 + """ + while True: + row, col = randint(0, self.height - 1), randint(0, self.width - 1) + if self.planet[row][col] is None: + self.planet[row][col] = Entity(prey=prey, coords=(row, col)) + return + + def get_entities(self) -> list[Entity]: + """ + Returns a list of all the entities within the planet. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + return [entity for column in self.planet for entity in column if entity] + + def balance_predators_and_prey(self) -> None: + """ + Balances predators and preys so that prey + can not dominate the predators, blocking up + space for them to reproduce. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> for i in range(2000): + ... row, col = i // HEIGHT, i % WIDTH + ... wt.planet[row][col] = Entity(True, coords=(row, col)) + >>> entities = len(wt.get_entities()) + >>> wt.balance_predators_and_prey() + >>> len(wt.get_entities()) == entities + False + """ + entities = self.get_entities() + shuffle(entities) + + if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: + prey = [entity for entity in entities if entity.prey] + predators = [entity for entity in entities if not entity.prey] + + prey_count, predator_count = len(prey), len(predators) + + entities_to_purge = ( + prey[:DELETE_UNBALANCED_ENTITIES] + if prey_count > predator_count + else predators[:DELETE_UNBALANCED_ENTITIES] + ) + for entity in entities_to_purge: + self.planet[entity.coords[0]][entity.coords[1]] = None + + def get_surrounding_prey(self, entity: Entity) -> list[Entity]: + """ + Returns all the prey entities around (N, S, E, W) a predator entity. + + Subtly different to the `move_and_reproduce`. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([ + ... [None, Entity(True, (0, 1)), None], + ... [None, Entity(False, (1, 1)), None], + ... [None, Entity(True, (2, 1)), None]]) + >>> wt.get_surrounding_prey( + ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE + [Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5), + Entity(prey=True, coords=(2, 1), remaining_reproduction_time=5)] + >>> wt.set_planet([[Entity(False, (0, 0))]]) + >>> wt.get_surrounding_prey(Entity(False, (0, 0))) + [] + >>> wt.set_planet([ + ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], + ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], + ... [None, None, None]]) + >>> wt.get_surrounding_prey(Entity(False, (1, 0))) + [Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5)] + """ + row, col = entity.coords + adjacent: list[tuple[int, int]] = [ + (row - 1, col), # North + (row + 1, col), # South + (row, col - 1), # West + (row, col + 1), # East + ] + + return [ + ent + for r, c in adjacent + if 0 <= r < self.height + and 0 <= c < self.width + and (ent := self.planet[r][c]) is not None + and ent.prey + ] + + def move_and_reproduce( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Attempts to move to an unoccupied neighbouring square + in either of the four directions (North, South, East, West). + If the move was successful and the `remaining_reproduction_time` is + equal to 0, then a new prey or predator can also be created + in the previous square. + + :param direction_orders: Ordered list (like priority queue) depicting + order to attempt to move. Removes any systematic + approach of checking neighbouring squares. + + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None], + ... [None, None, None] + ... ] + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet(planet) + >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[None, Entity(prey=True, coords=(0, 1), remaining_reproduction_time=4), None], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "E", "S"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, + Entity(prey=True, coords=(0, 2), remaining_reproduction_time=4)], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][1] = wt.planet[0][2] + >>> wt.planet[0][2] = None + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, None], + [None, Entity(prey=True, coords=(1, 1), remaining_reproduction_time=4), None], + [None, None, None]] + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(False, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.move_and_reproduce(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=15), + Entity(prey=False, coords=(0, 1), remaining_reproduction_time=20, + energy_value=15)]] + """ + row, col = coords = entity.coords + + adjacent_squares: dict[Literal["N", "E", "S", "W"], tuple[int, int]] = { + "N": (row - 1, col), # North + "S": (row + 1, col), # South + "W": (row, col - 1), # West + "E": (row, col + 1), # East + } + # Weight adjacent locations + adjacent: list[tuple[int, int]] = [] + for order in direction_orders: + adjacent.append(adjacent_squares[order]) + + for r, c in adjacent: + if ( + 0 <= r < self.height + and 0 <= c < self.width + and self.planet[r][c] is None + ): + # Move entity to empty adjacent square + self.planet[r][c] = entity + self.planet[row][col] = None + entity.coords = (r, c) + break + + # (2.) See if it possible to reproduce in previous square + if coords != entity.coords and entity.remaining_reproduction_time <= 0: + # Check if the entities on the planet is less than the max limit + if len(self.get_entities()) < MAX_ENTITIES: + # Reproduce in previous square + self.planet[row][col] = Entity(prey=entity.prey, coords=coords) + entity.reset_reproduction_time() + else: + entity.remaining_reproduction_time -= 1 + + def perform_prey_actions( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Performs the actions for a prey entity + + For prey the rules are: + 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied + squares. If there are no free squares, no movement takes place. + 2. Once a prey has survived a certain number of chronons it may reproduce. + This is done as it moves to a neighbouring square, + leaving behind a new prey in its old position. + Its reproduction time is also reset to zero. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(True, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.perform_prey_actions(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), + Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5)]] + """ + self.move_and_reproduce(entity, direction_orders) + + def perform_predator_actions( + self, + entity: Entity, + occupied_by_prey_coords: tuple[int, int] | None, + direction_orders: list[Literal["N", "E", "S", "W"]], + ) -> None: + """ + Performs the actions for a predator entity + + :param occupied_by_prey_coords: Move to this location if there is prey there + + For predators the rules are: + 1. At each chronon, a predator moves randomly to an adjacent square occupied + by a prey. If there is none, the predator moves to a random adjacent + unoccupied square. If there are no free squares, no movement takes place. + 2. At each chronon, each predator is deprived of a unit of energy. + 3. Upon reaching zero energy, a predator dies. + 4. If a predator moves to a square occupied by a prey, + it eats the prey and earns a certain amount of energy. + 5. Once a predator has survived a certain number of chronons + it may reproduce in exactly the same way as the prey. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) + >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=19), None]] + """ + assert entity.energy_value is not None # [type checking] + + # (3.) If the entity has 0 energy, it will die + if entity.energy_value == 0: + self.planet[entity.coords[0]][entity.coords[1]] = None + return + + # (1.) Move to entity if possible + if occupied_by_prey_coords is not None: + # Kill the prey + prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] + assert prey is not None + prey.alive = False + + # Move onto prey + self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity + self.planet[entity.coords[0]][entity.coords[1]] = None + + entity.coords = occupied_by_prey_coords + # (4.) Eats the prey and earns energy + entity.energy_value += PREDATOR_FOOD_VALUE + else: + # (5.) If it has survived the certain number of chronons it will also + # reproduce in this function + self.move_and_reproduce(entity, direction_orders) + + # (2.) Each chronon, the predator is deprived of a unit of energy + entity.energy_value -= 1 + + def run(self, *, iteration_count: int) -> None: + """ + Emulate time passing by looping `iteration_count` times + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) + >>> len(list(filter(lambda entity: entity.prey is False, + ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT + True + """ + for iter_num in range(iteration_count): + # Generate list of all entities in order to randomly + # pop an entity at a time to simulate true randomness + # This removes the systematic approach of iterating + # through each entity width by height + all_entities = self.get_entities() + + for __ in range(len(all_entities)): + entity = all_entities.pop(randint(0, len(all_entities) - 1)) + if entity.alive is False: + continue + + directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] + shuffle(directions) # Randomly shuffle directions + + if entity.prey: + self.perform_prey_actions(entity, directions) + else: + # Create list of surrounding prey + surrounding_prey = self.get_surrounding_prey(entity) + surrounding_prey_coords = None + + if surrounding_prey: + # Again, randomly shuffle directions + shuffle(surrounding_prey) + surrounding_prey_coords = surrounding_prey[0].coords + + self.perform_predator_actions( + entity, surrounding_prey_coords, directions + ) + + # Balance out the predators and prey + self.balance_predators_and_prey() + + if self.time_passed is not None: + # Call time_passed function for Wa-Tor planet + # visualisation in a terminal or a graph. + self.time_passed(self, iter_num) + + +def visualise(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: + """ + Visually displays the Wa-Tor planet using + an ascii code in terminal to clear and re-print + the Wa-Tor planet at intervals. + + Uses ascii colour codes to colourfully display the predators and prey: + * (0x60f197) Prey = ``#`` + * (0xfffff) Predator = ``x`` + + >>> wt = WaTor(30, 30) + >>> wt.set_planet([ + ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], + ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], + ... [None, Entity(True, coords=(2, 1)), None] + ... ]) + >>> visualise(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE + # x . + x . x + . # . + + Iteration: 0 | Prey count: 2 | Predator count: 3 | + """ + if colour: + __import__("os").system("") + print("\x1b[0;0H\x1b[2J\x1b[?25l") + + reprint = "\x1b[0;0H" if colour else "" + ansi_colour_end = "\x1b[0m " if colour else " " + + planet = wt.planet + output = "" + + # Iterate over every entity in the planet + for row in planet: + for entity in row: + if entity is None: + output += " . " + else: + if colour is True: + output += ( + "\x1b[38;2;96;241;151m" + if entity.prey + else "\x1b[38;2;255;255;15m" + ) + output += f" {'#' if entity.prey else 'x'}{ansi_colour_end}" + + output += "\n" + + entities = wt.get_entities() + prey_count = sum(entity.prey for entity in entities) + + print( + f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " + f"Predator count: {len(entities) - prey_count} | {reprint}" + ) + # Block the thread to be able to visualise seeing the algorithm + sleep(0.05) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + wt = WaTor(WIDTH, HEIGHT) + wt.time_passed = visualise + wt.run(iteration_count=100_000) diff --git a/ciphers/a1z26.py b/ciphers/a1z26.py index 0f0eb7c5c083..a1377ea6d397 100644 --- a/ciphers/a1z26.py +++ b/ciphers/a1z26.py @@ -5,6 +5,7 @@ https://www.dcode.fr/letter-number-cipher http://bestcodes.weebly.com/a1z26.html """ + from __future__ import annotations diff --git a/ciphers/affine_cipher.py b/ciphers/affine_cipher.py index cd1e33b88425..10d16367cced 100644 --- a/ciphers/affine_cipher.py +++ b/ciphers/affine_cipher.py @@ -1,6 +1,8 @@ import random import sys +from maths.greatest_common_divisor import gcd_by_iterative + from . import cryptomath_module as cryptomath SYMBOLS = ( @@ -26,7 +28,7 @@ def check_keys(key_a: int, key_b: int, mode: str) -> None: "Key A must be greater than 0 and key B must " f"be between 0 and {len(SYMBOLS) - 1}." ) - if cryptomath.gcd(key_a, len(SYMBOLS)) != 1: + if gcd_by_iterative(key_a, len(SYMBOLS)) != 1: sys.exit( f"Key A {key_a} and the symbol set size {len(SYMBOLS)} " "are not relatively prime. Choose a different key." @@ -76,7 +78,7 @@ def get_random_key() -> int: while True: key_b = random.randint(2, len(SYMBOLS)) key_b = random.randint(2, len(SYMBOLS)) - if cryptomath.gcd(key_b, len(SYMBOLS)) == 1 and key_b % len(SYMBOLS) != 0: + if gcd_by_iterative(key_b, len(SYMBOLS)) == 1 and key_b % len(SYMBOLS) != 0: return key_b * len(SYMBOLS) + key_b diff --git a/ciphers/atbash.py b/ciphers/atbash.py index 0a86a800c51a..4e8f663ed02d 100644 --- a/ciphers/atbash.py +++ b/ciphers/atbash.py @@ -1,4 +1,5 @@ -""" https://en.wikipedia.org/wiki/Atbash """ +"""https://en.wikipedia.org/wiki/Atbash""" + import string diff --git a/ciphers/autokey.py b/ciphers/autokey.py index 8683e6d37001..7751a32d7546 100644 --- a/ciphers/autokey.py +++ b/ciphers/autokey.py @@ -1,5 +1,6 @@ """ https://en.wikipedia.org/wiki/Autokey_cipher + An autokey cipher (also known as the autoclave cipher) is a cipher that incorporates the message (the plaintext) into the key. The key is generated from the message in some automated fashion, @@ -10,8 +11,9 @@ def encrypt(plaintext: str, key: str) -> str: """ - Encrypt a given plaintext (string) and key (string), returning the + Encrypt a given `plaintext` (string) and `key` (string), returning the encrypted ciphertext. + >>> encrypt("hello world", "coffee") 'jsqqs avvwo' >>> encrypt("coffee is good as python", "TheAlgorithms") @@ -24,6 +26,14 @@ def encrypt(plaintext: str, key: str) -> str: Traceback (most recent call last): ... ValueError: plaintext is empty + >>> encrypt("coffee is good as python", "") + Traceback (most recent call last): + ... + ValueError: key is empty + >>> encrypt(527.26, "TheAlgorithms") + Traceback (most recent call last): + ... + TypeError: plaintext must be a string """ if not isinstance(plaintext, str): raise TypeError("plaintext must be a string") @@ -66,8 +76,9 @@ def encrypt(plaintext: str, key: str) -> str: def decrypt(ciphertext: str, key: str) -> str: """ - Decrypt a given ciphertext (string) and key (string), returning the decrypted + Decrypt a given `ciphertext` (string) and `key` (string), returning the decrypted ciphertext. + >>> decrypt("jsqqs avvwo", "coffee") 'hello world' >>> decrypt("vvjfpk wj ohvp su ddylsv", "TheAlgorithms") @@ -80,6 +91,14 @@ def decrypt(ciphertext: str, key: str) -> str: Traceback (most recent call last): ... TypeError: ciphertext must be a string + >>> decrypt("", "TheAlgorithms") + Traceback (most recent call last): + ... + ValueError: ciphertext is empty + >>> decrypt("vvjfpk wj ohvp su ddylsv", 2) + Traceback (most recent call last): + ... + TypeError: key must be a string """ if not isinstance(ciphertext, str): raise TypeError("ciphertext must be a string") diff --git a/ciphers/base32.py b/ciphers/base32.py index fee53ccaf0c4..911afa2452c0 100644 --- a/ciphers/base32.py +++ b/ciphers/base32.py @@ -1,42 +1,46 @@ -import base64 +""" +Base32 encoding and decoding +https://en.wikipedia.org/wiki/Base32 +""" -def base32_encode(string: str) -> bytes: +B32_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + + +def base32_encode(data: bytes) -> bytes: """ - Encodes a given string to base32, returning a bytes-like object - >>> base32_encode("Hello World!") + >>> base32_encode(b"Hello World!") b'JBSWY3DPEBLW64TMMQQQ====' - >>> base32_encode("123456") + >>> base32_encode(b"123456") b'GEZDGNBVGY======' - >>> base32_encode("some long complex string") + >>> base32_encode(b"some long complex string") b'ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=' """ + binary_data = "".join(bin(ord(d))[2:].zfill(8) for d in data.decode("utf-8")) + binary_data = binary_data.ljust(5 * ((len(binary_data) // 5) + 1), "0") + b32_chunks = map("".join, zip(*[iter(binary_data)] * 5)) + b32_result = "".join(B32_CHARSET[int(chunk, 2)] for chunk in b32_chunks) + return bytes(b32_result.ljust(8 * ((len(b32_result) // 8) + 1), "="), "utf-8") - # encoded the input (we need a bytes like object) - # then, b32encoded the bytes-like object - return base64.b32encode(string.encode("utf-8")) - -def base32_decode(encoded_bytes: bytes) -> str: +def base32_decode(data: bytes) -> bytes: """ - Decodes a given bytes-like object to a string, returning a string >>> base32_decode(b'JBSWY3DPEBLW64TMMQQQ====') - 'Hello World!' + b'Hello World!' >>> base32_decode(b'GEZDGNBVGY======') - '123456' + b'123456' >>> base32_decode(b'ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=') - 'some long complex string' + b'some long complex string' """ - - # decode the bytes from base32 - # then, decode the bytes-like object to return as a string - return base64.b32decode(encoded_bytes).decode("utf-8") + binary_chunks = "".join( + bin(B32_CHARSET.index(_d))[2:].zfill(5) + for _d in data.decode("utf-8").strip("=") + ) + binary_data = list(map("".join, zip(*[iter(binary_chunks)] * 8))) + return bytes("".join([chr(int(_d, 2)) for _d in binary_data]), "utf-8") if __name__ == "__main__": - test = "Hello World!" - encoded = base32_encode(test) - print(encoded) + import doctest - decoded = base32_decode(encoded) - print(decoded) + doctest.testmod() diff --git a/ciphers/base64.py b/ciphers/base64_cipher.py similarity index 90% rename from ciphers/base64.py rename to ciphers/base64_cipher.py index 38a952acc307..038d13963d95 100644 --- a/ciphers/base64.py +++ b/ciphers/base64_cipher.py @@ -34,9 +34,8 @@ def base64_encode(data: bytes) -> bytes: """ # Make sure the supplied data is a bytes-like object if not isinstance(data, bytes): - raise TypeError( - f"a bytes-like object is required, not '{data.__class__.__name__}'" - ) + msg = f"a bytes-like object is required, not '{data.__class__.__name__}'" + raise TypeError(msg) binary_stream = "".join(bin(byte)[2:].zfill(8) for byte in data) @@ -88,10 +87,11 @@ def base64_decode(encoded_data: str) -> bytes: """ # Make sure encoded_data is either a string or a bytes-like object if not isinstance(encoded_data, bytes) and not isinstance(encoded_data, str): - raise TypeError( - "argument should be a bytes-like object or ASCII string, not " - f"'{encoded_data.__class__.__name__}'" + msg = ( + "argument should be a bytes-like object or ASCII string, " + f"not '{encoded_data.__class__.__name__}'" ) + raise TypeError(msg) # In case encoded_data is a bytes-like object, make sure it contains only # ASCII characters so we convert it to a string object @@ -105,13 +105,13 @@ def base64_decode(encoded_data: str) -> bytes: # Check if the encoded string contains non base64 characters if padding: - assert all( - char in B64_CHARSET for char in encoded_data[:-padding] - ), "Invalid base64 character(s) found." + assert all(char in B64_CHARSET for char in encoded_data[:-padding]), ( + "Invalid base64 character(s) found." + ) else: - assert all( - char in B64_CHARSET for char in encoded_data - ), "Invalid base64 character(s) found." + assert all(char in B64_CHARSET for char in encoded_data), ( + "Invalid base64 character(s) found." + ) # Check the padding assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding" diff --git a/ciphers/base85.py b/ciphers/base85.py index afd1aff79d11..f0228e5052dd 100644 --- a/ciphers/base85.py +++ b/ciphers/base85.py @@ -1,30 +1,55 @@ -import base64 +""" +Base85 (Ascii85) encoding and decoding +https://en.wikipedia.org/wiki/Ascii85 +""" -def base85_encode(string: str) -> bytes: + +def _base10_to_85(d: int) -> str: + return "".join(chr(d % 85 + 33)) + _base10_to_85(d // 85) if d > 0 else "" + + +def _base85_to_10(digits: list) -> int: + return sum(char * 85**i for i, char in enumerate(reversed(digits))) + + +def ascii85_encode(data: bytes) -> bytes: """ - >>> base85_encode("") + >>> ascii85_encode(b"") b'' - >>> base85_encode("12345") + >>> ascii85_encode(b"12345") b'0etOA2#' - >>> base85_encode("base 85") + >>> ascii85_encode(b"base 85") b'@UX=h+?24' """ - # encoded the input to a bytes-like object and then a85encode that - return base64.a85encode(string.encode("utf-8")) + binary_data = "".join(bin(ord(d))[2:].zfill(8) for d in data.decode("utf-8")) + null_values = (32 * ((len(binary_data) // 32) + 1) - len(binary_data)) // 8 + binary_data = binary_data.ljust(32 * ((len(binary_data) // 32) + 1), "0") + b85_chunks = [int(_s, 2) for _s in map("".join, zip(*[iter(binary_data)] * 32))] + result = "".join(_base10_to_85(chunk)[::-1] for chunk in b85_chunks) + return bytes(result[:-null_values] if null_values % 4 != 0 else result, "utf-8") -def base85_decode(a85encoded: bytes) -> str: +def ascii85_decode(data: bytes) -> bytes: """ - >>> base85_decode(b"") - '' - >>> base85_decode(b"0etOA2#") - '12345' - >>> base85_decode(b"@UX=h+?24") - 'base 85' + >>> ascii85_decode(b"") + b'' + >>> ascii85_decode(b"0etOA2#") + b'12345' + >>> ascii85_decode(b"@UX=h+?24") + b'base 85' """ - # a85decode the input into bytes and decode that into a human readable string - return base64.a85decode(a85encoded).decode("utf-8") + null_values = 5 * ((len(data) // 5) + 1) - len(data) + binary_data = data.decode("utf-8") + "u" * null_values + b85_chunks = map("".join, zip(*[iter(binary_data)] * 5)) + b85_segments = [[ord(_s) - 33 for _s in chunk] for chunk in b85_chunks] + results = [bin(_base85_to_10(chunk))[2::].zfill(32) for chunk in b85_segments] + char_chunks = [ + [chr(int(_s, 2)) for _s in map("".join, zip(*[iter(r)] * 8))] for r in results + ] + result = "".join("".join(char) for char in char_chunks) + offset = int(null_values % 5 == 0) + return bytes(result[: offset - null_values], "utf-8") if __name__ == "__main__": diff --git a/ciphers/beaufort_cipher.py b/ciphers/beaufort_cipher.py index 8eae847a7ff7..788fc72b89c3 100644 --- a/ciphers/beaufort_cipher.py +++ b/ciphers/beaufort_cipher.py @@ -5,7 +5,7 @@ from string import ascii_uppercase dict1 = {char: i for i, char in enumerate(ascii_uppercase)} -dict2 = {i: char for i, char in enumerate(ascii_uppercase)} +dict2 = dict(enumerate(ascii_uppercase)) # This function generates the key in diff --git a/ciphers/bifid.py b/ciphers/bifid.py index c005e051a6ba..a15b381640aa 100644 --- a/ciphers/bifid.py +++ b/ciphers/bifid.py @@ -33,7 +33,7 @@ def letter_to_numbers(self, letter: str) -> np.ndarray: >>> np.array_equal(BifidCipher().letter_to_numbers('u'), [4,5]) True """ - index1, index2 = np.where(self.SQUARE == letter) + index1, index2 = np.where(letter == self.SQUARE) indexes = np.concatenate([index1 + 1, index2 + 1]) return indexes diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index d19b9a337221..1cf4d67cbaed 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -7,24 +7,29 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: """ encrypt ======= + Encodes a given string with the caesar cipher and returns the encoded message Parameters: ----------- - * input_string: the plain-text that needs to be encoded - * key: the number of letters to shift the message by + + * `input_string`: the plain-text that needs to be encoded + * `key`: the number of letters to shift the message by Optional: - * alphabet (None): the alphabet used to encode the cipher, if not + + * `alphabet` (``None``): the alphabet used to encode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used Returns: + * A string containing the encoded cipher-text More on the caesar cipher ========================= + The caesar cipher is named after Julius Caesar who used it when sending secret military messages to his troops. This is a simple substitution cipher where every character in the plain-text is shifted by a certain number known @@ -32,26 +37,28 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: Example: Say we have the following message: - "Hello, captain" + ``Hello, captain`` And our alphabet is made up of lower and uppercase letters: - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` - And our shift is "2" + And our shift is ``2`` - We can then encode the message, one letter at a time. "H" would become "J", - since "J" is two letters away, and so on. If the shift is ever two large, or + We can then encode the message, one letter at a time. ``H`` would become ``J``, + since ``J`` is two letters away, and so on. If the shift is ever two large, or our letter is at the end of the alphabet, we just start at the beginning - ("Z" would shift to "a" then "b" and so on). + (``Z`` would shift to ``a`` then ``b`` and so on). - Our final message would be "Jgnnq, ecrvckp" + Our final message would be ``Jgnnq, ecrvckp`` Further reading =============== + * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> encrypt('The quick brown fox jumps over the lazy dog', 8) 'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo' @@ -85,23 +92,28 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str: """ decrypt ======= + Decodes a given string of cipher-text and returns the decoded plain-text Parameters: ----------- - * input_string: the cipher-text that needs to be decoded - * key: the number of letters to shift the message backwards by to decode + + * `input_string`: the cipher-text that needs to be decoded + * `key`: the number of letters to shift the message backwards by to decode Optional: - * alphabet (None): the alphabet used to decode the cipher, if not + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used Returns: + * A string containing the decoded plain-text More on the caesar cipher ========================= + The caesar cipher is named after Julius Caesar who used it when sending secret military messages to his troops. This is a simple substitution cipher where very character in the plain-text is shifted by a certain number known @@ -110,27 +122,29 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str: Example: Say we have the following cipher-text: - "Jgnnq, ecrvckp" + ``Jgnnq, ecrvckp`` And our alphabet is made up of lower and uppercase letters: - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` - And our shift is "2" + And our shift is ``2`` To decode the message, we would do the same thing as encoding, but in - reverse. The first letter, "J" would become "H" (remember: we are decoding) - because "H" is two letters in reverse (to the left) of "J". We would - continue doing this. A letter like "a" would shift back to the end of - the alphabet, and would become "Z" or "Y" and so on. + reverse. The first letter, ``J`` would become ``H`` (remember: we are decoding) + because ``H`` is two letters in reverse (to the left) of ``J``. We would + continue doing this. A letter like ``a`` would shift back to the end of + the alphabet, and would become ``Z`` or ``Y`` and so on. - Our final message would be "Hello, captain" + Our final message would be ``Hello, captain`` Further reading =============== + * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> decrypt('bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo', 8) 'The quick brown fox jumps over the lazy dog' @@ -150,41 +164,44 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str """ brute_force =========== + Returns all the possible combinations of keys and the decoded strings in the form of a dictionary Parameters: ----------- - * input_string: the cipher-text that needs to be used during brute-force + + * `input_string`: the cipher-text that needs to be used during brute-force Optional: - * alphabet: (None): the alphabet used to decode the cipher, if not + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used More about brute force ====================== + Brute force is when a person intercepts a message or password, not knowing the key and tries every single combination. This is easy with the caesar cipher since there are only all the letters in the alphabet. The more complex the cipher, the larger amount of time it will take to do brute force Ex: - Say we have a 5 letter alphabet (abcde), for simplicity and we intercepted the - following message: - - "dbc" - + Say we have a ``5`` letter alphabet (``abcde``), for simplicity and we intercepted + the following message: ``dbc``, we could then just write out every combination: - ecd... and so on, until we reach a combination that makes sense: - "cab" + ``ecd``... and so on, until we reach a combination that makes sense: + ``cab`` Further reading =============== + * https://en.wikipedia.org/wiki/Brute_force Doctests ======== + >>> brute_force("jFyuMy xIH'N vLONy zILwy Gy!")[20] "Please don't brute force me!" @@ -208,7 +225,7 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str if __name__ == "__main__": while True: - print(f'\n{"-" * 10}\n Menu\n{"-" * 10}') + print(f"\n{'-' * 10}\n Menu\n{'-' * 10}") print(*["1.Encrypt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n") # get user input diff --git a/ciphers/cryptomath_module.py b/ciphers/cryptomath_module.py index be8764ff38c3..02e94e4b9e92 100644 --- a/ciphers/cryptomath_module.py +++ b/ciphers/cryptomath_module.py @@ -1,12 +1,10 @@ -def gcd(a: int, b: int) -> int: - while a != 0: - a, b = b % a, a - return b +from maths.greatest_common_divisor import gcd_by_iterative def find_mod_inverse(a: int, m: int) -> int: - if gcd(a, m) != 1: - raise ValueError(f"mod inverse of {a!r} and {m!r} does not exist") + if gcd_by_iterative(a, m) != 1: + msg = f"mod inverse of {a!r} and {m!r} does not exist" + raise ValueError(msg) u1, u2, u3 = 1, 0, a v1, v2, v3 = 0, 1, m while v3 != 0: diff --git a/ciphers/decrypt_caesar_with_chi_squared.py b/ciphers/decrypt_caesar_with_chi_squared.py index 6c36860207cd..fb95c0f90628 100644 --- a/ciphers/decrypt_caesar_with_chi_squared.py +++ b/ciphers/decrypt_caesar_with_chi_squared.py @@ -11,33 +11,31 @@ def decrypt_caesar_with_chi_squared( """ Basic Usage =========== + Arguments: - * ciphertext (str): the text to decode (encoded with the caesar cipher) + * `ciphertext` (str): the text to decode (encoded with the caesar cipher) Optional Arguments: - * cipher_alphabet (list): the alphabet used for the cipher (each letter is - a string separated by commas) - * frequencies_dict (dict): a dictionary of word frequencies where keys are - the letters and values are a percentage representation of the frequency as - a decimal/float - * case_sensitive (bool): a boolean value: True if the case matters during - decryption, False if it doesn't + * `cipher_alphabet` (list): the alphabet used for the cipher (each letter is + a string separated by commas) + * `frequencies_dict` (dict): a dictionary of word frequencies where keys are + the letters and values are a percentage representation of the frequency as + a decimal/float + * `case_sensitive` (bool): a boolean value: ``True`` if the case matters during + decryption, ``False`` if it doesn't Returns: - * A tuple in the form of: - ( - most_likely_cipher, - most_likely_cipher_chi_squared_value, - decoded_most_likely_cipher - ) + * A tuple in the form of: + (`most_likely_cipher`, `most_likely_cipher_chi_squared_value`, + `decoded_most_likely_cipher`) - where... - - most_likely_cipher is an integer representing the shift of the smallest - chi-squared statistic (most likely key) - - most_likely_cipher_chi_squared_value is a float representing the - chi-squared statistic of the most likely shift - - decoded_most_likely_cipher is a string with the decoded cipher - (decoded by the most_likely_cipher key) + where... + - `most_likely_cipher` is an integer representing the shift of the smallest + chi-squared statistic (most likely key) + - `most_likely_cipher_chi_squared_value` is a float representing the + chi-squared statistic of the most likely shift + - `decoded_most_likely_cipher` is a string with the decoded cipher + (decoded by the most_likely_cipher key) The Chi-squared test @@ -45,52 +43,57 @@ def decrypt_caesar_with_chi_squared( The caesar cipher ----------------- + The caesar cipher is a very insecure encryption algorithm, however it has been used since Julius Caesar. The cipher is a simple substitution cipher where each character in the plain text is replaced by a character in the alphabet a certain number of characters after the original character. The number of characters away is called the shift or key. For example: - Plain text: hello - Key: 1 - Cipher text: ifmmp - (each letter in hello has been shifted one to the right in the eng. alphabet) + | Plain text: ``hello`` + | Key: ``1`` + | Cipher text: ``ifmmp`` + | (each letter in ``hello`` has been shifted one to the right in the eng. alphabet) As you can imagine, this doesn't provide lots of security. In fact decrypting ciphertext by brute-force is extremely easy even by hand. However - one way to do that is the chi-squared test. + one way to do that is the chi-squared test. The chi-squared test - ------------------- + -------------------- + Each letter in the english alphabet has a frequency, or the amount of times it shows up compared to other letters (usually expressed as a decimal representing the percentage likelihood). The most common letter in the - english language is "e" with a frequency of 0.11162 or 11.162%. The test is - completed in the following fashion. + english language is ``e`` with a frequency of ``0.11162`` or ``11.162%``. + The test is completed in the following fashion. 1. The ciphertext is decoded in a brute force way (every combination of the - 26 possible combinations) + ``26`` possible combinations) 2. For every combination, for each letter in the combination, the average amount of times the letter should appear the message is calculated by - multiplying the total number of characters by the frequency of the letter + multiplying the total number of characters by the frequency of the letter. + + | For example: + | In a message of ``100`` characters, ``e`` should appear around ``11.162`` + times. - For example: - In a message of 100 characters, e should appear around 11.162 times. + 3. Then, to calculate the margin of error (the amount of times the letter + SHOULD appear with the amount of times the letter DOES appear), we use + the chi-squared test. The following formula is used: - 3. Then, to calculate the margin of error (the amount of times the letter - SHOULD appear with the amount of times the letter DOES appear), we use - the chi-squared test. The following formula is used: + Let: + - n be the number of times the letter actually appears + - p be the predicted value of the number of times the letter should + appear (see item ``2``) + - let v be the chi-squared test result (referred to here as chi-squared + value/statistic) - Let: - - n be the number of times the letter actually appears - - p be the predicted value of the number of times the letter should - appear (see #2) - - let v be the chi-squared test result (referred to here as chi-squared - value/statistic) + :: - (n - p)^2 - --------- = v - p + (n - p)^2 + --------- = v + p 4. Each chi squared value for each letter is then added up to the total. The total is the chi-squared statistic for that encryption key. @@ -98,16 +101,16 @@ def decrypt_caesar_with_chi_squared( to be the decoded answer. Further Reading - ================ + =============== - * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared- - statistic/ + * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared-statistic/ * https://en.wikipedia.org/wiki/Letter_frequency * https://en.wikipedia.org/wiki/Chi-squared_test * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> decrypt_caesar_with_chi_squared( ... 'dof pz aol jhlzhy jpwoly zv wvwbshy? pa pz avv lhzf av jyhjr!' ... ) # doctest: +NORMALIZE_WHITESPACE @@ -206,20 +209,19 @@ def decrypt_caesar_with_chi_squared( # Add the margin of error to the total chi squared statistic chi_squared_statistic += chi_letter_value - else: - if letter.lower() in frequencies: - # Get the amount of times the letter occurs in the message - occurrences = decrypted_with_shift.count(letter) + elif letter.lower() in frequencies: + # Get the amount of times the letter occurs in the message + occurrences = decrypted_with_shift.count(letter) - # Get the excepcted amount of times the letter should appear based - # on letter frequencies - expected = frequencies[letter] * occurrences + # Get the excepcted amount of times the letter should appear based + # on letter frequencies + expected = frequencies[letter] * occurrences - # Complete the chi squared statistic formula - chi_letter_value = ((occurrences - expected) ** 2) / expected + # Complete the chi squared statistic formula + chi_letter_value = ((occurrences - expected) ** 2) / expected - # Add the margin of error to the total chi squared statistic - chi_squared_statistic += chi_letter_value + # Add the margin of error to the total chi squared statistic + chi_squared_statistic += chi_letter_value # Add the data to the chi_squared_statistic_values dictionary chi_squared_statistic_values[shift] = ( diff --git a/ciphers/diffie.py b/ciphers/diffie.py index 4ff90be009c1..1e1e868999b6 100644 --- a/ciphers/diffie.py +++ b/ciphers/diffie.py @@ -1,11 +1,28 @@ from __future__ import annotations -def find_primitive(n: int) -> int | None: - for r in range(1, n): +def find_primitive(modulus: int) -> int | None: + """ + Find a primitive root modulo modulus, if one exists. + + Args: + modulus : The modulus for which to find a primitive root. + + Returns: + The primitive root if one exists, or None if there is none. + + Examples: + >>> find_primitive(7) # Modulo 7 has primitive root 3 + 3 + >>> find_primitive(11) # Modulo 11 has primitive root 2 + 2 + >>> find_primitive(8) == None # Modulo 8 has no primitive root + True + """ + for r in range(1, modulus): li = [] - for x in range(n - 1): - val = pow(r, x, n) + for x in range(modulus - 1): + val = pow(r, x, modulus) if val in li: break li.append(val) @@ -15,18 +32,22 @@ def find_primitive(n: int) -> int | None: if __name__ == "__main__": - q = int(input("Enter a prime number q: ")) - a = find_primitive(q) - if a is None: - print(f"Cannot find the primitive for the value: {a!r}") + import doctest + + doctest.testmod() + + prime = int(input("Enter a prime number q: ")) + primitive_root = find_primitive(prime) + if primitive_root is None: + print(f"Cannot find the primitive for the value: {primitive_root!r}") else: a_private = int(input("Enter private key of A: ")) - a_public = pow(a, a_private, q) + a_public = pow(primitive_root, a_private, prime) b_private = int(input("Enter private key of B: ")) - b_public = pow(a, b_private, q) + b_public = pow(primitive_root, b_private, prime) - a_secret = pow(b_public, a_private, q) - b_secret = pow(a_public, b_private, q) + a_secret = pow(b_public, a_private, prime) + b_secret = pow(a_public, b_private, prime) print("The key value generated by A is: ", a_secret) print("The key value generated by B is: ", b_secret) diff --git a/ciphers/diffie_hellman.py b/ciphers/diffie_hellman.py index 072f4aaaa6da..395d98ec6e60 100644 --- a/ciphers/diffie_hellman.py +++ b/ciphers/diffie_hellman.py @@ -1,22 +1,25 @@ -from binascii import hexlify -from hashlib import sha256 -from os import urandom +""" +https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange +""" -# RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups for -# Internet Key Exchange (IKE) https://tools.ietf.org/html/rfc3526 +import random -primes = { +""" +RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups for +Internet Key Exchange (IKE) https://tools.ietf.org/html/rfc3526 +""" +GROUPS = { # 1536-bit 5: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" - + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" - + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" - + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" - + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" - + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" - + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -25,16 +28,16 @@ 14: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" - + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" - + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" - + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" - + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" - + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" - + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" - + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" - + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" - + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -43,21 +46,21 @@ 15: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" - + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" - + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" - + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" - + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" - + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" - + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" - + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" - + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" - + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" - + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" - + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" - + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" - + "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -66,27 +69,27 @@ 16: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" - + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" - + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" - + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" - + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" - + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" - + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" - + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" - + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" - + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" - + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" - + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" - + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" - + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" - + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" - + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" - + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" - + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" - + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" - + "FFFFFFFFFFFFFFFF", + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" + "FFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -95,33 +98,33 @@ 17: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" - + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" - + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" - + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" - + "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" - + "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" - + "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" - + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" - + "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" - + "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" - + "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" - + "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" - + "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" - + "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" - + "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" - + "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" - + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406" - + "AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918" - + "DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151" - + "2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03" - + "F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F" - + "BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" - + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B" - + "B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632" - + "387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E" - + "6DCC4024FFFFFFFFFFFFFFFF", + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" + "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" + "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" + "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" + "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" + "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" + "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" + "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" + "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" + "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" + "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" + "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406" + "AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918" + "DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151" + "2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03" + "F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F" + "BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B" + "B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632" + "387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E" + "6DCC4024FFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -130,48 +133,48 @@ 18: { "prime": int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" - + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" - + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" - + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" - + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" - + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" - + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" - + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" - + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" - + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" - + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" - + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" - + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" - + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" - + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" - + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" - + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" - + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" - + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" - + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" - + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BD" - + "F8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831" - + "179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B" - + "DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF" - + "5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6" - + "D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3" - + "23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" - + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE328" - + "06A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C" - + "DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE" - + "12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4" - + "38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300" - + "741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F568" - + "3423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" - + "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B" - + "4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A" - + "062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A36" - + "4597E899A0255DC164F31CC50846851DF9AB48195DED7EA1" - + "B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92" - + "4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E47" - + "9558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" - + "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BD" + "F8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831" + "179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B" + "DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF" + "5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6" + "D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3" + "23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE328" + "06A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C" + "DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE" + "12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4" + "38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300" + "741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F568" + "3423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" + "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B" + "4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A" + "062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A36" + "4597E899A0255DC164F31CC50846851DF9AB48195DED7EA1" + "B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92" + "4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E47" + "9558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" + "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", base=16, ), "generator": 2, @@ -181,84 +184,46 @@ class DiffieHellman: """ - Class to represent the Diffie-Hellman key exchange protocol + Class to represent one party in the Diffie-Hellman key exchange protocol + Current minimum recommendation is 2048 bit + >>> group = GROUPS[14] + >>> prime, generator = group['prime'], group['generator'] + >>> alice = DiffieHellman(prime, generator) - >>> alice = DiffieHellman() - >>> bob = DiffieHellman() + Both parties should agree on the same public parameters + >>> bob = DiffieHellman(alice.prime, alice.generator) - >>> alice_private = alice.get_private_key() - >>> alice_public = alice.generate_public_key() + Alice sends Bob its public key, + >>> bob_shared = bob.generate_shared_key(alice.public_key) - >>> bob_private = bob.get_private_key() - >>> bob_public = bob.generate_public_key() + and the same vice versa: + >>> alice_shared = alice.generate_shared_key(bob.public_key) - >>> # generating shared key using the DH object - >>> alice_shared = alice.generate_shared_key(bob_public) - >>> bob_shared = bob.generate_shared_key(alice_public) - - >>> assert alice_shared == bob_shared - - >>> # generating shared key using static methods - >>> alice_shared = DiffieHellman.generate_shared_key_static( - ... alice_private, bob_public - ... ) - >>> bob_shared = DiffieHellman.generate_shared_key_static( - ... bob_private, alice_public - ... ) - - >>> assert alice_shared == bob_shared + >>> alice_shared == bob_shared + True """ - # Current minimum recommendation is 2048 bit (group 14) - def __init__(self, group: int = 14) -> None: - if group not in primes: - raise ValueError("Unsupported Group") - self.prime = primes[group]["prime"] - self.generator = primes[group]["generator"] - - self.__private_key = int(hexlify(urandom(32)), base=16) - - def get_private_key(self) -> str: - return hex(self.__private_key)[2:] + def __init__(self, prime: int, generator: int) -> None: + self.prime = prime + self.generator = generator + self.__private_key = random.getrandbits(256) - def generate_public_key(self) -> str: - public_key = pow(self.generator, self.__private_key, self.prime) - return hex(public_key)[2:] + @property + def public_key(self) -> int: + return pow(self.generator, self.__private_key, self.prime) def is_valid_public_key(self, key: int) -> bool: # check if the other public key is valid based on NIST SP800-56 - if 2 <= key and key <= self.prime - 2: - if pow(key, (self.prime - 1) // 2, self.prime) == 1: - return True - return False + return ( + 2 <= key <= self.prime - 2 + and pow(key, (self.prime - 1) // 2, self.prime) == 1 + ) - def generate_shared_key(self, other_key_str: str) -> str: - other_key = int(other_key_str, base=16) + def generate_shared_key(self, other_key: int) -> int: if not self.is_valid_public_key(other_key): raise ValueError("Invalid public key") - shared_key = pow(other_key, self.__private_key, self.prime) - return sha256(str(shared_key).encode()).hexdigest() - - @staticmethod - def is_valid_public_key_static(remote_public_key_str: int, prime: int) -> bool: - # check if the other public key is valid based on NIST SP800-56 - if 2 <= remote_public_key_str and remote_public_key_str <= prime - 2: - if pow(remote_public_key_str, (prime - 1) // 2, prime) == 1: - return True - return False - - @staticmethod - def generate_shared_key_static( - local_private_key_str: str, remote_public_key_str: str, group: int = 14 - ) -> str: - local_private_key = int(local_private_key_str, base=16) - remote_public_key = int(remote_public_key_str, base=16) - prime = primes[group]["prime"] - if not DiffieHellman.is_valid_public_key_static(remote_public_key, prime): - raise ValueError("Invalid public key") - shared_key = pow(remote_public_key, local_private_key, prime) - return sha256(str(shared_key).encode()).hexdigest() + return pow(other_key, self.__private_key, self.prime) if __name__ == "__main__": diff --git a/ciphers/enigma_machine2.py b/ciphers/enigma_machine2.py index 07d21893f192..e42fdd82ed41 100644 --- a/ciphers/enigma_machine2.py +++ b/ciphers/enigma_machine2.py @@ -1,19 +1,22 @@ """ -Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine -Video explanation: https://youtu.be/QwQVMqfoB2E -Also check out Numberphile's and Computerphile's videos on this topic +| Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine +| Video explanation: https://youtu.be/QwQVMqfoB2E +| Also check out Numberphile's and Computerphile's videos on this topic -This module contains function 'enigma' which emulates +This module contains function ``enigma`` which emulates the famous Enigma machine from WWII. + Module includes: -- enigma function + +- ``enigma`` function - showcase of function usage -- 9 randomly generated rotors +- ``9`` randomly generated rotors - reflector (aka static rotor) - original alphabet Created by TrapinchO """ + from __future__ import annotations RotorPositionT = tuple[int, int, int] @@ -72,7 +75,7 @@ def _validator( rotpos: RotorPositionT, rotsel: RotorSelectionT, pb: str ) -> tuple[RotorPositionT, RotorSelectionT, dict[str, str]]: """ - Checks if the values can be used for the 'enigma' function + Checks if the values can be used for the ``enigma`` function >>> _validator((1,1,1), (rotor1, rotor2, rotor3), 'POLAND') ((1, 1, 1), ('EGZWVONAHDCLFQMSIPJBYUKXTR', 'FOBHMDKEXQNRAULPGSJVTYICZW', \ @@ -82,27 +85,25 @@ def _validator( :param rotpos: rotor_positon :param rotsel: rotor_selection :param pb: plugb -> validated and transformed - :return: (rotpos, rotsel, pb) + :return: (`rotpos`, `rotsel`, `pb`) """ # Checks if there are 3 unique rotors if (unique_rotsel := len(set(rotsel))) < 3: - raise Exception(f"Please use 3 unique rotors (not {unique_rotsel})") + msg = f"Please use 3 unique rotors (not {unique_rotsel})" + raise Exception(msg) # Checks if rotor positions are valid rotorpos1, rotorpos2, rotorpos3 = rotpos if not 0 < rotorpos1 <= len(abc): - raise ValueError( - "First rotor position is not within range of 1..26 (" f"{rotorpos1}" - ) + msg = f"First rotor position is not within range of 1..26 ({rotorpos1}" + raise ValueError(msg) if not 0 < rotorpos2 <= len(abc): - raise ValueError( - "Second rotor position is not within range of 1..26 (" f"{rotorpos2})" - ) + msg = f"Second rotor position is not within range of 1..26 ({rotorpos2})" + raise ValueError(msg) if not 0 < rotorpos3 <= len(abc): - raise ValueError( - "Third rotor position is not within range of 1..26 (" f"{rotorpos3})" - ) + msg = f"Third rotor position is not within range of 1..26 ({rotorpos3})" + raise ValueError(msg) # Validates string and returns dict pbdict = _plugboard(pb) @@ -119,9 +120,10 @@ def _plugboard(pbstring: str) -> dict[str, str]: >>> _plugboard('POLAND') {'P': 'O', 'O': 'P', 'L': 'A', 'A': 'L', 'N': 'D', 'D': 'N'} - In the code, 'pb' stands for 'plugboard' + In the code, ``pb`` stands for ``plugboard`` Pairs can be separated by spaces + :param pbstring: string containing plugboard setting for the Enigma machine :return: dictionary containing converted pairs """ @@ -130,9 +132,11 @@ def _plugboard(pbstring: str) -> dict[str, str]: # a) is type string # b) has even length (so pairs can be made) if not isinstance(pbstring, str): - raise TypeError(f"Plugboard setting isn't type string ({type(pbstring)})") + msg = f"Plugboard setting isn't type string ({type(pbstring)})" + raise TypeError(msg) elif len(pbstring) % 2 != 0: - raise Exception(f"Odd number of symbols ({len(pbstring)})") + msg = f"Odd number of symbols ({len(pbstring)})" + raise Exception(msg) elif pbstring == "": return {} @@ -142,9 +146,11 @@ def _plugboard(pbstring: str) -> dict[str, str]: tmppbl = set() for i in pbstring: if i not in abc: - raise Exception(f"'{i}' not in list of symbols") + msg = f"'{i}' not in list of symbols" + raise Exception(msg) elif i in tmppbl: - raise Exception(f"Duplicate symbol ({i})") + msg = f"Duplicate symbol ({i})" + raise Exception(msg) else: tmppbl.add(i) del tmppbl @@ -165,31 +171,34 @@ def enigma( plugb: str = "", ) -> str: """ - The only difference with real-world enigma is that I allowed string input. + The only difference with real-world enigma is that ``I`` allowed string input. All characters are converted to uppercase. (non-letter symbol are ignored) - How it works: - (for every letter in the message) + + | How it works: + | (for every letter in the message) - Input letter goes into the plugboard. - If it is connected to another one, switch it. + If it is connected to another one, switch it. + + - Letter goes through ``3`` rotors. + Each rotor can be represented as ``2`` sets of symbol, where one is shuffled. + Each symbol from the first set has corresponding symbol in + the second set and vice versa. - - Letter goes through 3 rotors. - Each rotor can be represented as 2 sets of symbol, where one is shuffled. - Each symbol from the first set has corresponding symbol in - the second set and vice versa. + example:: - example: - | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F - | VKLEPDBGRNWTFCJOHQAMUZYIXS | + | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F + | VKLEPDBGRNWTFCJOHQAMUZYIXS | - Symbol then goes through reflector (static rotor). - There it is switched with paired symbol - The reflector can be represented as2 sets, each with half of the alphanet. - There are usually 10 pairs of letters. + There it is switched with paired symbol. + The reflector can be represented as ``2`` sets, each with half of the alphanet. + There are usually ``10`` pairs of letters. + + Example:: - Example: - | ABCDEFGHIJKLM | e.g. E is paired to X - | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa + | ABCDEFGHIJKLM | e.g. E is paired to X + | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa - Letter then goes through the rotors again @@ -208,9 +217,9 @@ def enigma( :param text: input message - :param rotor_position: tuple with 3 values in range 1..26 - :param rotor_selection: tuple with 3 rotors () - :param plugb: string containing plugboard configuration (default '') + :param rotor_position: tuple with ``3`` values in range ``1``.. ``26`` + :param rotor_selection: tuple with ``3`` rotors + :param plugb: string containing plugboard configuration (default ``''``) :return: en/decrypted string """ diff --git a/ciphers/fractionated_morse_cipher.py b/ciphers/fractionated_morse_cipher.py new file mode 100644 index 000000000000..6c4c415abac1 --- /dev/null +++ b/ciphers/fractionated_morse_cipher.py @@ -0,0 +1,168 @@ +""" +Python program for the Fractionated Morse Cipher. + +The Fractionated Morse cipher first converts the plaintext to Morse code, +then enciphers fixed-size blocks of Morse code back to letters. +This procedure means plaintext letters are mixed into the ciphertext letters, +making it more secure than substitution ciphers. + +http://practicalcryptography.com/ciphers/fractionated-morse-cipher/ +""" + +import string + +MORSE_CODE_DICT = { + "A": ".-", + "B": "-...", + "C": "-.-.", + "D": "-..", + "E": ".", + "F": "..-.", + "G": "--.", + "H": "....", + "I": "..", + "J": ".---", + "K": "-.-", + "L": ".-..", + "M": "--", + "N": "-.", + "O": "---", + "P": ".--.", + "Q": "--.-", + "R": ".-.", + "S": "...", + "T": "-", + "U": "..-", + "V": "...-", + "W": ".--", + "X": "-..-", + "Y": "-.--", + "Z": "--..", + " ": "", +} + +# Define possible trigrams of Morse code +MORSE_COMBINATIONS = [ + "...", + "..-", + "..x", + ".-.", + ".--", + ".-x", + ".x.", + ".x-", + ".xx", + "-..", + "-.-", + "-.x", + "--.", + "---", + "--x", + "-x.", + "-x-", + "-xx", + "x..", + "x.-", + "x.x", + "x-.", + "x--", + "x-x", + "xx.", + "xx-", + "xxx", +] + +# Create a reverse dictionary for Morse code +REVERSE_DICT = {value: key for key, value in MORSE_CODE_DICT.items()} + + +def encode_to_morse(plaintext: str) -> str: + """Encode a plaintext message into Morse code. + + Args: + plaintext: The plaintext message to encode. + + Returns: + The Morse code representation of the plaintext message. + + Example: + >>> encode_to_morse("defend the east") + '-..x.x..-.x.x-.x-..xx-x....x.xx.x.-x...x-' + """ + return "x".join([MORSE_CODE_DICT.get(letter.upper(), "") for letter in plaintext]) + + +def encrypt_fractionated_morse(plaintext: str, key: str) -> str: + """Encrypt a plaintext message using Fractionated Morse Cipher. + + Args: + plaintext: The plaintext message to encrypt. + key: The encryption key. + + Returns: + The encrypted ciphertext. + + Example: + >>> encrypt_fractionated_morse("defend the east","Roundtable") + 'ESOAVVLJRSSTRX' + + """ + morse_code = encode_to_morse(plaintext) + key = key.upper() + string.ascii_uppercase + key = "".join(sorted(set(key), key=key.find)) + + # Ensure morse_code length is a multiple of 3 + padding_length = 3 - (len(morse_code) % 3) + morse_code += "x" * padding_length + + fractionated_morse_dict = {v: k for k, v in zip(key, MORSE_COMBINATIONS)} + fractionated_morse_dict["xxx"] = "" + encrypted_text = "".join( + [ + fractionated_morse_dict[morse_code[i : i + 3]] + for i in range(0, len(morse_code), 3) + ] + ) + return encrypted_text + + +def decrypt_fractionated_morse(ciphertext: str, key: str) -> str: + """Decrypt a ciphertext message encrypted with Fractionated Morse Cipher. + + Args: + ciphertext: The ciphertext message to decrypt. + key: The decryption key. + + Returns: + The decrypted plaintext message. + + Example: + >>> decrypt_fractionated_morse("ESOAVVLJRSSTRX","Roundtable") + 'DEFEND THE EAST' + """ + key = key.upper() + string.ascii_uppercase + key = "".join(sorted(set(key), key=key.find)) + + inverse_fractionated_morse_dict = dict(zip(key, MORSE_COMBINATIONS)) + morse_code = "".join( + [inverse_fractionated_morse_dict.get(letter, "") for letter in ciphertext] + ) + decrypted_text = "".join( + [REVERSE_DICT[code] for code in morse_code.split("x")] + ).strip() + return decrypted_text + + +if __name__ == "__main__": + """ + Example usage of Fractionated Morse Cipher. + """ + plaintext = "defend the east" + print("Plain Text:", plaintext) + key = "ROUNDTABLE" + + ciphertext = encrypt_fractionated_morse(plaintext, key) + print("Encrypted:", ciphertext) + + decrypted_text = decrypt_fractionated_morse(ciphertext, key) + print("Decrypted:", decrypted_text) diff --git a/ciphers/gronsfeld_cipher.py b/ciphers/gronsfeld_cipher.py new file mode 100644 index 000000000000..8fbeab4307fc --- /dev/null +++ b/ciphers/gronsfeld_cipher.py @@ -0,0 +1,45 @@ +from string import ascii_uppercase + + +def gronsfeld(text: str, key: str) -> str: + """ + Encrypt plaintext with the Gronsfeld cipher + + >>> gronsfeld('hello', '412') + 'LFNPP' + >>> gronsfeld('hello', '123') + 'IGOMQ' + >>> gronsfeld('', '123') + '' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '0') + 'YES, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '01') + 'YFS, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '012') + 'YFU, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '') + Traceback (most recent call last): + ... + ZeroDivisionError: integer modulo by zero + """ + ascii_len = len(ascii_uppercase) + key_len = len(key) + encrypted_text = "" + keys = [int(char) for char in key] + upper_case_text = text.upper() + + for i, char in enumerate(upper_case_text): + if char in ascii_uppercase: + new_position = (ascii_uppercase.index(char) + keys[i % key_len]) % ascii_len + shifted_letter = ascii_uppercase[new_position] + encrypted_text += shifted_letter + else: + encrypted_text += char + + return encrypted_text + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/ciphers/hill_cipher.py b/ciphers/hill_cipher.py index f646d567b4c8..33b2529f017b 100644 --- a/ciphers/hill_cipher.py +++ b/ciphers/hill_cipher.py @@ -35,23 +35,12 @@ https://www.youtube.com/watch?v=4RhLNDqcjpA """ -import string -import numpy +import string +import numpy as np -def greatest_common_divisor(a: int, b: int) -> int: - """ - >>> greatest_common_divisor(4, 8) - 4 - >>> greatest_common_divisor(8, 4) - 4 - >>> greatest_common_divisor(4, 7) - 1 - >>> greatest_common_divisor(0, 10) - 10 - """ - return b if a == 0 else greatest_common_divisor(b % a, a) +from maths.greatest_common_divisor import greatest_common_divisor class HillCipher: @@ -60,11 +49,11 @@ class HillCipher: # i.e. a total of 36 characters # take x and return x % len(key_string) - modulus = numpy.vectorize(lambda x: x % 36) + modulus = np.vectorize(lambda x: x % 36) - to_int = numpy.vectorize(round) + to_int = np.vectorize(round) - def __init__(self, encrypt_key: numpy.ndarray) -> None: + def __init__(self, encrypt_key: np.ndarray) -> None: """ encrypt_key is an NxN numpy array """ @@ -74,7 +63,7 @@ def __init__(self, encrypt_key: numpy.ndarray) -> None: def replace_letters(self, letter: str) -> int: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.replace_letters('T') 19 >>> hill_cipher.replace_letters('0') @@ -84,7 +73,7 @@ def replace_letters(self, letter: str) -> int: def replace_digits(self, num: int) -> str: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.replace_digits(19) 'T' >>> hill_cipher.replace_digits(26) @@ -94,24 +83,25 @@ def replace_digits(self, num: int) -> str: def check_determinant(self) -> None: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.check_determinant() """ - det = round(numpy.linalg.det(self.encrypt_key)) + det = round(np.linalg.det(self.encrypt_key)) if det < 0: det = det % len(self.key_string) req_l = len(self.key_string) if greatest_common_divisor(det, len(self.key_string)) != 1: - raise ValueError( - f"determinant modular {req_l} of encryption key({det}) is not co prime " - f"w.r.t {req_l}.\nTry another key." + msg = ( + f"determinant modular {req_l} of encryption key({det}) " + f"is not co prime w.r.t {req_l}.\nTry another key." ) + raise ValueError(msg) def process_text(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.process_text('Testing Hill Cipher') 'TESTINGHILLCIPHERR' >>> hill_cipher.process_text('hello') @@ -127,7 +117,7 @@ def process_text(self, text: str) -> str: def encrypt(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.encrypt('testing hill cipher') 'WHXYJOLM9C6XT085LL' >>> hill_cipher.encrypt('hello') @@ -139,7 +129,7 @@ def encrypt(self, text: str) -> str: for i in range(0, len(text) - self.break_key + 1, self.break_key): batch = text[i : i + self.break_key] vec = [self.replace_letters(char) for char in batch] - batch_vec = numpy.array([vec]).T + batch_vec = np.array([vec]).T batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[ 0 ] @@ -150,14 +140,14 @@ def encrypt(self, text: str) -> str: return encrypted - def make_decrypt_key(self) -> numpy.ndarray: + def make_decrypt_key(self) -> np.ndarray: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.make_decrypt_key() array([[ 6, 25], [ 5, 26]]) """ - det = round(numpy.linalg.det(self.encrypt_key)) + det = round(np.linalg.det(self.encrypt_key)) if det < 0: det = det % len(self.key_string) @@ -168,16 +158,14 @@ def make_decrypt_key(self) -> numpy.ndarray: break inv_key = ( - det_inv - * numpy.linalg.det(self.encrypt_key) - * numpy.linalg.inv(self.encrypt_key) + det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key) ) return self.to_int(self.modulus(inv_key)) def decrypt(self, text: str) -> str: """ - >>> hill_cipher = HillCipher(numpy.array([[2, 5], [1, 6]])) + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) >>> hill_cipher.decrypt('WHXYJOLM9C6XT085LL') 'TESTINGHILLCIPHERR' >>> hill_cipher.decrypt('85FF00') @@ -190,7 +178,7 @@ def decrypt(self, text: str) -> str: for i in range(0, len(text) - self.break_key + 1, self.break_key): batch = text[i : i + self.break_key] vec = [self.replace_letters(char) for char in batch] - batch_vec = numpy.array([vec]).T + batch_vec = np.array([vec]).T batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0] decrypted_batch = "".join( self.replace_digits(num) for num in batch_decrypted @@ -209,7 +197,7 @@ def main() -> None: row = [int(x) for x in input().split()] hill_matrix.append(row) - hc = HillCipher(numpy.array(hill_matrix)) + hc = HillCipher(np.array(hill_matrix)) print("Would you like to encrypt or decrypt some text? (1 or 2)") option = input("\n1. Encrypt\n2. Decrypt\n") diff --git a/ciphers/mixed_keyword_cypher.py b/ciphers/mixed_keyword_cypher.py index 806004faa079..1b186108a73e 100644 --- a/ciphers/mixed_keyword_cypher.py +++ b/ciphers/mixed_keyword_cypher.py @@ -1,7 +1,11 @@ -def mixed_keyword(key: str = "college", pt: str = "UNIVERSITY") -> str: - """ +from string import ascii_uppercase + - For key:hello +def mixed_keyword( + keyword: str, plaintext: str, verbose: bool = False, alphabet: str = ascii_uppercase +) -> str: + """ + For keyword: hello H E L O A B C D @@ -12,57 +16,60 @@ def mixed_keyword(key: str = "college", pt: str = "UNIVERSITY") -> str: Y Z and map vertically - >>> mixed_keyword("college", "UNIVERSITY") # doctest: +NORMALIZE_WHITESPACE + >>> mixed_keyword("college", "UNIVERSITY", True) # doctest: +NORMALIZE_WHITESPACE {'A': 'C', 'B': 'A', 'C': 'I', 'D': 'P', 'E': 'U', 'F': 'Z', 'G': 'O', 'H': 'B', 'I': 'J', 'J': 'Q', 'K': 'V', 'L': 'L', 'M': 'D', 'N': 'K', 'O': 'R', 'P': 'W', 'Q': 'E', 'R': 'F', 'S': 'M', 'T': 'S', 'U': 'X', 'V': 'G', 'W': 'H', 'X': 'N', 'Y': 'T', 'Z': 'Y'} 'XKJGUFMJST' + + >>> mixed_keyword("college", "UNIVERSITY", False) # doctest: +NORMALIZE_WHITESPACE + 'XKJGUFMJST' """ - key = key.upper() - pt = pt.upper() - temp = [] - for i in key: - if i not in temp: - temp.append(i) - len_temp = len(temp) - # print(temp) - alpha = [] - modalpha = [] - for j in range(65, 91): - t = chr(j) - alpha.append(t) - if t not in temp: - temp.append(t) - # print(temp) - r = int(26 / 4) - # print(r) - k = 0 - for _ in range(r): - s = [] - for _ in range(len_temp): - s.append(temp[k]) - if k >= 25: - break - k += 1 - modalpha.append(s) - # print(modalpha) - d = {} - j = 0 - k = 0 - for j in range(len_temp): - for m in modalpha: - if not len(m) - 1 >= j: - break - d[alpha[k]] = m[j] - if not k < 25: + keyword = keyword.upper() + plaintext = plaintext.upper() + alphabet_set = set(alphabet) + + # create a list of unique characters in the keyword - their order matters + # it determines how we will map plaintext characters to the ciphertext + unique_chars = [] + for char in keyword: + if char in alphabet_set and char not in unique_chars: + unique_chars.append(char) + # the number of those unique characters will determine the number of rows + num_unique_chars_in_keyword = len(unique_chars) + + # create a shifted version of the alphabet + shifted_alphabet = unique_chars + [ + char for char in alphabet if char not in unique_chars + ] + + # create a modified alphabet by splitting the shifted alphabet into rows + modified_alphabet = [ + shifted_alphabet[k : k + num_unique_chars_in_keyword] + for k in range(0, 26, num_unique_chars_in_keyword) + ] + + # map the alphabet characters to the modified alphabet characters + # going 'vertically' through the modified alphabet - consider columns first + mapping = {} + letter_index = 0 + for column in range(num_unique_chars_in_keyword): + for row in modified_alphabet: + # if current row (the last one) is too short, break out of loop + if len(row) <= column: break - k += 1 - print(d) - cypher = "" - for i in pt: - cypher += d[i] - return cypher + + # map current letter to letter in modified alphabet + mapping[alphabet[letter_index]] = row[column] + letter_index += 1 + + if verbose: + print(mapping) + # create the encrypted text by mapping the plaintext to the modified alphabet + return "".join(mapping.get(char, char) for char in plaintext) -print(mixed_keyword("college", "UNIVERSITY")) +if __name__ == "__main__": + # example use + print(mixed_keyword("college", "UNIVERSITY")) diff --git a/ciphers/onepad_cipher.py b/ciphers/onepad_cipher.py index 4bfe35b7180a..c4fb22e14a06 100644 --- a/ciphers/onepad_cipher.py +++ b/ciphers/onepad_cipher.py @@ -4,7 +4,27 @@ class Onepad: @staticmethod def encrypt(text: str) -> tuple[list[int], list[int]]: - """Function to encrypt text using pseudo-random numbers""" + """ + Function to encrypt text using pseudo-random numbers + >>> Onepad().encrypt("") + ([], []) + >>> Onepad().encrypt([]) + ([], []) + >>> random.seed(1) + >>> Onepad().encrypt(" ") + ([6969], [69]) + >>> random.seed(1) + >>> Onepad().encrypt("Hello") + ([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) + >>> Onepad().encrypt(1) + Traceback (most recent call last): + ... + TypeError: 'int' object is not iterable + >>> Onepad().encrypt(1.1) + Traceback (most recent call last): + ... + TypeError: 'float' object is not iterable + """ plain = [ord(i) for i in text] key = [] cipher = [] @@ -17,7 +37,20 @@ def encrypt(text: str) -> tuple[list[int], list[int]]: @staticmethod def decrypt(cipher: list[int], key: list[int]) -> str: - """Function to decrypt text using pseudo-random numbers.""" + """ + Function to decrypt text using pseudo-random numbers. + >>> Onepad().decrypt([], []) + '' + >>> Onepad().decrypt([35], []) + '' + >>> Onepad().decrypt([], [35]) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> random.seed(1) + >>> Onepad().decrypt([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) + 'Hello' + """ plain = [] for i in range(len(key)): p = int((cipher[i] - (key[i]) ** 2) / key[i]) diff --git a/ciphers/permutation_cipher.py b/ciphers/permutation_cipher.py new file mode 100644 index 000000000000..9e1c64a7b4ea --- /dev/null +++ b/ciphers/permutation_cipher.py @@ -0,0 +1,143 @@ +""" +The permutation cipher, also called the transposition cipher, is a simple encryption +technique that rearranges the characters in a message based on a secret key. It +divides the message into blocks and applies a permutation to the characters within +each block according to the key. The key is a sequence of unique integers that +determine the order of character rearrangement. + +For more info: https://www.nku.edu/~christensen/1402%20permutation%20ciphers.pdf +""" + +import random + + +def generate_valid_block_size(message_length: int) -> int: + """ + Generate a valid block size that is a factor of the message length. + + Args: + message_length (int): The length of the message. + + Returns: + int: A valid block size. + + Example: + >>> random.seed(1) + >>> generate_valid_block_size(12) + 3 + """ + block_sizes = [ + block_size + for block_size in range(2, message_length + 1) + if message_length % block_size == 0 + ] + return random.choice(block_sizes) + + +def generate_permutation_key(block_size: int) -> list[int]: + """ + Generate a random permutation key of a specified block size. + + Args: + block_size (int): The size of each permutation block. + + Returns: + list[int]: A list containing a random permutation of digits. + + Example: + >>> random.seed(0) + >>> generate_permutation_key(4) + [2, 0, 1, 3] + """ + digits = list(range(block_size)) + random.shuffle(digits) + return digits + + +def encrypt( + message: str, key: list[int] | None = None, block_size: int | None = None +) -> tuple[str, list[int]]: + """ + Encrypt a message using a permutation cipher with block rearrangement using a key. + + Args: + message (str): The plaintext message to be encrypted. + key (list[int]): The permutation key for decryption. + block_size (int): The size of each permutation block. + + Returns: + tuple: A tuple containing the encrypted message and the encryption key. + + Example: + >>> encrypted_message, key = encrypt("HELLO WORLD") + >>> decrypted_message = decrypt(encrypted_message, key) + >>> decrypted_message + 'HELLO WORLD' + """ + message = message.upper() + message_length = len(message) + + if key is None or block_size is None: + block_size = generate_valid_block_size(message_length) + key = generate_permutation_key(block_size) + + encrypted_message = "" + + for i in range(0, message_length, block_size): + block = message[i : i + block_size] + rearranged_block = [block[digit] for digit in key] + encrypted_message += "".join(rearranged_block) + + return encrypted_message, key + + +def decrypt(encrypted_message: str, key: list[int]) -> str: + """ + Decrypt an encrypted message using a permutation cipher with block rearrangement. + + Args: + encrypted_message (str): The encrypted message. + key (list[int]): The permutation key for decryption. + + Returns: + str: The decrypted plaintext message. + + Example: + >>> encrypted_message, key = encrypt("HELLO WORLD") + >>> decrypted_message = decrypt(encrypted_message, key) + >>> decrypted_message + 'HELLO WORLD' + """ + key_length = len(key) + decrypted_message = "" + + for i in range(0, len(encrypted_message), key_length): + block = encrypted_message[i : i + key_length] + original_block = [""] * key_length + for j, digit in enumerate(key): + original_block[digit] = block[j] + decrypted_message += "".join(original_block) + + return decrypted_message + + +def main() -> None: + """ + Driver function to pass message to get encrypted, then decrypted. + + Example: + >>> main() + Decrypted message: HELLO WORLD + """ + message = "HELLO WORLD" + encrypted_message, key = encrypt(message) + + decrypted_message = decrypt(encrypted_message, key) + print(f"Decrypted message: {decrypted_message}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/playfair_cipher.py b/ciphers/playfair_cipher.py index 7279fb23ecb2..d48f113f02e0 100644 --- a/ciphers/playfair_cipher.py +++ b/ciphers/playfair_cipher.py @@ -1,9 +1,30 @@ +""" +https://en.wikipedia.org/wiki/Playfair_cipher#Description + +The Playfair cipher was developed by Charles Wheatstone in 1854 +It's use was heavily promotedby Lord Playfair, hence its name + +Some features of the Playfair cipher are: + +1) It was the first literal diagram substitution cipher +2) It is a manual symmetric encryption technique +3) It is a multiple letter encryption cipher + +The implementation in the code below encodes alphabets only. +It removes spaces, special characters and numbers from the +code. + +Playfair is no longer used by military forces because of known +insecurities and of the advent of automated encryption devices. +This cipher is regarded as insecure since before World War I. +""" + import itertools import string from collections.abc import Generator, Iterable -def chunker(seq: Iterable[str], size: int) -> Generator[tuple[str, ...], None, None]: +def chunker(seq: Iterable[str], size: int) -> Generator[tuple[str, ...]]: it = iter(seq) while True: chunk = tuple(itertools.islice(it, size)) @@ -60,11 +81,26 @@ def generate_table(key: str) -> list[str]: def encode(plaintext: str, key: str) -> str: + """ + Encode the given plaintext using the Playfair cipher. + Takes the plaintext and the key as input and returns the encoded string. + + >>> encode("Hello", "MONARCHY") + 'CFSUPM' + >>> encode("attack on the left flank", "EMERGENCY") + 'DQZSBYFSDZFMFNLOHFDRSG' + >>> encode("Sorry!", "SPECIAL") + 'AVXETX' + >>> encode("Number 1", "NUMBER") + 'UMBENF' + >>> encode("Photosynthesis!", "THE SUN") + 'OEMHQHVCHESUKE' + """ + table = generate_table(key) plaintext = prepare_input(plaintext) ciphertext = "" - # https://en.wikipedia.org/wiki/Playfair_cipher#Description for char1, char2 in chunker(plaintext, 2): row1, col1 = divmod(table.index(char1), 5) row2, col2 = divmod(table.index(char2), 5) @@ -83,10 +119,20 @@ def encode(plaintext: str, key: str) -> str: def decode(ciphertext: str, key: str) -> str: + """ + Decode the input string using the provided key. + + >>> decode("BMZFAZRZDH", "HAZARD") + 'FIREHAZARD' + >>> decode("HNBWBPQT", "AUTOMOBILE") + 'DRIVINGX' + >>> decode("SLYSSAQS", "CASTLE") + 'ATXTACKX' + """ + table = generate_table(key) plaintext = "" - # https://en.wikipedia.org/wiki/Playfair_cipher#Description for char1, char2 in chunker(ciphertext, 2): row1, col1 = divmod(table.index(char1), 5) row2, col2 = divmod(table.index(char2), 5) @@ -102,3 +148,12 @@ def decode(ciphertext: str, key: str) -> str: plaintext += table[row2 * 5 + col1] return plaintext + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("Encoded:", encode("BYE AND THANKS", "GREETING")) + print("Decoded:", decode("CXRBANRLBALQ", "GREETING")) diff --git a/ciphers/polybius.py b/ciphers/polybius.py index 3539ab70c303..d83badf4ac0a 100644 --- a/ciphers/polybius.py +++ b/ciphers/polybius.py @@ -31,7 +31,7 @@ def letter_to_numbers(self, letter: str) -> np.ndarray: >>> np.array_equal(PolybiusCipher().letter_to_numbers('u'), [4,5]) True """ - index1, index2 = np.where(self.SQUARE == letter) + index1, index2 = np.where(letter == self.SQUARE) indexes = np.concatenate([index1 + 1, index2 + 1]) return indexes diff --git a/ciphers/prehistoric_men.txt b/ciphers/prehistoric_men.txt index a58e533a8405..8d1b2bd8c8d1 100644 --- a/ciphers/prehistoric_men.txt +++ b/ciphers/prehistoric_men.txt @@ -40,8 +40,8 @@ Transcriber's note: version referred to above. One example of this might occur in the second paragraph under "Choppers and Adze-like Tools", page 46, which contains the phrase - “an adze cutting edge is ? shaped”. The symbol before - “shaped” looks like a sharply-italicized sans-serif “L”. + �an adze cutting edge is ? shaped�. The symbol before + �shaped� looks like a sharply-italicized sans-serif �L�. Devices that cannot display that symbol may substitute a question mark, a square, or other symbol. @@ -98,7 +98,7 @@ forced or pedantic; at least I have done my very best to tell the story simply and clearly. Many friends have aided in the preparation of the book. The whimsical -charm of Miss Susan Richert’s illustrations add enormously to the +charm of Miss Susan Richert�s illustrations add enormously to the spirit I wanted. She gave freely of her own time on the drawings and in planning the book with me. My colleagues at the University of Chicago, especially Professor Wilton M. Krogman (now of the University @@ -108,7 +108,7 @@ the Department of Anthropology, gave me counsel in matters bearing on their special fields, and the Department of Anthropology bore some of the expense of the illustrations. From Mrs. Irma Hunter and Mr. Arnold Maremont, who are not archeologists at all and have only an intelligent -layman’s notion of archeology, I had sound advice on how best to tell +layman�s notion of archeology, I had sound advice on how best to tell the story. I am deeply indebted to all these friends. While I was preparing the second edition, I had the great fortune @@ -117,13 +117,13 @@ Washburn, now of the Department of Anthropology of the University of California, and the fourth, fifth, and sixth chapters with Professor Hallum L. Movius, Jr., of the Peabody Museum, Harvard University. The book has gained greatly in accuracy thereby. In matters of dating, -Professor Movius and the indications of Professor W. F. Libby’s Carbon +Professor Movius and the indications of Professor W. F. Libby�s Carbon 14 chronology project have both encouraged me to choose the lowest dates now current for the events of the Pleistocene Ice Age. There is still no certain way of fixing a direct chronology for most of the -Pleistocene, but Professor Libby’s method appears very promising for +Pleistocene, but Professor Libby�s method appears very promising for its end range and for proto-historic dates. In any case, this book -names “periods,” and new dates may be written in against mine, if new +names �periods,� and new dates may be written in against mine, if new and better dating systems appear. I wish to thank Dr. Clifford C. Gregg, Director of Chicago Natural @@ -150,7 +150,7 @@ Clark Howell of the Department of Anthropology of the University of Chicago in reworking the earlier chapters, and he was very patient in the matter, which I sincerely appreciate. -All of Mrs. Susan Richert Allen’s original drawings appear, but a few +All of Mrs. Susan Richert Allen�s original drawings appear, but a few necessary corrections have been made in some of the charts and some new drawings have been added by Mr. John Pfiffner, Staff Artist, Chicago Natural History Museum. @@ -200,7 +200,7 @@ HOW WE LEARN about Prehistoric Men Prehistory means the time before written history began. Actually, more -than 99 per cent of man’s story is prehistory. Man is at least half a +than 99 per cent of man�s story is prehistory. Man is at least half a million years old, but he did not begin to write history (or to write anything) until about 5,000 years ago. @@ -216,7 +216,7 @@ The scientists who study the bones and teeth and any other parts they find of the bodies of prehistoric men, are called _physical anthropologists_. Physical anthropologists are trained, much like doctors, to know all about the human body. They study living people, -too; they know more about the biological facts of human “races” than +too; they know more about the biological facts of human �races� than anybody else. If the police find a badly decayed body in a trunk, they ask a physical anthropologist to tell them what the person originally looked like. The physical anthropologists who specialize in @@ -228,14 +228,14 @@ ARCHEOLOGISTS There is a kind of scientist who studies the things that prehistoric men made and did. Such a scientist is called an _archeologist_. It is -the archeologist’s business to look for the stone and metal tools, the +the archeologist�s business to look for the stone and metal tools, the pottery, the graves, and the caves or huts of the men who lived before history began. But there is more to archeology than just looking for things. In -Professor V. Gordon Childe’s words, archeology “furnishes a sort of +Professor V. Gordon Childe�s words, archeology �furnishes a sort of history of human activity, provided always that the actions have -produced concrete results and left recognizable material traces.” You +produced concrete results and left recognizable material traces.� You will see that there are at least three points in what Childe says: 1. The archeologists have to find the traces of things left behind by @@ -245,7 +245,7 @@ will see that there are at least three points in what Childe says: too soft or too breakable to last through the years. However, 3. The archeologist must use whatever he can find to tell a story--to - make a “sort of history”--from the objects and living-places and + make a �sort of history�--from the objects and living-places and graves that have escaped destruction. What I mean is this: Let us say you are walking through a dump yard, @@ -253,8 +253,8 @@ and you find a rusty old spark plug. If you want to think about what the spark plug means, you quickly remember that it is a part of an automobile motor. This tells you something about the man who threw the spark plug on the dump. He either had an automobile, or he knew -or lived near someone who did. He can’t have lived so very long ago, -you’ll remember, because spark plugs and automobiles are only about +or lived near someone who did. He can�t have lived so very long ago, +you�ll remember, because spark plugs and automobiles are only about sixty years old. When you think about the old spark plug in this way you have @@ -264,8 +264,8 @@ It is the same way with the man-made things we archeologists find and put in museums. Usually, only a few of these objects are pretty to look at; but each of them has some sort of story to tell. Making the interpretation of his finds is the most important part of the -archeologist’s job. It is the way he gets at the “sort of history of -human activity” which is expected of archeology. +archeologist�s job. It is the way he gets at the �sort of history of +human activity� which is expected of archeology. SOME OTHER SCIENTISTS @@ -274,7 +274,7 @@ There are many other scientists who help the archeologist and the physical anthropologist find out about prehistoric men. The geologists help us tell the age of the rocks or caves or gravel beds in which human bones or man-made objects are found. There are other scientists -with names which all begin with “paleo” (the Greek word for “old”). The +with names which all begin with �paleo� (the Greek word for �old�). The _paleontologists_ study fossil animals. There are also, for example, such scientists as _paleobotanists_ and _paleoclimatologists_, who study ancient plants and climates. These scientists help us to know @@ -306,20 +306,20 @@ systems. The rate of disappearance of radioactivity as time passes.[1]] [1] It is important that the limitations of the radioactive carbon - “dating” system be held in mind. As the statistics involved in + �dating� system be held in mind. As the statistics involved in the system are used, there are two chances in three that the - “date” of the sample falls within the range given as plus or - minus an added number of years. For example, the “date” for the - Jarmo village (see chart), given as 6750 ± 200 B.C., really + �date� of the sample falls within the range given as plus or + minus an added number of years. For example, the �date� for the + Jarmo village (see chart), given as 6750 � 200 B.C., really means that there are only two chances in three that the real date of the charcoal sampled fell between 6950 and 6550 B.C. We have also begun to suspect that there are ways in which the - samples themselves may have become “contaminated,” either on + samples themselves may have become �contaminated,� either on the early or on the late side. We now tend to be suspicious of single radioactive carbon determinations, or of determinations from one site alone. But as a fabric of consistent determinations for several or more sites of one archeological - period, we gain confidence in the “dates.” + period, we gain confidence in the dates. HOW THE SCIENTISTS FIND OUT @@ -330,9 +330,9 @@ about prehistoric men. We also need a word about _how_ they find out. All our finds came by accident until about a hundred years ago. Men digging wells, or digging in caves for fertilizer, often turned up ancient swords or pots or stone arrowheads. People also found some odd -pieces of stone that didn’t look like natural forms, but they also -didn’t look like any known tool. As a result, the people who found them -gave them queer names; for example, “thunderbolts.” The people thought +pieces of stone that didn�t look like natural forms, but they also +didn�t look like any known tool. As a result, the people who found them +gave them queer names; for example, �thunderbolts.� The people thought the strange stones came to earth as bolts of lightning. We know now that these strange stones were prehistoric stone tools. @@ -349,7 +349,7 @@ story of cave men on Mount Carmel, in Palestine, began to be known. Planned archeological digging is only about a century old. Even before this, however, a few men realized the significance of objects they dug from the ground; one of these early archeologists was our own Thomas -Jefferson. The first real mound-digger was a German grocer’s clerk, +Jefferson. The first real mound-digger was a German grocer�s clerk, Heinrich Schliemann. Schliemann made a fortune as a merchant, first in Europe and then in the California gold-rush of 1849. He became an American citizen. Then he retired and had both money and time to test @@ -389,16 +389,16 @@ used had been a soft, unbaked mud-brick, and most of the debris consisted of fallen or rain-melted mud from these mud-bricks. This idea of _stratification_, like the cake layers, was already a -familiar one to the geologists by Schliemann’s time. They could show +familiar one to the geologists by Schliemann�s time. They could show that their lowest layer of rock was oldest or earliest, and that the -overlying layers became more recent as one moved upward. Schliemann’s +overlying layers became more recent as one moved upward. Schliemann�s digging proved the same thing at Troy. His first (lowest and earliest) city had at least nine layers above it; he thought that the second -layer contained the remains of Homer’s Troy. We now know that Homeric +layer contained the remains of Homer�s Troy. We now know that Homeric Troy was layer VIIa from the bottom; also, we count eleven layers or sub-layers in total. -Schliemann’s work marks the beginnings of modern archeology. Scholars +Schliemann�s work marks the beginnings of modern archeology. Scholars soon set out to dig on ancient sites, from Egypt to Central America. @@ -410,21 +410,21 @@ Archeologists began to get ideas as to the kinds of objects that belonged together. If you compared a mail-order catalogue of 1890 with one of today, you would see a lot of differences. If you really studied the two catalogues hard, you would also begin to see that certain -objects “go together.” Horseshoes and metal buggy tires and pieces of +objects �go together.� Horseshoes and metal buggy tires and pieces of harness would begin to fit into a picture with certain kinds of coal stoves and furniture and china dishes and kerosene lamps. Our friend the spark plug, and radios and electric refrigerators and light bulbs would fit into a picture with different kinds of furniture and dishes -and tools. You won’t be old enough to remember the kind of hats that -women wore in 1890, but you’ve probably seen pictures of them, and you -know very well they couldn’t be worn with the fashions of today. +and tools. You won�t be old enough to remember the kind of hats that +women wore in 1890, but you�ve probably seen pictures of them, and you +know very well they couldn�t be worn with the fashions of today. This is one of the ways that archeologists study their materials. The various tools and weapons and jewelry, the pottery, the kinds of houses, and even the ways of burying the dead tend to fit into pictures. Some archeologists call all of the things that go together to make such a picture an _assemblage_. The assemblage of the first layer -of Schliemann’s Troy was as different from that of the seventh layer as +of Schliemann�s Troy was as different from that of the seventh layer as our 1900 mail-order catalogue is from the one of today. The archeologists who came after Schliemann began to notice other @@ -433,23 +433,23 @@ idea that people will buy better mousetraps goes back into very ancient times. Today, if we make good automobiles or radios, we can sell some of them in Turkey or even in Timbuktu. This means that a few present-day types of American automobiles and radios form part -of present-day “assemblages” in both Turkey and Timbuktu. The total -present-day “assemblage” of Turkey is quite different from that of +of present-day �assemblages� in both Turkey and Timbuktu. The total +present-day �assemblage� of Turkey is quite different from that of Timbuktu or that of America, but they have at least some automobiles and some radios in common. Now these automobiles and radios will eventually wear out. Let us suppose we could go to some remote part of Turkey or to Timbuktu in a -dream. We don’t know what the date is, in our dream, but we see all +dream. We don�t know what the date is, in our dream, but we see all sorts of strange things and ways of living in both places. Nobody tells us what the date is. But suddenly we see a 1936 Ford; so we know that in our dream it has to be at least the year 1936, and only as many years after that as we could reasonably expect a Ford to keep -in running order. The Ford would probably break down in twenty years’ -time, so the Turkish or Timbuktu “assemblage” we’re seeing in our dream +in running order. The Ford would probably break down in twenty years� +time, so the Turkish or Timbuktu �assemblage� we�re seeing in our dream has to date at about A.D. 1936-56. -Archeologists not only “date” their ancient materials in this way; they +Archeologists not only �date� their ancient materials in this way; they also see over what distances and between which peoples trading was done. It turns out that there was a good deal of trading in ancient times, probably all on a barter and exchange basis. @@ -480,13 +480,13 @@ site. They find the remains of everything that would last through time, in several different layers. They know that the assemblage in the bottom layer was laid down earlier than the assemblage in the next layer above, and so on up to the topmost layer, which is the latest. -They look at the results of other “digs” and find that some other +They look at the results of other �digs� and find that some other archeologist 900 miles away has found ax-heads in his lowest layer, exactly like the ax-heads of their fifth layer. This means that their fifth layer must have been lived in at about the same time as was the first layer in the site 200 miles away. It also may mean that the people who lived in the two layers knew and traded with each other. Or -it could mean that they didn’t necessarily know each other, but simply +it could mean that they didn�t necessarily know each other, but simply that both traded with a third group at about the same time. You can see that the more we dig and find, the more clearly the main @@ -501,8 +501,8 @@ those of domesticated animals, for instance, sheep or cattle, and therefore the people must have kept herds. More important than anything else--as our structure grows more -complicated and our materials increase--is the fact that “a sort -of history of human activity” does begin to appear. The habits or +complicated and our materials increase--is the fact that �a sort +of history of human activity� does begin to appear. The habits or traditions that men formed in the making of their tools and in the ways they did things, begin to stand out for us. How characteristic were these habits and traditions? What areas did they spread over? @@ -519,7 +519,7 @@ method--chemical tests of the bones--that will enable them to discover what the blood-type may have been. One thing is sure. We have never found a group of skeletons so absolutely similar among themselves--so cast from a single mould, so to speak--that we could claim to have a -“pure” race. I am sure we never shall. +�pure� race. I am sure we never shall. We become particularly interested in any signs of change--when new materials and tool types and ways of doing things replace old ones. We @@ -527,7 +527,7 @@ watch for signs of social change and progress in one way or another. We must do all this without one word of written history to aid us. Everything we are concerned with goes back to the time _before_ men -learned to write. That is the prehistorian’s job--to find out what +learned to write. That is the prehistorian�s job--to find out what happened before history began. @@ -538,9 +538,9 @@ THE CHANGING WORLD in which Prehistoric Men Lived [Illustration] -Mankind, we’ll say, is at least a half million years old. It is very +Mankind, we�ll say, is at least a half million years old. It is very hard to understand how long a time half a million years really is. -If we were to compare this whole length of time to one day, we’d get +If we were to compare this whole length of time to one day, we�d get something like this: The present time is midnight, and Jesus was born just five minutes and thirty-six seconds ago. Earliest history began less than fifteen minutes ago. Everything before 11:45 was in @@ -569,7 +569,7 @@ book; it would mainly affect the dates earlier than 25,000 years ago. CHANGES IN ENVIRONMENT -The earth probably hasn’t changed much in the last 5,000 years (250 +The earth probably hasn�t changed much in the last 5,000 years (250 generations). Men have built things on its surface and dug into it and drawn boundaries on maps of it, but the places where rivers, lakes, seas, and mountains now stand have changed very little. @@ -605,7 +605,7 @@ the glaciers covered most of Canada and the northern United States and reached down to southern England and France in Europe. Smaller ice sheets sat like caps on the Rockies, the Alps, and the Himalayas. The continental glaciation only happened north of the equator, however, so -remember that “Ice Age” is only half true. +remember that �Ice Age� is only half true. As you know, the amount of water on and about the earth does not vary. These large glaciers contained millions of tons of water frozen into @@ -677,9 +677,9 @@ their dead. At about the time when the last great glacier was finally melting away, men in the Near East made the first basic change in human economy. They began to plant grain, and they learned to raise and herd certain -animals. This meant that they could store food in granaries and “on the -hoof” against the bad times of the year. This first really basic change -in man’s way of living has been called the “food-producing revolution.” +animals. This meant that they could store food in granaries and �on the +hoof� against the bad times of the year. This first really basic change +in man�s way of living has been called the �food-producing revolution.� By the time it happened, a modern kind of climate was beginning. Men had already grown to look as they do now. Know-how in ways of living had developed and progressed, slowly but surely, up to a point. It was @@ -698,25 +698,25 @@ Prehistoric Men THEMSELVES DO WE KNOW WHERE MAN ORIGINATED? -For a long time some scientists thought the “cradle of mankind” was in +For a long time some scientists thought the �cradle of mankind� was in central Asia. Other scientists insisted it was in Africa, and still -others said it might have been in Europe. Actually, we don’t know -where it was. We don’t even know that there was only _one_ “cradle.” -If we had to choose a “cradle” at this moment, we would probably say +others said it might have been in Europe. Actually, we don�t know +where it was. We don�t even know that there was only _one_ �cradle.� +If we had to choose a �cradle� at this moment, we would probably say Africa. But the southern portions of Asia and Europe may also have been included in the general area. The scene of the early development of -mankind was certainly the Old World. It is pretty certain men didn’t +mankind was certainly the Old World. It is pretty certain men didn�t reach North or South America until almost the end of the Ice Age--had they done so earlier we would certainly have found some trace of them by now. The earliest tools we have yet found come from central and south -Africa. By the dating system I’m using, these tools must be over +Africa. By the dating system I�m using, these tools must be over 500,000 years old. There are now reports that a few such early tools have been found--at the Sterkfontein cave in South Africa--along with -the bones of small fossil men called “australopithecines.” +the bones of small fossil men called �australopithecines.� -Not all scientists would agree that the australopithecines were “men,” +Not all scientists would agree that the australopithecines were �men,� or would agree that the tools were made by the australopithecines themselves. For these sticklers, the earliest bones of men come from the island of Java. The date would be about 450,000 years ago. So far, @@ -727,12 +727,12 @@ Let me say it another way. How old are the earliest traces of men we now have? Over half a million years. This was a time when the first alpine glaciation was happening in the north. What has been found so far? The tools which the men of those times made, in different parts -of Africa. It is now fairly generally agreed that the “men” who made -the tools were the australopithecines. There is also a more “man-like” +of Africa. It is now fairly generally agreed that the �men� who made +the tools were the australopithecines. There is also a more �man-like� jawbone at Kanam in Kenya, but its find-spot has been questioned. The next earliest bones we have were found in Java, and they may be almost a hundred thousand years younger than the earliest African finds. We -haven’t yet found the tools of these early Javanese. Our knowledge of +haven�t yet found the tools of these early Javanese. Our knowledge of tool-using in Africa spreads quickly as time goes on: soon after the appearance of tools in the south we shall have them from as far north as Algeria. @@ -758,30 +758,30 @@ prove it. MEN AND APES Many people used to get extremely upset at the ill-formed notion -that “man descended from the apes.” Such words were much more likely -to start fights or “monkey trials” than the correct notion that all +that �man descended from the apes.� Such words were much more likely +to start fights or �monkey trials� than the correct notion that all living animals, including man, ascended or evolved from a single-celled organism which lived in the primeval seas hundreds of millions of years -ago. Men are mammals, of the order called Primates, and man’s living -relatives are the great apes. Men didn’t “descend” from the apes or +ago. Men are mammals, of the order called Primates, and man�s living +relatives are the great apes. Men didn�t �descend� from the apes or apes from men, and mankind must have had much closer relatives who have since become extinct. Men stand erect. They also walk and run on their two feet. Apes are happiest in trees, swinging with their arms from branch to branch. Few branches of trees will hold the mighty gorilla, although he still -manages to sleep in trees. Apes can’t stand really erect in our sense, +manages to sleep in trees. Apes can�t stand really erect in our sense, and when they have to run on the ground, they use the knuckles of their hands as well as their feet. A key group of fossil bones here are the south African australopithecines. These are called the _Australopithecinae_ or -“man-apes” or sometimes even “ape-men.” We do not _know_ that they were +�man-apes� or sometimes even �ape-men.� We do not _know_ that they were directly ancestral to men but they can hardly have been so to apes. -Presently I’ll describe them a bit more. The reason I mention them +Presently I�ll describe them a bit more. The reason I mention them here is that while they had brains no larger than those of apes, their hipbones were enough like ours so that they must have stood erect. -There is no good reason to think they couldn’t have walked as we do. +There is no good reason to think they couldn�t have walked as we do. BRAINS, HANDS, AND TOOLS @@ -801,12 +801,12 @@ Nobody knows which of these three is most important, or which came first. Most probably the growth of all three things was very much blended together. If you think about each of the things, you will see what I mean. Unless your hand is more flexible than a paw, and your -thumb will work against (or oppose) your fingers, you can’t hold a tool -very well. But you wouldn’t get the idea of using a tool unless you had +thumb will work against (or oppose) your fingers, you can�t hold a tool +very well. But you wouldn�t get the idea of using a tool unless you had enough brain to help you see cause and effect. And it is rather hard to see how your hand and brain would develop unless they had something to -practice on--like using tools. In Professor Krogman’s words, “the hand -must become the obedient servant of the eye and the brain.” It is the +practice on--like using tools. In Professor Krogman�s words, �the hand +must become the obedient servant of the eye and the brain.� It is the _co-ordination_ of these things that counts. Many other things must have been happening to the bodies of the @@ -820,17 +820,17 @@ little by little, all together. Men became men very slowly. WHEN SHALL WE CALL MEN MEN? -What do I mean when I say “men”? People who looked pretty much as we +What do I mean when I say �men�? People who looked pretty much as we do, and who used different tools to do different things, are men to me. -We’ll probably never know whether the earliest ones talked or not. They +We�ll probably never know whether the earliest ones talked or not. They probably had vocal cords, so they could make sounds, but did they know how to make sounds work as symbols to carry meanings? But if the fossil -bones look like our skeletons, and if we find tools which we’ll agree -couldn’t have been made by nature or by animals, then I’d say we had +bones look like our skeletons, and if we find tools which we�ll agree +couldn�t have been made by nature or by animals, then I�d say we had traces of _men_. The australopithecine finds of the Transvaal and Bechuanaland, in -south Africa, are bound to come into the discussion here. I’ve already +south Africa, are bound to come into the discussion here. I�ve already told you that the australopithecines could have stood upright and walked on their two hind legs. They come from the very base of the Pleistocene or Ice Age, and a few coarse stone tools have been found @@ -848,17 +848,17 @@ bones. The doubt as to whether the australopithecines used the tools themselves goes like this--just suppose some man-like creature (whose bones we have not yet found) made the tools and used them to kill and butcher australopithecines. Hence a few experts tend to let -australopithecines still hang in limbo as “man-apes.” +australopithecines still hang in limbo as �man-apes.� THE EARLIEST MEN WE KNOW -I’ll postpone talking about the tools of early men until the next +I�ll postpone talking about the tools of early men until the next chapter. The men whose bones were the earliest of the Java lot have been given the name _Meganthropus_. The bones are very fragmentary. We would not understand them very well unless we had the somewhat later -Javanese lot--the more commonly known _Pithecanthropus_ or “Java -man”--against which to refer them for study. One of the less well-known +Javanese lot--the more commonly known _Pithecanthropus_ or �Java +man�--against which to refer them for study. One of the less well-known and earliest fragments, a piece of lower jaw and some teeth, rather strongly resembles the lower jaws and teeth of the australopithecine type. Was _Meganthropus_ a sort of half-way point between the @@ -872,7 +872,7 @@ finds of Java man were made in 1891-92 by Dr. Eugene Dubois, a Dutch doctor in the colonial service. Finds have continued to be made. There are now bones enough to account for four skulls. There are also four jaws and some odd teeth and thigh bones. Java man, generally speaking, -was about five feet six inches tall, and didn’t hold his head very +was about five feet six inches tall, and didn�t hold his head very erect. His skull was very thick and heavy and had room for little more than two-thirds as large a brain as we have. He had big teeth and a big jaw and enormous eyebrow ridges. @@ -885,22 +885,22 @@ belonged to his near descendants. Remember that there are several varieties of men in the whole early Java lot, at least two of which are earlier than the _Pithecanthropus_, -“Java man.” Some of the earlier ones seem to have gone in for +�Java man.� Some of the earlier ones seem to have gone in for bigness, in tooth-size at least. _Meganthropus_ is one of these earlier varieties. As we said, he _may_ turn out to be a link to the australopithecines, who _may_ or _may not_ be ancestral to men. _Meganthropus_ is best understandable in terms of _Pithecanthropus_, who appeared later in the same general area. _Pithecanthropus_ is pretty well understandable from the bones he left us, and also because -of his strong resemblance to the fully tool-using cave-dwelling “Peking -man,” _Sinanthropus_, about whom we shall talk next. But you can see +of his strong resemblance to the fully tool-using cave-dwelling �Peking +man,� _Sinanthropus_, about whom we shall talk next. But you can see that the physical anthropologists and prehistoric archeologists still have a lot of work to do on the problem of earliest men. PEKING MEN AND SOME EARLY WESTERNERS -The earliest known Chinese are called _Sinanthropus_, or “Peking man,” +The earliest known Chinese are called _Sinanthropus_, or �Peking man,� because the finds were made near that city. In World War II, the United States Marine guard at our Embassy in Peking tried to help get the bones out of the city before the Japanese attack. Nobody knows where @@ -913,9 +913,9 @@ casts of the bones. Peking man lived in a cave in a limestone hill, made tools, cracked animal bones to get the marrow out, and used fire. Incidentally, the bones of Peking man were found because Chinese dig for what they call -“dragon bones” and “dragon teeth.” Uneducated Chinese buy these things +�dragon bones� and �dragon teeth.� Uneducated Chinese buy these things in their drug stores and grind them into powder for medicine. The -“dragon teeth” and “bones” are really fossils of ancient animals, and +�dragon teeth� and �bones� are really fossils of ancient animals, and sometimes of men. The people who supply the drug stores have learned where to dig for strange bones and teeth. Paleontologists who get to China go to the drug stores to buy fossils. In a roundabout way, this @@ -924,7 +924,7 @@ is how the fallen-in cave of Peking man at Choukoutien was discovered. Peking man was not quite as tall as Java man but he probably stood straighter. His skull looked very much like that of the Java skull except that it had room for a slightly larger brain. His face was less -brutish than was Java man’s face, but this isn’t saying much. +brutish than was Java man�s face, but this isn�t saying much. Peking man dates from early in the interglacial period following the second alpine glaciation. He probably lived close to 350,000 years @@ -946,9 +946,9 @@ big ridges over the eyes. The more fragmentary skull from Swanscombe in England (p. 11) has been much more carefully studied. Only the top and back of that skull have been found. Since the skull rounds up nicely, it has been assumed that the face and forehead must have been quite -“modern.” Careful comparison with Steinheim shows that this was not +�modern.� Careful comparison with Steinheim shows that this was not necessarily so. This is important because it bears on the question of -how early truly “modern” man appeared. +how early truly �modern� man appeared. Recently two fragmentary jaws were found at Ternafine in Algeria, northwest Africa. They look like the jaws of Peking man. Tools were @@ -971,22 +971,22 @@ modern Australian natives. During parts of the Ice Age there was a land bridge all the way from Java to Australia. -TWO ENGLISHMEN WHO WEREN’T OLD +TWO ENGLISHMEN WHO WEREN�T OLD The older textbooks contain descriptions of two English finds which were thought to be very old. These were called Piltdown (_Eoanthropus dawsoni_) and Galley Hill. The skulls were very modern in appearance. In 1948-49, British scientists began making chemical tests which proved that neither of these finds is very old. It is now known that both -“Piltdown man” and the tools which were said to have been found with +�Piltdown man� and the tools which were said to have been found with him were part of an elaborate fake! -TYPICAL “CAVE MEN” +TYPICAL �CAVE MEN� The next men we have to talk about are all members of a related group. -These are the Neanderthal group. “Neanderthal man” himself was found in -the Neander Valley, near Düsseldorf, Germany, in 1856. He was the first +These are the Neanderthal group. �Neanderthal man� himself was found in +the Neander Valley, near D�sseldorf, Germany, in 1856. He was the first human fossil to be recognized as such. [Illustration: PRINCIPAL KNOWN TYPES OF FOSSIL MEN @@ -999,7 +999,7 @@ human fossil to be recognized as such. PITHECANTHROPUS] Some of us think that the neanderthaloids proper are only those people -of western Europe who didn’t get out before the beginning of the last +of western Europe who didn�t get out before the beginning of the last great glaciation, and who found themselves hemmed in by the glaciers in the Alps and northern Europe. Being hemmed in, they intermarried a bit too much and developed into a special type. Professor F. Clark @@ -1010,7 +1010,7 @@ pre-neanderthaloids. There are traces of these pre-neanderthaloids pretty much throughout Europe during the third interglacial period--say 100,000 years ago. The pre-neanderthaloids are represented by such finds as the ones at Ehringsdorf in Germany and Saccopastore in Italy. -I won’t describe them for you, since they are simply less extreme than +I won�t describe them for you, since they are simply less extreme than the neanderthaloids proper--about half way between Steinheim and the classic Neanderthal people. @@ -1019,24 +1019,24 @@ get caught in the pocket of the southwest corner of Europe at the onset of the last great glaciation became the classic Neanderthalers. Out in the Near East, Howell thinks, it is possible to see traces of people evolving from the pre-neanderthaloid type toward that of fully modern -man. Certainly, we don’t see such extreme cases of “neanderthaloidism” +man. Certainly, we don�t see such extreme cases of �neanderthaloidism� outside of western Europe. There are at least a dozen good examples in the main or classic Neanderthal group in Europe. They date to just before and in the earlier part of the last great glaciation (85,000 to 40,000 years ago). -Many of the finds have been made in caves. The “cave men” the movies +Many of the finds have been made in caves. The �cave men� the movies and the cartoonists show you are probably meant to be Neanderthalers. -I’m not at all sure they dragged their women by the hair; the women +I�m not at all sure they dragged their women by the hair; the women were probably pretty tough, too! Neanderthal men had large bony heads, but plenty of room for brains. Some had brain cases even larger than the average for modern man. Their faces were heavy, and they had eyebrow ridges of bone, but the ridges were not as big as those of Java man. Their foreheads were very low, -and they didn’t have much chin. They were about five feet three inches -tall, but were heavy and barrel-chested. But the Neanderthalers didn’t -slouch as much as they’ve been blamed for, either. +and they didn�t have much chin. They were about five feet three inches +tall, but were heavy and barrel-chested. But the Neanderthalers didn�t +slouch as much as they�ve been blamed for, either. One important thing about the Neanderthal group is that there is a fair number of them to study. Just as important is the fact that we know @@ -1059,10 +1059,10 @@ different-looking people. EARLY MODERN MEN -How early is modern man (_Homo sapiens_), the “wise man”? Some people +How early is modern man (_Homo sapiens_), the �wise man�? Some people have thought that he was very early, a few still think so. Piltdown and Galley Hill, which were quite modern in anatomical appearance and -_supposedly_ very early in date, were the best “evidence” for very +_supposedly_ very early in date, were the best �evidence� for very early modern men. Now that Piltdown has been liquidated and Galley Hill is known to be very late, what is left of the idea? @@ -1073,13 +1073,13 @@ the Ternafine jaws, you might come to the conclusion that the crown of the Swanscombe head was that of a modern-like man. Two more skulls, again without faces, are available from a French -cave site, Fontéchevade. They come from the time of the last great +cave site, Font�chevade. They come from the time of the last great interglacial, as did the pre-neanderthaloids. The crowns of the -Fontéchevade skulls also look quite modern. There is a bit of the +Font�chevade skulls also look quite modern. There is a bit of the forehead preserved on one of these skulls and the brow-ridge is not heavy. Nevertheless, there is a suggestion that the bones belonged to an immature individual. In this case, his (or even more so, if _her_) -brow-ridges would have been weak anyway. The case for the Fontéchevade +brow-ridges would have been weak anyway. The case for the Font�chevade fossils, as modern type men, is little stronger than that for Swanscombe, although Professor Vallois believes it a good case. @@ -1101,8 +1101,8 @@ of the onset of colder weather, when the last glaciation was beginning in the north--say 75,000 years ago. The 70 per cent modern group came from only one cave, Mugharet es-Skhul -(“cave of the kids”). The other group, from several caves, had bones of -men of the type we’ve been calling pre-neanderthaloid which we noted +(�cave of the kids�). The other group, from several caves, had bones of +men of the type we�ve been calling pre-neanderthaloid which we noted were widespread in Europe and beyond. The tools which came with each of these finds were generally similar, and McCown and Keith, and other scholars since their study, have tended to assume that both the Skhul @@ -1131,26 +1131,26 @@ important fossil men of later Europe are shown in the chart on page DIFFERENCES IN THE EARLY MODERNS The main early European moderns have been divided into two groups, the -Cro-Magnon group and the Combe Capelle-Brünn group. Cro-Magnon people +Cro-Magnon group and the Combe Capelle-Br�nn group. Cro-Magnon people were tall and big-boned, with large, long, and rugged heads. They must have been built like many present-day Scandinavians. The Combe -Capelle-Brünn people were shorter; they had narrow heads and faces, and -big eyebrow-ridges. Of course we don’t find the skin or hair of these -people. But there is little doubt they were Caucasoids (“Whites”). +Capelle-Br�nn people were shorter; they had narrow heads and faces, and +big eyebrow-ridges. Of course we don�t find the skin or hair of these +people. But there is little doubt they were Caucasoids (�Whites�). Another important find came in the Italian Riviera, near Monte Carlo. Here, in a cave near Grimaldi, there was a grave containing a woman and a young boy, buried together. The two skeletons were first called -“Negroid” because some features of their bones were thought to resemble +�Negroid� because some features of their bones were thought to resemble certain features of modern African Negro bones. But more recently, Professor E. A. Hooton and other experts questioned the use of the word -“Negroid” in describing the Grimaldi skeletons. It is true that nothing +�Negroid� in describing the Grimaldi skeletons. It is true that nothing is known of the skin color, hair form, or any other fleshy feature of -the Grimaldi people, so that the word “Negroid” in its usual meaning is +the Grimaldi people, so that the word �Negroid� in its usual meaning is not proper here. It is also not clear whether the features of the bones -claimed to be “Negroid” are really so at all. +claimed to be �Negroid� are really so at all. -From a place called Wadjak, in Java, we have “proto-Australoid” skulls +From a place called Wadjak, in Java, we have �proto-Australoid� skulls which closely resemble those of modern Australian natives. Some of the skulls found in South Africa, especially the Boskop skull, look like those of modern Bushmen, but are much bigger. The ancestors of @@ -1159,12 +1159,12 @@ Desert. True African Negroes were forest people who apparently expanded out of the west central African area only in the last several thousand years. Although dark in skin color, neither the Australians nor the Bushmen are Negroes; neither the Wadjak nor the Boskop skulls are -“Negroid.” +�Negroid.� -As we’ve already mentioned, Professor Weidenreich believed that Peking +As we�ve already mentioned, Professor Weidenreich believed that Peking man was already on the way to becoming a Mongoloid. Anyway, the -Mongoloids would seem to have been present by the time of the “Upper -Cave” at Choukoutien, the _Sinanthropus_ find-spot. +Mongoloids would seem to have been present by the time of the �Upper +Cave� at Choukoutien, the _Sinanthropus_ find-spot. WHAT THE DIFFERENCES MEAN @@ -1175,14 +1175,14 @@ From area to area, men tended to look somewhat different, just as they do today. This is all quite natural. People _tended_ to mate near home; in the anthropological jargon, they made up geographically localized breeding populations. The simple continental division of -“stocks”--black = Africa, yellow = Asia, white = Europe--is too simple +�stocks�--black = Africa, yellow = Asia, white = Europe--is too simple a picture to fit the facts. People became accustomed to life in some -particular area within a continent (we might call it a “natural area”). +particular area within a continent (we might call it a �natural area�). As they went on living there, they evolved towards some particular physical variety. It would, of course, have been difficult to draw a clear boundary between two adjacent areas. There must always have been some mating across the boundaries in every case. One thing human -beings don’t do, and never have done, is to mate for “purity.” It is +beings don�t do, and never have done, is to mate for �purity.� It is self-righteous nonsense when we try to kid ourselves into thinking that they do. @@ -1195,28 +1195,28 @@ and they must do the writing about races. I shall, however, give two modern definitions of race, and then make one comment. Dr. William G. Boyd, professor of Immunochemistry, School of - Medicine, Boston University: “We may define a human race as a + Medicine, Boston University: �We may define a human race as a population which differs significantly from other human populations in regard to the frequency of one or more of the genes it - possesses.” + possesses.� Professor Sherwood L. Washburn, professor of Physical Anthropology, - Department of Anthropology, the University of California: “A ‘race’ + Department of Anthropology, the University of California: �A �race� is a group of genetically similar populations, and races intergrade - because there are always intermediate populations.” + because there are always intermediate populations.� My comment is that the ideas involved here are all biological: they concern groups, _not_ individuals. Boyd and Washburn may differ a bit -on what they want to consider a “population,” but a population is a +on what they want to consider a �population,� but a population is a group nevertheless, and genetics is biology to the hilt. Now a lot of people still think of race in terms of how people dress or fix their food or of other habits or customs they have. The next step is to talk -about racial “purity.” None of this has anything whatever to do with +about racial �purity.� None of this has anything whatever to do with race proper, which is a matter of the biology of groups. -Incidentally, I’m told that if man very carefully _controls_ +Incidentally, I�m told that if man very carefully _controls_ the breeding of certain animals over generations--dogs, cattle, -chickens--he might achieve a “pure” race of animals. But he doesn’t do +chickens--he might achieve a �pure� race of animals. But he doesn�t do it. Some unfortunate genetic trait soon turns up, so this has just as carefully to be bred out again, and so on. @@ -1240,20 +1240,20 @@ date to the second great interglacial period, about 350,000 years ago. Piltdown and Galley Hill are out, and with them, much of the starch in the old idea that there were two distinct lines of development -in human evolution: (1) a line of “paleoanthropic” development from +in human evolution: (1) a line of �paleoanthropic� development from Heidelberg to the Neanderthalers where it became extinct, and (2) a -very early “modern” line, through Piltdown, Galley Hill, Swanscombe, to +very early �modern� line, through Piltdown, Galley Hill, Swanscombe, to us. Swanscombe, Steinheim, and Ternafine are just as easily cases of very early pre-neanderthaloids. The pre-neanderthaloids were very widespread during the third interglacial: Ehringsdorf, Saccopastore, some of the Mount Carmel -people, and probably Fontéchevade are cases in point. A variety of +people, and probably Font�chevade are cases in point. A variety of their descendants can be seen, from Java (Solo), Africa (Rhodesian man), and about the Mediterranean and in western Europe. As the acute cold of the last glaciation set in, the western Europeans found themselves surrounded by water, ice, or bitter cold tundra. To vastly -over-simplify it, they “bred in” and became classic neanderthaloids. +over-simplify it, they �bred in� and became classic neanderthaloids. But on Mount Carmel, the Skhul cave-find with its 70 per cent modern features shows what could happen elsewhere at the same time. @@ -1263,12 +1263,12 @@ modern skeletons of men. The modern skeletons differ from place to place, just as different groups of men living in different places still look different. -What became of the Neanderthalers? Nobody can tell me for sure. I’ve a -hunch they were simply “bred out” again when the cold weather was over. +What became of the Neanderthalers? Nobody can tell me for sure. I�ve a +hunch they were simply �bred out� again when the cold weather was over. Many Americans, as the years go by, are no longer ashamed to claim they -have “Indian blood in their veins.” Give us a few more generations +have �Indian blood in their veins.� Give us a few more generations and there will not be very many other Americans left to whom we can -brag about it. It certainly isn’t inconceivable to me to imagine a +brag about it. It certainly isn�t inconceivable to me to imagine a little Cro-Magnon boy bragging to his friends about his tough, strong, Neanderthaler great-great-great-great-grandfather! @@ -1281,15 +1281,15 @@ Cultural BEGINNINGS Men, unlike the lower animals, are made up of much more than flesh and -blood and bones; for men have “culture.” +blood and bones; for men have �culture.� WHAT IS CULTURE? -“Culture” is a word with many meanings. The doctors speak of making a -“culture” of a certain kind of bacteria, and ants are said to have a -“culture.” Then there is the Emily Post kind of “culture”--you say a -person is “cultured,” or that he isn’t, depending on such things as +�Culture� is a word with many meanings. The doctors speak of making a +�culture� of a certain kind of bacteria, and ants are said to have a +�culture.� Then there is the Emily Post kind of �culture�--you say a +person is �cultured,� or that he isn�t, depending on such things as whether or not he eats peas with his knife. The anthropologists use the word too, and argue heatedly over its finer @@ -1300,7 +1300,7 @@ men from another. In this sense, a CULTURE means the way the members of a group of people think and believe and live, the tools they make, and the way they do things. Professor Robert Redfield says a culture is an organized or formalized body of conventional understandings. -“Conventional understandings” means the whole set of rules, beliefs, +�Conventional understandings� means the whole set of rules, beliefs, and standards which a group of people lives by. These understandings show themselves in art, and in the other things a people may make and do. The understandings continue to last, through tradition, from one @@ -1325,12 +1325,12 @@ Egyptians. I mean their beliefs as to why grain grew, as well as their ability to make tools with which to reap the grain. I mean their beliefs about life after death. What I am thinking about as culture is a thing which lasted in time. If any one Egyptian, even the Pharaoh, -died, it didn’t affect the Egyptian culture of that particular moment. +died, it didn�t affect the Egyptian culture of that particular moment. PREHISTORIC CULTURES -For that long period of man’s history that is all prehistory, we have +For that long period of man�s history that is all prehistory, we have no written descriptions of cultures. We find only the tools men made, the places where they lived, the graves in which they buried their dead. Fortunately for us, these tools and living places and graves all @@ -1345,15 +1345,15 @@ of the classic European Neanderthal group of men, we have found few cave-dwelling places of very early prehistoric men. First, there is the fallen-in cave where Peking man was found, near Peking. Then there are two or three other _early_, but not _very early_, possibilities. The -finds at the base of the French cave of Fontéchevade, those in one of +finds at the base of the French cave of Font�chevade, those in one of the Makapan caves in South Africa, and several open sites such as Dr. -L. S. B. Leakey’s Olorgesailie in Kenya doubtless all lie earlier than +L. S. B. Leakey�s Olorgesailie in Kenya doubtless all lie earlier than the time of the main European Neanderthal group, but none are so early as the Peking finds. You can see that we know very little about the home life of earlier prehistoric men. We find different kinds of early stone tools, but we -can’t even be really sure which tools may have been used together. +can�t even be really sure which tools may have been used together. WHY LITTLE HAS LASTED FROM EARLY TIMES @@ -1380,11 +1380,11 @@ there first! The front of this enormous sheet of ice moved down over the country, crushing and breaking and plowing up everything, like a gigantic bulldozer. You can see what happened to our camp site. -Everything the glacier couldn’t break, it pushed along in front of it +Everything the glacier couldn�t break, it pushed along in front of it or plowed beneath it. Rocks were ground to gravel, and soil was caught into the ice, which afterwards melted and ran off as muddy water. Hard -tools of flint sometimes remained whole. Human bones weren’t so hard; -it’s a wonder _any_ of them lasted. Gushing streams of melt water +tools of flint sometimes remained whole. Human bones weren�t so hard; +it�s a wonder _any_ of them lasted. Gushing streams of melt water flushed out the debris from underneath the glacier, and water flowed off the surface and through great crevasses. The hard materials these waters carried were even more rolled and ground up. Finally, such @@ -1407,26 +1407,26 @@ all up, and so we cannot say which particular sets of tools belonged together in the first place. -“EOLITHS” +�EOLITHS� But what sort of tools do we find earliest? For almost a century, people have been picking up odd bits of flint and other stone in the oldest Ice Age gravels in England and France. It is now thought these -odd bits of stone weren’t actually worked by prehistoric men. The -stones were given a name, _eoliths_, or “dawn stones.” You can see them +odd bits of stone weren�t actually worked by prehistoric men. The +stones were given a name, _eoliths_, or �dawn stones.� You can see them in many museums; but you can be pretty sure that very few of them were actually fashioned by men. -It is impossible to pick out “eoliths” that seem to be made in any -one _tradition_. By “tradition” I mean a set of habits for making one -kind of tool for some particular job. No two “eoliths” look very much +It is impossible to pick out �eoliths� that seem to be made in any +one _tradition_. By �tradition� I mean a set of habits for making one +kind of tool for some particular job. No two �eoliths� look very much alike: tools made as part of some one tradition all look much alike. -Now it’s easy to suppose that the very earliest prehistoric men picked -up and used almost any sort of stone. This wouldn’t be surprising; you -and I do it when we go camping. In other words, some of these “eoliths” +Now it�s easy to suppose that the very earliest prehistoric men picked +up and used almost any sort of stone. This wouldn�t be surprising; you +and I do it when we go camping. In other words, some of these �eoliths� may actually have been used by prehistoric men. They must have used anything that might be handy when they needed it. We could have figured -that out without the “eoliths.” +that out without the �eoliths.� THE ROAD TO STANDARDIZATION @@ -1434,7 +1434,7 @@ THE ROAD TO STANDARDIZATION Reasoning from what we know or can easily imagine, there should have been three major steps in the prehistory of tool-making. The first step would have been simple _utilization_ of what was at hand. This is the -step into which the “eoliths” would fall. The second step would have +step into which the �eoliths� would fall. The second step would have been _fashioning_--the haphazard preparation of a tool when there was a need for it. Probably many of the earlier pebble tools, which I shall describe next, fall into this group. The third step would have been @@ -1447,7 +1447,7 @@ tradition appears. PEBBLE TOOLS -At the beginning of the last chapter, you’ll remember that I said there +At the beginning of the last chapter, you�ll remember that I said there were tools from very early geological beds. The earliest bones of men have not yet been found in such early beds although the Sterkfontein australopithecine cave approaches this early date. The earliest tools @@ -1467,7 +1467,7 @@ Old World besides Africa; in fact, some prehistorians already claim to have identified a few. Since the forms and the distinct ways of making the earlier pebble tools had not yet sufficiently jelled into a set tradition, they are difficult for us to recognize. It is not -so difficult, however, if there are great numbers of “possibles” +so difficult, however, if there are great numbers of �possibles� available. A little later in time the tradition becomes more clearly set, and pebble tools are easier to recognize. So far, really large collections of pebble tools have only been found and examined in Africa. @@ -1475,9 +1475,9 @@ collections of pebble tools have only been found and examined in Africa. CORE-BIFACE TOOLS -The next tradition we’ll look at is the _core_ or biface one. The tools +The next tradition we�ll look at is the _core_ or biface one. The tools are large pear-shaped pieces of stone trimmed flat on the two opposite -sides or “faces.” Hence “biface” has been used to describe these tools. +sides or �faces.� Hence �biface� has been used to describe these tools. The front view is like that of a pear with a rather pointed top, and the back view looks almost exactly the same. Look at them side on, and you can see that the front and back faces are the same and have been @@ -1488,7 +1488,7 @@ illustration. [Illustration: ABBEVILLIAN BIFACE] We have very little idea of the way in which these core-bifaces were -used. They have been called “hand axes,” but this probably gives the +used. They have been called �hand axes,� but this probably gives the wrong idea, for an ax, to us, is not a pointed tool. All of these early tools must have been used for a number of jobs--chopping, scraping, cutting, hitting, picking, and prying. Since the core-bifaces tend to @@ -1505,7 +1505,7 @@ a big block of stone. You had to break off the flake in such a way that it was broad and thin, and also had a good sharp cutting edge. Once you really got on to the trick of doing it, this was probably a simpler way to make a good cutting tool than preparing a biface. You have to know -how, though; I’ve tried it and have mashed my fingers more than once. +how, though; I�ve tried it and have mashed my fingers more than once. The flake tools look as if they were meant mainly for chopping, scraping, and cutting jobs. When one made a flake tool, the idea seems @@ -1535,9 +1535,9 @@ tradition. It probably has its earliest roots in the pebble tool tradition of African type. There are several kinds of tools in this tradition, but all differ from the western core-bifaces and flakes. There are broad, heavy scrapers or cleavers, and tools with an -adze-like cutting edge. These last-named tools are called “hand adzes,” -just as the core-bifaces of the west have often been called “hand -axes.” The section of an adze cutting edge is ? shaped; the section of +adze-like cutting edge. These last-named tools are called �hand adzes,� +just as the core-bifaces of the west have often been called �hand +axes.� The section of an adze cutting edge is ? shaped; the section of an ax is < shaped. [Illustration: ANYATHIAN ADZE-LIKE TOOL] @@ -1581,17 +1581,17 @@ stratification.[3] Soan (India) Flake: - “Typical Mousterian” + �Typical Mousterian� Levalloiso-Mousterian Levalloisian Tayacian Clactonian (localized in England) Core-biface: - Some blended elements in “Mousterian” + Some blended elements in �Mousterian� Micoquian (= Acheulean 6 and 7) Acheulean - Abbevillian (once called “Chellean”) + Abbevillian (once called �Chellean�) Pebble tool: Oldowan @@ -1608,8 +1608,8 @@ out of glacial gravels the easiest thing to do first is to isolate individual types of tools into groups. First you put a bushel-basketful of tools on a table and begin matching up types. Then you give names to the groups of each type. The groups and the types are really matters of -the archeologists’ choice; in real life, they were probably less exact -than the archeologists’ lists of them. We now know pretty well in which +the archeologists� choice; in real life, they were probably less exact +than the archeologists� lists of them. We now know pretty well in which of the early traditions the various early groups belong. @@ -1635,9 +1635,9 @@ production must have been passed on from one generation to another. I could even guess that the notions of the ideal type of one or the other of these tools stood out in the minds of men of those times -somewhat like a symbol of “perfect tool for good job.” If this were -so--remember it’s only a wild guess of mine--then men were already -symbol users. Now let’s go on a further step to the fact that the words +somewhat like a symbol of �perfect tool for good job.� If this were +so--remember it�s only a wild guess of mine--then men were already +symbol users. Now let�s go on a further step to the fact that the words men speak are simply sounds, each different sound being a symbol for a different meaning. If standardized tool-making suggests symbol-making, is it also possible that crude word-symbols were also being made? I @@ -1650,7 +1650,7 @@ of our second step is more suggestive, although we may not yet feel sure that many of the earlier pebble tools were man-made products. But with the step to standardization and the appearance of the traditions, I believe we must surely be dealing with the traces of culture-bearing -_men_. The “conventional understandings” which Professor Redfield’s +_men_. The �conventional understandings� which Professor Redfield�s definition of culture suggests are now evidenced for us in the persistent habits for the preparation of stone tools. Were we able to see the other things these prehistoric men must have made--in materials @@ -1666,19 +1666,19 @@ In the last chapter, I told you that many of the older archeologists and human paleontologists used to think that modern man was very old. The supposed ages of Piltdown and Galley Hill were given as evidence of the great age of anatomically modern man, and some interpretations -of the Swanscombe and Fontéchevade fossils were taken to support +of the Swanscombe and Font�chevade fossils were taken to support this view. The conclusion was that there were two parallel lines or -“phyla” of men already present well back in the Pleistocene. The -first of these, the more primitive or “paleoanthropic” line, was +�phyla� of men already present well back in the Pleistocene. The +first of these, the more primitive or �paleoanthropic� line, was said to include Heidelberg, the proto-neanderthaloids and classic -Neanderthal. The more anatomically modern or “neanthropic” line was +Neanderthal. The more anatomically modern or �neanthropic� line was thought to consist of Piltdown and the others mentioned above. The Neanderthaler or paleoanthropic line was thought to have become extinct after the first phase of the last great glaciation. Of course, the modern or neanthropic line was believed to have persisted into the -present, as the basis for the world’s population today. But with +present, as the basis for the world�s population today. But with Piltdown liquidated, Galley Hill known to be very late, and Swanscombe -and Fontéchevade otherwise interpreted, there is little left of the +and Font�chevade otherwise interpreted, there is little left of the so-called parallel phyla theory. While the theory was in vogue, however, and as long as the European @@ -1695,9 +1695,9 @@ where they had actually been dropped by the men who made and used them. The tools came, rather, from the secondary hodge-podge of the glacial gravels. I tried to give you a picture of the bulldozing action of glaciers (p. 40) and of the erosion and weathering that were -side-effects of a glacially conditioned climate on the earth’s surface. +side-effects of a glacially conditioned climate on the earth�s surface. As we said above, if one simply plucks tools out of the redeposited -gravels, his natural tendency is to “type” the tools by groups, and to +gravels, his natural tendency is to �type� the tools by groups, and to think that the groups stand for something _on their own_. In 1906, M. Victor Commont actually made a rare find of what seems @@ -1705,15 +1705,15 @@ to have been a kind of workshop site, on a terrace above the Somme river in France. Here, Commont realized, flake tools appeared clearly in direct association with core-biface tools. Few prehistorians paid attention to Commont or his site, however. It was easier to believe -that flake tools represented a distinct “culture” and that this -“culture” was that of the Neanderthaler or paleoanthropic line, and -that the core-bifaces stood for another “culture” which was that of the +that flake tools represented a distinct �culture� and that this +�culture� was that of the Neanderthaler or paleoanthropic line, and +that the core-bifaces stood for another �culture� which was that of the supposed early modern or neanthropic line. Of course, I am obviously skipping many details here. Some later sites with Neanderthal fossils do seem to have only flake tools, but other such sites have both types of tools. The flake tools which appeared _with_ the core-bifaces in the Swanscombe gravels were never made much of, although it -was embarrassing for the parallel phyla people that Fontéchevade +was embarrassing for the parallel phyla people that Font�chevade ran heavily to flake tools. All in all, the parallel phyla theory flourished because it seemed so neat and easy to understand. @@ -1722,20 +1722,20 @@ TRADITIONS ARE TOOL-MAKING HABITS, NOT CULTURES In case you think I simply enjoy beating a dead horse, look in any standard book on prehistory written twenty (or even ten) years ago, or -in most encyclopedias. You’ll find that each of the individual tool -types, of the West, at least, was supposed to represent a “culture.” -The “cultures” were believed to correspond to parallel lines of human +in most encyclopedias. You�ll find that each of the individual tool +types, of the West, at least, was supposed to represent a �culture.� +The �cultures� were believed to correspond to parallel lines of human evolution. In 1937, Mr. Harper Kelley strongly re-emphasized the importance -of Commont’s workshop site and the presence of flake tools with -core-bifaces. Next followed Dr. Movius’ clear delineation of the +of Commont�s workshop site and the presence of flake tools with +core-bifaces. Next followed Dr. Movius� clear delineation of the chopper-chopping tool tradition of the Far East. This spoiled the nice symmetry of the flake-tool = paleoanthropic, core-biface = neanthropic equations. Then came increasing understanding of the importance of the pebble tools in Africa, and the location of several more workshop sites there, especially at Olorgesailie in Kenya. Finally came the -liquidation of Piltdown and the deflation of Galley Hill’s date. So it +liquidation of Piltdown and the deflation of Galley Hill�s date. So it is at last possible to picture an individual prehistoric man making a flake tool to do one job and a core-biface tool to do another. Commont showed us this picture in 1906, but few believed him. @@ -1751,7 +1751,7 @@ that of the cave on Mount Carmel in Palestine, where the blended pre-neanderthaloid, 70 per cent modern-type skulls were found. Here, in the same level with the skulls, were 9,784 flint tools. Of these, only three--doubtless strays--were core-bifaces; all the rest were flake -tools or flake chips. We noted above how the Fontéchevade cave ran to +tools or flake chips. We noted above how the Font�chevade cave ran to flake tools. The only conclusion I would draw from this is that times and circumstances did exist in which prehistoric men needed only flake tools. So they only made flake tools for those particular times and @@ -1773,13 +1773,13 @@ piece of bone. From the gravels which yield the Clactonian flakes of England comes the fire-hardened point of a wooden spear. There are also the chance finds of the fossil human bones themselves, of which we spoke in the last chapter. Aside from the cave of Peking man, none -of the earliest tools have been found in caves. Open air or “workshop” +of the earliest tools have been found in caves. Open air or �workshop� sites which do not seem to have been disturbed later by some geological agency are very rare. The chart on page 65 shows graphically what the situation in west-central Europe seems to have been. It is not yet certain whether -there were pebble tools there or not. The Fontéchevade cave comes +there were pebble tools there or not. The Font�chevade cave comes into the picture about 100,000 years ago or more. But for the earlier hundreds of thousands of years--below the red-dotted line on the chart--the tools we find come almost entirely from the haphazard @@ -1790,13 +1790,13 @@ kinds of all-purpose tools. Almost any one of them could be used for hacking, chopping, cutting, and scraping; so the men who used them must have been living in a rough and ready sort of way. They found or hunted their food wherever they could. In the anthropological jargon, they -were “food-gatherers,” pure and simple. +were �food-gatherers,� pure and simple. Because of the mixture in the gravels and in the materials they -carried, we can’t be sure which animals these men hunted. Bones of +carried, we can�t be sure which animals these men hunted. Bones of the larger animals turn up in the gravels, but they could just as well belong to the animals who hunted the men, rather than the other -way about. We don’t know. This is why camp sites like Commont’s and +way about. We don�t know. This is why camp sites like Commont�s and Olorgesailie in Kenya are so important when we do find them. The animal bones at Olorgesailie belonged to various mammals of extremely large size. Probably they were taken in pit-traps, but there are a number of @@ -1809,18 +1809,18 @@ animal. Professor F. Clark Howell recently returned from excavating another important open air site at Isimila in Tanganyika. The site yielded the bones of many fossil animals and also thousands of core-bifaces, -flakes, and choppers. But Howell’s reconstruction of the food-getting -habits of the Isimila people certainly suggests that the word “hunting” -is too dignified for what they did; “scavenging” would be much nearer +flakes, and choppers. But Howell�s reconstruction of the food-getting +habits of the Isimila people certainly suggests that the word �hunting� +is too dignified for what they did; �scavenging� would be much nearer the mark. During a great part of this time the climate was warm and pleasant. The second interglacial period (the time between the second and third great alpine glaciations) lasted a long time, and during much of this time -the climate may have been even better than ours is now. We don’t know +the climate may have been even better than ours is now. We don�t know that earlier prehistoric men in Europe or Africa lived in caves. They may not have needed to; much of the weather may have been so nice that -they lived in the open. Perhaps they didn’t wear clothes, either. +they lived in the open. Perhaps they didn�t wear clothes, either. WHAT THE PEKING CAVE-FINDS TELL US @@ -1832,7 +1832,7 @@ were bones of dangerous animals, members of the wolf, bear, and cat families. Some of the cat bones belonged to beasts larger than tigers. There were also bones of other wild animals: buffalo, camel, deer, elephants, horses, sheep, and even ostriches. Seventy per cent of the -animals Peking man killed were fallow deer. It’s much too cold and dry +animals Peking man killed were fallow deer. It�s much too cold and dry in north China for all these animals to live there today. So this list helps us know that the weather was reasonably warm, and that there was enough rain to grow grass for the grazing animals. The list also helps @@ -1840,7 +1840,7 @@ the paleontologists to date the find. Peking man also seems to have eaten plant food, for there are hackberry seeds in the debris of the cave. His tools were made of sandstone and -quartz and sometimes of a rather bad flint. As we’ve already seen, they +quartz and sometimes of a rather bad flint. As we�ve already seen, they belong in the chopper-tool tradition. It seems fairly clear that some of the edges were chipped by right-handed people. There are also many split pieces of heavy bone. Peking man probably split them so he could @@ -1850,10 +1850,10 @@ Many of these split bones were the bones of Peking men. Each one of the skulls had already had the base broken out of it. In no case were any of the bones resting together in their natural relation to one another. There is nothing like a burial; all of the bones are scattered. Now -it’s true that animals could have scattered bodies that were not cared +it�s true that animals could have scattered bodies that were not cared for or buried. But splitting bones lengthwise and carefully removing the base of a skull call for both the tools and the people to use them. -It’s pretty clear who the people were. Peking man was a cannibal. +It�s pretty clear who the people were. Peking man was a cannibal. * * * * * @@ -1862,8 +1862,8 @@ prehistoric men. In those days life was rough. You evidently had to watch out not only for dangerous animals but also for your fellow men. You ate whatever you could catch or find growing. But you had sense enough to build fires, and you had already formed certain habits for -making the kinds of stone tools you needed. That’s about all we know. -But I think we’ll have to admit that cultural beginnings had been made, +making the kinds of stone tools you needed. That�s about all we know. +But I think we�ll have to admit that cultural beginnings had been made, and that these early people were really _men_. @@ -1876,16 +1876,16 @@ MORE EVIDENCE of Culture While the dating is not yet sure, the material that we get from caves in Europe must go back to about 100,000 years ago; the time of the -classic Neanderthal group followed soon afterwards. We don’t know why +classic Neanderthal group followed soon afterwards. We don�t know why there is no earlier material in the caves; apparently they were not used before the last interglacial phase (the period just before the last great glaciation). We know that men of the classic Neanderthal group were living in caves from about 75,000 to 45,000 years ago. New radioactive carbon dates even suggest that some of the traces of -culture we’ll describe in this chapter may have lasted to about 35,000 +culture we�ll describe in this chapter may have lasted to about 35,000 years ago. Probably some of the pre-neanderthaloid types of men had also lived in caves. But we have so far found their bones in caves only -in Palestine and at Fontéchevade. +in Palestine and at Font�chevade. THE CAVE LAYERS @@ -1893,7 +1893,7 @@ THE CAVE LAYERS In parts of France, some peasants still live in caves. In prehistoric time, many generations of people lived in them. As a result, many caves have deep layers of debris. The first people moved in and lived -on the rock floor. They threw on the floor whatever they didn’t want, +on the rock floor. They threw on the floor whatever they didn�t want, and they tracked in mud; nobody bothered to clean house in those days. Their debris--junk and mud and garbage and what not--became packed into a layer. As time went on, and generations passed, the layer grew @@ -1910,20 +1910,20 @@ earliest to latest. This is the _stratification_ we talked about (p. [Illustration: SECTION OF SHELTER ON LOWER TERRACE, LE MOUSTIER] -While we may find a mix-up in caves, it’s not nearly as bad as the +While we may find a mix-up in caves, it�s not nearly as bad as the mixing up that was done by glaciers. The animal bones and shells, the fireplaces, the bones of men, and the tools the men made all belong -together, if they come from one layer. That’s the reason why the cave +together, if they come from one layer. That�s the reason why the cave of Peking man is so important. It is also the reason why the caves in Europe and the Near East are so important. We can get an idea of which things belong together and which lot came earliest and which latest. In most cases, prehistoric men lived only in the mouths of caves. -They didn’t like the dark inner chambers as places to live in. They +They didn�t like the dark inner chambers as places to live in. They preferred rock-shelters, at the bases of overhanging cliffs, if there was enough overhang to give shelter. When the weather was good, they no -doubt lived in the open air as well. I’ll go on using the term “cave” -since it’s more familiar, but remember that I really mean rock-shelter, +doubt lived in the open air as well. I�ll go on using the term �cave� +since it�s more familiar, but remember that I really mean rock-shelter, as a place in which people actually lived. The most important European cave sites are in Spain, France, and @@ -1933,29 +1933,29 @@ found when the out-of-the-way parts of Europe, Africa, and Asia are studied. -AN “INDUSTRY” DEFINED +AN �INDUSTRY� DEFINED We have already seen that the earliest European cave materials are -those from the cave of Fontéchevade. Movius feels certain that the +those from the cave of Font�chevade. Movius feels certain that the lowest materials here date back well into the third interglacial stage, -that which lay between the Riss (next to the last) and the Würm I +that which lay between the Riss (next to the last) and the W�rm I (first stage of the last) alpine glaciations. This material consists of an _industry_ of stone tools, apparently all made in the flake -tradition. This is the first time we have used the word “industry.” +tradition. This is the first time we have used the word �industry.� It is useful to call all of the different tools found together in one layer and made of _one kind of material_ an industry; that is, the tools must be found together as men left them. Tools taken from the glacial gravels (or from windswept desert surfaces or river gravels -or any geological deposit) are not “together” in this sense. We might -say the latter have only “geological,” not “archeological” context. +or any geological deposit) are not �together� in this sense. We might +say the latter have only �geological,� not �archeological� context. Archeological context means finding things just as men left them. We -can tell what tools go together in an “industrial” sense only if we +can tell what tools go together in an �industrial� sense only if we have archeological context. -Up to now, the only things we could have called “industries” were the +Up to now, the only things we could have called �industries� were the worked stone industry and perhaps the worked (?) bone industry of the Peking cave. We could add some of the very clear cases of open air -sites, like Olorgesailie. We couldn’t use the term for the stone tools +sites, like Olorgesailie. We couldn�t use the term for the stone tools from the glacial gravels, because we do not know which tools belonged together. But when the cave materials begin to appear in Europe, we can begin to speak of industries. Most of the European caves of this time @@ -1964,16 +1964,16 @@ contain industries of flint tools alone. THE EARLIEST EUROPEAN CAVE LAYERS -We’ve just mentioned the industry from what is said to be the oldest +We�ve just mentioned the industry from what is said to be the oldest inhabited cave in Europe; that is, the industry from the deepest layer -of the site at Fontéchevade. Apparently it doesn’t amount to much. The +of the site at Font�chevade. Apparently it doesn�t amount to much. The tools are made of stone, in the flake tradition, and are very poorly worked. This industry is called _Tayacian_. Its type tool seems to be a smallish flake tool, but there are also larger flakes which seem to have been fashioned for hacking. In fact, the type tool seems to be simply a smaller edition of the Clactonian tool (pictured on p. 45). -None of the Fontéchevade tools are really good. There are scrapers, +None of the Font�chevade tools are really good. There are scrapers, and more or less pointed tools, and tools that may have been used for hacking and chopping. Many of the tools from the earlier glacial gravels are better made than those of this first industry we see in @@ -2005,7 +2005,7 @@ core-biface and the flake traditions. The core-biface tools usually make up less than half of all the tools in the industry. However, the name of the biface type of tool is generally given to the whole industry. It is called the _Acheulean_, actually a late form of it, as -“Acheulean” is also used for earlier core-biface tools taken from the +�Acheulean� is also used for earlier core-biface tools taken from the glacial gravels. In western Europe, the name used is _Upper Acheulean_ or _Micoquian_. The same terms have been borrowed to name layers E and F in the Tabun cave, on Mount Carmel in Palestine. @@ -2029,7 +2029,7 @@ those used for at least one of the flake industries we shall mention presently. There is very little else in these early cave layers. We do not have -a proper “industry” of bone tools. There are traces of fire, and of +a proper �industry� of bone tools. There are traces of fire, and of animal bones, and a few shells. In Palestine, there are many more bones of deer than of gazelle in these layers; the deer lives in a wetter climate than does the gazelle. In the European cave layers, the @@ -2043,18 +2043,18 @@ bones of fossil men definitely in place with this industry. FLAKE INDUSTRIES FROM THE CAVES Two more stone industries--the _Levalloisian_ and the -“_Mousterian_”--turn up at approximately the same time in the European +�_Mousterian_�--turn up at approximately the same time in the European cave layers. Their tools seem to be mainly in the flake tradition, but according to some of the authorities their preparation also shows some combination with the habits by which the core-biface tools were prepared. -Now notice that I don’t tell you the Levalloisian and the “Mousterian” +Now notice that I don�t tell you the Levalloisian and the �Mousterian� layers are both above the late Acheulean layers. Look at the cave -section (p. 57) and you’ll find that some “Mousterian of Acheulean -tradition” appears above some “typical Mousterian.” This means that +section (p. 57) and you�ll find that some �Mousterian of Acheulean +tradition� appears above some �typical Mousterian.� This means that there may be some kinds of Acheulean industries that are later than -some kinds of “Mousterian.” The same is true of the Levalloisian. +some kinds of �Mousterian.� The same is true of the Levalloisian. There were now several different kinds of habits that men used in making stone tools. These habits were based on either one or the other @@ -2072,7 +2072,7 @@ were no patent laws in those days. The extremely complicated interrelationships of the different habits used by the tool-makers of this range of time are at last being -systematically studied. M. François Bordes has developed a statistical +systematically studied. M. Fran�ois Bordes has developed a statistical method of great importance for understanding these tool preparation habits. @@ -2081,22 +2081,22 @@ THE LEVALLOISIAN AND MOUSTERIAN The easiest Levalloisian tool to spot is a big flake tool. The trick in making it was to fashion carefully a big chunk of stone (called -the Levalloisian “tortoise core,” because it resembles the shape of +the Levalloisian �tortoise core,� because it resembles the shape of a turtle-shell) and then to whack this in such a way that a large flake flew off. This large thin flake, with sharp cutting edges, is the finished Levalloisian tool. There were various other tools in a Levalloisian industry, but this is the characteristic _Levalloisian_ tool. -There are several “typical Mousterian” stone tools. Different from -the tools of the Levalloisian type, these were made from “disc-like -cores.” There are medium-sized flake “side scrapers.” There are also -some small pointed tools and some small “hand axes.” The last of these +There are several �typical Mousterian� stone tools. Different from +the tools of the Levalloisian type, these were made from �disc-like +cores.� There are medium-sized flake �side scrapers.� There are also +some small pointed tools and some small �hand axes.� The last of these tool types is often a flake worked on both of the flat sides (that is, bifacially). There are also pieces of flint worked into the form of crude balls. The pointed tools may have been fixed on shafts to make short jabbing spears; the round flint balls may have been used as -bolas. Actually, we don’t _know_ what either tool was used for. The +bolas. Actually, we don�t _know_ what either tool was used for. The points and side scrapers are illustrated (pp. 64 and 66). [Illustration: LEVALLOIS FLAKE] @@ -2108,9 +2108,9 @@ Nowadays the archeologists are less and less sure of the importance of any one specific tool type and name. Twenty years ago, they used to speak simply of Acheulean or Levalloisian or Mousterian tools. Now, more and more, _all_ of the tools from some one layer in a -cave are called an “industry,” which is given a mixed name. Thus we -have “Levalloiso-Mousterian,” and “Acheuleo-Levalloisian,” and even -“Acheuleo-Mousterian” (or “Mousterian of Acheulean tradition”). Bordes’ +cave are called an �industry,� which is given a mixed name. Thus we +have �Levalloiso-Mousterian,� and �Acheuleo-Levalloisian,� and even +�Acheuleo-Mousterian� (or �Mousterian of Acheulean tradition�). Bordes� systematic work is beginning to clear up some of our confusion. The time of these late Acheuleo-Levalloiso-Mousterioid industries @@ -2120,16 +2120,16 @@ phase of the last great glaciation. It was also the time that the classic group of Neanderthal men was living in Europe. A number of the Neanderthal fossil finds come from these cave layers. Before the different habits of tool preparation were understood it used to be -popular to say Neanderthal man was “Mousterian man.” I think this is -wrong. What used to be called “Mousterian” is now known to be a variety +popular to say Neanderthal man was �Mousterian man.� I think this is +wrong. What used to be called �Mousterian� is now known to be a variety of industries with tools of both core-biface and flake habits, and -so mixed that the word “Mousterian” used alone really doesn’t mean +so mixed that the word �Mousterian� used alone really doesn�t mean anything. The Neanderthalers doubtless understood the tool preparation habits by means of which Acheulean, Levalloisian and Mousterian type tools were produced. We also have the more modern-like Mount Carmel people, found in a cave layer of Palestine with tools almost entirely -in the flake tradition, called “Levalloiso-Mousterian,” and the -Fontéchevade-Tayacian (p. 59). +in the flake tradition, called �Levalloiso-Mousterian,� and the +Font�chevade-Tayacian (p. 59). [Illustration: MOUSTERIAN POINT] @@ -2165,7 +2165,7 @@ which seem to have served as anvils or chopping blocks, are fairly common. Bits of mineral, used as coloring matter, have also been found. We -don’t know what the color was used for. +don�t know what the color was used for. [Illustration: MOUSTERIAN SIDE SCRAPER] @@ -2230,7 +2230,7 @@ might suggest some notion of hoarding up the spirits or the strength of bears killed in the hunt. Probably the people lived in small groups, as hunting and food-gathering seldom provide enough food for large groups of people. These groups probably had some kind of leader or -“chief.” Very likely the rude beginnings of rules for community life +�chief.� Very likely the rude beginnings of rules for community life and politics, and even law, were being made. But what these were, we do not know. We can only guess about such things, as we can only guess about many others; for example, how the idea of a family must have been @@ -2246,8 +2246,8 @@ small. The mixtures and blendings of the habits used in making stone tools must mean that there were also mixtures and blends in many of the other ideas and beliefs of these small groups. And what this probably means is that there was no one _culture_ of the time. It is -certainly unlikely that there were simply three cultures, “Acheulean,” -“Levalloisian,” and “Mousterian,” as has been thought in the past. +certainly unlikely that there were simply three cultures, �Acheulean,� +�Levalloisian,� and �Mousterian,� as has been thought in the past. Rather there must have been a great variety of loosely related cultures at about the same stage of advancement. We could say, too, that here we really begin to see, for the first time, that remarkable ability @@ -2272,7 +2272,7 @@ related habits for the making of tools. But the men who made them must have looked much like the men of the West. Their tools were different, but just as useful. -As to what the men of the West looked like, I’ve already hinted at all +As to what the men of the West looked like, I�ve already hinted at all we know so far (pp. 29 ff.). The Neanderthalers were present at the time. Some more modern-like men must have been about, too, since fossils of them have turned up at Mount Carmel in Palestine, and at @@ -2306,7 +2306,7 @@ A NEW TRADITION APPEARS Something new was probably beginning to happen in the European-Mediterranean area about 40,000 years ago, though all the rest of the Old World seems to have been going on as it had been. I -can’t be sure of this because the information we are using as a basis +can�t be sure of this because the information we are using as a basis for dates is very inaccurate for the areas outside of Europe and the Mediterranean. @@ -2325,7 +2325,7 @@ drawing shows. It has sharp cutting edges, and makes a very useful knife. The real trick is to be able to make one. It is almost impossible to make a blade out of any stone but flint or a natural volcanic glass called obsidian. And even if you have flint or obsidian, -you first have to work up a special cone-shaped “blade-core,” from +you first have to work up a special cone-shaped �blade-core,� from which to whack off blades. [Illustration: PLAIN BLADE] @@ -2351,8 +2351,8 @@ found in equally early cave levels in Syria; their popularity there seems to fluctuate a bit. Some more or less parallel-sided flakes are known in the Levalloisian industry in France, but they are probably no earlier than Tabun E. The Tabun blades are part of a local late -“Acheulean” industry, which is characterized by core-biface “hand -axes,” but which has many flake tools as well. Professor F. E. +�Acheulean� industry, which is characterized by core-biface �hand +axes,� but which has many flake tools as well. Professor F. E. Zeuner believes that this industry may be more than 120,000 years old; actually its date has not yet been fixed, but it is very old--older than the fossil finds of modern-like men in the same caves. @@ -2371,7 +2371,7 @@ We are not sure just where the earliest _persisting_ habits for the production of blade tools developed. Impressed by the very early momentary appearance of blades at Tabun on Mount Carmel, Professor Dorothy A. Garrod first favored the Near East as a center of origin. -She spoke of “some as yet unidentified Asiatic centre,” which she +She spoke of �some as yet unidentified Asiatic centre,� which she thought might be in the highlands of Iran or just beyond. But more recent work has been done in this area, especially by Professor Coon, and the blade tools do not seem to have an early appearance there. When @@ -2395,21 +2395,21 @@ core (and the striking of the Levalloisian flake from it) might have followed through to the conical core and punch technique for the production of blades. Professor Garrod is much impressed with the speed of change during the later phases of the last glaciation, and its -probable consequences. She speaks of “the greater number of industries +probable consequences. She speaks of �the greater number of industries having enough individual character to be classified as distinct ... -since evolution now starts to outstrip diffusion.” Her “evolution” here +since evolution now starts to outstrip diffusion.� Her �evolution� here is of course an industrial evolution rather than a biological one. Certainly the people of Europe had begun to make blade tools during the warm spell after the first phase of the last glaciation. By about 40,000 years ago blades were well established. The bones of the blade -tool makers we’ve found so far indicate that anatomically modern men +tool makers we�ve found so far indicate that anatomically modern men had now certainly appeared. Unfortunately, only a few fossil men have so far been found from the very beginning of the blade tool range in Europe (or elsewhere). What I certainly shall _not_ tell you is that conquering bands of fine, strong, anatomically modern men, armed with superior blade tools, came sweeping out of the East to exterminate the -lowly Neanderthalers. Even if we don’t know exactly what happened, I’d -lay a good bet it wasn’t that simple. +lowly Neanderthalers. Even if we don�t know exactly what happened, I�d +lay a good bet it wasn�t that simple. We do know a good deal about different blade industries in Europe. Almost all of them come from cave layers. There is a great deal of @@ -2418,7 +2418,7 @@ this complication; in fact, it doubtless simplifies it too much. But it may suggest all the complication of industries which is going on at this time. You will note that the upper portion of my much simpler chart (p. 65) covers the same material (in the section -marked “Various Blade-Tool Industries”). That chart is certainly too +marked �Various Blade-Tool Industries�). That chart is certainly too simplified. You will realize that all this complication comes not only from @@ -2429,7 +2429,7 @@ a good deal of climatic change at this time. The plants and animals that men used for food were changing, too. The great variety of tools and industries we now find reflect these changes and the ability of men to keep up with the times. Now, for example, is the first time we are -sure that there are tools to _make_ other tools. They also show men’s +sure that there are tools to _make_ other tools. They also show men�s increasing ability to adapt themselves. @@ -2437,15 +2437,15 @@ SPECIAL TYPES OF BLADE TOOLS The most useful tools that appear at this time were made from blades. - 1. The “backed” blade. This is a knife made of a flint blade, with - one edge purposely blunted, probably to save the user’s fingers + 1. The �backed� blade. This is a knife made of a flint blade, with + one edge purposely blunted, probably to save the user�s fingers from being cut. There are several shapes of backed blades (p. 73). [Illustration: TWO BURINS] - 2. The _burin_ or “graver.” The burin was the original chisel. Its - cutting edge is _transverse_, like a chisel’s. Some burins are + 2. The _burin_ or �graver.� The burin was the original chisel. Its + cutting edge is _transverse_, like a chisel�s. Some burins are made like a screw-driver, save that burins are sharp. Others have edges more like the blade of a chisel or a push plane, with only one bevel. Burins were probably used to make slots in wood @@ -2456,29 +2456,29 @@ The most useful tools that appear at this time were made from blades. [Illustration: TANGED POINT] - 3. The “tanged” point. These stone points were used to tip arrows or + 3. The �tanged� point. These stone points were used to tip arrows or light spears. They were made from blades, and they had a long tang at the bottom where they were fixed to the shaft. At the place where the tang met the main body of the stone point, there was - a marked “shoulder,” the beginnings of a barb. Such points had + a marked �shoulder,� the beginnings of a barb. Such points had either one or two shoulders. [Illustration: NOTCHED BLADE] - 4. The “notched” or “strangulated” blade. Along with the points for + 4. The �notched� or �strangulated� blade. Along with the points for arrows or light spears must go a tool to prepare the arrow or - spear shaft. Today, such a tool would be called a “draw-knife” or - a “spoke-shave,” and this is what the notched blades probably are. + spear shaft. Today, such a tool would be called a �draw-knife� or + a �spoke-shave,� and this is what the notched blades probably are. Our spoke-shaves have sharp straight cutting blades and really - “shave.” Notched blades of flint probably scraped rather than cut. + �shave.� Notched blades of flint probably scraped rather than cut. - 5. The “awl,” “drill,” or “borer.” These blade tools are worked out + 5. The �awl,� �drill,� or �borer.� These blade tools are worked out to a spike-like point. They must have been used for making holes in wood, bone, shell, skin, or other things. [Illustration: DRILL OR AWL] - 6. The “end-scraper on a blade” is a tool with one or both ends + 6. The �end-scraper on a blade� is a tool with one or both ends worked so as to give a good scraping edge. It could have been used to hollow out wood or bone, scrape hides, remove bark from trees, and a number of other things (p. 78). @@ -2489,11 +2489,11 @@ usually made of blades, but the best examples are so carefully worked on both sides (bifacially) that it is impossible to see the original blade. This tool is - 7. The “laurel leaf” point. Some of these tools were long and + 7. The �laurel leaf� point. Some of these tools were long and dagger-like, and must have been used as knives or daggers. Others - were small, called “willow leaf,” and must have been mounted on + were small, called �willow leaf,� and must have been mounted on spear or arrow shafts. Another typical Solutrean tool is the - “shouldered” point. Both the “laurel leaf” and “shouldered” point + �shouldered� point. Both the �laurel leaf� and �shouldered� point types are illustrated (see above and p. 79). [Illustration: END-SCRAPER ON A BLADE] @@ -2507,17 +2507,17 @@ second is a core tool. [Illustration: SHOULDERED POINT] - 8. The “keel-shaped round scraper” is usually small and quite round, + 8. The �keel-shaped round scraper� is usually small and quite round, and has had chips removed up to a peak in the center. It is called - “keel-shaped” because it is supposed to look (when upside down) + �keel-shaped� because it is supposed to look (when upside down) like a section through a boat. Actually, it looks more like a tent or an umbrella. Its outer edges are sharp all the way around, and it was probably a general purpose scraping tool (see illustration, p. 81). - 9. The “keel-shaped nosed scraper” is a much larger and heavier tool + 9. The �keel-shaped nosed scraper� is a much larger and heavier tool than the round scraper. It was made on a core with a flat bottom, - and has one nicely worked end or “nose.” Such tools are usually + and has one nicely worked end or �nose.� Such tools are usually large enough to be easily grasped, and probably were used like push planes (see illustration, p. 81). @@ -2530,7 +2530,7 @@ the most easily recognized blade tools, although they show differences in detail at different times. There are also many other kinds. Not all of these tools appear in any one industry at one time. Thus the different industries shown in the chart (p. 72) each have only some -of the blade tools we’ve just listed, and also a few flake tools. Some +of the blade tools we�ve just listed, and also a few flake tools. Some industries even have a few core tools. The particular types of blade tools appearing in one cave layer or another, and the frequency of appearance of the different types, tell which industry we have in each @@ -2545,15 +2545,15 @@ to appear. There are knives, pins, needles with eyes, and little double-pointed straight bars of bone that were probably fish-hooks. The fish-line would have been fastened in the center of the bar; when the fish swallowed the bait, the bar would have caught cross-wise in the -fish’s mouth. +fish�s mouth. One quite special kind of bone tool is a long flat point for a light spear. It has a deep notch cut up into the breadth of its base, and is -called a “split-based bone point” (p. 82). We know examples of bone +called a �split-based bone point� (p. 82). We know examples of bone beads from these times, and of bone handles for flint tools. Pierced teeth of some animals were worn as beads or pendants, but I am not sure -that elks’ teeth were worn this early. There are even spool-shaped -“buttons” or toggles. +that elks� teeth were worn this early. There are even spool-shaped +�buttons� or toggles. [Illustration: SPLIT-BASED BONE POINT] @@ -2595,12 +2595,12 @@ almost to have served as sketch blocks. The surfaces of these various objects may show animals, or rather abstract floral designs, or geometric designs. -[Illustration: “VENUS” FIGURINE FROM WILLENDORF] +[Illustration: �VENUS� FIGURINE FROM WILLENDORF] Some of the movable art is not done on tools. The most remarkable examples of this class are little figures of women. These women seem to be pregnant, and their most female characteristics are much emphasized. -It is thought that these “Venus” or “Mother-goddess” figurines may be +It is thought that these �Venus� or �Mother-goddess� figurines may be meant to show the great forces of nature--fertility and the birth of life. @@ -2616,21 +2616,21 @@ are different styles in the cave art. The really great cave art is pretty well restricted to southern France and Cantabrian (northwestern) Spain. -There are several interesting things about the “Franco-Cantabrian” cave +There are several interesting things about the �Franco-Cantabrian� cave art. It was done deep down in the darkest and most dangerous parts of the caves, although the men lived only in the openings of caves. If you think what they must have had for lights--crude lamps of hollowed stone have been found, which must have burned some kind of oil or grease, with a matted hair or fiber wick--and of the animals that may have -lurked in the caves, you’ll understand the part about danger. Then, -too, we’re sure the pictures these people painted were not simply to be +lurked in the caves, you�ll understand the part about danger. Then, +too, we�re sure the pictures these people painted were not simply to be looked at and admired, for they painted one picture right over other pictures which had been done earlier. Clearly, it was the _act_ of _painting_ that counted. The painter had to go way down into the most mysterious depths of the earth and create an animal in paint. Possibly he believed that by doing this he gained some sort of magic power over the same kind of animal when he hunted it in the open air. It certainly -doesn’t look as if he cared very much about the picture he painted--as +doesn�t look as if he cared very much about the picture he painted--as a finished product to be admired--for he or somebody else soon went down and painted another animal right over the one he had done. @@ -2683,10 +2683,10 @@ it. Their art is another example of the direction the human mind was taking. And when I say human, I mean it in the fullest sense, for this is the time in which fully modern man has appeared. On page 34, we -spoke of the Cro-Magnon group and of the Combe Capelle-Brünn group of -Caucasoids and of the Grimaldi “Negroids,” who are no longer believed +spoke of the Cro-Magnon group and of the Combe Capelle-Br�nn group of +Caucasoids and of the Grimaldi �Negroids,� who are no longer believed to be Negroid. I doubt that any one of these groups produced most of -the achievements of the times. It’s not yet absolutely sure which +the achievements of the times. It�s not yet absolutely sure which particular group produced the great cave art. The artists were almost certainly a blend of several (no doubt already mixed) groups. The pair of Grimaldians were buried in a grave with a sprinkling of red ochre, @@ -2705,9 +2705,9 @@ also found about the shore of the Mediterranean basin, and it moved into northern Europe as the last glaciation pulled northward. People began making blade tools of very small size. They learned how to chip very slender and tiny blades from a prepared core. Then they made these -little blades into tiny triangles, half-moons (“lunates”), trapezoids, +little blades into tiny triangles, half-moons (�lunates�), trapezoids, and several other geometric forms. These little tools are called -“microliths.” They are so small that most of them must have been fixed +�microliths.� They are so small that most of them must have been fixed in handles or shafts. [Illustration: MICROLITHS @@ -2726,7 +2726,7 @@ One corner of each little triangle stuck out, and the whole thing made a fine barbed harpoon. In historic times in Egypt, geometric trapezoidal microliths were still in use as arrowheads. They were fastened--broad end out--on the end of an arrow shaft. It seems queer -to give an arrow a point shaped like a “T.” Actually, the little points +to give an arrow a point shaped like a �T.� Actually, the little points were very sharp, and must have pierced the hides of animals very easily. We also think that the broader cutting edge of the point may have caused more bleeding than a pointed arrowhead would. In hunting @@ -2739,7 +2739,7 @@ is some evidence that they appear early in the Near East. Their use was very common in northwest Africa but this came later. The microlith makers who reached south Russia and central Europe possibly moved up out of the Near East. Or it may have been the other way around; we -simply don’t yet know. +simply don�t yet know. Remember that the microliths we are talking about here were made from carefully prepared little blades, and are often geometric in outline. @@ -2749,7 +2749,7 @@ even some flake scrapers, in most microlithic industries. I emphasize this bladelet and the geometric character of the microlithic industries of the western Old World, since there has sometimes been confusion in the matter. Sometimes small flake chips, utilized as minute pointed -tools, have been called “microliths.” They may be _microlithic_ in size +tools, have been called �microliths.� They may be _microlithic_ in size in terms of the general meaning of the word, but they do not seem to belong to the sub-tradition of the blade tool preparation habits which we have been discussing here. @@ -2763,10 +2763,10 @@ in western Asia too, and early, although Professor Garrod is no longer sure that the whole tradition originated in the Near East. If you look again at my chart (p. 72) you will note that in western Asia I list some of the names of the western European industries, but with the -qualification “-like” (for example, “Gravettian-like”). The western +qualification �-like� (for example, �Gravettian-like�). The western Asiatic blade-tool industries do vaguely recall some aspects of those of western Europe, but we would probably be better off if we used -completely local names for them. The “Emiran” of my chart is such an +completely local names for them. The �Emiran� of my chart is such an example; its industry includes a long spike-like blade point which has no western European counterpart. @@ -2774,13 +2774,13 @@ When we last spoke of Africa (p. 66), I told you that stone tools there were continuing in the Levalloisian flake tradition, and were becoming smaller. At some time during this process, two new tool types appeared in northern Africa: one was the Aterian point with -a tang (p. 67), and the other was a sort of “laurel leaf” point, -called the “Sbaikian.” These two tool types were both produced from +a tang (p. 67), and the other was a sort of �laurel leaf� point, +called the �Sbaikian.� These two tool types were both produced from flakes. The Sbaikian points, especially, are roughly similar to some of the Solutrean points of Europe. It has been suggested that both the Sbaikian and Aterian points may be seen on their way to France through their appearance in the Spanish cave deposits of Parpallo, but there is -also a rival “pre-Solutrean” in central Europe. We still do not know +also a rival �pre-Solutrean� in central Europe. We still do not know whether there was any contact between the makers of these north African tools and the Solutrean tool-makers. What does seem clear is that the blade-tool tradition itself arrived late in northern Africa. @@ -2788,11 +2788,11 @@ blade-tool tradition itself arrived late in northern Africa. NETHER AFRICA -Blade tools and “laurel leaf” points and some other probably late +Blade tools and �laurel leaf� points and some other probably late stone tool types also appear in central and southern Africa. There are geometric microliths on bladelets and even some coarse pottery in east Africa. There is as yet no good way of telling just where these -items belong in time; in broad geological terms they are “late.” +items belong in time; in broad geological terms they are �late.� Some people have guessed that they are as early as similar European and Near Eastern examples, but I doubt it. The makers of small-sized Levalloisian flake tools occupied much of Africa until very late in @@ -2823,18 +2823,18 @@ ancestors of the American Indians came from Asia. The stone-tool traditions of Europe, Africa, the Near and Middle East, and central Siberia, did _not_ move into the New World. With only a very few special or late exceptions, there are _no_ core-bifaces, -flakes, or blade tools of the Old World. Such things just haven’t been +flakes, or blade tools of the Old World. Such things just haven�t been found here. -This is why I say it’s a shame we don’t know more of the end of the +This is why I say it�s a shame we don�t know more of the end of the chopper-tool tradition in the Far East. According to Weidenreich, the Mongoloids were in the Far East long before the end of the last glaciation. If the genetics of the blood group types do demand a non-Mongoloid ancestry for the American Indians, who else may have been in the Far East 25,000 years ago? We know a little about the habits for making stone tools which these first people brought with them, -and these habits don’t conform with those of the western Old World. -We’d better keep our eyes open for whatever happened to the end of +and these habits don�t conform with those of the western Old World. +We�d better keep our eyes open for whatever happened to the end of the chopper-tool tradition in northern China; already there are hints that it lasted late there. Also we should watch future excavations in eastern Siberia. Perhaps we shall find the chopper-tool tradition @@ -2846,13 +2846,13 @@ THE NEW ERA Perhaps it comes in part from the way I read the evidence and perhaps in part it is only intuition, but I feel that the materials of this chapter suggest a new era in the ways of life. Before about 40,000 -years ago, people simply “gathered” their food, wandering over large +years ago, people simply �gathered� their food, wandering over large areas to scavenge or to hunt in a simple sort of way. But here we -have seen them “settling-in” more, perhaps restricting themselves in +have seen them �settling-in� more, perhaps restricting themselves in their wanderings and adapting themselves to a given locality in more intensive ways. This intensification might be suggested by the word -“collecting.” The ways of life we described in the earlier chapters -were “food-gathering” ways, but now an era of “food-collecting” has +�collecting.� The ways of life we described in the earlier chapters +were �food-gathering� ways, but now an era of �food-collecting� has begun. We shall see further intensifications of it in the next chapter. @@ -2883,8 +2883,8 @@ The last great glaciation of the Ice Age was a two-part affair, with a sub-phase at the end of the second part. In Europe the last sub-phase of this glaciation commenced somewhere around 15,000 years ago. Then the glaciers began to melt back, for the last time. Remember that -Professor Antevs (p. 19) isn’t sure the Ice Age is over yet! This -melting sometimes went by fits and starts, and the weather wasn’t +Professor Antevs (p. 19) isn�t sure the Ice Age is over yet! This +melting sometimes went by fits and starts, and the weather wasn�t always changing for the better; but there was at least one time when European weather was even better than it is now. @@ -2927,16 +2927,16 @@ Sweden. Much of this north European material comes from bogs and swamps where it had become water-logged and has kept very well. Thus we have much more complete _assemblages_[4] than for any time earlier. - [4] “Assemblage” is a useful word when there are different kinds of + [4] �Assemblage� is a useful word when there are different kinds of archeological materials belonging together, from one area and of - one time. An assemblage is made up of a number of “industries” + one time. An assemblage is made up of a number of �industries� (that is, all the tools in chipped stone, all the tools in bone, all the tools in wood, the traces of houses, etc.) and everything else that manages to survive, such as the art, the burials, the bones of the animals used as food, and the traces of plant foods; in fact, everything that has been left to us and can be used to help reconstruct the lives of the people to - whom it once belonged. Our own present-day “assemblage” would be + whom it once belonged. Our own present-day �assemblage� would be the sum total of all the objects in our mail-order catalogues, department stores and supply houses of every sort, our churches, our art galleries and other buildings, together with our roads, @@ -2976,7 +2976,7 @@ found. It seems likely that the Maglemosian bog finds are remains of summer camps, and that in winter the people moved to higher and drier regions. -Childe calls them the “Forest folk”; they probably lived much the +Childe calls them the �Forest folk�; they probably lived much the same sort of life as did our pre-agricultural Indians of the north central states. They hunted small game or deer; they did a great deal of fishing; they collected what plant food they could find. In fact, @@ -3010,7 +3010,7 @@ South of the north European belt the hunting-food-collecting peoples were living on as best they could during this time. One interesting group, which seems to have kept to the regions of sandy soil and scrub forest, made great quantities of geometric microliths. These are the -materials called _Tardenoisian_. The materials of the “Forest folk” of +materials called _Tardenoisian_. The materials of the �Forest folk� of France and central Europe generally are called _Azilian_; Dr. Movius believes the term might best be restricted to the area south of the Loire River. @@ -3032,24 +3032,24 @@ to it than this. Professor Mathiassen of Copenhagen, who knows the archeological remains of this time very well, poses a question. He speaks of the material -as being neither rich nor progressive, in fact “rather stagnant,” but -he goes on to add that the people had a certain “receptiveness” and +as being neither rich nor progressive, in fact �rather stagnant,� but +he goes on to add that the people had a certain �receptiveness� and were able to adapt themselves quickly when the next change did come. -My own understanding of the situation is that the “Forest folk” made +My own understanding of the situation is that the �Forest folk� made nothing as spectacular as had the producers of the earlier Magdalenian assemblage and the Franco-Cantabrian art. On the other hand, they _seem_ to have been making many more different kinds of tools for many more different kinds of tasks than had their Ice Age forerunners. I -emphasize “seem” because the preservation in the Maglemosian bogs +emphasize �seem� because the preservation in the Maglemosian bogs is very complete; certainly we cannot list anywhere near as many different things for earlier times as we did for the Maglemosians (p. 94). I believe this experimentation with all kinds of new tools and gadgets, this intensification of adaptiveness (p. 91), this -“receptiveness,” even if it is still only pointed toward hunting, +�receptiveness,� even if it is still only pointed toward hunting, fishing, and food-collecting, is an important thing. Remember that the only marker we have handy for the _beginning_ of -this tendency toward “receptiveness” and experimentation is the +this tendency toward �receptiveness� and experimentation is the little microlithic blade tools of various geometric forms. These, we saw, began before the last ice had melted away, and they lasted on in use for a very long time. I wish there were a better marker than @@ -3063,7 +3063,7 @@ CHANGES IN OTHER AREAS? All this last section was about Europe. How about the rest of the world when the last glaciers were melting away? -We simply don’t know much about this particular time in other parts +We simply don�t know much about this particular time in other parts of the world except in Europe, the Mediterranean basin and the Middle East. People were certainly continuing to move into the New World by way of Siberia and the Bering Strait about this time. But for the @@ -3075,10 +3075,10 @@ clear information. REAL CHANGE AND PRELUDE IN THE NEAR EAST The appearance of the microliths and the developments made by the -“Forest folk” of northwestern Europe also mark an end. They show us +�Forest folk� of northwestern Europe also mark an end. They show us the terminal phase of the old food-collecting way of life. It grows increasingly clear that at about the same time that the Maglemosian and -other “Forest folk” were adapting themselves to hunting, fishing, and +other �Forest folk� were adapting themselves to hunting, fishing, and collecting in new ways to fit the post-glacial environment, something completely new was being made ready in western Asia. @@ -3098,7 +3098,7 @@ simply gathering or collecting it. When their food-production became reasonably effective, people could and did settle down in village-farming communities. With the appearance of the little farming villages, a new way of life was actually under way. Professor Childe -has good reason to speak of the “food-producing revolution,” for it was +has good reason to speak of the �food-producing revolution,� for it was indeed a revolution. @@ -3117,8 +3117,8 @@ before the _how_ and _why_ answers begin to appear. Anthropologically trained archeologists are fascinated with the cultures of men in times of great change. About ten or twelve thousand years ago, the general level of culture in many parts of the world seems to have been ready -for change. In northwestern Europe, we saw that cultures “changed -just enough so that they would not have to change.” We linked this to +for change. In northwestern Europe, we saw that cultures �changed +just enough so that they would not have to change.� We linked this to environmental changes with the coming of post-glacial times. In western Asia, we archeologists can prove that the food-producing @@ -3155,7 +3155,7 @@ living as the Maglemosians did? These are the questions we still have to face. -CULTURAL “RECEPTIVENESS” AND PROMISING ENVIRONMENTS +CULTURAL �RECEPTIVENESS� AND PROMISING ENVIRONMENTS Until the archeologists and the natural scientists--botanists, geologists, zoologists, and general ecologists--have spent many more @@ -3163,15 +3163,15 @@ years on the problem, we shall not have full _how_ and _why_ answers. I do think, however, that we are beginning to understand what to look for. We shall have to learn much more of what makes the cultures of men -“receptive” and experimental. Did change in the environment alone -force it? Was it simply a case of Professor Toynbee’s “challenge and -response?” I cannot believe the answer is quite that simple. Were it -so simple, we should want to know why the change hadn’t come earlier, +�receptive� and experimental. Did change in the environment alone +force it? Was it simply a case of Professor Toynbee�s �challenge and +response?� I cannot believe the answer is quite that simple. Were it +so simple, we should want to know why the change hadn�t come earlier, along with earlier environmental changes. We shall not know the answer, however, until we have excavated the traces of many more cultures of the time in question. We shall doubtless also have to learn more about, and think imaginatively about, the simpler cultures still left today. -The “mechanics” of culture in general will be bound to interest us. +The �mechanics� of culture in general will be bound to interest us. It will also be necessary to learn much more of the environments of 10,000 to 12,000 years ago. In which regions of the world were the @@ -3228,7 +3228,7 @@ THE OLD THEORY TOO SIMPLE FOR THE FACTS This theory was set up before we really knew anything in detail about the later prehistory of the Near and Middle East. We now know that -the facts which have been found don’t fit the old theory at all well. +the facts which have been found don�t fit the old theory at all well. Also, I have yet to find an American meteorologist who feels that we know enough about the changes in the weather pattern to say that it can have been so simple and direct. And, of course, the glacial ice which @@ -3238,7 +3238,7 @@ of great alpine glaciers, and long periods of warm weather in between. If the rain belt moved north as the glaciers melted for the last time, it must have moved in the same direction in earlier times. Thus, the forced neighborliness of men, plants, and animals in river valleys and -oases must also have happened earlier. Why didn’t domestication happen +oases must also have happened earlier. Why didn�t domestication happen earlier, then? Furthermore, it does not seem to be in the oases and river valleys @@ -3275,20 +3275,20 @@ archeologists, probably through habit, favor an old scheme of Grecized names for the subdivisions: paleolithic, mesolithic, neolithic. I refuse to use these words myself. They have meant too many different things to too many different people and have tended to hide some pretty -fuzzy thinking. Probably you haven’t even noticed my own scheme of -subdivision up to now, but I’d better tell you in general what it is. +fuzzy thinking. Probably you haven�t even noticed my own scheme of +subdivision up to now, but I�d better tell you in general what it is. I think of the earliest great group of archeological materials, from which we can deduce only a food-gathering way of culture, as the -_food-gathering stage_. I say “stage” rather than “age,” because it +_food-gathering stage_. I say �stage� rather than �age,� because it is not quite over yet; there are still a few primitive people in out-of-the-way parts of the world who remain in the _food-gathering stage_. In fact, Professor Julian Steward would probably prefer to call it a food-gathering _level_ of existence, rather than a stage. This would be perfectly acceptable to me. I also tend to find myself using _collecting_, rather than _gathering_, for the more recent aspects or -era of the stage, as the word “collecting” appears to have more sense -of purposefulness and specialization than does “gathering” (see p. +era of the stage, as the word �collecting� appears to have more sense +of purposefulness and specialization than does �gathering� (see p. 91). Now, while I think we could make several possible subdivisions of the @@ -3297,22 +3297,22 @@ believe the only one which means much to us here is the last or _terminal sub-era of food-collecting_ of the whole food-gathering stage. The microliths seem to mark its approach in the northwestern part of the Old World. It is really shown best in the Old World by -the materials of the “Forest folk,” the cultural adaptation to the +the materials of the �Forest folk,� the cultural adaptation to the post-glacial environment in northwestern Europe. We talked about -the “Forest folk” at the beginning of this chapter, and I used the +the �Forest folk� at the beginning of this chapter, and I used the Maglemosian assemblage of Denmark as an example. [5] It is difficult to find words which have a sequence or gradation of meaning with respect to both development and a range of time in the past, or with a range of time from somewhere in the past which is perhaps not yet ended. One standard Webster definition - of _stage_ is: “One of the steps into which the material - development of man ... is divided.” I cannot find any dictionary + of _stage_ is: �One of the steps into which the material + development of man ... is divided.� I cannot find any dictionary definition that suggests which of the words, _stage_ or _era_, has the meaning of a longer span of time. Therefore, I have chosen to let my eras be shorter, and to subdivide my stages - into eras. Webster gives _era_ as: “A signal stage of history, - an epoch.” When I want to subdivide my eras, I find myself using + into eras. Webster gives _era_ as: �A signal stage of history, + an epoch.� When I want to subdivide my eras, I find myself using _sub-eras_. Thus I speak of the _eras_ within a _stage_ and of the _sub-eras_ within an _era_; that is, I do so when I feel that I really have to, and when the evidence is clear enough to @@ -3328,9 +3328,9 @@ realms of culture. It is rather that for most of prehistoric time the materials left to the archeologists tend to limit our deductions to technology and economics. -I’m so soon out of my competence, as conventional ancient history +I�m so soon out of my competence, as conventional ancient history begins, that I shall only suggest the earlier eras of the -food-producing stage to you. This book is about prehistory, and I’m not +food-producing stage to you. This book is about prehistory, and I�m not a universal historian. @@ -3339,28 +3339,28 @@ THE TWO EARLIEST ERAS OF THE FOOD-PRODUCING STAGE The food-producing stage seems to appear in western Asia with really revolutionary suddenness. It is seen by the relative speed with which the traces of new crafts appear in the earliest village-farming -community sites we’ve dug. It is seen by the spread and multiplication +community sites we�ve dug. It is seen by the spread and multiplication of these sites themselves, and the remarkable growth in human -population we deduce from this increase in sites. We’ll look at some +population we deduce from this increase in sites. We�ll look at some of these sites and the archeological traces they yield in the next chapter. When such village sites begin to appear, I believe we are in the _era of the primary village-farming community_. I also believe this is the second era of the food-producing stage. The first era of the food-producing stage, I believe, was an _era of -incipient cultivation and animal domestication_. I keep saying “I -believe” because the actual evidence for this earlier era is so slight +incipient cultivation and animal domestication_. I keep saying �I +believe� because the actual evidence for this earlier era is so slight that one has to set it up mainly by playing a hunch for it. The reason for playing the hunch goes about as follows. One thing we seem to be able to see, in the food-collecting era in general, is a tendency for people to begin to settle down. This settling down seemed to become further intensified in the terminal -era. How this is connected with Professor Mathiassen’s “receptiveness” +era. How this is connected with Professor Mathiassen�s �receptiveness� and the tendency to be experimental, we do not exactly know. The evidence from the New World comes into play here as well as that from the Old World. With this settling down in one place, the people of the -terminal era--especially the “Forest folk” whom we know best--began +terminal era--especially the �Forest folk� whom we know best--began making a great variety of new things. I remarked about this earlier in the chapter. Dr. Robert M. Adams is of the opinion that this atmosphere of experimentation with new tools--with new ways of collecting food--is @@ -3368,9 +3368,9 @@ the kind of atmosphere in which one might expect trials at planting and at animal domestication to have been made. We first begin to find traces of more permanent life in outdoor camp sites, although caves were still inhabited at the beginning of the terminal era. It is not -surprising at all that the “Forest folk” had already domesticated the +surprising at all that the �Forest folk� had already domesticated the dog. In this sense, the whole era of food-collecting was becoming ready -and almost “incipient” for cultivation and animal domestication. +and almost �incipient� for cultivation and animal domestication. Northwestern Europe was not the place for really effective beginnings in agriculture and animal domestication. These would have had to take @@ -3425,13 +3425,13 @@ zone which surrounds the drainage basin of the Tigris and Euphrates Rivers at elevations of from approximately 2,000 to 5,000 feet. The lower alluvial land of the Tigris-Euphrates basin itself has very little rainfall. Some years ago Professor James Henry Breasted called -the alluvial lands of the Tigris-Euphrates a part of the “fertile -crescent.” These alluvial lands are very fertile if irrigated. Breasted +the alluvial lands of the Tigris-Euphrates a part of the �fertile +crescent.� These alluvial lands are very fertile if irrigated. Breasted was most interested in the oriental civilizations of conventional ancient history, and irrigation had been discovered before they appeared. -The country of hilly flanks above Breasted’s crescent receives from +The country of hilly flanks above Breasted�s crescent receives from 10 to 20 or more inches of winter rainfall each year, which is about what Kansas has. Above the hilly-flanks zone tower the peaks and ridges of the Lebanon-Amanus chain bordering the coast-line from Palestine @@ -3440,7 +3440,7 @@ range of the Iraq-Iran borderland. This rugged mountain frame for our hilly-flanks zone rises to some magnificent alpine scenery, with peaks of from ten to fifteen thousand feet in elevation. There are several gaps in the Mediterranean coastal portion of the frame, through which -the winter’s rain-bearing winds from the sea may break so as to carry +the winter�s rain-bearing winds from the sea may break so as to carry rain to the foothills of the Taurus and the Zagros. The picture I hope you will have from this description is that of an @@ -3482,7 +3482,7 @@ hilly-flanks zone in their wild state. With a single exception--that of the dog--the earliest positive evidence of domestication includes the two forms of wheat, the barley, and the goat. The evidence comes from within the hilly-flanks zone. -However, it comes from a settled village proper, Jarmo (which I’ll +However, it comes from a settled village proper, Jarmo (which I�ll describe in the next chapter), and is thus from the era of the primary village-farming community. We are still without positive evidence of domesticated grain and animals in the first era of the food-producing @@ -3534,9 +3534,9 @@ and the spread of ideas of people who had passed on into one of the more developed eras. In many cases, the terminal era of food-collecting was ended by the incoming of the food-producing peoples themselves. For example, the practices of food-production were carried into Europe -by the actual movement of some numbers of peoples (we don’t know how +by the actual movement of some numbers of peoples (we don�t know how many) who had reached at least the level of the primary village-farming -community. The “Forest folk” learned food-production from them. There +community. The �Forest folk� learned food-production from them. There was never an era of incipient cultivation and domestication proper in Europe, if my hunch is right. @@ -3547,16 +3547,16 @@ The way I see it, two things were required in order that an era of incipient cultivation and domestication could begin. First, there had to be the natural environment of a nuclear area, with its whole group of plants and animals capable of domestication. This is the aspect of -the matter which we’ve said is directly given by nature. But it is +the matter which we�ve said is directly given by nature. But it is quite possible that such an environment with such a group of plants and animals in it may have existed well before ten thousand years ago in the Near East. It is also quite possible that the same promising condition may have existed in regions which never developed into nuclear areas proper. Here, again, we come back to the cultural factor. -I think it was that “atmosphere of experimentation” we’ve talked about -once or twice before. I can’t define it for you, other than to say that +I think it was that �atmosphere of experimentation� we�ve talked about +once or twice before. I can�t define it for you, other than to say that by the end of the Ice Age, the general level of many cultures was ready -for change. Ask me how and why this was so, and I’ll tell you we don’t +for change. Ask me how and why this was so, and I�ll tell you we don�t know yet, and that if we did understand this kind of question, there would be no need for me to go on being a prehistorian! @@ -3590,7 +3590,7 @@ such collections for the modern wild forms of animals and plants from some of our nuclear areas. In the nuclear area in the Near East, some of the wild animals, at least, have already become extinct. There are no longer wild cattle or wild horses in western Asia. We know they were -there from the finds we’ve made in caves of late Ice Age times, and +there from the finds we�ve made in caves of late Ice Age times, and from some slightly later sites. @@ -3601,7 +3601,7 @@ incipient era of cultivation and animal domestication. I am closing this chapter with descriptions of two of the best Near Eastern examples I know of. You may not be satisfied that what I am able to describe makes a full-bodied era of development at all. Remember, however, that -I’ve told you I’m largely playing a kind of a hunch, and also that the +I�ve told you I�m largely playing a kind of a hunch, and also that the archeological materials of this era will always be extremely difficult to interpret. At the beginning of any new way of life, there will be a great tendency for people to make-do, at first, with tools and habits @@ -3613,7 +3613,7 @@ THE NATUFIAN, AN ASSEMBLAGE OF THE INCIPIENT ERA The assemblage called the Natufian comes from the upper layers of a number of caves in Palestine. Traces of its flint industry have also -turned up in Syria and Lebanon. We don’t know just how old it is. I +turned up in Syria and Lebanon. We don�t know just how old it is. I guess that it probably falls within five hundred years either way of about 5000 B.C. @@ -3662,7 +3662,7 @@ pendants. There were also beads and pendants of pierced teeth and shell. A number of Natufian burials have been found in the caves; some burials were grouped together in one grave. The people who were buried within the Mount Carmel cave were laid on their backs in an extended position, -while those on the terrace seem to have been “flexed” (placed in their +while those on the terrace seem to have been �flexed� (placed in their graves in a curled-up position). This may mean no more than that it was easier to dig a long hole in cave dirt than in the hard-packed dirt of the terrace. The people often had some kind of object buried with them, @@ -3679,7 +3679,7 @@ beads. GROUND STONE BONE] -The animal bones of the Natufian layers show beasts of a “modern” type, +The animal bones of the Natufian layers show beasts of a �modern� type, but with some differences from those of present-day Palestine. The bones of the gazelle far outnumber those of the deer; since gazelles like a much drier climate than deer, Palestine must then have had much @@ -3692,9 +3692,9 @@ Maglemosian of northern Europe. More recently, it has been reported that a domesticated goat is also part of the Natufian finds. The study of the human bones from the Natufian burials is not yet -complete. Until Professor McCown’s study becomes available, we may note -Professor Coon’s assessment that these people were of a “basically -Mediterranean type.” +complete. Until Professor McCown�s study becomes available, we may note +Professor Coon�s assessment that these people were of a �basically +Mediterranean type.� THE KARIM SHAHIR ASSEMBLAGE @@ -3704,11 +3704,11 @@ of a temporary open site or encampment. It lies on the top of a bluff in the Kurdish hill-country of northeastern Iraq. It was dug by Dr. Bruce Howe of the expedition I directed in 1950-51 for the Oriental Institute and the American Schools of Oriental Research. In 1954-55, -our expedition located another site, M’lefaat, with general resemblance +our expedition located another site, M�lefaat, with general resemblance to Karim Shahir, but about a hundred miles north of it. In 1956, Dr. Ralph Solecki located still another Karim Shahir type of site called Zawi Chemi Shanidar. The Zawi Chemi site has a radiocarbon date of 8900 -± 300 B.C. +� 300 B.C. Karim Shahir has evidence of only one very shallow level of occupation. It was probably not lived on very long, although the people who lived @@ -3717,7 +3717,7 @@ layer yielded great numbers of fist-sized cracked pieces of limestone, which had been carried up from the bed of a stream at the bottom of the bluff. We think these cracked stones had something to do with a kind of architecture, but we were unable to find positive traces of hut plans. -At M’lefaat and Zawi Chemi, there were traces of rounded hut plans. +At M�lefaat and Zawi Chemi, there were traces of rounded hut plans. As in the Natufian, the great bulk of small objects of the Karim Shahir assemblage was in chipped flint. A large proportion of the flint tools @@ -3737,7 +3737,7 @@ clay figurines which seemed to be of animal form. UNBAKED CLAY SHELL BONE - “ARCHITECTURE”] + �ARCHITECTURE�] Karim Shahir did not yield direct evidence of the kind of vegetable food its people ate. The animal bones showed a considerable @@ -3746,7 +3746,7 @@ domestication--sheep, goat, cattle, horse, dog--as compared with animal bones from the earlier cave sites of the area, which have a high proportion of bones of wild forms like deer and gazelle. But we do not know that any of the Karim Shahir animals were actually domesticated. -Some of them may have been, in an “incipient” way, but we have no means +Some of them may have been, in an �incipient� way, but we have no means at the moment that will tell us from the bones alone. @@ -3761,7 +3761,7 @@ goat, and the general animal situation at Karim Shahir to hint at an incipient approach to food-production. At Karim Shahir, there was the tendency to settle down out in the open; this is echoed by the new reports of open air Natufian sites. The large number of cracked stones -certainly indicates that it was worth the peoples’ while to have some +certainly indicates that it was worth the peoples� while to have some kind of structure, even if the site as a whole was short-lived. It is a part of my hunch that these things all point toward @@ -3771,13 +3771,13 @@ which we shall look at next, are fully food-producing, the Natufian and Karim Shahir folk had not yet arrived. I think they were part of a general build-up to full scale food-production. They were possibly controlling a few animals of several kinds and perhaps one or two -plants, without realizing the full possibilities of this “control” as a +plants, without realizing the full possibilities of this �control� as a new way of life. This is why I think of the Karim Shahir and Natufian folk as being at a level, or in an era, of incipient cultivation and domestication. But we shall have to do a great deal more excavation in this range of time -before we’ll get the kind of positive information we need. +before we�ll get the kind of positive information we need. SUMMARY @@ -3798,7 +3798,7 @@ history. We know the earliest village-farming communities appeared in western Asia, in a nuclear area. We do not yet know why the Near Eastern -experiment came first, or why it didn’t happen earlier in some other +experiment came first, or why it didn�t happen earlier in some other nuclear area. Apparently, the level of culture and the promise of the natural environment were ready first in western Asia. The next sites we look at will show a simple but effective food-production already @@ -3835,7 +3835,7 @@ contrast between food-collecting and food-producing as ways of life. THE DIFFERENCE BETWEEN FOOD-COLLECTORS AND FOOD-PRODUCERS -Childe used the word “revolution” because of the radical change that +Childe used the word �revolution� because of the radical change that took place in the habits and customs of man. Food-collectors--that is, hunters, fishers, berry- and nut-gatherers--had to live in small groups or bands, for they had to be ready to move wherever their food supply @@ -3851,7 +3851,7 @@ for clothing beyond the tools that were probably used to dress the skins of animals; no time to think of much of anything but food and protection and disposal of the dead when death did come: an existence which takes nature as it finds it, which does little or nothing to -modify nature--all in all, a savage’s existence, and a very tough one. +modify nature--all in all, a savage�s existence, and a very tough one. A man who spends his whole life following animals just to kill them to eat, or moving from one berry patch to another, is really living just like an animal himself. @@ -3859,10 +3859,10 @@ like an animal himself. THE FOOD-PRODUCING ECONOMY -Against this picture let me try to draw another--that of man’s life -after food-production had begun. His meat was stored “on the hoof,” +Against this picture let me try to draw another--that of man�s life +after food-production had begun. His meat was stored �on the hoof,� his grain in silos or great pottery jars. He lived in a house: it was -worth his while to build one, because he couldn’t move far from his +worth his while to build one, because he couldn�t move far from his fields and flocks. In his neighborhood enough food could be grown and enough animals bred so that many people were kept busy. They all lived close to their flocks and fields, in a village. The village was @@ -3872,7 +3872,7 @@ Children and old men could shepherd the animals by day or help with the lighter work in the fields. After the crops had been harvested the younger men might go hunting and some of them would fish, but the food they brought in was only an addition to the food in the village; the -villagers wouldn’t starve, even if the hunters and fishermen came home +villagers wouldn�t starve, even if the hunters and fishermen came home empty-handed. There was more time to do different things, too. They began to modify @@ -3885,23 +3885,23 @@ people in the village who were becoming full-time craftsmen. Other things were changing, too. The villagers must have had to agree on new rules for living together. The head man of the village had problems different from those of the chief of the small -food-collectors’ band. If somebody’s flock of sheep spoiled a wheat +food-collectors� band. If somebody�s flock of sheep spoiled a wheat field, the owner wanted payment for the grain he lost. The chief of the hunters was never bothered with such questions. Even the gods had changed. The spirits and the magic that had been used by hunters -weren’t of any use to the villagers. They needed gods who would watch +weren�t of any use to the villagers. They needed gods who would watch over the fields and the flocks, and they eventually began to erect buildings where their gods might dwell, and where the men who knew most about the gods might live. -WAS FOOD-PRODUCTION A “REVOLUTION”? +WAS FOOD-PRODUCTION A �REVOLUTION�? If you can see the difference between these two pictures--between life in the food-collecting stage and life after food-production -had begun--you’ll see why Professor Childe speaks of a revolution. -By revolution, he doesn’t mean that it happened over night or that -it happened only once. We don’t know exactly how long it took. Some +had begun--you�ll see why Professor Childe speaks of a revolution. +By revolution, he doesn�t mean that it happened over night or that +it happened only once. We don�t know exactly how long it took. Some people think that all these changes may have occurred in less than 500 years, but I doubt that. The incipient era was probably an affair of some duration. Once the level of the village-farming community had @@ -3915,7 +3915,7 @@ been achieved with truly revolutionary suddenness. GAPS IN OUR KNOWLEDGE OF THE NEAR EAST -If you’ll look again at the chart (p. 111) you’ll see that I have +If you�ll look again at the chart (p. 111) you�ll see that I have very few sites and assemblages to name in the incipient era of cultivation and domestication, and not many in the earlier part of the primary village-farming level either. Thanks in no small part @@ -3926,20 +3926,20 @@ yard-stick here. But I am far from being able to show you a series of Sears Roebuck catalogues, even century by century, for any part of the nuclear area. There is still a great deal of earth to move, and a great mass of material to recover and interpret before we even begin to -understand “how” and “why.” +understand �how� and �why.� Perhaps here, because this kind of archeology is really my specialty, -you’ll excuse it if I become personal for a moment. I very much look +you�ll excuse it if I become personal for a moment. I very much look forward to having further part in closing some of the gaps in knowledge -of the Near East. This is not, as I’ve told you, the spectacular +of the Near East. This is not, as I�ve told you, the spectacular range of Near Eastern archeology. There are no royal tombs, no gold, no great buildings or sculpture, no writing, in fact nothing to excite the normal museum at all. Nevertheless it is a range which, idea-wise, gives the archeologist tremendous satisfaction. The country of the hilly flanks is an exciting combination of green grasslands and mountainous ridges. The Kurds, who inhabit the part of the area -in which I’ve worked most recently, are an extremely interesting and -hospitable people. Archeologists don’t become rich, but I’ll forego +in which I�ve worked most recently, are an extremely interesting and +hospitable people. Archeologists don�t become rich, but I�ll forego the Cadillac for any bright spring morning in the Kurdish hills, on a good site with a happy crew of workmen and an interested and efficient staff. It is probably impossible to convey the full feeling which life @@ -3965,15 +3965,15 @@ like the use of pottery borrowed from the more developed era of the same time in the nuclear area. The same general explanation doubtless holds true for certain materials in Egypt, along the upper Nile and in the Kharga oasis: these materials, called Sebilian III, the Khartoum -“neolithic,” and the Khargan microlithic, are from surface sites, +�neolithic,� and the Khargan microlithic, are from surface sites, not from caves. The chart (p. 111) shows where I would place these materials in era and time. [Illustration: THE HILLY FLANKS OF THE CRESCENT AND EARLY SITES OF THE NEAR EAST] -Both M’lefaat and Dr. Solecki’s Zawi Chemi Shanidar site appear to have -been slightly more “settled in” than was Karim Shahir itself. But I do +Both M�lefaat and Dr. Solecki�s Zawi Chemi Shanidar site appear to have +been slightly more �settled in� than was Karim Shahir itself. But I do not think they belong to the era of farming-villages proper. The first site of this era, in the hills of Iraqi Kurdistan, is Jarmo, on which we have spent three seasons of work. Following Jarmo comes a variety of @@ -3989,9 +3989,9 @@ times when their various cultures flourished, there must have been many little villages which shared the same general assemblage. We are only now beginning to locate them again. Thus, if I speak of Jarmo, or Jericho, or Sialk as single examples of their particular kinds of -assemblages, I don’t mean that they were unique at all. I think I could +assemblages, I don�t mean that they were unique at all. I think I could take you to the sites of at least three more Jarmos, within twenty -miles of the original one. They are there, but they simply haven’t yet +miles of the original one. They are there, but they simply haven�t yet been excavated. In 1956, a Danish expedition discovered material of Jarmo type at Shimshara, only two dozen miles northeast of Jarmo, and below an assemblage of Hassunan type (which I shall describe presently). @@ -4000,15 +4000,15 @@ below an assemblage of Hassunan type (which I shall describe presently). THE GAP BETWEEN KARIM SHAHIR AND JARMO As we see the matter now, there is probably still a gap in the -available archeological record between the Karim Shahir-M’lefaat-Zawi +available archeological record between the Karim Shahir-M�lefaat-Zawi Chemi group (of the incipient era) and that of Jarmo (of the village-farming era). Although some items of the Jarmo type materials do reflect the beginnings of traditions set in the Karim Shahir group (see p. 120), there is not a clear continuity. Moreover--to the degree that we may trust a few radiocarbon dates--there would appear to be around two thousand years of difference in time. The single -available Zawi Chemi “date” is 8900 ± 300 B.C.; the most reasonable -group of “dates” from Jarmo average to about 6750 ± 200 B.C. I am +available Zawi Chemi �date� is 8900 � 300 B.C.; the most reasonable +group of �dates� from Jarmo average to about 6750 � 200 B.C. I am uncertain about this two thousand years--I do not think it can have been so long. @@ -4021,7 +4021,7 @@ JARMO, IN THE KURDISH HILLS, IRAQ The site of Jarmo has a depth of deposit of about twenty-seven feet, and approximately a dozen layers of architectural renovation and -change. Nevertheless it is a “one period” site: its assemblage remains +change. Nevertheless it is a �one period� site: its assemblage remains essentially the same throughout, although one or two new items are added in later levels. It covers about four acres of the top of a bluff, below which runs a small stream. Jarmo lies in the hill country @@ -4078,7 +4078,7 @@ human beings in clay; one type of human figurine they favored was that of a markedly pregnant woman, probably the expression of some sort of fertility spirit. They provided their house floors with baked-in-place depressions, either as basins or hearths, and later with domed ovens of -clay. As we’ve noted, the houses themselves were of clay or mud; one +clay. As we�ve noted, the houses themselves were of clay or mud; one could almost say they were built up like a house-sized pot. Then, finally, the idea of making portable pottery itself appeared, although I very much doubt that the people of the Jarmo village discovered the @@ -4095,11 +4095,11 @@ over three hundred miles to the north. Already a bulk carrying trade had been established--the forerunner of commerce--and the routes were set by which, in later times, the metal trade was to move. -There are now twelve radioactive carbon “dates” from Jarmo. The most -reasonable cluster of determinations averages to about 6750 ± 200 -B.C., although there is a completely unreasonable range of “dates” +There are now twelve radioactive carbon �dates� from Jarmo. The most +reasonable cluster of determinations averages to about 6750 � 200 +B.C., although there is a completely unreasonable range of �dates� running from 3250 to 9250 B.C.! _If_ I am right in what I take to be -“reasonable,” the first flush of the food-producing revolution had been +�reasonable,� the first flush of the food-producing revolution had been achieved almost nine thousand years ago. @@ -4117,7 +4117,7 @@ it, but the Hassunan sites seem to cluster at slightly lower elevations than those we have been talking about so far. The catalogue of the Hassuna assemblage is of course more full and -elaborate than that of Jarmo. The Iraqi government’s archeologists +elaborate than that of Jarmo. The Iraqi government�s archeologists who dug Hassuna itself, exposed evidence of increasing architectural know-how. The walls of houses were still formed of puddled mud; sun-dried bricks appear only in later periods. There were now several @@ -4130,16 +4130,16 @@ largely disappeared by Hassunan times. The flint work of the Hassunan catalogue is, by and large, a wretched affair. We might guess that the kinaesthetic concentration of the Hassuna craftsmen now went into other categories; that is, they suddenly discovered they might have more fun -working with the newer materials. It’s a shame, for example, that none +working with the newer materials. It�s a shame, for example, that none of their weaving is preserved for us. The two available radiocarbon determinations from Hassunan contexts -stand at about 5100 and 5600 B.C. ± 250 years. +stand at about 5100 and 5600 B.C. � 250 years. OTHER EARLY VILLAGE SITES IN THE NUCLEAR AREA -I’ll now name and very briefly describe a few of the other early +I�ll now name and very briefly describe a few of the other early village assemblages either in or adjacent to the hilly flanks of the crescent. Unfortunately, we do not have radioactive carbon dates for many of these materials. We may guess that some particular assemblage, @@ -4177,7 +4177,7 @@ ecological niche, some seven hundred feet below sea level; it is geographically within the hilly-flanks zone but environmentally not part of it. -Several radiocarbon “dates” for Jericho fall within the range of those +Several radiocarbon �dates� for Jericho fall within the range of those I find reasonable for Jarmo, and their internal statistical consistency is far better than that for the Jarmo determinations. It is not yet clear exactly what this means. @@ -4226,7 +4226,7 @@ how things were made are different; the Sialk assemblage represents still another cultural pattern. I suspect it appeared a bit later in time than did that of Hassuna. There is an important new item in the Sialk catalogue. The Sialk people made small drills or pins of -hammered copper. Thus the metallurgist’s specialized craft had made its +hammered copper. Thus the metallurgist�s specialized craft had made its appearance. There is at least one very early Iranian site on the inward slopes @@ -4246,7 +4246,7 @@ shore of the Fayum lake. The Fayum materials come mainly from grain bins or silos. Another site, Merimde, in the western part of the Nile delta, shows the remains of a true village, but it may be slightly later than the settlement of the Fayum. There are radioactive carbon -“dates” for the Fayum materials at about 4275 B.C. ± 320 years, which +�dates� for the Fayum materials at about 4275 B.C. � 320 years, which is almost fifteen hundred years later than the determinations suggested for the Hassunan or Syro-Cilician assemblages. I suspect that this is a somewhat over-extended indication of the time it took for the @@ -4260,13 +4260,13 @@ the mound called Shaheinab. The Shaheinab catalogue roughly corresponds to that of the Fayum; the distance between the two places, as the Nile flows, is roughly 1,500 miles. Thus it took almost a thousand years for the new way of life to be carried as far south into Africa as Khartoum; -the two Shaheinab “dates” average about 3300 B.C. ± 400 years. +the two Shaheinab �dates� average about 3300 B.C. � 400 years. If the movement was up the Nile (southward), as these dates suggest, then I suspect that the earliest available village material of middle Egypt, the so-called Tasian, is also later than that of the Fayum. The Tasian materials come from a few graves near a village called Deir -Tasa, and I have an uncomfortable feeling that the Tasian “assemblage” +Tasa, and I have an uncomfortable feeling that the Tasian �assemblage� may be mainly an artificial selection of poor examples of objects which belong in the following range of time. @@ -4280,7 +4280,7 @@ spread outward in space from the nuclear area, as time went on. There is good archeological evidence that both these processes took place. For the hill country of northeastern Iraq, in the nuclear area, we have already noticed how the succession (still with gaps) from Karim -Shahir, through M’lefaat and Jarmo, to Hassuna can be charted (see +Shahir, through M�lefaat and Jarmo, to Hassuna can be charted (see chart, p. 111). In the next chapter, we shall continue this charting and description of what happened in Iraq upward through time. We also watched traces of the new way of life move through space up the Nile @@ -4299,7 +4299,7 @@ appearance of the village-farming community there--is still an open one. In the last chapter, we noted the probability of an independent nuclear area in southeastern Asia. Professor Carl Sauer strongly champions the great importance of this area as _the_ original center -of agricultural pursuits, as a kind of “cradle” of all incipient eras +of agricultural pursuits, as a kind of �cradle� of all incipient eras of the Old World at least. While there is certainly not the slightest archeological evidence to allow us to go that far, we may easily expect that an early southeast Asian development would have been felt in @@ -4311,13 +4311,13 @@ way of life moved well beyond Khartoum in Africa. THE SPREAD OF THE VILLAGE-FARMING COMMUNITY WAY OF LIFE INTO EUROPE -How about Europe? I won’t give you many details. You can easily imagine +How about Europe? I won�t give you many details. You can easily imagine that the late prehistoric prelude to European history is a complicated affair. We all know very well how complicated an area Europe is now, with its welter of different languages and cultures. Remember, however, that a great deal of archeology has been done on the late prehistory of Europe, and very little on that of further Asia and Africa. If we knew -as much about these areas as we do of Europe, I expect we’d find them +as much about these areas as we do of Europe, I expect we�d find them just as complicated. This much is clear for Europe, as far as the spread of the @@ -4329,21 +4329,21 @@ in western Asia. I do not, of course, mean that there were traveling salesmen who carried these ideas and things to Europe with a commercial gleam in their eyes. The process took time, and the ideas and things must have been passed on from one group of people to the next. There -was also some actual movement of peoples, but we don’t know the size of +was also some actual movement of peoples, but we don�t know the size of the groups that moved. -The story of the “colonization” of Europe by the first farmers is +The story of the �colonization� of Europe by the first farmers is thus one of (1) the movement from the eastern Mediterranean lands of some people who were farmers; (2) the spread of ideas and things beyond the Near East itself and beyond the paths along which the -“colonists” moved; and (3) the adaptations of the ideas and things -by the indigenous “Forest folk”, about whose “receptiveness” Professor +�colonists� moved; and (3) the adaptations of the ideas and things +by the indigenous �Forest folk�, about whose �receptiveness� Professor Mathiassen speaks (p. 97). It is important to note that the resulting cultures in the new European environment were European, not Near -Eastern. The late Professor Childe remarked that “the peoples of the +Eastern. The late Professor Childe remarked that �the peoples of the West were not slavish imitators; they adapted the gifts from the East ... into a new and organic whole capable of developing on its own -original lines.” +original lines.� THE WAYS TO EUROPE @@ -4389,19 +4389,19 @@ Hill, the earliest known trace of village-farming communities in England, is about 2500 B.C. I would expect about 5500 B.C. to be a safe date to give for the well-developed early village communities of Syro-Cilicia. We suspect that the spread throughout Europe did not -proceed at an even rate. Professor Piggott writes that “at a date +proceed at an even rate. Professor Piggott writes that �at a date probably about 2600 B.C., simple agricultural communities were being established in Spain and southern France, and from the latter region a spread northwards can be traced ... from points on the French seaboard of the [English] Channel ... there were emigrations of a certain number of these tribes by boat, across to the chalk lands of Wessex and Sussex [in England], probably not more than three or four generations later -than the formation of the south French colonies.” +than the formation of the south French colonies.� New radiocarbon determinations are becoming available all the time--already several suggest that the food-producing way of life had reached the lower Rhine and Holland by 4000 B.C. But not all -prehistorians accept these “dates,” so I do not show them on my map +prehistorians accept these �dates,� so I do not show them on my map (p. 139). @@ -4427,7 +4427,7 @@ concentric sets of banks and ditches. Traces of oblong timber houses have been found, but not within the enclosures. The second type of structure is mine-shafts, dug down into the chalk beds where good flint for the making of axes or hoes could be found. The third type -of structure is long simple mounds or “unchambered barrows,” in one +of structure is long simple mounds or �unchambered barrows,� in one end of which burials were made. It has been commonly believed that the Windmill Hill assemblage belonged entirely to the cultural tradition which moved up through France to the Channel. Professor Piggott is now @@ -4443,12 +4443,12 @@ consists mainly of tombs and the contents of tombs, with only very rare settlement sites. The tombs were of some size and received the bodies of many people. The tombs themselves were built of stone, heaped over with earth; the stones enclosed a passage to a central chamber -(“passage graves”), or to a simple long gallery, along the sides of -which the bodies were laid (“gallery graves”). The general type of -construction is called “megalithic” (= great stone), and the whole +(�passage graves�), or to a simple long gallery, along the sides of +which the bodies were laid (�gallery graves�). The general type of +construction is called �megalithic� (= great stone), and the whole earth-mounded structure is often called a _barrow_. Since many have -proper chambers, in one sense or another, we used the term “unchambered -barrow” above to distinguish those of the Windmill Hill type from these +proper chambers, in one sense or another, we used the term �unchambered +barrow� above to distinguish those of the Windmill Hill type from these megalithic structures. There is some evidence for sacrifice, libations, and ceremonial fires, and it is clear that some form of community ritual was focused on the megalithic tombs. @@ -4466,7 +4466,7 @@ The third early British group of antiquities of this general time It is not so certain that the people who made this assemblage, called Peterborough, were actually farmers. While they may on occasion have practiced a simple agriculture, many items of their assemblage link -them closely with that of the “Forest folk” of earlier times in +them closely with that of the �Forest folk� of earlier times in England and in the Baltic countries. Their pottery is decorated with impressions of cords and is quite different from that of Windmill Hill and the megalithic builders. In addition, the distribution of their @@ -4479,7 +4479,7 @@ to acquire the raw material for stone axes. A probably slightly later culture, whose traces are best known from Skara Brae on Orkney, also had its roots in those cultures of the -Baltic area which fused out of the meeting of the “Forest folk” and +Baltic area which fused out of the meeting of the �Forest folk� and the peoples who took the eastern way into Europe. Skara Brae is very well preserved, having been built of thin stone slabs about which dune-sand drifted after the village died. The individual houses, the @@ -4498,14 +4498,14 @@ details which I have omitted in order to shorten the story. I believe some of the difficulty we have in understanding the establishment of the first farming communities in Europe is with -the word “colonization.” We have a natural tendency to think of -“colonization” as it has happened within the last few centuries. In the +the word �colonization.� We have a natural tendency to think of +�colonization� as it has happened within the last few centuries. In the case of the colonization of the Americas, for example, the colonists came relatively quickly, and in increasingly vast numbers. They had vastly superior technical, political, and war-making skills, compared with those of the Indians. There was not much mixing with the Indians. The case in Europe five or six thousand years ago must have been very -different. I wonder if it is even proper to call people “colonists” +different. I wonder if it is even proper to call people �colonists� who move some miles to a new region, settle down and farm it for some years, then move on again, generation after generation? The ideas and the things which these new people carried were only _potentially_ @@ -4521,12 +4521,12 @@ migrants were moving by boat, long distances may have been covered in a short time. Remember, however, we seem to have about three thousand years between the early Syro-Cilician villages and Windmill Hill. -Let me repeat Professor Childe again. “The peoples of the West were +Let me repeat Professor Childe again. �The peoples of the West were not slavish imitators: they adapted the gifts from the East ... into a new and organic whole capable of developing on its own original -lines.” Childe is of course completely conscious of the fact that his -“peoples of the West” were in part the descendants of migrants who came -originally from the “East,” bringing their “gifts” with them. This +lines.� Childe is of course completely conscious of the fact that his +�peoples of the West� were in part the descendants of migrants who came +originally from the �East,� bringing their �gifts� with them. This was the late prehistoric achievement of Europe--to take new ideas and things and some migrant peoples and, by mixing them with the old in its own environments, to forge a new and unique series of cultures. @@ -4553,14 +4553,14 @@ things first happened there and also because I know it best. There is another interesting thing, too. We have seen that the first experiment in village-farming took place in the Near East. So did -the first experiment in civilization. Both experiments “took.” The +the first experiment in civilization. Both experiments �took.� The traditions we live by today are based, ultimately, on those ancient beginnings in food-production and civilization in the Near East. -WHAT “CIVILIZATION” MEANS +WHAT �CIVILIZATION� MEANS -I shall not try to define “civilization” for you; rather, I shall +I shall not try to define �civilization� for you; rather, I shall tell you what the word brings to my mind. To me civilization means urbanization: the fact that there are cities. It means a formal political set-up--that there are kings or governing bodies that the @@ -4606,7 +4606,7 @@ of Mexico, the Mayas of Yucatan and Guatemala, and the Incas of the Andes were civilized. -WHY DIDN’T CIVILIZATION COME TO ALL FOOD-PRODUCERS? +WHY DIDN�T CIVILIZATION COME TO ALL FOOD-PRODUCERS? Once you have food-production, even at the well-advanced level of the village-farming community, what else has to happen before you @@ -4625,13 +4625,13 @@ early civilization, is still an open and very interesting question. WHERE CIVILIZATION FIRST APPEARED IN THE NEAR EAST You remember that our earliest village-farming communities lay along -the hilly flanks of a great “crescent.” (See map on p. 125.) -Professor Breasted’s “fertile crescent” emphasized the rich river +the hilly flanks of a great �crescent.� (See map on p. 125.) +Professor Breasted�s �fertile crescent� emphasized the rich river valleys of the Nile and the Tigris-Euphrates Rivers. Our hilly-flanks area of the crescent zone arches up from Egypt through Palestine and Syria, along southern Turkey into northern Iraq, and down along the southwestern fringe of Iran. The earliest food-producing villages we -know already existed in this area by about 6750 B.C. (± 200 years). +know already existed in this area by about 6750 B.C. (� 200 years). Now notice that this hilly-flanks zone does not include southern Mesopotamia, the alluvial land of the lower Tigris and Euphrates in @@ -4639,7 +4639,7 @@ Iraq, or the Nile Valley proper. The earliest known villages of classic Mesopotamia and Egypt seem to appear fifteen hundred or more years after those of the hilly-flanks zone. For example, the early Fayum village which lies near a lake west of the Nile Valley proper (see p. -135) has a radiocarbon date of 4275 B.C. ± 320 years. It was in the +135) has a radiocarbon date of 4275 B.C. � 320 years. It was in the river lands, however, that the immediate beginnings of civilization were made. @@ -4657,8 +4657,8 @@ THE HILLY-FLANKS ZONE VERSUS THE RIVER LANDS Why did these two civilizations spring up in these two river lands which apparently were not even part of the area where the -village-farming community began? Why didn’t we have the first -civilizations in Palestine, Syria, north Iraq, or Iran, where we’re +village-farming community began? Why didn�t we have the first +civilizations in Palestine, Syria, north Iraq, or Iran, where we�re sure food-production had had a long time to develop? I think the probable answer gives a clue to the ways in which civilization began in Egypt and Mesopotamia. @@ -4669,7 +4669,7 @@ and Syria. There are pleasant mountain slopes, streams running out to the sea, and rain, at least in the winter months. The rain belt and the foothills of the Turkish mountains also extend to northern Iraq and on to the Iranian plateau. The Iranian plateau has its mountain valleys, -streams, and some rain. These hilly flanks of the “crescent,” through +streams, and some rain. These hilly flanks of the �crescent,� through most of its arc, are almost made-to-order for beginning farmers. The grassy slopes of the higher hills would be pasture for their herds and flocks. As soon as the earliest experiments with agriculture and @@ -4720,10 +4720,10 @@ Obviously, we can no longer find the first dikes or reservoirs of the Nile Valley, or the first canals or ditches of Mesopotamia. The same land has been lived on far too long for any traces of the first attempts to be left; or, especially in Egypt, it has been covered by -the yearly deposits of silt, dropped by the river floods. But we’re +the yearly deposits of silt, dropped by the river floods. But we�re pretty sure the first food-producers of Egypt and southern Mesopotamia must have made such dikes, canals, and ditches. In the first place, -there can’t have been enough rain for them to grow things otherwise. +there can�t have been enough rain for them to grow things otherwise. In the second place, the patterns for such projects seem to have been pretty well set by historic times. @@ -4733,10 +4733,10 @@ CONTROL OF THE RIVERS THE BUSINESS OF EVERYONE Here, then, is a _part_ of the reason why civilization grew in Egypt and Mesopotamia first--not in Palestine, Syria, or Iran. In the latter areas, people could manage to produce their food as individuals. It -wasn’t too hard; there were rain and some streams, and good pasturage +wasn�t too hard; there were rain and some streams, and good pasturage for the animals even if a crop or two went wrong. In Egypt and Mesopotamia, people had to put in a much greater amount of work, and -this work couldn’t be individual work. Whole villages or groups of +this work couldn�t be individual work. Whole villages or groups of people had to turn out to fix dikes or dig ditches. The dikes had to be repaired and the ditches carefully cleared of silt each year, or they would become useless. @@ -4745,7 +4745,7 @@ There also had to be hard and fast rules. The person who lived nearest the ditch or the reservoir must not be allowed to take all the water and leave none for his neighbors. It was not only a business of learning to control the rivers and of making their waters do the -farmer’s work. It also meant controlling men. But once these men had +farmer�s work. It also meant controlling men. But once these men had managed both kinds of controls, what a wonderful yield they had! The soil was already fertile, and the silt which came in the floods and ditches kept adding fertile soil. @@ -4756,7 +4756,7 @@ THE GERM OF CIVILIZATION IN EGYPT AND MESOPOTAMIA This learning to work together for the common good was the real germ of the Egyptian and the Mesopotamian civilizations. The bare elements of civilization were already there: the need for a governing hand and for -laws to see that the communities’ work was done and that the water was +laws to see that the communities� work was done and that the water was justly shared. You may object that there is a sort of chicken and egg paradox in this idea. How could the people set up the rules until they had managed to get a way to live, and how could they manage to get a @@ -4781,12 +4781,12 @@ My explanation has been pointed particularly at Egypt and Mesopotamia. I have already told you that the irrigation and water-control part of it does not apply to the development of the Aztecs or the Mayas, or perhaps anybody else. But I think that a fair part of the story of -Egypt and Mesopotamia must be as I’ve just told you. +Egypt and Mesopotamia must be as I�ve just told you. I am particularly anxious that you do _not_ understand me to mean that irrigation _caused_ civilization. I am sure it was not that simple at all. For, in fact, a complex and highly engineered irrigation system -proper did not come until later times. Let’s say rather that the simple +proper did not come until later times. Let�s say rather that the simple beginnings of irrigation allowed and in fact encouraged a great number of things in the technological, political, social, and moral realms of culture. We do not yet understand what all these things were or how @@ -4842,7 +4842,7 @@ the mound which later became the holy Sumerian city of Eridu, Iraqi archeologists uncovered a handsome painted pottery. Pottery of the same type had been noticed earlier by German archeologists on the surface of a small mound, awash in the spring floods, near the remains of the -Biblical city of Erich (Sumerian = Uruk; Arabic = Warka). This “Eridu” +Biblical city of Erich (Sumerian = Uruk; Arabic = Warka). This �Eridu� pottery, which is about all we have of the assemblage of the people who once produced it, may be seen as a blend of the Samarran and Halafian painted pottery styles. This may over-simplify the case, but as yet we @@ -4864,7 +4864,7 @@ seems to move into place before the Halaf manifestation is finished, and to blend with it. The Ubaidian assemblage in the south is by far the more spectacular. The development of the temple has been traced at Eridu from a simple little structure to a monumental building some -62 feet long, with a pilaster-decorated façade and an altar in its +62 feet long, with a pilaster-decorated fa�ade and an altar in its central chamber. There is painted Ubaidian pottery, but the style is hurried and somewhat careless and gives the _impression_ of having been a cheap mass-production means of decoration when compared with the @@ -4879,7 +4879,7 @@ turtle-like faces are another item in the southern Ubaidian assemblage. There is a large Ubaid cemetery at Eridu, much of it still awaiting excavation. The few skeletons so far tentatively studied reveal a -completely modern type of “Mediterraneanoid”; the individuals whom the +completely modern type of �Mediterraneanoid�; the individuals whom the skeletons represent would undoubtedly blend perfectly into the modern population of southern Iraq. What the Ubaidian assemblage says to us is that these people had already adapted themselves and their culture to @@ -4925,7 +4925,7 @@ woven stuffs must have been the mediums of exchange. Over what area did the trading net-work of Ubaid extend? We start with the idea that the Ubaidian assemblage is most richly developed in the south. We assume, I think, correctly, that it represents a cultural flowering of the south. -On the basis of the pottery of the still elusive “Eridu” immigrants +On the basis of the pottery of the still elusive �Eridu� immigrants who had first followed the rivers into alluvial Mesopotamia, we get the notion that the characteristic painted pottery style of Ubaid was developed in the southland. If this reconstruction is correct @@ -4935,7 +4935,7 @@ assemblage of (and from the southern point of view, _fairly_ pure) Ubaidian material in northern Iraq. The pottery appears all along the Iranian flanks, even well east of the head of the Persian Gulf, and ends in a later and spectacular flourish in an extremely handsome -painted style called the “Susa” style. Ubaidian pottery has been noted +painted style called the �Susa� style. Ubaidian pottery has been noted up the valleys of both of the great rivers, well north of the Iraqi and Syrian borders on the southern flanks of the Anatolian plateau. It reaches the Mediterranean Sea and the valley of the Orontes in @@ -4965,10 +4965,10 @@ Mesopotamia. Next, much to our annoyance, we have what is almost a temporary black-out. According to the system of terminology I favor, our next -“assemblage” after that of Ubaid is called the _Warka_ phase, from +�assemblage� after that of Ubaid is called the _Warka_ phase, from the Arabic name for the site of Uruk or Erich. We know it only from six or seven levels in a narrow test-pit at Warka, and from an even -smaller hole at another site. This “assemblage,” so far, is known only +smaller hole at another site. This �assemblage,� so far, is known only by its pottery, some of which still bears Ubaidian style painting. The characteristic Warkan pottery is unpainted, with smoothed red or gray surfaces and peculiar shapes. Unquestionably, there must be a great @@ -4979,7 +4979,7 @@ have to excavate it! THE DAWN OF CIVILIZATION After our exasperation with the almost unknown Warka interlude, -following the brilliant “false dawn” of Ubaid, we move next to an +following the brilliant �false dawn� of Ubaid, we move next to an assemblage which yields traces of a preponderance of those elements which we noted (p. 144) as meaning civilization. This assemblage is that called _Proto-Literate_; it already contains writing. On @@ -4988,8 +4988,8 @@ history--and no longer prehistory--the assemblage is named for the historical implications of its content, and no longer after the name of the site where it was first found. Since some of the older books used site-names for this assemblage, I will tell you that the Proto-Literate -includes the latter half of what used to be called the “Uruk period” -_plus_ all of what used to be called the “Jemdet Nasr period.” It shows +includes the latter half of what used to be called the �Uruk period� +_plus_ all of what used to be called the �Jemdet Nasr period.� It shows a consistent development from beginning to end. I shall, in fact, leave much of the description and the historic @@ -5033,18 +5033,18 @@ mental block seems to have been removed. Clay tablets bearing pictographic signs are the Proto-Literate forerunners of cuneiform writing. The earliest examples are not well -understood but they seem to be “devices for making accounts and -for remembering accounts.” Different from the later case in Egypt, +understood but they seem to be �devices for making accounts and +for remembering accounts.� Different from the later case in Egypt, where writing appears fully formed in the earliest examples, the development from simple pictographic signs to proper cuneiform writing may be traced, step by step, in Mesopotamia. It is most probable that the development of writing was connected with the temple and -the need for keeping account of the temple’s possessions. Professor +the need for keeping account of the temple�s possessions. Professor Jacobsen sees writing as a means for overcoming space, time, and the -increasing complications of human affairs: “Literacy, which began +increasing complications of human affairs: �Literacy, which began with ... civilization, enhanced mightily those very tendencies in its development which characterize it as a civilization and mark it off as -such from other types of culture.” +such from other types of culture.� [Illustration: RELIEF ON A PROTO-LITERATE STONE VASE, WARKA @@ -5098,7 +5098,7 @@ civilized way of life. I suppose you could say that the difference in the approach is that as a prehistorian I have been looking forward or upward in time, while the -historians look backward to glimpse what I’ve been describing here. My +historians look backward to glimpse what I�ve been describing here. My base-line was half a million years ago with a being who had little more than the capacity to make tools and fire to distinguish him from the animals about him. Thus my point of view and that of the conventional @@ -5114,17 +5114,17 @@ End of PREHISTORY [Illustration] -You’ll doubtless easily recall your general course in ancient history: +You�ll doubtless easily recall your general course in ancient history: how the Sumerian dynasties of Mesopotamia were supplanted by those of Babylonia, how the Hittite kingdom appeared in Anatolian Turkey, and about the three great phases of Egyptian history. The literate kingdom of Crete arose, and by 1500 B.C. there were splendid fortified Mycenean towns on the mainland of Greece. This was the time--about the whole eastern end of the Mediterranean--of what Professor Breasted called the -“first great internationalism,” with flourishing trade, international +�first great internationalism,� with flourishing trade, international treaties, and royal marriages between Egyptians, Babylonians, and -Hittites. By 1200 B.C., the whole thing had fragmented: “the peoples of -the sea were restless in their isles,” and the great ancient centers in +Hittites. By 1200 B.C., the whole thing had fragmented: �the peoples of +the sea were restless in their isles,� and the great ancient centers in Egypt, Mesopotamia, and Anatolia were eclipsed. Numerous smaller states arose--Assyria, Phoenicia, Israel--and the Trojan war was fought. Finally Assyria became the paramount power of all the Near East, @@ -5135,7 +5135,7 @@ but casting them with its own tradition into a new mould, arose in mainland Greece. I once shocked my Classical colleagues to the core by referring to -Greece as “a second degree derived civilization,” but there is much +Greece as �a second degree derived civilization,� but there is much truth in this. The principles of bronze- and then of iron-working, of the alphabet, and of many other elements in Greek culture were borrowed from western Asia. Our debt to the Greeks is too well known for me even @@ -5146,7 +5146,7 @@ Greece fell in its turn to Rome, and in 55 B.C. Caesar invaded Britain. I last spoke of Britain on page 142; I had chosen it as my single example for telling you something of how the earliest farming communities were established in Europe. Now I will continue with -Britain’s later prehistory, so you may sense something of the end of +Britain�s later prehistory, so you may sense something of the end of prehistory itself. Remember that Britain is simply a single example we select; the same thing could be done for all the other countries of Europe, and will be possible also, some day, for further Asia and @@ -5186,20 +5186,20 @@ few Battle-axe folk elements, including, in fact, stone battle-axes, reached England with the earliest Beaker folk,[6] coming from the Rhineland. - [6] The British authors use the term “Beaker folk” to mean both + [6] The British authors use the term �Beaker folk� to mean both archeological assemblage and human physical type. They speak - of a “... tall, heavy-boned, rugged, and round-headed” strain + of a �... tall, heavy-boned, rugged, and round-headed� strain which they take to have developed, apparently in the Rhineland, by a mixture of the original (Spanish?) beaker-makers and the northeast European battle-axe makers. However, since the science of physical anthropology is very much in flux at the moment, and since I am not able to assess the evidence for these - physical types, I _do not_ use the term “folk” in this book with + physical types, I _do not_ use the term �folk� in this book with its usual meaning of standardized physical type. When I use - “folk” here, I mean simply _the makers of a given archeological + �folk� here, I mean simply _the makers of a given archeological assemblage_. The difficulty only comes when assemblages are named for some item in them; it is too clumsy to make an - adjective of the item and refer to a “beakerian” assemblage. + adjective of the item and refer to a �beakerian� assemblage. The Beaker folk settled earliest in the agriculturally fertile south and east. There seem to have been several phases of Beaker folk @@ -5211,7 +5211,7 @@ folk are known. They buried their dead singly, sometimes in conspicuous individual barrows with the dead warrior in his full trappings. The spectacular element in the assemblage of the Beaker folk is a group of large circular monuments with ditches and with uprights of wood or -stone. These “henges” became truly monumental several hundred years +stone. These �henges� became truly monumental several hundred years later; while they were occasionally dedicated with a burial, they were not primarily tombs. The effect of the invasion of the Beaker folk seems to cut across the whole fabric of life in Britain. @@ -5221,7 +5221,7 @@ seems to cut across the whole fabric of life in Britain. There was, however, a second major element in British life at this time. It shows itself in the less well understood traces of a group again called after one of the items in their catalogue, the Food-vessel -folk. There are many burials in these “food-vessel” pots in northern +folk. There are many burials in these �food-vessel� pots in northern England, Scotland, and Ireland, and the pottery itself seems to link back to that of the Peterborough assemblage. Like the earlier Peterborough people in the highland zone before them, the makers of @@ -5238,8 +5238,8 @@ MORE INVASIONS About 1500 B.C., the situation became further complicated by the arrival of new people in the region of southern England anciently called Wessex. The traces suggest the Brittany coast of France as a -source, and the people seem at first to have been a small but “heroic” -group of aristocrats. Their “heroes” are buried with wealth and +source, and the people seem at first to have been a small but �heroic� +group of aristocrats. Their �heroes� are buried with wealth and ceremony, surrounded by their axes and daggers of bronze, their gold ornaments, and amber and jet beads. These rich finds show that the trade-linkage these warriors patronized spread from the Baltic sources @@ -5265,10 +5265,10 @@ which must have been necessary before such a great monument could have been built. -“THIS ENGLAND” +�THIS ENGLAND� The range from 1900 to about 1400 B.C. includes the time of development -of the archeological features usually called the “Early Bronze Age” +of the archeological features usually called the �Early Bronze Age� in Britain. In fact, traces of the Wessex warriors persisted down to about 1200 B.C. The main regions of the island were populated, and the adjustments to the highland and lowland zones were distinct and well @@ -5279,7 +5279,7 @@ trading role, separated from the European continent but conveniently adjacent to it. The tin of Cornwall--so important in the production of good bronze--as well as the copper of the west and of Ireland, taken with the gold of Ireland and the general excellence of Irish -metal work, assured Britain a trader’s place in the then known world. +metal work, assured Britain a trader�s place in the then known world. Contacts with the eastern Mediterranean may have been by sea, with Cornish tin as the attraction, or may have been made by the Food-vessel middlemen on their trips to the Baltic coast. There they would have @@ -5292,9 +5292,9 @@ relative isolation gave some peace and also gave time for a leveling and further fusion of culture. The separate cultural traditions began to have more in common. The growing of barley, the herding of sheep and cattle, and the production of woolen garments were already features -common to all Britain’s inhabitants save a few in the remote highlands, +common to all Britain�s inhabitants save a few in the remote highlands, the far north, and the distant islands not yet fully touched by -food-production. The “personality of Britain” was being formed. +food-production. The �personality of Britain� was being formed. CREMATION BURIALS BEGIN @@ -5325,9 +5325,9 @@ which we shall mention below. The British cremation-burial-in-urns folk survived a long time in the highland zone. In the general British scheme, they make up what is -called the “Middle Bronze Age,” but in the highland zone they last +called the �Middle Bronze Age,� but in the highland zone they last until after 900 B.C. and are considered to be a specialized highland -“Late Bronze Age.” In the highland zone, these later cremation-burial +�Late Bronze Age.� In the highland zone, these later cremation-burial folk seem to have continued the older Food-vessel tradition of being middlemen in the metal market. @@ -5379,12 +5379,12 @@ to get a picture of estate or tribal boundaries which included village communities; we find a variety of tools in bronze, and even whetstones which show that iron has been honed on them (although the scarce iron has not been found). Let me give you the picture in Professor S. -Piggott’s words: “The ... Late Bronze Age of southern England was but +Piggott�s words: �The ... Late Bronze Age of southern England was but the forerunner of the earliest Iron Age in the same region, not only in the techniques of agriculture, but almost certainly in terms of ethnic kinship ... we can with some assurance talk of the Celts ... the great early Celtic expansion of the Continent is recognized to be that of the -Urnfield people.” +Urnfield people.� Thus, certainly by 500 B.C., there were people in Britain, some of whose descendants we may recognize today in name or language in remote @@ -5399,11 +5399,11 @@ efficient set of tools than does bronze. Iron tools seem first to have been made in quantity in Hittite Anatolia about 1500 B.C. In continental Europe, the earliest, so-called Hallstatt, iron-using cultures appeared in Germany soon after 750 B.C. Somewhat later, -Greek and especially Etruscan exports of _objets d’art_--which moved +Greek and especially Etruscan exports of _objets d�art_--which moved with a flourishing trans-Alpine wine trade--influenced the Hallstatt iron-working tradition. Still later new classical motifs, together with older Hallstatt, oriental, and northern nomad motifs, gave rise to a -new style in metal decoration which characterizes the so-called La Tène +new style in metal decoration which characterizes the so-called La T�ne phase. A few iron users reached Britain a little before 400 B.C. Not long @@ -5422,7 +5422,7 @@ HILL-FORTS AND FARMS The earliest iron-users seem to have entrenched themselves temporarily within hill-top forts, mainly in the south. Gradually, they moved inland, establishing _individual_ farm sites with extensive systems -of rectangular fields. We recognize these fields by the “lynchets” or +of rectangular fields. We recognize these fields by the �lynchets� or lines of soil-creep which plowing left on the slopes of hills. New crops appeared; there were now bread wheat, oats, and rye, as well as barley. @@ -5434,7 +5434,7 @@ various outbuildings and pits for the storage of grain. Weaving was done on the farm, but not blacksmithing, which must have been a specialized trade. Save for the lack of firearms, the place might almost be taken for a farmstead on the American frontier in the early -1800’s. +1800�s. Toward 250 B.C. there seems to have been a hasty attempt to repair the hill-forts and to build new ones, evidently in response to signs of @@ -5446,9 +5446,9 @@ THE SECOND PHASE Perhaps the hill-forts were not entirely effective or perhaps a compromise was reached. In any case, the newcomers from the Marne district did establish themselves, first in the southeast and then to -the north and west. They brought iron with decoration of the La Tène +the north and west. They brought iron with decoration of the La T�ne type and also the two-wheeled chariot. Like the Wessex warriors of -over a thousand years earlier, they made “heroes’” graves, with their +over a thousand years earlier, they made �heroes�� graves, with their warriors buried in the war-chariots and dressed in full trappings. [Illustration: CELTIC BUCKLE] @@ -5457,7 +5457,7 @@ The metal work of these Marnian newcomers is excellent. The peculiar Celtic art style, based originally on the classic tendril motif, is colorful and virile, and fits with Greek and Roman descriptions of Celtic love of color in dress. There is a strong trace of these -newcomers northward in Yorkshire, linked by Ptolemy’s description to +newcomers northward in Yorkshire, linked by Ptolemy�s description to the Parisii, doubtless part of the Celtic tribe which originally gave its name to Paris on the Seine. Near Glastonbury, in Somerset, two villages in swamps have been excavated. They seem to date toward the @@ -5469,7 +5469,7 @@ villagers. In Scotland, which yields its first iron tools at a date of about 100 B.C., and in northern Ireland even slightly earlier, the effects of the -two phases of newcomers tend especially to blend. Hill-forts, “brochs” +two phases of newcomers tend especially to blend. Hill-forts, �brochs� (stone-built round towers) and a variety of other strange structures seem to appear as the new ideas develop in the comparative isolation of northern Britain. @@ -5493,27 +5493,27 @@ at last, we can even begin to speak of dynasties and individuals. Some time before 55 B.C., the Catuvellauni, originally from the Marne district in France, had possessed themselves of a large part of southeastern England. They evidently sailed up the Thames and built a -town of over a hundred acres in area. Here ruled Cassivellaunus, “the -first man in England whose name we know,” and whose town Caesar sacked. +town of over a hundred acres in area. Here ruled Cassivellaunus, �the +first man in England whose name we know,� and whose town Caesar sacked. The town sprang up elsewhere again, however. THE END OF PREHISTORY Prehistory, strictly speaking, is now over in southern Britain. -Claudius’ effective invasion took place in 43 A.D.; by 83 A.D., a raid +Claudius� effective invasion took place in 43 A.D.; by 83 A.D., a raid had been made as far north as Aberdeen in Scotland. But by 127 A.D., Hadrian had completed his wall from the Solway to the Tyne, and the Romans settled behind it. In Scotland, Romanization can have affected -the countryside very little. Professor Piggott adds that “... it is +the countryside very little. Professor Piggott adds that �... it is when the pressure of Romanization is relaxed by the break-up of the Dark Ages that we see again the Celtic metal-smiths handling their material with the same consummate skill as they had before the Roman Conquest, and with traditional styles that had not even then forgotten -their Marnian and Belgic heritage.” +their Marnian and Belgic heritage.� In fact, many centuries go by, in Britain as well as in the rest of -Europe, before the archeologist’s task is complete and the historian on +Europe, before the archeologist�s task is complete and the historian on his own is able to describe the ways of men in the past. @@ -5524,7 +5524,7 @@ you will have noticed how often I had to refer to the European continent itself. Britain, beyond the English Channel for all of her later prehistory, had a much simpler course of events than did most of the rest of Europe in later prehistoric times. This holds, in spite -of all the “invasions” and “reverberations” from the continent. Most +of all the �invasions� and �reverberations� from the continent. Most of Europe was the scene of an even more complicated ebb and flow of cultural change, save in some of its more remote mountain valleys and peninsulas. @@ -5536,7 +5536,7 @@ accounts and some good general accounts of part of the range from about 3000 B.C. to A.D. 1. I suspect that the difficulty of making a good book that covers all of its later prehistory is another aspect of what makes Europe so very complicated a continent today. The prehistoric -foundations for Europe’s very complicated set of civilizations, +foundations for Europe�s very complicated set of civilizations, cultures, and sub-cultures--which begin to appear as history proceeds--were in themselves very complicated. @@ -5552,8 +5552,8 @@ of their journeys. But by the same token, they had had time en route to take on their characteristic European aspects. Some time ago, Sir Cyril Fox wrote a famous book called _The -Personality of Britain_, sub-titled “Its Influence on Inhabitant and -Invader in Prehistoric and Early Historic Times.” We have not gone +Personality of Britain_, sub-titled �Its Influence on Inhabitant and +Invader in Prehistoric and Early Historic Times.� We have not gone into the post-Roman early historic period here; there are still the Anglo-Saxons and Normans to account for as well as the effects of the Romans. But what I have tried to do was to begin the story of @@ -5570,7 +5570,7 @@ Summary In the pages you have read so far, you have been brought through the -earliest 99 per cent of the story of man’s life on this planet. I have +earliest 99 per cent of the story of man�s life on this planet. I have left only 1 per cent of the story for the historians to tell. @@ -5601,7 +5601,7 @@ But I think there may have been a few. Certainly the pace of the first act accelerated with the swing from simple gathering to more intensified collecting. The great cave art of France and Spain was probably an expression of a climax. Even the ideas of burying the dead -and of the “Venus” figurines must also point to levels of human thought +and of the �Venus� figurines must also point to levels of human thought and activity that were over and above pure food-getting. @@ -5629,7 +5629,7 @@ five thousand years after the second act began. But it could never have happened in the first act at all. There is another curious thing about the first act. Many of the players -didn’t know it was over and they kept on with their roles long after +didn�t know it was over and they kept on with their roles long after the second act had begun. On the edges of the stage there are today some players who are still going on with the first act. The Eskimos, and the native Australians, and certain tribes in the Amazon jungle are @@ -5680,20 +5680,20 @@ act may have lessons for us and give depth to our thinking. I know there are at least _some_ lessons, even in the present incomplete state of our knowledge. The players who began the second act--that of food-production--separately, in different parts of the world, were not -all of one “pure race” nor did they have “pure” cultural traditions. +all of one �pure race� nor did they have �pure� cultural traditions. Some apparently quite mixed Mediterraneans got off to the first start on the second act and brought it to its first two climaxes as well. Peoples of quite different physical type achieved the first climaxes in China and in the New World. In our British example of how the late prehistory of Europe worked, we -listed a continuous series of “invasions” and “reverberations.” After +listed a continuous series of �invasions� and �reverberations.� After each of these came fusion. Even though the Channel protected Britain from some of the extreme complications of the mixture and fusion of continental Europe, you can see how silly it would be to refer to a -“pure” British race or a “pure” British culture. We speak of the United -States as a “melting pot.” But this is nothing new. Actually, Britain -and all the rest of the world have been “melting pots” at one time or +�pure� British race or a �pure� British culture. We speak of the United +States as a �melting pot.� But this is nothing new. Actually, Britain +and all the rest of the world have been �melting pots� at one time or another. By the time the written records of Mesopotamia and Egypt begin to turn @@ -5703,12 +5703,12 @@ itself, we are thrown back on prehistoric archeology. And this is as true for China, India, Middle America, and the Andes, as it is for the Near East. -There are lessons to be learned from all of man’s past, not simply +There are lessons to be learned from all of man�s past, not simply lessons of how to fight battles or win peace conferences, but of how human society evolves from one stage to another. Many of these lessons can only be looked for in the prehistoric past. So far, we have only made a beginning. There is much still to do, and many gaps in the story -are yet to be filled. The prehistorian’s job is to find the evidence, +are yet to be filled. The prehistorian�s job is to find the evidence, to fill the gaps, and to discover the lessons men have learned in the past. As I see it, this is not only an exciting but a very practical goal for which to strive. @@ -5745,7 +5745,7 @@ paperbound books.) GEOCHRONOLOGY AND THE ICE AGE -(Two general books. Some Pleistocene geologists disagree with Zeuner’s +(Two general books. Some Pleistocene geologists disagree with Zeuner�s interpretation of the dating evidence, but their points of view appear in professional journals, in articles too cumbersome to list here.) @@ -5815,7 +5815,7 @@ GENERAL PREHISTORY Press. Movius, Hallam L., Jr. - “Old World Prehistory: Paleolithic” in _Anthropology Today_. + �Old World Prehistory: Paleolithic� in _Anthropology Today_. Kroeber, A. L., ed. 1953. University of Chicago Press. Oakley, Kenneth P. @@ -5826,7 +5826,7 @@ GENERAL PREHISTORY _British Prehistory._ 1949. Oxford University Press. Pittioni, Richard - _Die Urgeschichtlichen Grundlagen der Europäischen Kultur._ + _Die Urgeschichtlichen Grundlagen der Europ�ischen Kultur._ 1949. Deuticke. (A single book which does attempt to cover the whole range of European prehistory to ca. 1 A.D.) @@ -5834,7 +5834,7 @@ GENERAL PREHISTORY THE NEAR EAST Adams, Robert M. - “Developmental Stages in Ancient Mesopotamia,” _in_ Steward, + �Developmental Stages in Ancient Mesopotamia,� _in_ Steward, Julian, _et al_, _Irrigation Civilizations: A Comparative Study_. 1955. Pan American Union. @@ -6000,7 +6000,7 @@ Index Bolas, 54 - Bordes, François, 62 + Bordes, Fran�ois, 62 Borer, 77 @@ -6028,7 +6028,7 @@ Index killed by stampede, 86 Burials, 66, 86; - in “henges,” 164; + in �henges,� 164; in urns, 168 Burins, 75 @@ -6085,7 +6085,7 @@ Index Combe Capelle, 30 - Combe Capelle-Brünn group, 34 + Combe Capelle-Br�nn group, 34 Commont, Victor, 51 @@ -6097,7 +6097,7 @@ Index Corrals for cattle, 140 - “Cradle of mankind,” 136 + �Cradle of mankind,� 136 Cremation, 167 @@ -6123,7 +6123,7 @@ Index Domestication, of animals, 100, 105, 107; of plants, 100 - “Dragon teeth” fossils in China, 28 + �Dragon teeth� fossils in China, 28 Drill, 77 @@ -6176,9 +6176,9 @@ Index Fayum, 135; radiocarbon date, 146 - “Fertile Crescent,” 107, 146 + �Fertile Crescent,� 107, 146 - Figurines, “Venus,” 84; + Figurines, �Venus,� 84; at Jarmo, 128; at Ubaid, 153 @@ -6197,7 +6197,7 @@ Index Flint industry, 127 - Fontéchevade, 32, 56, 58 + Font�chevade, 32, 56, 58 Food-collecting, 104, 121; end of, 104 @@ -6223,7 +6223,7 @@ Index Food-vessel folk, 164 - “Forest folk,” 97, 98, 104, 110 + �Forest folk,� 97, 98, 104, 110 Fox, Sir Cyril, 174 @@ -6379,7 +6379,7 @@ Index Land bridges in Mediterranean, 19 - La Tène phase, 170 + La T�ne phase, 170 Laurel leaf point, 78, 89 @@ -6404,7 +6404,7 @@ Index Mammoth, 93; in cave art, 85 - “Man-apes,” 26 + �Man-apes,� 26 Mango, 107 @@ -6435,7 +6435,7 @@ Index Microliths, 87; at Jarmo, 130; - “lunates,” 87; + �lunates,� 87; trapezoids, 87; triangles, 87 @@ -6443,7 +6443,7 @@ Index Mine-shafts, 140 - M’lefaat, 126, 127 + M�lefaat, 126, 127 Mongoloids, 29, 90 @@ -6453,9 +6453,9 @@ Index Mount Carmel, 11, 33, 52, 59, 64, 69, 113, 114 - “Mousterian man,” 64 + �Mousterian man,� 64 - “Mousterian” tools, 61, 62; + �Mousterian� tools, 61, 62; of Acheulean tradition, 62 Movius, H. L., 47 @@ -6471,7 +6471,7 @@ Index Near East, beginnings of civilization in, 20, 144; cave sites, 58; climate in Ice Age, 99; - “Fertile Crescent,” 107, 146; + �Fertile Crescent,� 107, 146; food-production in, 99; Natufian assemblage in, 113-115; stone tools, 114 @@ -6539,7 +6539,7 @@ Index Pig, wild, 108 - “Piltdown man,” 29 + �Piltdown man,� 29 Pins, 80 @@ -6578,7 +6578,7 @@ Index Race, 35; biological, 36; - “pure,” 16 + �pure,� 16 Radioactivity, 9, 10 @@ -6795,7 +6795,7 @@ Index Writing, 158; cuneiform, 158 - Würm I glaciation, 58 + W�rm I glaciation, 58 Zebu cattle, domestication of, 107 @@ -6810,7 +6810,7 @@ Index -Transcriber’s note: +Transcriber�s note: Punctuation, hyphenation, and spelling were made consistent when a predominant preference was found in this book; otherwise they were not diff --git a/ciphers/rail_fence_cipher.py b/ciphers/rail_fence_cipher.py index 47ee7db89831..5b2311a115e4 100644 --- a/ciphers/rail_fence_cipher.py +++ b/ciphers/rail_fence_cipher.py @@ -1,4 +1,4 @@ -""" https://en.wikipedia.org/wiki/Rail_fence_cipher """ +"""https://en.wikipedia.org/wiki/Rail_fence_cipher""" def encrypt(input_string: str, key: int) -> str: diff --git a/ciphers/rsa_cipher.py b/ciphers/rsa_cipher.py index de26992f5eeb..ac9782a49fff 100644 --- a/ciphers/rsa_cipher.py +++ b/ciphers/rsa_cipher.py @@ -76,10 +76,9 @@ def encrypt_and_write_to_file( key_size, n, e = read_key_file(key_filename) if key_size < block_size * 8: sys.exit( - "ERROR: Block size is %s bits and key size is %s bits. The RSA cipher " - "requires the block size to be equal to or greater than the key size. " - "Either decrease the block size or use different keys." - % (block_size * 8, key_size) + f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} " + "bits. The RSA cipher requires the block size to be equal to or greater " + "than the key size. Either decrease the block size or use different keys." ) encrypted_blocks = [str(i) for i in encrypt_message(message, (n, e), block_size)] @@ -101,10 +100,9 @@ def read_from_file_and_decrypt(message_filename: str, key_filename: str) -> str: if key_size < block_size * 8: sys.exit( - "ERROR: Block size is %s bits and key size is %s bits. The RSA cipher " - "requires the block size to be equal to or greater than the key size. " - "Did you specify the correct key file and encrypted file?" - % (block_size * 8, key_size) + f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} " + "bits. The RSA cipher requires the block size to be equal to or greater " + "than the key size. Were the correct key file and encrypted file specified?" ) encrypted_blocks = [] diff --git a/ciphers/rsa_factorization.py b/ciphers/rsa_factorization.py index 9ee52777ed83..585b21fac856 100644 --- a/ciphers/rsa_factorization.py +++ b/ciphers/rsa_factorization.py @@ -3,10 +3,13 @@ The program can efficiently factor RSA prime number given the private key d and public key e. -Source: on page 3 of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf -More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html + +| Source: on page ``3`` of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf +| More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html + large number can take minutes to factor, therefore are not included in doctest. """ + from __future__ import annotations import math @@ -16,13 +19,14 @@ def rsafactor(d: int, e: int, n: int) -> list[int]: """ This function returns the factors of N, where p*q=N - Return: [p, q] + + Return: [p, q] We call N the RSA modulus, e the encryption exponent, and d the decryption exponent. The pair (N, e) is the public key. As its name suggests, it is public and is used to - encrypt messages. + encrypt messages. The pair (N, d) is the secret key or private key and is known only to the recipient - of encrypted messages. + of encrypted messages. >>> rsafactor(3, 16971, 25777) [149, 173] diff --git a/ciphers/rsa_key_generator.py b/ciphers/rsa_key_generator.py index 2573ed01387b..44970e8cbc15 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -2,8 +2,9 @@ import random import sys -from . import cryptomath_module as cryptoMath # noqa: N812 -from . import rabin_miller as rabinMiller # noqa: N812 +from maths.greatest_common_divisor import gcd_by_iterative + +from . import cryptomath_module, rabin_miller def main() -> None: @@ -13,20 +14,26 @@ def main() -> None: def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]: - print("Generating prime p...") - p = rabinMiller.generate_large_prime(key_size) - print("Generating prime q...") - q = rabinMiller.generate_large_prime(key_size) + """ + >>> random.seed(0) # for repeatability + >>> public_key, private_key = generate_key(8) + >>> public_key + (26569, 239) + >>> private_key + (26569, 2855) + """ + p = rabin_miller.generate_large_prime(key_size) + q = rabin_miller.generate_large_prime(key_size) n = p * q - print("Generating e that is relatively prime to (p - 1) * (q - 1)...") + # Generate e that is relatively prime to (p - 1) * (q - 1) while True: e = random.randrange(2 ** (key_size - 1), 2 ** (key_size)) - if cryptoMath.gcd(e, (p - 1) * (q - 1)) == 1: + if gcd_by_iterative(e, (p - 1) * (q - 1)) == 1: break - print("Calculating d that is mod inverse of e...") - d = cryptoMath.find_mod_inverse(e, (p - 1) * (q - 1)) + # Calculate d that is mod inverse of e + d = cryptomath_module.find_mod_inverse(e, (p - 1) * (q - 1)) public_key = (n, e) private_key = (n, d) diff --git a/ciphers/running_key_cipher.py b/ciphers/running_key_cipher.py new file mode 100644 index 000000000000..6bda417be898 --- /dev/null +++ b/ciphers/running_key_cipher.py @@ -0,0 +1,75 @@ +""" +https://en.wikipedia.org/wiki/Running_key_cipher +""" + + +def running_key_encrypt(key: str, plaintext: str) -> str: + """ + Encrypts the plaintext using the Running Key Cipher. + + :param key: The running key (long piece of text). + :param plaintext: The plaintext to be encrypted. + :return: The ciphertext. + """ + plaintext = plaintext.replace(" ", "").upper() + key = key.replace(" ", "").upper() + key_length = len(key) + ciphertext = [] + ord_a = ord("A") + + for i, char in enumerate(plaintext): + p = ord(char) - ord_a + k = ord(key[i % key_length]) - ord_a + c = (p + k) % 26 + ciphertext.append(chr(c + ord_a)) + + return "".join(ciphertext) + + +def running_key_decrypt(key: str, ciphertext: str) -> str: + """ + Decrypts the ciphertext using the Running Key Cipher. + + :param key: The running key (long piece of text). + :param ciphertext: The ciphertext to be decrypted. + :return: The plaintext. + """ + ciphertext = ciphertext.replace(" ", "").upper() + key = key.replace(" ", "").upper() + key_length = len(key) + plaintext = [] + ord_a = ord("A") + + for i, char in enumerate(ciphertext): + c = ord(char) - ord_a + k = ord(key[i % key_length]) - ord_a + p = (c - k) % 26 + plaintext.append(chr(p + ord_a)) + + return "".join(plaintext) + + +def test_running_key_encrypt() -> None: + """ + >>> key = "How does the duck know that? said Victor" + >>> ciphertext = running_key_encrypt(key, "DEFEND THIS") + >>> running_key_decrypt(key, ciphertext) == "DEFENDTHIS" + True + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + test_running_key_encrypt() + + plaintext = input("Enter the plaintext: ").upper() + print(f"\n{plaintext = }") + + key = "How does the duck know that? said Victor" + encrypted_text = running_key_encrypt(key, plaintext) + print(f"{encrypted_text = }") + + decrypted_text = running_key_decrypt(key, encrypted_text) + print(f"{decrypted_text = }") diff --git a/ciphers/simple_keyword_cypher.py b/ciphers/simple_keyword_cypher.py index 1635471aebd1..bde137d826c3 100644 --- a/ciphers/simple_keyword_cypher.py +++ b/ciphers/simple_keyword_cypher.py @@ -1,16 +1,18 @@ def remove_duplicates(key: str) -> str: """ Removes duplicate alphabetic characters in a keyword (letter is ignored after its - first appearance). + first appearance). + :param key: Keyword to use :return: String with duplicates removed + >>> remove_duplicates('Hello World!!') 'Helo Wrd' """ key_no_dups = "" for ch in key: - if ch == " " or ch not in key_no_dups and ch.isalpha(): + if ch == " " or (ch not in key_no_dups and ch.isalpha()): key_no_dups += ch return key_no_dups @@ -18,6 +20,7 @@ def remove_duplicates(key: str) -> str: def create_cipher_map(key: str) -> dict[str, str]: """ Returns a cipher map given a keyword. + :param key: keyword to use :return: dictionary cipher map """ @@ -43,9 +46,11 @@ def create_cipher_map(key: str) -> dict[str, str]: def encipher(message: str, cipher_map: dict[str, str]) -> str: """ Enciphers a message given a cipher map. + :param message: Message to encipher :param cipher_map: Cipher map :return: enciphered string + >>> encipher('Hello World!!', create_cipher_map('Goodbye!!')) 'CYJJM VMQJB!!' """ @@ -55,9 +60,11 @@ def encipher(message: str, cipher_map: dict[str, str]) -> str: def decipher(message: str, cipher_map: dict[str, str]) -> str: """ Deciphers a message given a cipher map + :param message: Message to decipher :param cipher_map: Dictionary mapping to use :return: Deciphered string + >>> cipher_map = create_cipher_map('Goodbye!!') >>> decipher(encipher('Hello World!!', cipher_map), cipher_map) 'HELLO WORLD!!' @@ -70,6 +77,7 @@ def decipher(message: str, cipher_map: dict[str, str]) -> str: def main() -> None: """ Handles I/O + :return: void """ message = input("Enter message to encode or decode: ").strip() diff --git a/ciphers/trafid_cipher.py b/ciphers/trafid_cipher.py deleted file mode 100644 index 108ac652f0e4..000000000000 --- a/ciphers/trafid_cipher.py +++ /dev/null @@ -1,135 +0,0 @@ -# https://en.wikipedia.org/wiki/Trifid_cipher -from __future__ import annotations - - -def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str: - one, two, three = "", "", "" - tmp = [] - - for character in message_part: - tmp.append(character_to_number[character]) - - for each in tmp: - one += each[0] - two += each[1] - three += each[2] - - return one + two + three - - -def __decrypt_part( - message_part: str, character_to_number: dict[str, str] -) -> tuple[str, str, str]: - tmp, this_part = "", "" - result = [] - - for character in message_part: - this_part += character_to_number[character] - - for digit in this_part: - tmp += digit - if len(tmp) == len(message_part): - result.append(tmp) - tmp = "" - - return result[0], result[1], result[2] - - -def __prepare( - message: str, alphabet: str -) -> tuple[str, str, dict[str, str], dict[str, str]]: - # Validate message and alphabet, set to upper and remove spaces - alphabet = alphabet.replace(" ", "").upper() - message = message.replace(" ", "").upper() - - # Check length and characters - if len(alphabet) != 27: - raise KeyError("Length of alphabet has to be 27.") - for each in message: - if each not in alphabet: - raise ValueError("Each message character has to be included in alphabet!") - - # Generate dictionares - numbers = ( - "111", - "112", - "113", - "121", - "122", - "123", - "131", - "132", - "133", - "211", - "212", - "213", - "221", - "222", - "223", - "231", - "232", - "233", - "311", - "312", - "313", - "321", - "322", - "323", - "331", - "332", - "333", - ) - character_to_number = {} - number_to_character = {} - for letter, number in zip(alphabet, numbers): - character_to_number[letter] = number - number_to_character[number] = letter - - return message, alphabet, character_to_number, number_to_character - - -def encrypt_message( - message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 -) -> str: - message, alphabet, character_to_number, number_to_character = __prepare( - message, alphabet - ) - encrypted, encrypted_numeric = "", "" - - for i in range(0, len(message) + 1, period): - encrypted_numeric += __encrypt_part( - message[i : i + period], character_to_number - ) - - for i in range(0, len(encrypted_numeric), 3): - encrypted += number_to_character[encrypted_numeric[i : i + 3]] - - return encrypted - - -def decrypt_message( - message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 -) -> str: - message, alphabet, character_to_number, number_to_character = __prepare( - message, alphabet - ) - decrypted_numeric = [] - decrypted = "" - - for i in range(0, len(message) + 1, period): - a, b, c = __decrypt_part(message[i : i + period], character_to_number) - - for j in range(0, len(a)): - decrypted_numeric.append(a[j] + b[j] + c[j]) - - for each in decrypted_numeric: - decrypted += number_to_character[each] - - return decrypted - - -if __name__ == "__main__": - msg = "DEFEND THE EAST WALL OF THE CASTLE." - encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") - decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") - print(f"Encrypted: {encrypted}\nDecrypted: {decrypted}") diff --git a/ciphers/transposition_cipher.py b/ciphers/transposition_cipher.py index f1f07ddc3f35..76178cb6a1bc 100644 --- a/ciphers/transposition_cipher.py +++ b/ciphers/transposition_cipher.py @@ -52,10 +52,8 @@ def decrypt_message(key: int, message: str) -> str: plain_text[col] += symbol col += 1 - if ( - (col == num_cols) - or (col == num_cols - 1) - and (row >= num_rows - num_shaded_boxes) + if (col == num_cols) or ( + (col == num_cols - 1) and (row >= num_rows - num_shaded_boxes) ): col = 0 row += 1 diff --git a/ciphers/transposition_cipher_encrypt_decrypt_file.py b/ciphers/transposition_cipher_encrypt_decrypt_file.py index 6296b1e6d709..b9630243d7f3 100644 --- a/ciphers/transposition_cipher_encrypt_decrypt_file.py +++ b/ciphers/transposition_cipher_encrypt_decrypt_file.py @@ -6,8 +6,8 @@ def main() -> None: - input_file = "Prehistoric Men.txt" - output_file = "Output.txt" + input_file = "./prehistoric_men.txt" + output_file = "./Output.txt" key = int(input("Enter key: ")) mode = input("Encrypt/Decrypt [e/d]: ") diff --git a/ciphers/trifid_cipher.py b/ciphers/trifid_cipher.py new file mode 100644 index 000000000000..9613cee0669d --- /dev/null +++ b/ciphers/trifid_cipher.py @@ -0,0 +1,215 @@ +""" +The trifid cipher uses a table to fractionate each plaintext letter into a trigram, +mixes the constituents of the trigrams, and then applies the table in reverse to turn +these mixed trigrams into ciphertext letters. + +https://en.wikipedia.org/wiki/Trifid_cipher +""" + +from __future__ import annotations + +# fmt: off +TEST_CHARACTER_TO_NUMBER = { + "A": "111", "B": "112", "C": "113", "D": "121", "E": "122", "F": "123", "G": "131", + "H": "132", "I": "133", "J": "211", "K": "212", "L": "213", "M": "221", "N": "222", + "O": "223", "P": "231", "Q": "232", "R": "233", "S": "311", "T": "312", "U": "313", + "V": "321", "W": "322", "X": "323", "Y": "331", "Z": "332", "+": "333", +} +# fmt: off + +TEST_NUMBER_TO_CHARACTER = {val: key for key, val in TEST_CHARACTER_TO_NUMBER.items()} + + +def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str: + """ + Arrange the triagram value of each letter of `message_part` vertically and join + them horizontally. + + >>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER) + '132111112' + """ + one, two, three = "", "", "" + for each in (character_to_number[character] for character in message_part): + one += each[0] + two += each[1] + three += each[2] + + return one + two + three + + +def __decrypt_part( + message_part: str, character_to_number: dict[str, str] +) -> tuple[str, str, str]: + """ + Convert each letter of the input string into their respective trigram values, join + them and split them into three equal groups of strings which are returned. + + >>> __decrypt_part('ABCDE', TEST_CHARACTER_TO_NUMBER) + ('11111', '21131', '21122') + """ + this_part = "".join(character_to_number[character] for character in message_part) + result = [] + tmp = "" + for digit in this_part: + tmp += digit + if len(tmp) == len(message_part): + result.append(tmp) + tmp = "" + + return result[0], result[1], result[2] + + +def __prepare( + message: str, alphabet: str +) -> tuple[str, str, dict[str, str], dict[str, str]]: + """ + A helper function that generates the triagrams and assigns each letter of the + alphabet to its corresponding triagram and stores this in a dictionary + (`character_to_number` and `number_to_character`) after confirming if the + alphabet's length is ``27``. + + >>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+') + >>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+', + ... TEST_CHARACTER_TO_NUMBER, TEST_NUMBER_TO_CHARACTER) + >>> test == expected + True + + Testing with incomplete alphabet + + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw') + Traceback (most recent call last): + ... + KeyError: 'Length of alphabet has to be 27.' + + Testing with extra long alphabets + + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd') + Traceback (most recent call last): + ... + KeyError: 'Length of alphabet has to be 27.' + + Testing with punctuations that are not in the given alphabet + + >>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+') + Traceback (most recent call last): + ... + ValueError: Each message character has to be included in alphabet! + + Testing with numbers + + >>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+') + Traceback (most recent call last): + ... + AttributeError: 'int' object has no attribute 'replace' + """ + # Validate message and alphabet, set to upper and remove spaces + alphabet = alphabet.replace(" ", "").upper() + message = message.replace(" ", "").upper() + + # Check length and characters + if len(alphabet) != 27: + raise KeyError("Length of alphabet has to be 27.") + if any(char not in alphabet for char in message): + raise ValueError("Each message character has to be included in alphabet!") + + # Generate dictionares + character_to_number = dict(zip(alphabet, TEST_CHARACTER_TO_NUMBER.values())) + number_to_character = { + number: letter for letter, number in character_to_number.items() + } + + return message, alphabet, character_to_number, number_to_character + + +def encrypt_message( + message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 +) -> str: + """ + encrypt_message + =============== + + Encrypts a message using the trifid_cipher. Any punctuatuions that + would be used should be added to the alphabet. + + PARAMETERS + ---------- + + * `message`: The message you want to encrypt. + * `alphabet` (optional): The characters to be used for the cipher . + * `period` (optional): The number of characters you want in a group whilst + encrypting. + + >>> encrypt_message('I am a boy') + 'BCDGBQY' + + >>> encrypt_message(' ') + '' + + >>> encrypt_message(' aide toi le c iel ta id era ', + ... 'FELIXMARDSTBCGHJKNOPQUVWYZ+',5) + 'FMJFVOISSUFTFPUFEQQC' + + """ + message, alphabet, character_to_number, number_to_character = __prepare( + message, alphabet + ) + + encrypted_numeric = "" + for i in range(0, len(message) + 1, period): + encrypted_numeric += __encrypt_part( + message[i : i + period], character_to_number + ) + + encrypted = "" + for i in range(0, len(encrypted_numeric), 3): + encrypted += number_to_character[encrypted_numeric[i : i + 3]] + return encrypted + + +def decrypt_message( + message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 +) -> str: + """ + decrypt_message + =============== + + Decrypts a trifid_cipher encrypted message. + + PARAMETERS + ---------- + + * `message`: The message you want to decrypt. + * `alphabet` (optional): The characters used for the cipher. + * `period` (optional): The number of characters used in grouping when it + was encrypted. + + >>> decrypt_message('BCDGBQY') + 'IAMABOY' + + Decrypting with your own alphabet and period + + >>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5) + 'AIDETOILECIELTAIDERA' + """ + message, alphabet, character_to_number, number_to_character = __prepare( + message, alphabet + ) + + decrypted_numeric = [] + for i in range(0, len(message), period): + a, b, c = __decrypt_part(message[i : i + period], character_to_number) + + for j in range(len(a)): + decrypted_numeric.append(a[j] + b[j] + c[j]) + + return "".join(number_to_character[each] for each in decrypted_numeric) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + msg = "DEFEND THE EAST WALL OF THE CASTLE." + encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") + decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") + print(f"Encrypted: {encrypted}\nDecrypted: {decrypted}") diff --git a/ciphers/vernam_cipher.py b/ciphers/vernam_cipher.py new file mode 100644 index 000000000000..197f28635a1c --- /dev/null +++ b/ciphers/vernam_cipher.py @@ -0,0 +1,42 @@ +def vernam_encrypt(plaintext: str, key: str) -> str: + """ + >>> vernam_encrypt("HELLO","KEY") + 'RIJVS' + """ + ciphertext = "" + for i in range(len(plaintext)): + ct = ord(key[i % len(key)]) - 65 + ord(plaintext[i]) - 65 + while ct > 25: + ct = ct - 26 + ciphertext += chr(65 + ct) + return ciphertext + + +def vernam_decrypt(ciphertext: str, key: str) -> str: + """ + >>> vernam_decrypt("RIJVS","KEY") + 'HELLO' + """ + decrypted_text = "" + for i in range(len(ciphertext)): + ct = ord(ciphertext[i]) - ord(key[i % len(key)]) + while ct < 0: + ct = 26 + ct + decrypted_text += chr(65 + ct) + return decrypted_text + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + # Example usage + plaintext = "HELLO" + key = "KEY" + encrypted_text = vernam_encrypt(plaintext, key) + decrypted_text = vernam_decrypt(encrypted_text, key) + print("\n\n") + print("Plaintext:", plaintext) + print("Encrypted:", encrypted_text) + print("Decrypted:", decrypted_text) diff --git a/ciphers/xor_cipher.py b/ciphers/xor_cipher.py index 379ef0ef7e50..24d88a0fd588 100644 --- a/ciphers/xor_cipher.py +++ b/ciphers/xor_cipher.py @@ -1,21 +1,22 @@ """ - author: Christian Bender - date: 21.12.2017 - class: XORCipher - - This class implements the XOR-cipher algorithm and provides - some useful methods for encrypting and decrypting strings and - files. - - Overview about methods - - - encrypt : list of char - - decrypt : list of char - - encrypt_string : str - - decrypt_string : str - - encrypt_file : boolean - - decrypt_file : boolean +author: Christian Bender +date: 21.12.2017 +class: XORCipher + +This class implements the XOR-cipher algorithm and provides +some useful methods for encrypting and decrypting strings and +files. + +Overview about methods + +- encrypt : list of char +- decrypt : list of char +- encrypt_string : str +- decrypt_string : str +- encrypt_file : boolean +- decrypt_file : boolean """ + from __future__ import annotations @@ -35,15 +36,32 @@ def encrypt(self, content: str, key: int) -> list[str]: output: encrypted string 'content' as a list of chars if key not passed the method uses the key by the constructor. otherwise key = 1 + + Empty list + >>> XORCipher().encrypt("", 5) + [] + + One key + >>> XORCipher().encrypt("hallo welt", 1) + ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u'] + + Normal key + >>> XORCipher().encrypt("HALLO WELT", 32) + ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't'] + + Key greater than 255 + >>> XORCipher().encrypt("hallo welt", 256) + ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't'] """ # precondition - assert isinstance(key, int) and isinstance(content, str) + assert isinstance(key, int) + assert isinstance(content, str) key = key or self.__key or 1 # make sure key is an appropriate size - key %= 255 + key %= 256 return [chr(ord(ch) ^ key) for ch in content] @@ -53,15 +71,32 @@ def decrypt(self, content: str, key: int) -> list[str]: output: decrypted string 'content' as a list of chars if key not passed the method uses the key by the constructor. otherwise key = 1 + + Empty list + >>> XORCipher().decrypt("", 5) + [] + + One key + >>> XORCipher().decrypt("hallo welt", 1) + ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u'] + + Normal key + >>> XORCipher().decrypt("HALLO WELT", 32) + ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't'] + + Key greater than 255 + >>> XORCipher().decrypt("hallo welt", 256) + ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't'] """ # precondition - assert isinstance(key, int) and isinstance(content, list) + assert isinstance(key, int) + assert isinstance(content, str) key = key or self.__key or 1 # make sure key is an appropriate size - key %= 255 + key %= 256 return [chr(ord(ch) ^ key) for ch in content] @@ -71,16 +106,32 @@ def encrypt_string(self, content: str, key: int = 0) -> str: output: encrypted string 'content' if key not passed the method uses the key by the constructor. otherwise key = 1 + + Empty list + >>> XORCipher().encrypt_string("", 5) + '' + + One key + >>> XORCipher().encrypt_string("hallo welt", 1) + 'i`mmn!vdmu' + + Normal key + >>> XORCipher().encrypt_string("HALLO WELT", 32) + 'hallo\\x00welt' + + Key greater than 255 + >>> XORCipher().encrypt_string("hallo welt", 256) + 'hallo welt' """ # precondition - assert isinstance(key, int) and isinstance(content, str) + assert isinstance(key, int) + assert isinstance(content, str) key = key or self.__key or 1 - # make sure key can be any size - while key > 255: - key -= 255 + # make sure key is an appropriate size + key %= 256 # This will be returned ans = "" @@ -96,16 +147,32 @@ def decrypt_string(self, content: str, key: int = 0) -> str: output: decrypted string 'content' if key not passed the method uses the key by the constructor. otherwise key = 1 + + Empty list + >>> XORCipher().decrypt_string("", 5) + '' + + One key + >>> XORCipher().decrypt_string("hallo welt", 1) + 'i`mmn!vdmu' + + Normal key + >>> XORCipher().decrypt_string("HALLO WELT", 32) + 'hallo\\x00welt' + + Key greater than 255 + >>> XORCipher().decrypt_string("hallo welt", 256) + 'hallo welt' """ # precondition - assert isinstance(key, int) and isinstance(content, str) + assert isinstance(key, int) + assert isinstance(content, str) key = key or self.__key or 1 - # make sure key can be any size - while key > 255: - key -= 255 + # make sure key is an appropriate size + key %= 256 # This will be returned ans = "" @@ -125,14 +192,17 @@ def encrypt_file(self, file: str, key: int = 0) -> bool: """ # precondition - assert isinstance(file, str) and isinstance(key, int) + assert isinstance(file, str) + assert isinstance(key, int) + + # make sure key is an appropriate size + key %= 256 try: - with open(file) as fin: - with open("encrypt.out", "w+") as fout: - # actual encrypt-process - for line in fin: - fout.write(self.encrypt_string(line, key)) + with open(file) as fin, open("encrypt.out", "w+") as fout: + # actual encrypt-process + for line in fin: + fout.write(self.encrypt_string(line, key)) except OSError: return False @@ -149,14 +219,17 @@ def decrypt_file(self, file: str, key: int) -> bool: """ # precondition - assert isinstance(file, str) and isinstance(key, int) + assert isinstance(file, str) + assert isinstance(key, int) + + # make sure key is an appropriate size + key %= 256 try: - with open(file) as fin: - with open("decrypt.out", "w+") as fout: - # actual encrypt-process - for line in fin: - fout.write(self.decrypt_string(line, key)) + with open(file) as fin, open("decrypt.out", "w+") as fout: + # actual encrypt-process + for line in fin: + fout.write(self.decrypt_string(line, key)) except OSError: return False @@ -164,6 +237,11 @@ def decrypt_file(self, file: str, key: int) -> bool: return True +if __name__ == "__main__": + from doctest import testmod + + testmod() + # Tests # crypt = XORCipher() # key = 67 diff --git a/compression/README.md b/compression/README.md index cf54ea986175..bad7ae1a2f76 100644 --- a/compression/README.md +++ b/compression/README.md @@ -1,9 +1,9 @@ # Compression Data compression is everywhere, you need it to store data without taking too much space. -Either the compression lose some data (then we talk about lossy compression, such as .jpg) or it does not (and then it is lossless compression, such as .png) +Either the compression loses some data (then we talk about lossy compression, such as .jpg) or it does not (and then it is lossless compression, such as .png) -Lossless compression is mainly used for archive purpose as it allow storing data without losing information about the file archived. On the other hand, lossy compression is used for transfer of file where quality isn't necessarily what is required (i.e: images on Twitter). +Lossless compression is mainly used for archive purpose as it allows storing data without losing information about the file archived. On the other hand, lossy compression is used for transfer of file where quality isn't necessarily what is required (i.e: images on Twitter). * * diff --git a/compression/burrows_wheeler.py b/compression/burrows_wheeler.py index 0916b8a654d2..857d677c904e 100644 --- a/compression/burrows_wheeler.py +++ b/compression/burrows_wheeler.py @@ -1,7 +1,7 @@ """ https://en.wikipedia.org/wiki/Burrows%E2%80%93Wheeler_transform -The Burrows–Wheeler transform (BWT, also called block-sorting compression) +The Burrows-Wheeler transform (BWT, also called block-sorting compression) rearranges a character string into runs of similar characters. This is useful for compression, since it tends to be easy to compress a string that has runs of repeated characters by techniques such as move-to-front transform and @@ -10,6 +10,7 @@ original character. The BWT is thus a "free" method of improving the efficiency of text compression algorithms, costing only some extra computation. """ + from __future__ import annotations from typing import TypedDict @@ -150,7 +151,7 @@ def reverse_bwt(bwt_string: str, idx_original_string: int) -> str: raise ValueError("The parameter idx_original_string must not be lower than 0.") if idx_original_string >= len(bwt_string): raise ValueError( - "The parameter idx_original_string must be lower than" " len(bwt_string)." + "The parameter idx_original_string must be lower than len(bwt_string)." ) ordered_rotations = [""] * len(bwt_string) diff --git a/compression/huffman.py b/compression/huffman.py index 65e5c2f25385..44eda6c03180 100644 --- a/compression/huffman.py +++ b/compression/huffman.py @@ -40,7 +40,7 @@ def build_tree(letters: list[Letter]) -> Letter | TreeNode: Run through the list of Letters and build the min heap for the Huffman Tree. """ - response: list[Letter | TreeNode] = letters # type: ignore + response: list[Letter | TreeNode] = list(letters) while len(response) > 1: left = response.pop(0) right = response.pop(0) @@ -59,7 +59,7 @@ def traverse_tree(root: Letter | TreeNode, bitstring: str) -> list[Letter]: if isinstance(root, Letter): root.bitstring[root.letter] = bitstring return [root] - treenode: TreeNode = root # type: ignore + treenode: TreeNode = root letters = [] letters += traverse_tree(treenode.left, bitstring + "0") letters += traverse_tree(treenode.right, bitstring + "1") diff --git a/compression/lempel_ziv.py b/compression/lempel_ziv.py index ea6f33944a91..648b029471bd 100644 --- a/compression/lempel_ziv.py +++ b/compression/lempel_ziv.py @@ -1,6 +1,6 @@ """ - One of the several implementations of Lempel–Ziv–Welch compression algorithm - https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch +One of the several implementations of Lempel-Ziv-Welch compression algorithm +https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch """ import math @@ -35,15 +35,15 @@ def add_key_to_lexicon( lexicon[curr_string + "0"] = last_match_id if math.log2(index).is_integer(): - for curr_key in lexicon: - lexicon[curr_key] = "0" + lexicon[curr_key] + for curr_key, value in lexicon.items(): + lexicon[curr_key] = f"0{value}" lexicon[curr_string + "1"] = bin(index)[2:] def compress_data(data_bits: str) -> str: """ - Compresses given data_bits using Lempel–Ziv–Welch compression algorithm + Compresses given data_bits using Lempel-Ziv-Welch compression algorithm and returns the result as a string """ lexicon = {"0": "0", "1": "1"} diff --git a/compression/lempel_ziv_decompress.py b/compression/lempel_ziv_decompress.py index ddedc3d6d32a..225e96236c2c 100644 --- a/compression/lempel_ziv_decompress.py +++ b/compression/lempel_ziv_decompress.py @@ -1,6 +1,6 @@ """ - One of the several implementations of Lempel–Ziv–Welch decompression algorithm - https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch +One of the several implementations of Lempel-Ziv-Welch decompression algorithm +https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch """ import math @@ -26,7 +26,7 @@ def read_file_binary(file_path: str) -> str: def decompress_data(data_bits: str) -> str: """ - Decompresses given data_bits using Lempel–Ziv–Welch compression algorithm + Decompresses given data_bits using Lempel-Ziv-Welch compression algorithm and returns the result as a string """ lexicon = {"0": "0", "1": "1"} diff --git a/compression/lz77.py b/compression/lz77.py index 1b201c59f186..09b8b021e9d5 100644 --- a/compression/lz77.py +++ b/compression/lz77.py @@ -28,7 +28,6 @@ en.wikipedia.org/wiki/LZ77_and_LZ78 """ - from dataclasses import dataclass __version__ = "0.1" diff --git a/computer_vision/README.md b/computer_vision/README.md index 8d2f4a130d05..61462567b662 100644 --- a/computer_vision/README.md +++ b/computer_vision/README.md @@ -8,4 +8,3 @@ Image processing and computer vision are a little different from each other. Ima While computer vision comes from modelling image processing using the techniques of machine learning, computer vision applies machine learning to recognize patterns for interpretation of images (much like the process of visual reasoning of human vision). * -* diff --git a/computer_vision/cnn_classification.py b/computer_vision/cnn_classification.py index 1c193fcbb50b..115333eba0d1 100644 --- a/computer_vision/cnn_classification.py +++ b/computer_vision/cnn_classification.py @@ -11,10 +11,10 @@ https://lhncbc.nlm.nih.gov/LHC-publications/pubs/TuberculosisChestXrayImageDataSets.html 1. Download the dataset folder and create two folder training set and test set -in the parent dataste folder +in the parent dataset folder 2. Move 30-40 image from both TB positive and TB Negative folder in the test set folder -3. The labels of the iamges will be extracted from the folder name +3. The labels of the images will be extracted from the folder name the image is present in. """ @@ -25,7 +25,7 @@ # Importing the Keras libraries and packages import tensorflow as tf -from tensorflow.keras import layers, models +from keras import layers, models if __name__ == "__main__": # Initialising the CNN @@ -93,7 +93,7 @@ test_image = tf.keras.preprocessing.image.img_to_array(test_image) test_image = np.expand_dims(test_image, axis=0) result = classifier.predict(test_image) - training_set.class_indices + # training_set.class_indices if result[0][0] == 0: prediction = "Normal" if result[0][0] == 1: diff --git a/computer_vision/flip_augmentation.py b/computer_vision/flip_augmentation.py index 93b4e3f6da79..7301424824df 100644 --- a/computer_vision/flip_augmentation.py +++ b/computer_vision/flip_augmentation.py @@ -32,13 +32,13 @@ def main() -> None: letter_code = random_chars(32) file_name = paths[index].split(os.sep)[-1].rsplit(".", 1)[0] file_root = f"{OUTPUT_DIR}/{file_name}_FLIP_{letter_code}" - cv2.imwrite(f"/{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) - print(f"Success {index+1}/{len(new_images)} with {file_name}") + cv2.imwrite(f"{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) + print(f"Success {index + 1}/{len(new_images)} with {file_name}") annos_list = [] for anno in new_annos[index]: obj = f"{anno[0]} {anno[1]} {anno[2]} {anno[3]} {anno[4]}" annos_list.append(obj) - with open(f"/{file_root}.txt", "w") as outfile: + with open(f"{file_root}.txt", "w") as outfile: outfile.write("\n".join(line for line in annos_list)) diff --git a/computer_vision/haralick_descriptors.py b/computer_vision/haralick_descriptors.py new file mode 100644 index 000000000000..54632160dcf2 --- /dev/null +++ b/computer_vision/haralick_descriptors.py @@ -0,0 +1,434 @@ +""" +https://en.wikipedia.org/wiki/Image_texture +https://en.wikipedia.org/wiki/Co-occurrence_matrix#Application_to_image_analysis +""" + +import imageio.v2 as imageio +import numpy as np + + +def root_mean_square_error(original: np.ndarray, reference: np.ndarray) -> float: + """Simple implementation of Root Mean Squared Error + for two N dimensional numpy arrays. + + Examples: + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([1, 2, 3])) + 0.0 + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([2, 2, 2])) + 0.816496580927726 + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([6, 4, 2])) + 3.1622776601683795 + """ + return float(np.sqrt(((original - reference) ** 2).mean())) + + +def normalize_image( + image: np.ndarray, cap: float = 255.0, data_type: np.dtype = np.uint8 +) -> np.ndarray: + """ + Normalizes image in Numpy 2D array format, between ranges 0-cap, + as to fit uint8 type. + + Args: + image: 2D numpy array representing image as matrix, with values in any range + cap: Maximum cap amount for normalization + data_type: numpy data type to set output variable to + Returns: + return 2D numpy array of type uint8, corresponding to limited range matrix + + Examples: + >>> normalize_image(np.array([[1, 2, 3], [4, 5, 10]]), + ... cap=1.0, data_type=np.float64) + array([[0. , 0.11111111, 0.22222222], + [0.33333333, 0.44444444, 1. ]]) + >>> normalize_image(np.array([[4, 4, 3], [1, 7, 2]])) + array([[127, 127, 85], + [ 0, 255, 42]], dtype=uint8) + """ + normalized = (image - np.min(image)) / (np.max(image) - np.min(image)) * cap + return normalized.astype(data_type) + + +def normalize_array(array: np.ndarray, cap: float = 1) -> np.ndarray: + """Normalizes a 1D array, between ranges 0-cap. + + Args: + array: List containing values to be normalized between cap range. + cap: Maximum cap amount for normalization. + Returns: + return 1D numpy array, corresponding to limited range array + + Examples: + >>> normalize_array(np.array([2, 3, 5, 7])) + array([0. , 0.2, 0.6, 1. ]) + >>> normalize_array(np.array([[5], [7], [11], [13]])) + array([[0. ], + [0.25], + [0.75], + [1. ]]) + """ + diff = np.max(array) - np.min(array) + return (array - np.min(array)) / (1 if diff == 0 else diff) * cap + + +def grayscale(image: np.ndarray) -> np.ndarray: + """ + Uses luminance weights to transform RGB channel to greyscale, by + taking the dot product between the channel and the weights. + + Example: + >>> grayscale(np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]])) + array([[158, 97], + [ 56, 200]], dtype=uint8) + """ + return np.dot(image[:, :, 0:3], [0.299, 0.587, 0.114]).astype(np.uint8) + + +def binarize(image: np.ndarray, threshold: float = 127.0) -> np.ndarray: + """ + Binarizes a grayscale image based on a given threshold value, + setting values to 1 or 0 accordingly. + + Examples: + >>> binarize(np.array([[128, 255], [101, 156]])) + array([[1, 1], + [0, 1]]) + >>> binarize(np.array([[0.07, 1], [0.51, 0.3]]), threshold=0.5) + array([[0, 1], + [1, 0]]) + """ + return np.where(image > threshold, 1, 0) + + +def transform( + image: np.ndarray, kind: str, kernel: np.ndarray | None = None +) -> np.ndarray: + """ + Simple image transformation using one of two available filter functions: + Erosion and Dilation. + + Args: + image: binarized input image, onto which to apply transformation + kind: Can be either 'erosion', in which case the :func:np.max + function is called, or 'dilation', when :func:np.min is used instead. + kernel: n x n kernel with shape < :attr:image.shape, + to be used when applying convolution to original image + + Returns: + returns a numpy array with same shape as input image, + corresponding to applied binary transformation. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> transform(img, 'erosion') + array([[1, 1], + [1, 1]], dtype=uint8) + >>> transform(img, 'dilation') + array([[0, 0], + [0, 0]], dtype=uint8) + """ + if kernel is None: + kernel = np.ones((3, 3)) + + if kind == "erosion": + constant = 1 + apply = np.max + else: + constant = 0 + apply = np.min + + center_x, center_y = (x // 2 for x in kernel.shape) + + # Use padded image when applying convolution + # to not go out of bounds of the original the image + transformed = np.zeros(image.shape, dtype=np.uint8) + padded = np.pad(image, 1, "constant", constant_values=constant) + + for x in range(center_x, padded.shape[0] - center_x): + for y in range(center_y, padded.shape[1] - center_y): + center = padded[ + x - center_x : x + center_x + 1, y - center_y : y + center_y + 1 + ] + # Apply transformation method to the centered section of the image + transformed[x - center_x, y - center_y] = apply(center[kernel == 1]) + + return transformed + + +def opening_filter(image: np.ndarray, kernel: np.ndarray | None = None) -> np.ndarray: + """ + Opening filter, defined as the sequence of + erosion and then a dilation filter on the same image. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> opening_filter(img) + array([[1, 1], + [1, 1]], dtype=uint8) + """ + if kernel is None: + np.ones((3, 3)) + + return transform(transform(image, "dilation", kernel), "erosion", kernel) + + +def closing_filter(image: np.ndarray, kernel: np.ndarray | None = None) -> np.ndarray: + """ + Opening filter, defined as the sequence of + dilation and then erosion filter on the same image. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> closing_filter(img) + array([[0, 0], + [0, 0]], dtype=uint8) + """ + if kernel is None: + kernel = np.ones((3, 3)) + return transform(transform(image, "erosion", kernel), "dilation", kernel) + + +def binary_mask( + image_gray: np.ndarray, image_map: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """ + Apply binary mask, or thresholding based + on bit mask value (mapping mask is binary). + + Returns the mapped true value mask and its complementary false value mask. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> binary_mask(gray, morphological) + (array([[1, 1], + [1, 1]], dtype=uint8), array([[158, 97], + [ 56, 200]], dtype=uint8)) + """ + true_mask, false_mask = image_gray.copy(), image_gray.copy() + true_mask[image_map == 1] = 1 + false_mask[image_map == 0] = 0 + + return true_mask, false_mask + + +def matrix_concurrency(image: np.ndarray, coordinate: tuple[int, int]) -> np.ndarray: + """ + Calculate sample co-occurrence matrix based on input image + as well as selected coordinates on image. + + Implementation is made using basic iteration, + as function to be performed (np.max) is non-linear and therefore + not callable on the frequency domain. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> mask_1 = binary_mask(gray, morphological)[0] + >>> matrix_concurrency(mask_1, (0, 1)) + array([[0., 0.], + [0., 0.]]) + """ + matrix = np.zeros([np.max(image) + 1, np.max(image) + 1]) + + offset_x, offset_y = coordinate + + for x in range(1, image.shape[0] - 1): + for y in range(1, image.shape[1] - 1): + base_pixel = image[x, y] + offset_pixel = image[x + offset_x, y + offset_y] + + matrix[base_pixel, offset_pixel] += 1 + matrix_sum = np.sum(matrix) + return matrix / (1 if matrix_sum == 0 else matrix_sum) + + +def haralick_descriptors(matrix: np.ndarray) -> list[float]: + """Calculates all 8 Haralick descriptors based on co-occurrence input matrix. + All descriptors are as follows: + Maximum probability, Inverse Difference, Homogeneity, Entropy, + Energy, Dissimilarity, Contrast and Correlation + + Args: + matrix: Co-occurrence matrix to use as base for calculating descriptors. + + Returns: + Reverse ordered list of resulting descriptors + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> mask_1 = binary_mask(gray, morphological)[0] + >>> concurrency = matrix_concurrency(mask_1, (0, 1)) + >>> [float(f) for f in haralick_descriptors(concurrency)] + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + """ + # Function np.indices could be used for bigger input types, + # but np.ogrid works just fine + i, j = np.ogrid[0 : matrix.shape[0], 0 : matrix.shape[1]] # np.indices() + + # Pre-calculate frequent multiplication and subtraction + prod = np.multiply(i, j) + sub = np.subtract(i, j) + + # Calculate numerical value of Maximum Probability + maximum_prob = np.max(matrix) + # Using the definition for each descriptor individually to calculate its matrix + correlation = prod * matrix + energy = np.power(matrix, 2) + contrast = matrix * np.power(sub, 2) + + dissimilarity = matrix * np.abs(sub) + inverse_difference = matrix / (1 + np.abs(sub)) + homogeneity = matrix / (1 + np.power(sub, 2)) + entropy = -(matrix[matrix > 0] * np.log(matrix[matrix > 0])) + + # Sum values for descriptors ranging from the first one to the last, + # as all are their respective origin matrix and not the resulting value yet. + return [ + maximum_prob, + correlation.sum(), + energy.sum(), + contrast.sum(), + dissimilarity.sum(), + inverse_difference.sum(), + homogeneity.sum(), + entropy.sum(), + ] + + +def get_descriptors( + masks: tuple[np.ndarray, np.ndarray], coordinate: tuple[int, int] +) -> np.ndarray: + """ + Calculate all Haralick descriptors for a sequence of + different co-occurrence matrices, given input masks and coordinates. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> get_descriptors(binary_mask(gray, morphological), (0, 1)) + array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) + """ + descriptors = np.array( + [haralick_descriptors(matrix_concurrency(mask, coordinate)) for mask in masks] + ) + + # Concatenate each individual descriptor into + # one single list containing sequence of descriptors + return np.concatenate(descriptors, axis=None) + + +def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> float: + """ + Simple method for calculating the euclidean distance between two points, + with type np.ndarray. + + Example: + >>> a = np.array([1, 0, -2]) + >>> b = np.array([2, -1, 1]) + >>> euclidean(a, b) + 3.3166247903554 + """ + return float(np.sqrt(np.sum(np.square(point_1 - point_2)))) + + +def get_distances(descriptors: np.ndarray, base: int) -> list[tuple[int, float]]: + """ + Calculate all Euclidean distances between a selected base descriptor + and all other Haralick descriptors + The resulting comparison is return in decreasing order, + showing which descriptor is the most similar to the selected base. + + Args: + descriptors: Haralick descriptors to compare with base index + base: Haralick descriptor index to use as base when calculating respective + euclidean distance to other descriptors. + + Returns: + Ordered distances between descriptors + + Example: + >>> index = 1 + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> get_distances(get_descriptors( + ... binary_mask(gray, morphological), (0, 1)), + ... index) + [(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0), (4, 0.0), (5, 0.0), \ +(6, 0.0), (7, 0.0), (8, 0.0), (9, 0.0), (10, 0.0), (11, 0.0), (12, 0.0), \ +(13, 0.0), (14, 0.0), (15, 0.0)] + """ + distances = np.array( + [euclidean(descriptor, descriptors[base]) for descriptor in descriptors] + ) + # Normalize distances between range [0, 1] + normalized_distances: list[float] = normalize_array(distances, 1).tolist() + enum_distances = list(enumerate(normalized_distances)) + enum_distances.sort(key=lambda tup: tup[1], reverse=True) + return enum_distances + + +if __name__ == "__main__": + # Index to compare haralick descriptors to + index = int(input()) + q_value_list = [int(value) for value in input().split()] + q_value = (q_value_list[0], q_value_list[1]) + + # Format is the respective filter to apply, + # can be either 1 for the opening filter or else for the closing + parameters = {"format": int(input()), "threshold": int(input())} + + # Number of images to perform methods on + b_number = int(input()) + + files, descriptors = [], [] + + for _ in range(b_number): + file = input().rstrip() + files.append(file) + + # Open given image and calculate morphological filter, + # respective masks and correspondent Harralick Descriptors. + image = imageio.imread(file).astype(np.float32) + gray = grayscale(image) + threshold = binarize(gray, parameters["threshold"]) + + morphological = ( + opening_filter(threshold) + if parameters["format"] == 1 + else closing_filter(threshold) + ) + masks = binary_mask(gray, morphological) + descriptors.append(get_descriptors(masks, q_value)) + + # Transform ordered distances array into a sequence of indexes + # corresponding to original file position + distances = get_distances(np.array(descriptors), index) + indexed_distances = np.array(distances).astype(np.uint8)[:, 0] + + # Finally, print distances considering the Haralick descriptions from the base + # file to all other images using the morphology method of choice. + print(f"Query: {files[index]}") + print("Ranking:") + for idx, file_idx in enumerate(indexed_distances): + print(f"({idx}) {files[file_idx]}", end="\n") diff --git a/computer_vision/horn_schunck.py b/computer_vision/horn_schunck.py index b63e0268294c..f33b5b1c794b 100644 --- a/computer_vision/horn_schunck.py +++ b/computer_vision/horn_schunck.py @@ -1,12 +1,12 @@ """ - The Horn-Schunck method estimates the optical flow for every single pixel of - a sequence of images. - It works by assuming brightness constancy between two consecutive frames - and smoothness in the optical flow. - - Useful resources: - Wikipedia: https://en.wikipedia.org/wiki/Horn%E2%80%93Schunck_method - Paper: http://image.diku.dk/imagecanon/material/HornSchunckOptical_Flow.pdf +The Horn-Schunck method estimates the optical flow for every single pixel of +a sequence of images. +It works by assuming brightness constancy between two consecutive frames +and smoothness in the optical flow. + +Useful resources: +Wikipedia: https://en.wikipedia.org/wiki/Horn%E2%80%93Schunck_method +Paper: http://image.diku.dk/imagecanon/material/HornSchunckOptical_Flow.pdf """ from typing import SupportsIndex diff --git a/computer_vision/intensity_based_segmentation.py b/computer_vision/intensity_based_segmentation.py new file mode 100644 index 000000000000..7f2b1141acc4 --- /dev/null +++ b/computer_vision/intensity_based_segmentation.py @@ -0,0 +1,62 @@ +# Source: "https://www.ijcse.com/docs/IJCSE11-02-03-117.pdf" + +# Importing necessary libraries +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + + +def segment_image(image: np.ndarray, thresholds: list[int]) -> np.ndarray: + """ + Performs image segmentation based on intensity thresholds. + + Args: + image: Input grayscale image as a 2D array. + thresholds: Intensity thresholds to define segments. + + Returns: + A labeled 2D array where each region corresponds to a threshold range. + + Example: + >>> img = np.array([[80, 120, 180], [40, 90, 150], [20, 60, 100]]) + >>> segment_image(img, [50, 100, 150]) + array([[1, 2, 3], + [0, 1, 2], + [0, 1, 1]], dtype=int32) + """ + # Initialize segmented array with zeros + segmented = np.zeros_like(image, dtype=np.int32) + + # Assign labels based on thresholds + for i, threshold in enumerate(thresholds): + segmented[image > threshold] = i + 1 + + return segmented + + +if __name__ == "__main__": + # Load the image + image_path = "path_to_image" # Replace with your image path + original_image = Image.open(image_path).convert("L") + image_array = np.array(original_image) + + # Define thresholds + thresholds = [50, 100, 150, 200] + + # Perform segmentation + segmented_image = segment_image(image_array, thresholds) + + # Display the results + plt.figure(figsize=(10, 5)) + + plt.subplot(1, 2, 1) + plt.title("Original Image") + plt.imshow(image_array, cmap="gray") + plt.axis("off") + + plt.subplot(1, 2, 2) + plt.title("Segmented Image") + plt.imshow(segmented_image, cmap="tab20") + plt.axis("off") + + plt.show() diff --git a/computer_vision/mosaic_augmentation.py b/computer_vision/mosaic_augmentation.py index e2953749753f..d881347121ea 100644 --- a/computer_vision/mosaic_augmentation.py +++ b/computer_vision/mosaic_augmentation.py @@ -8,7 +8,7 @@ import cv2 import numpy as np -# Parrameters +# Parameters OUTPUT_SIZE = (720, 1280) # Height, Width SCALE_RANGE = (0.4, 0.6) # if height or width lower than this scale, drop it. FILTER_TINY_SCALE = 1 / 100 @@ -41,7 +41,7 @@ def main() -> None: file_name = path.split(os.sep)[-1].rsplit(".", 1)[0] file_root = f"{OUTPUT_DIR}/{file_name}_MOSAIC_{letter_code}" cv2.imwrite(f"{file_root}.jpg", new_image, [cv2.IMWRITE_JPEG_QUALITY, 85]) - print(f"Succeeded {index+1}/{NUMBER_IMAGES} with {file_name}") + print(f"Succeeded {index + 1}/{NUMBER_IMAGES} with {file_name}") annos_list = [] for anno in new_annos: width = anno[3] - anno[1] @@ -159,7 +159,7 @@ def update_image_and_anno( new_anno.append([bbox[0], xmin, ymin, xmax, ymax]) # Remove bounding box small than scale of filter - if 0 < filter_scale: + if filter_scale > 0: new_anno = [ anno for anno in new_anno diff --git a/conversions/astronomical_length_scale_conversion.py b/conversions/astronomical_length_scale_conversion.py index 804d82487a25..0f413644906d 100644 --- a/conversions/astronomical_length_scale_conversion.py +++ b/conversions/astronomical_length_scale_conversion.py @@ -77,15 +77,17 @@ def length_conversion(value: float, from_type: str, to_type: str) -> float: to_sanitized = UNIT_SYMBOL.get(to_sanitized, to_sanitized) if from_sanitized not in METRIC_CONVERSION: - raise ValueError( + msg = ( f"Invalid 'from_type' value: {from_type!r}.\n" f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" ) + raise ValueError(msg) if to_sanitized not in METRIC_CONVERSION: - raise ValueError( + msg = ( f"Invalid 'to_type' value: {to_type!r}.\n" f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" ) + raise ValueError(msg) from_exponent = METRIC_CONVERSION[from_sanitized] to_exponent = METRIC_CONVERSION[to_sanitized] exponent = 1 diff --git a/conversions/convert_number_to_words.py b/conversions/convert_number_to_words.py new file mode 100644 index 000000000000..6aa43738b9fe --- /dev/null +++ b/conversions/convert_number_to_words.py @@ -0,0 +1,205 @@ +from enum import Enum +from typing import Literal + + +class NumberingSystem(Enum): + SHORT = ( + (15, "quadrillion"), + (12, "trillion"), + (9, "billion"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + LONG = ( + (15, "billiard"), + (9, "milliard"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + INDIAN = ( + (14, "crore crore"), + (12, "lakh crore"), + (7, "crore"), + (5, "lakh"), + (3, "thousand"), + (2, "hundred"), + ) + + @classmethod + def max_value(cls, system: str) -> int: + """ + Gets the max value supported by the given number system. + + >>> NumberingSystem.max_value("short") == 10**18 - 1 + True + >>> NumberingSystem.max_value("long") == 10**21 - 1 + True + >>> NumberingSystem.max_value("indian") == 10**19 - 1 + True + """ + match system_enum := cls[system.upper()]: + case cls.SHORT: + max_exp = system_enum.value[0][0] + 3 + case cls.LONG: + max_exp = system_enum.value[0][0] + 6 + case cls.INDIAN: + max_exp = 19 + case _: + raise ValueError("Invalid numbering system") + return 10**max_exp - 1 + + +class NumberWords(Enum): + ONES = { # noqa: RUF012 + 0: "", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + } + + TEENS = { # noqa: RUF012 + 0: "ten", + 1: "eleven", + 2: "twelve", + 3: "thirteen", + 4: "fourteen", + 5: "fifteen", + 6: "sixteen", + 7: "seventeen", + 8: "eighteen", + 9: "nineteen", + } + + TENS = { # noqa: RUF012 + 2: "twenty", + 3: "thirty", + 4: "forty", + 5: "fifty", + 6: "sixty", + 7: "seventy", + 8: "eighty", + 9: "ninety", + } + + +def convert_small_number(num: int) -> str: + """ + Converts small, non-negative integers with irregular constructions in English (i.e., + numbers under 100) into words. + + >>> convert_small_number(0) + 'zero' + >>> convert_small_number(5) + 'five' + >>> convert_small_number(10) + 'ten' + >>> convert_small_number(15) + 'fifteen' + >>> convert_small_number(20) + 'twenty' + >>> convert_small_number(25) + 'twenty-five' + >>> convert_small_number(-1) + Traceback (most recent call last): + ... + ValueError: This function only accepts non-negative integers + >>> convert_small_number(123) + Traceback (most recent call last): + ... + ValueError: This function only converts numbers less than 100 + """ + if num < 0: + raise ValueError("This function only accepts non-negative integers") + if num >= 100: + raise ValueError("This function only converts numbers less than 100") + tens, ones = divmod(num, 10) + if tens == 0: + return NumberWords.ONES.value[ones] or "zero" + if tens == 1: + return NumberWords.TEENS.value[ones] + return ( + NumberWords.TENS.value[tens] + + ("-" if NumberWords.ONES.value[ones] else "") + + NumberWords.ONES.value[ones] + ) + + +def convert_number( + num: int, system: Literal["short", "long", "indian"] = "short" +) -> str: + """ + Converts an integer to English words. + + :param num: The integer to be converted + :param system: The numbering system (short, long, or Indian) + + >>> convert_number(0) + 'zero' + >>> convert_number(1) + 'one' + >>> convert_number(100) + 'one hundred' + >>> convert_number(-100) + 'negative one hundred' + >>> convert_number(123_456_789_012_345) # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three trillion four hundred fifty-six billion + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(123_456_789_012_345, "long") # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three thousand four hundred fifty-six milliard + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(12_34_56_78_90_12_345, "indian") # doctest: +NORMALIZE_WHITESPACE + 'one crore crore twenty-three lakh crore + forty-five thousand six hundred seventy-eight crore + ninety lakh twelve thousand three hundred forty-five' + >>> convert_number(10**18) + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**21, "long") + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**19, "indian") + Traceback (most recent call last): + ... + ValueError: Input number is too large + """ + word_groups = [] + + if num < 0: + word_groups.append("negative") + num *= -1 + + if num > NumberingSystem.max_value(system): + raise ValueError("Input number is too large") + + for power, unit in NumberingSystem[system.upper()].value: + digit_group, num = divmod(num, 10**power) + if digit_group > 0: + word_group = ( + convert_number(digit_group, system) + if digit_group >= 100 + else convert_small_number(digit_group) + ) + word_groups.append(f"{word_group} {unit}") + if num > 0 or not word_groups: # word_groups is only empty if input num was 0 + word_groups.append(convert_small_number(num)) + return " ".join(word_groups) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(f"{convert_number(123456789) = }") diff --git a/conversions/decimal_to_binary.py b/conversions/decimal_to_binary.py index 973c47c8af67..cf2b6040ec2a 100644 --- a/conversions/decimal_to_binary.py +++ b/conversions/decimal_to_binary.py @@ -1,27 +1,27 @@ """Convert a Decimal Number to a Binary Number.""" -def decimal_to_binary(num: int) -> str: +def decimal_to_binary_iterative(num: int) -> str: """ Convert an Integer Decimal Number to a Binary Number as str. - >>> decimal_to_binary(0) + >>> decimal_to_binary_iterative(0) '0b0' - >>> decimal_to_binary(2) + >>> decimal_to_binary_iterative(2) '0b10' - >>> decimal_to_binary(7) + >>> decimal_to_binary_iterative(7) '0b111' - >>> decimal_to_binary(35) + >>> decimal_to_binary_iterative(35) '0b100011' >>> # negatives work too - >>> decimal_to_binary(-2) + >>> decimal_to_binary_iterative(-2) '-0b10' >>> # other floats will error - >>> decimal_to_binary(16.16) # doctest: +ELLIPSIS + >>> decimal_to_binary_iterative(16.16) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: 'float' object cannot be interpreted as an integer >>> # strings will error as well - >>> decimal_to_binary('0xfffff') # doctest: +ELLIPSIS + >>> decimal_to_binary_iterative('0xfffff') # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: 'str' object cannot be interpreted as an integer @@ -52,7 +52,58 @@ def decimal_to_binary(num: int) -> str: return "0b" + "".join(str(e) for e in binary) +def decimal_to_binary_recursive_helper(decimal: int) -> str: + """ + Take a positive integer value and return its binary equivalent. + >>> decimal_to_binary_recursive_helper(1000) + '1111101000' + >>> decimal_to_binary_recursive_helper("72") + '1001000' + >>> decimal_to_binary_recursive_helper("number") + Traceback (most recent call last): + ... + ValueError: invalid literal for int() with base 10: 'number' + """ + decimal = int(decimal) + if decimal in (0, 1): # Exit cases for the recursion + return str(decimal) + div, mod = divmod(decimal, 2) + return decimal_to_binary_recursive_helper(div) + str(mod) + + +def decimal_to_binary_recursive(number: str) -> str: + """ + Take an integer value and raise ValueError for wrong inputs, + call the function above and return the output with prefix "0b" & "-0b" + for positive and negative integers respectively. + >>> decimal_to_binary_recursive(0) + '0b0' + >>> decimal_to_binary_recursive(40) + '0b101000' + >>> decimal_to_binary_recursive(-40) + '-0b101000' + >>> decimal_to_binary_recursive(40.8) + Traceback (most recent call last): + ... + ValueError: Input value is not an integer + >>> decimal_to_binary_recursive("forty") + Traceback (most recent call last): + ... + ValueError: Input value is not an integer + """ + number = str(number).strip() + if not number: + raise ValueError("No input value was provided") + negative = "-" if number.startswith("-") else "" + number = number.lstrip("-") + if not number.isnumeric(): + raise ValueError("Input value is not an integer") + return f"{negative}0b{decimal_to_binary_recursive_helper(int(number))}" + + if __name__ == "__main__": import doctest doctest.testmod() + + print(decimal_to_binary_recursive(input("Input a decimal number: "))) diff --git a/conversions/decimal_to_binary_recursion.py b/conversions/decimal_to_binary_recursion.py deleted file mode 100644 index 05833ca670c3..000000000000 --- a/conversions/decimal_to_binary_recursion.py +++ /dev/null @@ -1,53 +0,0 @@ -def binary_recursive(decimal: int) -> str: - """ - Take a positive integer value and return its binary equivalent. - >>> binary_recursive(1000) - '1111101000' - >>> binary_recursive("72") - '1001000' - >>> binary_recursive("number") - Traceback (most recent call last): - ... - ValueError: invalid literal for int() with base 10: 'number' - """ - decimal = int(decimal) - if decimal in (0, 1): # Exit cases for the recursion - return str(decimal) - div, mod = divmod(decimal, 2) - return binary_recursive(div) + str(mod) - - -def main(number: str) -> str: - """ - Take an integer value and raise ValueError for wrong inputs, - call the function above and return the output with prefix "0b" & "-0b" - for positive and negative integers respectively. - >>> main(0) - '0b0' - >>> main(40) - '0b101000' - >>> main(-40) - '-0b101000' - >>> main(40.8) - Traceback (most recent call last): - ... - ValueError: Input value is not an integer - >>> main("forty") - Traceback (most recent call last): - ... - ValueError: Input value is not an integer - """ - number = str(number).strip() - if not number: - raise ValueError("No input value was provided") - negative = "-" if number.startswith("-") else "" - number = number.lstrip("-") - if not number.isnumeric(): - raise ValueError("Input value is not an integer") - return f"{negative}0b{binary_recursive(int(number))}" - - -if __name__ == "__main__": - from doctest import testmod - - testmod() diff --git a/conversions/decimal_to_hexadecimal.py b/conversions/decimal_to_hexadecimal.py index 5ea48401f488..ee79592de5ca 100644 --- a/conversions/decimal_to_hexadecimal.py +++ b/conversions/decimal_to_hexadecimal.py @@ -1,4 +1,4 @@ -""" Convert Base 10 (Decimal) Values to Hexadecimal Representations """ +"""Convert Base 10 (Decimal) Values to Hexadecimal Representations""" # set decimal value for each hexadecimal digit values = { @@ -57,7 +57,8 @@ def decimal_to_hexadecimal(decimal: float) -> str: >>> decimal_to_hexadecimal(-256) == hex(-256) True """ - assert type(decimal) in (int, float) and decimal == int(decimal) + assert isinstance(decimal, (int, float)) + assert decimal == int(decimal) decimal = int(decimal) hexadecimal = "" negative = False diff --git a/conversions/energy_conversions.py b/conversions/energy_conversions.py new file mode 100644 index 000000000000..51de6b313928 --- /dev/null +++ b/conversions/energy_conversions.py @@ -0,0 +1,114 @@ +""" +Conversion of energy units. + +Available units: joule, kilojoule, megajoule, gigajoule,\ + wattsecond, watthour, kilowatthour, newtonmeter, calorie_nutr,\ + kilocalorie_nutr, electronvolt, britishthermalunit_it, footpound + +USAGE : +-> Import this file into their respective project. +-> Use the function energy_conversion() for conversion of energy units. +-> Parameters : + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert + -> value : the value which you want to convert + +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Units_of_energy +-> Wikipedia reference: https://en.wikipedia.org/wiki/Joule +-> Wikipedia reference: https://en.wikipedia.org/wiki/Kilowatt-hour +-> Wikipedia reference: https://en.wikipedia.org/wiki/Newton-metre +-> Wikipedia reference: https://en.wikipedia.org/wiki/Calorie +-> Wikipedia reference: https://en.wikipedia.org/wiki/Electronvolt +-> Wikipedia reference: https://en.wikipedia.org/wiki/British_thermal_unit +-> Wikipedia reference: https://en.wikipedia.org/wiki/Foot-pound_(energy) +-> Unit converter reference: https://www.unitconverters.net/energy-converter.html +""" + +ENERGY_CONVERSION: dict[str, float] = { + "joule": 1.0, + "kilojoule": 1_000, + "megajoule": 1_000_000, + "gigajoule": 1_000_000_000, + "wattsecond": 1.0, + "watthour": 3_600, + "kilowatthour": 3_600_000, + "newtonmeter": 1.0, + "calorie_nutr": 4_186.8, + "kilocalorie_nutr": 4_186_800.00, + "electronvolt": 1.602_176_634e-19, + "britishthermalunit_it": 1_055.055_85, + "footpound": 1.355_818, +} + + +def energy_conversion(from_type: str, to_type: str, value: float) -> float: + """ + Conversion of energy units. + >>> energy_conversion("joule", "joule", 1) + 1.0 + >>> energy_conversion("joule", "kilojoule", 1) + 0.001 + >>> energy_conversion("joule", "megajoule", 1) + 1e-06 + >>> energy_conversion("joule", "gigajoule", 1) + 1e-09 + >>> energy_conversion("joule", "wattsecond", 1) + 1.0 + >>> energy_conversion("joule", "watthour", 1) + 0.0002777777777777778 + >>> energy_conversion("joule", "kilowatthour", 1) + 2.7777777777777776e-07 + >>> energy_conversion("joule", "newtonmeter", 1) + 1.0 + >>> energy_conversion("joule", "calorie_nutr", 1) + 0.00023884589662749592 + >>> energy_conversion("joule", "kilocalorie_nutr", 1) + 2.388458966274959e-07 + >>> energy_conversion("joule", "electronvolt", 1) + 6.241509074460763e+18 + >>> energy_conversion("joule", "britishthermalunit_it", 1) + 0.0009478171226670134 + >>> energy_conversion("joule", "footpound", 1) + 0.7375621211696556 + >>> energy_conversion("joule", "megajoule", 1000) + 0.001 + >>> energy_conversion("calorie_nutr", "kilocalorie_nutr", 1000) + 1.0 + >>> energy_conversion("kilowatthour", "joule", 10) + 36000000.0 + >>> energy_conversion("britishthermalunit_it", "footpound", 1) + 778.1692306784539 + >>> energy_conversion("watthour", "joule", "a") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for /: 'str' and 'float' + >>> energy_conversion("wrongunit", "joule", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: 'wrongunit', 'joule' + Valid values are: joule, ... footpound + >>> energy_conversion("joule", "wrongunit", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: 'joule', 'wrongunit' + Valid values are: joule, ... footpound + >>> energy_conversion("123", "abc", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: '123', 'abc' + Valid values are: joule, ... footpound + """ + if to_type not in ENERGY_CONVERSION or from_type not in ENERGY_CONVERSION: + msg = ( + f"Incorrect 'from_type' or 'to_type' value: {from_type!r}, {to_type!r}\n" + f"Valid values are: {', '.join(ENERGY_CONVERSION)}" + ) + raise ValueError(msg) + return value * ENERGY_CONVERSION[from_type] / ENERGY_CONVERSION[to_type] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/ipv4_conversion.py b/conversions/ipv4_conversion.py new file mode 100644 index 000000000000..862309b7251e --- /dev/null +++ b/conversions/ipv4_conversion.py @@ -0,0 +1,85 @@ +# https://www.geeksforgeeks.org/convert-ip-address-to-integer-and-vice-versa/ + + +def ipv4_to_decimal(ipv4_address: str) -> int: + """ + Convert an IPv4 address to its decimal representation. + + Args: + ip_address: A string representing an IPv4 address (e.g., "192.168.0.1"). + + Returns: + int: The decimal representation of the IP address. + + >>> ipv4_to_decimal("192.168.0.1") + 3232235521 + >>> ipv4_to_decimal("10.0.0.255") + 167772415 + >>> ipv4_to_decimal("10.0.255") + Traceback (most recent call last): + ... + ValueError: Invalid IPv4 address format + >>> ipv4_to_decimal("10.0.0.256") + Traceback (most recent call last): + ... + ValueError: Invalid IPv4 octet 256 + """ + + octets = [int(octet) for octet in ipv4_address.split(".")] + if len(octets) != 4: + raise ValueError("Invalid IPv4 address format") + + decimal_ipv4 = 0 + for octet in octets: + if not 0 <= octet <= 255: + raise ValueError(f"Invalid IPv4 octet {octet}") # noqa: EM102 + decimal_ipv4 = (decimal_ipv4 << 8) + int(octet) + + return decimal_ipv4 + + +def alt_ipv4_to_decimal(ipv4_address: str) -> int: + """ + >>> alt_ipv4_to_decimal("192.168.0.1") + 3232235521 + >>> alt_ipv4_to_decimal("10.0.0.255") + 167772415 + """ + return int("0x" + "".join(f"{int(i):02x}" for i in ipv4_address.split(".")), 16) + + +def decimal_to_ipv4(decimal_ipv4: int) -> str: + """ + Convert a decimal representation of an IP address to its IPv4 format. + + Args: + decimal_ipv4: An integer representing the decimal IP address. + + Returns: + The IPv4 representation of the decimal IP address. + + >>> decimal_to_ipv4(3232235521) + '192.168.0.1' + >>> decimal_to_ipv4(167772415) + '10.0.0.255' + >>> decimal_to_ipv4(-1) + Traceback (most recent call last): + ... + ValueError: Invalid decimal IPv4 address + """ + + if not (0 <= decimal_ipv4 <= 4294967295): + raise ValueError("Invalid decimal IPv4 address") + + ip_parts = [] + for _ in range(4): + ip_parts.append(str(decimal_ipv4 & 255)) + decimal_ipv4 >>= 8 + + return ".".join(reversed(ip_parts)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/length_conversion.py b/conversions/length_conversion.py index 790d9c116845..07fa93a198c7 100644 --- a/conversions/length_conversion.py +++ b/conversions/length_conversion.py @@ -22,9 +22,13 @@ -> Wikipedia reference: https://en.wikipedia.org/wiki/Millimeter """ -from collections import namedtuple +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float -from_to = namedtuple("from_to", "from_ to") TYPE_CONVERSION = { "millimeter": "mm", @@ -40,14 +44,14 @@ } METRIC_CONVERSION = { - "mm": from_to(0.001, 1000), - "cm": from_to(0.01, 100), - "m": from_to(1, 1), - "km": from_to(1000, 0.001), - "in": from_to(0.0254, 39.3701), - "ft": from_to(0.3048, 3.28084), - "yd": from_to(0.9144, 1.09361), - "mi": from_to(1609.34, 0.000621371), + "mm": FromTo(0.001, 1000), + "cm": FromTo(0.01, 100), + "m": FromTo(1, 1), + "km": FromTo(1000, 0.001), + "in": FromTo(0.0254, 39.3701), + "ft": FromTo(0.3048, 3.28084), + "yd": FromTo(0.9144, 1.09361), + "mi": FromTo(1609.34, 0.000621371), } @@ -104,16 +108,22 @@ def length_conversion(value: float, from_type: str, to_type: str) -> float: new_to = to_type.lower().rstrip("s") new_to = TYPE_CONVERSION.get(new_to, new_to) if new_from not in METRIC_CONVERSION: - raise ValueError( + msg = ( f"Invalid 'from_type' value: {from_type!r}.\n" f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" ) + raise ValueError(msg) if new_to not in METRIC_CONVERSION: - raise ValueError( + msg = ( f"Invalid 'to_type' value: {to_type!r}.\n" f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" ) - return value * METRIC_CONVERSION[new_from].from_ * METRIC_CONVERSION[new_to].to + raise ValueError(msg) + return ( + value + * METRIC_CONVERSION[new_from].from_factor + * METRIC_CONVERSION[new_to].to_factor + ) if __name__ == "__main__": diff --git a/conversions/octal_to_binary.py b/conversions/octal_to_binary.py new file mode 100644 index 000000000000..84e1e85f33ca --- /dev/null +++ b/conversions/octal_to_binary.py @@ -0,0 +1,54 @@ +""" +* Author: Bama Charan Chhandogi (https://github.com/BamaCharanChhandogi) +* Description: Convert a Octal number to Binary. + +References for better understanding: +https://en.wikipedia.org/wiki/Binary_number +https://en.wikipedia.org/wiki/Octal +""" + + +def octal_to_binary(octal_number: str) -> str: + """ + Convert an Octal number to Binary. + + >>> octal_to_binary("17") + '001111' + >>> octal_to_binary("7") + '111' + >>> octal_to_binary("Av") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> octal_to_binary("@#") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> octal_to_binary("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + if not octal_number: + raise ValueError("Empty string was passed to the function") + + binary_number = "" + octal_digits = "01234567" + for digit in octal_number: + if digit not in octal_digits: + raise ValueError("Non-octal value was passed to the function") + + binary_digit = "" + value = int(digit) + for _ in range(3): + binary_digit = str(value % 2) + binary_digit + value //= 2 + binary_number += binary_digit + + return binary_number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/octal_to_hexadecimal.py b/conversions/octal_to_hexadecimal.py new file mode 100644 index 000000000000..0615d79b5c53 --- /dev/null +++ b/conversions/octal_to_hexadecimal.py @@ -0,0 +1,65 @@ +def octal_to_hex(octal: str) -> str: + """ + Convert an Octal number to Hexadecimal number. + For more information: https://en.wikipedia.org/wiki/Octal + + >>> octal_to_hex("100") + '0x40' + >>> octal_to_hex("235") + '0x9D' + >>> octal_to_hex(17) + Traceback (most recent call last): + ... + TypeError: Expected a string as input + >>> octal_to_hex("Av") + Traceback (most recent call last): + ... + ValueError: Not a Valid Octal Number + >>> octal_to_hex("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + + if not isinstance(octal, str): + raise TypeError("Expected a string as input") + if octal.startswith("0o"): + octal = octal[2:] + if octal == "": + raise ValueError("Empty string was passed to the function") + if any(char not in "01234567" for char in octal): + raise ValueError("Not a Valid Octal Number") + + decimal = 0 + for char in octal: + decimal <<= 3 + decimal |= int(char) + + hex_char = "0123456789ABCDEF" + + revhex = "" + while decimal: + revhex += hex_char[decimal & 15] + decimal >>= 4 + + return "0x" + revhex[::-1] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + nums = ["030", "100", "247", "235", "007"] + + ## Main Tests + + for num in nums: + hexadecimal = octal_to_hex(num) + expected = "0x" + hex(int(num, 8))[2:].upper() + + assert hexadecimal == expected + + print(f"Hex of '0o{num}' is: {hexadecimal}") + print(f"Expected was: {expected}") + print("---") diff --git a/conversions/prefix_conversions.py b/conversions/prefix_conversions.py index 06b759e355a7..714677f3b242 100644 --- a/conversions/prefix_conversions.py +++ b/conversions/prefix_conversions.py @@ -1,6 +1,7 @@ """ Convert International System of Units (SI) and Binary prefixes """ + from __future__ import annotations from enum import Enum diff --git a/conversions/prefix_conversions_string.py b/conversions/prefix_conversions_string.py index 3851d7c8b993..9344c9672a1f 100644 --- a/conversions/prefix_conversions_string.py +++ b/conversions/prefix_conversions_string.py @@ -96,7 +96,7 @@ def add_si_prefix(value: float) -> str: for name_prefix, value_prefix in prefixes.items(): numerical_part = value / (10**value_prefix) if numerical_part > 1: - return f"{str(numerical_part)} {name_prefix}" + return f"{numerical_part!s} {name_prefix}" return str(value) @@ -111,7 +111,7 @@ def add_binary_prefix(value: float) -> str: for prefix in BinaryUnit: numerical_part = value / (2**prefix.value) if numerical_part > 1: - return f"{str(numerical_part)} {prefix.name}" + return f"{numerical_part!s} {prefix.name}" return str(value) diff --git a/conversions/pressure_conversions.py b/conversions/pressure_conversions.py index e0cd18d234ba..fe78b1382677 100644 --- a/conversions/pressure_conversions.py +++ b/conversions/pressure_conversions.py @@ -19,19 +19,23 @@ -> https://www.unitconverters.net/pressure-converter.html """ -from collections import namedtuple +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float -from_to = namedtuple("from_to", "from_ to") PRESSURE_CONVERSION = { - "atm": from_to(1, 1), - "pascal": from_to(0.0000098, 101325), - "bar": from_to(0.986923, 1.01325), - "kilopascal": from_to(0.00986923, 101.325), - "megapascal": from_to(9.86923, 0.101325), - "psi": from_to(0.068046, 14.6959), - "inHg": from_to(0.0334211, 29.9213), - "torr": from_to(0.00131579, 760), + "atm": FromTo(1, 1), + "pascal": FromTo(0.0000098, 101325), + "bar": FromTo(0.986923, 1.01325), + "kilopascal": FromTo(0.00986923, 101.325), + "megapascal": FromTo(9.86923, 0.101325), + "psi": FromTo(0.068046, 14.6959), + "inHg": FromTo(0.0334211, 29.9213), + "torr": FromTo(0.00131579, 760), } @@ -71,7 +75,9 @@ def pressure_conversion(value: float, from_type: str, to_type: str) -> float: + ", ".join(PRESSURE_CONVERSION) ) return ( - value * PRESSURE_CONVERSION[from_type].from_ * PRESSURE_CONVERSION[to_type].to + value + * PRESSURE_CONVERSION[from_type].from_factor + * PRESSURE_CONVERSION[to_type].to_factor ) diff --git a/conversions/rectangular_to_polar.py b/conversions/rectangular_to_polar.py new file mode 100644 index 000000000000..bed97d7410ec --- /dev/null +++ b/conversions/rectangular_to_polar.py @@ -0,0 +1,32 @@ +import math + + +def rectangular_to_polar(real: float, img: float) -> tuple[float, float]: + """ + https://en.wikipedia.org/wiki/Polar_coordinate_system + + >>> rectangular_to_polar(5,-5) + (7.07, -45.0) + >>> rectangular_to_polar(-1,1) + (1.41, 135.0) + >>> rectangular_to_polar(-1,-1) + (1.41, -135.0) + >>> rectangular_to_polar(1e-10,1e-10) + (0.0, 45.0) + >>> rectangular_to_polar(-1e-10,1e-10) + (0.0, 135.0) + >>> rectangular_to_polar(9.75,5.93) + (11.41, 31.31) + >>> rectangular_to_polar(10000,99999) + (100497.76, 84.29) + """ + + mod = round(math.sqrt((real**2) + (img**2)), 2) + ang = round(math.degrees(math.atan2(img, real)), 2) + return (mod, ang) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/rgb_cmyk_conversion.py b/conversions/rgb_cmyk_conversion.py new file mode 100644 index 000000000000..07d65b704c44 --- /dev/null +++ b/conversions/rgb_cmyk_conversion.py @@ -0,0 +1,71 @@ +def rgb_to_cmyk(r_input: int, g_input: int, b_input: int) -> tuple[int, int, int, int]: + """ + Simple RGB to CMYK conversion. Returns percentages of CMYK paint. + https://www.programmingalgorithms.com/algorithm/rgb-to-cmyk/ + + Note: this is a very popular algorithm that converts colors linearly and gives + only approximate results. Actual preparation for printing requires advanced color + conversion considering the color profiles and parameters of the target device. + + >>> rgb_to_cmyk(255, 200, "a") + Traceback (most recent call last): + ... + ValueError: Expected int, found (, , ) + + >>> rgb_to_cmyk(255, 255, 999) + Traceback (most recent call last): + ... + ValueError: Expected int of the range 0..255 + + >>> rgb_to_cmyk(255, 255, 255) # white + (0, 0, 0, 0) + + >>> rgb_to_cmyk(128, 128, 128) # gray + (0, 0, 0, 50) + + >>> rgb_to_cmyk(0, 0, 0) # black + (0, 0, 0, 100) + + >>> rgb_to_cmyk(255, 0, 0) # red + (0, 100, 100, 0) + + >>> rgb_to_cmyk(0, 255, 0) # green + (100, 0, 100, 0) + + >>> rgb_to_cmyk(0, 0, 255) # blue + (100, 100, 0, 0) + """ + + if ( + not isinstance(r_input, int) + or not isinstance(g_input, int) + or not isinstance(b_input, int) + ): + msg = f"Expected int, found {type(r_input), type(g_input), type(b_input)}" + raise ValueError(msg) + + if not 0 <= r_input < 256 or not 0 <= g_input < 256 or not 0 <= b_input < 256: + raise ValueError("Expected int of the range 0..255") + + # changing range from 0..255 to 0..1 + r = r_input / 255 + g = g_input / 255 + b = b_input / 255 + + k = 1 - max(r, g, b) + + if k == 1: # pure black + return 0, 0, 0, 100 + + c = round(100 * (1 - r - k) / (1 - k)) + m = round(100 * (1 - g - k) / (1 - k)) + y = round(100 * (1 - b - k) / (1 - k)) + k = round(100 * k) + + return c, m, y, k + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/rgb_hsv_conversion.py b/conversions/rgb_hsv_conversion.py index 081cfe1d75e0..74b3d33e49e7 100644 --- a/conversions/rgb_hsv_conversion.py +++ b/conversions/rgb_hsv_conversion.py @@ -121,8 +121,8 @@ def rgb_to_hsv(red: int, green: int, blue: int) -> list[float]: float_red = red / 255 float_green = green / 255 float_blue = blue / 255 - value = max(max(float_red, float_green), float_blue) - chroma = value - min(min(float_red, float_green), float_blue) + value = max(float_red, float_green, float_blue) + chroma = value - min(float_red, float_green, float_blue) saturation = 0 if value == 0 else chroma / value if chroma == 0: diff --git a/conversions/speed_conversions.py b/conversions/speed_conversions.py index 62da9e137bc7..ba497119d3f5 100644 --- a/conversions/speed_conversions.py +++ b/conversions/speed_conversions.py @@ -57,10 +57,11 @@ def convert_speed(speed: float, unit_from: str, unit_to: str) -> float: 115.078 """ if unit_to not in speed_chart or unit_from not in speed_chart_inverse: - raise ValueError( + msg = ( f"Incorrect 'from_type' or 'to_type' value: {unit_from!r}, {unit_to!r}\n" f"Valid values are: {', '.join(speed_chart_inverse)}" ) + raise ValueError(msg) return round(speed * speed_chart[unit_from] * speed_chart_inverse[unit_to], 3) diff --git a/conversions/temperature_conversions.py b/conversions/temperature_conversions.py index f7af6c8f1e2b..dde1d2f0f166 100644 --- a/conversions/temperature_conversions.py +++ b/conversions/temperature_conversions.py @@ -1,4 +1,4 @@ -""" Convert between different units of temperature """ +"""Convert between different units of temperature""" def celsius_to_fahrenheit(celsius: float, ndigits: int = 2) -> float: diff --git a/conversions/time_conversions.py b/conversions/time_conversions.py new file mode 100644 index 000000000000..8c30f5bc4a45 --- /dev/null +++ b/conversions/time_conversions.py @@ -0,0 +1,86 @@ +""" +A unit of time is any particular time interval, used as a standard way of measuring or +expressing duration. The base unit of time in the International System of Units (SI), +and by extension most of the Western world, is the second, defined as about 9 billion +oscillations of the caesium atom. + +https://en.wikipedia.org/wiki/Unit_of_time +""" + +time_chart: dict[str, float] = { + "seconds": 1.0, + "minutes": 60.0, # 1 minute = 60 sec + "hours": 3600.0, # 1 hour = 60 minutes = 3600 seconds + "days": 86400.0, # 1 day = 24 hours = 1440 min = 86400 sec + "weeks": 604800.0, # 1 week=7d=168hr=10080min = 604800 sec + "months": 2629800.0, # Approximate value for a month in seconds + "years": 31557600.0, # Approximate value for a year in seconds +} + +time_chart_inverse: dict[str, float] = { + key: 1 / value for key, value in time_chart.items() +} + + +def convert_time(time_value: float, unit_from: str, unit_to: str) -> float: + """ + Convert time from one unit to another using the time_chart above. + + >>> convert_time(3600, "seconds", "hours") + 1.0 + >>> convert_time(3500, "Seconds", "Hours") + 0.972 + >>> convert_time(1, "DaYs", "hours") + 24.0 + >>> convert_time(120, "minutes", "SeCoNdS") + 7200.0 + >>> convert_time(2, "WEEKS", "days") + 14.0 + >>> convert_time(0.5, "hours", "MINUTES") + 30.0 + >>> convert_time(-3600, "seconds", "hours") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time("Hello", "hours", "minutes") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time([0, 1, 2], "weeks", "days") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time(1, "cool", "century") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Invalid unit cool is not in seconds, minutes, hours, days, weeks, ... + >>> convert_time(1, "seconds", "hot") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Invalid unit hot is not in seconds, minutes, hours, days, weeks, ... + """ + if not isinstance(time_value, (int, float)) or time_value < 0: + msg = "'time_value' must be a non-negative number." + raise ValueError(msg) + + unit_from = unit_from.lower() + unit_to = unit_to.lower() + if unit_from not in time_chart or unit_to not in time_chart: + invalid_unit = unit_from if unit_from not in time_chart else unit_to + msg = f"Invalid unit {invalid_unit} is not in {', '.join(time_chart)}." + raise ValueError(msg) + + return round( + time_value * time_chart[unit_from] * time_chart_inverse[unit_to], + 3, + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(f"{convert_time(3600,'seconds', 'hours') = :,}") + print(f"{convert_time(360, 'days', 'months') = :,}") + print(f"{convert_time(360, 'months', 'years') = :,}") + print(f"{convert_time(1, 'years', 'seconds') = :,}") diff --git a/conversions/volume_conversions.py b/conversions/volume_conversions.py index 44d29009120c..cb240380534b 100644 --- a/conversions/volume_conversions.py +++ b/conversions/volume_conversions.py @@ -18,35 +18,39 @@ -> Wikipedia reference: https://en.wikipedia.org/wiki/Cup_(unit) """ -from collections import namedtuple +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float -from_to = namedtuple("from_to", "from_ to") METRIC_CONVERSION = { - "cubicmeter": from_to(1, 1), - "litre": from_to(0.001, 1000), - "kilolitre": from_to(1, 1), - "gallon": from_to(0.00454, 264.172), - "cubicyard": from_to(0.76455, 1.30795), - "cubicfoot": from_to(0.028, 35.3147), - "cup": from_to(0.000236588, 4226.75), + "cubic meter": FromTo(1, 1), + "litre": FromTo(0.001, 1000), + "kilolitre": FromTo(1, 1), + "gallon": FromTo(0.00454, 264.172), + "cubic yard": FromTo(0.76455, 1.30795), + "cubic foot": FromTo(0.028, 35.3147), + "cup": FromTo(0.000236588, 4226.75), } def volume_conversion(value: float, from_type: str, to_type: str) -> float: """ Conversion between volume units. - >>> volume_conversion(4, "cubicmeter", "litre") + >>> volume_conversion(4, "cubic meter", "litre") 4000 >>> volume_conversion(1, "litre", "gallon") 0.264172 - >>> volume_conversion(1, "kilolitre", "cubicmeter") + >>> volume_conversion(1, "kilolitre", "cubic meter") 1 - >>> volume_conversion(3, "gallon", "cubicyard") + >>> volume_conversion(3, "gallon", "cubic yard") 0.017814279 - >>> volume_conversion(2, "cubicyard", "litre") + >>> volume_conversion(2, "cubic yard", "litre") 1529.1 - >>> volume_conversion(4, "cubicfoot", "cup") + >>> volume_conversion(4, "cubic foot", "cup") 473.396 >>> volume_conversion(1, "cup", "kilolitre") 0.000236588 @@ -54,7 +58,7 @@ def volume_conversion(value: float, from_type: str, to_type: str) -> float: Traceback (most recent call last): ... ValueError: Invalid 'from_type' value: 'wrongUnit' Supported values are: - cubicmeter, litre, kilolitre, gallon, cubicyard, cubicfoot, cup + cubic meter, litre, kilolitre, gallon, cubic yard, cubic foot, cup """ if from_type not in METRIC_CONVERSION: raise ValueError( @@ -66,7 +70,11 @@ def volume_conversion(value: float, from_type: str, to_type: str) -> float: f"Invalid 'to_type' value: {to_type!r}. Supported values are:\n" + ", ".join(METRIC_CONVERSION) ) - return value * METRIC_CONVERSION[from_type].from_ * METRIC_CONVERSION[to_type].to + return ( + value + * METRIC_CONVERSION[from_type].from_factor + * METRIC_CONVERSION[to_type].to_factor + ) if __name__ == "__main__": diff --git a/conversions/weight_conversion.py b/conversions/weight_conversion.py index 5c032a497a7b..0777aead9f02 100644 --- a/conversions/weight_conversion.py +++ b/conversions/weight_conversion.py @@ -297,12 +297,19 @@ def weight_conversion(from_type: str, to_type: str, value: float) -> float: 1.660540199e-23 >>> weight_conversion("atomic-mass-unit","atomic-mass-unit",2) 1.999999998903455 + >>> weight_conversion("slug", "kilogram", 1) + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' or 'to_type' value: 'slug', 'kilogram' + Supported values are: kilogram, gram, milligram, metric-ton, long-ton, short-ton, \ +pound, stone, ounce, carrat, atomic-mass-unit """ if to_type not in KILOGRAM_CHART or from_type not in WEIGHT_TYPE_CHART: - raise ValueError( + msg = ( f"Invalid 'from_type' or 'to_type' value: {from_type!r}, {to_type!r}\n" f"Supported values are: {', '.join(WEIGHT_TYPE_CHART)}" ) + raise ValueError(msg) return value * KILOGRAM_CHART[to_type] * WEIGHT_TYPE_CHART[from_type] diff --git a/arithmetic_analysis/__init__.py b/data_structures/arrays/__init__.py similarity index 100% rename from arithmetic_analysis/__init__.py rename to data_structures/arrays/__init__.py diff --git a/data_structures/arrays/equilibrium_index_in_array.py b/data_structures/arrays/equilibrium_index_in_array.py new file mode 100644 index 000000000000..0717a45d9f4b --- /dev/null +++ b/data_structures/arrays/equilibrium_index_in_array.py @@ -0,0 +1,58 @@ +""" +Find the Equilibrium Index of an Array. +Reference: https://www.geeksforgeeks.org/equilibrium-index-of-an-array/ + +Python doctest can be run with the following command: +python -m doctest -v equilibrium_index_in_array.py + +Given a sequence arr[] of size n, this function returns +an equilibrium index (if any) or -1 if no equilibrium index exists. + +The equilibrium index of an array is an index such that the sum of +elements at lower indexes is equal to the sum of elements at higher indexes. + + + +Example Input: +arr = [-7, 1, 5, 2, -4, 3, 0] +Output: 3 + +""" + + +def equilibrium_index(arr: list[int]) -> int: + """ + Find the equilibrium index of an array. + + Args: + arr (list[int]): The input array of integers. + + Returns: + int: The equilibrium index or -1 if no equilibrium index exists. + + Examples: + >>> equilibrium_index([-7, 1, 5, 2, -4, 3, 0]) + 3 + >>> equilibrium_index([1, 2, 3, 4, 5]) + -1 + >>> equilibrium_index([1, 1, 1, 1, 1]) + 2 + >>> equilibrium_index([2, 4, 6, 8, 10, 3]) + -1 + """ + total_sum = sum(arr) + left_sum = 0 + + for i, value in enumerate(arr): + total_sum -= value + if left_sum == total_sum: + return i + left_sum += value + + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/find_triplets_with_0_sum.py b/data_structures/arrays/find_triplets_with_0_sum.py new file mode 100644 index 000000000000..52e521906873 --- /dev/null +++ b/data_structures/arrays/find_triplets_with_0_sum.py @@ -0,0 +1,87 @@ +from itertools import combinations + + +def find_triplets_with_0_sum(nums: list[int]) -> list[list[int]]: + """ + Given a list of integers, return elements a, b, c such that a + b + c = 0. + Args: + nums: list of integers + Returns: + list of lists of integers where sum(each_list) == 0 + Examples: + >>> find_triplets_with_0_sum([-1, 0, 1, 2, -1, -4]) + [[-1, -1, 2], [-1, 0, 1]] + >>> find_triplets_with_0_sum([]) + [] + >>> find_triplets_with_0_sum([0, 0, 0]) + [[0, 0, 0]] + >>> find_triplets_with_0_sum([1, 2, 3, 0, -1, -2, -3]) + [[-3, 0, 3], [-3, 1, 2], [-2, -1, 3], [-2, 0, 2], [-1, 0, 1]] + """ + return [ + list(x) + for x in sorted({abc for abc in combinations(sorted(nums), 3) if not sum(abc)}) + ] + + +def find_triplets_with_0_sum_hashing(arr: list[int]) -> list[list[int]]: + """ + Function for finding the triplets with a given sum in the array using hashing. + + Given a list of integers, return elements a, b, c such that a + b + c = 0. + + Args: + nums: list of integers + Returns: + list of lists of integers where sum(each_list) == 0 + Examples: + >>> find_triplets_with_0_sum_hashing([-1, 0, 1, 2, -1, -4]) + [[-1, 0, 1], [-1, -1, 2]] + >>> find_triplets_with_0_sum_hashing([]) + [] + >>> find_triplets_with_0_sum_hashing([0, 0, 0]) + [[0, 0, 0]] + >>> find_triplets_with_0_sum_hashing([1, 2, 3, 0, -1, -2, -3]) + [[-1, 0, 1], [-3, 1, 2], [-2, 0, 2], [-2, -1, 3], [-3, 0, 3]] + + Time complexity: O(N^2) + Auxiliary Space: O(N) + + """ + target_sum = 0 + + # Initialize the final output array with blank. + output_arr = [] + + # Set the initial element as arr[i]. + for index, item in enumerate(arr[:-2]): + # to store second elements that can complement the final sum. + set_initialize = set() + + # current sum needed for reaching the target sum + current_sum = target_sum - item + + # Traverse the subarray arr[i+1:]. + for other_item in arr[index + 1 :]: + # required value for the second element + required_value = current_sum - other_item + + # Verify if the desired value exists in the set. + if required_value in set_initialize: + # finding triplet elements combination. + combination_array = sorted([item, other_item, required_value]) + if combination_array not in output_arr: + output_arr.append(combination_array) + + # Include the current element in the set + # for subsequent complement verification. + set_initialize.add(other_item) + + # Return all the triplet combinations. + return output_arr + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/arrays/index_2d_array_in_1d.py b/data_structures/arrays/index_2d_array_in_1d.py new file mode 100644 index 000000000000..27a9fa5f9121 --- /dev/null +++ b/data_structures/arrays/index_2d_array_in_1d.py @@ -0,0 +1,105 @@ +""" +Retrieves the value of an 0-indexed 1D index from a 2D array. +There are two ways to retrieve value(s): + +1. Index2DArrayIterator(matrix) -> Iterator[int] +This iterator allows you to iterate through a 2D array by passing in the matrix and +calling next(your_iterator). You can also use the iterator in a loop. +Examples: +list(Index2DArrayIterator(matrix)) +set(Index2DArrayIterator(matrix)) +tuple(Index2DArrayIterator(matrix)) +sum(Index2DArrayIterator(matrix)) +-5 in Index2DArrayIterator(matrix) + +2. index_2d_array_in_1d(array: list[int], index: int) -> int +This function allows you to provide a 2D array and a 0-indexed 1D integer index, +and retrieves the integer value at that index. + +Python doctests can be run using this command: +python3 -m doctest -v index_2d_array_in_1d.py +""" + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Index2DArrayIterator: + matrix: list[list[int]] + + def __iter__(self) -> Iterator[int]: + """ + >>> tuple(Index2DArrayIterator([[5], [-523], [-1], [34], [0]])) + (5, -523, -1, 34, 0) + >>> tuple(Index2DArrayIterator([[5, -523, -1], [34, 0]])) + (5, -523, -1, 34, 0) + >>> tuple(Index2DArrayIterator([[5, -523, -1, 34, 0]])) + (5, -523, -1, 34, 0) + >>> t = Index2DArrayIterator([[5, 2, 25], [23, 14, 5], [324, -1, 0]]) + >>> tuple(t) + (5, 2, 25, 23, 14, 5, 324, -1, 0) + >>> list(t) + [5, 2, 25, 23, 14, 5, 324, -1, 0] + >>> sorted(t) + [-1, 0, 2, 5, 5, 14, 23, 25, 324] + >>> tuple(t)[3] + 23 + >>> sum(t) + 397 + >>> -1 in t + True + >>> t = iter(Index2DArrayIterator([[5], [-523], [-1], [34], [0]])) + >>> next(t) + 5 + >>> next(t) + -523 + """ + for row in self.matrix: + yield from row + + +def index_2d_array_in_1d(array: list[list[int]], index: int) -> int: + """ + Retrieves the value of the one-dimensional index from a two-dimensional array. + + Args: + array: A 2D array of integers where all rows are the same size and all + columns are the same size. + index: A 1D index. + + Returns: + int: The 0-indexed value of the 1D index in the array. + + Examples: + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 5) + 5 + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], -1) + Traceback (most recent call last): + ... + ValueError: index out of range + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 12) + Traceback (most recent call last): + ... + ValueError: index out of range + >>> index_2d_array_in_1d([[]], 0) + Traceback (most recent call last): + ... + ValueError: no items in array + """ + rows = len(array) + cols = len(array[0]) + + if rows == 0 or cols == 0: + raise ValueError("no items in array") + + if index < 0 or index >= rows * cols: + raise ValueError("index out of range") + + return array[index // cols][index % cols] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/kth_largest_element.py b/data_structures/arrays/kth_largest_element.py new file mode 100644 index 000000000000..f25cc68e9b72 --- /dev/null +++ b/data_structures/arrays/kth_largest_element.py @@ -0,0 +1,117 @@ +""" +Given an array of integers and an integer k, find the kth largest element in the array. + +https://stackoverflow.com/questions/251781 +""" + + +def partition(arr: list[int], low: int, high: int) -> int: + """ + Partitions list based on the pivot element. + + This function rearranges the elements in the input list 'elements' such that + all elements greater than or equal to the chosen pivot are on the right side + of the pivot, and all elements smaller than the pivot are on the left side. + + Args: + arr: The list to be partitioned + low: The lower index of the list + high: The higher index of the list + + Returns: + int: The index of pivot element after partitioning + + Examples: + >>> partition([3, 1, 4, 5, 9, 2, 6, 5, 3, 5], 0, 9) + 4 + >>> partition([7, 1, 4, 5, 9, 2, 6, 5, 8], 0, 8) + 1 + >>> partition(['apple', 'cherry', 'date', 'banana'], 0, 3) + 2 + >>> partition([3.1, 1.2, 5.6, 4.7], 0, 3) + 1 + """ + pivot = arr[high] + i = low - 1 + for j in range(low, high): + if arr[j] >= pivot: + i += 1 + arr[i], arr[j] = arr[j], arr[i] + arr[i + 1], arr[high] = arr[high], arr[i + 1] + return i + 1 + + +def kth_largest_element(arr: list[int], position: int) -> int: + """ + Finds the kth largest element in a list. + Should deliver similar results to: + ```python + def kth_largest_element(arr, position): + return sorted(arr)[-position] + ``` + + Args: + nums: The list of numbers. + k: The position of the desired kth largest element. + + Returns: + int: The kth largest element. + + Examples: + >>> kth_largest_element([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5], 3) + 5 + >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], 1) + 9 + >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], -2) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element([9, 1, 3, 6, 7, 9, 8, 4, 2, 4, 9], 110) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element([1, 2, 4, 3, 5, 9, 7, 6, 5, 9, 3], 0) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element(['apple', 'cherry', 'date', 'banana'], 2) + 'cherry' + >>> kth_largest_element([3.1, 1.2, 5.6, 4.7,7.9,5,0], 2) + 5.6 + >>> kth_largest_element([-2, -5, -4, -1], 1) + -1 + >>> kth_largest_element([], 1) + -1 + >>> kth_largest_element([3.1, 1.2, 5.6, 4.7, 7.9, 5, 0], 1.5) + Traceback (most recent call last): + ... + ValueError: The position should be an integer + >>> kth_largest_element((4, 6, 1, 2), 4) + Traceback (most recent call last): + ... + TypeError: 'tuple' object does not support item assignment + """ + if not arr: + return -1 + if not isinstance(position, int): + raise ValueError("The position should be an integer") + if not 1 <= position <= len(arr): + raise ValueError("Invalid value of 'position'") + low, high = 0, len(arr) - 1 + while low <= high: + if low > len(arr) - 1 or high < 0: + return -1 + pivot_index = partition(arr, low, high) + if pivot_index == position - 1: + return arr[pivot_index] + elif pivot_index > position - 1: + high = pivot_index - 1 + else: + low = pivot_index + 1 + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/median_two_array.py b/data_structures/arrays/median_two_array.py new file mode 100644 index 000000000000..972b0ee44201 --- /dev/null +++ b/data_structures/arrays/median_two_array.py @@ -0,0 +1,61 @@ +""" +https://www.enjoyalgorithms.com/blog/median-of-two-sorted-arrays +""" + + +def find_median_sorted_arrays(nums1: list[int], nums2: list[int]) -> float: + """ + Find the median of two arrays. + + Args: + nums1: The first array. + nums2: The second array. + + Returns: + The median of the two arrays. + + Examples: + >>> find_median_sorted_arrays([1, 3], [2]) + 2.0 + + >>> find_median_sorted_arrays([1, 2], [3, 4]) + 2.5 + + >>> find_median_sorted_arrays([0, 0], [0, 0]) + 0.0 + + >>> find_median_sorted_arrays([], []) + Traceback (most recent call last): + ... + ValueError: Both input arrays are empty. + + >>> find_median_sorted_arrays([], [1]) + 1.0 + + >>> find_median_sorted_arrays([-1000], [1000]) + 0.0 + + >>> find_median_sorted_arrays([-1.1, -2.2], [-3.3, -4.4]) + -2.75 + """ + if not nums1 and not nums2: + raise ValueError("Both input arrays are empty.") + + # Merge the arrays into a single sorted array. + merged = sorted(nums1 + nums2) + total = len(merged) + + if total % 2 == 1: # If the total number of elements is odd + return float(merged[total // 2]) # then return the middle element + + # If the total number of elements is even, calculate + # the average of the two middle elements as the median. + middle1 = merged[total // 2 - 1] + middle2 = merged[total // 2] + return (float(middle1) + float(middle2)) / 2.0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/monotonic_array.py b/data_structures/arrays/monotonic_array.py new file mode 100644 index 000000000000..342d443a9cfc --- /dev/null +++ b/data_structures/arrays/monotonic_array.py @@ -0,0 +1,37 @@ +# https://leetcode.com/problems/monotonic-array/ +def is_monotonic(nums: list[int]) -> bool: + """ + Check if a list is monotonic. + + >>> is_monotonic([1, 2, 2, 3]) + True + >>> is_monotonic([6, 5, 4, 4]) + True + >>> is_monotonic([1, 3, 2]) + False + >>> is_monotonic([1,2,3,4,5,6,5]) + False + >>> is_monotonic([-3,-2,-1]) + True + >>> is_monotonic([-5,-6,-7]) + True + >>> is_monotonic([0,0,0]) + True + >>> is_monotonic([-100,0,100]) + True + """ + return all(nums[i] <= nums[i + 1] for i in range(len(nums) - 1)) or all( + nums[i] >= nums[i + 1] for i in range(len(nums) - 1) + ) + + +# Test the function with your examples +if __name__ == "__main__": + # Test the function with your examples + print(is_monotonic([1, 2, 2, 3])) # Output: True + print(is_monotonic([6, 5, 4, 4])) # Output: True + print(is_monotonic([1, 3, 2])) # Output: False + + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/pairs_with_given_sum.py b/data_structures/arrays/pairs_with_given_sum.py new file mode 100644 index 000000000000..b27bd78e1e0f --- /dev/null +++ b/data_structures/arrays/pairs_with_given_sum.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +""" +Given an array of integers and an integer req_sum, find the number of pairs of array +elements whose sum is equal to req_sum. + +https://practice.geeksforgeeks.org/problems/count-pairs-with-given-sum5022/0 +""" + +from itertools import combinations + + +def pairs_with_sum(arr: list, req_sum: int) -> int: + """ + Return the no. of pairs with sum "sum" + >>> pairs_with_sum([1, 5, 7, 1], 6) + 2 + >>> pairs_with_sum([1, 1, 1, 1, 1, 1, 1, 1], 2) + 28 + >>> pairs_with_sum([1, 7, 6, 2, 5, 4, 3, 1, 9, 8], 7) + 4 + """ + return len([1 for a, b in combinations(arr, 2) if a + b == req_sum]) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/arrays/permutations.py b/data_structures/arrays/permutations.py index eb3f26517863..4906dd5c2ae1 100644 --- a/data_structures/arrays/permutations.py +++ b/data_structures/arrays/permutations.py @@ -1,18 +1,16 @@ -def permute(nums: list[int]) -> list[list[int]]: +def permute_recursive(nums: list[int]) -> list[list[int]]: """ Return all permutations. - >>> from itertools import permutations - >>> numbers= [1,2,3] - >>> all(list(nums) in permute(numbers) for nums in permutations(numbers)) - True + >>> permute_recursive([1, 2, 3]) + [[3, 2, 1], [2, 3, 1], [1, 3, 2], [3, 1, 2], [2, 1, 3], [1, 2, 3]] """ - result = [] - if len(nums) == 1: - return [nums.copy()] + result: list[list[int]] = [] + if len(nums) == 0: + return [[]] for _ in range(len(nums)): n = nums.pop(0) - permutations = permute(nums) + permutations = permute_recursive(nums.copy()) for perm in permutations: perm.append(n) result.extend(permutations) @@ -20,7 +18,31 @@ def permute(nums: list[int]) -> list[list[int]]: return result +def permute_backtrack(nums: list[int]) -> list[list[int]]: + """ + Return all permutations of the given list. + + >>> permute_backtrack([1, 2, 3]) + [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]] + """ + + def backtrack(start: int) -> None: + if start == len(nums) - 1: + output.append(nums[:]) + else: + for i in range(start, len(nums)): + nums[start], nums[i] = nums[i], nums[start] + backtrack(start + 1) + nums[start], nums[i] = nums[i], nums[start] # backtrack + + output: list[list[int]] = [] + backtrack(0) + return output + + if __name__ == "__main__": import doctest + result = permute_backtrack([1, 2, 3]) + print(result) doctest.testmod() diff --git a/data_structures/arrays/product_sum.py b/data_structures/arrays/product_sum.py new file mode 100644 index 000000000000..4fb906f369ab --- /dev/null +++ b/data_structures/arrays/product_sum.py @@ -0,0 +1,98 @@ +""" +Calculate the Product Sum from a Special Array. +reference: https://dev.to/sfrasica/algorithms-product-sum-from-an-array-dc6 + +Python doctests can be run with the following command: +python -m doctest -v product_sum.py + +Calculate the product sum of a "special" array which can contain integers or nested +arrays. The product sum is obtained by adding all elements and multiplying by their +respective depths. + +For example, in the array [x, y], the product sum is (x + y). In the array [x, [y, z]], +the product sum is x + 2 * (y + z). In the array [x, [y, [z]]], +the product sum is x + 2 * (y + 3z). + +Example Input: +[5, 2, [-7, 1], 3, [6, [-13, 8], 4]] +Output: 12 + +""" + + +def product_sum(arr: list[int | list], depth: int) -> int: + """ + Recursively calculates the product sum of an array. + + The product sum of an array is defined as the sum of its elements multiplied by + their respective depths. If an element is a list, its product sum is calculated + recursively by multiplying the sum of its elements with its depth plus one. + + Args: + arr: The array of integers and nested lists. + depth: The current depth level. + + Returns: + int: The product sum of the array. + + Examples: + >>> product_sum([1, 2, 3], 1) + 6 + >>> product_sum([-1, 2, [-3, 4]], 2) + 8 + >>> product_sum([1, 2, 3], -1) + -6 + >>> product_sum([1, 2, 3], 0) + 0 + >>> product_sum([1, 2, 3], 7) + 42 + >>> product_sum((1, 2, 3), 7) + 42 + >>> product_sum({1, 2, 3}, 7) + 42 + >>> product_sum([1, -1], 1) + 0 + >>> product_sum([1, -2], 1) + -1 + >>> product_sum([-3.5, [1, [0.5]]], 1) + 1.5 + + """ + total_sum = 0 + for ele in arr: + total_sum += product_sum(ele, depth + 1) if isinstance(ele, list) else ele + return total_sum * depth + + +def product_sum_array(array: list[int | list]) -> int: + """ + Calculates the product sum of an array. + + Args: + array (List[Union[int, List]]): The array of integers and nested lists. + + Returns: + int: The product sum of the array. + + Examples: + >>> product_sum_array([1, 2, 3]) + 6 + >>> product_sum_array([1, [2, 3]]) + 11 + >>> product_sum_array([1, [2, [3, 4]]]) + 47 + >>> product_sum_array([0]) + 0 + >>> product_sum_array([-3.5, [1, [0.5]]]) + 1.5 + >>> product_sum_array([1, -2]) + -1 + + """ + return product_sum(array, 1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/sparse_table.py b/data_structures/arrays/sparse_table.py new file mode 100644 index 000000000000..4606fe908607 --- /dev/null +++ b/data_structures/arrays/sparse_table.py @@ -0,0 +1,95 @@ +""" +Sparse table is a data structure that allows answering range queries on +a static number list, i.e. the elements do not change throughout all the queries. + +The implementation below will solve the problem of Range Minimum Query: +Finding the minimum value of a subset [L..R] of a static number list. + +Overall time complexity: O(nlogn) +Overall space complexity: O(nlogn) + +Wikipedia link: https://en.wikipedia.org/wiki/Range_minimum_query +""" + +from math import log2 + + +def build_sparse_table(number_list: list[int]) -> list[list[int]]: + """ + Precompute range minimum queries with power of two length and store the precomputed + values in a table. + + >>> build_sparse_table([8, 1, 0, 3, 4, 9, 3]) + [[8, 1, 0, 3, 4, 9, 3], [1, 0, 0, 3, 4, 3, 0], [0, 0, 0, 3, 0, 0, 0]] + >>> build_sparse_table([3, 1, 9]) + [[3, 1, 9], [1, 1, 0]] + >>> build_sparse_table([]) + Traceback (most recent call last): + ... + ValueError: empty number list not allowed + """ + if not number_list: + raise ValueError("empty number list not allowed") + + length = len(number_list) + # Initialise sparse_table -- sparse_table[j][i] represents the minimum value of the + # subset of length (2 ** j) of number_list, starting from index i. + + # smallest power of 2 subset length that fully covers number_list + row = int(log2(length)) + 1 + sparse_table = [[0 for i in range(length)] for j in range(row)] + + # minimum of subset of length 1 is that value itself + for i, value in enumerate(number_list): + sparse_table[0][i] = value + j = 1 + + # compute the minimum value for all intervals with size (2 ** j) + while (1 << j) <= length: + i = 0 + # while subset starting from i still have at least (2 ** j) elements + while (i + (1 << j) - 1) < length: + # split range [i, i + 2 ** j] and find minimum of 2 halves + sparse_table[j][i] = min( + sparse_table[j - 1][i + (1 << (j - 1))], sparse_table[j - 1][i] + ) + i += 1 + j += 1 + return sparse_table + + +def query(sparse_table: list[list[int]], left_bound: int, right_bound: int) -> int: + """ + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 0, 4) + 0 + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 4, 6) + 3 + >>> query(build_sparse_table([3, 1, 9]), 2, 2) + 9 + >>> query(build_sparse_table([3, 1, 9]), 0, 1) + 1 + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 0, 11) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> query(build_sparse_table([]), 0, 0) + Traceback (most recent call last): + ... + ValueError: empty number list not allowed + """ + if left_bound < 0 or right_bound >= len(sparse_table[0]): + raise IndexError("list index out of range") + + # highest subset length of power of 2 that is within range [left_bound, right_bound] + j = int(log2(right_bound - left_bound + 1)) + + # minimum of 2 overlapping smaller subsets: + # [left_bound, left_bound + 2 ** j - 1] and [right_bound - 2 ** j + 1, right_bound] + return min(sparse_table[j][right_bound - (1 << j) + 1], sparse_table[j][left_bound]) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(f"{query(build_sparse_table([3, 1, 9]), 2, 2) = }") diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py new file mode 100644 index 000000000000..e1714e57ece8 --- /dev/null +++ b/data_structures/arrays/sudoku_solver.py @@ -0,0 +1,246 @@ +""" +Please do not modify this file! It is published at https://norvig.com/sudoku.html with +only minimal changes to work with modern versions of Python. If you have improvements, +please make them in a separate file. +""" + +import random +import time + + +def cross(items_a, items_b): + """ + Cross product of elements in A and elements in B. + """ + return [a + b for a in items_a for b in items_b] + + +digits = "123456789" +rows = "ABCDEFGHI" +cols = digits +squares = cross(rows, cols) +unitlist = ( + [cross(rows, c) for c in cols] + + [cross(r, cols) for r in rows] + + [cross(rs, cs) for rs in ("ABC", "DEF", "GHI") for cs in ("123", "456", "789")] +) +units = {s: [u for u in unitlist if s in u] for s in squares} +peers = {s: {x for u in units[s] for x in u} - {s} for s in squares} + + +def test(): + """A set of unit tests.""" + assert len(squares) == 81 + assert len(unitlist) == 27 + assert all(len(units[s]) == 3 for s in squares) + assert all(len(peers[s]) == 20 for s in squares) + assert units["C2"] == [ + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2", "I2"], + ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"], + ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"], + ] + # fmt: off + assert peers["C2"] == { + "A2", "B2", "D2", "E2", "F2", "G2", "H2", "I2", "C1", "C3", + "C4", "C5", "C6", "C7", "C8", "C9", "A1", "A3", "B1", "B3" + } + # fmt: on + print("All tests pass.") + + +def parse_grid(grid): + """ + Convert grid to a dict of possible values, {square: digits}, or + return False if a contradiction is detected. + """ + ## To start, every square can be any digit; then assign values from the grid. + values = {s: digits for s in squares} + for s, d in grid_values(grid).items(): + if d in digits and not assign(values, s, d): + return False ## (Fail if we can't assign d to square s.) + return values + + +def grid_values(grid): + """ + Convert grid into a dict of {square: char} with '0' or '.' for empties. + """ + chars = [c for c in grid if c in digits or c in "0."] + assert len(chars) == 81 + return dict(zip(squares, chars)) + + +def assign(values, s, d): + """ + Eliminate all the other values (except d) from values[s] and propagate. + Return values, except return False if a contradiction is detected. + """ + other_values = values[s].replace(d, "") + if all(eliminate(values, s, d2) for d2 in other_values): + return values + else: + return False + + +def eliminate(values, s, d): + """ + Eliminate d from values[s]; propagate when values or places <= 2. + Return values, except return False if a contradiction is detected. + """ + if d not in values[s]: + return values ## Already eliminated + values[s] = values[s].replace(d, "") + ## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers. + if len(values[s]) == 0: + return False ## Contradiction: removed last value + elif len(values[s]) == 1: + d2 = values[s] + if not all(eliminate(values, s2, d2) for s2 in peers[s]): + return False + ## (2) If a unit u is reduced to only one place for a value d, then put it there. + for u in units[s]: + dplaces = [s for s in u if d in values[s]] + if len(dplaces) == 0: + return False ## Contradiction: no place for this value + # d can only be in one place in unit; assign it there + elif len(dplaces) == 1 and not assign(values, dplaces[0], d): + return False + return values + + +def display(values): + """ + Display these values as a 2-D grid. + """ + width = 1 + max(len(values[s]) for s in squares) + line = "+".join(["-" * (width * 3)] * 3) + for r in rows: + print( + "".join( + values[r + c].center(width) + ("|" if c in "36" else "") for c in cols + ) + ) + if r in "CF": + print(line) + print() + + +def solve(grid): + """ + Solve the grid. + """ + return search(parse_grid(grid)) + + +def some(seq): + """Return some element of seq that is true.""" + for e in seq: + if e: + return e + return False + + +def search(values): + """ + Using depth-first search and propagation, try all possible values. + """ + if values is False: + return False ## Failed earlier + if all(len(values[s]) == 1 for s in squares): + return values ## Solved! + ## Chose the unfilled square s with the fewest possibilities + n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) + return some(search(assign(values.copy(), s, d)) for d in values[s]) + + +def solve_all(grids, name="", showif=0.0): + """ + Attempt to solve a sequence of grids. Report results. + When showif is a number of seconds, display puzzles that take longer. + When showif is None, don't display any puzzles. + """ + + def time_solve(grid): + start = time.monotonic() + values = solve(grid) + t = time.monotonic() - start + ## Display puzzles that take long enough + if showif is not None and t > showif: + display(grid_values(grid)) + if values: + display(values) + print(f"({t:.5f} seconds)\n") + return (t, solved(values)) + + times, results = zip(*[time_solve(grid) for grid in grids]) + if (n := len(grids)) > 1: + print( + "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)." # noqa: UP031 + % (sum(results), n, name, sum(times) / n, n / sum(times), max(times)) + ) + + +def solved(values): + """ + A puzzle is solved if each unit is a permutation of the digits 1 to 9. + """ + + def unitsolved(unit): + return {values[s] for s in unit} == set(digits) + + return values is not False and all(unitsolved(unit) for unit in unitlist) + + +def from_file(filename, sep="\n"): + "Parse a file into a list of strings, separated by sep." + with open(filename) as file: + return file.read().strip().split(sep) + + +def random_puzzle(assignments=17): + """ + Make a random puzzle with N or more assignments. Restart on contradictions. + Note the resulting puzzle is not guaranteed to be solvable, but empirically + about 99.8% of them are solvable. Some have multiple solutions. + """ + values = {s: digits for s in squares} + for s in shuffled(squares): + if not assign(values, s, random.choice(values[s])): + break + ds = [values[s] for s in squares if len(values[s]) == 1] + if len(ds) >= assignments and len(set(ds)) >= 8: + return "".join(values[s] if len(values[s]) == 1 else "." for s in squares) + return random_puzzle(assignments) ## Give up and make a new puzzle + + +def shuffled(seq): + """ + Return a randomly shuffled copy of the input sequence. + """ + seq = list(seq) + random.shuffle(seq) + return seq + + +grid1 = ( + "003020600900305001001806400008102900700000008006708200002609500800203009005010300" +) +grid2 = ( + "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" +) +hard1 = ( + ".....6....59.....82....8....45........3........6..3.54...325..6.................." +) + +if __name__ == "__main__": + test() + # solve_all(from_file("easy50.txt", '========'), "easy", None) + # solve_all(from_file("top95.txt"), "hard", None) + # solve_all(from_file("hardest.txt"), "hardest", None) + solve_all([random_puzzle() for _ in range(99)], "random", 100.0) + for puzzle in (grid1, grid2): # , hard1): # Takes 22 sec to solve on my M1 Mac. + display(parse_grid(puzzle)) + start = time.monotonic() + solve(puzzle) + t = time.monotonic() - start + print(f"Solved: {t:.5f} sec") diff --git a/data_structures/binary_tree/binary_tree_traversals.md b/data_structures/binary_tree/README.md similarity index 100% rename from data_structures/binary_tree/binary_tree_traversals.md rename to data_structures/binary_tree/README.md diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py index 320e7ed0d792..8558305eefe4 100644 --- a/data_structures/binary_tree/avl_tree.py +++ b/data_structures/binary_tree/avl_tree.py @@ -5,6 +5,7 @@ For testing run: python avl_tree.py """ + from __future__ import annotations import math @@ -60,19 +61,15 @@ def get_height(self) -> int: def set_data(self, data: Any) -> None: self.data = data - return def set_left(self, node: MyNode | None) -> None: self.left = node - return def set_right(self, node: MyNode | None) -> None: self.right = node - return def set_height(self, height: int) -> None: self.height = height - return def get_height(node: MyNode | None) -> int: @@ -218,11 +215,15 @@ def del_node(root: MyNode, data: Any) -> MyNode | None: return root else: root.set_left(del_node(left_child, data)) - else: # root.get_data() < data - if right_child is None: - return root - else: - root.set_right(del_node(right_child, data)) + # root.get_data() < data + elif right_child is None: + return root + else: + root.set_right(del_node(right_child, data)) + + # Re-fetch left_child and right_child references + left_child = root.get_left() + right_child = root.get_right() if get_height(right_child) - get_height(left_child) == 2: assert right_child is not None diff --git a/data_structures/binary_tree/basic_binary_tree.py b/data_structures/binary_tree/basic_binary_tree.py index 65dccf247b51..9d4c1bdbb57a 100644 --- a/data_structures/binary_tree/basic_binary_tree.py +++ b/data_structures/binary_tree/basic_binary_tree.py @@ -1,101 +1,110 @@ from __future__ import annotations +from collections.abc import Iterator +from dataclasses import dataclass + +@dataclass class Node: - """ - A Node has data variable and pointers to Nodes to its left and right. - """ - - def __init__(self, data: int) -> None: - self.data = data - self.left: Node | None = None - self.right: Node | None = None - - -def display(tree: Node | None) -> None: # In Order traversal of the tree - """ - >>> root = Node(1) - >>> root.left = Node(0) - >>> root.right = Node(2) - >>> display(root) - 0 - 1 - 2 - >>> display(root.right) - 2 - """ - if tree: - display(tree.left) - print(tree.data) - display(tree.right) - - -def depth_of_tree(tree: Node | None) -> int: - """ - Recursive function that returns the depth of a binary tree. - - >>> root = Node(0) - >>> depth_of_tree(root) - 1 - >>> root.left = Node(0) - >>> depth_of_tree(root) - 2 - >>> root.right = Node(0) - >>> depth_of_tree(root) - 2 - >>> root.left.right = Node(0) - >>> depth_of_tree(root) - 3 - >>> depth_of_tree(root.left) - 2 - """ - return 1 + max(depth_of_tree(tree.left), depth_of_tree(tree.right)) if tree else 0 - - -def is_full_binary_tree(tree: Node) -> bool: - """ - Returns True if this is a full binary tree - - >>> root = Node(0) - >>> is_full_binary_tree(root) - True - >>> root.left = Node(0) - >>> is_full_binary_tree(root) - False - >>> root.right = Node(0) - >>> is_full_binary_tree(root) - True - >>> root.left.left = Node(0) - >>> is_full_binary_tree(root) - False - >>> root.right.right = Node(0) - >>> is_full_binary_tree(root) - False - """ - if not tree: - return True - if tree.left and tree.right: - return is_full_binary_tree(tree.left) and is_full_binary_tree(tree.right) - else: - return not tree.left and not tree.right - - -def main() -> None: # Main function for testing. - tree = Node(1) - tree.left = Node(2) - tree.right = Node(3) - tree.left.left = Node(4) - tree.left.right = Node(5) - tree.left.right.left = Node(6) - tree.right.left = Node(7) - tree.right.left.left = Node(8) - tree.right.left.left.right = Node(9) - - print(is_full_binary_tree(tree)) - print(depth_of_tree(tree)) - print("Tree is: ") - display(tree) + data: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + def is_full(self) -> bool: + if not self or (not self.left and not self.right): + return True + if self.left and self.right: + return self.left.is_full() and self.right.is_full() + return False + + +@dataclass +class BinaryTree: + root: Node + + def __iter__(self) -> Iterator[int]: + return iter(self.root) + + def __len__(self) -> int: + return len(self.root) + + @classmethod + def small_tree(cls) -> BinaryTree: + """ + Return a small binary tree with 3 nodes. + >>> binary_tree = BinaryTree.small_tree() + >>> len(binary_tree) + 3 + >>> list(binary_tree) + [1, 2, 3] + """ + binary_tree = BinaryTree(Node(2)) + binary_tree.root.left = Node(1) + binary_tree.root.right = Node(3) + return binary_tree + + @classmethod + def medium_tree(cls) -> BinaryTree: + """ + Return a medium binary tree with 3 nodes. + >>> binary_tree = BinaryTree.medium_tree() + >>> len(binary_tree) + 7 + >>> list(binary_tree) + [1, 2, 3, 4, 5, 6, 7] + """ + binary_tree = BinaryTree(Node(4)) + binary_tree.root.left = two = Node(2) + two.left = Node(1) + two.right = Node(3) + binary_tree.root.right = five = Node(5) + five.right = six = Node(6) + six.right = Node(7) + return binary_tree + + def depth(self) -> int: + """ + Returns the depth of the tree + + >>> BinaryTree(Node(1)).depth() + 1 + >>> BinaryTree.small_tree().depth() + 2 + >>> BinaryTree.medium_tree().depth() + 4 + """ + return self._depth(self.root) + + def _depth(self, node: Node | None) -> int: + if not node: + return 0 + return 1 + max(self._depth(node.left), self._depth(node.right)) + + def is_full(self) -> bool: + """ + Returns True if the tree is full + + >>> BinaryTree(Node(1)).is_full() + True + >>> BinaryTree.small_tree().is_full() + True + >>> BinaryTree.medium_tree().is_full() + False + """ + return self.root.is_full() if __name__ == "__main__": - main() + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py index fc512944eb50..3f214d0113a4 100644 --- a/data_structures/binary_tree/binary_search_tree.py +++ b/data_structures/binary_tree/binary_search_tree.py @@ -1,17 +1,118 @@ -""" +r""" A binary search Tree + +Example + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + +>>> t = BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> print(" ".join(repr(i.value) for i in t.traversal_tree())) +8 3 1 6 4 7 10 14 13 + +>>> tuple(i.value for i in t.traversal_tree(inorder)) +(1, 3, 4, 6, 7, 8, 10, 13, 14) +>>> tuple(t) +(1, 3, 4, 6, 7, 8, 10, 13, 14) +>>> t.find_kth_smallest(3, t.root) +4 +>>> tuple(t)[3-1] +4 + +>>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) +1 4 7 6 3 13 14 10 8 +>>> t.remove(20) +Traceback (most recent call last): + ... +ValueError: Value 20 not found +>>> BinarySearchTree().search(6) +Traceback (most recent call last): + ... +IndexError: Warning: Tree is empty! please use another. + +Other example: + +>>> testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> t = BinarySearchTree() +>>> for i in testlist: +... t.insert(i) # doctest: +ELLIPSIS +BinarySearchTree(root=8) +BinarySearchTree(root={'8': (3, None)}) +BinarySearchTree(root={'8': ({'3': (None, 6)}, None)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, None)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, 10)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, 14)})}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, {'14': (13, None)})})}) +BinarySearchTree(root={'8': ({'3': (1, {'6': (4, None)})}, {'10': (None, {'14': ... +BinarySearchTree(root={'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, ... + +Prints all the elements of the list in order traversal +>>> print(t) +{'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, None)})})} + +Test existence +>>> t.search(6) is not None +True +>>> 6 in t +True +>>> t.search(-1) is not None +False +>>> -1 in t +False + +>>> t.search(6).is_right +True +>>> t.search(1).is_right +False + +>>> t.get_max().value +14 +>>> max(t) +14 +>>> t.get_min().value +1 +>>> min(t) +1 +>>> t.empty() +False +>>> not t +False +>>> for i in testlist: +... t.remove(i) +>>> t.empty() +True +>>> not t +True """ -from collections.abc import Iterable -from typing import Any +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass +from typing import Any, Self +@dataclass class Node: - def __init__(self, value: int | None = None): - self.value = value - self.parent: Node | None = None # Added in order to delete a node easier - self.left: Node | None = None - self.right: Node | None = None + value: int + left: Node | None = None + right: Node | None = None + parent: Node | None = None # Added in order to delete a node easier + + def __iter__(self) -> Iterator[int]: + """ + >>> list(Node(0)) + [0] + >>> list(Node(0, Node(-1), Node(1), None)) + [-1, 0, 1] + """ + yield from self.left or [] + yield self.value + yield from self.right or [] def __repr__(self) -> str: from pprint import pformat @@ -20,10 +121,20 @@ def __repr__(self) -> str: return str(self.value) return pformat({f"{self.value}": (self.left, self.right)}, indent=1) + @property + def is_right(self) -> bool: + return bool(self.parent and self is self.parent.right) + +@dataclass class BinarySearchTree: - def __init__(self, root: Node | None = None): - self.root = root + root: Node | None = None + + def __bool__(self) -> bool: + return bool(self.root) + + def __iter__(self) -> Iterator[int]: + yield from self.root or [] def __str__(self) -> str: """ @@ -35,20 +146,26 @@ def __reassign_nodes(self, node: Node, new_children: Node | None) -> None: if new_children is not None: # reset its kids new_children.parent = node.parent if node.parent is not None: # reset its parent - if self.is_right(node): # If it is the right children + if node.is_right: # If it is the right child node.parent.right = new_children else: node.parent.left = new_children else: - self.root = None - - def is_right(self, node: Node) -> bool: - if node.parent and node.parent.right: - return node == node.parent.right - return False + self.root = new_children def empty(self) -> bool: - return self.root is None + """ + Returns True if the tree does not have any element(s). + False if the tree has element(s). + + >>> BinarySearchTree().empty() + True + >>> BinarySearchTree().insert(1).empty() + False + >>> BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7).empty() + False + """ + return not self.root def __insert(self, value) -> None: """ @@ -60,7 +177,7 @@ def __insert(self, value) -> None: else: # Tree is not empty parent_node = self.root # from root if parent_node is None: - return None + return while True: # While we don't get to a leaf if value < parent_node.value: # We go left if parent_node.left is None: @@ -68,19 +185,43 @@ def __insert(self, value) -> None: break else: parent_node = parent_node.left + elif parent_node.right is None: + parent_node.right = new_node + break else: - if parent_node.right is None: - parent_node.right = new_node - break - else: - parent_node = parent_node.right + parent_node = parent_node.right new_node.parent = parent_node - def insert(self, *values) -> None: + def insert(self, *values) -> Self: for value in values: self.__insert(value) + return self def search(self, value) -> Node | None: + """ + >>> tree = BinarySearchTree().insert(10, 20, 30, 40, 50) + >>> tree.search(10) + {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})} + >>> tree.search(20) + {'20': (None, {'30': (None, {'40': (None, 50)})})} + >>> tree.search(30) + {'30': (None, {'40': (None, 50)})} + >>> tree.search(40) + {'40': (None, 50)} + >>> tree.search(50) + 50 + >>> tree.search(5) is None # element not present + True + >>> tree.search(0) is None # element not present + True + >>> tree.search(-5) is None # element not present + True + >>> BinarySearchTree().search(10) + Traceback (most recent call last): + ... + IndexError: Warning: Tree is empty! please use another. + """ + if self.empty(): raise IndexError("Warning: Tree is empty! please use another.") else: @@ -93,6 +234,15 @@ def search(self, value) -> Node | None: def get_max(self, node: Node | None = None) -> Node | None: """ We go deep on the right branch + + >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_max() + 50 + >>> BinarySearchTree().insert(-5, -1, 0.1, -0.3, -4.5).get_max() + {'0.1': (-0.3, None)} + >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_max() + {'78.3': ({'30': (1, 74.0)}, None)} + >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_max() + {'783': ({'30': (1, 740)}, None)} """ if node is None: if self.root is None: @@ -107,6 +257,15 @@ def get_max(self, node: Node | None = None) -> Node | None: def get_min(self, node: Node | None = None) -> Node | None: """ We go deep on the left branch + + >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_min() + {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})} + >>> BinarySearchTree().insert(-5, -1, 0, -0.3, -4.5).get_min() + {'-5': (None, {'-1': (-4.5, {'0': (-0.3, None)})})} + >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_min() + {'1': (None, {'78.3': ({'30': (1, 74.0)}, None)})} + >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_min() + {'1': (None, {'783': ({'30': (1, 740)}, None)})} """ if node is None: node = self.root @@ -119,22 +278,26 @@ def get_min(self, node: Node | None = None) -> Node | None: return node def remove(self, value: int) -> None: - node = self.search(value) # Look for the node with that label - if node is not None: - if node.left is None and node.right is None: # If it has no children - self.__reassign_nodes(node, None) - elif node.left is None: # Has only right children - self.__reassign_nodes(node, node.right) - elif node.right is None: # Has only left children - self.__reassign_nodes(node, node.left) - else: - tmp_node = self.get_max( - node.left - ) # Gets the max value of the left branch - self.remove(tmp_node.value) # type: ignore - node.value = ( - tmp_node.value # type: ignore - ) # Assigns the value to the node to delete and keep tree structure + # Look for the node with that label + node = self.search(value) + if node is None: + msg = f"Value {value} not found" + raise ValueError(msg) + + if node.left is None and node.right is None: # If it has no children + self.__reassign_nodes(node, None) + elif node.left is None: # Has only right children + self.__reassign_nodes(node, node.right) + elif node.right is None: # Has only left children + self.__reassign_nodes(node, node.left) + else: + predecessor = self.get_max( + node.left + ) # Gets the max value of the left branch + self.remove(predecessor.value) # type: ignore[union-attr] + node.value = ( + predecessor.value # type: ignore[union-attr] + ) # Assigns the value to the node to delete and keep tree structure def preorder_traverse(self, node: Node | None) -> Iterable: if node is not None: @@ -167,63 +330,24 @@ def find_kth_smallest(self, k: int, node: Node) -> int: return arr[k - 1] -def postorder(curr_node: Node | None) -> list[Node]: +def inorder(curr_node: Node | None) -> list[Node]: """ - postOrder (left, right, self) + inorder (left, self, right) """ node_list = [] if curr_node is not None: - node_list = postorder(curr_node.left) + postorder(curr_node.right) + [curr_node] + node_list = [*inorder(curr_node.left), curr_node, *inorder(curr_node.right)] return node_list -def binary_search_tree() -> None: - r""" - Example - 8 - / \ - 3 10 - / \ \ - 1 6 14 - / \ / - 4 7 13 - - >>> t = BinarySearchTree() - >>> t.insert(8, 3, 6, 1, 10, 14, 13, 4, 7) - >>> print(" ".join(repr(i.value) for i in t.traversal_tree())) - 8 3 1 6 4 7 10 14 13 - >>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) - 1 4 7 6 3 13 14 10 8 - >>> BinarySearchTree().search(6) - Traceback (most recent call last): - ... - IndexError: Warning: Tree is empty! please use another. +def postorder(curr_node: Node | None) -> list[Node]: + """ + postOrder (left, right, self) """ - testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) - t = BinarySearchTree() - for i in testlist: - t.insert(i) - - # Prints all the elements of the list in order traversal - print(t) - - if t.search(6) is not None: - print("The value 6 exists") - else: - print("The value 6 doesn't exist") - - if t.search(-1) is not None: - print("The value -1 exists") - else: - print("The value -1 doesn't exist") - - if not t.empty(): - print("Max Value: ", t.get_max().value) # type: ignore - print("Min Value: ", t.get_min().value) # type: ignore - - for i in testlist: - t.remove(i) - print(t) + node_list = [] + if curr_node is not None: + node_list = postorder(curr_node.left) + postorder(curr_node.right) + [curr_node] + return node_list if __name__ == "__main__": diff --git a/data_structures/binary_tree/binary_search_tree_recursive.py b/data_structures/binary_tree/binary_search_tree_recursive.py index 97eb8e25bedd..d94ac5253360 100644 --- a/data_structures/binary_tree/binary_search_tree_recursive.py +++ b/data_structures/binary_tree/binary_search_tree_recursive.py @@ -7,11 +7,14 @@ To run an example: python binary_search_tree_recursive.py """ + from __future__ import annotations import unittest from collections.abc import Iterator +import pytest + class Node: def __init__(self, label: int, parent: Node | None) -> None: @@ -71,13 +74,13 @@ def put(self, label: int) -> None: def _put(self, node: Node | None, label: int, parent: Node | None = None) -> Node: if node is None: node = Node(label, parent) + elif label < node.label: + node.left = self._put(node.left, label, node) + elif label > node.label: + node.right = self._put(node.right, label, node) else: - if label < node.label: - node.left = self._put(node.left, label, node) - elif label > node.label: - node.right = self._put(node.right, label, node) - else: - raise Exception(f"Node with label {label} already exists") + msg = f"Node with label {label} already exists" + raise ValueError(msg) return node @@ -94,18 +97,18 @@ def search(self, label: int) -> Node: >>> node = t.search(3) Traceback (most recent call last): ... - Exception: Node with label 3 does not exist + ValueError: Node with label 3 does not exist """ return self._search(self.root, label) def _search(self, node: Node | None, label: int) -> Node: if node is None: - raise Exception(f"Node with label {label} does not exist") - else: - if label < node.label: - node = self._search(node.left, label) - elif label > node.label: - node = self._search(node.right, label) + msg = f"Node with label {label} does not exist" + raise ValueError(msg) + elif label < node.label: + node = self._search(node.left, label) + elif label > node.label: + node = self._search(node.right, label) return node @@ -122,7 +125,7 @@ def remove(self, label: int) -> None: >>> t.remove(3) Traceback (most recent call last): ... - Exception: Node with label 3 does not exist + ValueError: Node with label 3 does not exist """ node = self.search(label) if node.right and node.left: @@ -177,7 +180,7 @@ def exists(self, label: int) -> bool: try: self.search(label) return True - except Exception: + except ValueError: return False def get_max_label(self) -> int: @@ -188,7 +191,7 @@ def get_max_label(self) -> int: >>> t.get_max_label() Traceback (most recent call last): ... - Exception: Binary search tree is empty + ValueError: Binary search tree is empty >>> t.put(8) >>> t.put(10) @@ -196,7 +199,7 @@ def get_max_label(self) -> int: 10 """ if self.root is None: - raise Exception("Binary search tree is empty") + raise ValueError("Binary search tree is empty") node = self.root while node.right is not None: @@ -212,7 +215,7 @@ def get_min_label(self) -> int: >>> t.get_min_label() Traceback (most recent call last): ... - Exception: Binary search tree is empty + ValueError: Binary search tree is empty >>> t.put(8) >>> t.put(10) @@ -220,7 +223,7 @@ def get_min_label(self) -> int: 8 """ if self.root is None: - raise Exception("Binary search tree is empty") + raise ValueError("Binary search tree is empty") node = self.root while node.left is not None: @@ -357,7 +360,7 @@ def test_put(self) -> None: assert t.root.left.left.parent == t.root.left assert t.root.left.left.label == 1 - with self.assertRaises(Exception): # noqa: B017 + with pytest.raises(ValueError): t.put(1) def test_search(self) -> None: @@ -369,7 +372,7 @@ def test_search(self) -> None: node = t.search(13) assert node.label == 13 - with self.assertRaises(Exception): # noqa: B017 + with pytest.raises(ValueError): t.search(2) def test_remove(self) -> None: @@ -515,7 +518,7 @@ def test_get_max_label(self) -> None: assert t.get_max_label() == 14 t.empty() - with self.assertRaises(Exception): # noqa: B017 + with pytest.raises(ValueError): t.get_max_label() def test_get_min_label(self) -> None: @@ -524,7 +527,7 @@ def test_get_min_label(self) -> None: assert t.get_min_label() == 1 t.empty() - with self.assertRaises(Exception): # noqa: B017 + with pytest.raises(ValueError): t.get_min_label() def test_inorder_traversal(self) -> None: diff --git a/data_structures/binary_tree/binary_tree_mirror.py b/data_structures/binary_tree/binary_tree_mirror.py index 1ef950ad62d7..b8548f4ec515 100644 --- a/data_structures/binary_tree/binary_tree_mirror.py +++ b/data_structures/binary_tree/binary_tree_mirror.py @@ -31,7 +31,8 @@ def binary_tree_mirror(binary_tree: dict, root: int = 1) -> dict: if not binary_tree: raise ValueError("binary tree cannot be empty") if root not in binary_tree: - raise ValueError(f"root {root} is not present in the binary_tree") + msg = f"root {root} is not present in the binary_tree" + raise ValueError(msg) binary_tree_mirror_dictionary = dict(binary_tree) binary_tree_mirror_dict(binary_tree_mirror_dictionary, root) return binary_tree_mirror_dictionary diff --git a/data_structures/binary_tree/binary_tree_node_sum.py b/data_structures/binary_tree/binary_tree_node_sum.py index 5a13e74e3c9f..066617b616c4 100644 --- a/data_structures/binary_tree/binary_tree_node_sum.py +++ b/data_structures/binary_tree/binary_tree_node_sum.py @@ -8,7 +8,6 @@ frames that could be in memory is `n` """ - from __future__ import annotations from collections.abc import Iterator diff --git a/data_structures/binary_tree/binary_tree_traversals.py b/data_structures/binary_tree/binary_tree_traversals.py index 24dd1bd8cdc8..5ba149d0cbc6 100644 --- a/data_structures/binary_tree/binary_tree_traversals.py +++ b/data_structures/binary_tree/binary_tree_traversals.py @@ -1,12 +1,11 @@ -# https://en.wikipedia.org/wiki/Tree_traversal from __future__ import annotations from collections import deque -from collections.abc import Sequence +from collections.abc import Generator from dataclasses import dataclass -from typing import Any +# https://en.wikipedia.org/wiki/Tree_traversal @dataclass class Node: data: int @@ -31,31 +30,56 @@ def make_tree() -> Node | None: return tree -def preorder(root: Node | None) -> list[int]: +def preorder(root: Node | None) -> Generator[int]: """ Pre-order traversal visits root node, left subtree, right subtree. - >>> preorder(make_tree()) + >>> list(preorder(make_tree())) [1, 2, 4, 5, 3] """ - return [root.data] + preorder(root.left) + preorder(root.right) if root else [] + if not root: + return + yield root.data + yield from preorder(root.left) + yield from preorder(root.right) -def postorder(root: Node | None) -> list[int]: +def postorder(root: Node | None) -> Generator[int]: """ Post-order traversal visits left subtree, right subtree, root node. - >>> postorder(make_tree()) + >>> list(postorder(make_tree())) [4, 5, 2, 3, 1] """ - return postorder(root.left) + postorder(root.right) + [root.data] if root else [] + if not root: + return + yield from postorder(root.left) + yield from postorder(root.right) + yield root.data -def inorder(root: Node | None) -> list[int]: +def inorder(root: Node | None) -> Generator[int]: """ In-order traversal visits left subtree, root node, right subtree. - >>> inorder(make_tree()) + >>> list(inorder(make_tree())) [4, 2, 5, 1, 3] """ - return inorder(root.left) + [root.data] + inorder(root.right) if root else [] + if not root: + return + yield from inorder(root.left) + yield root.data + yield from inorder(root.right) + + +def reverse_inorder(root: Node | None) -> Generator[int]: + """ + Reverse in-order traversal visits right subtree, root node, left subtree. + >>> list(reverse_inorder(make_tree())) + [3, 1, 5, 2, 4] + """ + if not root: + return + yield from reverse_inorder(root.right) + yield root.data + yield from reverse_inorder(root.left) def height(root: Node | None) -> int: @@ -69,122 +93,117 @@ def height(root: Node | None) -> int: return (max(height(root.left), height(root.right)) + 1) if root else 0 -def level_order(root: Node | None) -> Sequence[Node | None]: +def level_order(root: Node | None) -> Generator[int]: """ Returns a list of nodes value from a whole binary tree in Level Order Traverse. Level Order traverse: Visit nodes of the tree level-by-level. + >>> list(level_order(make_tree())) + [1, 2, 3, 4, 5] """ - output: list[Any] = [] if root is None: - return output + return process_queue = deque([root]) while process_queue: node = process_queue.popleft() - output.append(node.data) + yield node.data if node.left: process_queue.append(node.left) if node.right: process_queue.append(node.right) - return output -def get_nodes_from_left_to_right( - root: Node | None, level: int -) -> Sequence[Node | None]: +def get_nodes_from_left_to_right(root: Node | None, level: int) -> Generator[int]: """ Returns a list of nodes value from a particular level: Left to right direction of the binary tree. + >>> list(get_nodes_from_left_to_right(make_tree(), 1)) + [1] + >>> list(get_nodes_from_left_to_right(make_tree(), 2)) + [2, 3] """ - output: list[Any] = [] - def populate_output(root: Node | None, level: int) -> None: + def populate_output(root: Node | None, level: int) -> Generator[int]: if not root: return if level == 1: - output.append(root.data) + yield root.data elif level > 1: - populate_output(root.left, level - 1) - populate_output(root.right, level - 1) + yield from populate_output(root.left, level - 1) + yield from populate_output(root.right, level - 1) - populate_output(root, level) - return output + yield from populate_output(root, level) -def get_nodes_from_right_to_left( - root: Node | None, level: int -) -> Sequence[Node | None]: +def get_nodes_from_right_to_left(root: Node | None, level: int) -> Generator[int]: """ Returns a list of nodes value from a particular level: Right to left direction of the binary tree. + >>> list(get_nodes_from_right_to_left(make_tree(), 1)) + [1] + >>> list(get_nodes_from_right_to_left(make_tree(), 2)) + [3, 2] """ - output: list[Any] = [] - def populate_output(root: Node | None, level: int) -> None: - if root is None: + def populate_output(root: Node | None, level: int) -> Generator[int]: + if not root: return if level == 1: - output.append(root.data) + yield root.data elif level > 1: - populate_output(root.right, level - 1) - populate_output(root.left, level - 1) + yield from populate_output(root.right, level - 1) + yield from populate_output(root.left, level - 1) - populate_output(root, level) - return output + yield from populate_output(root, level) -def zigzag(root: Node | None) -> Sequence[Node | None] | list[Any]: +def zigzag(root: Node | None) -> Generator[int]: """ ZigZag traverse: Returns a list of nodes value from left to right and right to left, alternatively. + >>> list(zigzag(make_tree())) + [1, 3, 2, 4, 5] """ if root is None: - return [] - - output: list[Sequence[Node | None]] = [] + return flag = 0 height_tree = height(root) for h in range(1, height_tree + 1): if not flag: - output.append(get_nodes_from_left_to_right(root, h)) + yield from get_nodes_from_left_to_right(root, h) flag = 1 else: - output.append(get_nodes_from_right_to_left(root, h)) + yield from get_nodes_from_right_to_left(root, h) flag = 0 - return output - def main() -> None: # Main function for testing. - """ - Create binary tree. - """ + # Create binary tree. root = make_tree() - """ - All Traversals of the binary are as follows: - """ - print(f"In-order Traversal: {inorder(root)}") - print(f"Pre-order Traversal: {preorder(root)}") - print(f"Post-order Traversal: {postorder(root)}", "\n") + # All Traversals of the binary are as follows: + print(f"In-order Traversal: {list(inorder(root))}") + print(f"Reverse In-order Traversal: {list(reverse_inorder(root))}") + print(f"Pre-order Traversal: {list(preorder(root))}") + print(f"Post-order Traversal: {list(postorder(root))}", "\n") print(f"Height of Tree: {height(root)}", "\n") print("Complete Level Order Traversal: ") - print(level_order(root), "\n") + print(f"{list(level_order(root))} \n") print("Level-wise order Traversal: ") for level in range(1, height(root) + 1): - print(f"Level {level}:", get_nodes_from_left_to_right(root, level=level)) + print(f"Level {level}:", list(get_nodes_from_left_to_right(root, level=level))) print("\nZigZag order Traversal: ") - print(zigzag(root)) + print(f"{list(zigzag(root))}") if __name__ == "__main__": diff --git a/data_structures/binary_tree/diameter_of_binary_tree.py b/data_structures/binary_tree/diameter_of_binary_tree.py new file mode 100644 index 000000000000..75e5e7373323 --- /dev/null +++ b/data_structures/binary_tree/diameter_of_binary_tree.py @@ -0,0 +1,73 @@ +""" +The diameter/width of a tree is defined as the number of nodes on the longest path +between two end nodes. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + def depth(self) -> int: + """ + >>> root = Node(1) + >>> root.depth() + 1 + >>> root.left = Node(2) + >>> root.depth() + 2 + >>> root.left.depth() + 1 + >>> root.right = Node(3) + >>> root.depth() + 2 + """ + left_depth = self.left.depth() if self.left else 0 + right_depth = self.right.depth() if self.right else 0 + return max(left_depth, right_depth) + 1 + + def diameter(self) -> int: + """ + >>> root = Node(1) + >>> root.diameter() + 1 + >>> root.left = Node(2) + >>> root.diameter() + 2 + >>> root.left.diameter() + 1 + >>> root.right = Node(3) + >>> root.diameter() + 3 + """ + left_depth = self.left.depth() if self.left else 0 + right_depth = self.right.depth() if self.right else 0 + return left_depth + right_depth + 1 + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + r""" + Constructed binary tree is + 1 + / \ + 2 3 + / \ + 4 5 + """ + print(f"{root.diameter() = }") # 4 + print(f"{root.left.diameter() = }") # 3 + print(f"{root.right.diameter() = }") # 1 diff --git a/data_structures/binary_tree/distribute_coins.py b/data_structures/binary_tree/distribute_coins.py index ea02afc2cea6..5712604cb87c 100644 --- a/data_structures/binary_tree/distribute_coins.py +++ b/data_structures/binary_tree/distribute_coins.py @@ -39,8 +39,8 @@ from __future__ import annotations -from collections import namedtuple from dataclasses import dataclass +from typing import NamedTuple @dataclass @@ -50,7 +50,9 @@ class TreeNode: right: TreeNode | None = None -CoinsDistribResult = namedtuple("CoinsDistribResult", "moves excess") +class CoinsDistribResult(NamedTuple): + moves: int + excess: int def distribute_coins(root: TreeNode | None) -> int: @@ -79,7 +81,7 @@ def distribute_coins(root: TreeNode | None) -> int: # Validation def count_nodes(node: TreeNode | None) -> int: """ - >>> count_nodes(None): + >>> count_nodes(None) 0 """ if node is None: @@ -89,7 +91,7 @@ def count_nodes(node: TreeNode | None) -> int: def count_coins(node: TreeNode | None) -> int: """ - >>> count_coins(None): + >>> count_coins(None) 0 """ if node is None: diff --git a/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py new file mode 100644 index 000000000000..9b2c7b9af24b --- /dev/null +++ b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py @@ -0,0 +1,139 @@ +""" +Binary Tree Flattening Algorithm + +This code defines an algorithm to flatten a binary tree into a linked list +represented using the right pointers of the tree nodes. It uses in-place +flattening and demonstrates the flattening process along with a display +function to visualize the flattened linked list. +https://www.geeksforgeeks.org/flatten-a-binary-tree-into-linked-list + +Author: Arunkumar A +Date: 04/09/2023 +""" + +from __future__ import annotations + + +class TreeNode: + """ + A TreeNode has data variable and pointers to TreeNode objects + for its left and right children. + """ + + def __init__(self, data: int) -> None: + self.data = data + self.left: TreeNode | None = None + self.right: TreeNode | None = None + + +def build_tree() -> TreeNode: + """ + Build and return a sample binary tree. + + Returns: + TreeNode: The root of the binary tree. + + Examples: + >>> root = build_tree() + >>> root.data + 1 + >>> root.left.data + 2 + >>> root.right.data + 5 + >>> root.left.left.data + 3 + >>> root.left.right.data + 4 + >>> root.right.right.data + 6 + """ + root = TreeNode(1) + root.left = TreeNode(2) + root.right = TreeNode(5) + root.left.left = TreeNode(3) + root.left.right = TreeNode(4) + root.right.right = TreeNode(6) + return root + + +def flatten(root: TreeNode | None) -> None: + """ + Flatten a binary tree into a linked list in-place, where the linked list is + represented using the right pointers of the tree nodes. + + Args: + root (TreeNode): The root of the binary tree to be flattened. + + Examples: + >>> root = TreeNode(1) + >>> root.left = TreeNode(2) + >>> root.right = TreeNode(5) + >>> root.left.left = TreeNode(3) + >>> root.left.right = TreeNode(4) + >>> root.right.right = TreeNode(6) + >>> flatten(root) + >>> root.data + 1 + >>> root.right.right is None + False + >>> root.right.right = TreeNode(3) + >>> root.right.right.right is None + True + """ + if not root: + return + + # Flatten the left subtree + flatten(root.left) + + # Save the right subtree + right_subtree = root.right + + # Make the left subtree the new right subtree + root.right = root.left + root.left = None + + # Find the end of the new right subtree + current = root + while current.right: + current = current.right + + # Append the original right subtree to the end + current.right = right_subtree + + # Flatten the updated right subtree + flatten(right_subtree) + + +def display_linked_list(root: TreeNode | None) -> None: + """ + Display the flattened linked list. + + Args: + root (TreeNode | None): The root of the flattened linked list. + + Examples: + >>> root = TreeNode(1) + >>> root.right = TreeNode(2) + >>> root.right.right = TreeNode(3) + >>> display_linked_list(root) + 1 2 3 + >>> root = None + >>> display_linked_list(root) + + """ + current = root + while current: + if current.right is None: + print(current.data, end="") + break + print(current.data, end=" ") + current = current.right + + +if __name__ == "__main__": + print("Flattened Linked List:") + root = build_tree() + flatten(root) + display_linked_list(root) diff --git a/data_structures/binary_tree/floor_and_ceiling.py b/data_structures/binary_tree/floor_and_ceiling.py new file mode 100644 index 000000000000..b464aefad3a2 --- /dev/null +++ b/data_structures/binary_tree/floor_and_ceiling.py @@ -0,0 +1,88 @@ +""" +In a binary search tree (BST): +* The floor of key 'k' is the maximum value that is smaller than or equal to 'k'. +* The ceiling of key 'k' is the minimum value that is greater than or equal to 'k'. + +Reference: +https://bit.ly/46uB0a2 + +Author : Arunkumar +Date : 14th October 2023 +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + key: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.key + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + +def floor_ceiling(root: Node | None, key: int) -> tuple[int | None, int | None]: + """ + Find the floor and ceiling values for a given key in a Binary Search Tree (BST). + + Args: + root: The root of the binary search tree. + key: The key for which to find the floor and ceiling. + + Returns: + A tuple containing the floor and ceiling values, respectively. + + Examples: + >>> root = Node(10) + >>> root.left = Node(5) + >>> root.right = Node(20) + >>> root.left.left = Node(3) + >>> root.left.right = Node(7) + >>> root.right.left = Node(15) + >>> root.right.right = Node(25) + >>> tuple(root) + (3, 5, 7, 10, 15, 20, 25) + >>> floor_ceiling(root, 8) + (7, 10) + >>> floor_ceiling(root, 14) + (10, 15) + >>> floor_ceiling(root, -1) + (None, 3) + >>> floor_ceiling(root, 30) + (25, None) + """ + floor_val = None + ceiling_val = None + + while root: + if root.key == key: + floor_val = root.key + ceiling_val = root.key + break + + if key < root.key: + ceiling_val = root.key + root = root.left + else: + floor_val = root.key + root = root.right + + return floor_val, ceiling_val + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/inorder_tree_traversal_2022.py b/data_structures/binary_tree/inorder_tree_traversal_2022.py index e94ba7013a82..1357527d2953 100644 --- a/data_structures/binary_tree/inorder_tree_traversal_2022.py +++ b/data_structures/binary_tree/inorder_tree_traversal_2022.py @@ -50,7 +50,7 @@ def inorder(node: None | BinaryTreeNode) -> list[int]: # if node is None,return """ if node: inorder_array = inorder(node.left_child) - inorder_array = inorder_array + [node.data] + inorder_array = [*inorder_array, node.data] inorder_array = inorder_array + inorder(node.right_child) else: inorder_array = [] diff --git a/data_structures/binary_tree/is_bst.py b/data_structures/binary_tree/is_bst.py deleted file mode 100644 index 0b2ef8c9ffde..000000000000 --- a/data_structures/binary_tree/is_bst.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Author : Alexander Pantyukhin -Date : November 2, 2022 - -Task: -Given the root of a binary tree, determine if it is a valid binary search -tree (BST). - -A valid binary search tree is defined as follows: - -- The left subtree of a node contains only nodes with keys less than the node's key. -- The right subtree of a node contains only nodes with keys greater than the node's key. -- Both the left and right subtrees must also be binary search trees. - -Implementation notes: -Depth-first search approach. - -leetcode: https://leetcode.com/problems/validate-binary-search-tree/ - -Let n is the number of nodes in tree -Runtime: O(n) -Space: O(1) -""" - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class TreeNode: - data: float - left: TreeNode | None = None - right: TreeNode | None = None - - -def is_binary_search_tree(root: TreeNode | None) -> bool: - """ - >>> is_binary_search_tree(TreeNode(data=2, - ... left=TreeNode(data=1), - ... right=TreeNode(data=3)) - ... ) - True - - >>> is_binary_search_tree(TreeNode(data=0, - ... left=TreeNode(data=-11), - ... right=TreeNode(data=3)) - ... ) - True - - >>> is_binary_search_tree(TreeNode(data=5, - ... left=TreeNode(data=1), - ... right=TreeNode(data=4, left=TreeNode(data=3))) - ... ) - False - - >>> is_binary_search_tree(TreeNode(data='a', - ... left=TreeNode(data=1), - ... right=TreeNode(data=4, left=TreeNode(data=3))) - ... ) - Traceback (most recent call last): - ... - ValueError: Each node should be type of TreeNode and data should be float. - - >>> is_binary_search_tree(TreeNode(data=2, - ... left=TreeNode([]), - ... right=TreeNode(data=4, left=TreeNode(data=3))) - ... ) - Traceback (most recent call last): - ... - ValueError: Each node should be type of TreeNode and data should be float. - """ - - # Validation - def is_valid_tree(node: TreeNode | None) -> bool: - """ - >>> is_valid_tree(None) - True - >>> is_valid_tree('abc') - False - >>> is_valid_tree(TreeNode(data='not a float')) - False - >>> is_valid_tree(TreeNode(data=1, left=TreeNode('123'))) - False - """ - if node is None: - return True - - if not isinstance(node, TreeNode): - return False - - try: - float(node.data) - except (TypeError, ValueError): - return False - - return is_valid_tree(node.left) and is_valid_tree(node.right) - - if not is_valid_tree(root): - raise ValueError( - "Each node should be type of TreeNode and data should be float." - ) - - def is_binary_search_tree_recursive_check( - node: TreeNode | None, left_bound: float, right_bound: float - ) -> bool: - """ - >>> is_binary_search_tree_recursive_check(None) - True - >>> is_binary_search_tree_recursive_check(TreeNode(data=1), 10, 20) - False - """ - - if node is None: - return True - - return ( - left_bound < node.data < right_bound - and is_binary_search_tree_recursive_check(node.left, left_bound, node.data) - and is_binary_search_tree_recursive_check( - node.right, node.data, right_bound - ) - ) - - return is_binary_search_tree_recursive_check(root, -float("inf"), float("inf")) - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/data_structures/binary_tree/is_sorted.py b/data_structures/binary_tree/is_sorted.py new file mode 100644 index 000000000000..91fc8ca82633 --- /dev/null +++ b/data_structures/binary_tree/is_sorted.py @@ -0,0 +1,98 @@ +""" +Given the root of a binary tree, determine if it is a valid binary search tree (BST). + +A valid binary search tree is defined as follows: +- The left subtree of a node contains only nodes with keys less than the node's key. +- The right subtree of a node contains only nodes with keys greater than the node's key. +- Both the left and right subtrees must also be binary search trees. + +In effect, a binary tree is a valid BST if its nodes are sorted in ascending order. +leetcode: https://leetcode.com/problems/validate-binary-search-tree/ + +If n is the number of nodes in the tree then: +Runtime: O(n) +Space: O(1) +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: float + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[float]: + """ + >>> root = Node(data=2.1) + >>> list(root) + [2.1] + >>> root.left=Node(data=2.0) + >>> list(root) + [2.0, 2.1] + >>> root.right=Node(data=2.2) + >>> list(root) + [2.0, 2.1, 2.2] + """ + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + @property + def is_sorted(self) -> bool: + """ + >>> Node(data='abc').is_sorted + True + >>> Node(data=2, + ... left=Node(data=1.999), + ... right=Node(data=3)).is_sorted + True + >>> Node(data=0, + ... left=Node(data=0), + ... right=Node(data=0)).is_sorted + True + >>> Node(data=0, + ... left=Node(data=-11), + ... right=Node(data=3)).is_sorted + True + >>> Node(data=5, + ... left=Node(data=1), + ... right=Node(data=4, left=Node(data=3))).is_sorted + False + >>> Node(data='a', + ... left=Node(data=1), + ... right=Node(data=4, left=Node(data=3))).is_sorted + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + >>> Node(data=2, + ... left=Node([]), + ... right=Node(data=4, left=Node(data=3))).is_sorted + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'int' and 'list' + """ + if self.left and (self.data < self.left.data or not self.left.is_sorted): + return False + return not ( + self.right and (self.data > self.right.data or not self.right.is_sorted) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + tree = Node(data=2.1, left=Node(data=2.0), right=Node(data=2.2)) + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") + assert tree.right + tree.right.data = 2.0 + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") + tree.right.data = 2.1 + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") diff --git a/data_structures/binary_tree/is_sum_tree.py b/data_structures/binary_tree/is_sum_tree.py new file mode 100644 index 000000000000..846bea0fe0f2 --- /dev/null +++ b/data_structures/binary_tree/is_sum_tree.py @@ -0,0 +1,162 @@ +""" +Is a binary tree a sum tree where the value of every non-leaf node is equal to the sum +of the values of its left and right subtrees? +https://www.geeksforgeeks.org/check-if-a-given-binary-tree-is-sumtree +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + """ + >>> root = Node(2) + >>> list(root) + [2] + >>> root.left = Node(1) + >>> tuple(root) + (1, 2) + """ + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + def __len__(self) -> int: + """ + >>> root = Node(2) + >>> len(root) + 1 + >>> root.left = Node(1) + >>> len(root) + 2 + """ + return sum(1 for _ in self) + + @property + def is_sum_node(self) -> bool: + """ + >>> root = Node(3) + >>> root.is_sum_node + True + >>> root.left = Node(1) + >>> root.is_sum_node + False + >>> root.right = Node(2) + >>> root.is_sum_node + True + """ + if not self.left and not self.right: + return True # leaf nodes are considered sum nodes + left_sum = sum(self.left) if self.left else 0 + right_sum = sum(self.right) if self.right else 0 + return all( + ( + self.data == left_sum + right_sum, + self.left.is_sum_node if self.left else True, + self.right.is_sum_node if self.right else True, + ) + ) + + +@dataclass +class BinaryTree: + root: Node + + def __iter__(self) -> Iterator[int]: + """ + >>> list(BinaryTree.build_a_tree()) + [1, 2, 7, 11, 15, 29, 35, 40] + """ + return iter(self.root) + + def __len__(self) -> int: + """ + >>> len(BinaryTree.build_a_tree()) + 8 + """ + return len(self.root) + + def __str__(self) -> str: + """ + Returns a string representation of the inorder traversal of the binary tree. + + >>> str(list(BinaryTree.build_a_tree())) + '[1, 2, 7, 11, 15, 29, 35, 40]' + """ + return str(list(self)) + + @property + def is_sum_tree(self) -> bool: + """ + >>> BinaryTree.build_a_tree().is_sum_tree + False + >>> BinaryTree.build_a_sum_tree().is_sum_tree + True + """ + return self.root.is_sum_node + + @classmethod + def build_a_tree(cls) -> BinaryTree: + r""" + Create a binary tree with the specified structure: + 11 + / \ + 2 29 + / \ / \ + 1 7 15 40 + \ + 35 + >>> list(BinaryTree.build_a_tree()) + [1, 2, 7, 11, 15, 29, 35, 40] + """ + tree = BinaryTree(Node(11)) + root = tree.root + root.left = Node(2) + root.right = Node(29) + root.left.left = Node(1) + root.left.right = Node(7) + root.right.left = Node(15) + root.right.right = Node(40) + root.right.right.left = Node(35) + return tree + + @classmethod + def build_a_sum_tree(cls) -> BinaryTree: + r""" + Create a binary tree with the specified structure: + 26 + / \ + 10 3 + / \ \ + 4 6 3 + >>> list(BinaryTree.build_a_sum_tree()) + [4, 10, 6, 26, 3, 3] + """ + tree = BinaryTree(Node(26)) + root = tree.root + root.left = Node(10) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(6) + root.right.right = Node(3) + return tree + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + tree = BinaryTree.build_a_tree() + print(f"{tree} has {len(tree)} nodes and {tree.is_sum_tree = }.") + tree = BinaryTree.build_a_sum_tree() + print(f"{tree} has {len(tree)} nodes and {tree.is_sum_tree = }.") diff --git a/data_structures/binary_tree/lazy_segment_tree.py b/data_structures/binary_tree/lazy_segment_tree.py index 050dfe0a6f2f..c26b0619380c 100644 --- a/data_structures/binary_tree/lazy_segment_tree.py +++ b/data_structures/binary_tree/lazy_segment_tree.py @@ -7,10 +7,10 @@ class SegmentTree: def __init__(self, size: int) -> None: self.size = size # approximate the overall size of segment tree with given value - self.segment_tree = [0 for i in range(0, 4 * size)] + self.segment_tree = [0 for i in range(4 * size)] # create array to store lazy update - self.lazy = [0 for i in range(0, 4 * size)] - self.flag = [0 for i in range(0, 4 * size)] # flag for lazy update + self.lazy = [0 for i in range(4 * size)] + self.flag = [0 for i in range(4 * size)] # flag for lazy update def left(self, idx: int) -> int: """ diff --git a/data_structures/binary_tree/maximum_sum_bst.py b/data_structures/binary_tree/maximum_sum_bst.py new file mode 100644 index 000000000000..7dadc7b95920 --- /dev/null +++ b/data_structures/binary_tree/maximum_sum_bst.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import sys +from dataclasses import dataclass + +INT_MIN = -sys.maxsize + 1 +INT_MAX = sys.maxsize - 1 + + +@dataclass +class TreeNode: + val: int = 0 + left: TreeNode | None = None + right: TreeNode | None = None + + +def max_sum_bst(root: TreeNode | None) -> int: + """ + The solution traverses a binary tree to find the maximum sum of + keys in any subtree that is a Binary Search Tree (BST). It uses + recursion to validate BST properties and calculates sums, returning + the highest sum found among all valid BST subtrees. + + >>> t1 = TreeNode(4) + >>> t1.left = TreeNode(3) + >>> t1.left.left = TreeNode(1) + >>> t1.left.right = TreeNode(2) + >>> print(max_sum_bst(t1)) + 2 + >>> t2 = TreeNode(-4) + >>> t2.left = TreeNode(-2) + >>> t2.right = TreeNode(-5) + >>> print(max_sum_bst(t2)) + 0 + >>> t3 = TreeNode(1) + >>> t3.left = TreeNode(4) + >>> t3.left.left = TreeNode(2) + >>> t3.left.right = TreeNode(4) + >>> t3.right = TreeNode(3) + >>> t3.right.left = TreeNode(2) + >>> t3.right.right = TreeNode(5) + >>> t3.right.right.left = TreeNode(4) + >>> t3.right.right.right = TreeNode(6) + >>> print(max_sum_bst(t3)) + 20 + """ + ans: int = 0 + + def solver(node: TreeNode | None) -> tuple[bool, int, int, int]: + """ + Returns the maximum sum by making recursive calls + >>> t1 = TreeNode(1) + >>> print(solver(t1)) + 1 + """ + nonlocal ans + + if not node: + return True, INT_MAX, INT_MIN, 0 # Valid BST, min, max, sum + + is_left_valid, min_left, max_left, sum_left = solver(node.left) + is_right_valid, min_right, max_right, sum_right = solver(node.right) + + if is_left_valid and is_right_valid and max_left < node.val < min_right: + total_sum = sum_left + sum_right + node.val + ans = max(ans, total_sum) + return True, min(min_left, node.val), max(max_right, node.val), total_sum + + return False, -1, -1, -1 # Not a valid BST + + solver(root) + return ans + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/merge_two_binary_trees.py b/data_structures/binary_tree/merge_two_binary_trees.py index 3380f8c5fb31..6bbb30428704 100644 --- a/data_structures/binary_tree/merge_two_binary_trees.py +++ b/data_structures/binary_tree/merge_two_binary_trees.py @@ -5,6 +5,7 @@ both nodes to the new value of the merged node. Otherwise, the NOT null node will be used as the node of new tree. """ + from __future__ import annotations diff --git a/data_structures/binary_tree/mirror_binary_tree.py b/data_structures/binary_tree/mirror_binary_tree.py new file mode 100644 index 000000000000..f6611d66d676 --- /dev/null +++ b/data_structures/binary_tree/mirror_binary_tree.py @@ -0,0 +1,160 @@ +""" +Given the root of a binary tree, mirror the tree, and return its root. + +Leetcode problem reference: https://leetcode.com/problems/mirror-binary-tree/ +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + """ + A Node has value variable and pointers to Nodes to its left and right. + """ + + value: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.value + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + def mirror(self) -> Node: + """ + Mirror the binary tree rooted at this node by swapping left and right children. + + >>> tree = Node(0) + >>> list(tree) + [0] + >>> list(tree.mirror()) + [0] + >>> tree = Node(1, Node(0), Node(3, Node(2), Node(4, None, Node(5)))) + >>> tuple(tree) + (0, 1, 2, 3, 4, 5) + >>> tuple(tree.mirror()) + (5, 4, 3, 2, 1, 0) + """ + self.left, self.right = self.right, self.left + if self.left: + self.left.mirror() + if self.right: + self.right.mirror() + return self + + +def make_tree_seven() -> Node: + r""" + Return a binary tree with 7 nodes that looks like this: + :: + + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 + + >>> tree_seven = make_tree_seven() + >>> len(tree_seven) + 7 + >>> list(tree_seven) + [4, 2, 5, 1, 6, 3, 7] + """ + tree = Node(1) + tree.left = Node(2) + tree.right = Node(3) + tree.left.left = Node(4) + tree.left.right = Node(5) + tree.right.left = Node(6) + tree.right.right = Node(7) + return tree + + +def make_tree_nine() -> Node: + r""" + Return a binary tree with 9 nodes that looks like this: + :: + + 1 + / \ + 2 3 + / \ \ + 4 5 6 + / \ \ + 7 8 9 + + >>> tree_nine = make_tree_nine() + >>> len(tree_nine) + 9 + >>> list(tree_nine) + [7, 4, 8, 2, 5, 9, 1, 3, 6] + """ + tree = Node(1) + tree.left = Node(2) + tree.right = Node(3) + tree.left.left = Node(4) + tree.left.right = Node(5) + tree.right.right = Node(6) + tree.left.left.left = Node(7) + tree.left.left.right = Node(8) + tree.left.right.right = Node(9) + return tree + + +def main() -> None: + r""" + Mirror binary trees with the given root and returns the root + + >>> tree = make_tree_nine() + >>> tuple(tree) + (7, 4, 8, 2, 5, 9, 1, 3, 6) + >>> tuple(tree.mirror()) + (6, 3, 1, 9, 5, 2, 8, 4, 7) + + nine_tree:: + + 1 + / \ + 2 3 + / \ \ + 4 5 6 + / \ \ + 7 8 9 + + The mirrored tree looks like this:: + + 1 + / \ + 3 2 + / / \ + 6 5 4 + / / \ + 9 8 7 + """ + trees = {"zero": Node(0), "seven": make_tree_seven(), "nine": make_tree_nine()} + for name, tree in trees.items(): + print(f" The {name} tree: {tuple(tree)}") + # (0,) + # (4, 2, 5, 1, 6, 3, 7) + # (7, 4, 8, 2, 5, 9, 1, 3, 6) + print(f"Mirror of {name} tree: {tuple(tree.mirror())}") + # (0,) + # (7, 3, 6, 1, 5, 2, 4) + # (6, 3, 1, 9, 5, 2, 8, 4, 7) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/binary_tree/non_recursive_segment_tree.py b/data_structures/binary_tree/non_recursive_segment_tree.py index 04164e5cba4e..ca0d5c111c4f 100644 --- a/data_structures/binary_tree/non_recursive_segment_tree.py +++ b/data_structures/binary_tree/non_recursive_segment_tree.py @@ -35,6 +35,7 @@ >>> st.query(0, 2) [1, 2, 3] """ + from __future__ import annotations from collections.abc import Callable @@ -86,12 +87,12 @@ def update(self, p: int, v: T) -> None: p = p // 2 self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1]) - def query(self, l: int, r: int) -> T | None: # noqa: E741 + def query(self, left: int, right: int) -> T | None: """ Get range query value in log(N) time - :param l: left element index - :param r: right element index - :return: element combined in the range [l, r] + :param left: left element index + :param right: right element index + :return: element combined in the range [left, right] >>> st = SegmentTree([1, 2, 3, 4], lambda a, b: a + b) >>> st.query(0, 2) @@ -103,15 +104,15 @@ def query(self, l: int, r: int) -> T | None: # noqa: E741 >>> st.query(2, 3) 7 """ - l, r = l + self.N, r + self.N + left, right = left + self.N, right + self.N res: T | None = None - while l <= r: - if l % 2 == 1: - res = self.st[l] if res is None else self.fn(res, self.st[l]) - if r % 2 == 0: - res = self.st[r] if res is None else self.fn(res, self.st[r]) - l, r = (l + 1) // 2, (r - 1) // 2 + while left <= right: + if left % 2 == 1: + res = self.st[left] if res is None else self.fn(res, self.st[left]) + if right % 2 == 0: + res = self.st[right] if res is None else self.fn(res, self.st[right]) + left, right = (left + 1) // 2, (right - 1) // 2 return res diff --git a/data_structures/binary_tree/number_of_possible_binary_trees.py b/data_structures/binary_tree/number_of_possible_binary_trees.py index 684c518b1eb6..b39cbafd0a61 100644 --- a/data_structures/binary_tree/number_of_possible_binary_trees.py +++ b/data_structures/binary_tree/number_of_possible_binary_trees.py @@ -6,6 +6,7 @@ Further details at Wikipedia: https://en.wikipedia.org/wiki/Catalan_number """ + """ Our Contribution: Basically we Create the 2 function: @@ -30,8 +31,7 @@ def binomial_coefficient(n: int, k: int) -> int: """ result = 1 # To kept the Calculated Value # Since C(n, k) = C(n, n-k) - if k > (n - k): - k = n - k + k = min(k, n - k) # Calculate C(n,k) for i in range(k): result *= n - i diff --git a/data_structures/binary_tree/red_black_tree.py b/data_structures/binary_tree/red_black_tree.py index a9dbd699c3c1..752db1e7026c 100644 --- a/data_structures/binary_tree/red_black_tree.py +++ b/data_structures/binary_tree/red_black_tree.py @@ -1,7 +1,3 @@ -""" -python/black : true -flake8 : passed -""" from __future__ import annotations from collections.abc import Iterator @@ -16,7 +12,7 @@ class RedBlackTree: and slower for reading in the average case, though, because they're both balanced binary search trees, both will get the same asymptotic performance. - To read more about them, https://en.wikipedia.org/wiki/Red–black_tree + To read more about them, https://en.wikipedia.org/wiki/Red-black_tree Unless otherwise specified, all asymptotic runtimes are specified in terms of the size of the tree. """ @@ -106,12 +102,11 @@ def insert(self, label: int) -> RedBlackTree: else: self.left = RedBlackTree(label, 1, self) self.left._insert_repair() + elif self.right: + self.right.insert(label) else: - if self.right: - self.right.insert(label) - else: - self.right = RedBlackTree(label, 1, self) - self.right._insert_repair() + self.right = RedBlackTree(label, 1, self) + self.right._insert_repair() return self.parent or self def _insert_repair(self) -> None: @@ -177,36 +172,34 @@ def remove(self, label: int) -> RedBlackTree: self.parent.left = None else: self.parent.right = None - else: - # The node is black - if child is None: - # This node and its child are black - if self.parent is None: - # The tree is now empty - return RedBlackTree(None) - else: - self._remove_repair() - if self.is_left(): - self.parent.left = None - else: - self.parent.right = None - self.parent = None + # The node is black + elif child is None: + # This node and its child are black + if self.parent is None: + # The tree is now empty + return RedBlackTree(None) else: - # This node is black and its child is red - # Move the child node here and make it black - self.label = child.label - self.left = child.left - self.right = child.right - if self.left: - self.left.parent = self - if self.right: - self.right.parent = self + self._remove_repair() + if self.is_left(): + self.parent.left = None + else: + self.parent.right = None + self.parent = None + else: + # This node is black and its child is red + # Move the child node here and make it black + self.label = child.label + self.left = child.left + self.right = child.right + if self.left: + self.left.parent = self + if self.right: + self.right.parent = self elif self.label is not None and self.label > label: if self.left: self.left.remove(label) - else: - if self.right: - self.right.remove(label) + elif self.right: + self.right.remove(label) return self.parent or self def _remove_repair(self) -> None: @@ -319,14 +312,11 @@ def check_coloring(self) -> bool: """A helper function to recursively check Property 4 of a Red-Black Tree. See check_color_properties for more info. """ - if self.color == 1: - if color(self.left) == 1 or color(self.right) == 1: - return False - if self.left and not self.left.check_coloring(): + if self.color == 1 and 1 in (color(self.left), color(self.right)): return False - if self.right and not self.right.check_coloring(): + if self.left and not self.left.check_coloring(): return False - return True + return not (self.right and not self.right.check_coloring()) def black_height(self) -> int | None: """Returns the number of black nodes from this node to the @@ -369,11 +359,10 @@ def search(self, label: int) -> RedBlackTree | None: return None else: return self.right.search(label) + elif self.left is None: + return None else: - if self.left is None: - return None - else: - return self.left.search(label) + return self.left.search(label) def floor(self, label: int) -> int | None: """Returns the largest element in this tree which is at most label. @@ -452,7 +441,7 @@ def is_left(self) -> bool: """Returns true iff this node is the left child of its parent.""" if self.parent is None: return False - return self.parent.left is self.parent.left is self + return self.parent.left is self def is_right(self) -> bool: """Returns true iff this node is the right child of its parent.""" @@ -565,9 +554,7 @@ def test_rotations() -> bool: right_rot.right.right = RedBlackTree(10, parent=right_rot.right) right_rot.right.right.left = RedBlackTree(5, parent=right_rot.right.right) right_rot.right.right.right = RedBlackTree(20, parent=right_rot.right.right) - if tree != right_rot: - return False - return True + return tree == right_rot def test_insertion_speed() -> bool: @@ -610,13 +597,11 @@ def test_insert_and_search() -> bool: tree.insert(12) tree.insert(10) tree.insert(11) - if 5 in tree or -6 in tree or -10 in tree or 13 in tree: + if any(i in tree for i in (5, -6, -10, 13)): # Found something not in there return False - if not (11 in tree and 12 in tree and -8 in tree and 0 in tree): - # Didn't find something in there - return False - return True + # Find all these things in there + return all(i in tree for i in (11, 12, -8, 0)) def test_insert_delete() -> bool: @@ -638,9 +623,7 @@ def test_insert_delete() -> bool: tree = tree.remove(9) if not tree.check_color_properties(): return False - if list(tree.inorder_traverse()) != [-8, 0, 4, 8, 10, 11, 12]: - return False - return True + return list(tree.inorder_traverse()) == [-8, 0, 4, 8, 10, 11, 12] def test_floor_ceil() -> bool: @@ -668,9 +651,7 @@ def test_min_max() -> bool: tree.insert(24) tree.insert(20) tree.insert(22) - if tree.get_max() != 22 or tree.get_min() != -16: - return False - return True + return not (tree.get_max() != 22 or tree.get_min() != -16) def test_tree_traversal() -> bool: @@ -686,9 +667,7 @@ def test_tree_traversal() -> bool: return False if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: return False - if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]: - return False - return True + return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0] def test_tree_chaining() -> bool: @@ -699,9 +678,7 @@ def test_tree_chaining() -> bool: return False if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: return False - if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]: - return False - return True + return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0] def print_results(msg: str, passes: bool) -> None: diff --git a/data_structures/binary_tree/segment_tree.py b/data_structures/binary_tree/segment_tree.py index b0580386954a..084fcf84955d 100644 --- a/data_structures/binary_tree/segment_tree.py +++ b/data_structures/binary_tree/segment_tree.py @@ -3,64 +3,102 @@ class SegmentTree: def __init__(self, a): - self.N = len(a) + self.A = a + self.N = len(self.A) self.st = [0] * ( 4 * self.N ) # approximate the overall size of segment tree with array N - self.build(1, 0, self.N - 1) + if self.N: + self.build(1, 0, self.N - 1) def left(self, idx): + """ + Returns the left child index for a given index in a binary tree. + + >>> s = SegmentTree([1, 2, 3]) + >>> s.left(1) + 2 + >>> s.left(2) + 4 + """ return idx * 2 def right(self, idx): + """ + Returns the right child index for a given index in a binary tree. + + >>> s = SegmentTree([1, 2, 3]) + >>> s.right(1) + 3 + >>> s.right(2) + 5 + """ return idx * 2 + 1 - def build(self, idx, l, r): # noqa: E741 - if l == r: - self.st[idx] = A[l] + def build(self, idx, left, right): + if left == right: + self.st[idx] = self.A[left] else: - mid = (l + r) // 2 - self.build(self.left(idx), l, mid) - self.build(self.right(idx), mid + 1, r) + mid = (left + right) // 2 + self.build(self.left(idx), left, mid) + self.build(self.right(idx), mid + 1, right) self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) def update(self, a, b, val): + """ + Update the values in the segment tree in the range [a,b] with the given value. + + >>> s = SegmentTree([1, 2, 3, 4, 5]) + >>> s.update(2, 4, 10) + True + >>> s.query(1, 5) + 10 + """ return self.update_recursive(1, 0, self.N - 1, a - 1, b - 1, val) - def update_recursive(self, idx, l, r, a, b, val): # noqa: E741 + def update_recursive(self, idx, left, right, a, b, val): """ update(1, 1, N, a, b, v) for update val v to [a,b] """ - if r < a or l > b: + if right < a or left > b: return True - if l == r: + if left == right: self.st[idx] = val return True - mid = (l + r) // 2 - self.update_recursive(self.left(idx), l, mid, a, b, val) - self.update_recursive(self.right(idx), mid + 1, r, a, b, val) + mid = (left + right) // 2 + self.update_recursive(self.left(idx), left, mid, a, b, val) + self.update_recursive(self.right(idx), mid + 1, right, a, b, val) self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) return True def query(self, a, b): + """ + Query the maximum value in the range [a,b]. + + >>> s = SegmentTree([1, 2, 3, 4, 5]) + >>> s.query(1, 3) + 3 + >>> s.query(1, 5) + 5 + """ return self.query_recursive(1, 0, self.N - 1, a - 1, b - 1) - def query_recursive(self, idx, l, r, a, b): # noqa: E741 + def query_recursive(self, idx, left, right, a, b): """ query(1, 1, N, a, b) for query max of [a,b] """ - if r < a or l > b: + if right < a or left > b: return -math.inf - if l >= a and r <= b: + if left >= a and right <= b: return self.st[idx] - mid = (l + r) // 2 - q1 = self.query_recursive(self.left(idx), l, mid, a, b) - q2 = self.query_recursive(self.right(idx), mid + 1, r, a, b) + mid = (left + right) // 2 + q1 = self.query_recursive(self.left(idx), left, mid, a, b) + q2 = self.query_recursive(self.right(idx), mid + 1, right, a, b) return max(q1, q2) def show_data(self): show_list = [] - for i in range(1, N + 1): + for i in range(1, self.N + 1): show_list += [self.query(i, i)] print(show_list) diff --git a/data_structures/binary_tree/segment_tree_other.py b/data_structures/binary_tree/segment_tree_other.py index cc77c4951f1a..95f21ddd4777 100644 --- a/data_structures/binary_tree/segment_tree_other.py +++ b/data_structures/binary_tree/segment_tree_other.py @@ -3,6 +3,7 @@ allowing queries to be done later in log(N) time function takes 2 values and returns a same type value """ + from collections.abc import Sequence from queue import Queue diff --git a/data_structures/binary_tree/serialize_deserialize_binary_tree.py b/data_structures/binary_tree/serialize_deserialize_binary_tree.py new file mode 100644 index 000000000000..7d3e0c61f96d --- /dev/null +++ b/data_structures/binary_tree/serialize_deserialize_binary_tree.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class TreeNode: + """ + A binary tree node has a value, left child, and right child. + + Props: + value: The value of the node. + left: The left child of the node. + right: The right child of the node. + """ + + value: int = 0 + left: TreeNode | None = None + right: TreeNode | None = None + + def __post_init__(self): + if not isinstance(self.value, int): + raise TypeError("Value must be an integer.") + + def __iter__(self) -> Iterator[TreeNode]: + """ + Iterate through the tree in preorder. + + Returns: + An iterator of the tree nodes. + + >>> list(TreeNode(1)) + [1,null,null] + >>> tuple(TreeNode(1, TreeNode(2), TreeNode(3))) + (1,2,null,null,3,null,null, 2,null,null, 3,null,null) + """ + yield self + yield from self.left or () + yield from self.right or () + + def __len__(self) -> int: + """ + Count the number of nodes in the tree. + + Returns: + The number of nodes in the tree. + + >>> len(TreeNode(1)) + 1 + >>> len(TreeNode(1, TreeNode(2), TreeNode(3))) + 3 + """ + return sum(1 for _ in self) + + def __repr__(self) -> str: + """ + Represent the tree as a string. + + Returns: + A string representation of the tree. + + >>> repr(TreeNode(1)) + '1,null,null' + >>> repr(TreeNode(1, TreeNode(2), TreeNode(3))) + '1,2,null,null,3,null,null' + >>> repr(TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))) + '1,2,null,null,3,4,null,null,5,null,null' + """ + return f"{self.value},{self.left!r},{self.right!r}".replace("None", "null") + + @classmethod + def five_tree(cls) -> TreeNode: + """ + >>> repr(TreeNode.five_tree()) + '1,2,null,null,3,4,null,null,5,null,null' + """ + root = TreeNode(1) + root.left = TreeNode(2) + root.right = TreeNode(3) + root.right.left = TreeNode(4) + root.right.right = TreeNode(5) + return root + + +def deserialize(data: str) -> TreeNode | None: + """ + Deserialize a string to a binary tree. + + Args: + data(str): The serialized string. + + Returns: + The root of the binary tree. + + >>> root = TreeNode.five_tree() + >>> serialzed_data = repr(root) + >>> deserialized = deserialize(serialzed_data) + >>> root == deserialized + True + >>> root is deserialized # two separate trees + False + >>> root.right.right.value = 6 + >>> root == deserialized + False + >>> serialzed_data = repr(root) + >>> deserialized = deserialize(serialzed_data) + >>> root == deserialized + True + >>> deserialize("") + Traceback (most recent call last): + ... + ValueError: Data cannot be empty. + """ + + if not data: + raise ValueError("Data cannot be empty.") + + # Split the serialized string by a comma to get node values + nodes = data.split(",") + + def build_tree() -> TreeNode | None: + # Get the next value from the list + value = nodes.pop(0) + + if value == "null": + return None + + node = TreeNode(int(value)) + node.left = build_tree() # Recursively build left subtree + node.right = build_tree() # Recursively build right subtree + return node + + return build_tree() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/symmetric_tree.py b/data_structures/binary_tree/symmetric_tree.py new file mode 100644 index 000000000000..2bfeac98b2c9 --- /dev/null +++ b/data_structures/binary_tree/symmetric_tree.py @@ -0,0 +1,159 @@ +""" +Given the root of a binary tree, check whether it is a mirror of itself +(i.e., symmetric around its center). + +Leetcode reference: https://leetcode.com/problems/symmetric-tree/ +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + """ + A Node represents an element of a binary tree, which contains: + + Attributes: + data: The value stored in the node (int). + left: Pointer to the left child node (Node or None). + right: Pointer to the right child node (Node or None). + + Example: + >>> node = Node(1, Node(2), Node(3)) + >>> node.data + 1 + >>> node.left.data + 2 + >>> node.right.data + 3 + """ + + data: int + left: Node | None = None + right: Node | None = None + + +def make_symmetric_tree() -> Node: + r""" + Create a symmetric tree for testing. + + The tree looks like this: + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + + Returns: + Node: Root node of a symmetric tree. + + Example: + >>> tree = make_symmetric_tree() + >>> tree.data + 1 + >>> tree.left.data == tree.right.data + True + >>> tree.left.left.data == tree.right.right.data + True + """ + root = Node(1) + root.left = Node(2) + root.right = Node(2) + root.left.left = Node(3) + root.left.right = Node(4) + root.right.left = Node(4) + root.right.right = Node(3) + return root + + +def make_asymmetric_tree() -> Node: + r""" + Create an asymmetric tree for testing. + + The tree looks like this: + 1 + / \ + 2 2 + / \ / \ + 3 4 3 4 + + Returns: + Node: Root node of an asymmetric tree. + + Example: + >>> tree = make_asymmetric_tree() + >>> tree.data + 1 + >>> tree.left.data == tree.right.data + True + >>> tree.left.left.data == tree.right.right.data + False + """ + root = Node(1) + root.left = Node(2) + root.right = Node(2) + root.left.left = Node(3) + root.left.right = Node(4) + root.right.left = Node(3) + root.right.right = Node(4) + return root + + +def is_symmetric_tree(tree: Node) -> bool: + """ + Check if a binary tree is symmetric (i.e., a mirror of itself). + + Parameters: + tree: The root node of the binary tree. + + Returns: + bool: True if the tree is symmetric, False otherwise. + + Example: + >>> is_symmetric_tree(make_symmetric_tree()) + True + >>> is_symmetric_tree(make_asymmetric_tree()) + False + """ + if tree: + return is_mirror(tree.left, tree.right) + return True # An empty tree is considered symmetric. + + +def is_mirror(left: Node | None, right: Node | None) -> bool: + """ + Check if two subtrees are mirror images of each other. + + Parameters: + left: The root node of the left subtree. + right: The root node of the right subtree. + + Returns: + bool: True if the two subtrees are mirrors of each other, False otherwise. + + Example: + >>> tree1 = make_symmetric_tree() + >>> is_mirror(tree1.left, tree1.right) + True + >>> tree2 = make_asymmetric_tree() + >>> is_mirror(tree2.left, tree2.right) + False + """ + if left is None and right is None: + # Both sides are empty, which is symmetric. + return True + if left is None or right is None: + # One side is empty while the other is not, which is not symmetric. + return False + if left.data == right.data: + # The values match, so check the subtrees recursively. + return is_mirror(left.left, right.right) and is_mirror(left.right, right.left) + return False + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/binary_tree/treap.py b/data_structures/binary_tree/treap.py index a53ac566ed54..3114c6fa1c26 100644 --- a/data_structures/binary_tree/treap.py +++ b/data_structures/binary_tree/treap.py @@ -39,26 +39,23 @@ def split(root: Node | None, value: int) -> tuple[Node | None, Node | None]: Left tree contains all values less than split value. Right tree contains all values greater or equal, than split value """ - if root is None: # None tree is split into 2 Nones - return None, None - elif root.value is None: + if root is None or root.value is None: # None tree is split into 2 Nones return None, None + elif value < root.value: + """ + Right tree's root will be current node. + Now we split(with the same value) current node's left son + Left tree: left part of that split + Right tree's left son: right part of that split + """ + left, root.left = split(root.left, value) + return left, root else: - if value < root.value: - """ - Right tree's root will be current node. - Now we split(with the same value) current node's left son - Left tree: left part of that split - Right tree's left son: right part of that split - """ - left, root.left = split(root.left, value) - return left, root - else: - """ - Just symmetric to previous case - """ - root.right, right = split(root.right, value) - return root, right + """ + Just symmetric to previous case + """ + root.right, right = split(root.right, value) + return root, right def merge(left: Node | None, right: Node | None) -> Node | None: diff --git a/data_structures/binary_tree/wavelet_tree.py b/data_structures/binary_tree/wavelet_tree.py index 041e140f5b15..2da571e8d326 100644 --- a/data_structures/binary_tree/wavelet_tree.py +++ b/data_structures/binary_tree/wavelet_tree.py @@ -7,6 +7,7 @@ 2. https://www.youtube.com/watch?v=4aSv9PcecDw&t=811s 3. https://www.youtube.com/watch?v=CybAgVF-MMc&t=1178s """ + from __future__ import annotations test_array = [2, 1, 4, 5, 6, 0, 8, 9, 1, 2, 0, 6, 4, 2, 0, 6, 5, 3, 2, 7] diff --git a/data_structures/disjoint_set/disjoint_set.py b/data_structures/disjoint_set/disjoint_set.py index f8500bf2c3af..edc4736b6132 100644 --- a/data_structures/disjoint_set/disjoint_set.py +++ b/data_structures/disjoint_set/disjoint_set.py @@ -1,6 +1,6 @@ """ - Disjoint set. - Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure +Disjoint set. +Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure """ @@ -56,7 +56,8 @@ def find_python_set(node: Node) -> set: for s in sets: if node.data in s: return s - raise ValueError(f"{node.data} is not in {sets}") + msg = f"{node.data} is not in {sets}" + raise ValueError(msg) def test_disjoint_set() -> None: diff --git a/data_structures/hashing/bloom_filter.py b/data_structures/hashing/bloom_filter.py new file mode 100644 index 000000000000..eb2cb4b79c46 --- /dev/null +++ b/data_structures/hashing/bloom_filter.py @@ -0,0 +1,106 @@ +""" +See https://en.wikipedia.org/wiki/Bloom_filter + +The use of this data structure is to test membership in a set. +Compared to Python's built-in set() it is more space-efficient. +In the following example, only 8 bits of memory will be used: +>>> bloom = Bloom(size=8) + +Initially, the filter contains all zeros: +>>> bloom.bitstring +'00000000' + +When an element is added, two bits are set to 1 +since there are 2 hash functions in this implementation: +>>> "Titanic" in bloom +False +>>> bloom.add("Titanic") +>>> bloom.bitstring +'01100000' +>>> "Titanic" in bloom +True + +However, sometimes only one bit is added +because both hash functions return the same value +>>> bloom.add("Avatar") +>>> "Avatar" in bloom +True +>>> bloom.format_hash("Avatar") +'00000100' +>>> bloom.bitstring +'01100100' + +Not added elements should return False ... +>>> not_present_films = ("The Godfather", "Interstellar", "Parasite", "Pulp Fiction") +>>> { +... film: bloom.format_hash(film) for film in not_present_films +... } # doctest: +NORMALIZE_WHITESPACE +{'The Godfather': '00000101', + 'Interstellar': '00000011', + 'Parasite': '00010010', + 'Pulp Fiction': '10000100'} +>>> any(film in bloom for film in not_present_films) +False + +but sometimes there are false positives: +>>> "Ratatouille" in bloom +True +>>> bloom.format_hash("Ratatouille") +'01100000' + +The probability increases with the number of elements added. +The probability decreases with the number of bits in the bitarray. +>>> bloom.estimated_error_rate +0.140625 +>>> bloom.add("The Godfather") +>>> bloom.estimated_error_rate +0.25 +>>> bloom.bitstring +'01100101' +""" + +from hashlib import md5, sha256 + +HASH_FUNCTIONS = (sha256, md5) + + +class Bloom: + def __init__(self, size: int = 8) -> None: + self.bitarray = 0b0 + self.size = size + + def add(self, value: str) -> None: + h = self.hash_(value) + self.bitarray |= h + + def exists(self, value: str) -> bool: + h = self.hash_(value) + return (h & self.bitarray) == h + + def __contains__(self, other: str) -> bool: + return self.exists(other) + + def format_bin(self, bitarray: int) -> str: + res = bin(bitarray)[2:] + return res.zfill(self.size) + + @property + def bitstring(self) -> str: + return self.format_bin(self.bitarray) + + def hash_(self, value: str) -> int: + res = 0b0 + for func in HASH_FUNCTIONS: + position = ( + int.from_bytes(func(value.encode()).digest(), "little") % self.size + ) + res |= 2**position + return res + + def format_hash(self, value: str) -> str: + return self.format_bin(self.hash_(value)) + + @property + def estimated_error_rate(self) -> float: + n_ones = bin(self.bitarray).count("1") + return (n_ones / self.size) ** len(HASH_FUNCTIONS) diff --git a/data_structures/hashing/double_hash.py b/data_structures/hashing/double_hash.py index be21e74cadd0..324282cbfd8d 100644 --- a/data_structures/hashing/double_hash.py +++ b/data_structures/hashing/double_hash.py @@ -11,6 +11,7 @@ Reference: https://en.wikipedia.org/wiki/Double_hashing """ + from .hash_table import HashTable from .number_theory.prime_numbers import is_prime, next_prime @@ -35,6 +36,33 @@ def __hash_double_function(self, key, data, increment): return (increment * self.__hash_function_2(key, data)) % self.size_table def _collision_resolution(self, key, data=None): + """ + Examples: + + 1. Try to add three data elements when the size is three + >>> dh = DoubleHash(3) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {1: 10, 2: 20, 0: 30} + + 2. Try to add three data elements when the size is two + >>> dh = DoubleHash(2) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {10: 10, 9: 20, 8: 30} + + 3. Try to add three data elements when the size is four + >>> dh = DoubleHash(4) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {9: 20, 10: 10, 8: 30} + """ i = 1 new_key = self.hash_function(data) @@ -50,3 +78,9 @@ def _collision_resolution(self, key, data=None): i += 1 return new_key + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/hash_map.py b/data_structures/hashing/hash_map.py new file mode 100644 index 000000000000..9213d6930f67 --- /dev/null +++ b/data_structures/hashing/hash_map.py @@ -0,0 +1,305 @@ +""" +Hash map with open addressing. + +https://en.wikipedia.org/wiki/Hash_table + +Another hash map implementation, with a good explanation. +Modern Dictionaries by Raymond Hettinger +https://www.youtube.com/watch?v=p33CVV29OG8 +""" + +from collections.abc import Iterator, MutableMapping +from dataclasses import dataclass +from typing import Generic, TypeVar + +KEY = TypeVar("KEY") +VAL = TypeVar("VAL") + + +@dataclass(frozen=True, slots=True) +class _Item(Generic[KEY, VAL]): + key: KEY + val: VAL + + +class _DeletedItem(_Item): + def __init__(self) -> None: + super().__init__(None, None) + + def __bool__(self) -> bool: + return False + + +_deleted = _DeletedItem() + + +class HashMap(MutableMapping[KEY, VAL]): + """ + Hash map with open addressing. + """ + + def __init__( + self, initial_block_size: int = 8, capacity_factor: float = 0.75 + ) -> None: + self._initial_block_size = initial_block_size + self._buckets: list[_Item | None] = [None] * initial_block_size + assert 0.0 < capacity_factor < 1.0 + self._capacity_factor = capacity_factor + self._len = 0 + + def _get_bucket_index(self, key: KEY) -> int: + return hash(key) % len(self._buckets) + + def _get_next_ind(self, ind: int) -> int: + """ + Get next index. + + Implements linear open addressing. + >>> HashMap(5)._get_next_ind(3) + 4 + >>> HashMap(5)._get_next_ind(5) + 1 + >>> HashMap(5)._get_next_ind(6) + 2 + >>> HashMap(5)._get_next_ind(9) + 0 + """ + return (ind + 1) % len(self._buckets) + + def _try_set(self, ind: int, key: KEY, val: VAL) -> bool: + """ + Try to add value to the bucket. + + If bucket is empty or key is the same, does insert and return True. + + If bucket has another key or deleted placeholder, + that means that we need to check next bucket. + """ + stored = self._buckets[ind] + if not stored: + self._buckets[ind] = _Item(key, val) + self._len += 1 + return True + elif stored.key == key: + self._buckets[ind] = _Item(key, val) + return True + else: + return False + + def _is_full(self) -> bool: + """ + Return true if we have reached safe capacity. + + So we need to increase the number of buckets to avoid collisions. + + >>> hm = HashMap(2) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._is_full() + True + >>> HashMap(2)._is_full() + False + """ + limit = len(self._buckets) * self._capacity_factor + return len(self) >= int(limit) + + def _is_sparse(self) -> bool: + """Return true if we need twice fewer buckets when we have now.""" + if len(self._buckets) <= self._initial_block_size: + return False + limit = len(self._buckets) * self._capacity_factor / 2 + return len(self) < limit + + def _resize(self, new_size: int) -> None: + old_buckets = self._buckets + self._buckets = [None] * new_size + self._len = 0 + for item in old_buckets: + if item: + self._add_item(item.key, item.val) + + def _size_up(self) -> None: + self._resize(len(self._buckets) * 2) + + def _size_down(self) -> None: + self._resize(len(self._buckets) // 2) + + def _iterate_buckets(self, key: KEY) -> Iterator[int]: + ind = self._get_bucket_index(key) + for _ in range(len(self._buckets)): + yield ind + ind = self._get_next_ind(ind) + + def _add_item(self, key: KEY, val: VAL) -> None: + """ + Try to add 3 elements when the size is 5 + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm + HashMap(1: 10, 2: 20, 3: 30) + + Try to add 3 elements when the size is 5 + >>> hm = HashMap(5) + >>> hm._add_item(-5, 10) + >>> hm._add_item(6, 30) + >>> hm._add_item(-7, 20) + >>> hm + HashMap(-5: 10, 6: 30, -7: 20) + + Try to add 3 elements when size is 1 + >>> hm = HashMap(1) + >>> hm._add_item(10, 13.2) + >>> hm._add_item(6, 5.26) + >>> hm._add_item(7, 5.155) + >>> hm + HashMap(10: 13.2) + + Trying to add an element with a key that is a floating point value + >>> hm = HashMap(5) + >>> hm._add_item(1.5, 10) + >>> hm + HashMap(1.5: 10) + + 5. Trying to add an item with the same key + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(1, 20) + >>> hm + HashMap(1: 20) + """ + for ind in self._iterate_buckets(key): + if self._try_set(ind, key, val): + break + + def __setitem__(self, key: KEY, val: VAL) -> None: + """ + 1. Changing value of item whose key is present + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(1, 20) + >>> hm + HashMap(1: 20) + + 2. Changing value of item whose key is not present + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(0, 20) + >>> hm + HashMap(0: 20, 1: 10) + + 3. Changing the value of the same item multiple times + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(1, 20) + >>> hm.__setitem__(1, 30) + >>> hm + HashMap(1: 30) + """ + if self._is_full(): + self._size_up() + + self._add_item(key, val) + + def __delitem__(self, key: KEY) -> None: + """ + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__delitem__(3) + >>> hm + HashMap(1: 10, 2: 20) + >>> hm = HashMap(5) + >>> hm._add_item(-5, 10) + >>> hm._add_item(6, 30) + >>> hm._add_item(-7, 20) + >>> hm.__delitem__(-5) + >>> hm + HashMap(6: 30, -7: 20) + + # Trying to remove a non-existing item + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__delitem__(4) + Traceback (most recent call last): + ... + KeyError: 4 + """ + for ind in self._iterate_buckets(key): + item = self._buckets[ind] + if item is None: + raise KeyError(key) + if item is _deleted: + continue + if item.key == key: + self._buckets[ind] = _deleted + self._len -= 1 + break + if self._is_sparse(): + self._size_down() + + def __getitem__(self, key: KEY) -> VAL: + """ + Returns the item at the given key + + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__getitem__(1) + 10 + + >>> hm = HashMap(5) + >>> hm._add_item(10, -10) + >>> hm._add_item(20, -20) + >>> hm.__getitem__(20) + -20 + + >>> hm = HashMap(5) + >>> hm._add_item(-1, 10) + >>> hm.__getitem__(-1) + 10 + """ + for ind in self._iterate_buckets(key): + item = self._buckets[ind] + if item is None: + break + if item is _deleted: + continue + if item.key == key: + return item.val + raise KeyError(key) + + def __len__(self) -> int: + """ + Returns the number of items present in hashmap + + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__len__() + 3 + + >>> hm = HashMap(5) + >>> hm.__len__() + 0 + """ + return self._len + + def __iter__(self) -> Iterator[KEY]: + yield from (item.key for item in self._buckets if item) + + def __repr__(self) -> str: + val_string = ", ".join( + f"{item.key}: {item.val}" for item in self._buckets if item + ) + return f"HashMap({val_string})" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/hash_table.py b/data_structures/hashing/hash_table.py index 7ca2f7c401cf..40fcad9a3dab 100644 --- a/data_structures/hashing/hash_table.py +++ b/data_structures/hashing/hash_table.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from abc import abstractmethod + from .number_theory.prime_numbers import next_prime @@ -21,6 +23,29 @@ def __init__( self._keys: dict = {} def keys(self): + """ + The keys function returns a dictionary containing the key value pairs. + key being the index number in hash table and value being the data value. + + Examples: + 1. creating HashTable with size 10 and inserting 3 elements + >>> ht = HashTable(10) + >>> ht.insert_data(10) + >>> ht.insert_data(20) + >>> ht.insert_data(30) + >>> ht.keys() + {0: 10, 1: 20, 2: 30} + + 2. creating HashTable with size 5 and inserting 5 elements + >>> ht = HashTable(5) + >>> ht.insert_data(5) + >>> ht.insert_data(4) + >>> ht.insert_data(3) + >>> ht.insert_data(2) + >>> ht.insert_data(1) + >>> ht.keys() + {0: 5, 4: 4, 3: 3, 2: 2, 1: 1} + """ return self._keys def balanced_factor(self): @@ -29,6 +54,30 @@ def balanced_factor(self): ) def hash_function(self, key): + """ + Generates hash for the given key value + + Examples: + + Creating HashTable with size 5 + >>> ht = HashTable(5) + >>> ht.hash_function(10) + 0 + >>> ht.hash_function(20) + 0 + >>> ht.hash_function(4) + 4 + >>> ht.hash_function(18) + 3 + >>> ht.hash_function(-18) + 2 + >>> ht.hash_function(18.5) + 3.5 + >>> ht.hash_function(0) + 0 + >>> ht.hash_function(-0) + 0 + """ return key % self.size_table def _step_by_step(self, step_ord): @@ -37,6 +86,43 @@ def _step_by_step(self, step_ord): print(self.values) def bulk_insert(self, values): + """ + bulk_insert is used for entering more than one element at a time + in the HashTable. + + Examples: + 1. + >>> ht = HashTable(5) + >>> ht.bulk_insert((10,20,30)) + step 1 + [0, 1, 2, 3, 4] + [10, None, None, None, None] + step 2 + [0, 1, 2, 3, 4] + [10, 20, None, None, None] + step 3 + [0, 1, 2, 3, 4] + [10, 20, 30, None, None] + + 2. + >>> ht = HashTable(5) + >>> ht.bulk_insert([5,4,3,2,1]) + step 1 + [0, 1, 2, 3, 4] + [5, None, None, None, None] + step 2 + [0, 1, 2, 3, 4] + [5, None, None, None, 4] + step 3 + [0, 1, 2, 3, 4] + [5, None, None, 3, 4] + step 4 + [0, 1, 2, 3, 4] + [5, None, 2, 3, 4] + step 5 + [0, 1, 2, 3, 4] + [5, 1, 2, 3, 4] + """ i = 1 self.__aux_list = values for value in values: @@ -45,10 +131,100 @@ def bulk_insert(self, values): i += 1 def _set_value(self, key, data): + """ + _set_value functions allows to update value at a particular hash + + Examples: + 1. _set_value in HashTable of size 5 + >>> ht = HashTable(5) + >>> ht.insert_data(10) + >>> ht.insert_data(20) + >>> ht.insert_data(30) + >>> ht._set_value(0,15) + >>> ht.keys() + {0: 15, 1: 20, 2: 30} + + 2. _set_value in HashTable of size 2 + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(3,15) + >>> ht.keys() + {3: 15, 2: 17, 4: 99} + + 3. _set_value in HashTable when hash is not present + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(0,15) + >>> ht.keys() + {3: 18, 2: 17, 4: 99, 0: 15} + + 4. _set_value in HashTable when multiple hash are not present + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(0,15) + >>> ht._set_value(1,20) + >>> ht.keys() + {3: 18, 2: 17, 4: 99, 0: 15, 1: 20} + """ self.values[key] = data self._keys[key] = data + @abstractmethod def _collision_resolution(self, key, data=None): + """ + This method is a type of open addressing which is used for handling collision. + + In this implementation the concept of linear probing has been used. + + The hash table is searched sequentially from the original location of the + hash, if the new hash/location we get is already occupied we check for the next + hash/location. + + references: + - https://en.wikipedia.org/wiki/Linear_probing + + Examples: + 1. The collision will be with keys 18 & 99, so new hash will be created for 99 + >>> ht = HashTable(3) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.keys() + {2: 17, 0: 18, 1: 99} + + 2. The collision will be with keys 17 & 101, so new hash + will be created for 101 + >>> ht = HashTable(4) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.insert_data(101) + >>> ht.keys() + {1: 17, 2: 18, 3: 99, 0: 101} + + 2. The collision will be with all keys, so new hash will be created for all + >>> ht = HashTable(1) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.keys() + {2: 17, 3: 18, 4: 99} + + 3. Trying to insert float key in hash + >>> ht = HashTable(1) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99.99) + Traceback (most recent call last): + ... + TypeError: list indices must be integers or slices, not float + """ new_key = self.hash_function(key + 1) while self.values[new_key] is not None and self.values[new_key] != key: @@ -69,6 +245,21 @@ def rehashing(self): self.insert_data(value) def insert_data(self, data): + """ + insert_data is used for inserting a single element at a time in the HashTable. + + Examples: + + >>> ht = HashTable(3) + >>> ht.insert_data(5) + >>> ht.keys() + {2: 5} + >>> ht = HashTable(5) + >>> ht.insert_data(30) + >>> ht.insert_data(50) + >>> ht.keys() + {0: 30, 1: 50} + """ key = self.hash_function(data) if self.values[key] is None: @@ -84,3 +275,9 @@ def insert_data(self, data): else: self.rehashing() self.insert_data(data) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/number_theory/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py index b88ab76ecc23..82071b5e9f09 100644 --- a/data_structures/hashing/number_theory/prime_numbers.py +++ b/data_structures/hashing/number_theory/prime_numbers.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ - module to operations with prime numbers +module to operations with prime numbers """ import math @@ -32,9 +32,9 @@ def is_prime(number: int) -> bool: """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and positive" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and positive" + ) if 1 < number < 4: # 2 and 3 are primes @@ -52,7 +52,7 @@ def next_prime(value, factor=1, **kwargs): first_value_val = value while not is_prime(value): - value += 1 if not ("desc" in kwargs.keys() and kwargs["desc"] is True) else -1 + value += 1 if not ("desc" in kwargs and kwargs["desc"] is True) else -1 if value == first_value_val: return next_prime(value + 1, **kwargs) diff --git a/data_structures/hashing/quadratic_probing.py b/data_structures/hashing/quadratic_probing.py index 0930340a347f..56d4926eee9b 100644 --- a/data_structures/hashing/quadratic_probing.py +++ b/data_structures/hashing/quadratic_probing.py @@ -11,7 +11,56 @@ class QuadraticProbing(HashTable): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def _collision_resolution(self, key, data=None): + def _collision_resolution(self, key, data=None): # noqa: ARG002 + """ + Quadratic probing is an open addressing scheme used for resolving + collisions in hash table. + + It works by taking the original hash index and adding successive + values of an arbitrary quadratic polynomial until open slot is found. + + Hash + 1², Hash + 2², Hash + 3² .... Hash + n² + + reference: + - https://en.wikipedia.org/wiki/Quadratic_probing + e.g: + 1. Create hash table with size 7 + >>> qp = QuadraticProbing(7) + >>> qp.insert_data(90) + >>> qp.insert_data(340) + >>> qp.insert_data(24) + >>> qp.insert_data(45) + >>> qp.insert_data(99) + >>> qp.insert_data(73) + >>> qp.insert_data(7) + >>> qp.keys() + {11: 45, 14: 99, 7: 24, 0: 340, 5: 73, 6: 90, 8: 7} + + 2. Create hash table with size 8 + >>> qp = QuadraticProbing(8) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {0: 0, 7: 999, 3: 111} + + 3. Try to add three data elements when the size is two + >>> qp = QuadraticProbing(2) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {0: 0, 4: 999, 1: 111} + + 4. Try to add three data elements when the size is one + >>> qp = QuadraticProbing(1) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {4: 999, 1: 111} + """ + i = 1 new_key = self.hash_function(key + i * i) @@ -27,3 +76,9 @@ def _collision_resolution(self, key, data=None): break return new_key + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/arithmetic_analysis/image_data/__init__.py b/data_structures/hashing/tests/__init__.py similarity index 100% rename from arithmetic_analysis/image_data/__init__.py rename to data_structures/hashing/tests/__init__.py diff --git a/data_structures/hashing/tests/test_hash_map.py b/data_structures/hashing/tests/test_hash_map.py new file mode 100644 index 000000000000..4292c0178b7b --- /dev/null +++ b/data_structures/hashing/tests/test_hash_map.py @@ -0,0 +1,97 @@ +from operator import delitem, getitem, setitem + +import pytest + +from data_structures.hashing.hash_map import HashMap + + +def _get(k): + return getitem, k + + +def _set(k, v): + return setitem, k, v + + +def _del(k): + return delitem, k + + +def _run_operation(obj, fun, *args): + try: + return fun(obj, *args), None + except Exception as e: + return None, e + + +_add_items = ( + _set("key_a", "val_a"), + _set("key_b", "val_b"), +) + +_overwrite_items = [ + _set("key_a", "val_a"), + _set("key_a", "val_b"), +] + +_delete_items = [ + _set("key_a", "val_a"), + _set("key_b", "val_b"), + _del("key_a"), + _del("key_b"), + _set("key_a", "val_a"), + _del("key_a"), +] + +_access_absent_items = [ + _get("key_a"), + _del("key_a"), + _set("key_a", "val_a"), + _del("key_a"), + _del("key_a"), + _get("key_a"), +] + +_add_with_resize_up = [ + *[_set(x, x) for x in range(5)], # guaranteed upsize +] + +_add_with_resize_down = [ + *[_set(x, x) for x in range(5)], # guaranteed upsize + *[_del(x) for x in range(5)], + _set("key_a", "val_b"), +] + + +@pytest.mark.parametrize( + "operations", + [ + pytest.param(_add_items, id="add items"), + pytest.param(_overwrite_items, id="overwrite items"), + pytest.param(_delete_items, id="delete items"), + pytest.param(_access_absent_items, id="access absent items"), + pytest.param(_add_with_resize_up, id="add with resize up"), + pytest.param(_add_with_resize_down, id="add with resize down"), + ], +) +def test_hash_map_is_the_same_as_dict(operations): + my = HashMap(initial_block_size=4) + py = {} + for _, (fun, *args) in enumerate(operations): + my_res, my_exc = _run_operation(my, fun, *args) + py_res, py_exc = _run_operation(py, fun, *args) + assert my_res == py_res + assert str(my_exc) == str(py_exc) + assert set(py) == set(my) + assert len(py) == len(my) + assert set(my.items()) == set(py.items()) + + +def test_no_new_methods_was_added_to_api(): + def is_public(name: str) -> bool: + return not name.startswith("_") + + dict_public_names = {name for name in dir({}) if is_public(name)} + hash_public_names = {name for name in dir(HashMap()) if is_public(name)} + + assert dict_public_names > hash_public_names diff --git a/data_structures/heap/binomial_heap.py b/data_structures/heap/binomial_heap.py index 2e05c5c80a22..9cfdf0c12fe0 100644 --- a/data_structures/heap/binomial_heap.py +++ b/data_structures/heap/binomial_heap.py @@ -73,7 +73,7 @@ class BinomialHeap: 30 Deleting - delete() test - >>> [first_heap.delete_min() for _ in range(20)] + >>> [int(first_heap.delete_min()) for _ in range(20)] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] Create a new Heap @@ -118,7 +118,7 @@ class BinomialHeap: values in merged heap; (merge is inplace) >>> results = [] >>> while not first_heap.is_empty(): - ... results.append(first_heap.delete_min()) + ... results.append(int(first_heap.delete_min())) >>> results [17, 20, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34] """ @@ -136,12 +136,12 @@ def merge_heaps(self, other): # Empty heaps corner cases if other.size == 0: - return + return None if self.size == 0: self.size = other.size self.bottom_root = other.bottom_root self.min_node = other.min_node - return + return None # Update size self.size = self.size + other.size @@ -354,7 +354,7 @@ def delete_min(self): # Merge heaps self.merge_heaps(new_heap) - return min_value + return int(min_value) def pre_order(self): """ diff --git a/data_structures/heap/heap.py b/data_structures/heap/heap.py index b14c55d9db4c..7b15e69f13ca 100644 --- a/data_structures/heap/heap.py +++ b/data_structures/heap/heap.py @@ -1,9 +1,28 @@ from __future__ import annotations +from abc import abstractmethod from collections.abc import Iterable +from typing import Generic, Protocol, TypeVar -class Heap: +class Comparable(Protocol): + @abstractmethod + def __lt__(self: T, other: T) -> bool: + pass + + @abstractmethod + def __gt__(self: T, other: T) -> bool: + pass + + @abstractmethod + def __eq__(self: T, other: object) -> bool: + pass + + +T = TypeVar("T", bound=Comparable) + + +class Heap(Generic[T]): """A Max Heap Implementation >>> unsorted = [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5] @@ -27,14 +46,44 @@ class Heap: """ def __init__(self) -> None: - self.h: list[float] = [] + self.h: list[T] = [] self.heap_size: int = 0 def __repr__(self) -> str: return str(self.h) def parent_index(self, child_idx: int) -> int | None: - """return the parent index of given child""" + """ + returns the parent index based on the given child index + + >>> h = Heap() + >>> h.build_max_heap([103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5]) + >>> h + [209, 201, 25, 103, 107, 15, 1, 9, 7, 11, 5] + + >>> h.parent_index(-1) # returns none if index is <=0 + + >>> h.parent_index(0) # returns none if index is <=0 + + >>> h.parent_index(1) + 0 + >>> h.parent_index(2) + 0 + >>> h.parent_index(3) + 1 + >>> h.parent_index(4) + 1 + >>> h.parent_index(5) + 2 + >>> h.parent_index(10.5) + 4.0 + >>> h.parent_index(209.0) + 104.0 + >>> h.parent_index("Test") + Traceback (most recent call last): + ... + TypeError: '>' not supported between instances of 'str' and 'int' + """ if child_idx > 0: return (child_idx - 1) // 2 return None @@ -62,6 +111,9 @@ def right_child_idx(self, parent_idx: int) -> int | None: def max_heapify(self, index: int) -> None: """ correct a single violation of the heap property in a subtree's root. + + It is the function that is responsible for restoring the property + of Max heap i.e the maximum element is always at top. """ if index < self.heap_size: violation: int = index @@ -79,8 +131,30 @@ def max_heapify(self, index: int) -> None: # fix the subsequent violation recursively if any self.max_heapify(violation) - def build_max_heap(self, collection: Iterable[float]) -> None: - """build max heap from an unsorted array""" + def build_max_heap(self, collection: Iterable[T]) -> None: + """ + build max heap from an unsorted array + + >>> h = Heap() + >>> h.build_max_heap([20,40,50,20,10]) + >>> h + [50, 40, 20, 20, 10] + + >>> h = Heap() + >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0]) + >>> h + [9, 8, 7, 4, 5, 6, 3, 2, 1, 0] + + >>> h = Heap() + >>> h.build_max_heap([514,5,61,57,8,99,105]) + >>> h + [514, 57, 105, 5, 8, 99, 61] + + >>> h = Heap() + >>> h.build_max_heap([514,5,61.6,57,8,9.9,105]) + >>> h + [514, 57, 105, 5, 8, 9.9, 61.6] + """ self.h = list(collection) self.heap_size = len(self.h) if self.heap_size > 1: @@ -88,8 +162,25 @@ def build_max_heap(self, collection: Iterable[float]) -> None: for i in range(self.heap_size // 2 - 1, -1, -1): self.max_heapify(i) - def extract_max(self) -> float: - """get and remove max from heap""" + def extract_max(self) -> T: + """ + get and remove max from heap + + >>> h = Heap() + >>> h.build_max_heap([20,40,50,20,10]) + >>> h.extract_max() + 50 + + >>> h = Heap() + >>> h.build_max_heap([514,5,61,57,8,99,105]) + >>> h.extract_max() + 514 + + >>> h = Heap() + >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0]) + >>> h.extract_max() + 9 + """ if self.heap_size >= 2: me = self.h[0] self.h[0] = self.h.pop(-1) @@ -102,8 +193,35 @@ def extract_max(self) -> float: else: raise Exception("Empty heap") - def insert(self, value: float) -> None: - """insert a new value into the max heap""" + def insert(self, value: T) -> None: + """ + insert a new value into the max heap + + >>> h = Heap() + >>> h.insert(10) + >>> h + [10] + + >>> h = Heap() + >>> h.insert(10) + >>> h.insert(10) + >>> h + [10, 10] + + >>> h = Heap() + >>> h.insert(10) + >>> h.insert(10.1) + >>> h + [10.1, 10] + + >>> h = Heap() + >>> h.insert(0.1) + >>> h.insert(0) + >>> h.insert(9) + >>> h.insert(5) + >>> h + [9, 5, 0.1, 0] + """ self.h.append(value) idx = (self.heap_size - 1) // 2 self.heap_size += 1 @@ -144,7 +262,7 @@ def heap_sort(self) -> None: ]: print(f"unsorted array: {unsorted}") - heap = Heap() + heap: Heap[int] = Heap() heap.build_max_heap(unsorted) print(f"after build heap: {heap}") diff --git a/data_structures/heap/heap_generic.py b/data_structures/heap/heap_generic.py index b4d7019f41f9..ee92149e25a9 100644 --- a/data_structures/heap/heap_generic.py +++ b/data_structures/heap/heap_generic.py @@ -166,7 +166,6 @@ def test_heap() -> None: >>> h.get_top() [9, -40] """ - pass if __name__ == "__main__": diff --git a/data_structures/heap/max_heap.py b/data_structures/heap/max_heap.py index fbc8eed09226..589f2595a8da 100644 --- a/data_structures/heap/max_heap.py +++ b/data_structures/heap/max_heap.py @@ -38,13 +38,12 @@ def insert(self, value: int) -> None: def __swap_down(self, i: int) -> None: """Swap the element down""" while self.__size >= 2 * i: - if 2 * i + 1 > self.__size: + if 2 * i + 1 > self.__size: # noqa: SIM114 + bigger_child = 2 * i + elif self.__heap[2 * i] > self.__heap[2 * i + 1]: bigger_child = 2 * i else: - if self.__heap[2 * i] > self.__heap[2 * i + 1]: - bigger_child = 2 * i - else: - bigger_child = 2 * i + 1 + bigger_child = 2 * i + 1 temporary = self.__heap[i] if self.__heap[i] < self.__heap[bigger_child]: self.__heap[i] = self.__heap[bigger_child] diff --git a/data_structures/heap/min_heap.py b/data_structures/heap/min_heap.py index ecb1876493b0..577b98d788a1 100644 --- a/data_structures/heap/min_heap.py +++ b/data_structures/heap/min_heap.py @@ -66,14 +66,14 @@ def build_heap(self, array): # this is min-heapify method def sift_down(self, idx, array): while True: - l = self.get_left_child_idx(idx) # noqa: E741 - r = self.get_right_child_idx(idx) + left = self.get_left_child_idx(idx) + right = self.get_right_child_idx(idx) smallest = idx - if l < len(array) and array[l] < array[idx]: - smallest = l - if r < len(array) and array[r] < array[smallest]: - smallest = r + if left < len(array) and array[left] < array[idx]: + smallest = left + if right < len(array) and array[right] < array[smallest]: + smallest = right if smallest != idx: array[idx], array[smallest] = array[smallest], array[idx] @@ -124,9 +124,9 @@ def is_empty(self): return len(self.heap) == 0 def decrease_key(self, node, new_value): - assert ( - self.heap[self.idx_of_element[node]].val > new_value - ), "newValue must be less that current value" + assert self.heap[self.idx_of_element[node]].val > new_value, ( + "newValue must be less that current value" + ) node.val = new_value self.heap_dict[node.name] = new_value self.sift_up(self.idx_of_element[node]) diff --git a/data_structures/heap/randomized_heap.py b/data_structures/heap/randomized_heap.py index c0f9888f80c7..12888c1f4089 100644 --- a/data_structures/heap/randomized_heap.py +++ b/data_structures/heap/randomized_heap.py @@ -22,14 +22,40 @@ def __init__(self, value: T) -> None: @property def value(self) -> T: - """Return the value of the node.""" + """ + Return the value of the node. + + >>> rhn = RandomizedHeapNode(10) + >>> rhn.value + 10 + >>> rhn = RandomizedHeapNode(-10) + >>> rhn.value + -10 + """ return self._value @staticmethod def merge( root1: RandomizedHeapNode[T] | None, root2: RandomizedHeapNode[T] | None ) -> RandomizedHeapNode[T] | None: - """Merge 2 nodes together.""" + """ + Merge 2 nodes together. + + >>> rhn1 = RandomizedHeapNode(10) + >>> rhn2 = RandomizedHeapNode(20) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 10 + + >>> rhn1 = RandomizedHeapNode(20) + >>> rhn2 = RandomizedHeapNode(10) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 10 + + >>> rhn1 = RandomizedHeapNode(5) + >>> rhn2 = RandomizedHeapNode(0) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 0 + """ if not root1: return root2 diff --git a/data_structures/heap/skew_heap.py b/data_structures/heap/skew_heap.py index c4c13b08276a..0839db711cb1 100644 --- a/data_structures/heap/skew_heap.py +++ b/data_structures/heap/skew_heap.py @@ -21,14 +21,55 @@ def __init__(self, value: T) -> None: @property def value(self) -> T: - """Return the value of the node.""" + """ + Return the value of the node. + + >>> SkewNode(0).value + 0 + >>> SkewNode(3.14159).value + 3.14159 + >>> SkewNode("hello").value + 'hello' + >>> SkewNode(None).value + + >>> SkewNode(True).value + True + >>> SkewNode([]).value + [] + >>> SkewNode({}).value + {} + >>> SkewNode(set()).value + set() + >>> SkewNode(0.0).value + 0.0 + >>> SkewNode(-1e-10).value + -1e-10 + >>> SkewNode(10).value + 10 + >>> SkewNode(-10.5).value + -10.5 + >>> SkewNode().value + Traceback (most recent call last): + ... + TypeError: SkewNode.__init__() missing 1 required positional argument: 'value' + """ return self._value @staticmethod def merge( root1: SkewNode[T] | None, root2: SkewNode[T] | None ) -> SkewNode[T] | None: - """Merge 2 nodes together.""" + """ + Merge 2 nodes together. + >>> SkewNode.merge(SkewNode(10),SkewNode(-10.5)).value + -10.5 + >>> SkewNode.merge(SkewNode(10),SkewNode(10.5)).value + 10 + >>> SkewNode.merge(SkewNode(10),SkewNode(10)).value + 10 + >>> SkewNode.merge(SkewNode(-100),SkewNode(-10.5)).value + -100 + """ if not root1: return root2 diff --git a/data_structures/kd_tree/__init__.py b/data_structures/kd_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/build_kdtree.py b/data_structures/kd_tree/build_kdtree.py new file mode 100644 index 000000000000..074a5dac4d42 --- /dev/null +++ b/data_structures/kd_tree/build_kdtree.py @@ -0,0 +1,43 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.kd_tree.kd_node import KDNode + + +def build_kdtree(points: list[list[float]], depth: int = 0) -> KDNode | None: + """ + Builds a KD-Tree from a list of points. + + Args: + points: The list of points to build the KD-Tree from. + depth: The current depth in the tree + (used to determine axis for splitting). + + Returns: + The root node of the KD-Tree, + or None if no points are provided. + """ + if not points: + return None + + k = len(points[0]) # Dimensionality of the points + axis = depth % k + + # Sort point list and choose median as pivot element + points.sort(key=lambda point: point[axis]) + median_idx = len(points) // 2 + + # Create node and construct subtrees + left_points = points[:median_idx] + right_points = points[median_idx + 1 :] + + return KDNode( + point=points[median_idx], + left=build_kdtree(left_points, depth + 1), + right=build_kdtree(right_points, depth + 1), + ) diff --git a/data_structures/kd_tree/example/__init__.py b/data_structures/kd_tree/example/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/example/example_usage.py b/data_structures/kd_tree/example/example_usage.py new file mode 100644 index 000000000000..892c3b8c4a2a --- /dev/null +++ b/data_structures/kd_tree/example/example_usage.py @@ -0,0 +1,46 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np + +from data_structures.kd_tree.build_kdtree import build_kdtree +from data_structures.kd_tree.example.hypercube_points import hypercube_points +from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search + + +def main() -> None: + """ + Demonstrates the use of KD-Tree by building it from random points + in a 10-dimensional hypercube and performing a nearest neighbor search. + """ + num_points: int = 5000 + cube_size: float = 10.0 # Size of the hypercube (edge length) + num_dimensions: int = 10 + + # Generate random points within the hypercube + points: np.ndarray = hypercube_points(num_points, cube_size, num_dimensions) + hypercube_kdtree = build_kdtree(points.tolist()) + + # Generate a random query point within the same space + rng = np.random.default_rng() + query_point: list[float] = rng.random(num_dimensions).tolist() + + # Perform nearest neighbor search + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + hypercube_kdtree, query_point + ) + + # Print the results + print(f"Query point: {query_point}") + print(f"Nearest point: {nearest_point}") + print(f"Distance: {nearest_dist:.4f}") + print(f"Nodes visited: {nodes_visited}") + + +if __name__ == "__main__": + main() diff --git a/data_structures/kd_tree/example/hypercube_points.py b/data_structures/kd_tree/example/hypercube_points.py new file mode 100644 index 000000000000..66744856e6d5 --- /dev/null +++ b/data_structures/kd_tree/example/hypercube_points.py @@ -0,0 +1,29 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np + + +def hypercube_points( + num_points: int, hypercube_size: float, num_dimensions: int +) -> np.ndarray: + """ + Generates random points uniformly distributed within an n-dimensional hypercube. + + Args: + num_points: Number of points to generate. + hypercube_size: Size of the hypercube. + num_dimensions: Number of dimensions of the hypercube. + + Returns: + An array of shape (num_points, num_dimensions) + with generated points. + """ + rng = np.random.default_rng() + shape = (num_points, num_dimensions) + return hypercube_size * rng.random(shape) diff --git a/data_structures/kd_tree/kd_node.py b/data_structures/kd_tree/kd_node.py new file mode 100644 index 000000000000..5a22ef609077 --- /dev/null +++ b/data_structures/kd_tree/kd_node.py @@ -0,0 +1,38 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from __future__ import annotations + + +class KDNode: + """ + Represents a node in a KD-Tree. + + Attributes: + point: The point stored in this node. + left: The left child node. + right: The right child node. + """ + + def __init__( + self, + point: list[float], + left: KDNode | None = None, + right: KDNode | None = None, + ) -> None: + """ + Initializes a KDNode with the given point and child nodes. + + Args: + point (list[float]): The point stored in this node. + left (Optional[KDNode]): The left child node. + right (Optional[KDNode]): The right child node. + """ + self.point = point + self.left = left + self.right = right diff --git a/data_structures/kd_tree/nearest_neighbour_search.py b/data_structures/kd_tree/nearest_neighbour_search.py new file mode 100644 index 000000000000..8104944c08f0 --- /dev/null +++ b/data_structures/kd_tree/nearest_neighbour_search.py @@ -0,0 +1,79 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.kd_tree.kd_node import KDNode + + +def nearest_neighbour_search( + root: KDNode | None, query_point: list[float] +) -> tuple[list[float] | None, float, int]: + """ + Performs a nearest neighbor search in a KD-Tree for a given query point. + + Args: + root (KDNode | None): The root node of the KD-Tree. + query_point (list[float]): The point for which the nearest neighbor + is being searched. + + Returns: + tuple[list[float] | None, float, int]: + - The nearest point found in the KD-Tree to the query point, + or None if no point is found. + - The squared distance to the nearest point. + - The number of nodes visited during the search. + """ + nearest_point: list[float] | None = None + nearest_dist: float = float("inf") + nodes_visited: int = 0 + + def search(node: KDNode | None, depth: int = 0) -> None: + """ + Recursively searches for the nearest neighbor in the KD-Tree. + + Args: + node: The current node in the KD-Tree. + depth: The current depth in the KD-Tree. + """ + nonlocal nearest_point, nearest_dist, nodes_visited + if node is None: + return + + nodes_visited += 1 + + # Calculate the current distance (squared distance) + current_point = node.point + current_dist = sum( + (query_coord - point_coord) ** 2 + for query_coord, point_coord in zip(query_point, current_point) + ) + + # Update nearest point if the current node is closer + if nearest_point is None or current_dist < nearest_dist: + nearest_point = current_point + nearest_dist = current_dist + + # Determine which subtree to search first (based on axis and query point) + k = len(query_point) # Dimensionality of points + axis = depth % k + + if query_point[axis] <= current_point[axis]: + nearer_subtree = node.left + further_subtree = node.right + else: + nearer_subtree = node.right + further_subtree = node.left + + # Search the nearer subtree first + search(nearer_subtree, depth + 1) + + # If the further subtree has a closer point + if (query_point[axis] - current_point[axis]) ** 2 < nearest_dist: + search(further_subtree, depth + 1) + + search(root, 0) + return nearest_point, nearest_dist, nodes_visited diff --git a/data_structures/kd_tree/tests/__init__.py b/data_structures/kd_tree/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/tests/test_kdtree.py b/data_structures/kd_tree/tests/test_kdtree.py new file mode 100644 index 000000000000..d6a4a66dd24d --- /dev/null +++ b/data_structures/kd_tree/tests/test_kdtree.py @@ -0,0 +1,108 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np +import pytest + +from data_structures.kd_tree.build_kdtree import build_kdtree +from data_structures.kd_tree.example.hypercube_points import hypercube_points +from data_structures.kd_tree.kd_node import KDNode +from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search + + +@pytest.mark.parametrize( + ("num_points", "cube_size", "num_dimensions", "depth", "expected_result"), + [ + (0, 10.0, 2, 0, None), # Empty points list + (10, 10.0, 2, 2, KDNode), # Depth = 2, 2D points + (10, 10.0, 3, -2, KDNode), # Depth = -2, 3D points + ], +) +def test_build_kdtree(num_points, cube_size, num_dimensions, depth, expected_result): + """ + Test that KD-Tree is built correctly. + + Cases: + - Empty points list. + - Positive depth value. + - Negative depth value. + """ + points = ( + hypercube_points(num_points, cube_size, num_dimensions).tolist() + if num_points > 0 + else [] + ) + + kdtree = build_kdtree(points, depth=depth) + + if expected_result is None: + # Empty points list case + assert kdtree is None, f"Expected None for empty points list, got {kdtree}" + else: + # Check if root node is not None + assert kdtree is not None, "Expected a KDNode, got None" + + # Check if root has correct dimensions + assert len(kdtree.point) == num_dimensions, ( + f"Expected point dimension {num_dimensions}, got {len(kdtree.point)}" + ) + + # Check that the tree is balanced to some extent (simplistic check) + assert isinstance(kdtree, KDNode), ( + f"Expected KDNode instance, got {type(kdtree)}" + ) + + +def test_nearest_neighbour_search(): + """ + Test the nearest neighbor search function. + """ + num_points = 10 + cube_size = 10.0 + num_dimensions = 2 + points = hypercube_points(num_points, cube_size, num_dimensions) + kdtree = build_kdtree(points.tolist()) + + rng = np.random.default_rng() + query_point = rng.random(num_dimensions).tolist() + + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + kdtree, query_point + ) + + # Check that nearest point is not None + assert nearest_point is not None + + # Check that distance is a non-negative number + assert nearest_dist >= 0 + + # Check that nodes visited is a non-negative integer + assert nodes_visited >= 0 + + +def test_edge_cases(): + """ + Test edge cases such as an empty KD-Tree. + """ + empty_kdtree = build_kdtree([]) + query_point = [0.0] * 2 # Using a default 2D query point + + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + empty_kdtree, query_point + ) + + # With an empty KD-Tree, nearest_point should be None + assert nearest_point is None + assert nearest_dist == float("inf") + assert nodes_visited == 0 + + +if __name__ == "__main__": + import pytest + + pytest.main() diff --git a/data_structures/linked_list/__init__.py b/data_structures/linked_list/__init__.py index 56b0e51baa93..00ef337a1211 100644 --- a/data_structures/linked_list/__init__.py +++ b/data_structures/linked_list/__init__.py @@ -5,6 +5,7 @@ head node gives us access of the complete list - Last node: points to null """ + from __future__ import annotations from typing import Any @@ -21,8 +22,56 @@ def __init__(self) -> None: self.head: Node | None = None self.size = 0 - def add(self, item: Any) -> None: - self.head = Node(item, self.head) + def add(self, item: Any, position: int = 0) -> None: + """ + Add an item to the LinkedList at the specified position. + Default position is 0 (the head). + + Args: + item (Any): The item to add to the LinkedList. + position (int, optional): The position at which to add the item. + Defaults to 0. + + Raises: + ValueError: If the position is negative or out of bounds. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 + + # Test adding to a negative position + >>> linked_list.add(5, -3) + Traceback (most recent call last): + ... + ValueError: Position must be non-negative + + # Test adding to an out-of-bounds position + >>> linked_list.add(5,7) + Traceback (most recent call last): + ... + ValueError: Out of bounds + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + """ + if position < 0: + raise ValueError("Position must be non-negative") + + if position == 0 or self.head is None: + new_node = Node(item, self.head) + self.head = new_node + else: + current = self.head + for _ in range(position - 1): + current = current.next + if current is None: + raise ValueError("Out of bounds") + new_node = Node(item, current.next) + current.next = new_node self.size += 1 def remove(self) -> Any: diff --git a/data_structures/linked_list/circular_linked_list.py b/data_structures/linked_list/circular_linked_list.py index 67a63cd55e19..bb64441d4560 100644 --- a/data_structures/linked_list/circular_linked_list.py +++ b/data_structures/linked_list/circular_linked_list.py @@ -1,90 +1,156 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from typing import Any +@dataclass class Node: - def __init__(self, data: Any): - self.data: Any = data - self.next: Node | None = None + data: Any + next_node: Node | None = None +@dataclass class CircularLinkedList: - def __init__(self): - self.head = None - self.tail = None + head: Node | None = None # Reference to the head (first node) + tail: Node | None = None # Reference to the tail (last node) def __iter__(self) -> Iterator[Any]: + """ + Iterate through all nodes in the Circular Linked List yielding their data. + Yields: + The data of each node in the linked list. + """ node = self.head - while self.head: + while node: yield node.data - node = node.next + node = node.next_node if node == self.head: break def __len__(self) -> int: - return len(tuple(iter(self))) - - def __repr__(self): + """ + Get the length (number of nodes) in the Circular Linked List. + """ + return sum(1 for _ in self) + + def __repr__(self) -> str: + """ + Generate a string representation of the Circular Linked List. + Returns: + A string of the format "1->2->....->N". + """ return "->".join(str(item) for item in iter(self)) def insert_tail(self, data: Any) -> None: + """ + Insert a node with the given data at the end of the Circular Linked List. + """ self.insert_nth(len(self), data) def insert_head(self, data: Any) -> None: + """ + Insert a node with the given data at the beginning of the Circular Linked List. + """ self.insert_nth(0, data) def insert_nth(self, index: int, data: Any) -> None: + """ + Insert the data of the node at the nth pos in the Circular Linked List. + Args: + index: The index at which the data should be inserted. + data: The data to be inserted. + + Raises: + IndexError: If the index is out of range. + """ if index < 0 or index > len(self): raise IndexError("list index out of range.") - new_node = Node(data) + new_node: Node = Node(data) if self.head is None: - new_node.next = new_node # first node points itself + new_node.next_node = new_node # First node points to itself self.tail = self.head = new_node - elif index == 0: # insert at head - new_node.next = self.head - self.head = self.tail.next = new_node + elif index == 0: # Insert at the head + new_node.next_node = self.head + assert self.tail is not None # List is not empty, tail exists + self.head = self.tail.next_node = new_node else: - temp = self.head + temp: Node | None = self.head for _ in range(index - 1): - temp = temp.next - new_node.next = temp.next - temp.next = new_node - if index == len(self) - 1: # insert at tail + assert temp is not None + temp = temp.next_node + assert temp is not None + new_node.next_node = temp.next_node + temp.next_node = new_node + if index == len(self) - 1: # Insert at the tail self.tail = new_node - def delete_front(self): + def delete_front(self) -> Any: + """ + Delete and return the data of the node at the front of the Circular Linked List. + Raises: + IndexError: If the list is empty. + """ return self.delete_nth(0) def delete_tail(self) -> Any: + """ + Delete and return the data of the node at the end of the Circular Linked List. + Returns: + Any: The data of the deleted node. + Raises: + IndexError: If the index is out of range. + """ return self.delete_nth(len(self) - 1) def delete_nth(self, index: int = 0) -> Any: + """ + Delete and return the data of the node at the nth pos in Circular Linked List. + Args: + index (int): The index of the node to be deleted. Defaults to 0. + Returns: + Any: The data of the deleted node. + Raises: + IndexError: If the index is out of range. + """ if not 0 <= index < len(self): raise IndexError("list index out of range.") - delete_node = self.head - if self.head == self.tail: # just one node + + assert self.head is not None + assert self.tail is not None + delete_node: Node = self.head + if self.head == self.tail: # Just one node self.head = self.tail = None - elif index == 0: # delete head node - self.tail.next = self.tail.next.next - self.head = self.head.next + elif index == 0: # Delete head node + assert self.tail.next_node is not None + self.tail.next_node = self.tail.next_node.next_node + self.head = self.head.next_node else: - temp = self.head + temp: Node | None = self.head for _ in range(index - 1): - temp = temp.next - delete_node = temp.next - temp.next = temp.next.next - if index == len(self) - 1: # delete at tail + assert temp is not None + temp = temp.next_node + assert temp is not None + assert temp.next_node is not None + delete_node = temp.next_node + temp.next_node = temp.next_node.next_node + if index == len(self) - 1: # Delete at tail self.tail = temp return delete_node.data def is_empty(self) -> bool: + """ + Check if the Circular Linked List is empty. + Returns: + bool: True if the list is empty, False otherwise. + """ return len(self) == 0 def test_circular_linked_list() -> None: """ + Test cases for the CircularLinkedList class. >>> test_circular_linked_list() """ circular_linked_list = CircularLinkedList() @@ -94,25 +160,25 @@ def test_circular_linked_list() -> None: try: circular_linked_list.delete_front() - raise AssertionError() # This should not happen + raise AssertionError # This should not happen except IndexError: assert True # This should happen try: circular_linked_list.delete_tail() - raise AssertionError() # This should not happen + raise AssertionError # This should not happen except IndexError: assert True # This should happen try: circular_linked_list.delete_nth(-1) - raise AssertionError() + raise AssertionError except IndexError: assert True try: circular_linked_list.delete_nth(0) - raise AssertionError() + raise AssertionError except IndexError: assert True @@ -125,7 +191,7 @@ def test_circular_linked_list() -> None: circular_linked_list.insert_tail(6) assert str(circular_linked_list) == "->".join(str(i) for i in range(1, 7)) circular_linked_list.insert_head(0) - assert str(circular_linked_list) == "->".join(str(i) for i in range(0, 7)) + assert str(circular_linked_list) == "->".join(str(i) for i in range(7)) assert circular_linked_list.delete_front() == 0 assert circular_linked_list.delete_tail() == 6 diff --git a/data_structures/linked_list/deque_doubly.py b/data_structures/linked_list/deque_doubly.py index 2b9d70c223c4..e554ead91c5a 100644 --- a/data_structures/linked_list/deque_doubly.py +++ b/data_structures/linked_list/deque_doubly.py @@ -12,7 +12,7 @@ class _DoublyLinkedBase: """A Private class (to be inherited)""" class _Node: - __slots__ = "_prev", "_data", "_next" + __slots__ = "_data", "_next", "_prev" def __init__(self, link_p, element, link_n): self._prev = link_p diff --git a/data_structures/linked_list/doubly_linked_list.py b/data_structures/linked_list/doubly_linked_list.py index 6c81493fff85..bd3445f9f6c5 100644 --- a/data_structures/linked_list/doubly_linked_list.py +++ b/data_structures/linked_list/doubly_linked_list.py @@ -51,7 +51,7 @@ def __len__(self): >>> len(linked_list) == 5 True """ - return len(tuple(iter(self))) + return sum(1 for _ in self) def insert_at_head(self, data): self.insert_at_nth(0, data) @@ -81,7 +81,9 @@ def insert_at_nth(self, index: int, data): .... IndexError: list index out of range """ - if not 0 <= index <= len(self): + length = len(self) + + if not 0 <= index <= length: raise IndexError("list index out of range") new_node = Node(data) if self.head is None: @@ -90,13 +92,13 @@ def insert_at_nth(self, index: int, data): self.head.previous = new_node new_node.next = self.head self.head = new_node - elif index == len(self): + elif index == length: self.tail.next = new_node new_node.previous = self.tail self.tail = new_node else: temp = self.head - for _ in range(0, index): + for _ in range(index): temp = temp.next temp.previous.next = new_node new_node.previous = temp.previous @@ -131,21 +133,23 @@ def delete_at_nth(self, index: int): .... IndexError: list index out of range """ - if not 0 <= index <= len(self) - 1: + length = len(self) + + if not 0 <= index <= length - 1: raise IndexError("list index out of range") delete_node = self.head # default first node - if len(self) == 1: + if length == 1: self.head = self.tail = None elif index == 0: self.head = self.head.next self.head.previous = None - elif index == len(self) - 1: + elif index == length - 1: delete_node = self.tail self.tail = self.tail.previous self.tail.next = None else: temp = self.head - for _ in range(0, index): + for _ in range(index): temp = temp.next delete_node = temp temp.next.previous = temp.previous @@ -194,13 +198,13 @@ def test_doubly_linked_list() -> None: try: linked_list.delete_head() - raise AssertionError() # This should not happen. + raise AssertionError # This should not happen. except IndexError: assert True # This should happen. try: linked_list.delete_tail() - raise AssertionError() # This should not happen. + raise AssertionError # This should not happen. except IndexError: assert True # This should happen. @@ -211,7 +215,7 @@ def test_doubly_linked_list() -> None: linked_list.insert_at_head(0) linked_list.insert_at_tail(11) - assert str(linked_list) == "->".join(str(i) for i in range(0, 12)) + assert str(linked_list) == "->".join(str(i) for i in range(12)) assert linked_list.delete_head() == 0 assert linked_list.delete_at_nth(9) == 10 diff --git a/data_structures/linked_list/doubly_linked_list_two.py b/data_structures/linked_list/doubly_linked_list_two.py index c19309c9f5a7..e993cc5a20af 100644 --- a/data_structures/linked_list/doubly_linked_list_two.py +++ b/data_structures/linked_list/doubly_linked_list_two.py @@ -128,7 +128,7 @@ def insert_at_position(self, position: int, value: int) -> None: while node: if current_position == position: self.insert_before_node(node, new_node) - return None + return current_position += 1 node = node.next self.insert_after_node(self.tail, new_node) diff --git a/data_structures/linked_list/floyds_cycle_detection.py b/data_structures/linked_list/floyds_cycle_detection.py new file mode 100644 index 000000000000..6c3f13760260 --- /dev/null +++ b/data_structures/linked_list/floyds_cycle_detection.py @@ -0,0 +1,150 @@ +""" +Floyd's cycle detection algorithm is a popular algorithm used to detect cycles +in a linked list. It uses two pointers, a slow pointer and a fast pointer, +to traverse the linked list. The slow pointer moves one node at a time while the fast +pointer moves two nodes at a time. If there is a cycle in the linked list, +the fast pointer will eventually catch up to the slow pointer and they will +meet at the same node. If there is no cycle, the fast pointer will reach the end of +the linked list and the algorithm will terminate. + +For more information: https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare +""" + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any, Self + + +@dataclass +class Node: + """ + A class representing a node in a singly linked list. + """ + + data: Any + next_node: Self | None = None + + +@dataclass +class LinkedList: + """ + A class representing a singly linked list. + """ + + head: Node | None = None + + def __iter__(self) -> Iterator: + """ + Iterates through the linked list. + + Returns: + Iterator: An iterator over the linked list. + + Examples: + >>> linked_list = LinkedList() + >>> list(linked_list) + [] + >>> linked_list.add_node(1) + >>> tuple(linked_list) + (1,) + """ + visited = [] + node = self.head + while node: + # Avoid infinite loop in there's a cycle + if node in visited: + return + visited.append(node) + yield node.data + node = node.next_node + + def add_node(self, data: Any) -> None: + """ + Adds a new node to the end of the linked list. + + Args: + data (Any): The data to be stored in the new node. + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.add_node(1) + >>> linked_list.add_node(2) + >>> linked_list.add_node(3) + >>> linked_list.add_node(4) + >>> tuple(linked_list) + (1, 2, 3, 4) + """ + new_node = Node(data) + + if self.head is None: + self.head = new_node + return + + current_node = self.head + while current_node.next_node is not None: + current_node = current_node.next_node + + current_node.next_node = new_node + + def detect_cycle(self) -> bool: + """ + Detects if there is a cycle in the linked list using + Floyd's cycle detection algorithm. + + Returns: + bool: True if there is a cycle, False otherwise. + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.add_node(1) + >>> linked_list.add_node(2) + >>> linked_list.add_node(3) + >>> linked_list.add_node(4) + + >>> linked_list.detect_cycle() + False + + # Create a cycle in the linked list + >>> linked_list.head.next_node.next_node.next_node = linked_list.head.next_node + + >>> linked_list.detect_cycle() + True + """ + if self.head is None: + return False + + slow_pointer: Node | None = self.head + fast_pointer: Node | None = self.head + + while fast_pointer is not None and fast_pointer.next_node is not None: + slow_pointer = slow_pointer.next_node if slow_pointer else None + fast_pointer = fast_pointer.next_node.next_node + if slow_pointer == fast_pointer: + return True + + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + linked_list = LinkedList() + linked_list.add_node(1) + linked_list.add_node(2) + linked_list.add_node(3) + linked_list.add_node(4) + + # Create a cycle in the linked list + # It first checks if the head, next_node, and next_node.next_node attributes of the + # linked list are not None to avoid any potential type errors. + if ( + linked_list.head + and linked_list.head.next_node + and linked_list.head.next_node.next_node + ): + linked_list.head.next_node.next_node.next_node = linked_list.head.next_node + + has_cycle = linked_list.detect_cycle() + print(has_cycle) # Output: True diff --git a/data_structures/linked_list/has_loop.py b/data_structures/linked_list/has_loop.py index bc06ffe150e8..f49e01579adc 100644 --- a/data_structures/linked_list/has_loop.py +++ b/data_structures/linked_list/has_loop.py @@ -14,11 +14,11 @@ def __init__(self, data: Any) -> None: def __iter__(self): node = self - visited = [] + visited = set() while node: if node in visited: raise ContainsLoopError - visited.append(node) + visited.add(node) yield node.data node = node.next_node diff --git a/data_structures/linked_list/is_palindrome.py b/data_structures/linked_list/is_palindrome.py index ec19e99f78c0..da788e3e5045 100644 --- a/data_structures/linked_list/is_palindrome.py +++ b/data_structures/linked_list/is_palindrome.py @@ -1,65 +1,169 @@ -def is_palindrome(head): +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class ListNode: + val: int = 0 + next_node: ListNode | None = None + + +def is_palindrome(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome. + + Args: + head: The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome(None) + True + + >>> is_palindrome(ListNode(1)) + True + + >>> is_palindrome(ListNode(1, ListNode(2))) + False + + >>> is_palindrome(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + """ if not head: return True # split the list to two parts - fast, slow = head.next, head - while fast and fast.next: - fast = fast.next.next - slow = slow.next - second = slow.next - slow.next = None # Don't forget here! But forget still works! + fast: ListNode | None = head.next_node + slow: ListNode | None = head + while fast and fast.next_node: + fast = fast.next_node.next_node + slow = slow.next_node if slow else None + if slow: + # slow will always be defined, + # adding this check to resolve mypy static check + second = slow.next_node + slow.next_node = None # Don't forget here! But forget still works! # reverse the second part - node = None + node: ListNode | None = None while second: - nxt = second.next - second.next = node + nxt = second.next_node + second.next_node = node node = second second = nxt # compare two parts # second part has the same or one less node - while node: + while node and head: if node.val != head.val: return False - node = node.next - head = head.next + node = node.next_node + head = head.next_node return True -def is_palindrome_stack(head): - if not head or not head.next: +def is_palindrome_stack(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome using a stack. + + Args: + head (ListNode): The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome_stack(None) + True + + >>> is_palindrome_stack(ListNode(1)) + True + + >>> is_palindrome_stack(ListNode(1, ListNode(2))) + False + + >>> is_palindrome_stack(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome_stack(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + """ + if not head or not head.next_node: return True # 1. Get the midpoint (slow) - slow = fast = cur = head - while fast and fast.next: - fast, slow = fast.next.next, slow.next - - # 2. Push the second half into the stack - stack = [slow.val] - while slow.next: - slow = slow.next - stack.append(slow.val) - - # 3. Comparison - while stack: - if stack.pop() != cur.val: - return False - cur = cur.next + slow: ListNode | None = head + fast: ListNode | None = head + while fast and fast.next_node: + fast = fast.next_node.next_node + slow = slow.next_node if slow else None + + # slow will always be defined, + # adding this check to resolve mypy static check + if slow: + stack = [slow.val] + + # 2. Push the second half into the stack + while slow.next_node: + slow = slow.next_node + stack.append(slow.val) + + # 3. Comparison + cur: ListNode | None = head + while stack and cur: + if stack.pop() != cur.val: + return False + cur = cur.next_node return True -def is_palindrome_dict(head): - if not head or not head.next: +def is_palindrome_dict(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome using a dictionary. + + Args: + head (ListNode): The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome_dict(None) + True + + >>> is_palindrome_dict(ListNode(1)) + True + + >>> is_palindrome_dict(ListNode(1, ListNode(2))) + False + + >>> is_palindrome_dict(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome_dict(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + + >>> is_palindrome_dict( + ... ListNode( + ... 1, ListNode(2, ListNode(1, ListNode(3, ListNode(2, ListNode(1))))) + ... ) + ... ) + False + """ + if not head or not head.next_node: return True - d = {} + d: dict[int, list[int]] = {} pos = 0 while head: if head.val in d: d[head.val].append(pos) else: d[head.val] = [pos] - head = head.next + head = head.next_node pos += 1 checksum = pos - 1 middle = 0 @@ -67,11 +171,15 @@ def is_palindrome_dict(head): if len(v) % 2 != 0: middle += 1 else: - step = 0 - for i in range(0, len(v)): + for step, i in enumerate(range(len(v))): if v[i] + v[len(v) - 1 - step] != checksum: return False - step += 1 if middle > 1: return False return True + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/merge_two_lists.py b/data_structures/linked_list/merge_two_lists.py index 61e2412aa7fd..e47dbdadcf39 100644 --- a/data_structures/linked_list/merge_two_lists.py +++ b/data_structures/linked_list/merge_two_lists.py @@ -1,6 +1,7 @@ """ Algorithm that merges two sorted linked lists into one sorted linked list. """ + from __future__ import annotations from collections.abc import Iterable, Iterator @@ -44,7 +45,7 @@ def __len__(self) -> int: >>> len(SortedLinkedList(test_data_odd)) 8 """ - return len(tuple(iter(self))) + return sum(1 for _ in self) def __str__(self) -> str: """ diff --git a/data_structures/linked_list/print_reverse.py b/data_structures/linked_list/print_reverse.py index f83d5607ffdd..a023745dee69 100644 --- a/data_structures/linked_list/print_reverse.py +++ b/data_structures/linked_list/print_reverse.py @@ -1,22 +1,91 @@ from __future__ import annotations +from collections.abc import Iterable, Iterator +from dataclasses import dataclass + +@dataclass class Node: - def __init__(self, data=None): - self.data = data - self.next = None + data: int + next_node: Node | None = None + + +class LinkedList: + """A class to represent a Linked List. + Use a tail pointer to speed up the append() operation. + """ + + def __init__(self) -> None: + """Initialize a LinkedList with the head node set to None. + >>> linked_list = LinkedList() + >>> (linked_list.head, linked_list.tail) + (None, None) + """ + self.head: Node | None = None + self.tail: Node | None = None # Speeds up the append() operation + + def __iter__(self) -> Iterator[int]: + """Iterate the LinkedList yielding each Node's data. + >>> linked_list = LinkedList() + >>> items = (1, 2, 3, 4, 5) + >>> linked_list.extend(items) + >>> tuple(linked_list) == items + True + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __repr__(self) -> str: + """Returns a string representation of the LinkedList. + >>> linked_list = LinkedList() + >>> str(linked_list) + '' + >>> linked_list.append(1) + >>> str(linked_list) + '1' + >>> linked_list.extend([2, 3, 4, 5]) + >>> str(linked_list) + '1 -> 2 -> 3 -> 4 -> 5' + """ + return " -> ".join([str(data) for data in self]) - def __repr__(self): - """Returns a visual representation of the node and all its following nodes.""" - string_rep = [] - temp = self - while temp: - string_rep.append(f"{temp.data}") - temp = temp.next - return "->".join(string_rep) + def append(self, data: int) -> None: + """Appends a new node with the given data to the end of the LinkedList. + >>> linked_list = LinkedList() + >>> str(linked_list) + '' + >>> linked_list.append(1) + >>> str(linked_list) + '1' + >>> linked_list.append(2) + >>> str(linked_list) + '1 -> 2' + """ + if self.tail: + self.tail.next_node = self.tail = Node(data) + else: + self.head = self.tail = Node(data) + def extend(self, items: Iterable[int]) -> None: + """Appends each item to the end of the LinkedList. + >>> linked_list = LinkedList() + >>> linked_list.extend([]) + >>> str(linked_list) + '' + >>> linked_list.extend([1, 2]) + >>> str(linked_list) + '1 -> 2' + >>> linked_list.extend([3,4]) + >>> str(linked_list) + '1 -> 2 -> 3 -> 4' + """ + for item in items: + self.append(item) -def make_linked_list(elements_list: list): + +def make_linked_list(elements_list: Iterable[int]) -> LinkedList: """Creates a Linked List from the elements of the given sequence (list/tuple) and returns the head of the Linked List. >>> make_linked_list([]) @@ -28,43 +97,30 @@ def make_linked_list(elements_list: list): >>> make_linked_list(['abc']) abc >>> make_linked_list([7, 25]) - 7->25 + 7 -> 25 """ if not elements_list: raise Exception("The Elements List is empty") - current = head = Node(elements_list[0]) - for i in range(1, len(elements_list)): - current.next = Node(elements_list[i]) - current = current.next - return head + linked_list = LinkedList() + linked_list.extend(elements_list) + return linked_list -def print_reverse(head_node: Node) -> None: +def in_reverse(linked_list: LinkedList) -> str: """Prints the elements of the given Linked List in reverse order - >>> print_reverse([]) - >>> linked_list = make_linked_list([69, 88, 73]) - >>> print_reverse(linked_list) - 73 - 88 - 69 + >>> in_reverse(LinkedList()) + '' + >>> in_reverse(make_linked_list([69, 88, 73])) + '73 <- 88 <- 69' """ - if head_node is not None and isinstance(head_node, Node): - print_reverse(head_node.next) - print(head_node.data) + return " <- ".join(str(line) for line in reversed(tuple(linked_list))) -def main(): +if __name__ == "__main__": from doctest import testmod testmod() - - linked_list = make_linked_list([14, 52, 14, 12, 43]) - print("Linked List:") - print(linked_list) - print("Elements in Reverse:") - print_reverse(linked_list) - - -if __name__ == "__main__": - main() + linked_list = make_linked_list((14, 52, 14, 12, 43)) + print(f"Linked List: {linked_list}") + print(f"Reverse List: {in_reverse(linked_list)}") diff --git a/data_structures/linked_list/reverse_k_group.py b/data_structures/linked_list/reverse_k_group.py new file mode 100644 index 000000000000..5fc45491a540 --- /dev/null +++ b/data_structures/linked_list/reverse_k_group.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + next_node: Node | None = None + + +class LinkedList: + def __init__(self, ints: Iterable[int]) -> None: + self.head: Node | None = None + for i in ints: + self.append(i) + + def __iter__(self) -> Iterator[int]: + """ + >>> ints = [] + >>> list(LinkedList(ints)) == ints + True + >>> ints = tuple(range(5)) + >>> tuple(LinkedList(ints)) == ints + True + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + >>> for i in range(3): + ... len(LinkedList(range(i))) == i + True + True + True + >>> len(LinkedList("abcdefgh")) + 8 + """ + return sum(1 for _ in self) + + def __str__(self) -> str: + """ + >>> str(LinkedList([])) + '' + >>> str(LinkedList(range(5))) + '0 -> 1 -> 2 -> 3 -> 4' + """ + return " -> ".join([str(node) for node in self]) + + def append(self, data: int) -> None: + """ + >>> ll = LinkedList([1, 2]) + >>> tuple(ll) + (1, 2) + >>> ll.append(3) + >>> tuple(ll) + (1, 2, 3) + >>> ll.append(4) + >>> tuple(ll) + (1, 2, 3, 4) + >>> len(ll) + 4 + """ + if not self.head: + self.head = Node(data) + return + node = self.head + while node.next_node: + node = node.next_node + node.next_node = Node(data) + + def reverse_k_nodes(self, group_size: int) -> None: + """ + reverse nodes within groups of size k + >>> ll = LinkedList([1, 2, 3, 4, 5]) + >>> ll.reverse_k_nodes(2) + >>> tuple(ll) + (2, 1, 4, 3, 5) + >>> str(ll) + '2 -> 1 -> 4 -> 3 -> 5' + """ + if self.head is None or self.head.next_node is None: + return + + length = len(self) + dummy_head = Node(0) + dummy_head.next_node = self.head + previous_node = dummy_head + + while length >= group_size: + current_node = previous_node.next_node + assert current_node + next_node = current_node.next_node + for _ in range(1, group_size): + assert next_node, current_node + current_node.next_node = next_node.next_node + assert previous_node + next_node.next_node = previous_node.next_node + previous_node.next_node = next_node + next_node = current_node.next_node + previous_node = current_node + length -= group_size + self.head = dummy_head.next_node + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + ll = LinkedList([1, 2, 3, 4, 5]) + print(f"Original Linked List: {ll}") + k = 2 + ll.reverse_k_nodes(k) + print(f"After reversing groups of size {k}: {ll}") diff --git a/data_structures/linked_list/rotate_to_the_right.py b/data_structures/linked_list/rotate_to_the_right.py new file mode 100644 index 000000000000..6b1c54f4be4d --- /dev/null +++ b/data_structures/linked_list/rotate_to_the_right.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + next_node: Node | None = None + + +def print_linked_list(head: Node | None) -> None: + """ + Print the entire linked list iteratively. + + This function prints the elements of a linked list separated by '->'. + + Parameters: + head (Node | None): The head of the linked list to be printed, + or None if the linked list is empty. + + >>> head = insert_node(None, 0) + >>> head = insert_node(head, 2) + >>> head = insert_node(head, 1) + >>> print_linked_list(head) + 0->2->1 + >>> head = insert_node(head, 4) + >>> head = insert_node(head, 5) + >>> print_linked_list(head) + 0->2->1->4->5 + """ + if head is None: + return + while head.next_node is not None: + print(head.data, end="->") + head = head.next_node + print(head.data) + + +def insert_node(head: Node | None, data: int) -> Node: + """ + Insert a new node at the end of a linked list and return the new head. + + Parameters: + head (Node | None): The head of the linked list. + data (int): The data to be inserted into the new node. + + Returns: + Node: The new head of the linked list. + + >>> head = insert_node(None, 10) + >>> head = insert_node(head, 9) + >>> head = insert_node(head, 8) + >>> print_linked_list(head) + 10->9->8 + """ + new_node = Node(data) + # If the linked list is empty, the new_node becomes the head + if head is None: + return new_node + + temp_node = head + while temp_node.next_node: + temp_node = temp_node.next_node + + temp_node.next_node = new_node + return head + + +def rotate_to_the_right(head: Node, places: int) -> Node: + """ + Rotate a linked list to the right by places times. + + Parameters: + head: The head of the linked list. + places: The number of places to rotate. + + Returns: + Node: The head of the rotated linked list. + + >>> rotate_to_the_right(None, places=1) + Traceback (most recent call last): + ... + ValueError: The linked list is empty. + >>> head = insert_node(None, 1) + >>> rotate_to_the_right(head, places=1) == head + True + >>> head = insert_node(None, 1) + >>> head = insert_node(head, 2) + >>> head = insert_node(head, 3) + >>> head = insert_node(head, 4) + >>> head = insert_node(head, 5) + >>> new_head = rotate_to_the_right(head, places=2) + >>> print_linked_list(new_head) + 4->5->1->2->3 + """ + # Check if the list is empty or has only one element + if not head: + raise ValueError("The linked list is empty.") + + if head.next_node is None: + return head + + # Calculate the length of the linked list + length = 1 + temp_node = head + while temp_node.next_node is not None: + length += 1 + temp_node = temp_node.next_node + + # Adjust the value of places to avoid places longer than the list. + places %= length + + if places == 0: + return head # As no rotation is needed. + + # Find the new head position after rotation. + new_head_index = length - places + + # Traverse to the new head position + temp_node = head + for _ in range(new_head_index - 1): + assert temp_node.next_node + temp_node = temp_node.next_node + + # Update pointers to perform rotation + assert temp_node.next_node + new_head = temp_node.next_node + temp_node.next_node = None + temp_node = new_head + while temp_node.next_node: + temp_node = temp_node.next_node + temp_node.next_node = head + + assert new_head + return new_head + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + head = insert_node(None, 5) + head = insert_node(head, 1) + head = insert_node(head, 2) + head = insert_node(head, 4) + head = insert_node(head, 3) + + print("Original list: ", end="") + print_linked_list(head) + + places = 3 + new_head = rotate_to_the_right(head, places) + + print(f"After {places} iterations: ", end="") + print_linked_list(new_head) diff --git a/data_structures/linked_list/singly_linked_list.py b/data_structures/linked_list/singly_linked_list.py index 3e52c7e43cf5..2c6713a47ad9 100644 --- a/data_structures/linked_list/singly_linked_list.py +++ b/data_structures/linked_list/singly_linked_list.py @@ -1,27 +1,38 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass from typing import Any +@dataclass class Node: - def __init__(self, data: Any): - """ - Create and initialize Node class instance. - >>> Node(20) - Node(20) - >>> Node("Hello, world!") - Node(Hello, world!) - >>> Node(None) - Node(None) - >>> Node(True) - Node(True) - """ - self.data = data - self.next = None + """ + Create and initialize Node class instance. + >>> Node(20) + Node(20) + >>> Node("Hello, world!") + Node(Hello, world!) + >>> Node(None) + Node(None) + >>> Node(True) + Node(True) + """ + + data: Any + next_node: Node | None = None def __repr__(self) -> str: """ Get the string representation of this node. >>> Node(10).__repr__() 'Node(10)' + >>> repr(Node(10)) + 'Node(10)' + >>> str(Node(10)) + 'Node(10)' + >>> Node(10) + Node(10) """ return f"Node({self.data})" @@ -31,10 +42,12 @@ def __init__(self): """ Create and initialize LinkedList class instance. >>> linked_list = LinkedList() + >>> linked_list.head is None + True """ self.head = None - def __iter__(self) -> Any: + def __iter__(self) -> Iterator[Any]: """ This function is intended for iterators to access and iterate through data inside linked list. @@ -51,7 +64,7 @@ def __iter__(self) -> Any: node = self.head while node: yield node.data - node = node.next + node = node.next_node def __len__(self) -> int: """ @@ -72,7 +85,7 @@ def __len__(self) -> int: >>> len(linked_list) 0 """ - return len(tuple(iter(self))) + return sum(1 for _ in self) def __repr__(self) -> str: """ @@ -81,9 +94,16 @@ def __repr__(self) -> str: >>> linked_list.insert_tail(1) >>> linked_list.insert_tail(3) >>> linked_list.__repr__() - '1->3' + '1 -> 3' + >>> repr(linked_list) + '1 -> 3' + >>> str(linked_list) + '1 -> 3' + >>> linked_list.insert_tail(5) + >>> f"{linked_list}" + '1 -> 3 -> 5' """ - return "->".join([str(item) for item in self]) + return " -> ".join([str(item) for item in self]) def __getitem__(self, index: int) -> Any: """ @@ -107,6 +127,7 @@ def __getitem__(self, index: int) -> Any: for i, node in enumerate(self): if i == index: return node + return None # Used to change the data of a particular node def __setitem__(self, index: int, data: Any) -> None: @@ -133,7 +154,7 @@ def __setitem__(self, index: int, data: Any) -> None: raise ValueError("list index out of range.") current = self.head for _ in range(index): - current = current.next + current = current.next_node current.data = data def insert_tail(self, data: Any) -> None: @@ -145,10 +166,10 @@ def insert_tail(self, data: Any) -> None: tail >>> linked_list.insert_tail("tail_2") >>> linked_list - tail->tail_2 + tail -> tail_2 >>> linked_list.insert_tail("tail_3") >>> linked_list - tail->tail_2->tail_3 + tail -> tail_2 -> tail_3 """ self.insert_nth(len(self), data) @@ -161,10 +182,10 @@ def insert_head(self, data: Any) -> None: head >>> linked_list.insert_head("head_2") >>> linked_list - head_2->head + head_2 -> head >>> linked_list.insert_head("head_3") >>> linked_list - head_3->head_2->head + head_3 -> head_2 -> head """ self.insert_nth(0, data) @@ -176,13 +197,13 @@ def insert_nth(self, index: int, data: Any) -> None: >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third >>> linked_list.insert_nth(1, "fourth") >>> linked_list - first->fourth->second->third + first -> fourth -> second -> third >>> linked_list.insert_nth(3, "fifth") >>> linked_list - first->fourth->second->fifth->third + first -> fourth -> second -> fifth -> third """ if not 0 <= index <= len(self): raise IndexError("list index out of range") @@ -190,14 +211,14 @@ def insert_nth(self, index: int, data: Any) -> None: if self.head is None: self.head = new_node elif index == 0: - new_node.next = self.head # link new_node to head + new_node.next_node = self.head # link new_node to head self.head = new_node else: temp = self.head for _ in range(index - 1): - temp = temp.next - new_node.next = temp.next - temp.next = new_node + temp = temp.next_node + new_node.next_node = temp.next_node + temp.next_node = new_node def print_list(self) -> None: # print every node data """ @@ -207,7 +228,7 @@ def print_list(self) -> None: # print every node data >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third """ print(self) @@ -220,11 +241,11 @@ def delete_head(self) -> Any: >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third >>> linked_list.delete_head() 'first' >>> linked_list - second->third + second -> third >>> linked_list.delete_head() 'second' >>> linked_list @@ -247,11 +268,11 @@ def delete_tail(self) -> Any: # delete from tail >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third >>> linked_list.delete_tail() 'third' >>> linked_list - first->second + first -> second >>> linked_list.delete_tail() 'second' >>> linked_list @@ -274,11 +295,11 @@ def delete_nth(self, index: int = 0) -> Any: >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third >>> linked_list.delete_nth(1) # delete middle 'second' >>> linked_list - first->third + first -> third >>> linked_list.delete_nth(5) # this raises error Traceback (most recent call last): ... @@ -292,13 +313,13 @@ def delete_nth(self, index: int = 0) -> Any: raise IndexError("List index out of range.") delete_node = self.head # default first node if index == 0: - self.head = self.head.next + self.head = self.head.next_node else: temp = self.head for _ in range(index - 1): - temp = temp.next - delete_node = temp.next - temp.next = temp.next.next + temp = temp.next_node + delete_node = temp.next_node + temp.next_node = temp.next_node.next_node return delete_node.data def is_empty(self) -> bool: @@ -321,22 +342,22 @@ def reverse(self) -> None: >>> linked_list.insert_tail("second") >>> linked_list.insert_tail("third") >>> linked_list - first->second->third + first -> second -> third >>> linked_list.reverse() >>> linked_list - third->second->first + third -> second -> first """ prev = None current = self.head while current: # Store the current node's next node. - next_node = current.next - # Make the current node's next point backwards - current.next = prev + next_node = current.next_node + # Make the current node's next_node point backwards + current.next_node = prev # Make the previous node be the current node prev = current - # Make the current node the next node (to progress iteration) + # Make the current node the next_node node (to progress iteration) current = next_node # Return prev in order to put the head at the end self.head = prev @@ -352,39 +373,39 @@ def test_singly_linked_list() -> None: try: linked_list.delete_head() - raise AssertionError() # This should not happen. + raise AssertionError # This should not happen. except IndexError: assert True # This should happen. try: linked_list.delete_tail() - raise AssertionError() # This should not happen. + raise AssertionError # This should not happen. except IndexError: assert True # This should happen. for i in range(10): assert len(linked_list) == i linked_list.insert_nth(i, i + 1) - assert str(linked_list) == "->".join(str(i) for i in range(1, 11)) + assert str(linked_list) == " -> ".join(str(i) for i in range(1, 11)) linked_list.insert_head(0) linked_list.insert_tail(11) - assert str(linked_list) == "->".join(str(i) for i in range(0, 12)) + assert str(linked_list) == " -> ".join(str(i) for i in range(12)) assert linked_list.delete_head() == 0 assert linked_list.delete_nth(9) == 10 assert linked_list.delete_tail() == 11 assert len(linked_list) == 9 - assert str(linked_list) == "->".join(str(i) for i in range(1, 10)) + assert str(linked_list) == " -> ".join(str(i) for i in range(1, 10)) - assert all(linked_list[i] == i + 1 for i in range(0, 9)) is True + assert all(linked_list[i] == i + 1 for i in range(9)) is True - for i in range(0, 9): + for i in range(9): linked_list[i] = -i - assert all(linked_list[i] == -i for i in range(0, 9)) is True + assert all(linked_list[i] == -i for i in range(9)) is True linked_list.reverse() - assert str(linked_list) == "->".join(str(i) for i in range(-8, 1)) + assert str(linked_list) == " -> ".join(str(i) for i in range(-8, 1)) def test_singly_linked_list_2() -> None: @@ -416,56 +437,57 @@ def test_singly_linked_list_2() -> None: # Check if it's empty or not assert linked_list.is_empty() is False assert ( - str(linked_list) == "-9->100->Node(77345112)->dlrow olleH->7->5555->0->" - "-192.55555->Hello, world!->77.9->Node(10)->None->None->12.2" + str(linked_list) + == "-9 -> 100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> " + "0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None -> 12.2" ) # Delete the head result = linked_list.delete_head() assert result == -9 assert ( - str(linked_list) == "100->Node(77345112)->dlrow olleH->7->5555->0->-192.55555->" - "Hello, world!->77.9->Node(10)->None->None->12.2" + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None -> 12.2" ) # Delete the tail result = linked_list.delete_tail() assert result == 12.2 assert ( - str(linked_list) == "100->Node(77345112)->dlrow olleH->7->5555->0->-192.55555->" - "Hello, world!->77.9->Node(10)->None->None" + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None" ) # Delete a node in specific location in linked list result = linked_list.delete_nth(10) assert result is None assert ( - str(linked_list) == "100->Node(77345112)->dlrow olleH->7->5555->0->-192.55555->" - "Hello, world!->77.9->Node(10)->None" + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None" ) # Add a Node instance to its head linked_list.insert_head(Node("Hello again, world!")) assert ( str(linked_list) - == "Node(Hello again, world!)->100->Node(77345112)->dlrow olleH->" - "7->5555->0->-192.55555->Hello, world!->77.9->Node(10)->None" + == "Node(Hello again, world!) -> 100 -> Node(77345112) -> dlrow olleH -> " + "7 -> 5555 -> 0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None" ) # Add None to its tail linked_list.insert_tail(None) assert ( str(linked_list) - == "Node(Hello again, world!)->100->Node(77345112)->dlrow olleH->" - "7->5555->0->-192.55555->Hello, world!->77.9->Node(10)->None->None" + == "Node(Hello again, world!) -> 100 -> Node(77345112) -> dlrow olleH -> 7 -> " + "5555 -> 0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None" ) # Reverse the linked list linked_list.reverse() assert ( str(linked_list) - == "None->None->Node(10)->77.9->Hello, world!->-192.55555->0->5555->" - "7->dlrow olleH->Node(77345112)->100->Node(Hello again, world!)" + == "None -> None -> Node(10) -> 77.9 -> Hello, world! -> -192.55555 -> 0 -> " + "5555 -> 7 -> dlrow olleH -> Node(77345112) -> 100 -> Node(Hello again, world!)" ) diff --git a/data_structures/linked_list/skip_list.py b/data_structures/linked_list/skip_list.py index 96b0db7c896b..13e9a94a8698 100644 --- a/data_structures/linked_list/skip_list.py +++ b/data_structures/linked_list/skip_list.py @@ -2,8 +2,10 @@ Based on "Skip Lists: A Probabilistic Alternative to Balanced Trees" by William Pugh https://epaperpress.com/sortsearch/download/skiplist.pdf """ + from __future__ import annotations +from itertools import pairwise from random import random from typing import Generic, TypeVar @@ -388,10 +390,7 @@ def traverse_keys(node): def test_iter_always_yields_sorted_values(): def is_sorted(lst): - for item, next_item in zip(lst, lst[1:]): - if next_item < item: - return False - return True + return all(next_item >= item for item, next_item in pairwise(lst)) skip_list = SkipList() for i in range(10): diff --git a/data_structures/linked_list/swap_nodes.py b/data_structures/linked_list/swap_nodes.py index 3f825756b3d2..d66512087d2d 100644 --- a/data_structures/linked_list/swap_nodes.py +++ b/data_structures/linked_list/swap_nodes.py @@ -1,55 +1,148 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass from typing import Any +@dataclass class Node: - def __init__(self, data: Any): - self.data = data - self.next = None + data: Any + next_node: Node | None = None +@dataclass class LinkedList: - def __init__(self): - self.head = None - - def print_list(self): - temp = self.head - while temp is not None: - print(temp.data, end=" ") - temp = temp.next - print() - - # adding nodes - def push(self, new_data: Any): + head: Node | None = None + + def __iter__(self) -> Iterator: + """ + >>> linked_list = LinkedList() + >>> list(linked_list) + [] + >>> linked_list.push(0) + >>> tuple(linked_list) + (0,) + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.push(0) + >>> len(linked_list) + 1 + """ + return sum(1 for _ in self) + + def push(self, new_data: Any) -> None: + """ + Add a new node with the given data to the beginning of the Linked List. + + Args: + new_data (Any): The data to be added to the new node. + + Returns: + None + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.push(5) + >>> linked_list.push(4) + >>> linked_list.push(3) + >>> linked_list.push(2) + >>> linked_list.push(1) + >>> list(linked_list) + [1, 2, 3, 4, 5] + """ new_node = Node(new_data) - new_node.next = self.head + new_node.next_node = self.head self.head = new_node - # swapping nodes - def swap_nodes(self, node_data_1, node_data_2): - if node_data_1 == node_data_2: - return - else: - node_1 = self.head - while node_1 is not None and node_1.data != node_data_1: - node_1 = node_1.next + def swap_nodes(self, node_data_1: Any, node_data_2: Any) -> None: + """ + Swap the positions of two nodes in the Linked List based on their data values. + + Args: + node_data_1: Data value of the first node to be swapped. + node_data_2: Data value of the second node to be swapped. - node_2 = self.head - while node_2 is not None and node_2.data != node_data_2: - node_2 = node_2.next - if node_1 is None or node_2 is None: - return + Note: + If either of the specified data values isn't found then, no swapping occurs. - node_1.data, node_2.data = node_2.data, node_1.data + Examples: + When both values are present in a linked list. + >>> linked_list = LinkedList() + >>> linked_list.push(5) + >>> linked_list.push(4) + >>> linked_list.push(3) + >>> linked_list.push(2) + >>> linked_list.push(1) + >>> list(linked_list) + [1, 2, 3, 4, 5] + >>> linked_list.swap_nodes(1, 5) + >>> tuple(linked_list) + (5, 2, 3, 4, 1) + + When one value is present and the other isn't in the linked list. + >>> second_list = LinkedList() + >>> second_list.push(6) + >>> second_list.push(7) + >>> second_list.push(8) + >>> second_list.push(9) + >>> second_list.swap_nodes(1, 6) is None + True + + When both values are absent in the linked list. + >>> second_list = LinkedList() + >>> second_list.push(10) + >>> second_list.push(9) + >>> second_list.push(8) + >>> second_list.push(7) + >>> second_list.swap_nodes(1, 3) is None + True + + When linkedlist is empty. + >>> second_list = LinkedList() + >>> second_list.swap_nodes(1, 3) is None + True + + Returns: + None + """ + if node_data_1 == node_data_2: + return + + node_1 = self.head + while node_1 and node_1.data != node_data_1: + node_1 = node_1.next_node + node_2 = self.head + while node_2 and node_2.data != node_data_2: + node_2 = node_2.next_node + if node_1 is None or node_2 is None: + return + # Swap the data values of the two nodes + node_1.data, node_2.data = node_2.data, node_1.data if __name__ == "__main__": - ll = LinkedList() - for i in range(5, 0, -1): - ll.push(i) + """ + Python script that outputs the swap of nodes in a linked list. + """ + from doctest import testmod - ll.print_list() + testmod() + linked_list = LinkedList() + for i in range(5, 0, -1): + linked_list.push(i) - ll.swap_nodes(1, 4) - print("After swapping") - ll.print_list() + print(f"Original Linked List: {list(linked_list)}") + linked_list.swap_nodes(1, 4) + print(f"Modified Linked List: {list(linked_list)}") + print("After swapping the nodes whose data is 1 and 4.") diff --git a/data_structures/queue/circular_queue.py b/data_structures/queue/circular_queue.py index 93a6ef805c7c..f2fb4c01e467 100644 --- a/data_structures/queue/circular_queue.py +++ b/data_structures/queue/circular_queue.py @@ -25,6 +25,7 @@ def __len__(self) -> int: def is_empty(self) -> bool: """ + Checks whether the queue is empty or not >>> cq = CircularQueue(5) >>> cq.is_empty() True @@ -35,6 +36,7 @@ def is_empty(self) -> bool: def first(self): """ + Returns the first element of the queue >>> cq = CircularQueue(5) >>> cq.first() False @@ -45,7 +47,8 @@ def first(self): def enqueue(self, data): """ - This function insert an element in the queue using self.rear value as an index + This function inserts an element at the end of the queue using self.rear value + as an index. >>> cq = CircularQueue(5) >>> cq.enqueue("A") # doctest: +ELLIPSIS >> cq = CircularQueue(5) >>> cq.dequeue() Traceback (most recent call last): diff --git a/data_structures/queue/circular_queue_linked_list.py b/data_structures/queue/circular_queue_linked_list.py index e8c2b8bffc06..da8629678e52 100644 --- a/data_structures/queue/circular_queue_linked_list.py +++ b/data_structures/queue/circular_queue_linked_list.py @@ -39,7 +39,7 @@ def create_linked_list(self, initial_capacity: int) -> None: def is_empty(self) -> bool: """ - Checks where the queue is empty or not + Checks whether the queue is empty or not >>> cq = CircularQueueLinkedList() >>> cq.is_empty() True @@ -127,7 +127,7 @@ def dequeue(self) -> Any: """ self.check_can_perform_operation() if self.rear is None or self.front is None: - return + return None if self.front == self.rear: data = self.front.data self.front.data = None diff --git a/data_structures/queue/double_ended_queue.py b/data_structures/queue/double_ended_queue.py index 637b7f62fd2c..c28d46c65168 100644 --- a/data_structures/queue/double_ended_queue.py +++ b/data_structures/queue/double_ended_queue.py @@ -1,6 +1,7 @@ """ Implementation of double ended queue. """ + from __future__ import annotations from collections.abc import Iterable @@ -32,7 +33,7 @@ class Deque: the number of nodes """ - __slots__ = ["_front", "_back", "_len"] + __slots__ = ("_back", "_front", "_len") @dataclass class _Node: @@ -54,7 +55,7 @@ class _Iterator: the current node of the iteration. """ - __slots__ = ["_cur"] + __slots__ = ("_cur",) def __init__(self, cur: Deque._Node | None) -> None: self._cur = cur @@ -242,12 +243,20 @@ def pop(self) -> Any: Removes the last element of the deque and returns it. Time complexity: O(1) @returns topop.val: the value of the node to pop. - >>> our_deque = Deque([1, 2, 3, 15182]) - >>> our_popped = our_deque.pop() - >>> our_popped + >>> our_deque1 = Deque([1]) + >>> our_popped1 = our_deque1.pop() + >>> our_popped1 + 1 + >>> our_deque1 + [] + + >>> our_deque2 = Deque([1, 2, 3, 15182]) + >>> our_popped2 = our_deque2.pop() + >>> our_popped2 15182 - >>> our_deque + >>> our_deque2 [1, 2, 3] + >>> from collections import deque >>> deque_collections = deque([1, 2, 3, 15182]) >>> collections_popped = deque_collections.pop() @@ -255,18 +264,24 @@ def pop(self) -> Any: 15182 >>> deque_collections deque([1, 2, 3]) - >>> list(our_deque) == list(deque_collections) + >>> list(our_deque2) == list(deque_collections) True - >>> our_popped == collections_popped + >>> our_popped2 == collections_popped True """ # make sure the deque has elements to pop assert not self.is_empty(), "Deque is empty." topop = self._back - self._back = self._back.prev_node # set new back - # drop the last node - python will deallocate memory automatically - self._back.next_node = None + # if only one element in the queue: point the front and back to None + # else remove one element from back + if self._front == self._back: + self._front = None + self._back = None + else: + self._back = self._back.prev_node # set new back + # drop the last node, python will deallocate memory automatically + self._back.next_node = None self._len -= 1 @@ -277,11 +292,17 @@ def popleft(self) -> Any: Removes the first element of the deque and returns it. Time complexity: O(1) @returns topop.val: the value of the node to pop. - >>> our_deque = Deque([15182, 1, 2, 3]) - >>> our_popped = our_deque.popleft() - >>> our_popped + >>> our_deque1 = Deque([1]) + >>> our_popped1 = our_deque1.pop() + >>> our_popped1 + 1 + >>> our_deque1 + [] + >>> our_deque2 = Deque([15182, 1, 2, 3]) + >>> our_popped2 = our_deque2.popleft() + >>> our_popped2 15182 - >>> our_deque + >>> our_deque2 [1, 2, 3] >>> from collections import deque >>> deque_collections = deque([15182, 1, 2, 3]) @@ -290,17 +311,23 @@ def popleft(self) -> Any: 15182 >>> deque_collections deque([1, 2, 3]) - >>> list(our_deque) == list(deque_collections) + >>> list(our_deque2) == list(deque_collections) True - >>> our_popped == collections_popped + >>> our_popped2 == collections_popped True """ # make sure the deque has elements to pop assert not self.is_empty(), "Deque is empty." topop = self._front - self._front = self._front.next_node # set new front and drop the first node - self._front.prev_node = None + # if only one element in the queue: point the front and back to None + # else remove one element from front + if self._front == self._back: + self._front = None + self._back = None + else: + self._front = self._front.next_node # set new front and drop the first node + self._front.prev_node = None self._len -= 1 @@ -432,3 +459,5 @@ def __repr__(self) -> str: import doctest doctest.testmod() + dq = Deque([3]) + dq.pop() diff --git a/data_structures/queue/linked_queue.py b/data_structures/queue/linked_queue.py index 3af97d28e4f7..80f6d309af9a 100644 --- a/data_structures/queue/linked_queue.py +++ b/data_structures/queue/linked_queue.py @@ -1,4 +1,5 @@ -""" A Queue using a linked list like structure """ +"""A Queue using a linked list like structure""" + from __future__ import annotations from collections.abc import Iterator diff --git a/data_structures/queue/queue_by_list.py b/data_structures/queue/queue_by_list.py new file mode 100644 index 000000000000..4b05be9fd08e --- /dev/null +++ b/data_structures/queue/queue_by_list.py @@ -0,0 +1,141 @@ +"""Queue represented by a Python list""" + +from collections.abc import Iterable +from typing import Generic, TypeVar + +_T = TypeVar("_T") + + +class QueueByList(Generic[_T]): + def __init__(self, iterable: Iterable[_T] | None = None) -> None: + """ + >>> QueueByList() + Queue(()) + >>> QueueByList([10, 20, 30]) + Queue((10, 20, 30)) + >>> QueueByList((i**2 for i in range(1, 4))) + Queue((1, 4, 9)) + """ + self.entries: list[_T] = list(iterable or []) + + def __len__(self) -> int: + """ + >>> len(QueueByList()) + 0 + >>> from string import ascii_lowercase + >>> len(QueueByList(ascii_lowercase)) + 26 + >>> queue = QueueByList() + >>> for i in range(1, 11): + ... queue.put(i) + >>> len(queue) + 10 + >>> for i in range(2): + ... queue.get() + 1 + 2 + >>> len(queue) + 8 + """ + + return len(self.entries) + + def __repr__(self) -> str: + """ + >>> queue = QueueByList() + >>> queue + Queue(()) + >>> str(queue) + 'Queue(())' + >>> queue.put(10) + >>> queue + Queue((10,)) + >>> queue.put(20) + >>> queue.put(30) + >>> queue + Queue((10, 20, 30)) + """ + + return f"Queue({tuple(self.entries)})" + + def put(self, item: _T) -> None: + """Put `item` to the Queue + + >>> queue = QueueByList() + >>> queue.put(10) + >>> queue.put(20) + >>> len(queue) + 2 + >>> queue + Queue((10, 20)) + """ + + self.entries.append(item) + + def get(self) -> _T: + """ + Get `item` from the Queue + + >>> queue = QueueByList((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.put(40) + >>> queue.get() + 20 + >>> queue.get() + 30 + >>> len(queue) + 1 + >>> queue.get() + 40 + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: Queue is empty + """ + + if not self.entries: + raise IndexError("Queue is empty") + return self.entries.pop(0) + + def rotate(self, rotation: int) -> None: + """Rotate the items of the Queue `rotation` times + + >>> queue = QueueByList([10, 20, 30, 40]) + >>> queue + Queue((10, 20, 30, 40)) + >>> queue.rotate(1) + >>> queue + Queue((20, 30, 40, 10)) + >>> queue.rotate(2) + >>> queue + Queue((40, 10, 20, 30)) + """ + + put = self.entries.append + get = self.entries.pop + + for _ in range(rotation): + put(get(0)) + + def get_front(self) -> _T: + """Get the front item from the Queue + + >>> queue = QueueByList((10, 20, 30)) + >>> queue.get_front() + 10 + >>> queue + Queue((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.get_front() + 20 + """ + + return self.entries[0] + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/queue/queue_by_two_stacks.py b/data_structures/queue/queue_by_two_stacks.py new file mode 100644 index 000000000000..cd62f155a63b --- /dev/null +++ b/data_structures/queue/queue_by_two_stacks.py @@ -0,0 +1,115 @@ +"""Queue implementation using two stacks""" + +from collections.abc import Iterable +from typing import Generic, TypeVar + +_T = TypeVar("_T") + + +class QueueByTwoStacks(Generic[_T]): + def __init__(self, iterable: Iterable[_T] | None = None) -> None: + """ + >>> QueueByTwoStacks() + Queue(()) + >>> QueueByTwoStacks([10, 20, 30]) + Queue((10, 20, 30)) + >>> QueueByTwoStacks((i**2 for i in range(1, 4))) + Queue((1, 4, 9)) + """ + self._stack1: list[_T] = list(iterable or []) + self._stack2: list[_T] = [] + + def __len__(self) -> int: + """ + >>> len(QueueByTwoStacks()) + 0 + >>> from string import ascii_lowercase + >>> len(QueueByTwoStacks(ascii_lowercase)) + 26 + >>> queue = QueueByTwoStacks() + >>> for i in range(1, 11): + ... queue.put(i) + ... + >>> len(queue) + 10 + >>> for i in range(2): + ... queue.get() + 1 + 2 + >>> len(queue) + 8 + """ + + return len(self._stack1) + len(self._stack2) + + def __repr__(self) -> str: + """ + >>> queue = QueueByTwoStacks() + >>> queue + Queue(()) + >>> str(queue) + 'Queue(())' + >>> queue.put(10) + >>> queue + Queue((10,)) + >>> queue.put(20) + >>> queue.put(30) + >>> queue + Queue((10, 20, 30)) + """ + return f"Queue({tuple(self._stack2[::-1] + self._stack1)})" + + def put(self, item: _T) -> None: + """ + Put `item` into the Queue + + >>> queue = QueueByTwoStacks() + >>> queue.put(10) + >>> queue.put(20) + >>> len(queue) + 2 + >>> queue + Queue((10, 20)) + """ + + self._stack1.append(item) + + def get(self) -> _T: + """ + Get `item` from the Queue + + >>> queue = QueueByTwoStacks((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.put(40) + >>> queue.get() + 20 + >>> queue.get() + 30 + >>> len(queue) + 1 + >>> queue.get() + 40 + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: Queue is empty + """ + + # To reduce number of attribute look-ups in `while` loop. + stack1_pop = self._stack1.pop + stack2_append = self._stack2.append + + if not self._stack2: + while self._stack1: + stack2_append(stack1_pop()) + + if not self._stack2: + raise IndexError("Queue is empty") + return self._stack2.pop() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/queue/queue_on_list.py b/data_structures/queue/queue_on_list.py deleted file mode 100644 index 71fca6b2f5f4..000000000000 --- a/data_structures/queue/queue_on_list.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Queue represented by a Python list""" - - -class Queue: - def __init__(self): - self.entries = [] - self.length = 0 - self.front = 0 - - def __str__(self): - printed = "<" + str(self.entries)[1:-1] + ">" - return printed - - """Enqueues {@code item} - @param item - item to enqueue""" - - def put(self, item): - self.entries.append(item) - self.length = self.length + 1 - - """Dequeues {@code item} - @requirement: |self.length| > 0 - @return dequeued - item that was dequeued""" - - def get(self): - self.length = self.length - 1 - dequeued = self.entries[self.front] - # self.front-=1 - # self.entries = self.entries[self.front:] - self.entries = self.entries[1:] - return dequeued - - """Rotates the queue {@code rotation} times - @param rotation - number of times to rotate queue""" - - def rotate(self, rotation): - for _ in range(rotation): - self.put(self.get()) - - """Enqueues {@code item} - @return item at front of self.entries""" - - def get_front(self): - return self.entries[0] - - """Returns the length of this.entries""" - - def size(self): - return self.length diff --git a/data_structures/queue/queue_on_pseudo_stack.py b/data_structures/queue/queue_on_pseudo_stack.py index d9845100008e..2da67ecc263c 100644 --- a/data_structures/queue/queue_on_pseudo_stack.py +++ b/data_structures/queue/queue_on_pseudo_stack.py @@ -1,4 +1,5 @@ """Queue represented by a pseudo stack (represented by a list with pop and append)""" + from typing import Any diff --git a/data_structures/stacks/balanced_parentheses.py b/data_structures/stacks/balanced_parentheses.py index 3c036c220e5c..928815bb2111 100644 --- a/data_structures/stacks/balanced_parentheses.py +++ b/data_structures/stacks/balanced_parentheses.py @@ -19,9 +19,10 @@ def balanced_parentheses(parentheses: str) -> bool: for bracket in parentheses: if bracket in bracket_pairs: stack.push(bracket) - elif bracket in (")", "]", "}"): - if stack.is_empty() or bracket_pairs[stack.pop()] != bracket: - return False + elif bracket in (")", "]", "}") and ( + stack.is_empty() or bracket_pairs[stack.pop()] != bracket + ): + return False return stack.is_empty() diff --git a/data_structures/stacks/dijkstras_two_stack_algorithm.py b/data_structures/stacks/dijkstras_two_stack_algorithm.py index 976c9a53c931..94d19156f1c3 100644 --- a/data_structures/stacks/dijkstras_two_stack_algorithm.py +++ b/data_structures/stacks/dijkstras_two_stack_algorithm.py @@ -29,6 +29,7 @@ NOTE: It only works with whole numbers. """ + __author__ = "Alexander Joslin" import operator as op diff --git a/data_structures/stacks/evaluate_postfix_notations.py b/data_structures/stacks/evaluate_postfix_notations.py deleted file mode 100644 index 51ea353b17de..000000000000 --- a/data_structures/stacks/evaluate_postfix_notations.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -The Reverse Polish Nation also known as Polish postfix notation -or simply postfix notation. -https://en.wikipedia.org/wiki/Reverse_Polish_notation -Classic examples of simple stack implementations -Valid operators are +, -, *, /. -Each operand may be an integer or another expression. -""" -from __future__ import annotations - -from typing import Any - - -def evaluate_postfix(postfix_notation: list) -> int: - """ - >>> evaluate_postfix(["2", "1", "+", "3", "*"]) - 9 - >>> evaluate_postfix(["4", "13", "5", "/", "+"]) - 6 - >>> evaluate_postfix([]) - 0 - """ - if not postfix_notation: - return 0 - - operations = {"+", "-", "*", "/"} - stack: list[Any] = [] - - for token in postfix_notation: - if token in operations: - b, a = stack.pop(), stack.pop() - if token == "+": - stack.append(a + b) - elif token == "-": - stack.append(a - b) - elif token == "*": - stack.append(a * b) - else: - if a * b < 0 and a % b != 0: - stack.append(a // b + 1) - else: - stack.append(a // b) - else: - stack.append(int(token)) - - return stack.pop() - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/data_structures/stacks/infix_to_postfix_conversion.py b/data_structures/stacks/infix_to_postfix_conversion.py index 9017443091cf..e697061937c9 100644 --- a/data_structures/stacks/infix_to_postfix_conversion.py +++ b/data_structures/stacks/infix_to_postfix_conversion.py @@ -4,9 +4,26 @@ https://en.wikipedia.org/wiki/Shunting-yard_algorithm """ +from typing import Literal + from .balanced_parentheses import balanced_parentheses from .stack import Stack +PRECEDENCES: dict[str, int] = { + "+": 1, + "-": 1, + "*": 2, + "/": 2, + "^": 3, +} +ASSOCIATIVITIES: dict[str, Literal["LR", "RL"]] = { + "+": "LR", + "-": "LR", + "*": "LR", + "/": "LR", + "^": "RL", +} + def precedence(char: str) -> int: """ @@ -14,7 +31,15 @@ def precedence(char: str) -> int: order of operation. https://en.wikipedia.org/wiki/Order_of_operations """ - return {"+": 1, "-": 1, "*": 2, "/": 2, "^": 3}.get(char, -1) + return PRECEDENCES.get(char, -1) + + +def associativity(char: str) -> Literal["LR", "RL"]: + """ + Return the associativity of the operator `char`. + https://en.wikipedia.org/wiki/Operator_associativity + """ + return ASSOCIATIVITIES[char] def infix_to_postfix(expression_str: str) -> str: @@ -35,6 +60,8 @@ def infix_to_postfix(expression_str: str) -> str: 'a b c * + d e * f + g * +' >>> infix_to_postfix("x^y/(5*z)+2") 'x y ^ 5 z * / 2 +' + >>> infix_to_postfix("2^3^2") + '2 3 2 ^ ^' """ if not balanced_parentheses(expression_str): raise ValueError("Mismatched parentheses") @@ -50,9 +77,26 @@ def infix_to_postfix(expression_str: str) -> str: postfix.append(stack.pop()) stack.pop() else: - while not stack.is_empty() and precedence(char) <= precedence(stack.peek()): + while True: + if stack.is_empty(): + stack.push(char) + break + + char_precedence = precedence(char) + tos_precedence = precedence(stack.peek()) + + if char_precedence > tos_precedence: + stack.push(char) + break + if char_precedence < tos_precedence: + postfix.append(stack.pop()) + continue + # Precedences are equal + if associativity(char) == "RL": + stack.push(char) + break postfix.append(stack.pop()) - stack.push(char) + while not stack.is_empty(): postfix.append(stack.pop()) return " ".join(postfix) diff --git a/data_structures/stacks/infix_to_prefix_conversion.py b/data_structures/stacks/infix_to_prefix_conversion.py index 6f6d5d57e2cb..878473b93c19 100644 --- a/data_structures/stacks/infix_to_prefix_conversion.py +++ b/data_structures/stacks/infix_to_prefix_conversion.py @@ -15,7 +15,53 @@ """ -def infix_2_postfix(infix): +def infix_2_postfix(infix: str) -> str: + """ + >>> infix_2_postfix("a+b^c") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ---------------------------- + a | | a + + | + | a + b | + | ab + ^ | +^ | ab + c | +^ | abc + | + | abc^ + | | abc^+ + 'abc^+' + + >>> infix_2_postfix("1*((-a)*2+b)") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ------------------------------------------- + 1 | | 1 + * | * | 1 + ( | *( | 1 + ( | *(( | 1 + - | *((- | 1 + a | *((- | 1a + ) | *( | 1a- + * | *(* | 1a- + 2 | *(* | 1a-2 + + | *(+ | 1a-2* + b | *(+ | 1a-2*b + ) | * | 1a-2*b+ + | | 1a-2*b+* + '1a-2*b+*' + + >>> infix_2_postfix("") + Symbol | Stack | Postfix + ---------------------------- + '' + + >>> infix_2_postfix("(()") + Traceback (most recent call last): + ... + ValueError: invalid expression + + >>> infix_2_postfix("())") + Traceback (most recent call last): + ... + IndexError: list index out of range + """ stack = [] post_fix = [] priority = { @@ -26,7 +72,7 @@ def infix_2_postfix(infix): "+": 1, "-": 1, } # Priority of each operator - print_width = len(infix) if (len(infix) > 7) else 7 + print_width = max(len(infix), 7) # Print table header for output print( @@ -43,16 +89,18 @@ def infix_2_postfix(infix): elif x == "(": stack.append(x) # if x is "(" push to Stack elif x == ")": # if x is ")" pop stack until "(" is encountered + if len(stack) == 0: # close bracket without open bracket + raise IndexError("list index out of range") + while stack[-1] != "(": post_fix.append(stack.pop()) # Pop stack & add the content to Postfix stack.pop() - else: - if len(stack) == 0: - stack.append(x) # If stack is empty, push x to stack - else: # while priority of x is not > priority of element in the stack - while len(stack) > 0 and priority[x] <= priority[stack[-1]]: - post_fix.append(stack.pop()) # pop stack & add to Postfix - stack.append(x) # push x to stack + elif len(stack) == 0: + stack.append(x) # If stack is empty, push x to stack + else: # while priority of x is not > priority of element in the stack + while stack and stack[-1] != "(" and priority[x] <= priority[stack[-1]]: + post_fix.append(stack.pop()) # pop stack & add to Postfix + stack.append(x) # push x to stack print( x.center(8), @@ -62,6 +110,9 @@ def infix_2_postfix(infix): ) # Output in tabular format while len(stack) > 0: # while stack is not empty + if stack[-1] == "(": # open bracket with no close bracket + raise ValueError("invalid expression") + post_fix.append(stack.pop()) # pop stack & add to Postfix print( " ".center(8), @@ -73,21 +124,69 @@ def infix_2_postfix(infix): return "".join(post_fix) # return Postfix as str -def infix_2_prefix(infix): - infix = list(infix[::-1]) # reverse the infix equation +def infix_2_prefix(infix: str) -> str: + """ + >>> infix_2_prefix("a+b^c") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ---------------------------- + c | | c + ^ | ^ | c + b | ^ | cb + + | + | cb^ + a | + | cb^a + | | cb^a+ + '+a^bc' - for i in range(len(infix)): - if infix[i] == "(": - infix[i] = ")" # change "(" to ")" - elif infix[i] == ")": - infix[i] = "(" # change ")" to "(" + >>> infix_2_prefix("1*((-a)*2+b)") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ------------------------------------------- + ( | ( | + b | ( | b + + | (+ | b + 2 | (+ | b2 + * | (+* | b2 + ( | (+*( | b2 + a | (+*( | b2a + - | (+*(- | b2a + ) | (+* | b2a- + ) | | b2a-*+ + * | * | b2a-*+ + 1 | * | b2a-*+1 + | | b2a-*+1* + '*1+*-a2b' - return (infix_2_postfix("".join(infix)))[ - ::-1 - ] # call infix_2_postfix on Infix, return reverse of Postfix + >>> infix_2_prefix('') + Symbol | Stack | Postfix + ---------------------------- + '' + + >>> infix_2_prefix('(()') + Traceback (most recent call last): + ... + IndexError: list index out of range + + >>> infix_2_prefix('())') + Traceback (most recent call last): + ... + ValueError: invalid expression + """ + reversed_infix = list(infix[::-1]) # reverse the infix equation + + for i in range(len(reversed_infix)): + if reversed_infix[i] == "(": + reversed_infix[i] = ")" # change "(" to ")" + elif reversed_infix[i] == ")": + reversed_infix[i] = "(" # change ")" to "(" + + # call infix_2_postfix on Infix, return reverse of Postfix + return (infix_2_postfix("".join(reversed_infix)))[::-1] if __name__ == "__main__": + from doctest import testmod + + testmod() + Infix = input("\nEnter an Infix Equation = ") # Input an Infix equation Infix = "".join(Infix.split()) # Remove spaces from the input print("\n\t", Infix, "(Infix) -> ", infix_2_prefix(Infix), "(Prefix)") diff --git a/data_structures/stacks/lexicographical_numbers.py b/data_structures/stacks/lexicographical_numbers.py new file mode 100644 index 000000000000..6a174e7d9e95 --- /dev/null +++ b/data_structures/stacks/lexicographical_numbers.py @@ -0,0 +1,38 @@ +from collections.abc import Iterator + + +def lexical_order(max_number: int) -> Iterator[int]: + """ + Generate numbers in lexical order from 1 to max_number. + + >>> " ".join(map(str, lexical_order(13))) + '1 10 11 12 13 2 3 4 5 6 7 8 9' + >>> list(lexical_order(1)) + [1] + >>> " ".join(map(str, lexical_order(20))) + '1 10 11 12 13 14 15 16 17 18 19 2 20 3 4 5 6 7 8 9' + >>> " ".join(map(str, lexical_order(25))) + '1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 23 24 25 3 4 5 6 7 8 9' + >>> list(lexical_order(12)) + [1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9] + """ + + stack = [1] + + while stack: + num = stack.pop() + if num > max_number: + continue + + yield num + if (num % 10) != 9: + stack.append(num + 1) + + stack.append(num * 10) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(f"Numbers from 1 to 25 in lexical order: {list(lexical_order(26))}") diff --git a/data_structures/stacks/next_greater_element.py b/data_structures/stacks/next_greater_element.py index 7d76d1f47dfa..216850b4b894 100644 --- a/data_structures/stacks/next_greater_element.py +++ b/data_structures/stacks/next_greater_element.py @@ -6,9 +6,20 @@ def next_greatest_element_slow(arr: list[float]) -> list[float]: """ - Get the Next Greatest Element (NGE) for all elements in a list. - Maximum element present after the current one which is also greater than the - current one. + Get the Next Greatest Element (NGE) for each element in the array + by checking all subsequent elements to find the next greater one. + + This is a brute-force implementation, and it has a time complexity + of O(n^2), where n is the size of the array. + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: >>> next_greatest_element_slow(arr) == expect True """ @@ -28,9 +39,21 @@ def next_greatest_element_slow(arr: list[float]) -> list[float]: def next_greatest_element_fast(arr: list[float]) -> list[float]: """ - Like next_greatest_element_slow() but changes the loops to use - enumerate() instead of range(len()) for the outer loop and - for in a slice of arr for the inner loop. + Find the Next Greatest Element (NGE) for each element in the array + using a more readable approach. This implementation utilizes + enumerate() for the outer loop and slicing for the inner loop. + + While this improves readability over next_greatest_element_slow(), + it still has a time complexity of O(n^2). + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: >>> next_greatest_element_fast(arr) == expect True """ @@ -47,14 +70,23 @@ def next_greatest_element_fast(arr: list[float]) -> list[float]: def next_greatest_element(arr: list[float]) -> list[float]: """ - Get the Next Greatest Element (NGE) for all elements in a list. - Maximum element present after the current one which is also greater than the - current one. - - A naive way to solve this is to take two loops and check for the next bigger - number but that will make the time complexity as O(n^2). The better way to solve - this would be to use a stack to keep track of maximum number giving a linear time - solution. + Efficient solution to find the Next Greatest Element (NGE) for all elements + using a stack. The time complexity is reduced to O(n), making it suitable + for larger arrays. + + The stack keeps track of elements for which the next greater element hasn't + been found yet. By iterating through the array in reverse (from the last + element to the first), the stack is used to efficiently determine the next + greatest element for each element. + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: >>> next_greatest_element(arr) == expect True """ diff --git a/data_structures/stacks/postfix_evaluation.py b/data_structures/stacks/postfix_evaluation.py index 28128f82ec19..03a87b9e0fa3 100644 --- a/data_structures/stacks/postfix_evaluation.py +++ b/data_structures/stacks/postfix_evaluation.py @@ -1,4 +1,11 @@ """ +Reverse Polish Nation is also known as Polish postfix notation or simply postfix +notation. +https://en.wikipedia.org/wiki/Reverse_Polish_notation +Classic examples of simple stack implementations. +Valid operators are +, -, *, /. +Each operand may be an integer or another expression. + Output: Enter a Postfix Equation (space separated) = 5 6 9 * + @@ -17,52 +24,177 @@ Result = 59 """ -import operator as op +# Defining valid unary operator symbols +UNARY_OP_SYMBOLS = ("-", "+") + +# operators & their respective operation +OPERATORS = { + "^": lambda p, q: p**q, + "*": lambda p, q: p * q, + "/": lambda p, q: p / q, + "+": lambda p, q: p + q, + "-": lambda p, q: p - q, +} + + +def parse_token(token: str | float) -> float | str: + """ + Converts the given data to the appropriate number if it is indeed a number, else + returns the data as it is with a False flag. This function also serves as a check + of whether the input is a number or not. + + Parameters + ---------- + token: The data that needs to be converted to the appropriate operator or number. + + Returns + ------- + float or str + Returns a float if `token` is a number or a str if `token` is an operator + """ + if token in OPERATORS: + return token + try: + return float(token) + except ValueError: + msg = f"{token} is neither a number nor a valid operator" + raise ValueError(msg) + + +def evaluate(post_fix: list[str], verbose: bool = False) -> float: + """ + Evaluate postfix expression using a stack. + >>> evaluate(["0"]) + 0.0 + >>> evaluate(["-0"]) + -0.0 + >>> evaluate(["1"]) + 1.0 + >>> evaluate(["-1"]) + -1.0 + >>> evaluate(["-1.1"]) + -1.1 + >>> evaluate(["2", "1", "+", "3", "*"]) + 9.0 + >>> evaluate(["2", "1.9", "+", "3", "*"]) + 11.7 + >>> evaluate(["2", "-1.9", "+", "3", "*"]) + 0.30000000000000027 + >>> evaluate(["4", "13", "5", "/", "+"]) + 6.6 + >>> evaluate(["2", "-", "3", "+"]) + 1.0 + >>> evaluate(["-4", "5", "*", "6", "-"]) + -26.0 + >>> evaluate([]) + 0 + >>> evaluate(["4", "-", "6", "7", "/", "9", "8"]) + Traceback (most recent call last): + ... + ArithmeticError: Input is not a valid postfix expression + + Parameters + ---------- + post_fix: + The postfix expression is tokenized into operators and operands and stored + as a Python list + verbose: + Display stack contents while evaluating the expression if verbose is True -def solve(post_fix): + Returns + ------- + float + The evaluated value + """ + if not post_fix: + return 0 + # Checking the list to find out whether the postfix expression is valid + valid_expression = [parse_token(token) for token in post_fix] + if verbose: + # print table header + print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ") + print("-" * (30 + len(post_fix))) stack = [] - div = lambda x, y: int(x / y) # noqa: E731 integer division operation - opr = { - "^": op.pow, - "*": op.mul, - "/": div, - "+": op.add, - "-": op.sub, - } # operators & their respective operation - - # print table header - print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ") - print("-" * (30 + len(post_fix))) - - for x in post_fix: - if x.isdigit(): # if x in digit + for x in valid_expression: + if x not in OPERATORS: stack.append(x) # append x to stack - # output in tabular format - print(x.rjust(8), ("push(" + x + ")").ljust(12), ",".join(stack), sep=" | ") - else: + if verbose: + # output in tabular format + print( + f"{x}".rjust(8), + f"push({x})".ljust(12), + stack, + sep=" | ", + ) + continue + # If x is operator + # If only 1 value is inside the stack and + or - is encountered + # then this is unary + or - case + if x in UNARY_OP_SYMBOLS and len(stack) < 2: b = stack.pop() # pop stack + if x == "-": + b *= -1 # negate b + stack.append(b) + if verbose: + # output in tabular format + print( + "".rjust(8), + f"pop({b})".ljust(12), + stack, + sep=" | ", + ) + print( + str(x).rjust(8), + f"push({x}{b})".ljust(12), + stack, + sep=" | ", + ) + continue + b = stack.pop() # pop stack + if verbose: # output in tabular format - print("".rjust(8), ("pop(" + b + ")").ljust(12), ",".join(stack), sep=" | ") + print( + "".rjust(8), + f"pop({b})".ljust(12), + stack, + sep=" | ", + ) - a = stack.pop() # pop stack + a = stack.pop() # pop stack + if verbose: # output in tabular format - print("".rjust(8), ("pop(" + a + ")").ljust(12), ",".join(stack), sep=" | ") - - stack.append( - str(opr[x](int(a), int(b))) - ) # evaluate the 2 values popped from stack & push result to stack + print( + "".rjust(8), + f"pop({a})".ljust(12), + stack, + sep=" | ", + ) + # evaluate the 2 values popped from stack & push result to stack + stack.append(OPERATORS[x](a, b)) # type: ignore[index] + if verbose: # output in tabular format print( - x.rjust(8), - ("push(" + a + x + b + ")").ljust(12), - ",".join(stack), + f"{x}".rjust(8), + f"push({a}{x}{b})".ljust(12), + stack, sep=" | ", ) - - return int(stack[0]) + # If everything is executed correctly, the stack will contain + # only one element which is the result + if len(stack) != 1: + raise ArithmeticError("Input is not a valid postfix expression") + return float(stack[0]) if __name__ == "__main__": - Postfix = input("\n\nEnter a Postfix Equation (space separated) = ").split(" ") - print("\n\tResult = ", solve(Postfix)) + # Create a loop so that the user can evaluate postfix expressions multiple times + while True: + expression = input("Enter a Postfix Expression (space separated): ").split(" ") + prompt = "Do you want to see stack contents while evaluating? [y/N]: " + verbose = input(prompt).strip().lower() == "y" + output = evaluate(expression, verbose) + print("Result = ", output) + prompt = "Do you want to enter another expression? [y/N]: " + if input(prompt).strip().lower() != "y": + break diff --git a/data_structures/stacks/stack.py b/data_structures/stacks/stack.py index 55d424d5018b..93698f5aa116 100644 --- a/data_structures/stacks/stack.py +++ b/data_structures/stacks/stack.py @@ -33,7 +33,23 @@ def __str__(self) -> str: return str(self.stack) def push(self, data: T) -> None: - """Push an element to the top of the stack.""" + """ + Push an element to the top of the stack. + + >>> S = Stack(2) # stack size = 2 + >>> S.push(10) + >>> S.push(20) + >>> print(S) + [10, 20] + + >>> S = Stack(1) # stack size = 1 + >>> S.push(10) + >>> S.push(20) + Traceback (most recent call last): + ... + data_structures.stacks.stack.StackOverflowError + + """ if len(self.stack) >= self.limit: raise StackOverflowError self.stack.append(data) @@ -42,6 +58,12 @@ def pop(self) -> T: """ Pop an element off of the top of the stack. + >>> S = Stack() + >>> S.push(-5) + >>> S.push(10) + >>> S.pop() + 10 + >>> Stack().pop() Traceback (most recent call last): ... @@ -55,7 +77,13 @@ def peek(self) -> T: """ Peek at the top-most element of the stack. - >>> Stack().pop() + >>> S = Stack() + >>> S.push(-5) + >>> S.push(10) + >>> S.peek() + 10 + + >>> Stack().peek() Traceback (most recent call last): ... data_structures.stacks.stack.StackUnderflowError @@ -65,18 +93,68 @@ def peek(self) -> T: return self.stack[-1] def is_empty(self) -> bool: - """Check if a stack is empty.""" + """ + Check if a stack is empty. + + >>> S = Stack() + >>> S.is_empty() + True + + >>> S = Stack() + >>> S.push(10) + >>> S.is_empty() + False + """ return not bool(self.stack) def is_full(self) -> bool: + """ + >>> S = Stack() + >>> S.is_full() + False + + >>> S = Stack(1) + >>> S.push(10) + >>> S.is_full() + True + """ return self.size() == self.limit def size(self) -> int: - """Return the size of the stack.""" + """ + Return the size of the stack. + + >>> S = Stack(3) + >>> S.size() + 0 + + >>> S = Stack(3) + >>> S.push(10) + >>> S.size() + 1 + + >>> S = Stack(3) + >>> S.push(10) + >>> S.push(20) + >>> S.size() + 2 + """ return len(self.stack) def __contains__(self, item: T) -> bool: - """Check if item is in stack""" + """ + Check if item is in stack + + >>> S = Stack(3) + >>> S.push(10) + >>> 10 in S + True + + >>> S = Stack(3) + >>> S.push(10) + >>> 20 in S + False + """ return item in self.stack @@ -92,13 +170,13 @@ def test_stack() -> None: try: _ = stack.pop() - raise AssertionError() # This should not happen + raise AssertionError # This should not happen except StackUnderflowError: assert True # This should happen try: _ = stack.peek() - raise AssertionError() # This should not happen + raise AssertionError # This should not happen except StackUnderflowError: assert True # This should happen @@ -118,7 +196,7 @@ def test_stack() -> None: try: stack.push(200) - raise AssertionError() # This should not happen + raise AssertionError # This should not happen except StackOverflowError: assert True # This should happen @@ -131,3 +209,7 @@ def test_stack() -> None: if __name__ == "__main__": test_stack() + + import doctest + + doctest.testmod() diff --git a/data_structures/stacks/stack_using_two_queues.py b/data_structures/stacks/stack_using_two_queues.py new file mode 100644 index 000000000000..4b73246a045c --- /dev/null +++ b/data_structures/stacks/stack_using_two_queues.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass, field + + +@dataclass +class StackWithQueues: + """ + https://www.geeksforgeeks.org/implement-stack-using-queue/ + + >>> stack = StackWithQueues() + >>> stack.push(1) + >>> stack.push(2) + >>> stack.push(3) + >>> stack.peek() + 3 + >>> stack.pop() + 3 + >>> stack.peek() + 2 + >>> stack.pop() + 2 + >>> stack.pop() + 1 + >>> stack.peek() is None + True + >>> stack.pop() + Traceback (most recent call last): + ... + IndexError: pop from an empty deque + """ + + main_queue: deque[int] = field(default_factory=deque) + temp_queue: deque[int] = field(default_factory=deque) + + def push(self, item: int) -> None: + self.temp_queue.append(item) + while self.main_queue: + self.temp_queue.append(self.main_queue.popleft()) + self.main_queue, self.temp_queue = self.temp_queue, self.main_queue + + def pop(self) -> int: + return self.main_queue.popleft() + + def peek(self) -> int | None: + return self.main_queue[0] if self.main_queue else None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + stack: StackWithQueues | None = StackWithQueues() + while stack: + print("\nChoose operation:") + print("1. Push") + print("2. Pop") + print("3. Peek") + print("4. Quit") + + choice = input("Enter choice (1/2/3/4): ") + + if choice == "1": + element = int(input("Enter an integer to push: ").strip()) + stack.push(element) + print(f"{element} pushed onto the stack.") + elif choice == "2": + popped_element = stack.pop() + if popped_element is not None: + print(f"Popped element: {popped_element}") + else: + print("Stack is empty.") + elif choice == "3": + peeked_element = stack.peek() + if peeked_element is not None: + print(f"Top element: {peeked_element}") + else: + print("Stack is empty.") + elif choice == "4": + del stack + stack = None + else: + print("Invalid choice. Please try again.") diff --git a/data_structures/stacks/stack_with_singly_linked_list.py b/data_structures/stacks/stack_with_singly_linked_list.py index f5ce83b863ce..8e77c2b967ef 100644 --- a/data_structures/stacks/stack_with_singly_linked_list.py +++ b/data_structures/stacks/stack_with_singly_linked_list.py @@ -1,4 +1,5 @@ -""" A Stack using a linked list like structure """ +"""A Stack using a linked list like structure""" + from __future__ import annotations from collections.abc import Iterator diff --git a/data_structures/stacks/stock_span_problem.py b/data_structures/stacks/stock_span_problem.py index de423c1ebf66..5efe58d25798 100644 --- a/data_structures/stacks/stock_span_problem.py +++ b/data_structures/stacks/stock_span_problem.py @@ -36,7 +36,7 @@ def calculation_span(price, s): # A utility function to print elements of array def print_array(arr, n): - for i in range(0, n): + for i in range(n): print(arr[i], end=" ") diff --git a/data_structures/suffix_tree/__init__.py b/data_structures/suffix_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/example/__init__.py b/data_structures/suffix_tree/example/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/example/example_usage.py b/data_structures/suffix_tree/example/example_usage.py new file mode 100644 index 000000000000..724ac57e8bfb --- /dev/null +++ b/data_structures/suffix_tree/example/example_usage.py @@ -0,0 +1,37 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.suffix_tree.suffix_tree import SuffixTree + + +def main() -> None: + """ + Demonstrate the usage of the SuffixTree class. + + - Initializes a SuffixTree with a predefined text. + - Defines a list of patterns to search for within the suffix tree. + - Searches for each pattern in the suffix tree. + + Patterns tested: + - "ana" (found) --> True + - "ban" (found) --> True + - "na" (found) --> True + - "xyz" (not found) --> False + - "mon" (found) --> True + """ + text = "monkey banana" + suffix_tree = SuffixTree(text) + + patterns = ["ana", "ban", "na", "xyz", "mon"] + for pattern in patterns: + found = suffix_tree.search(pattern) + print(f"Pattern '{pattern}' found: {found}") + + +if __name__ == "__main__": + main() diff --git a/data_structures/suffix_tree/suffix_tree.py b/data_structures/suffix_tree/suffix_tree.py new file mode 100644 index 000000000000..ad54fb0ba009 --- /dev/null +++ b/data_structures/suffix_tree/suffix_tree.py @@ -0,0 +1,66 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.suffix_tree.suffix_tree_node import SuffixTreeNode + + +class SuffixTree: + def __init__(self, text: str) -> None: + """ + Initializes the suffix tree with the given text. + + Args: + text (str): The text for which the suffix tree is to be built. + """ + self.text: str = text + self.root: SuffixTreeNode = SuffixTreeNode() + self.build_suffix_tree() + + def build_suffix_tree(self) -> None: + """ + Builds the suffix tree for the given text by adding all suffixes. + """ + text = self.text + n = len(text) + for i in range(n): + suffix = text[i:] + self._add_suffix(suffix, i) + + def _add_suffix(self, suffix: str, index: int) -> None: + """ + Adds a suffix to the suffix tree. + + Args: + suffix (str): The suffix to add. + index (int): The starting index of the suffix in the original text. + """ + node = self.root + for char in suffix: + if char not in node.children: + node.children[char] = SuffixTreeNode() + node = node.children[char] + node.is_end_of_string = True + node.start = index + node.end = index + len(suffix) - 1 + + def search(self, pattern: str) -> bool: + """ + Searches for a pattern in the suffix tree. + + Args: + pattern (str): The pattern to search for. + + Returns: + bool: True if the pattern is found, False otherwise. + """ + node = self.root + for char in pattern: + if char not in node.children: + return False + node = node.children[char] + return True diff --git a/data_structures/suffix_tree/suffix_tree_node.py b/data_structures/suffix_tree/suffix_tree_node.py new file mode 100644 index 000000000000..e5b628645063 --- /dev/null +++ b/data_structures/suffix_tree/suffix_tree_node.py @@ -0,0 +1,36 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from __future__ import annotations + + +class SuffixTreeNode: + def __init__( + self, + children: dict[str, SuffixTreeNode] | None = None, + is_end_of_string: bool = False, + start: int | None = None, + end: int | None = None, + suffix_link: SuffixTreeNode | None = None, + ) -> None: + """ + Initializes a suffix tree node. + + Parameters: + children (dict[str, SuffixTreeNode] | None): The children of this node. + is_end_of_string (bool): Indicates if this node represents + the end of a string. + start (int | None): The start index of the suffix in the text. + end (int | None): The end index of the suffix in the text. + suffix_link (SuffixTreeNode | None): Link to another suffix tree node. + """ + self.children = children or {} + self.is_end_of_string = is_end_of_string + self.start = start + self.end = end + self.suffix_link = suffix_link diff --git a/data_structures/suffix_tree/tests/__init__.py b/data_structures/suffix_tree/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/tests/test_suffix_tree.py b/data_structures/suffix_tree/tests/test_suffix_tree.py new file mode 100644 index 000000000000..c9dbe199d19d --- /dev/null +++ b/data_structures/suffix_tree/tests/test_suffix_tree.py @@ -0,0 +1,59 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import unittest + +from data_structures.suffix_tree.suffix_tree import SuffixTree + + +class TestSuffixTree(unittest.TestCase): + def setUp(self) -> None: + """Set up the initial conditions for each test.""" + self.text = "banana" + self.suffix_tree = SuffixTree(self.text) + + def test_search_existing_patterns(self) -> None: + """Test searching for patterns that exist in the suffix tree.""" + patterns = ["ana", "ban", "na"] + for pattern in patterns: + with self.subTest(pattern=pattern): + assert self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should be found." + ) + + def test_search_non_existing_patterns(self) -> None: + """Test searching for patterns that do not exist in the suffix tree.""" + patterns = ["xyz", "apple", "cat"] + for pattern in patterns: + with self.subTest(pattern=pattern): + assert not self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should not be found." + ) + + def test_search_empty_pattern(self) -> None: + """Test searching for an empty pattern.""" + assert self.suffix_tree.search(""), "An empty pattern should be found." + + def test_search_full_text(self) -> None: + """Test searching for the full text.""" + assert self.suffix_tree.search(self.text), ( + "The full text should be found in the suffix tree." + ) + + def test_search_substrings(self) -> None: + """Test searching for substrings of the full text.""" + substrings = ["ban", "ana", "a", "na"] + for substring in substrings: + with self.subTest(substring=substring): + assert self.suffix_tree.search(substring), ( + f"Substring '{substring}' should be found." + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/data_structures/trie/radix_tree.py b/data_structures/trie/radix_tree.py index 66890346ec2b..caf566a6ce30 100644 --- a/data_structures/trie/radix_tree.py +++ b/data_structures/trie/radix_tree.py @@ -54,10 +54,17 @@ def insert(self, word: str) -> None: word (str): word to insert >>> RadixNode("myprefix").insert("mystring") + + >>> root = RadixNode() + >>> root.insert_many(['myprefix', 'myprefixA', 'myprefixAA']) + >>> root.print_tree() + - myprefix (leaf) + -- A (leaf) + --- A (leaf) """ # Case 1: If the word is the prefix of the node # Solution: We set the current node as leaf - if self.prefix == word: + if self.prefix == word and not self.is_leaf: self.is_leaf = True # Case 2: The node has no edges that have a prefix to the word @@ -146,31 +153,30 @@ def delete(self, word: str) -> bool: # We have word remaining so we check the next node elif remaining_word != "": return incoming_node.delete(remaining_word) + # If it is not a leaf, we don't have to delete + elif not incoming_node.is_leaf: + return False else: - # If it is not a leaf, we don't have to delete - if not incoming_node.is_leaf: - return False + # We delete the nodes if no edges go from it + if len(incoming_node.nodes) == 0: + del self.nodes[word[0]] + # We merge the current node with its only child + if len(self.nodes) == 1 and not self.is_leaf: + merging_node = next(iter(self.nodes.values())) + self.is_leaf = merging_node.is_leaf + self.prefix += merging_node.prefix + self.nodes = merging_node.nodes + # If there is more than 1 edge, we just mark it as non-leaf + elif len(incoming_node.nodes) > 1: + incoming_node.is_leaf = False + # If there is 1 edge, we merge it with its child else: - # We delete the nodes if no edges go from it - if len(incoming_node.nodes) == 0: - del self.nodes[word[0]] - # We merge the current node with its only child - if len(self.nodes) == 1 and not self.is_leaf: - merging_node = list(self.nodes.values())[0] - self.is_leaf = merging_node.is_leaf - self.prefix += merging_node.prefix - self.nodes = merging_node.nodes - # If there is more than 1 edge, we just mark it as non-leaf - elif len(incoming_node.nodes) > 1: - incoming_node.is_leaf = False - # If there is 1 edge, we merge it with its child - else: - merging_node = list(incoming_node.nodes.values())[0] - incoming_node.is_leaf = merging_node.is_leaf - incoming_node.prefix += merging_node.prefix - incoming_node.nodes = merging_node.nodes - - return True + merging_node = next(iter(incoming_node.nodes.values())) + incoming_node.is_leaf = merging_node.is_leaf + incoming_node.prefix += merging_node.prefix + incoming_node.nodes = merging_node.nodes + + return True def print_tree(self, height: int = 0) -> None: """Print the tree diff --git a/digital_image_processing/change_contrast.py b/digital_image_processing/change_contrast.py index 6a150400249f..7e49694708f8 100644 --- a/digital_image_processing/change_contrast.py +++ b/digital_image_processing/change_contrast.py @@ -4,8 +4,8 @@ This algorithm is used in https://noivce.pythonanywhere.com/ Python web app. -python/black: True -flake8 : True +psf/black: True +ruff : True """ from PIL import Image diff --git a/digital_image_processing/convert_to_negative.py b/digital_image_processing/convert_to_negative.py index 7df44138973c..9bf2d8f2c075 100644 --- a/digital_image_processing/convert_to_negative.py +++ b/digital_image_processing/convert_to_negative.py @@ -1,6 +1,7 @@ """ - Implemented an algorithm using opencv to convert a colored image into its negative +Implemented an algorithm using opencv to convert a colored image into its negative """ + from cv2 import destroyAllWindows, imread, imshow, waitKey diff --git a/digital_image_processing/dithering/burkes.py b/digital_image_processing/dithering/burkes.py index 2bf0bbe03225..4b59356d8f08 100644 --- a/digital_image_processing/dithering/burkes.py +++ b/digital_image_processing/dithering/burkes.py @@ -1,6 +1,7 @@ """ Implementation Burke's algorithm (dithering) """ + import numpy as np from cv2 import destroyAllWindows, imread, imshow, waitKey @@ -21,7 +22,8 @@ def __init__(self, input_img, threshold: int): self.max_threshold = int(self.get_greyscale(255, 255, 255)) if not self.min_threshold < threshold < self.max_threshold: - raise ValueError(f"Factor value should be from 0 to {self.max_threshold}") + msg = f"Factor value should be from 0 to {self.max_threshold}" + raise ValueError(msg) self.input_img = input_img self.threshold = threshold @@ -38,9 +40,18 @@ def __init__(self, input_img, threshold: int): def get_greyscale(cls, blue: int, green: int, red: int) -> float: """ >>> Burkes.get_greyscale(3, 4, 5) - 3.753 + 4.185 + >>> Burkes.get_greyscale(0, 0, 0) + 0.0 + >>> Burkes.get_greyscale(255, 255, 255) + 255.0 + """ + """ + Formula from https://en.wikipedia.org/wiki/HSL_and_HSV + cf Lightness section, and Fig 13c. + We use the first of four possible. """ - return 0.114 * blue + 0.587 * green + 0.2126 * red + return 0.114 * blue + 0.587 * green + 0.299 * red def process(self) -> None: for y in range(self.height): @@ -48,10 +59,10 @@ def process(self) -> None: greyscale = int(self.get_greyscale(*self.input_img[y][x])) if self.threshold > greyscale + self.error_table[y][x]: self.output_img[y][x] = (0, 0, 0) - current_error = greyscale + self.error_table[x][y] + current_error = greyscale + self.error_table[y][x] else: self.output_img[y][x] = (255, 255, 255) - current_error = greyscale + self.error_table[x][y] - 255 + current_error = greyscale + self.error_table[y][x] - 255 """ Burkes error propagation (`*` is current pixel): diff --git a/digital_image_processing/edge_detection/canny.py b/digital_image_processing/edge_detection/canny.py index a830355267c4..944161c31cfc 100644 --- a/digital_image_processing/edge_detection/canny.py +++ b/digital_image_processing/edge_detection/canny.py @@ -18,105 +18,126 @@ def gen_gaussian_kernel(k_size, sigma): return g -def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): - image_row, image_col = image.shape[0], image.shape[1] - # gaussian_filter - gaussian_out = img_convolve(image, gen_gaussian_kernel(9, sigma=1.4)) - # get the gradient and degree by sobel_filter - sobel_grad, sobel_theta = sobel_filter(gaussian_out) - gradient_direction = np.rad2deg(sobel_theta) - gradient_direction += PI - - dst = np.zeros((image_row, image_col)) - +def suppress_non_maximum(image_shape, gradient_direction, sobel_grad): """ Non-maximum suppression. If the edge strength of the current pixel is the largest compared to the other pixels in the mask with the same direction, the value will be preserved. Otherwise, the value will be suppressed. """ - for row in range(1, image_row - 1): - for col in range(1, image_col - 1): + destination = np.zeros(image_shape) + + for row in range(1, image_shape[0] - 1): + for col in range(1, image_shape[1] - 1): direction = gradient_direction[row, col] if ( - 0 <= direction < 22.5 + 0 <= direction < PI / 8 or 15 * PI / 8 <= direction <= 2 * PI or 7 * PI / 8 <= direction <= 9 * PI / 8 ): w = sobel_grad[row, col - 1] e = sobel_grad[row, col + 1] if sobel_grad[row, col] >= w and sobel_grad[row, col] >= e: - dst[row, col] = sobel_grad[row, col] + destination[row, col] = sobel_grad[row, col] - elif (PI / 8 <= direction < 3 * PI / 8) or ( - 9 * PI / 8 <= direction < 11 * PI / 8 + elif ( + PI / 8 <= direction < 3 * PI / 8 + or 9 * PI / 8 <= direction < 11 * PI / 8 ): sw = sobel_grad[row + 1, col - 1] ne = sobel_grad[row - 1, col + 1] if sobel_grad[row, col] >= sw and sobel_grad[row, col] >= ne: - dst[row, col] = sobel_grad[row, col] + destination[row, col] = sobel_grad[row, col] - elif (3 * PI / 8 <= direction < 5 * PI / 8) or ( - 11 * PI / 8 <= direction < 13 * PI / 8 + elif ( + 3 * PI / 8 <= direction < 5 * PI / 8 + or 11 * PI / 8 <= direction < 13 * PI / 8 ): n = sobel_grad[row - 1, col] s = sobel_grad[row + 1, col] if sobel_grad[row, col] >= n and sobel_grad[row, col] >= s: - dst[row, col] = sobel_grad[row, col] + destination[row, col] = sobel_grad[row, col] - elif (5 * PI / 8 <= direction < 7 * PI / 8) or ( - 13 * PI / 8 <= direction < 15 * PI / 8 + elif ( + 5 * PI / 8 <= direction < 7 * PI / 8 + or 13 * PI / 8 <= direction < 15 * PI / 8 ): nw = sobel_grad[row - 1, col - 1] se = sobel_grad[row + 1, col + 1] if sobel_grad[row, col] >= nw and sobel_grad[row, col] >= se: - dst[row, col] = sobel_grad[row, col] - - """ - High-Low threshold detection. If an edge pixel’s gradient value is higher - than the high threshold value, it is marked as a strong edge pixel. If an - edge pixel’s gradient value is smaller than the high threshold value and - larger than the low threshold value, it is marked as a weak edge pixel. If - an edge pixel's value is smaller than the low threshold value, it will be - suppressed. - """ - if dst[row, col] >= threshold_high: - dst[row, col] = strong - elif dst[row, col] <= threshold_low: - dst[row, col] = 0 + destination[row, col] = sobel_grad[row, col] + + return destination + + +def detect_high_low_threshold( + image_shape, destination, threshold_low, threshold_high, weak, strong +): + """ + High-Low threshold detection. If an edge pixel's gradient value is higher + than the high threshold value, it is marked as a strong edge pixel. If an + edge pixel's gradient value is smaller than the high threshold value and + larger than the low threshold value, it is marked as a weak edge pixel. If + an edge pixel's value is smaller than the low threshold value, it will be + suppressed. + """ + for row in range(1, image_shape[0] - 1): + for col in range(1, image_shape[1] - 1): + if destination[row, col] >= threshold_high: + destination[row, col] = strong + elif destination[row, col] <= threshold_low: + destination[row, col] = 0 else: - dst[row, col] = weak + destination[row, col] = weak + +def track_edge(image_shape, destination, weak, strong): """ Edge tracking. Usually a weak edge pixel caused from true edges will be connected to a strong edge pixel while noise responses are unconnected. As long as there is one strong edge pixel that is involved in its 8-connected neighborhood, that weak edge point can be identified as one that should be preserved. """ - for row in range(1, image_row): - for col in range(1, image_col): - if dst[row, col] == weak: + for row in range(1, image_shape[0]): + for col in range(1, image_shape[1]): + if destination[row, col] == weak: if 255 in ( - dst[row, col + 1], - dst[row, col - 1], - dst[row - 1, col], - dst[row + 1, col], - dst[row - 1, col - 1], - dst[row + 1, col - 1], - dst[row - 1, col + 1], - dst[row + 1, col + 1], + destination[row, col + 1], + destination[row, col - 1], + destination[row - 1, col], + destination[row + 1, col], + destination[row - 1, col - 1], + destination[row + 1, col - 1], + destination[row - 1, col + 1], + destination[row + 1, col + 1], ): - dst[row, col] = strong + destination[row, col] = strong else: - dst[row, col] = 0 + destination[row, col] = 0 + + +def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): + # gaussian_filter + gaussian_out = img_convolve(image, gen_gaussian_kernel(9, sigma=1.4)) + # get the gradient and degree by sobel_filter + sobel_grad, sobel_theta = sobel_filter(gaussian_out) + gradient_direction = PI + np.rad2deg(sobel_theta) + + destination = suppress_non_maximum(image.shape, gradient_direction, sobel_grad) + + detect_high_low_threshold( + image.shape, destination, threshold_low, threshold_high, weak, strong + ) + + track_edge(image.shape, destination, weak, strong) - return dst + return destination if __name__ == "__main__": # read original image in gray mode lena = cv2.imread(r"../image_data/lena.jpg", 0) # canny edge detection - canny_dst = canny(lena) - cv2.imshow("canny", canny_dst) + canny_destination = canny(lena) + cv2.imshow("canny", canny_destination) cv2.waitKey(0) diff --git a/digital_image_processing/filters/bilateral_filter.py b/digital_image_processing/filters/bilateral_filter.py index 565da73f6b0e..6ef4434d959c 100644 --- a/digital_image_processing/filters/bilateral_filter.py +++ b/digital_image_processing/filters/bilateral_filter.py @@ -9,6 +9,7 @@ Output: img:A 2d zero padded image with values in between 0 and 1 """ + import math import sys @@ -31,8 +32,8 @@ def get_slice(img: np.ndarray, x: int, y: int, kernel_size: int) -> np.ndarray: def get_gauss_kernel(kernel_size: int, spatial_variance: float) -> np.ndarray: # Creates a gaussian kernel of given dimension. arr = np.zeros((kernel_size, kernel_size)) - for i in range(0, kernel_size): - for j in range(0, kernel_size): + for i in range(kernel_size): + for j in range(kernel_size): arr[i, j] = math.sqrt( abs(i - kernel_size // 2) ** 2 + abs(j - kernel_size // 2) ** 2 ) diff --git a/digital_image_processing/filters/convolve.py b/digital_image_processing/filters/convolve.py index 299682010da6..004402f29ba9 100644 --- a/digital_image_processing/filters/convolve.py +++ b/digital_image_processing/filters/convolve.py @@ -11,8 +11,8 @@ def im2col(image, block_size): dst_width = rows - block_size[0] + 1 image_array = zeros((dst_height * dst_width, block_size[1] * block_size[0])) row = 0 - for i in range(0, dst_height): - for j in range(0, dst_width): + for i in range(dst_height): + for j in range(dst_width): window = ravel(image[i : i + block_size[0], j : j + block_size[1]]) image_array[row, :] = window row += 1 diff --git a/digital_image_processing/filters/gabor_filter.py b/digital_image_processing/filters/gabor_filter.py index 8f9212a35a79..aaec567f4c99 100644 --- a/digital_image_processing/filters/gabor_filter.py +++ b/digital_image_processing/filters/gabor_filter.py @@ -48,9 +48,9 @@ def gabor_filter_kernel( _y = -sin_theta * px + cos_theta * py # fill kernel - gabor[y, x] = np.exp( - -(_x**2 + gamma**2 * _y**2) / (2 * sigma**2) - ) * np.cos(2 * np.pi * _x / lambd + psi) + gabor[y, x] = np.exp(-(_x**2 + gamma**2 * _y**2) / (2 * sigma**2)) * np.cos( + 2 * np.pi * _x / lambd + psi + ) return gabor diff --git a/digital_image_processing/filters/gaussian_filter.py b/digital_image_processing/filters/gaussian_filter.py index 87fa67fb65ea..0c34e59fafe5 100644 --- a/digital_image_processing/filters/gaussian_filter.py +++ b/digital_image_processing/filters/gaussian_filter.py @@ -1,6 +1,7 @@ """ Implementation of gaussian filter algorithm """ + from itertools import product from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey @@ -22,11 +23,9 @@ def gaussian_filter(image, k_size, sigma): # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows image_array = zeros((dst_height * dst_width, k_size * k_size)) - row = 0 - for i, j in product(range(dst_height), range(dst_width)): + for row, (i, j) in enumerate(product(range(dst_height), range(dst_width))): window = ravel(image[i : i + k_size, j : j + k_size]) image_array[row, :] = window - row += 1 # turn the kernel into shape(k*k, 1) gaussian_kernel = gen_gaussian_kernel(k_size, sigma) diff --git a/digital_image_processing/filters/laplacian_filter.py b/digital_image_processing/filters/laplacian_filter.py new file mode 100644 index 000000000000..69b9616e4d30 --- /dev/null +++ b/digital_image_processing/filters/laplacian_filter.py @@ -0,0 +1,81 @@ +# @Author : ojas-wani +# @File : laplacian_filter.py +# @Date : 10/04/2023 + +import numpy as np +from cv2 import ( + BORDER_DEFAULT, + COLOR_BGR2GRAY, + CV_64F, + cvtColor, + filter2D, + imread, + imshow, + waitKey, +) + +from digital_image_processing.filters.gaussian_filter import gaussian_filter + + +def my_laplacian(src: np.ndarray, ksize: int) -> np.ndarray: + """ + :param src: the source image, which should be a grayscale or color image. + :param ksize: the size of the kernel used to compute the Laplacian filter, + which can be 1, 3, 5, or 7. + + >>> my_laplacian(src=np.array([]), ksize=0) + Traceback (most recent call last): + ... + ValueError: ksize must be in (1, 3, 5, 7) + """ + kernels = { + 1: np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]), + 3: np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]), + 5: np.array( + [ + [0, 0, -1, 0, 0], + [0, -1, -2, -1, 0], + [-1, -2, 16, -2, -1], + [0, -1, -2, -1, 0], + [0, 0, -1, 0, 0], + ] + ), + 7: np.array( + [ + [0, 0, 0, -1, 0, 0, 0], + [0, 0, -2, -3, -2, 0, 0], + [0, -2, -7, -10, -7, -2, 0], + [-1, -3, -10, 68, -10, -3, -1], + [0, -2, -7, -10, -7, -2, 0], + [0, 0, -2, -3, -2, 0, 0], + [0, 0, 0, -1, 0, 0, 0], + ] + ), + } + if ksize not in kernels: + msg = f"ksize must be in {tuple(kernels)}" + raise ValueError(msg) + + # Apply the Laplacian kernel using convolution + return filter2D( + src, CV_64F, kernels[ksize], 0, borderType=BORDER_DEFAULT, anchor=(0, 0) + ) + + +if __name__ == "__main__": + # read original image + img = imread(r"../image_data/lena.jpg") + + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + # Applying gaussian filter + blur_image = gaussian_filter(gray, 3, sigma=1) + + # Apply multiple Kernel to detect edges + laplacian_image = my_laplacian(ksize=3, src=blur_image) + + imshow("Original image", img) + imshow("Detected edges using laplacian filter", laplacian_image) + + waitKey(0) diff --git a/digital_image_processing/filters/local_binary_pattern.py b/digital_image_processing/filters/local_binary_pattern.py index 907fe2cb0555..861369ba6a32 100644 --- a/digital_image_processing/filters/local_binary_pattern.py +++ b/digital_image_processing/filters/local_binary_pattern.py @@ -71,8 +71,8 @@ def local_binary_value(image: np.ndarray, x_coordinate: int, y_coordinate: int) # Iterating through the image and calculating the # local binary pattern value for each pixel. - for i in range(0, image.shape[0]): - for j in range(0, image.shape[1]): + for i in range(image.shape[0]): + for j in range(image.shape[1]): lbp_image[i][j] = local_binary_value(image, i, j) cv2.imshow("local binary pattern", lbp_image) diff --git a/digital_image_processing/filters/median_filter.py b/digital_image_processing/filters/median_filter.py index 174018569d62..fc8b582ef67a 100644 --- a/digital_image_processing/filters/median_filter.py +++ b/digital_image_processing/filters/median_filter.py @@ -1,6 +1,7 @@ """ Implementation of median filter algorithm """ + from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey from numpy import divide, int8, multiply, ravel, sort, zeros_like diff --git a/digital_image_processing/histogram_equalization/histogram_stretch.py b/digital_image_processing/histogram_equalization/histogram_stretch.py index 5ea7773e32d9..1270c964dee6 100644 --- a/digital_image_processing/histogram_equalization/histogram_stretch.py +++ b/digital_image_processing/histogram_equalization/histogram_stretch.py @@ -3,6 +3,7 @@ @author: Binish125 """ + import copy import os diff --git a/digital_image_processing/index_calculation.py b/digital_image_processing/index_calculation.py index 67830668b0da..988f8e72b9a8 100644 --- a/digital_image_processing/index_calculation.py +++ b/digital_image_processing/index_calculation.py @@ -182,7 +182,7 @@ def arv12(self): Atmospherically Resistant Vegetation Index 2 https://www.indexdatabase.de/db/i-single.php?id=396 :return: index - −0.18+1.17*(self.nir−self.red)/(self.nir+self.red) + -0.18+1.17*(self.nir-self.red)/(self.nir+self.red) """ return -0.18 + (1.17 * ((self.nir - self.red) / (self.nir + self.red))) diff --git a/digital_image_processing/morphological_operations/__init__.py b/digital_image_processing/morphological_operations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/morphological_operations/dilation_operation.py b/digital_image_processing/morphological_operations/dilation_operation.py index 274880b0a50a..e49b955c1480 100644 --- a/digital_image_processing/morphological_operations/dilation_operation.py +++ b/digital_image_processing/morphological_operations/dilation_operation.py @@ -1,41 +1,43 @@ +from pathlib import Path + import numpy as np from PIL import Image -def rgb2gray(rgb: np.array) -> np.array: +def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: """ Return gray image from rgb image - >>> rgb2gray(np.array([[[127, 255, 0]]])) + >>> rgb_to_gray(np.array([[[127, 255, 0]]])) array([[187.6453]]) - >>> rgb2gray(np.array([[[0, 0, 0]]])) + >>> rgb_to_gray(np.array([[[0, 0, 0]]])) array([[0.]]) - >>> rgb2gray(np.array([[[2, 4, 1]]])) + >>> rgb_to_gray(np.array([[[2, 4, 1]]])) array([[3.0598]]) - >>> rgb2gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) + >>> rgb_to_gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) array([[159.0524, 90.0635, 117.6989]]) """ r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2] return 0.2989 * r + 0.5870 * g + 0.1140 * b -def gray2binary(gray: np.array) -> np.array: +def gray_to_binary(gray: np.ndarray) -> np.ndarray: """ Return binary image from gray image - >>> gray2binary(np.array([[127, 255, 0]])) + >>> gray_to_binary(np.array([[127, 255, 0]])) array([[False, True, False]]) - >>> gray2binary(np.array([[0]])) + >>> gray_to_binary(np.array([[0]])) array([[False]]) - >>> gray2binary(np.array([[26.2409, 4.9315, 1.4729]])) + >>> gray_to_binary(np.array([[26.2409, 4.9315, 1.4729]])) array([[False, False, False]]) - >>> gray2binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) + >>> gray_to_binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) array([[False, True, False], [False, True, False], [False, True, False]]) """ - return (127 < gray) & (gray <= 255) + return (gray > 127) & (gray <= 255) -def dilation(image: np.array, kernel: np.array) -> np.array: +def dilation(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: """ Return dilated image >>> dilation(np.array([[True, False, True]]), np.array([[0, 1, 0]])) @@ -61,14 +63,13 @@ def dilation(image: np.array, kernel: np.array) -> np.array: return output -# kernel to be applied -structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) - - if __name__ == "__main__": # read original image - image = np.array(Image.open(r"..\image_data\lena.jpg")) - output = dilation(gray2binary(rgb2gray(image)), structuring_element) + lena_path = Path(__file__).resolve().parent / "image_data" / "lena.jpg" + lena = np.array(Image.open(lena_path)) + # kernel to be applied + structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) + output = dilation(gray_to_binary(rgb_to_gray(lena)), structuring_element) # Save the output image pil_img = Image.fromarray(output).convert("RGB") pil_img.save("result_dilation.png") diff --git a/digital_image_processing/morphological_operations/erosion_operation.py b/digital_image_processing/morphological_operations/erosion_operation.py index 4b0a5eee8c03..53001da83468 100644 --- a/digital_image_processing/morphological_operations/erosion_operation.py +++ b/digital_image_processing/morphological_operations/erosion_operation.py @@ -1,43 +1,48 @@ +from pathlib import Path + import numpy as np from PIL import Image -def rgb2gray(rgb: np.array) -> np.array: +def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: """ Return gray image from rgb image - >>> rgb2gray(np.array([[[127, 255, 0]]])) + + >>> rgb_to_gray(np.array([[[127, 255, 0]]])) array([[187.6453]]) - >>> rgb2gray(np.array([[[0, 0, 0]]])) + >>> rgb_to_gray(np.array([[[0, 0, 0]]])) array([[0.]]) - >>> rgb2gray(np.array([[[2, 4, 1]]])) + >>> rgb_to_gray(np.array([[[2, 4, 1]]])) array([[3.0598]]) - >>> rgb2gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) + >>> rgb_to_gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) array([[159.0524, 90.0635, 117.6989]]) """ r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2] return 0.2989 * r + 0.5870 * g + 0.1140 * b -def gray2binary(gray: np.array) -> np.array: +def gray_to_binary(gray: np.ndarray) -> np.ndarray: """ Return binary image from gray image - >>> gray2binary(np.array([[127, 255, 0]])) + + >>> gray_to_binary(np.array([[127, 255, 0]])) array([[False, True, False]]) - >>> gray2binary(np.array([[0]])) + >>> gray_to_binary(np.array([[0]])) array([[False]]) - >>> gray2binary(np.array([[26.2409, 4.9315, 1.4729]])) + >>> gray_to_binary(np.array([[26.2409, 4.9315, 1.4729]])) array([[False, False, False]]) - >>> gray2binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) + >>> gray_to_binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) array([[False, True, False], [False, True, False], [False, True, False]]) """ - return (127 < gray) & (gray <= 255) + return (gray > 127) & (gray <= 255) -def erosion(image: np.array, kernel: np.array) -> np.array: +def erosion(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: """ Return eroded image + >>> erosion(np.array([[True, True, False]]), np.array([[0, 1, 0]])) array([[False, False, False]]) >>> erosion(np.array([[True, False, False]]), np.array([[1, 1, 0]])) @@ -61,14 +66,17 @@ def erosion(image: np.array, kernel: np.array) -> np.array: return output -# kernel to be applied -structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) - if __name__ == "__main__": # read original image - image = np.array(Image.open(r"..\image_data\lena.jpg")) + lena_path = Path(__file__).resolve().parent / "image_data" / "lena.jpg" + lena = np.array(Image.open(lena_path)) + + # kernel to be applied + structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) + # Apply erosion operation to a binary image - output = erosion(gray2binary(rgb2gray(image)), structuring_element) + output = erosion(gray_to_binary(rgb_to_gray(lena)), structuring_element) + # Save the output image pil_img = Image.fromarray(output).convert("RGB") pil_img.save("result_erosion.png") diff --git a/digital_image_processing/resize/resize.py b/digital_image_processing/resize/resize.py index 4836521f9f58..7bde118da69b 100644 --- a/digital_image_processing/resize/resize.py +++ b/digital_image_processing/resize/resize.py @@ -1,4 +1,5 @@ -""" Multiple image resizing techniques """ +"""Multiple image resizing techniques""" + import numpy as np from cv2 import destroyAllWindows, imread, imshow, waitKey diff --git a/digital_image_processing/rotation/rotation.py b/digital_image_processing/rotation/rotation.py index 958d16fafb91..0f5e36ddd5be 100644 --- a/digital_image_processing/rotation/rotation.py +++ b/digital_image_processing/rotation/rotation.py @@ -10,12 +10,12 @@ def get_rotation( ) -> np.ndarray: """ Get image rotation - :param img: np.array + :param img: np.ndarray :param pt1: 3x2 list :param pt2: 3x2 list :param rows: columns image shape :param cols: rows image shape - :return: np.array + :return: np.ndarray """ matrix = cv2.getAffineTransform(pt1, pt2) return cv2.warpAffine(img, matrix, (rows, cols)) diff --git a/digital_image_processing/sepia.py b/digital_image_processing/sepia.py index e9dd2c06066d..1924a80451e5 100644 --- a/digital_image_processing/sepia.py +++ b/digital_image_processing/sepia.py @@ -1,6 +1,7 @@ """ - Implemented an algorithm using opencv to tone an image with sepia technique +Implemented an algorithm using opencv to tone an image with sepia technique """ + from cv2 import destroyAllWindows, imread, imshow, waitKey diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py index c999464ce85e..d1200f4d65ca 100644 --- a/digital_image_processing/test_digital_image_processing.py +++ b/digital_image_processing/test_digital_image_processing.py @@ -1,6 +1,7 @@ """ PyTest's for Digital Image Processing """ + import numpy as np from cv2 import COLOR_BGR2GRAY, cvtColor, imread from numpy import array, uint8 @@ -73,7 +74,8 @@ def test_median_filter(): def test_sobel_filter(): grad, theta = sob.sobel_filter(gray) - assert grad.any() and theta.any() + assert grad.any() + assert theta.any() def test_sepia(): @@ -96,9 +98,16 @@ def test_nearest_neighbour( def test_local_binary_pattern(): - file_path: str = "digital_image_processing/image_data/lena.jpg" + # pull request 10161 before: + # "digital_image_processing/image_data/lena.jpg" + # after: "digital_image_processing/image_data/lena_small.jpg" + + from os import getenv # Speed up our Continuous Integration tests + + file_name = "lena_small.jpg" if getenv("CI") else "lena.jpg" + file_path = f"digital_image_processing/image_data/{file_name}" - # Reading the image and converting it to grayscale. + # Reading the image and converting it to grayscale image = imread(file_path, 0) # Test for get_neighbors_pixel function() return not None @@ -118,8 +127,8 @@ def test_local_binary_pattern(): # Iterating through the image and calculating the local binary pattern value # for each pixel. - for i in range(0, image.shape[0]): - for j in range(0, image.shape[1]): + for i in range(image.shape[0]): + for j in range(image.shape[1]): lbp_image[i][j] = lbp.local_binary_value(image, i, j) assert lbp_image.any() diff --git a/divide_and_conquer/closest_pair_of_points.py b/divide_and_conquer/closest_pair_of_points.py index cb7fa00d1c8f..534cbba9b718 100644 --- a/divide_and_conquer/closest_pair_of_points.py +++ b/divide_and_conquer/closest_pair_of_points.py @@ -54,8 +54,7 @@ def dis_between_closest_pair(points, points_counts, min_dis=float("inf")): for i in range(points_counts - 1): for j in range(i + 1, points_counts): current_dis = euclidean_distance_sqr(points[i], points[j]) - if current_dis < min_dis: - min_dis = current_dis + min_dis = min(min_dis, current_dis) return min_dis @@ -76,8 +75,7 @@ def dis_between_closest_in_strip(points, points_counts, min_dis=float("inf")): for i in range(min(6, points_counts - 1), points_counts): for j in range(max(0, i - 6), i): current_dis = euclidean_distance_sqr(points[i], points[j]) - if current_dis < min_dis: - min_dis = current_dis + min_dis = min(min_dis, current_dis) return min_dis diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 39e78be04a71..93f6daf1f88c 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -12,6 +12,7 @@ which have not been implemented here, yet. """ + from __future__ import annotations from collections.abc import Iterable @@ -174,12 +175,12 @@ def _validate_input(points: list[Point] | list[list[float]]) -> list[Point]: """ if not hasattr(points, "__iter__"): - raise ValueError( - f"Expecting an iterable object but got an non-iterable type {points}" - ) + msg = f"Expecting an iterable object but got an non-iterable type {points}" + raise ValueError(msg) if not points: - raise ValueError(f"Expecting a list of points but got {points}") + msg = f"Expecting a list of points but got {points}" + raise ValueError(msg) return _construct_points(points) @@ -266,21 +267,20 @@ def convex_hull_bf(points: list[Point]) -> list[Point]: points_left_of_ij = points_right_of_ij = False ij_part_of_convex_hull = True for k in range(n): - if k != i and k != j: + if k not in {i, j}: det_k = _det(points[i], points[j], points[k]) if det_k > 0: points_left_of_ij = True elif det_k < 0: points_right_of_ij = True - else: - # point[i], point[j], point[k] all lie on a straight line - # if point[k] is to the left of point[i] or it's to the - # right of point[j], then point[i], point[j] cannot be - # part of the convex hull of A - if points[k] < points[i] or points[k] > points[j]: - ij_part_of_convex_hull = False - break + # point[i], point[j], point[k] all lie on a straight line + # if point[k] is to the left of point[i] or it's to the + # right of point[j], then point[i], point[j] cannot be + # part of the convex hull of A + elif points[k] < points[i] or points[k] > points[j]: + ij_part_of_convex_hull = False + break if points_left_of_ij and points_right_of_ij: ij_part_of_convex_hull = False diff --git a/divide_and_conquer/kth_order_statistic.py b/divide_and_conquer/kth_order_statistic.py index 666ad1a39b8a..23fd8be5ea47 100644 --- a/divide_and_conquer/kth_order_statistic.py +++ b/divide_and_conquer/kth_order_statistic.py @@ -8,6 +8,7 @@ For more information of this algorithm: https://web.stanford.edu/class/archive/cs/cs161/cs161.1138/lectures/08/Small08.pdf """ + from __future__ import annotations from random import choice diff --git a/divide_and_conquer/max_subarray.py b/divide_and_conquer/max_subarray.py new file mode 100644 index 000000000000..0fad7ab5d920 --- /dev/null +++ b/divide_and_conquer/max_subarray.py @@ -0,0 +1,113 @@ +""" +The maximum subarray problem is the task of finding the continuous subarray that has the +maximum sum within a given array of numbers. For example, given the array +[-2, 1, -3, 4, -1, 2, 1, -5, 4], the contiguous subarray with the maximum sum is +[4, -1, 2, 1], which has a sum of 6. + +This divide-and-conquer algorithm finds the maximum subarray in O(n log n) time. +""" + +from __future__ import annotations + +import time +from collections.abc import Sequence +from random import randint + +from matplotlib import pyplot as plt + + +def max_subarray( + arr: Sequence[float], low: int, high: int +) -> tuple[int | None, int | None, float]: + """ + Solves the maximum subarray problem using divide and conquer. + :param arr: the given array of numbers + :param low: the start index + :param high: the end index + :return: the start index of the maximum subarray, the end index of the + maximum subarray, and the maximum subarray sum + + >>> nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] + >>> max_subarray(nums, 0, len(nums) - 1) + (3, 6, 6) + >>> nums = [2, 8, 9] + >>> max_subarray(nums, 0, len(nums) - 1) + (0, 2, 19) + >>> nums = [0, 0] + >>> max_subarray(nums, 0, len(nums) - 1) + (0, 0, 0) + >>> nums = [-1.0, 0.0, 1.0] + >>> max_subarray(nums, 0, len(nums) - 1) + (2, 2, 1.0) + >>> nums = [-2, -3, -1, -4, -6] + >>> max_subarray(nums, 0, len(nums) - 1) + (2, 2, -1) + >>> max_subarray([], 0, 0) + (None, None, 0) + """ + if not arr: + return None, None, 0 + if low == high: + return low, high, arr[low] + + mid = (low + high) // 2 + left_low, left_high, left_sum = max_subarray(arr, low, mid) + right_low, right_high, right_sum = max_subarray(arr, mid + 1, high) + cross_left, cross_right, cross_sum = max_cross_sum(arr, low, mid, high) + if left_sum >= right_sum and left_sum >= cross_sum: + return left_low, left_high, left_sum + elif right_sum >= left_sum and right_sum >= cross_sum: + return right_low, right_high, right_sum + return cross_left, cross_right, cross_sum + + +def max_cross_sum( + arr: Sequence[float], low: int, mid: int, high: int +) -> tuple[int, int, float]: + left_sum, max_left = float("-inf"), -1 + right_sum, max_right = float("-inf"), -1 + + summ: int | float = 0 + for i in range(mid, low - 1, -1): + summ += arr[i] + if summ > left_sum: + left_sum = summ + max_left = i + + summ = 0 + for i in range(mid + 1, high + 1): + summ += arr[i] + if summ > right_sum: + right_sum = summ + max_right = i + + return max_left, max_right, (left_sum + right_sum) + + +def time_max_subarray(input_size: int) -> float: + arr = [randint(1, input_size) for _ in range(input_size)] + start = time.time() + max_subarray(arr, 0, input_size - 1) + end = time.time() + return end - start + + +def plot_runtimes() -> None: + input_sizes = [10, 100, 1000, 10000, 50000, 100000, 200000, 300000, 400000, 500000] + runtimes = [time_max_subarray(input_size) for input_size in input_sizes] + print("No of Inputs\t\tTime Taken") + for input_size, runtime in zip(input_sizes, runtimes): + print(input_size, "\t\t", runtime) + plt.plot(input_sizes, runtimes) + plt.xlabel("Number of Inputs") + plt.ylabel("Time taken in seconds") + plt.show() + + +if __name__ == "__main__": + """ + A random simulation of this algorithm. + """ + from doctest import testmod + + testmod() diff --git a/divide_and_conquer/max_subarray_sum.py b/divide_and_conquer/max_subarray_sum.py deleted file mode 100644 index f23e81719025..000000000000 --- a/divide_and_conquer/max_subarray_sum.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Given a array of length n, max_subarray_sum() finds -the maximum of sum of contiguous sub-array using divide and conquer method. - -Time complexity : O(n log n) - -Ref : INTRODUCTION TO ALGORITHMS THIRD EDITION -(section : 4, sub-section : 4.1, page : 70) - -""" - - -def max_sum_from_start(array): - """This function finds the maximum contiguous sum of array from 0 index - - Parameters : - array (list[int]) : given array - - Returns : - max_sum (int) : maximum contiguous sum of array from 0 index - - """ - array_sum = 0 - max_sum = float("-inf") - for num in array: - array_sum += num - if array_sum > max_sum: - max_sum = array_sum - return max_sum - - -def max_cross_array_sum(array, left, mid, right): - """This function finds the maximum contiguous sum of left and right arrays - - Parameters : - array, left, mid, right (list[int], int, int, int) - - Returns : - (int) : maximum of sum of contiguous sum of left and right arrays - - """ - - max_sum_of_left = max_sum_from_start(array[left : mid + 1][::-1]) - max_sum_of_right = max_sum_from_start(array[mid + 1 : right + 1]) - return max_sum_of_left + max_sum_of_right - - -def max_subarray_sum(array, left, right): - """Maximum contiguous sub-array sum, using divide and conquer method - - Parameters : - array, left, right (list[int], int, int) : - given array, current left index and current right index - - Returns : - int : maximum of sum of contiguous sub-array - - """ - - # base case: array has only one element - if left == right: - return array[right] - - # Recursion - mid = (left + right) // 2 - left_half_sum = max_subarray_sum(array, left, mid) - right_half_sum = max_subarray_sum(array, mid + 1, right) - cross_sum = max_cross_array_sum(array, left, mid, right) - return max(left_half_sum, right_half_sum, cross_sum) - - -if __name__ == "__main__": - array = [-2, -5, 6, -2, -3, 1, 5, -6] - array_length = len(array) - print( - "Maximum sum of contiguous subarray:", - max_subarray_sum(array, 0, array_length - 1), - ) diff --git a/divide_and_conquer/peak.py b/divide_and_conquer/peak.py index e60f28bfbe29..71ab5ac86574 100644 --- a/divide_and_conquer/peak.py +++ b/divide_and_conquer/peak.py @@ -7,6 +7,7 @@ (From Kleinberg and Tardos. Algorithm Design. Addison Wesley 2006: Chapter 5 Solved Exercise 1) """ + from __future__ import annotations diff --git a/divide_and_conquer/power.py b/divide_and_conquer/power.py index f2e023afd536..faf6a3476d40 100644 --- a/divide_and_conquer/power.py +++ b/divide_and_conquer/power.py @@ -2,6 +2,20 @@ def actual_power(a: int, b: int): """ Function using divide and conquer to calculate a^b. It only works for integer a,b. + + :param a: The base of the power operation, an integer. + :param b: The exponent of the power operation, a non-negative integer. + :return: The result of a^b. + + Examples: + >>> actual_power(3, 2) + 9 + >>> actual_power(5, 3) + 125 + >>> actual_power(2, 5) + 32 + >>> actual_power(7, 0) + 1 """ if b == 0: return 1 @@ -13,6 +27,10 @@ def actual_power(a: int, b: int): def power(a: int, b: int) -> float: """ + :param a: The base (integer). + :param b: The exponent (integer). + :return: The result of a^b, as a float for negative exponents. + >>> power(4,6) 4096 >>> power(2,3) diff --git a/divide_and_conquer/strassen_matrix_multiplication.py b/divide_and_conquer/strassen_matrix_multiplication.py index 371605d6d4d4..f529a255d2ef 100644 --- a/divide_and_conquer/strassen_matrix_multiplication.py +++ b/divide_and_conquer/strassen_matrix_multiplication.py @@ -74,7 +74,7 @@ def print_matrix(matrix: list) -> None: def actual_strassen(matrix_a: list, matrix_b: list) -> list: """ Recursive function to calculate the product of two matrices, using the Strassen - Algorithm. It only supports even length matrices. + Algorithm. It only supports square matrices of any size that is a power of 2. """ if matrix_dimensions(matrix_a) == (2, 2): return default_matrix_multiplication(matrix_a, matrix_b) @@ -112,24 +112,26 @@ def strassen(matrix1: list, matrix2: list) -> list: [[139, 163], [121, 134], [100, 121]] """ if matrix_dimensions(matrix1)[1] != matrix_dimensions(matrix2)[0]: - raise Exception( - "Unable to multiply these matrices, please check the dimensions. \n" - f"Matrix A:{matrix1} \nMatrix B:{matrix2}" + msg = ( + "Unable to multiply these matrices, please check the dimensions.\n" + f"Matrix A: {matrix1}\n" + f"Matrix B: {matrix2}" ) + raise Exception(msg) dimension1 = matrix_dimensions(matrix1) dimension2 = matrix_dimensions(matrix2) if dimension1[0] == dimension1[1] and dimension2[0] == dimension2[1]: return [matrix1, matrix2] - maximum = max(max(dimension1), max(dimension2)) + maximum = max(*dimension1, *dimension2) maxim = int(math.pow(2, math.ceil(math.log2(maximum)))) new_matrix1 = matrix1 new_matrix2 = matrix2 - # Adding zeros to the matrices so that the arrays dimensions are the same and also - # power of 2 - for i in range(0, maxim): + # Adding zeros to the matrices to convert them both into square matrices of equal + # dimensions that are a power of 2 + for i in range(maxim): if i < dimension1[0]: for _ in range(dimension1[1], maxim): new_matrix1[i].append(0) @@ -144,7 +146,7 @@ def strassen(matrix1: list, matrix2: list) -> list: final_matrix = actual_strassen(new_matrix1, new_matrix2) # Removing the additional zeros - for i in range(0, maxim): + for i in range(maxim): if i < dimension1[0]: for _ in range(dimension2[1], maxim): final_matrix[i].pop() diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000000..f2481f107267 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,3 @@ +from sphinx_pyproject import SphinxConfig + +project = SphinxConfig("../pyproject.toml", globalns=globals()).name diff --git a/docs/source/__init__.py b/docs/source/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dynamic_programming/all_construct.py b/dynamic_programming/all_construct.py index 3839d01e6db0..ca00f2beb06a 100644 --- a/dynamic_programming/all_construct.py +++ b/dynamic_programming/all_construct.py @@ -2,14 +2,16 @@ Program to list all the ways a target string can be constructed from the given list of substrings """ + from __future__ import annotations def all_construct(target: str, word_bank: list[str] | None = None) -> list[list[str]]: """ - returns the list containing all the possible - combinations a string(target) can be constructed from - the given list of substrings(word_bank) + returns the list containing all the possible + combinations a string(`target`) can be constructed from + the given list of substrings(`word_bank`) + >>> all_construct("hello", ["he", "l", "o"]) [['he', 'l', 'l', 'o']] >>> all_construct("purple",["purp","p","ur","le","purpl"]) @@ -34,7 +36,7 @@ def all_construct(target: str, word_bank: list[str] | None = None) -> list[list[ # slice condition if target[i : i + len(word)] == word: new_combinations: list[list[str]] = [ - [word] + way for way in table[i] + [word, *way] for way in table[i] ] # adds the word to every combination the current position holds # now,push that combination to the table[i+len(word)] diff --git a/dynamic_programming/bitmask.py b/dynamic_programming/bitmask.py index 56bb8e96ba02..a6e6a0cda7bf 100644 --- a/dynamic_programming/bitmask.py +++ b/dynamic_programming/bitmask.py @@ -8,6 +8,7 @@ a person can do only one task and a task is performed only by one person. Find the total no of ways in which the tasks can be distributed. """ + from collections import defaultdict diff --git a/dynamic_programming/climbing_stairs.py b/dynamic_programming/climbing_stairs.py index d6273d025f08..38bdb427eedc 100644 --- a/dynamic_programming/climbing_stairs.py +++ b/dynamic_programming/climbing_stairs.py @@ -25,9 +25,9 @@ def climb_stairs(number_of_steps: int) -> int: ... AssertionError: number_of_steps needs to be positive integer, your input -7 """ - assert ( - isinstance(number_of_steps, int) and number_of_steps > 0 - ), f"number_of_steps needs to be positive integer, your input {number_of_steps}" + assert isinstance(number_of_steps, int) and number_of_steps > 0, ( + f"number_of_steps needs to be positive integer, your input {number_of_steps}" + ) if number_of_steps == 1: return 1 previous, current = 1, 1 diff --git a/dynamic_programming/combination_sum_iv.py b/dynamic_programming/combination_sum_iv.py index b2aeb0824f64..ed8dcd88e6fd 100644 --- a/dynamic_programming/combination_sum_iv.py +++ b/dynamic_programming/combination_sum_iv.py @@ -1,33 +1,34 @@ """ Question: -You are given an array of distinct integers and you have to tell how many -different ways of selecting the elements from the array are there such that -the sum of chosen elements is equal to the target number tar. + You are given an array of distinct integers and you have to tell how many + different ways of selecting the elements from the array are there such that + the sum of chosen elements is equal to the target number tar. Example Input: -N = 3 -target = 5 -array = [1, 2, 5] + * N = 3 + * target = 5 + * array = [1, 2, 5] Output: -9 + 9 Approach: -The basic idea is to go over recursively to find the way such that the sum -of chosen elements is “tarâ€. For every element, we have two choices - 1. Include the element in our set of chosen elements. - 2. Don’t include the element in our set of chosen elements. + The basic idea is to go over recursively to find the way such that the sum + of chosen elements is `target`. For every element, we have two choices + + 1. Include the element in our set of chosen elements. + 2. Don't include the element in our set of chosen elements. """ -def combination_sum_iv(n: int, array: list[int], target: int) -> int: +def combination_sum_iv(array: list[int], target: int) -> int: """ Function checks the all possible combinations, and returns the count of possible combination in exponential Time Complexity. - >>> combination_sum_iv(3, [1,2,5], 5) + >>> combination_sum_iv([1,2,5], 5) 9 """ @@ -41,13 +42,13 @@ def count_of_possible_combinations(target: int) -> int: return count_of_possible_combinations(target) -def combination_sum_iv_dp_array(n: int, array: list[int], target: int) -> int: +def combination_sum_iv_dp_array(array: list[int], target: int) -> int: """ Function checks the all possible combinations, and returns the count of possible combination in O(N^2) Time Complexity as we are using Dynamic programming array here. - >>> combination_sum_iv_dp_array(3, [1,2,5], 5) + >>> combination_sum_iv_dp_array([1,2,5], 5) 9 """ @@ -96,7 +97,6 @@ def combination_sum_iv_bottom_up(n: int, array: list[int], target: int) -> int: import doctest doctest.testmod() - n = 3 target = 5 array = [1, 2, 5] - print(combination_sum_iv(n, array, target)) + print(combination_sum_iv(array, target)) diff --git a/dynamic_programming/fast_fibonacci.py b/dynamic_programming/fast_fibonacci.py index f48186a34c25..d04a5ac8249b 100644 --- a/dynamic_programming/fast_fibonacci.py +++ b/dynamic_programming/fast_fibonacci.py @@ -4,6 +4,7 @@ This program calculates the nth Fibonacci number in O(log(n)). It's possible to calculate F(1_000_000) in less than a second. """ + from __future__ import annotations import sys @@ -25,7 +26,7 @@ def _fib(n: int) -> tuple[int, int]: if n == 0: # (F(0), F(1)) return (0, 1) - # F(2n) = F(n)[2F(n+1) − F(n)] + # F(2n) = F(n)[2F(n+1) - F(n)] # F(2n+1) = F(n+1)^2+F(n)^2 a, b = _fib(n // 2) c = a * (b * 2 - a) diff --git a/dynamic_programming/fibonacci.py b/dynamic_programming/fibonacci.py index 7ec5993ef38d..c102493aa00b 100644 --- a/dynamic_programming/fibonacci.py +++ b/dynamic_programming/fibonacci.py @@ -24,7 +24,7 @@ def get(self, index: int) -> list: return self.sequence[:index] -def main(): +def main() -> None: print( "Fibonacci Series Using Dynamic Programming\n", "Enter the index of the Fibonacci number you want to calculate ", diff --git a/dynamic_programming/fizz_buzz.py b/dynamic_programming/fizz_buzz.py index e77ab3de7b4b..0cb48897875b 100644 --- a/dynamic_programming/fizz_buzz.py +++ b/dynamic_programming/fizz_buzz.py @@ -3,11 +3,12 @@ def fizz_buzz(number: int, iterations: int) -> str: """ - Plays FizzBuzz. - Prints Fizz if number is a multiple of 3. - Prints Buzz if its a multiple of 5. - Prints FizzBuzz if its a multiple of both 3 and 5 or 15. - Else Prints The Number Itself. + | Plays FizzBuzz. + | Prints Fizz if number is a multiple of ``3``. + | Prints Buzz if its a multiple of ``5``. + | Prints FizzBuzz if its a multiple of both ``3`` and ``5`` or ``15``. + | Else Prints The Number Itself. + >>> fizz_buzz(1,7) '1 2 Fizz 4 Buzz Fizz 7 ' >>> fizz_buzz(1,0) @@ -49,7 +50,7 @@ def fizz_buzz(number: int, iterations: int) -> str: out += "Fizz" if number % 5 == 0: out += "Buzz" - if not number % 3 == 0 and not number % 5 == 0: + if 0 not in (number % 3, number % 5): out += str(number) # print(out) diff --git a/dynamic_programming/floyd_warshall.py b/dynamic_programming/floyd_warshall.py index 614a3c72a992..b92c6667fb5c 100644 --- a/dynamic_programming/floyd_warshall.py +++ b/dynamic_programming/floyd_warshall.py @@ -5,26 +5,65 @@ class Graph: def __init__(self, n=0): # a graph with Node 0,1,...,N-1 self.n = n self.w = [ - [math.inf for j in range(0, n)] for i in range(0, n) + [math.inf for j in range(n)] for i in range(n) ] # adjacency matrix for weight self.dp = [ - [math.inf for j in range(0, n)] for i in range(0, n) + [math.inf for j in range(n)] for i in range(n) ] # dp[i][j] stores minimum distance from i to j def add_edge(self, u, v, w): + """ + Adds a directed edge from node u + to node v with weight w. + + >>> g = Graph(3) + >>> g.add_edge(0, 1, 5) + >>> g.dp[0][1] + 5 + """ self.dp[u][v] = w def floyd_warshall(self): - for k in range(0, self.n): - for i in range(0, self.n): - for j in range(0, self.n): + """ + Computes the shortest paths between all pairs of + nodes using the Floyd-Warshall algorithm. + + >>> g = Graph(3) + >>> g.add_edge(0, 1, 1) + >>> g.add_edge(1, 2, 2) + >>> g.floyd_warshall() + >>> g.show_min(0, 2) + 3 + >>> g.show_min(2, 0) + inf + """ + for k in range(self.n): + for i in range(self.n): + for j in range(self.n): self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j]) def show_min(self, u, v): + """ + Returns the minimum distance from node u to node v. + + >>> g = Graph(3) + >>> g.add_edge(0, 1, 3) + >>> g.add_edge(1, 2, 4) + >>> g.floyd_warshall() + >>> g.show_min(0, 2) + 7 + >>> g.show_min(1, 0) + inf + """ return self.dp[u][v] if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example usage graph = Graph(5) graph.add_edge(0, 2, 9) graph.add_edge(0, 4, 10) @@ -38,5 +77,9 @@ def show_min(self, u, v): graph.add_edge(4, 2, 4) graph.add_edge(4, 3, 9) graph.floyd_warshall() - graph.show_min(1, 4) - graph.show_min(0, 3) + print( + graph.show_min(1, 4) + ) # Should output the minimum distance from node 1 to node 4 + print( + graph.show_min(0, 3) + ) # Should output the minimum distance from node 0 to node 3 diff --git a/dynamic_programming/integer_partition.py b/dynamic_programming/integer_partition.py index 8ed2e51bd4bd..145bc29d0fca 100644 --- a/dynamic_programming/integer_partition.py +++ b/dynamic_programming/integer_partition.py @@ -3,10 +3,34 @@ partitions into exactly k parts plus the number of partitions into at least k-1 parts. Subtracting 1 from each part of a partition of n into k parts gives a partition of n-k into k parts. These two facts together are used for this algorithm. +* https://en.wikipedia.org/wiki/Partition_(number_theory) +* https://en.wikipedia.org/wiki/Partition_function_(number_theory) """ def partition(m: int) -> int: + """ + >>> partition(5) + 7 + >>> partition(7) + 15 + >>> partition(100) + 190569292 + >>> partition(1_000) + 24061467864032622473692149727991 + >>> partition(-7) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> partition(0) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + >>> partition(7.8) + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + """ memo: list[list[int]] = [[0 for _ in range(m)] for _ in range(m + 1)] for i in range(m + 1): memo[i][0] = 1 diff --git a/dynamic_programming/iterating_through_submasks.py b/dynamic_programming/iterating_through_submasks.py index 4d0a250e8dfe..efab6dacff3f 100644 --- a/dynamic_programming/iterating_through_submasks.py +++ b/dynamic_programming/iterating_through_submasks.py @@ -5,6 +5,7 @@ its submasks. The mask s is submask of m if only bits that were included in bitmask are set """ + from __future__ import annotations @@ -36,9 +37,9 @@ def list_of_submasks(mask: int) -> list[int]: """ - assert ( - isinstance(mask, int) and mask > 0 - ), f"mask needs to be positive integer, your input {mask}" + assert isinstance(mask, int) and mask > 0, ( + f"mask needs to be positive integer, your input {mask}" + ) """ first submask iterated will be mask itself then operation will be performed diff --git a/dynamic_programming/k_means_clustering_tensorflow.py_tf b/dynamic_programming/k_means_clustering_tensorflow.py similarity index 98% rename from dynamic_programming/k_means_clustering_tensorflow.py_tf rename to dynamic_programming/k_means_clustering_tensorflow.py index 4fbcedeaa0dc..8d3f6f0dfbcb 100644 --- a/dynamic_programming/k_means_clustering_tensorflow.py_tf +++ b/dynamic_programming/k_means_clustering_tensorflow.py @@ -1,9 +1,10 @@ -import tensorflow as tf from random import shuffle + +import tensorflow as tf from numpy import array -def TFKMeansCluster(vectors, noofclusters): +def tf_k_means_cluster(vectors, noofclusters): """ K-Means Clustering using TensorFlow. 'vectors' should be a n*k 2-D NumPy array, where n is the number @@ -30,7 +31,6 @@ def TFKMeansCluster(vectors, noofclusters): graph = tf.Graph() with graph.as_default(): - # SESSION OF COMPUTATION sess = tf.Session() @@ -95,8 +95,7 @@ def TFKMeansCluster(vectors, noofclusters): # iterations. To keep things simple, we will only do a set number of # iterations, instead of using a Stopping Criterion. noofiterations = 100 - for iteration_n in range(noofiterations): - + for _ in range(noofiterations): ##EXPECTATION STEP ##Based on the centroid locations till last iteration, compute ##the _expected_ centroid assignments. diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index b12d30313e31..28c5b19dbe36 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -11,7 +11,7 @@ def mf_knapsack(i, wt, val, j): """ This code involves the concept of memory functions. Here we solve the subproblems which are needed unlike the below example - F is a 2D array with -1s filled up + F is a 2D array with ``-1`` s filled up """ global f # a global dp table for knapsack if f[i][j] < 0: @@ -45,22 +45,24 @@ def knapsack_with_example_solution(w: int, wt: list, val: list): the several possible optimal subsets. Parameters - --------- + ---------- - W: int, the total maximum weight for the given knapsack problem. - wt: list, the vector of weights for all items where wt[i] is the weight - of the i-th item. - val: list, the vector of values for all items where val[i] is the value - of the i-th item + * `w`: int, the total maximum weight for the given knapsack problem. + * `wt`: list, the vector of weights for all items where ``wt[i]`` is the weight + of the ``i``-th item. + * `val`: list, the vector of values for all items where ``val[i]`` is the value + of the ``i``-th item Returns ------- - optimal_val: float, the optimal value for the given knapsack problem - example_optional_set: set, the indices of one of the optimal subsets - which gave rise to the optimal value. + + * `optimal_val`: float, the optimal value for the given knapsack problem + * `example_optional_set`: set, the indices of one of the optimal subsets + which gave rise to the optimal value. Examples - ------- + -------- + >>> knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22]) (142, {2, 3, 4}) >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4, 4]) @@ -78,17 +80,18 @@ def knapsack_with_example_solution(w: int, wt: list, val: list): num_items = len(wt) if num_items != len(val): - raise ValueError( - "The number of weights must be the " - "same as the number of values.\nBut " - f"got {num_items} weights and {len(val)} values" + msg = ( + "The number of weights must be the same as the number of values.\n" + f"But got {num_items} weights and {len(val)} values" ) + raise ValueError(msg) for i in range(num_items): if not isinstance(wt[i], int): - raise TypeError( - "All weights must be integers but " - f"got weight of type {type(wt[i])} at index {i}" + msg = ( + "All weights must be integers but got weight of " + f"type {type(wt[i])} at index {i}" ) + raise TypeError(msg) optimal_val, dp_table = knapsack(w, wt, val, num_items) example_optional_set: set = set() @@ -103,19 +106,19 @@ def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set): a filled DP table and the vector of weights Parameters - --------- - - dp: list of list, the table of a solved integer weight dynamic programming problem + ---------- - wt: list or tuple, the vector of weights of the items - i: int, the index of the item under consideration - j: int, the current possible maximum weight - optimal_set: set, the optimal subset so far. This gets modified by the function. + * `dp`: list of list, the table of a solved integer weight dynamic programming + problem + * `wt`: list or tuple, the vector of weights of the items + * `i`: int, the index of the item under consideration + * `j`: int, the current possible maximum weight + * `optimal_set`: set, the optimal subset so far. This gets modified by the function. Returns ------- - None + ``None`` """ # for the current item i at a maximum weight j to be part of an optimal subset, # the optimal value at (i, j) must be greater than the optimal value at (i-1, j). diff --git a/dynamic_programming/largest_divisible_subset.py b/dynamic_programming/largest_divisible_subset.py new file mode 100644 index 000000000000..db38636e29db --- /dev/null +++ b/dynamic_programming/largest_divisible_subset.py @@ -0,0 +1,74 @@ +from __future__ import annotations + + +def largest_divisible_subset(items: list[int]) -> list[int]: + """ + Algorithm to find the biggest subset in the given array such that for any 2 elements + x and y in the subset, either x divides y or y divides x. + >>> largest_divisible_subset([1, 16, 7, 8, 4]) + [16, 8, 4, 1] + >>> largest_divisible_subset([1, 2, 3]) + [2, 1] + >>> largest_divisible_subset([-1, -2, -3]) + [-3] + >>> largest_divisible_subset([1, 2, 4, 8]) + [8, 4, 2, 1] + >>> largest_divisible_subset((1, 2, 4, 8)) + [8, 4, 2, 1] + >>> largest_divisible_subset([1, 1, 1]) + [1, 1, 1] + >>> largest_divisible_subset([0, 0, 0]) + [0, 0, 0] + >>> largest_divisible_subset([-1, -1, -1]) + [-1, -1, -1] + >>> largest_divisible_subset([]) + [] + """ + # Sort the array in ascending order as the sequence does not matter we only have to + # pick up a subset. + items = sorted(items) + + number_of_items = len(items) + + # Initialize memo with 1s and hash with increasing numbers + memo = [1] * number_of_items + hash_array = list(range(number_of_items)) + + # Iterate through the array + for i, item in enumerate(items): + for prev_index in range(i): + if ((items[prev_index] != 0 and item % items[prev_index]) == 0) and ( + (1 + memo[prev_index]) > memo[i] + ): + memo[i] = 1 + memo[prev_index] + hash_array[i] = prev_index + + ans = -1 + last_index = -1 + + # Find the maximum length and its corresponding index + for i, memo_item in enumerate(memo): + if memo_item > ans: + ans = memo_item + last_index = i + + # Reconstruct the divisible subset + if last_index == -1: + return [] + result = [items[last_index]] + while hash_array[last_index] != last_index: + last_index = hash_array[last_index] + result.append(items[last_index]) + + return result + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + items = [1, 16, 7, 8, 4] + print( + f"The longest divisible subset of {items} is {largest_divisible_subset(items)}." + ) diff --git a/dynamic_programming/longest_common_subsequence.py b/dynamic_programming/longest_common_subsequence.py index 3468fd87da8d..4a6c880aff61 100644 --- a/dynamic_programming/longest_common_subsequence.py +++ b/dynamic_programming/longest_common_subsequence.py @@ -28,6 +28,24 @@ def longest_common_subsequence(x: str, y: str): (2, 'ph') >>> longest_common_subsequence("computer", "food") (1, 'o') + >>> longest_common_subsequence("", "abc") # One string is empty + (0, '') + >>> longest_common_subsequence("abc", "") # Other string is empty + (0, '') + >>> longest_common_subsequence("", "") # Both strings are empty + (0, '') + >>> longest_common_subsequence("abc", "def") # No common subsequence + (0, '') + >>> longest_common_subsequence("abc", "abc") # Identical strings + (3, 'abc') + >>> longest_common_subsequence("a", "a") # Single character match + (1, 'a') + >>> longest_common_subsequence("a", "b") # Single character no match + (0, '') + >>> longest_common_subsequence("abcdef", "ace") # Interleaved subsequence + (3, 'ace') + >>> longest_common_subsequence("ABCD", "ACBD") # No repeated characters + (3, 'ABD') """ # find the length of strings @@ -38,36 +56,30 @@ def longest_common_subsequence(x: str, y: str): n = len(y) # declaring the array for storing the dp values - l = [[0] * (n + 1) for _ in range(m + 1)] # noqa: E741 + dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): - if x[i - 1] == y[j - 1]: - match = 1 - else: - match = 0 + match = 1 if x[i - 1] == y[j - 1] else 0 - l[i][j] = max(l[i - 1][j], l[i][j - 1], l[i - 1][j - 1] + match) + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] + match) seq = "" i, j = m, n while i > 0 and j > 0: - if x[i - 1] == y[j - 1]: - match = 1 - else: - match = 0 + match = 1 if x[i - 1] == y[j - 1] else 0 - if l[i][j] == l[i - 1][j - 1] + match: + if dp[i][j] == dp[i - 1][j - 1] + match: if match == 1: seq = x[i - 1] + seq i -= 1 j -= 1 - elif l[i][j] == l[i - 1][j]: + elif dp[i][j] == dp[i - 1][j]: i -= 1 else: j -= 1 - return l[m][n], seq + return dp[m][n], seq if __name__ == "__main__": diff --git a/dynamic_programming/longest_common_substring.py b/dynamic_programming/longest_common_substring.py index e2f944a5e336..ea5233eb2d17 100644 --- a/dynamic_programming/longest_common_substring.py +++ b/dynamic_programming/longest_common_substring.py @@ -1,15 +1,19 @@ """ -Longest Common Substring Problem Statement: Given two sequences, find the -longest common substring present in both of them. A substring is -necessarily continuous. -Example: "abcdef" and "xabded" have two longest common substrings, "ab" or "de". -Therefore, algorithm should return any one of them. +Longest Common Substring Problem Statement: + Given two sequences, find the + longest common substring present in both of them. A substring is + necessarily continuous. + +Example: + ``abcdef`` and ``xabded`` have two longest common substrings, ``ab`` or ``de``. + Therefore, algorithm should return any one of them. """ def longest_common_substring(text1: str, text2: str) -> str: """ Finds the longest common substring between two strings. + >>> longest_common_substring("", "") '' >>> longest_common_substring("a","") diff --git a/dynamic_programming/longest_increasing_subsequence.py b/dynamic_programming/longest_increasing_subsequence.py index 6feed23529f1..1863a882c41e 100644 --- a/dynamic_programming/longest_increasing_subsequence.py +++ b/dynamic_programming/longest_increasing_subsequence.py @@ -4,24 +4,30 @@ This is a pure Python implementation of Dynamic Programming solution to the longest increasing subsequence of a given sequence. -The problem is : -Given an array, to find the longest and increasing sub-array in that given array and -return it. -Example: [10, 22, 9, 33, 21, 50, 41, 60, 80] as input will return - [10, 22, 33, 41, 60, 80] as output +The problem is: + Given an array, to find the longest and increasing sub-array in that given array and + return it. + +Example: + ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return + ``[10, 22, 33, 41, 60, 80]`` as output """ + from __future__ import annotations def longest_subsequence(array: list[int]) -> list[int]: # This function is recursive """ Some examples + >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80]) [10, 22, 33, 41, 60, 80] >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9]) [1, 2, 3, 9] + >>> longest_subsequence([28, 26, 12, 23, 35, 39]) + [12, 23, 35, 39] >>> longest_subsequence([9, 8, 7, 6, 5, 7]) - [8] + [5, 7] >>> longest_subsequence([1, 1, 1]) [1, 1, 1] >>> longest_subsequence([]) @@ -40,7 +46,7 @@ def longest_subsequence(array: list[int]) -> list[int]: # This function is recu while not is_found and i < array_length: if array[i] < pivot: is_found = True - temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = array[i:] temp_array = longest_subsequence(temp_array) if len(temp_array) > len(longest_subseq): longest_subseq = temp_array @@ -48,7 +54,7 @@ def longest_subsequence(array: list[int]) -> list[int]: # This function is recu i += 1 temp_array = [element for element in array[1:] if element >= pivot] - temp_array = [pivot] + longest_subsequence(temp_array) + temp_array = [pivot, *longest_subsequence(temp_array)] if len(temp_array) > len(longest_subseq): return temp_array else: diff --git a/dynamic_programming/longest_increasing_subsequence_iterative.py b/dynamic_programming/longest_increasing_subsequence_iterative.py new file mode 100644 index 000000000000..665c86a35d2e --- /dev/null +++ b/dynamic_programming/longest_increasing_subsequence_iterative.py @@ -0,0 +1,72 @@ +""" +Author : Sanjay Muthu + +This is a pure Python implementation of Dynamic Programming solution to the longest +increasing subsequence of a given sequence. + +The problem is: + Given an array, to find the longest and increasing sub-array in that given array and + return it. + +Example: + ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return + ``[10, 22, 33, 50, 60, 80]`` as output +""" + +from __future__ import annotations + +import copy + + +def longest_subsequence(array: list[int]) -> list[int]: + """ + Some examples + + >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80]) + [10, 22, 33, 50, 60, 80] + >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9]) + [1, 2, 3, 9] + >>> longest_subsequence([9, 8, 7, 6, 5, 7]) + [7, 7] + >>> longest_subsequence([28, 26, 12, 23, 35, 39]) + [12, 23, 35, 39] + >>> longest_subsequence([1, 1, 1]) + [1, 1, 1] + >>> longest_subsequence([]) + [] + """ + n = len(array) + # The longest increasing subsequence ending at array[i] + longest_increasing_subsequence = [] + for i in range(n): + longest_increasing_subsequence.append([array[i]]) + + for i in range(1, n): + for prev in range(i): + # If array[prev] is less than or equal to array[i], then + # longest_increasing_subsequence[prev] + array[i] + # is a valid increasing subsequence + + # longest_increasing_subsequence[i] is only set to + # longest_increasing_subsequence[prev] + array[i] if the length is longer. + + if array[prev] <= array[i] and len( + longest_increasing_subsequence[prev] + ) + 1 > len(longest_increasing_subsequence[i]): + longest_increasing_subsequence[i] = copy.copy( + longest_increasing_subsequence[prev] + ) + longest_increasing_subsequence[i].append(array[i]) + + result: list[int] = [] + for i in range(n): + if len(longest_increasing_subsequence[i]) > len(result): + result = longest_increasing_subsequence[i] + + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py b/dynamic_programming/longest_increasing_subsequence_o_nlogn.py similarity index 85% rename from dynamic_programming/longest_increasing_subsequence_o(nlogn).py rename to dynamic_programming/longest_increasing_subsequence_o_nlogn.py index 5e11d729f395..bbc7a62b6b5c 100644 --- a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py +++ b/dynamic_programming/longest_increasing_subsequence_o_nlogn.py @@ -7,14 +7,14 @@ from __future__ import annotations -def ceil_index(v, l, r, key): # noqa: E741 - while r - l > 1: - m = (l + r) // 2 - if v[m] >= key: - r = m +def ceil_index(v, left, right, key): + while right - left > 1: + middle = (left + right) // 2 + if v[middle] >= key: + right = middle else: - l = m # noqa: E741 - return r + left = middle + return right def longest_increasing_subsequence_length(v: list[int]) -> int: diff --git a/dynamic_programming/longest_palindromic_subsequence.py b/dynamic_programming/longest_palindromic_subsequence.py new file mode 100644 index 000000000000..a60d95e460e6 --- /dev/null +++ b/dynamic_programming/longest_palindromic_subsequence.py @@ -0,0 +1,44 @@ +""" +author: Sanket Kittad +Given a string s, find the longest palindromic subsequence's length in s. +Input: s = "bbbab" +Output: 4 +Explanation: One possible longest palindromic subsequence is "bbbb". +Leetcode link: https://leetcode.com/problems/longest-palindromic-subsequence/description/ +""" + + +def longest_palindromic_subsequence(input_string: str) -> int: + """ + This function returns the longest palindromic subsequence in a string + >>> longest_palindromic_subsequence("bbbab") + 4 + >>> longest_palindromic_subsequence("bbabcbcab") + 7 + """ + n = len(input_string) + rev = input_string[::-1] + m = len(rev) + dp = [[-1] * (m + 1) for i in range(n + 1)] + for i in range(n + 1): + dp[i][0] = 0 + for i in range(m + 1): + dp[0][i] = 0 + + # create and initialise dp array + for i in range(1, n + 1): + for j in range(1, m + 1): + # If characters at i and j are the same + # include them in the palindromic subsequence + if input_string[i - 1] == rev[j - 1]: + dp[i][j] = 1 + dp[i - 1][j - 1] + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + + return dp[n][m] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/longest_sub_array.py b/dynamic_programming/longest_sub_array.py deleted file mode 100644 index b477acf61e66..000000000000 --- a/dynamic_programming/longest_sub_array.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Author : Yvonne - -This is a pure Python implementation of Dynamic Programming solution to the - longest_sub_array problem. - -The problem is : -Given an array, to find the longest and continuous sub array and get the max sum of the - sub array in the given array. -""" - - -class SubArray: - def __init__(self, arr): - # we need a list not a string, so do something to change the type - self.array = arr.split(",") - - def solve_sub_array(self): - rear = [int(self.array[0])] * len(self.array) - sum_value = [int(self.array[0])] * len(self.array) - for i in range(1, len(self.array)): - sum_value[i] = max( - int(self.array[i]) + sum_value[i - 1], int(self.array[i]) - ) - rear[i] = max(sum_value[i], rear[i - 1]) - return rear[len(self.array) - 1] - - -if __name__ == "__main__": - whole_array = input("please input some numbers:") - array = SubArray(whole_array) - re = array.solve_sub_array() - print(("the results is:", re)) diff --git a/dynamic_programming/matrix_chain_multiplication.py b/dynamic_programming/matrix_chain_multiplication.py new file mode 100644 index 000000000000..4c0c771f9092 --- /dev/null +++ b/dynamic_programming/matrix_chain_multiplication.py @@ -0,0 +1,151 @@ +""" +| Find the minimum number of multiplications needed to multiply chain of matrices. +| Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/ + +The algorithm has interesting real-world applications. + +Example: + 1. Image transformations in Computer Graphics as images are composed of matrix. + 2. Solve complex polynomial equations in the field of algebra using least processing + power. + 3. Calculate overall impact of macroeconomic decisions as economic equations involve a + number of variables. + 4. Self-driving car navigation can be made more accurate as matrix multiplication can + accurately determine position and orientation of obstacles in short time. + +Python doctests can be run with the following command:: + + python -m doctest -v matrix_chain_multiply.py + +Given a sequence ``arr[]`` that represents chain of 2D matrices such that the dimension +of the ``i`` th matrix is ``arr[i-1]*arr[i]``. +So suppose ``arr = [40, 20, 30, 10, 30]`` means we have ``4`` matrices of dimensions +``40*20``, ``20*30``, ``30*10`` and ``10*30``. + +``matrix_chain_multiply()`` returns an integer denoting minimum number of +multiplications to multiply the chain. + +We do not need to perform actual multiplication here. +We only need to decide the order in which to perform the multiplication. + +Hints: + 1. Number of multiplications (ie cost) to multiply ``2`` matrices + of size ``m*p`` and ``p*n`` is ``m*p*n``. + 2. Cost of matrix multiplication is not associative ie ``(M1*M2)*M3 != M1*(M2*M3)`` + 3. Matrix multiplication is not commutative. So, ``M1*M2`` does not mean ``M2*M1`` + can be done. + 4. To determine the required order, we can try different combinations. + +So, this problem has overlapping sub-problems and can be solved using recursion. +We use Dynamic Programming for optimal time complexity. + +Example input: + ``arr = [40, 20, 30, 10, 30]`` +output: + ``26000`` +""" + +from collections.abc import Iterator +from contextlib import contextmanager +from functools import cache +from sys import maxsize + + +def matrix_chain_multiply(arr: list[int]) -> int: + """ + Find the minimum number of multiplcations required to multiply the chain of matrices + + Args: + `arr`: The input array of integers. + + Returns: + Minimum number of multiplications needed to multiply the chain + + Examples: + + >>> matrix_chain_multiply([1, 2, 3, 4, 3]) + 30 + >>> matrix_chain_multiply([10]) + 0 + >>> matrix_chain_multiply([10, 20]) + 0 + >>> matrix_chain_multiply([19, 2, 19]) + 722 + >>> matrix_chain_multiply(list(range(1, 100))) + 323398 + >>> # matrix_chain_multiply(list(range(1, 251))) + # 2626798 + """ + if len(arr) < 2: + return 0 + # initialising 2D dp matrix + n = len(arr) + dp = [[maxsize for j in range(n)] for i in range(n)] + # we want minimum cost of multiplication of matrices + # of dimension (i*k) and (k*j). This cost is arr[i-1]*arr[k]*arr[j]. + for i in range(n - 1, 0, -1): + for j in range(i, n): + if i == j: + dp[i][j] = 0 + continue + for k in range(i, j): + dp[i][j] = min( + dp[i][j], dp[i][k] + dp[k + 1][j] + arr[i - 1] * arr[k] * arr[j] + ) + + return dp[1][n - 1] + + +def matrix_chain_order(dims: list[int]) -> int: + """ + Source: https://en.wikipedia.org/wiki/Matrix_chain_multiplication + + The dynamic programming solution is faster than cached the recursive solution and + can handle larger inputs. + + >>> matrix_chain_order([1, 2, 3, 4, 3]) + 30 + >>> matrix_chain_order([10]) + 0 + >>> matrix_chain_order([10, 20]) + 0 + >>> matrix_chain_order([19, 2, 19]) + 722 + >>> matrix_chain_order(list(range(1, 100))) + 323398 + >>> # matrix_chain_order(list(range(1, 251))) # Max before RecursionError is raised + # 2626798 + """ + + @cache + def a(i: int, j: int) -> int: + return min( + (a(i, k) + dims[i] * dims[k] * dims[j] + a(k, j) for k in range(i + 1, j)), + default=0, + ) + + return a(0, len(dims) - 1) + + +@contextmanager +def elapsed_time(msg: str) -> Iterator: + # print(f"Starting: {msg}") + from time import perf_counter_ns + + start = perf_counter_ns() + yield + print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10**9} seconds.") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + with elapsed_time("matrix_chain_order"): + print(f"{matrix_chain_order(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_multiply"): + print(f"{matrix_chain_multiply(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_order"): + print(f"{matrix_chain_order(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_multiply"): + print(f"{matrix_chain_multiply(list(range(1, 251))) = }") diff --git a/dynamic_programming/max_product_subarray.py b/dynamic_programming/max_product_subarray.py new file mode 100644 index 000000000000..6f4f38e38942 --- /dev/null +++ b/dynamic_programming/max_product_subarray.py @@ -0,0 +1,54 @@ +def max_product_subarray(numbers: list[int]) -> int: + """ + Returns the maximum product that can be obtained by multiplying a + contiguous subarray of the given integer list `numbers`. + + Example: + + >>> max_product_subarray([2, 3, -2, 4]) + 6 + >>> max_product_subarray((-2, 0, -1)) + 0 + >>> max_product_subarray([2, 3, -2, 4, -1]) + 48 + >>> max_product_subarray([-1]) + -1 + >>> max_product_subarray([0]) + 0 + >>> max_product_subarray([]) + 0 + >>> max_product_subarray("") + 0 + >>> max_product_subarray(None) + 0 + >>> max_product_subarray([2, 3, -2, 4.5, -1]) + Traceback (most recent call last): + ... + ValueError: numbers must be an iterable of integers + >>> max_product_subarray("ABC") + Traceback (most recent call last): + ... + ValueError: numbers must be an iterable of integers + """ + if not numbers: + return 0 + + if not isinstance(numbers, (list, tuple)) or not all( + isinstance(number, int) for number in numbers + ): + raise ValueError("numbers must be an iterable of integers") + + max_till_now = min_till_now = max_prod = numbers[0] + + for i in range(1, len(numbers)): + # update the maximum and minimum subarray products + number = numbers[i] + if number < 0: + max_till_now, min_till_now = min_till_now, max_till_now + max_till_now = max(number, max_till_now * number) + min_till_now = min(number, min_till_now * number) + + # update the maximum product found till now + max_prod = max(max_prod, max_till_now) + + return max_prod diff --git a/dynamic_programming/max_sub_array.py b/dynamic_programming/max_sub_array.py deleted file mode 100644 index 07717fba4172..000000000000 --- a/dynamic_programming/max_sub_array.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -author : Mayank Kumar Jha (mk9440) -""" -from __future__ import annotations - - -def find_max_sub_array(a, low, high): - if low == high: - return low, high, a[low] - else: - mid = (low + high) // 2 - left_low, left_high, left_sum = find_max_sub_array(a, low, mid) - right_low, right_high, right_sum = find_max_sub_array(a, mid + 1, high) - cross_left, cross_right, cross_sum = find_max_cross_sum(a, low, mid, high) - if left_sum >= right_sum and left_sum >= cross_sum: - return left_low, left_high, left_sum - elif right_sum >= left_sum and right_sum >= cross_sum: - return right_low, right_high, right_sum - else: - return cross_left, cross_right, cross_sum - - -def find_max_cross_sum(a, low, mid, high): - left_sum, max_left = -999999999, -1 - right_sum, max_right = -999999999, -1 - summ = 0 - for i in range(mid, low - 1, -1): - summ += a[i] - if summ > left_sum: - left_sum = summ - max_left = i - summ = 0 - for i in range(mid + 1, high + 1): - summ += a[i] - if summ > right_sum: - right_sum = summ - max_right = i - return max_left, max_right, (left_sum + right_sum) - - -def max_sub_array(nums: list[int]) -> int: - """ - Finds the contiguous subarray which has the largest sum and return its sum. - - >>> max_sub_array([-2, 1, -3, 4, -1, 2, 1, -5, 4]) - 6 - - An empty (sub)array has sum 0. - >>> max_sub_array([]) - 0 - - If all elements are negative, the largest subarray would be the empty array, - having the sum 0. - >>> max_sub_array([-1, -2, -3]) - 0 - >>> max_sub_array([5, -2, -3]) - 5 - >>> max_sub_array([31, -41, 59, 26, -53, 58, 97, -93, -23, 84]) - 187 - """ - best = 0 - current = 0 - for i in nums: - current += i - current = max(current, 0) - best = max(best, current) - return best - - -if __name__ == "__main__": - """ - A random simulation of this algorithm. - """ - import time - from random import randint - - from matplotlib import pyplot as plt - - inputs = [10, 100, 1000, 10000, 50000, 100000, 200000, 300000, 400000, 500000] - tim = [] - for i in inputs: - li = [randint(1, i) for j in range(i)] - strt = time.time() - (find_max_sub_array(li, 0, len(li) - 1)) - end = time.time() - tim.append(end - strt) - print("No of Inputs Time Taken") - for i in range(len(inputs)): - print(inputs[i], "\t\t", tim[i]) - plt.plot(inputs, tim) - plt.xlabel("Number of Inputs") - plt.ylabel("Time taken in seconds ") - plt.show() diff --git a/dynamic_programming/max_subarray_sum.py b/dynamic_programming/max_subarray_sum.py new file mode 100644 index 000000000000..8c1dc0889a85 --- /dev/null +++ b/dynamic_programming/max_subarray_sum.py @@ -0,0 +1,61 @@ +""" +The maximum subarray sum problem is the task of finding the maximum sum that can be +obtained from a contiguous subarray within a given array of numbers. For example, given +the array [-2, 1, -3, 4, -1, 2, 1, -5, 4], the contiguous subarray with the maximum sum +is [4, -1, 2, 1], so the maximum subarray sum is 6. + +Kadane's algorithm is a simple dynamic programming algorithm that solves the maximum +subarray sum problem in O(n) time and O(1) space. + +Reference: https://en.wikipedia.org/wiki/Maximum_subarray_problem +""" + +from collections.abc import Sequence + + +def max_subarray_sum( + arr: Sequence[float], allow_empty_subarrays: bool = False +) -> float: + """ + Solves the maximum subarray sum problem using Kadane's algorithm. + :param arr: the given array of numbers + :param allow_empty_subarrays: if True, then the algorithm considers empty subarrays + + >>> max_subarray_sum([2, 8, 9]) + 19 + >>> max_subarray_sum([0, 0]) + 0 + >>> max_subarray_sum([-1.0, 0.0, 1.0]) + 1.0 + >>> max_subarray_sum([1, 2, 3, 4, -2]) + 10 + >>> max_subarray_sum([-2, 1, -3, 4, -1, 2, 1, -5, 4]) + 6 + >>> max_subarray_sum([2, 3, -9, 8, -2]) + 8 + >>> max_subarray_sum([-2, -3, -1, -4, -6]) + -1 + >>> max_subarray_sum([-2, -3, -1, -4, -6], allow_empty_subarrays=True) + 0 + >>> max_subarray_sum([]) + 0 + """ + if not arr: + return 0 + + max_sum = 0 if allow_empty_subarrays else float("-inf") + curr_sum = 0.0 + for num in arr: + curr_sum = max(0 if allow_empty_subarrays else num, curr_sum + num) + max_sum = max(max_sum, curr_sum) + + return max_sum + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] + print(f"{max_subarray_sum(nums) = }") diff --git a/dynamic_programming/max_sum_contiguous_subsequence.py b/dynamic_programming/max_sum_contiguous_subsequence.py deleted file mode 100644 index bac592370c5d..000000000000 --- a/dynamic_programming/max_sum_contiguous_subsequence.py +++ /dev/null @@ -1,20 +0,0 @@ -def max_subarray_sum(nums: list) -> int: - """ - >>> max_subarray_sum([6 , 9, -1, 3, -7, -5, 10]) - 17 - """ - if not nums: - return 0 - n = len(nums) - - res, s, s_pre = nums[0], nums[0], nums[0] - for i in range(1, n): - s = max(nums[i], s_pre + nums[i]) - s_pre = s - res = max(res, s) - return res - - -if __name__ == "__main__": - nums = [6, 9, -1, 3, -7, -5, 10] - print(max_subarray_sum(nums)) diff --git a/dynamic_programming/min_distance_up_bottom.py b/dynamic_programming/min_distance_up_bottom.py index 49c361f24d45..6b38a41a1c0a 100644 --- a/dynamic_programming/min_distance_up_bottom.py +++ b/dynamic_programming/min_distance_up_bottom.py @@ -1,18 +1,15 @@ """ Author : Alexander Pantyukhin Date : October 14, 2022 -This is implementation Dynamic Programming up bottom approach -to find edit distance. -The aim is to demonstate up bottom approach for solving the task. -The implementation was tested on the -leetcode: https://leetcode.com/problems/edit-distance/ -""" +This is an implementation of the up-bottom approach to find edit distance. +The implementation was tested on Leetcode: https://leetcode.com/problems/edit-distance/ -""" Levinstein distance Dynamic Programming: up -> down. """ +import functools + def min_distance_up_bottom(word1: str, word2: str) -> int: """ @@ -25,18 +22,15 @@ def min_distance_up_bottom(word1: str, word2: str) -> int: >>> min_distance_up_bottom("zooicoarchaeologist", "zoologist") 10 """ - - from functools import lru_cache - len_word1 = len(word1) len_word2 = len(word2) - @lru_cache(maxsize=None) + @functools.cache def min_distance(index1: int, index2: int) -> int: - # if first word index is overflow - delete all from the second word + # if first word index overflows - delete all from the second word if index1 >= len_word1: return len_word2 - index2 - # if second word index is overflow - delete all from the first word + # if second word index overflows - delete all from the first word if index2 >= len_word2: return len_word1 - index1 diff = int(word1[index1] != word2[index2]) # current letters not identical diff --git a/dynamic_programming/minimum_partition.py b/dynamic_programming/minimum_partition.py index 3daa9767fde4..748c0599efb0 100644 --- a/dynamic_programming/minimum_partition.py +++ b/dynamic_programming/minimum_partition.py @@ -3,13 +3,53 @@ """ -def find_min(arr): - n = len(arr) - s = sum(arr) +def find_min(numbers: list[int]) -> int: + """ + >>> find_min([1, 2, 3, 4, 5]) + 1 + >>> find_min([5, 5, 5, 5, 5]) + 5 + >>> find_min([5, 5, 5, 5]) + 0 + >>> find_min([3]) + 3 + >>> find_min([]) + 0 + >>> find_min([1, 2, 3, 4]) + 0 + >>> find_min([0, 0, 0, 0]) + 0 + >>> find_min([-1, -5, 5, 1]) + 0 + >>> find_min([-1, -5, 5, 1]) + 0 + >>> find_min([9, 9, 9, 9, 9]) + 9 + >>> find_min([1, 5, 10, 3]) + 1 + >>> find_min([-1, 0, 1]) + 0 + >>> find_min(range(10, 0, -1)) + 1 + >>> find_min([-1]) + Traceback (most recent call last): + -- + IndexError: list assignment index out of range + >>> find_min([0, 0, 0, 1, 2, -4]) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + >>> find_min([-1, -5, -10, -3]) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + """ + n = len(numbers) + s = sum(numbers) dp = [[False for x in range(s + 1)] for y in range(n + 1)] - for i in range(1, n + 1): + for i in range(n + 1): dp[i][0] = True for i in range(1, s + 1): @@ -17,10 +57,10 @@ def find_min(arr): for i in range(1, n + 1): for j in range(1, s + 1): - dp[i][j] = dp[i][j - 1] + dp[i][j] = dp[i - 1][j] - if arr[i - 1] <= j: - dp[i][j] = dp[i][j] or dp[i - 1][j - arr[i - 1]] + if numbers[i - 1] <= j: + dp[i][j] = dp[i][j] or dp[i - 1][j - numbers[i - 1]] for j in range(int(s / 2), -1, -1): if dp[n][j] is True: @@ -28,3 +68,9 @@ def find_min(arr): break return diff + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/dynamic_programming/minimum_size_subarray_sum.py b/dynamic_programming/minimum_size_subarray_sum.py new file mode 100644 index 000000000000..3868d73535fb --- /dev/null +++ b/dynamic_programming/minimum_size_subarray_sum.py @@ -0,0 +1,62 @@ +import sys + + +def minimum_subarray_sum(target: int, numbers: list[int]) -> int: + """ + Return the length of the shortest contiguous subarray in a list of numbers whose sum + is at least target. Reference: https://stackoverflow.com/questions/8269916 + + >>> minimum_subarray_sum(7, [2, 3, 1, 2, 4, 3]) + 2 + >>> minimum_subarray_sum(7, [2, 3, -1, 2, 4, -3]) + 4 + >>> minimum_subarray_sum(11, [1, 1, 1, 1, 1, 1, 1, 1]) + 0 + >>> minimum_subarray_sum(10, [1, 2, 3, 4, 5, 6, 7]) + 2 + >>> minimum_subarray_sum(5, [1, 1, 1, 1, 1, 5]) + 1 + >>> minimum_subarray_sum(0, []) + 0 + >>> minimum_subarray_sum(0, [1, 2, 3]) + 1 + >>> minimum_subarray_sum(10, [10, 20, 30]) + 1 + >>> minimum_subarray_sum(7, [1, 1, 1, 1, 1, 1, 10]) + 1 + >>> minimum_subarray_sum(6, []) + 0 + >>> minimum_subarray_sum(2, [1, 2, 3]) + 1 + >>> minimum_subarray_sum(-6, []) + 0 + >>> minimum_subarray_sum(-6, [3, 4, 5]) + 1 + >>> minimum_subarray_sum(8, None) + 0 + >>> minimum_subarray_sum(2, "ABC") + Traceback (most recent call last): + ... + ValueError: numbers must be an iterable of integers + """ + if not numbers: + return 0 + if target == 0 and target in numbers: + return 0 + if not isinstance(numbers, (list, tuple)) or not all( + isinstance(number, int) for number in numbers + ): + raise ValueError("numbers must be an iterable of integers") + + left = right = curr_sum = 0 + min_len = sys.maxsize + + while right < len(numbers): + curr_sum += numbers[right] + while curr_sum >= target and left <= right: + min_len = min(min_len, right - left + 1) + curr_sum -= numbers[left] + left += 1 + right += 1 + + return 0 if min_len == sys.maxsize else min_len diff --git a/dynamic_programming/minimum_squares_to_represent_a_number.py b/dynamic_programming/minimum_squares_to_represent_a_number.py index bf5849f5bcb3..98c0602fa831 100644 --- a/dynamic_programming/minimum_squares_to_represent_a_number.py +++ b/dynamic_programming/minimum_squares_to_represent_a_number.py @@ -5,6 +5,7 @@ def minimum_squares_to_represent_a_number(number: int) -> int: """ Count the number of minimum squares to represent a number + >>> minimum_squares_to_represent_a_number(25) 1 >>> minimum_squares_to_represent_a_number(37) diff --git a/dynamic_programming/minimum_steps_to_one.py b/dynamic_programming/minimum_steps_to_one.py index f4eb7033dd20..68eaf56e21a7 100644 --- a/dynamic_programming/minimum_steps_to_one.py +++ b/dynamic_programming/minimum_steps_to_one.py @@ -1,7 +1,7 @@ """ YouTube Explanation: https://www.youtube.com/watch?v=f2xi3c1S95M -Given an integer n, return the minimum steps to 1 +Given an integer n, return the minimum steps from n to 1 AVAILABLE STEPS: * Decrement by 1 @@ -42,7 +42,8 @@ def min_steps_to_one(number: int) -> int: """ if number <= 0: - raise ValueError(f"n must be greater than 0. Got n = {number}") + msg = f"n must be greater than 0. Got n = {number}" + raise ValueError(msg) table = [number + 1] * (number + 1) diff --git a/dynamic_programming/minimum_tickets_cost.py b/dynamic_programming/minimum_tickets_cost.py index d07056d9217f..6790c21f16ed 100644 --- a/dynamic_programming/minimum_tickets_cost.py +++ b/dynamic_programming/minimum_tickets_cost.py @@ -22,7 +22,7 @@ Dynamic Programming: up -> down. """ -from functools import lru_cache +import functools def mincost_tickets(days: list[int], costs: list[int]) -> int: @@ -106,7 +106,7 @@ def mincost_tickets(days: list[int], costs: list[int]) -> int: days_set = set(days) - @lru_cache(maxsize=None) + @functools.cache def dynamic_programming(index: int) -> int: if index > 365: return 0 diff --git a/dynamic_programming/regex_match.py b/dynamic_programming/regex_match.py new file mode 100644 index 000000000000..e94d82093c8b --- /dev/null +++ b/dynamic_programming/regex_match.py @@ -0,0 +1,99 @@ +""" +Regex matching check if a text matches pattern or not. +Pattern: + + 1. ``.`` Matches any single character. + 2. ``*`` Matches zero or more of the preceding element. + +More info: + https://medium.com/trick-the-interviwer/regular-expression-matching-9972eb74c03 +""" + + +def recursive_match(text: str, pattern: str) -> bool: + r""" + Recursive matching algorithm. + + | Time complexity: O(2^(\|text\| + \|pattern\|)) + | Space complexity: Recursion depth is O(\|text\| + \|pattern\|). + + :param text: Text to match. + :param pattern: Pattern to match. + :return: ``True`` if `text` matches `pattern`, ``False`` otherwise. + + >>> recursive_match('abc', 'a.c') + True + >>> recursive_match('abc', 'af*.c') + True + >>> recursive_match('abc', 'a.c*') + True + >>> recursive_match('abc', 'a.c*d') + False + >>> recursive_match('aa', '.*') + True + """ + if not pattern: + return not text + + if not text: + return pattern[-1] == "*" and recursive_match(text, pattern[:-2]) + + if text[-1] == pattern[-1] or pattern[-1] == ".": + return recursive_match(text[:-1], pattern[:-1]) + + if pattern[-1] == "*": + return recursive_match(text[:-1], pattern) or recursive_match( + text, pattern[:-2] + ) + + return False + + +def dp_match(text: str, pattern: str) -> bool: + r""" + Dynamic programming matching algorithm. + + | Time complexity: O(\|text\| * \|pattern\|) + | Space complexity: O(\|text\| * \|pattern\|) + + :param text: Text to match. + :param pattern: Pattern to match. + :return: ``True`` if `text` matches `pattern`, ``False`` otherwise. + + >>> dp_match('abc', 'a.c') + True + >>> dp_match('abc', 'af*.c') + True + >>> dp_match('abc', 'a.c*') + True + >>> dp_match('abc', 'a.c*d') + False + >>> dp_match('aa', '.*') + True + """ + m = len(text) + n = len(pattern) + dp = [[False for _ in range(n + 1)] for _ in range(m + 1)] + dp[0][0] = True + + for j in range(1, n + 1): + dp[0][j] = pattern[j - 1] == "*" and dp[0][j - 2] + + for i in range(1, m + 1): + for j in range(1, n + 1): + if pattern[j - 1] in {".", text[i - 1]}: + dp[i][j] = dp[i - 1][j - 1] + elif pattern[j - 1] == "*": + dp[i][j] = dp[i][j - 2] + if pattern[j - 2] in {".", text[i - 1]}: + dp[i][j] |= dp[i - 1][j] + else: + dp[i][j] = False + + return dp[m][n] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/rod_cutting.py b/dynamic_programming/rod_cutting.py index 79104d8f4044..d12c759dc928 100644 --- a/dynamic_programming/rod_cutting.py +++ b/dynamic_programming/rod_cutting.py @@ -1,7 +1,7 @@ """ This module provides two implementations for the rod-cutting problem: -1. A naive recursive implementation which has an exponential runtime -2. Two dynamic programming implementations which have quadratic runtime + 1. A naive recursive implementation which has an exponential runtime + 2. Two dynamic programming implementations which have quadratic runtime The rod-cutting problem is the problem of finding the maximum possible revenue obtainable from a rod of length ``n`` given a list of prices for each integral piece @@ -20,18 +20,21 @@ def naive_cut_rod_recursive(n: int, prices: list): Runtime: O(2^n) Arguments - ------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- + + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. Examples -------- + >>> naive_cut_rod_recursive(4, [1, 5, 8, 9]) 10 >>> naive_cut_rod_recursive(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -54,28 +57,30 @@ def top_down_cut_rod(n: int, prices: list): """ Constructs a top-down dynamic programming solution for the rod-cutting problem via memoization. This function serves as a wrapper for - _top_down_cut_rod_recursive + ``_top_down_cut_rod_recursive`` Runtime: O(n^2) Arguments - -------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- - Note - ---- - For convenience and because Python's lists using 0-indexing, length(max_rev) = - n + 1, to accommodate for the revenue obtainable from a rod of length 0. + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + + .. note:: + For convenience and because Python's lists using ``0``-indexing, ``length(max_rev) + = n + 1``, to accommodate for the revenue obtainable from a rod of length ``0``. Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. Examples - ------- + -------- + >>> top_down_cut_rod(4, [1, 5, 8, 9]) 10 >>> top_down_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -94,16 +99,18 @@ def _top_down_cut_rod_recursive(n: int, prices: list, max_rev: list): Runtime: O(n^2) Arguments - -------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` - max_rev: list, the computed maximum revenue for a piece of rod. - ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i`` + --------- + + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + * `max_rev`: list, the computed maximum revenue for a piece of rod. + ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i`` Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. """ if max_rev[n] >= 0: @@ -130,18 +137,21 @@ def bottom_up_cut_rod(n: int, prices: list): Runtime: O(n^2) Arguments - ---------- - n: int, the maximum length of the rod. - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- + + * `n`: int, the maximum length of the rod. + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` Returns ------- - The maximum revenue obtainable from cutting a rod of length n given + + The maximum revenue obtainable from cutting a rod of length `n` given the prices for each piece of rod p. Examples - ------- + -------- + >>> bottom_up_cut_rod(4, [1, 5, 8, 9]) 10 >>> bottom_up_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -168,22 +178,23 @@ def _enforce_args(n: int, prices: list): """ Basic checks on the arguments to the rod-cutting algorithms - n: int, the length of the rod - prices: list, the price list for each piece of rod. - - Throws ValueError: + * `n`: int, the length of the rod + * `prices`: list, the price list for each piece of rod. - if n is negative or there are fewer items in the price list than the length of - the rod + Throws ``ValueError``: + if `n` is negative or there are fewer items in the price list than the length of + the rod """ if n < 0: - raise ValueError(f"n must be greater than or equal to 0. Got n = {n}") + msg = f"n must be greater than or equal to 0. Got n = {n}" + raise ValueError(msg) if n > len(prices): - raise ValueError( - "Each integral piece of rod must have a corresponding " - f"price. Got n = {n} but length of prices = {len(prices)}" + msg = ( + "Each integral piece of rod must have a corresponding price. " + f"Got n = {n} but length of prices = {len(prices)}" ) + raise ValueError(msg) def main(): diff --git a/dynamic_programming/smith_waterman.py b/dynamic_programming/smith_waterman.py new file mode 100644 index 000000000000..4c5d58379f07 --- /dev/null +++ b/dynamic_programming/smith_waterman.py @@ -0,0 +1,193 @@ +""" +https://en.wikipedia.org/wiki/Smith%E2%80%93Waterman_algorithm +The Smith-Waterman algorithm is a dynamic programming algorithm used for sequence +alignment. It is particularly useful for finding similarities between two sequences, +such as DNA or protein sequences. In this implementation, gaps are penalized +linearly, meaning that the score is reduced by a fixed amount for each gap introduced +in the alignment. However, it's important to note that the Smith-Waterman algorithm +supports other gap penalty methods as well. +""" + + +def score_function( + source_char: str, + target_char: str, + match: int = 1, + mismatch: int = -1, + gap: int = -2, +) -> int: + """ + Calculate the score for a character pair based on whether they match or mismatch. + Returns 1 if the characters match, -1 if they mismatch, and -2 if either of the + characters is a gap. + >>> score_function('A', 'A') + 1 + >>> score_function('A', 'C') + -1 + >>> score_function('-', 'A') + -2 + >>> score_function('A', '-') + -2 + >>> score_function('-', '-') + -2 + """ + if "-" in (source_char, target_char): + return gap + return match if source_char == target_char else mismatch + + +def smith_waterman( + query: str, + subject: str, + match: int = 1, + mismatch: int = -1, + gap: int = -2, +) -> list[list[int]]: + """ + Perform the Smith-Waterman local sequence alignment algorithm. + Returns a 2D list representing the score matrix. Each value in the matrix + corresponds to the score of the best local alignment ending at that point. + >>> smith_waterman('ACAC', 'CA') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + >>> smith_waterman('acac', 'ca') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + >>> smith_waterman('ACAC', 'ca') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + >>> smith_waterman('acac', 'CA') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + >>> smith_waterman('ACAC', '') + [[0], [0], [0], [0], [0]] + >>> smith_waterman('', 'CA') + [[0, 0, 0]] + >>> smith_waterman('ACAC', 'CA') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + + >>> smith_waterman('acac', 'ca') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + + >>> smith_waterman('ACAC', 'ca') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + + >>> smith_waterman('acac', 'CA') + [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]] + + >>> smith_waterman('ACAC', '') + [[0], [0], [0], [0], [0]] + + >>> smith_waterman('', 'CA') + [[0, 0, 0]] + + >>> smith_waterman('AGT', 'AGT') + [[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]] + + >>> smith_waterman('AGT', 'GTA') + [[0, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 2, 0]] + + >>> smith_waterman('AGT', 'GTC') + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 2, 0]] + + >>> smith_waterman('AGT', 'G') + [[0, 0], [0, 0], [0, 1], [0, 0]] + + >>> smith_waterman('G', 'AGT') + [[0, 0, 0, 0], [0, 0, 1, 0]] + + >>> smith_waterman('AGT', 'AGTCT') + [[0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 2, 0, 0, 0], [0, 0, 0, 3, 1, 1]] + + >>> smith_waterman('AGTCT', 'AGT') + [[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3], [0, 0, 0, 1], [0, 0, 0, 1]] + + >>> smith_waterman('AGTCT', 'GTC') + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3], [0, 0, 1, 1]] + """ + # make both query and subject uppercase + query = query.upper() + subject = subject.upper() + + # Initialize score matrix + m = len(query) + n = len(subject) + score = [[0] * (n + 1) for _ in range(m + 1)] + kwargs = {"match": match, "mismatch": mismatch, "gap": gap} + + for i in range(1, m + 1): + for j in range(1, n + 1): + # Calculate scores for each cell + match = score[i - 1][j - 1] + score_function( + query[i - 1], subject[j - 1], **kwargs + ) + delete = score[i - 1][j] + gap + insert = score[i][j - 1] + gap + + # Take maximum score + score[i][j] = max(0, match, delete, insert) + + return score + + +def traceback(score: list[list[int]], query: str, subject: str) -> str: + r""" + Perform traceback to find the optimal local alignment. + Starts from the highest scoring cell in the matrix and traces back recursively + until a 0 score is found. Returns the alignment strings. + >>> traceback([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]], 'ACAC', 'CA') + 'CA\nCA' + >>> traceback([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]], 'acac', 'ca') + 'CA\nCA' + >>> traceback([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]], 'ACAC', 'ca') + 'CA\nCA' + >>> traceback([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 2], [0, 1, 0]], 'acac', 'CA') + 'CA\nCA' + >>> traceback([[0, 0, 0]], 'ACAC', '') + '' + """ + # make both query and subject uppercase + query = query.upper() + subject = subject.upper() + # find the indices of the maximum value in the score matrix + max_value = float("-inf") + i_max = j_max = 0 + for i, row in enumerate(score): + for j, value in enumerate(row): + if value > max_value: + max_value = value + i_max, j_max = i, j + # Traceback logic to find optimal alignment + i = i_max + j = j_max + align1 = "" + align2 = "" + gap = score_function("-", "-") + # guard against empty query or subject + if i == 0 or j == 0: + return "" + while i > 0 and j > 0: + if score[i][j] == score[i - 1][j - 1] + score_function( + query[i - 1], subject[j - 1] + ): + # optimal path is a diagonal take both letters + align1 = query[i - 1] + align1 + align2 = subject[j - 1] + align2 + i -= 1 + j -= 1 + elif score[i][j] == score[i - 1][j] + gap: + # optimal path is a vertical + align1 = query[i - 1] + align1 + align2 = f"-{align2}" + i -= 1 + else: + # optimal path is a horizontal + align1 = f"-{align1}" + align2 = subject[j - 1] + align2 + j -= 1 + + return f"{align1}\n{align2}" + + +if __name__ == "__main__": + query = "HEAGAWGHEE" + subject = "PAWHEAE" + + score = smith_waterman(query, subject, match=1, mismatch=-1, gap=-2) + print(traceback(score, query, subject)) diff --git a/dynamic_programming/subset_generation.py b/dynamic_programming/subset_generation.py index 819fd8106def..08daaac6f88a 100644 --- a/dynamic_programming/subset_generation.py +++ b/dynamic_programming/subset_generation.py @@ -1,44 +1,63 @@ -# Print all subset combinations of n element in given set of r element. +def subset_combinations(elements: list[int], n: int) -> list: + """ + Compute n-element combinations from a given list using dynamic programming. + Args: + * `elements`: The list of elements from which combinations will be generated. + * `n`: The number of elements in each combination. -def combination_util(arr, n, r, index, data, i): - """ - Current combination is ready to be printed, print it - arr[] ---> Input Array - data[] ---> Temporary array to store current combination - start & end ---> Staring and Ending indexes in arr[] - index ---> Current index in data[] - r ---> Size of a combination to be printed + Returns: + A list of tuples, each representing a combination of `n` elements. + + >>> subset_combinations(elements=[10, 20, 30, 40], n=2) + [(10, 20), (10, 30), (10, 40), (20, 30), (20, 40), (30, 40)] + >>> subset_combinations(elements=[1, 2, 3], n=1) + [(1,), (2,), (3,)] + >>> subset_combinations(elements=[1, 2, 3], n=3) + [(1, 2, 3)] + >>> subset_combinations(elements=[42], n=1) + [(42,)] + >>> subset_combinations(elements=[6, 7, 8, 9], n=4) + [(6, 7, 8, 9)] + >>> subset_combinations(elements=[10, 20, 30, 40, 50], n=0) + [()] + >>> subset_combinations(elements=[1, 2, 3, 4], n=2) + [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + >>> subset_combinations(elements=[1, 'apple', 3.14], n=2) + [(1, 'apple'), (1, 3.14), ('apple', 3.14)] + >>> subset_combinations(elements=['single'], n=0) + [()] + >>> subset_combinations(elements=[], n=9) + [] + >>> from itertools import combinations + >>> all(subset_combinations(items, n) == list(combinations(items, n)) + ... for items, n in ( + ... ([10, 20, 30, 40], 2), ([1, 2, 3], 1), ([1, 2, 3], 3), ([42], 1), + ... ([6, 7, 8, 9], 4), ([10, 20, 30, 40, 50], 1), ([1, 2, 3, 4], 2), + ... ([1, 'apple', 3.14], 2), (['single'], 0), ([], 9))) + True """ - if index == r: - for j in range(r): - print(data[j], end=" ") - print(" ") - return - # When no more elements are there to put in data[] - if i >= n: - return - # current is included, put next at next location - data[index] = arr[i] - combination_util(arr, n, r, index + 1, data, i + 1) - # current is excluded, replace it with - # next (Note that i+1 is passed, but - # index is not changed) - combination_util(arr, n, r, index, data, i + 1) - # The main function that prints all combinations - # of size r in arr[] of size n. This function - # mainly uses combinationUtil() - - -def print_combination(arr, n, r): - # A temporary array to store all combination one by one - data = [0] * r - # Print all combination using temporary array 'data[]' - combination_util(arr, n, r, 0, data, 0) + r = len(elements) + if n > r: + return [] + + dp: list[list[tuple]] = [[] for _ in range(r + 1)] + + dp[0].append(()) + + for i in range(1, r + 1): + for j in range(i, 0, -1): + for prev_combination in dp[j - 1]: + dp[j].append((*prev_combination, elements[i - 1])) + + try: + return sorted(dp[n]) + except TypeError: + return dp[n] if __name__ == "__main__": - # Driver code to check the function above - arr = [10, 20, 30, 40, 50] - print_combination(arr, len(arr), 3) - # This code is contributed by Ambuj sahu + from doctest import testmod + + testmod() + print(f"{subset_combinations(elements=[10, 20, 30, 40], n=2) = }") diff --git a/dynamic_programming/trapped_water.py b/dynamic_programming/trapped_water.py new file mode 100644 index 000000000000..8bec9fac5fef --- /dev/null +++ b/dynamic_programming/trapped_water.py @@ -0,0 +1,60 @@ +""" +Given an array of non-negative integers representing an elevation map where the width +of each bar is 1, this program calculates how much rainwater can be trapped. + +Example - height = (0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1) +Output: 6 +This problem can be solved using the concept of "DYNAMIC PROGRAMMING". + +We calculate the maximum height of bars on the left and right of every bar in array. +Then iterate over the width of structure and at each index. +The amount of water that will be stored is equal to minimum of maximum height of bars +on both sides minus height of bar at current position. +""" + + +def trapped_rainwater(heights: tuple[int, ...]) -> int: + """ + The trapped_rainwater function calculates the total amount of rainwater that can be + trapped given an array of bar heights. + It uses a dynamic programming approach, determining the maximum height of bars on + both sides for each bar, and then computing the trapped water above each bar. + The function returns the total trapped water. + + >>> trapped_rainwater((0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1)) + 6 + >>> trapped_rainwater((7, 1, 5, 3, 6, 4)) + 9 + >>> trapped_rainwater((7, 1, 5, 3, 6, -1)) + Traceback (most recent call last): + ... + ValueError: No height can be negative + """ + if not heights: + return 0 + if any(h < 0 for h in heights): + raise ValueError("No height can be negative") + length = len(heights) + + left_max = [0] * length + left_max[0] = heights[0] + for i, height in enumerate(heights[1:], start=1): + left_max[i] = max(height, left_max[i - 1]) + + right_max = [0] * length + right_max[-1] = heights[-1] + for i in range(length - 2, -1, -1): + right_max[i] = max(heights[i], right_max[i + 1]) + + return sum( + min(left, right) - height + for left, right, height in zip(left_max, right_max, heights) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(f"{trapped_rainwater((0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1)) = }") + print(f"{trapped_rainwater((7, 1, 5, 3, 6, 4)) = }") diff --git a/dynamic_programming/tribonacci.py b/dynamic_programming/tribonacci.py new file mode 100644 index 000000000000..58e15da918e2 --- /dev/null +++ b/dynamic_programming/tribonacci.py @@ -0,0 +1,24 @@ +# Tribonacci sequence using Dynamic Programming + + +def tribonacci(num: int) -> list[int]: + """ + Given a number, return first n Tribonacci Numbers. + >>> tribonacci(5) + [0, 0, 1, 1, 2] + >>> tribonacci(8) + [0, 0, 1, 1, 2, 4, 7, 13] + """ + dp = [0] * num + dp[2] = 1 + + for i in range(3, num): + dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] + + return dp + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/viterbi.py b/dynamic_programming/viterbi.py index 93ab845e2ae8..5b78fa9e46d0 100644 --- a/dynamic_programming/viterbi.py +++ b/dynamic_programming/viterbi.py @@ -9,119 +9,102 @@ def viterbi( emission_probabilities: dict, ) -> list: """ - Viterbi Algorithm, to find the most likely path of - states from the start and the expected output. - https://en.wikipedia.org/wiki/Viterbi_algorithm - sdafads - Wikipedia example - >>> observations = ["normal", "cold", "dizzy"] - >>> states = ["Healthy", "Fever"] - >>> start_p = {"Healthy": 0.6, "Fever": 0.4} - >>> trans_p = { - ... "Healthy": {"Healthy": 0.7, "Fever": 0.3}, - ... "Fever": {"Healthy": 0.4, "Fever": 0.6}, - ... } - >>> emit_p = { - ... "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1}, - ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, - ... } - >>> viterbi(observations, states, start_p, trans_p, emit_p) - ['Healthy', 'Healthy', 'Fever'] + Viterbi Algorithm, to find the most likely path of + states from the start and the expected output. - >>> viterbi((), states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, (), start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, {}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, start_p, {}, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, start_p, trans_p, {}) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi("invalid", states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: observations_space must be a list + https://en.wikipedia.org/wiki/Viterbi_algorithm - >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: observations_space must be a list of strings - - >>> viterbi(observations, "invalid", start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: states_space must be a list - - >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: states_space must be a list of strings - - >>> viterbi(observations, states, "invalid", trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities must be a dict - - >>> viterbi(observations, states, {2:2}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities all keys must be strings - - >>> viterbi(observations, states, {"a":2}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities all values must be float + Wikipedia example - >>> viterbi(observations, states, start_p, "invalid", emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities must be a dict - - >>> viterbi(observations, states, start_p, {"a":2}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all values must be dict - - >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all keys must be strings - - >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all keys must be strings - - >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities nested dictionary all values must be float - - >>> viterbi(observations, states, start_p, trans_p, "invalid") - Traceback (most recent call last): - ... - ValueError: emission_probabilities must be a dict - - >>> viterbi(observations, states, start_p, trans_p, None) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter + >>> observations = ["normal", "cold", "dizzy"] + >>> states = ["Healthy", "Fever"] + >>> start_p = {"Healthy": 0.6, "Fever": 0.4} + >>> trans_p = { + ... "Healthy": {"Healthy": 0.7, "Fever": 0.3}, + ... "Fever": {"Healthy": 0.4, "Fever": 0.6}, + ... } + >>> emit_p = { + ... "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1}, + ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, + ... } + >>> viterbi(observations, states, start_p, trans_p, emit_p) + ['Healthy', 'Healthy', 'Fever'] + >>> viterbi((), states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, (), start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, {}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, start_p, {}, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, start_p, trans_p, {}) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi("invalid", states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: observations_space must be a list + >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: observations_space must be a list of strings + >>> viterbi(observations, "invalid", start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: states_space must be a list + >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: states_space must be a list of strings + >>> viterbi(observations, states, "invalid", trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities must be a dict + >>> viterbi(observations, states, {2:2}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities all keys must be strings + >>> viterbi(observations, states, {"a":2}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities all values must be float + >>> viterbi(observations, states, start_p, "invalid", emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities must be a dict + >>> viterbi(observations, states, start_p, {"a":2}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all values must be dict + >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all keys must be strings + >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all keys must be strings + >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities nested dictionary all values must be float + >>> viterbi(observations, states, start_p, trans_p, "invalid") + Traceback (most recent call last): + ... + ValueError: emission_probabilities must be a dict + >>> viterbi(observations, states, start_p, trans_p, None) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter """ _validation( @@ -213,7 +196,6 @@ def _validation( ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, ... } >>> _validation(observations, states, start_p, trans_p, emit_p) - >>> _validation([], states, start_p, trans_p, emit_p) Traceback (most recent call last): ... @@ -242,7 +224,6 @@ def _validate_not_empty( """ >>> _validate_not_empty(["a"], ["b"], {"c":0.5}, ... {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) - >>> _validate_not_empty(["a"], ["b"], {"c":0.5}, {}, {"f": {"g": 0.7}}) Traceback (most recent call last): ... @@ -267,12 +248,10 @@ def _validate_not_empty( def _validate_lists(observations_space: Any, states_space: Any) -> None: """ >>> _validate_lists(["a"], ["b"]) - >>> _validate_lists(1234, ["b"]) Traceback (most recent call last): ... ValueError: observations_space must be a list - >>> _validate_lists(["a"], [3]) Traceback (most recent call last): ... @@ -285,7 +264,6 @@ def _validate_lists(observations_space: Any, states_space: Any) -> None: def _validate_list(_object: Any, var_name: str) -> None: """ >>> _validate_list(["a"], "mock_name") - >>> _validate_list("a", "mock_name") Traceback (most recent call last): ... @@ -294,14 +272,15 @@ def _validate_list(_object: Any, var_name: str) -> None: Traceback (most recent call last): ... ValueError: mock_name must be a list of strings - """ if not isinstance(_object, list): - raise ValueError(f"{var_name} must be a list") + msg = f"{var_name} must be a list" + raise ValueError(msg) else: for x in _object: if not isinstance(x, str): - raise ValueError(f"{var_name} must be a list of strings") + msg = f"{var_name} must be a list of strings" + raise ValueError(msg) def _validate_dicts( @@ -311,7 +290,6 @@ def _validate_dicts( ) -> None: """ >>> _validate_dicts({"c":0.5}, {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) - >>> _validate_dicts("invalid", {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) Traceback (most recent call last): ... @@ -337,7 +315,6 @@ def _validate_dicts( def _validate_nested_dict(_object: Any, var_name: str) -> None: """ >>> _validate_nested_dict({"a":{"b": 0.5}}, "mock_name") - >>> _validate_nested_dict("invalid", "mock_name") Traceback (most recent call last): ... @@ -365,7 +342,6 @@ def _validate_dict( ) -> None: """ >>> _validate_dict({"b": 0.5}, "mock_name", float) - >>> _validate_dict("invalid", "mock_name", float) Traceback (most recent call last): ... @@ -384,14 +360,15 @@ def _validate_dict( ValueError: mock_name nested dictionary all values must be float """ if not isinstance(_object, dict): - raise ValueError(f"{var_name} must be a dict") + msg = f"{var_name} must be a dict" + raise ValueError(msg) if not all(isinstance(x, str) for x in _object): - raise ValueError(f"{var_name} all keys must be strings") + msg = f"{var_name} all keys must be strings" + raise ValueError(msg) if not all(isinstance(x, value_type) for x in _object.values()): nested_text = "nested dictionary " if nested else "" - raise ValueError( - f"{var_name} {nested_text}all values must be {value_type.__name__}" - ) + msg = f"{var_name} {nested_text}all values must be {value_type.__name__}" + raise ValueError(msg) if __name__ == "__main__": diff --git a/dynamic_programming/wildcard_matching.py b/dynamic_programming/wildcard_matching.py new file mode 100644 index 000000000000..d9a1392720bd --- /dev/null +++ b/dynamic_programming/wildcard_matching.py @@ -0,0 +1,68 @@ +""" +Author : ilyas dahhou +Date : Oct 7, 2023 + +Task: +Given an input string and a pattern, implement wildcard pattern matching with support +for '?' and '*' where: +'?' matches any single character. +'*' matches any sequence of characters (including the empty sequence). +The matching should cover the entire input string (not partial). + +Runtime complexity: O(m * n) + +The implementation was tested on the +leetcode: https://leetcode.com/problems/wildcard-matching/ +""" + + +def is_match(string: str, pattern: str) -> bool: + """ + >>> is_match("", "") + True + >>> is_match("aa", "a") + False + >>> is_match("abc", "abc") + True + >>> is_match("abc", "*c") + True + >>> is_match("abc", "a*") + True + >>> is_match("abc", "*a*") + True + >>> is_match("abc", "?b?") + True + >>> is_match("abc", "*?") + True + >>> is_match("abc", "a*d") + False + >>> is_match("abc", "a*c?") + False + >>> is_match('baaabab','*****ba*****ba') + False + >>> is_match('baaabab','*****ba*****ab') + True + >>> is_match('aa','*') + True + """ + dp = [[False] * (len(pattern) + 1) for _ in string + "1"] + dp[0][0] = True + # Fill in the first row + for j, char in enumerate(pattern, 1): + if char == "*": + dp[0][j] = dp[0][j - 1] + # Fill in the rest of the DP table + for i, s_char in enumerate(string, 1): + for j, p_char in enumerate(pattern, 1): + if p_char in (s_char, "?"): + dp[i][j] = dp[i - 1][j - 1] + elif pattern[j - 1] == "*": + dp[i][j] = dp[i - 1][j] or dp[i][j - 1] + return dp[len(string)][len(pattern)] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(f"{is_match('baaabab','*****ba*****ab') = }") diff --git a/dynamic_programming/word_break.py b/dynamic_programming/word_break.py index 642ea0edf40d..4d7ac869080c 100644 --- a/dynamic_programming/word_break.py +++ b/dynamic_programming/word_break.py @@ -20,7 +20,7 @@ Space: O(n) """ -from functools import lru_cache +import functools from typing import Any @@ -80,7 +80,7 @@ def word_break(string: str, words: list[str]) -> bool: len_string = len(string) # Dynamic programming method - @lru_cache(maxsize=None) + @functools.cache def is_breakable(index: int) -> bool: """ >>> string = 'a' diff --git a/electronics/__init__.py b/electronics/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/electronics/apparent_power.py b/electronics/apparent_power.py new file mode 100644 index 000000000000..0ce1c2aa95b9 --- /dev/null +++ b/electronics/apparent_power.py @@ -0,0 +1,37 @@ +import cmath +import math + + +def apparent_power( + voltage: float, current: float, voltage_angle: float, current_angle: float +) -> complex: + """ + Calculate the apparent power in a single-phase AC circuit. + + Reference: https://en.wikipedia.org/wiki/AC_power#Apparent_power + + >>> apparent_power(100, 5, 0, 0) + (500+0j) + >>> apparent_power(100, 5, 90, 0) + (3.061616997868383e-14+500j) + >>> apparent_power(100, 5, -45, -60) + (-129.40952255126027-482.9629131445341j) + >>> apparent_power(200, 10, -30, -90) + (-999.9999999999998-1732.0508075688776j) + """ + # Convert angles from degrees to radians + voltage_angle_rad = math.radians(voltage_angle) + current_angle_rad = math.radians(current_angle) + + # Convert voltage and current to rectangular form + voltage_rect = cmath.rect(voltage, voltage_angle_rad) + current_rect = cmath.rect(current, current_angle_rad) + + # Calculate apparent power + return voltage_rect * current_rect + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/capacitor_equivalence.py b/electronics/capacitor_equivalence.py new file mode 100644 index 000000000000..274b18afb3ef --- /dev/null +++ b/electronics/capacitor_equivalence.py @@ -0,0 +1,53 @@ +# https://farside.ph.utexas.edu/teaching/316/lectures/node46.html + +from __future__ import annotations + + +def capacitor_parallel(capacitors: list[float]) -> float: + """ + Ceq = C1 + C2 + ... + Cn + Calculate the equivalent resistance for any number of capacitors in parallel. + >>> capacitor_parallel([5.71389, 12, 3]) + 20.71389 + >>> capacitor_parallel([5.71389, 12, -3]) + Traceback (most recent call last): + ... + ValueError: Capacitor at index 2 has a negative value! + """ + sum_c = 0.0 + for index, capacitor in enumerate(capacitors): + if capacitor < 0: + msg = f"Capacitor at index {index} has a negative value!" + raise ValueError(msg) + sum_c += capacitor + return sum_c + + +def capacitor_series(capacitors: list[float]) -> float: + """ + Ceq = 1/ (1/C1 + 1/C2 + ... + 1/Cn) + >>> capacitor_series([5.71389, 12, 3]) + 1.6901062252507735 + >>> capacitor_series([5.71389, 12, -3]) + Traceback (most recent call last): + ... + ValueError: Capacitor at index 2 has a negative or zero value! + >>> capacitor_series([5.71389, 12, 0.000]) + Traceback (most recent call last): + ... + ValueError: Capacitor at index 2 has a negative or zero value! + """ + + first_sum = 0.0 + for index, capacitor in enumerate(capacitors): + if capacitor <= 0: + msg = f"Capacitor at index {index} has a negative or zero value!" + raise ValueError(msg) + first_sum += 1 / capacitor + return 1 / first_sum + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/charging_capacitor.py b/electronics/charging_capacitor.py new file mode 100644 index 000000000000..0021e4e345e0 --- /dev/null +++ b/electronics/charging_capacitor.py @@ -0,0 +1,72 @@ +# source - The ARRL Handbook for Radio Communications +# https://en.wikipedia.org/wiki/RC_time_constant + +""" +Description +----------- +When a capacitor is connected with a potential source (AC or DC). It starts to charge +at a general speed but when a resistor is connected in the circuit with in series to +a capacitor then the capacitor charges slowly means it will take more time than usual. +while the capacitor is being charged, the voltage is in exponential function with time. + +'resistance(ohms) * capacitance(farads)' is called RC-timeconstant which may also be +represented as Ï„ (tau). By using this RC-timeconstant we can find the voltage at any +time 't' from the initiation of charging a capacitor with the help of the exponential +function containing RC. Both at charging and discharging of a capacitor. +""" + +from math import exp # value of exp = 2.718281828459… + + +def charging_capacitor( + source_voltage: float, # voltage in volts. + resistance: float, # resistance in ohms. + capacitance: float, # capacitance in farads. + time_sec: float, # time in seconds after charging initiation of capacitor. +) -> float: + """ + Find capacitor voltage at any nth second after initiating its charging. + + Examples + -------- + >>> charging_capacitor(source_voltage=.2,resistance=.9,capacitance=8.4,time_sec=.5) + 0.013 + + >>> charging_capacitor(source_voltage=2.2,resistance=3.5,capacitance=2.4,time_sec=9) + 1.446 + + >>> charging_capacitor(source_voltage=15,resistance=200,capacitance=20,time_sec=2) + 0.007 + + >>> charging_capacitor(20, 2000, 30*pow(10,-5), 4) + 19.975 + + >>> charging_capacitor(source_voltage=0,resistance=10.0,capacitance=.30,time_sec=3) + Traceback (most recent call last): + ... + ValueError: Source voltage must be positive. + + >>> charging_capacitor(source_voltage=20,resistance=-2000,capacitance=30,time_sec=4) + Traceback (most recent call last): + ... + ValueError: Resistance must be positive. + + >>> charging_capacitor(source_voltage=30,resistance=1500,capacitance=0,time_sec=4) + Traceback (most recent call last): + ... + ValueError: Capacitance must be positive. + """ + + if source_voltage <= 0: + raise ValueError("Source voltage must be positive.") + if resistance <= 0: + raise ValueError("Resistance must be positive.") + if capacitance <= 0: + raise ValueError("Capacitance must be positive.") + return round(source_voltage * (1 - exp(-time_sec / (resistance * capacitance))), 3) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/charging_inductor.py b/electronics/charging_inductor.py new file mode 100644 index 000000000000..8a3bbc0bbfcd --- /dev/null +++ b/electronics/charging_inductor.py @@ -0,0 +1,97 @@ +# source - The ARRL Handbook for Radio Communications +# https://en.wikipedia.org/wiki/RL_circuit + +""" +Description +----------- +Inductor is a passive electronic device which stores energy but unlike capacitor, it +stores energy in its 'magnetic field' or 'magnetostatic field'. + +When inductor is connected to 'DC' current source nothing happens it just works like a +wire because it's real effect cannot be seen while 'DC' is connected, its not even +going to store energy. Inductor stores energy only when it is working on 'AC' current. + +Connecting a inductor in series with a resistor(when R = 0) to a 'AC' potential source, +from zero to a finite value causes a sudden voltage to induced in inductor which +opposes the current. which results in initially slowly current rise. However it would +cease if there is no further changes in current. With resistance zero current will never +stop rising. + +'Resistance(ohms) / Inductance(henrys)' is known as RL-timeconstant. It also represents +as Ï„ (tau). While the charging of a inductor with a resistor results in +a exponential function. + +when inductor is connected across 'AC' potential source. It starts to store the energy +in its 'magnetic field'.with the help 'RL-time-constant' we can find current at any time +in inductor while it is charging. +""" + +from math import exp # value of exp = 2.718281828459… + + +def charging_inductor( + source_voltage: float, # source_voltage should be in volts. + resistance: float, # resistance should be in ohms. + inductance: float, # inductance should be in henrys. + time: float, # time should in seconds. +) -> float: + """ + Find inductor current at any nth second after initiating its charging. + + Examples + -------- + >>> charging_inductor(source_voltage=5.8,resistance=1.5,inductance=2.3,time=2) + 2.817 + + >>> charging_inductor(source_voltage=8,resistance=5,inductance=3,time=2) + 1.543 + + >>> charging_inductor(source_voltage=8,resistance=5*pow(10,2),inductance=3,time=2) + 0.016 + + >>> charging_inductor(source_voltage=-8,resistance=100,inductance=15,time=12) + Traceback (most recent call last): + ... + ValueError: Source voltage must be positive. + + >>> charging_inductor(source_voltage=80,resistance=-15,inductance=100,time=5) + Traceback (most recent call last): + ... + ValueError: Resistance must be positive. + + >>> charging_inductor(source_voltage=12,resistance=200,inductance=-20,time=5) + Traceback (most recent call last): + ... + ValueError: Inductance must be positive. + + >>> charging_inductor(source_voltage=0,resistance=200,inductance=20,time=5) + Traceback (most recent call last): + ... + ValueError: Source voltage must be positive. + + >>> charging_inductor(source_voltage=10,resistance=0,inductance=20,time=5) + Traceback (most recent call last): + ... + ValueError: Resistance must be positive. + + >>> charging_inductor(source_voltage=15, resistance=25, inductance=0, time=5) + Traceback (most recent call last): + ... + ValueError: Inductance must be positive. + """ + + if source_voltage <= 0: + raise ValueError("Source voltage must be positive.") + if resistance <= 0: + raise ValueError("Resistance must be positive.") + if inductance <= 0: + raise ValueError("Inductance must be positive.") + return round( + source_voltage / resistance * (1 - exp((-time * resistance) / inductance)), 3 + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/circular_convolution.py b/electronics/circular_convolution.py new file mode 100644 index 000000000000..d06e76be759b --- /dev/null +++ b/electronics/circular_convolution.py @@ -0,0 +1,98 @@ +# https://en.wikipedia.org/wiki/Circular_convolution + +""" +Circular convolution, also known as cyclic convolution, +is a special case of periodic convolution, which is the convolution of two +periodic functions that have the same period. Periodic convolution arises, +for example, in the context of the discrete-time Fourier transform (DTFT). +In particular, the DTFT of the product of two discrete sequences is the periodic +convolution of the DTFTs of the individual sequences. And each DTFT is a periodic +summation of a continuous Fourier transform function. + +Source: https://en.wikipedia.org/wiki/Circular_convolution +""" + +import doctest +from collections import deque + +import numpy as np + + +class CircularConvolution: + """ + This class stores the first and second signal and performs the circular convolution + """ + + def __init__(self) -> None: + """ + First signal and second signal are stored as 1-D array + """ + + self.first_signal = [2, 1, 2, -1] + self.second_signal = [1, 2, 3, 4] + + def circular_convolution(self) -> list[float]: + """ + This function performs the circular convolution of the first and second signal + using matrix method + + Usage: + >>> convolution = CircularConvolution() + >>> convolution.circular_convolution() + [10.0, 10.0, 6.0, 14.0] + + >>> convolution.first_signal = [0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6] + >>> convolution.second_signal = [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5] + >>> convolution.circular_convolution() + [5.2, 6.0, 6.48, 6.64, 6.48, 6.0, 5.2, 4.08] + + >>> convolution.first_signal = [-1, 1, 2, -2] + >>> convolution.second_signal = [0.5, 1, -1, 2, 0.75] + >>> convolution.circular_convolution() + [6.25, -3.0, 1.5, -2.0, -2.75] + + >>> convolution.first_signal = [1, -1, 2, 3, -1] + >>> convolution.second_signal = [1, 2, 3] + >>> convolution.circular_convolution() + [8.0, -2.0, 3.0, 4.0, 11.0] + + """ + + length_first_signal = len(self.first_signal) + length_second_signal = len(self.second_signal) + + max_length = max(length_first_signal, length_second_signal) + + # create a zero matrix of max_length x max_length + matrix = [[0] * max_length for i in range(max_length)] + + # fills the smaller signal with zeros to make both signals of same length + if length_first_signal < length_second_signal: + self.first_signal += [0] * (max_length - length_first_signal) + elif length_first_signal > length_second_signal: + self.second_signal += [0] * (max_length - length_second_signal) + + """ + Fills the matrix in the following way assuming 'x' is the signal of length 4 + [ + [x[0], x[3], x[2], x[1]], + [x[1], x[0], x[3], x[2]], + [x[2], x[1], x[0], x[3]], + [x[3], x[2], x[1], x[0]] + ] + """ + for i in range(max_length): + rotated_signal = deque(self.second_signal) + rotated_signal.rotate(i) + for j, item in enumerate(rotated_signal): + matrix[i][j] += item + + # multiply the matrix with the first signal + final_signal = np.matmul(np.transpose(matrix), np.transpose(self.first_signal)) + + # rounding-off to two decimal places + return [float(round(i, 2)) for i in final_signal] + + +if __name__ == "__main__": + doctest.testmod() diff --git a/electronics/coulombs_law.py b/electronics/coulombs_law.py index 18c1a8179eb6..74bbea5ea8ec 100644 --- a/electronics/coulombs_law.py +++ b/electronics/coulombs_law.py @@ -20,8 +20,8 @@ def couloumbs_law( Reference ---------- - Coulomb (1785) "Premier mémoire sur l’électricité et le magnétisme," - Histoire de l’Académie Royale des Sciences, pp. 569–577. + Coulomb (1785) "Premier mémoire sur l'électricité et le magnétisme," + Histoire de l'Académie Royale des Sciences, pp. 569-577. Parameters ---------- diff --git a/electronics/electric_conductivity.py b/electronics/electric_conductivity.py index 11f2a607d214..65bb6c5ceaf0 100644 --- a/electronics/electric_conductivity.py +++ b/electronics/electric_conductivity.py @@ -21,6 +21,26 @@ def electric_conductivity( ('conductivity', 5.12672e-14) >>> electric_conductivity(conductivity=1000, electron_conc=0, mobility=1200) ('electron_conc', 5.201506356240767e+18) + >>> electric_conductivity(conductivity=-10, electron_conc=100, mobility=0) + Traceback (most recent call last): + ... + ValueError: Conductivity cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=-10, mobility=0) + Traceback (most recent call last): + ... + ValueError: Electron concentration cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=-10) + Traceback (most recent call last): + ... + ValueError: mobility cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=0) + Traceback (most recent call last): + ... + ValueError: You cannot supply more or less than 2 values + >>> electric_conductivity(conductivity=50, electron_conc=200, mobility=300) + Traceback (most recent call last): + ... + ValueError: You cannot supply more or less than 2 values """ if (conductivity, electron_conc, mobility).count(0) != 1: raise ValueError("You cannot supply more or less than 2 values") diff --git a/electronics/electric_power.py b/electronics/electric_power.py index e59795601791..8e3454e39c3f 100644 --- a/electronics/electric_power.py +++ b/electronics/electric_power.py @@ -1,7 +1,12 @@ # https://en.m.wikipedia.org/wiki/Electric_power from __future__ import annotations -from collections import namedtuple +from typing import NamedTuple + + +class Result(NamedTuple): + name: str + value: float def electric_power(voltage: float, current: float, power: float) -> tuple: @@ -10,41 +15,42 @@ def electric_power(voltage: float, current: float, power: float) -> tuple: fundamental value of electrical system. examples are below: >>> electric_power(voltage=0, current=2, power=5) - result(name='voltage', value=2.5) + Result(name='voltage', value=2.5) >>> electric_power(voltage=2, current=2, power=0) - result(name='power', value=4.0) + Result(name='power', value=4.0) >>> electric_power(voltage=-2, current=3, power=0) - result(name='power', value=6.0) + Result(name='power', value=6.0) >>> electric_power(voltage=2, current=4, power=2) Traceback (most recent call last): ... - ValueError: Only one argument must be 0 + ValueError: Exactly one argument must be 0 >>> electric_power(voltage=0, current=0, power=2) Traceback (most recent call last): ... - ValueError: Only one argument must be 0 + ValueError: Exactly one argument must be 0 >>> electric_power(voltage=0, current=2, power=-4) Traceback (most recent call last): ... ValueError: Power cannot be negative in any electrical/electronics system >>> electric_power(voltage=2.2, current=2.2, power=0) - result(name='power', value=4.84) + Result(name='power', value=4.84) + >>> electric_power(current=0, power=6, voltage=2) + Result(name='current', value=3.0) """ - result = namedtuple("result", "name value") if (voltage, current, power).count(0) != 1: - raise ValueError("Only one argument must be 0") + raise ValueError("Exactly one argument must be 0") elif power < 0: raise ValueError( "Power cannot be negative in any electrical/electronics system" ) elif voltage == 0: - return result("voltage", power / current) + return Result("voltage", power / current) elif current == 0: - return result("current", power / voltage) + return Result("current", power / voltage) elif power == 0: - return result("power", float(round(abs(voltage * current), 2))) + return Result("power", float(round(abs(voltage * current), 2))) else: - raise ValueError("Exactly one argument must be 0") + raise AssertionError if __name__ == "__main__": diff --git a/electronics/electrical_impedance.py b/electronics/electrical_impedance.py index 44041ff790b6..4f4f1d308293 100644 --- a/electronics/electrical_impedance.py +++ b/electronics/electrical_impedance.py @@ -6,7 +6,7 @@ from __future__ import annotations -from math import pow, sqrt +from math import pow, sqrt # noqa: A004 def electrical_impedance( diff --git a/electronics/ic_555_timer.py b/electronics/ic_555_timer.py new file mode 100644 index 000000000000..e187e1928dca --- /dev/null +++ b/electronics/ic_555_timer.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +""" + Calculate the frequency and/or duty cycle of an astable 555 timer. + * https://en.wikipedia.org/wiki/555_timer_IC#Astable + + These functions take in the value of the external resistances (in ohms) + and capacitance (in Microfarad), and calculates the following: + + ------------------------------------- + | Freq = 1.44 /[( R1+ 2 x R2) x C1] | ... in Hz + ------------------------------------- + where Freq is the frequency, + R1 is the first resistance in ohms, + R2 is the second resistance in ohms, + C1 is the capacitance in Microfarads. + + ------------------------------------------------ + | Duty Cycle = (R1 + R2) / (R1 + 2 x R2) x 100 | ... in % + ------------------------------------------------ + where R1 is the first resistance in ohms, + R2 is the second resistance in ohms. +""" + + +def astable_frequency( + resistance_1: float, resistance_2: float, capacitance: float +) -> float: + """ + Usage examples: + >>> astable_frequency(resistance_1=45, resistance_2=45, capacitance=7) + 1523.8095238095239 + >>> astable_frequency(resistance_1=356, resistance_2=234, capacitance=976) + 1.7905459175553078 + >>> astable_frequency(resistance_1=2, resistance_2=-1, capacitance=2) + Traceback (most recent call last): + ... + ValueError: All values must be positive + >>> astable_frequency(resistance_1=45, resistance_2=45, capacitance=0) + Traceback (most recent call last): + ... + ValueError: All values must be positive + """ + + if resistance_1 <= 0 or resistance_2 <= 0 or capacitance <= 0: + raise ValueError("All values must be positive") + return (1.44 / ((resistance_1 + 2 * resistance_2) * capacitance)) * 10**6 + + +def astable_duty_cycle(resistance_1: float, resistance_2: float) -> float: + """ + Usage examples: + >>> astable_duty_cycle(resistance_1=45, resistance_2=45) + 66.66666666666666 + >>> astable_duty_cycle(resistance_1=356, resistance_2=234) + 71.60194174757282 + >>> astable_duty_cycle(resistance_1=2, resistance_2=-1) + Traceback (most recent call last): + ... + ValueError: All values must be positive + >>> astable_duty_cycle(resistance_1=0, resistance_2=0) + Traceback (most recent call last): + ... + ValueError: All values must be positive + """ + + if resistance_1 <= 0 or resistance_2 <= 0: + raise ValueError("All values must be positive") + return (resistance_1 + resistance_2) / (resistance_1 + 2 * resistance_2) * 100 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/real_and_reactive_power.py b/electronics/real_and_reactive_power.py new file mode 100644 index 000000000000..81dcba800e82 --- /dev/null +++ b/electronics/real_and_reactive_power.py @@ -0,0 +1,49 @@ +import math + + +def real_power(apparent_power: float, power_factor: float) -> float: + """ + Calculate real power from apparent power and power factor. + + Examples: + >>> real_power(100, 0.9) + 90.0 + >>> real_power(0, 0.8) + 0.0 + >>> real_power(100, -0.9) + -90.0 + """ + if ( + not isinstance(power_factor, (int, float)) + or power_factor < -1 + or power_factor > 1 + ): + raise ValueError("power_factor must be a valid float value between -1 and 1.") + return apparent_power * power_factor + + +def reactive_power(apparent_power: float, power_factor: float) -> float: + """ + Calculate reactive power from apparent power and power factor. + + Examples: + >>> reactive_power(100, 0.9) + 43.58898943540673 + >>> reactive_power(0, 0.8) + 0.0 + >>> reactive_power(100, -0.9) + 43.58898943540673 + """ + if ( + not isinstance(power_factor, (int, float)) + or power_factor < -1 + or power_factor > 1 + ): + raise ValueError("power_factor must be a valid float value between -1 and 1.") + return apparent_power * math.sqrt(1 - power_factor**2) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/resistor_color_code.py b/electronics/resistor_color_code.py new file mode 100644 index 000000000000..189d19946d9d --- /dev/null +++ b/electronics/resistor_color_code.py @@ -0,0 +1,374 @@ +""" +Title : Calculating the resistance of a n band resistor using the color codes + +Description : + Resistors resist the flow of electrical current.Each one has a value that tells how + strongly it resists current flow.This value's unit is the ohm, often noted with the + Greek letter omega: Ω. + + The colored bands on a resistor can tell you everything you need to know about its + value and tolerance, as long as you understand how to read them. The order in which + the colors are arranged is very important, and each value of resistor has its own + unique combination. + + The color coding for resistors is an international standard that is defined in IEC + 60062. + + The number of bands present in a resistor varies from three to six. These represent + significant figures, multiplier, tolerance, reliability, and temperature coefficient + Each color used for a type of band has a value assigned to it. It is read from left + to right. + All resistors will have significant figures and multiplier bands. In a three band + resistor first two bands from the left represent significant figures and the third + represents the multiplier band. + + Significant figures - The number of significant figures band in a resistor can vary + from two to three. + Colors and values associated with significant figure bands - + (Black = 0, Brown = 1, Red = 2, Orange = 3, Yellow = 4, Green = 5, Blue = 6, + Violet = 7, Grey = 8, White = 9) + + Multiplier - There will be one multiplier band in a resistor. It is multiplied with + the significant figures obtained from previous bands. + Colors and values associated with multiplier band - + (Black = 100, Brown = 10^1, Red = 10^2, Orange = 10^3, Yellow = 10^4, Green = 10^5, + Blue = 10^6, Violet = 10^7, Grey = 10^8, White = 10^9, Gold = 10^-1, Silver = 10^-2) + Note that multiplier bands use Gold and Silver which are not used for significant + figure bands. + + Tolerance - The tolerance band is not always present. It can be seen in four band + resistors and above. This is a percentage by which the resistor value can vary. + Colors and values associated with tolerance band - + (Brown = 1%, Red = 2%, Orange = 0.05%, Yellow = 0.02%, Green = 0.5%,Blue = 0.25%, + Violet = 0.1%, Grey = 0.01%, Gold = 5%, Silver = 10%) + If no color is mentioned then by default tolerance is 20% + Note that tolerance band does not use Black and White colors. + + Temperature Coeffecient - Indicates the change in resistance of the component as + a function of ambient temperature in terms of ppm/K. + It is present in six band resistors. + Colors and values associated with Temperature coeffecient - + (Black = 250 ppm/K, Brown = 100 ppm/K, Red = 50 ppm/K, Orange = 15 ppm/K, + Yellow = 25 ppm/K, Green = 20 ppm/K, Blue = 10 ppm/K, Violet = 5 ppm/K, + Grey = 1 ppm/K) + Note that temperature coeffecient band does not use White, Gold, Silver colors. + +Sources : + https://www.calculator.net/resistor-calculator.html + https://learn.parallax.com/support/reference/resistor-color-codes + https://byjus.com/physics/resistor-colour-codes/ +""" + +valid_colors: list = [ + "Black", + "Brown", + "Red", + "Orange", + "Yellow", + "Green", + "Blue", + "Violet", + "Grey", + "White", + "Gold", + "Silver", +] + +significant_figures_color_values: dict[str, int] = { + "Black": 0, + "Brown": 1, + "Red": 2, + "Orange": 3, + "Yellow": 4, + "Green": 5, + "Blue": 6, + "Violet": 7, + "Grey": 8, + "White": 9, +} + +multiplier_color_values: dict[str, float] = { + "Black": 10**0, + "Brown": 10**1, + "Red": 10**2, + "Orange": 10**3, + "Yellow": 10**4, + "Green": 10**5, + "Blue": 10**6, + "Violet": 10**7, + "Grey": 10**8, + "White": 10**9, + "Gold": 10**-1, + "Silver": 10**-2, +} + +tolerance_color_values: dict[str, float] = { + "Brown": 1, + "Red": 2, + "Orange": 0.05, + "Yellow": 0.02, + "Green": 0.5, + "Blue": 0.25, + "Violet": 0.1, + "Grey": 0.01, + "Gold": 5, + "Silver": 10, +} + +temperature_coeffecient_color_values: dict[str, int] = { + "Black": 250, + "Brown": 100, + "Red": 50, + "Orange": 15, + "Yellow": 25, + "Green": 20, + "Blue": 10, + "Violet": 5, + "Grey": 1, +} + +band_types: dict[int, dict[str, int]] = { + 3: {"significant": 2, "multiplier": 1}, + 4: {"significant": 2, "multiplier": 1, "tolerance": 1}, + 5: {"significant": 3, "multiplier": 1, "tolerance": 1}, + 6: {"significant": 3, "multiplier": 1, "tolerance": 1, "temp_coeffecient": 1}, +} + + +def get_significant_digits(colors: list) -> str: + """ + Function returns the digit associated with the color. Function takes a + list containing colors as input and returns digits as string + + >>> get_significant_digits(['Black','Blue']) + '06' + + >>> get_significant_digits(['Aqua','Blue']) + Traceback (most recent call last): + ... + ValueError: Aqua is not a valid color for significant figure bands + + """ + digit = "" + for color in colors: + if color not in significant_figures_color_values: + msg = f"{color} is not a valid color for significant figure bands" + raise ValueError(msg) + digit = digit + str(significant_figures_color_values[color]) + return str(digit) + + +def get_multiplier(color: str) -> float: + """ + Function returns the multiplier value associated with the color. + Function takes color as input and returns multiplier value + + >>> get_multiplier('Gold') + 0.1 + + >>> get_multiplier('Ivory') + Traceback (most recent call last): + ... + ValueError: Ivory is not a valid color for multiplier band + + """ + if color not in multiplier_color_values: + msg = f"{color} is not a valid color for multiplier band" + raise ValueError(msg) + return multiplier_color_values[color] + + +def get_tolerance(color: str) -> float: + """ + Function returns the tolerance value associated with the color. + Function takes color as input and returns tolerance value. + + >>> get_tolerance('Green') + 0.5 + + >>> get_tolerance('Indigo') + Traceback (most recent call last): + ... + ValueError: Indigo is not a valid color for tolerance band + + """ + if color not in tolerance_color_values: + msg = f"{color} is not a valid color for tolerance band" + raise ValueError(msg) + return tolerance_color_values[color] + + +def get_temperature_coeffecient(color: str) -> int: + """ + Function returns the temperature coeffecient value associated with the color. + Function takes color as input and returns temperature coeffecient value. + + >>> get_temperature_coeffecient('Yellow') + 25 + + >>> get_temperature_coeffecient('Cyan') + Traceback (most recent call last): + ... + ValueError: Cyan is not a valid color for temperature coeffecient band + + """ + if color not in temperature_coeffecient_color_values: + msg = f"{color} is not a valid color for temperature coeffecient band" + raise ValueError(msg) + return temperature_coeffecient_color_values[color] + + +def get_band_type_count(total_number_of_bands: int, type_of_band: str) -> int: + """ + Function returns the number of bands of a given type in a resistor with n bands + Function takes total_number_of_bands and type_of_band as input and returns + number of bands belonging to that type in the given resistor + + >>> get_band_type_count(3,'significant') + 2 + + >>> get_band_type_count(2,'significant') + Traceback (most recent call last): + ... + ValueError: 2 is not a valid number of bands + + >>> get_band_type_count(3,'sign') + Traceback (most recent call last): + ... + ValueError: sign is not valid for a 3 band resistor + + >>> get_band_type_count(3,'tolerance') + Traceback (most recent call last): + ... + ValueError: tolerance is not valid for a 3 band resistor + + >>> get_band_type_count(5,'temp_coeffecient') + Traceback (most recent call last): + ... + ValueError: temp_coeffecient is not valid for a 5 band resistor + + """ + if total_number_of_bands not in band_types: + msg = f"{total_number_of_bands} is not a valid number of bands" + raise ValueError(msg) + if type_of_band not in band_types[total_number_of_bands]: + msg = f"{type_of_band} is not valid for a {total_number_of_bands} band resistor" + raise ValueError(msg) + return band_types[total_number_of_bands][type_of_band] + + +def check_validity(number_of_bands: int, colors: list) -> bool: + """ + Function checks if the input provided is valid or not. + Function takes number_of_bands and colors as input and returns + True if it is valid + + >>> check_validity(3, ["Black","Blue","Orange"]) + True + + >>> check_validity(4, ["Black","Blue","Orange"]) + Traceback (most recent call last): + ... + ValueError: Expecting 4 colors, provided 3 colors + + >>> check_validity(3, ["Cyan","Red","Yellow"]) + Traceback (most recent call last): + ... + ValueError: Cyan is not a valid color + + """ + if number_of_bands >= 3 and number_of_bands <= 6: + if number_of_bands == len(colors): + for color in colors: + if color not in valid_colors: + msg = f"{color} is not a valid color" + raise ValueError(msg) + return True + else: + msg = f"Expecting {number_of_bands} colors, provided {len(colors)} colors" + raise ValueError(msg) + else: + msg = "Invalid number of bands. Resistor bands must be 3 to 6" + raise ValueError(msg) + + +def calculate_resistance(number_of_bands: int, color_code_list: list) -> dict: + """ + Function calculates the total resistance of the resistor using the color codes. + Function takes number_of_bands, color_code_list as input and returns + resistance + + >>> calculate_resistance(3, ["Black","Blue","Orange"]) + {'resistance': '6000Ω ±20% '} + + >>> calculate_resistance(4, ["Orange","Green","Blue","Gold"]) + {'resistance': '35000000Ω ±5% '} + + >>> calculate_resistance(5, ["Violet","Brown","Grey","Silver","Green"]) + {'resistance': '7.18Ω ±0.5% '} + + >>> calculate_resistance(6, ["Red","Green","Blue","Yellow","Orange","Grey"]) + {'resistance': '2560000Ω ±0.05% 1 ppm/K'} + + >>> calculate_resistance(0, ["Violet","Brown","Grey","Silver","Green"]) + Traceback (most recent call last): + ... + ValueError: Invalid number of bands. Resistor bands must be 3 to 6 + + >>> calculate_resistance(4, ["Violet","Brown","Grey","Silver","Green"]) + Traceback (most recent call last): + ... + ValueError: Expecting 4 colors, provided 5 colors + + >>> calculate_resistance(4, ["Violet","Silver","Brown","Grey"]) + Traceback (most recent call last): + ... + ValueError: Silver is not a valid color for significant figure bands + + >>> calculate_resistance(4, ["Violet","Blue","Lime","Grey"]) + Traceback (most recent call last): + ... + ValueError: Lime is not a valid color + + """ + is_valid = check_validity(number_of_bands, color_code_list) + if is_valid: + number_of_significant_bands = get_band_type_count( + number_of_bands, "significant" + ) + significant_colors = color_code_list[:number_of_significant_bands] + significant_digits = int(get_significant_digits(significant_colors)) + multiplier_color = color_code_list[number_of_significant_bands] + multiplier = get_multiplier(multiplier_color) + if number_of_bands == 3: + tolerance_color = None + else: + tolerance_color = color_code_list[number_of_significant_bands + 1] + tolerance = ( + 20 if tolerance_color is None else get_tolerance(str(tolerance_color)) + ) + if number_of_bands != 6: + temperature_coeffecient_color = None + else: + temperature_coeffecient_color = color_code_list[ + number_of_significant_bands + 2 + ] + temperature_coeffecient = ( + 0 + if temperature_coeffecient_color is None + else get_temperature_coeffecient(str(temperature_coeffecient_color)) + ) + resisitance = significant_digits * multiplier + if temperature_coeffecient == 0: + answer = f"{resisitance}Ω ±{tolerance}% " + else: + answer = f"{resisitance}Ω ±{tolerance}% {temperature_coeffecient} ppm/K" + return {"resistance": answer} + else: + raise ValueError("Input is invalid") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/electronics/resistor_equivalence.py b/electronics/resistor_equivalence.py index 7142f838a065..c4ea7d4b757e 100644 --- a/electronics/resistor_equivalence.py +++ b/electronics/resistor_equivalence.py @@ -20,12 +20,11 @@ def resistor_parallel(resistors: list[float]) -> float: """ first_sum = 0.00 - index = 0 - for resistor in resistors: + for index, resistor in enumerate(resistors): if resistor <= 0: - raise ValueError(f"Resistor at index {index} has a negative or zero value!") + msg = f"Resistor at index {index} has a negative or zero value!" + raise ValueError(msg) first_sum += 1 / float(resistor) - index += 1 return 1 / first_sum @@ -43,12 +42,11 @@ def resistor_series(resistors: list[float]) -> float: ValueError: Resistor at index 2 has a negative value! """ sum_r = 0.00 - index = 0 - for resistor in resistors: + for index, resistor in enumerate(resistors): sum_r += resistor if resistor < 0: - raise ValueError(f"Resistor at index {index} has a negative value!") - index += 1 + msg = f"Resistor at index {index} has a negative value!" + raise ValueError(msg) return sum_r diff --git a/electronics/wheatstone_bridge.py b/electronics/wheatstone_bridge.py new file mode 100644 index 000000000000..3529a09339c4 --- /dev/null +++ b/electronics/wheatstone_bridge.py @@ -0,0 +1,41 @@ +# https://en.wikipedia.org/wiki/Wheatstone_bridge +from __future__ import annotations + + +def wheatstone_solver( + resistance_1: float, resistance_2: float, resistance_3: float +) -> float: + """ + This function can calculate the unknown resistance in an wheatstone network, + given that the three other resistances in the network are known. + The formula to calculate the same is: + + --------------- + |Rx=(R2/R1)*R3| + --------------- + + Usage examples: + >>> wheatstone_solver(resistance_1=2, resistance_2=4, resistance_3=5) + 10.0 + >>> wheatstone_solver(resistance_1=356, resistance_2=234, resistance_3=976) + 641.5280898876405 + >>> wheatstone_solver(resistance_1=2, resistance_2=-1, resistance_3=2) + Traceback (most recent call last): + ... + ValueError: All resistance values must be positive + >>> wheatstone_solver(resistance_1=0, resistance_2=0, resistance_3=2) + Traceback (most recent call last): + ... + ValueError: All resistance values must be positive + """ + + if resistance_1 <= 0 or resistance_2 <= 0 or resistance_3 <= 0: + raise ValueError("All resistance values must be positive") + else: + return float((resistance_2 / resistance_1) * resistance_3) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/file_transfer/receive_file.py b/file_transfer/receive_file.py index 37a503036dc2..f50ad9fe1107 100644 --- a/file_transfer/receive_file.py +++ b/file_transfer/receive_file.py @@ -1,8 +1,9 @@ -if __name__ == "__main__": - import socket # Import socket module +import socket + - sock = socket.socket() # Create a socket object - host = socket.gethostname() # Get local machine name +def main(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + host = socket.gethostname() port = 12312 sock.connect((host, port)) @@ -13,11 +14,14 @@ print("Receiving data...") while True: data = sock.recv(1024) - print(f"{data = }") if not data: break - out_file.write(data) # Write data to a file + out_file.write(data) - print("Successfully got the file") + print("Successfully received the file") sock.close() print("Connection closed") + + +if __name__ == "__main__": + main() diff --git a/financial/ABOUT.md b/financial/README.md similarity index 97% rename from financial/ABOUT.md rename to financial/README.md index f6b0647f8201..e5d3a84c8381 100644 --- a/financial/ABOUT.md +++ b/financial/README.md @@ -1,4 +1,4 @@ -### Interest +# Interest * Compound Interest: "Compound interest is calculated by multiplying the initial principal amount by one plus the annual interest rate raised to the number of compound periods minus one." [Compound Interest](https://www.investopedia.com/) * Simple Interest: "Simple interest paid or received over a certain period is a fixed percentage of the principal amount that was borrowed or lent. " [Simple Interest](https://www.investopedia.com/) diff --git a/financial/exponential_moving_average.py b/financial/exponential_moving_average.py new file mode 100644 index 000000000000..b56eb2712415 --- /dev/null +++ b/financial/exponential_moving_average.py @@ -0,0 +1,73 @@ +""" +Calculate the exponential moving average (EMA) on the series of stock prices. +Wikipedia Reference: https://en.wikipedia.org/wiki/Exponential_smoothing +https://www.investopedia.com/terms/e/ema.asp#toc-what-is-an-exponential +-moving-average-ema + +Exponential moving average is used in finance to analyze changes stock prices. +EMA is used in conjunction with Simple moving average (SMA), EMA reacts to the +changes in the value quicker than SMA, which is one of the advantages of using EMA. +""" + +from collections.abc import Iterator + + +def exponential_moving_average( + stock_prices: Iterator[float], window_size: int +) -> Iterator[float]: + """ + Yields exponential moving averages of the given stock prices. + >>> tuple(exponential_moving_average(iter([2, 5, 3, 8.2, 6, 9, 10]), 3)) + (2, 3.5, 3.25, 5.725, 5.8625, 7.43125, 8.715625) + + :param stock_prices: A stream of stock prices + :param window_size: The number of stock prices that will trigger a new calculation + of the exponential average (window_size > 0) + :return: Yields a sequence of exponential moving averages + + Formula: + + st = alpha * xt + (1 - alpha) * st_prev + + Where, + st : Exponential moving average at timestamp t + xt : stock price in from the stock prices at timestamp t + st_prev : Exponential moving average at timestamp t-1 + alpha : 2/(1 + window_size) - smoothing factor + + Exponential moving average (EMA) is a rule of thumb technique for + smoothing time series data using an exponential window function. + """ + + if window_size <= 0: + raise ValueError("window_size must be > 0") + + # Calculating smoothing factor + alpha = 2 / (1 + window_size) + + # Exponential average at timestamp t + moving_average = 0.0 + + for i, stock_price in enumerate(stock_prices): + if i <= window_size: + # Assigning simple moving average till the window_size for the first time + # is reached + moving_average = (moving_average + stock_price) * 0.5 if i else stock_price + else: + # Calculating exponential moving average based on current timestamp data + # point and previous exponential average value + moving_average = (alpha * stock_price) + ((1 - alpha) * moving_average) + yield moving_average + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + stock_prices = [2.0, 5, 3, 8.2, 6, 9, 10] + window_size = 3 + result = tuple(exponential_moving_average(iter(stock_prices), window_size)) + print(f"{stock_prices = }") + print(f"{window_size = }") + print(f"{result = }") diff --git a/financial/interest.py b/financial/interest.py index c69c730457d9..33d02e27ccb3 100644 --- a/financial/interest.py +++ b/financial/interest.py @@ -4,7 +4,7 @@ def simple_interest( - principal: float, daily_interest_rate: float, days_between_payments: int + principal: float, daily_interest_rate: float, days_between_payments: float ) -> float: """ >>> simple_interest(18000.0, 0.06, 3) @@ -42,7 +42,7 @@ def simple_interest( def compound_interest( principal: float, nominal_annual_interest_rate_percentage: float, - number_of_compounding_periods: int, + number_of_compounding_periods: float, ) -> float: """ >>> compound_interest(10000.0, 0.05, 3) @@ -77,6 +77,43 @@ def compound_interest( ) +def apr_interest( + principal: float, + nominal_annual_percentage_rate: float, + number_of_years: float, +) -> float: + """ + >>> apr_interest(10000.0, 0.05, 3) + 1618.223072263547 + >>> apr_interest(10000.0, 0.05, 1) + 512.6749646744732 + >>> apr_interest(0.5, 0.05, 3) + 0.08091115361317736 + >>> apr_interest(10000.0, 0.06, -4) + Traceback (most recent call last): + ... + ValueError: number_of_years must be > 0 + >>> apr_interest(10000.0, -3.5, 3.0) + Traceback (most recent call last): + ... + ValueError: nominal_annual_percentage_rate must be >= 0 + >>> apr_interest(-5500.0, 0.01, 5) + Traceback (most recent call last): + ... + ValueError: principal must be > 0 + """ + if number_of_years <= 0: + raise ValueError("number_of_years must be > 0") + if nominal_annual_percentage_rate < 0: + raise ValueError("nominal_annual_percentage_rate must be >= 0") + if principal <= 0: + raise ValueError("principal must be > 0") + + return compound_interest( + principal, nominal_annual_percentage_rate / 365, number_of_years * 365 + ) + + if __name__ == "__main__": import doctest diff --git a/financial/present_value.py b/financial/present_value.py new file mode 100644 index 000000000000..f74612b923af --- /dev/null +++ b/financial/present_value.py @@ -0,0 +1,42 @@ +""" +Reference: https://www.investopedia.com/terms/p/presentvalue.asp + +An algorithm that calculates the present value of a stream of yearly cash flows given... +1. The discount rate (as a decimal, not a percent) +2. An array of cash flows, with the index of the cash flow being the associated year + +Note: This algorithm assumes that cash flows are paid at the end of the specified year +""" + + +def present_value(discount_rate: float, cash_flows: list[float]) -> float: + """ + >>> present_value(0.13, [10, 20.70, -293, 297]) + 4.69 + >>> present_value(0.07, [-109129.39, 30923.23, 15098.93, 29734,39]) + -42739.63 + >>> present_value(0.07, [109129.39, 30923.23, 15098.93, 29734,39]) + 175519.15 + >>> present_value(-1, [109129.39, 30923.23, 15098.93, 29734,39]) + Traceback (most recent call last): + ... + ValueError: Discount rate cannot be negative + >>> present_value(0.03, []) + Traceback (most recent call last): + ... + ValueError: Cash flows list cannot be empty + """ + if discount_rate < 0: + raise ValueError("Discount rate cannot be negative") + if not cash_flows: + raise ValueError("Cash flows list cannot be empty") + present_value = sum( + cash_flow / ((1 + discount_rate) ** i) for i, cash_flow in enumerate(cash_flows) + ) + return round(present_value, ndigits=2) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/financial/simple_moving_average.py b/financial/simple_moving_average.py new file mode 100644 index 000000000000..f5ae444fd027 --- /dev/null +++ b/financial/simple_moving_average.py @@ -0,0 +1,69 @@ +""" +The Simple Moving Average (SMA) is a statistical calculation used to analyze data points +by creating a constantly updated average price over a specific time period. +In finance, SMA is often used in time series analysis to smooth out price data +and identify trends. + +Reference: https://en.wikipedia.org/wiki/Moving_average +""" + +from collections.abc import Sequence + + +def simple_moving_average( + data: Sequence[float], window_size: int +) -> list[float | None]: + """ + Calculate the simple moving average (SMA) for some given time series data. + + :param data: A list of numerical data points. + :param window_size: An integer representing the size of the SMA window. + :return: A list of SMA values with the same length as the input data. + + Examples: + >>> sma = simple_moving_average([10, 12, 15, 13, 14, 16, 18, 17, 19, 21], 3) + >>> [round(value, 2) if value is not None else None for value in sma] + [None, None, 12.33, 13.33, 14.0, 14.33, 16.0, 17.0, 18.0, 19.0] + >>> simple_moving_average([10, 12, 15], 5) + [None, None, None] + >>> simple_moving_average([10, 12, 15, 13, 14, 16, 18, 17, 19, 21], 0) + Traceback (most recent call last): + ... + ValueError: Window size must be a positive integer + """ + if window_size < 1: + raise ValueError("Window size must be a positive integer") + + sma: list[float | None] = [] + + for i in range(len(data)): + if i < window_size - 1: + sma.append(None) # SMA not available for early data points + else: + window = data[i - window_size + 1 : i + 1] + sma_value = sum(window) / window_size + sma.append(sma_value) + return sma + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example data (replace with your own time series data) + data = [10, 12, 15, 13, 14, 16, 18, 17, 19, 21] + + # Specify the window size for the SMA + window_size = 3 + + # Calculate the Simple Moving Average + sma_values = simple_moving_average(data, window_size) + + # Print the SMA values + print("Simple Moving Average (SMA) Values:") + for i, value in enumerate(sma_values): + if value is not None: + print(f"Day {i + 1}: {value:.2f}") + else: + print(f"Day {i + 1}: Not enough data for SMA") diff --git a/fractals/__init__.py b/fractals/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/fractals/julia_sets.py b/fractals/julia_sets.py index 482e1eddfecc..bea599d44339 100644 --- a/fractals/julia_sets.py +++ b/fractals/julia_sets.py @@ -25,8 +25,8 @@ from collections.abc import Callable from typing import Any -import numpy -from matplotlib import pyplot +import matplotlib.pyplot as plt +import numpy as np c_cauliflower = 0.25 + 0.0j c_polynomial_1 = -0.4 + 0.6j @@ -37,22 +37,20 @@ nb_pixels = 666 -def eval_exponential(c_parameter: complex, z_values: numpy.ndarray) -> numpy.ndarray: +def eval_exponential(c_parameter: complex, z_values: np.ndarray) -> np.ndarray: """ Evaluate $e^z + c$. - >>> eval_exponential(0, 0) + >>> float(eval_exponential(0, 0)) 1.0 - >>> abs(eval_exponential(1, numpy.pi*1.j)) < 1e-15 + >>> bool(abs(eval_exponential(1, np.pi*1.j)) < 1e-15) True - >>> abs(eval_exponential(1.j, 0)-1-1.j) < 1e-15 + >>> bool(abs(eval_exponential(1.j, 0)-1-1.j) < 1e-15) True """ - return numpy.exp(z_values) + c_parameter + return np.exp(z_values) + c_parameter -def eval_quadratic_polynomial( - c_parameter: complex, z_values: numpy.ndarray -) -> numpy.ndarray: +def eval_quadratic_polynomial(c_parameter: complex, z_values: np.ndarray) -> np.ndarray: """ >>> eval_quadratic_polynomial(0, 2) 4 @@ -66,7 +64,7 @@ def eval_quadratic_polynomial( return z_values * z_values + c_parameter -def prepare_grid(window_size: float, nb_pixels: int) -> numpy.ndarray: +def prepare_grid(window_size: float, nb_pixels: int) -> np.ndarray: """ Create a grid of complex values of size nb_pixels*nb_pixels with real and imaginary parts ranging from -window_size to window_size (inclusive). @@ -77,20 +75,20 @@ def prepare_grid(window_size: float, nb_pixels: int) -> numpy.ndarray: [ 0.-1.j, 0.+0.j, 0.+1.j], [ 1.-1.j, 1.+0.j, 1.+1.j]]) """ - x = numpy.linspace(-window_size, window_size, nb_pixels) + x = np.linspace(-window_size, window_size, nb_pixels) x = x.reshape((nb_pixels, 1)) - y = numpy.linspace(-window_size, window_size, nb_pixels) + y = np.linspace(-window_size, window_size, nb_pixels) y = y.reshape((1, nb_pixels)) return x + 1.0j * y def iterate_function( - eval_function: Callable[[Any, numpy.ndarray], numpy.ndarray], + eval_function: Callable[[Any, np.ndarray], np.ndarray], function_params: Any, nb_iterations: int, - z_0: numpy.ndarray, + z_0: np.ndarray, infinity: float | None = None, -) -> numpy.ndarray: +) -> np.ndarray: """ Iterate the function "eval_function" exactly nb_iterations times. The first argument of the function is a parameter which is contained in @@ -98,22 +96,22 @@ def iterate_function( values to iterate from. This function returns the final iterates. - >>> iterate_function(eval_quadratic_polynomial, 0, 3, numpy.array([0,1,2])).shape + >>> iterate_function(eval_quadratic_polynomial, 0, 3, np.array([0,1,2])).shape (3,) - >>> numpy.round(iterate_function(eval_quadratic_polynomial, + >>> complex(np.round(iterate_function(eval_quadratic_polynomial, ... 0, ... 3, - ... numpy.array([0,1,2]))[0]) + ... np.array([0,1,2]))[0])) 0j - >>> numpy.round(iterate_function(eval_quadratic_polynomial, + >>> complex(np.round(iterate_function(eval_quadratic_polynomial, ... 0, ... 3, - ... numpy.array([0,1,2]))[1]) + ... np.array([0,1,2]))[1])) (1+0j) - >>> numpy.round(iterate_function(eval_quadratic_polynomial, + >>> complex(np.round(iterate_function(eval_quadratic_polynomial, ... 0, ... 3, - ... numpy.array([0,1,2]))[2]) + ... np.array([0,1,2]))[2])) (256+0j) """ @@ -121,8 +119,8 @@ def iterate_function( for _ in range(nb_iterations): z_n = eval_function(function_params, z_n) if infinity is not None: - numpy.nan_to_num(z_n, copy=False, nan=infinity) - z_n[abs(z_n) == numpy.inf] = infinity + np.nan_to_num(z_n, copy=False, nan=infinity) + z_n[abs(z_n) == np.inf] = infinity return z_n @@ -130,21 +128,21 @@ def show_results( function_label: str, function_params: Any, escape_radius: float, - z_final: numpy.ndarray, + z_final: np.ndarray, ) -> None: """ Plots of whether the absolute value of z_final is greater than the value of escape_radius. Adds the function_label and function_params to the title. - >>> show_results('80', 0, 1, numpy.array([[0,1,.5],[.4,2,1.1],[.2,1,1.3]])) + >>> show_results('80', 0, 1, np.array([[0,1,.5],[.4,2,1.1],[.2,1,1.3]])) """ abs_z_final = (abs(z_final)).transpose() abs_z_final[:, :] = abs_z_final[::-1, :] - pyplot.matshow(abs_z_final < escape_radius) - pyplot.title(f"Julia set of ${function_label}$, $c={function_params}$") - pyplot.show() + plt.matshow(abs_z_final < escape_radius) + plt.title(f"Julia set of ${function_label}$, $c={function_params}$") + plt.show() def ignore_overflow_warnings() -> None: diff --git a/fractals/koch_snowflake.py b/fractals/koch_snowflake.py index b0aaa86b11d8..724b78f41a69 100644 --- a/fractals/koch_snowflake.py +++ b/fractals/koch_snowflake.py @@ -20,28 +20,27 @@ - numpy """ - from __future__ import annotations -import matplotlib.pyplot as plt # type: ignore -import numpy +import matplotlib.pyplot as plt +import numpy as np # initial triangle of Koch snowflake -VECTOR_1 = numpy.array([0, 0]) -VECTOR_2 = numpy.array([0.5, 0.8660254]) -VECTOR_3 = numpy.array([1, 0]) +VECTOR_1 = np.array([0, 0]) +VECTOR_2 = np.array([0.5, 0.8660254]) +VECTOR_3 = np.array([1, 0]) INITIAL_VECTORS = [VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_1] # uncomment for simple Koch curve instead of Koch snowflake # INITIAL_VECTORS = [VECTOR_1, VECTOR_3] -def iterate(initial_vectors: list[numpy.ndarray], steps: int) -> list[numpy.ndarray]: +def iterate(initial_vectors: list[np.ndarray], steps: int) -> list[np.ndarray]: """ Go through the number of iterations determined by the argument "steps". Be careful with high values (above 5) since the time to calculate increases exponentially. - >>> iterate([numpy.array([0, 0]), numpy.array([1, 0])], 1) + >>> iterate([np.array([0, 0]), np.array([1, 0])], 1) [array([0, 0]), array([0.33333333, 0. ]), array([0.5 , \ 0.28867513]), array([0.66666667, 0. ]), array([1, 0])] """ @@ -51,13 +50,13 @@ def iterate(initial_vectors: list[numpy.ndarray], steps: int) -> list[numpy.ndar return vectors -def iteration_step(vectors: list[numpy.ndarray]) -> list[numpy.ndarray]: +def iteration_step(vectors: list[np.ndarray]) -> list[np.ndarray]: """ Loops through each pair of adjacent vectors. Each line between two adjacent vectors is divided into 4 segments by adding 3 additional vectors in-between the original two vectors. The vector in the middle is constructed through a 60 degree rotation so it is bent outwards. - >>> iteration_step([numpy.array([0, 0]), numpy.array([1, 0])]) + >>> iteration_step([np.array([0, 0]), np.array([1, 0])]) [array([0, 0]), array([0.33333333, 0. ]), array([0.5 , \ 0.28867513]), array([0.66666667, 0. ]), array([1, 0])] """ @@ -75,22 +74,22 @@ def iteration_step(vectors: list[numpy.ndarray]) -> list[numpy.ndarray]: return new_vectors -def rotate(vector: numpy.ndarray, angle_in_degrees: float) -> numpy.ndarray: +def rotate(vector: np.ndarray, angle_in_degrees: float) -> np.ndarray: """ Standard rotation of a 2D vector with a rotation matrix (see https://en.wikipedia.org/wiki/Rotation_matrix ) - >>> rotate(numpy.array([1, 0]), 60) + >>> rotate(np.array([1, 0]), 60) array([0.5 , 0.8660254]) - >>> rotate(numpy.array([1, 0]), 90) + >>> rotate(np.array([1, 0]), 90) array([6.123234e-17, 1.000000e+00]) """ - theta = numpy.radians(angle_in_degrees) - c, s = numpy.cos(theta), numpy.sin(theta) - rotation_matrix = numpy.array(((c, -s), (s, c))) - return numpy.dot(rotation_matrix, vector) + theta = np.radians(angle_in_degrees) + c, s = np.cos(theta), np.sin(theta) + rotation_matrix = np.array(((c, -s), (s, c))) + return np.dot(rotation_matrix, vector) -def plot(vectors: list[numpy.ndarray]) -> None: +def plot(vectors: list[np.ndarray]) -> None: """ Utility function to plot the vectors using matplotlib.pyplot No doctest was implemented since this function does not have a return value diff --git a/fractals/mandelbrot.py b/fractals/mandelbrot.py index 84dbda997562..359d965a882d 100644 --- a/fractals/mandelbrot.py +++ b/fractals/mandelbrot.py @@ -15,10 +15,9 @@ (see also https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set ) """ - import colorsys -from PIL import Image # type: ignore +from PIL import Image def get_distance(x: float, y: float, max_step: int) -> float: diff --git a/fractals/sierpinski_triangle.py b/fractals/sierpinski_triangle.py index c28ec00b27fe..ceb2001b681d 100644 --- a/fractals/sierpinski_triangle.py +++ b/fractals/sierpinski_triangle.py @@ -22,6 +22,7 @@ This code was written by editing the code from https://www.riannetrujillo.com/blog/python-fractal/ """ + import sys import turtle @@ -82,3 +83,4 @@ def triangle( vertices = [(-175, -125), (0, 175), (175, -125)] # vertices of triangle triangle(vertices[0], vertices[1], vertices[2], int(sys.argv[1])) + turtle.Screen().exitonclick() diff --git a/fractals/vicsek.py b/fractals/vicsek.py new file mode 100644 index 000000000000..290fe95b79b4 --- /dev/null +++ b/fractals/vicsek.py @@ -0,0 +1,76 @@ +"""Authors Bastien Capiaux & Mehdi Oudghiri + +The Vicsek fractal algorithm is a recursive algorithm that creates a +pattern known as the Vicsek fractal or the Vicsek square. +It is based on the concept of self-similarity, where the pattern at each +level of recursion resembles the overall pattern. +The algorithm involves dividing a square into 9 equal smaller squares, +removing the center square, and then repeating this process on the remaining 8 squares. +This results in a pattern that exhibits self-similarity and has a +square-shaped outline with smaller squares within it. + +Source: https://en.wikipedia.org/wiki/Vicsek_fractal +""" + +import turtle + + +def draw_cross(x: float, y: float, length: float): + """ + Draw a cross at the specified position and with the specified length. + """ + turtle.up() + turtle.goto(x - length / 2, y - length / 6) + turtle.down() + turtle.seth(0) + turtle.begin_fill() + for _ in range(4): + turtle.fd(length / 3) + turtle.right(90) + turtle.fd(length / 3) + turtle.left(90) + turtle.fd(length / 3) + turtle.left(90) + turtle.end_fill() + + +def draw_fractal_recursive(x: float, y: float, length: float, depth: float): + """ + Recursively draw the Vicsek fractal at the specified position, with the + specified length and depth. + """ + if depth == 0: + draw_cross(x, y, length) + return + + draw_fractal_recursive(x, y, length / 3, depth - 1) + draw_fractal_recursive(x + length / 3, y, length / 3, depth - 1) + draw_fractal_recursive(x - length / 3, y, length / 3, depth - 1) + draw_fractal_recursive(x, y + length / 3, length / 3, depth - 1) + draw_fractal_recursive(x, y - length / 3, length / 3, depth - 1) + + +def set_color(rgb: str): + turtle.color(rgb) + + +def draw_vicsek_fractal(x: float, y: float, length: float, depth: float, color="blue"): + """ + Draw the Vicsek fractal at the specified position, with the specified + length and depth. + """ + turtle.speed(0) + turtle.hideturtle() + set_color(color) + draw_fractal_recursive(x, y, length, depth) + turtle.Screen().update() + + +def main(): + draw_vicsek_fractal(0, 0, 800, 4) + + turtle.done() + + +if __name__ == "__main__": + main() diff --git a/fuzzy_logic/fuzzy_operations.py b/fuzzy_logic/fuzzy_operations.py index 0786ef8b0c67..c5e4cbde019d 100644 --- a/fuzzy_logic/fuzzy_operations.py +++ b/fuzzy_logic/fuzzy_operations.py @@ -1,103 +1,195 @@ """ -README, Author - Jigyasa Gandhi(mailto:jigsgandhi97@gmail.com) -Requirements: - - scikit-fuzzy - - numpy - - matplotlib -Python: - - 3.5 +By @Shreya123714 + +https://en.wikipedia.org/wiki/Fuzzy_set """ + +from __future__ import annotations + +from dataclasses import dataclass + +import matplotlib.pyplot as plt import numpy as np -import skfuzzy as fuzz + + +@dataclass +class FuzzySet: + """ + A class for representing and manipulating triangular fuzzy sets. + Attributes: + name: The name or label of the fuzzy set. + left_boundary: The left boundary of the fuzzy set. + peak: The peak (central) value of the fuzzy set. + right_boundary: The right boundary of the fuzzy set. + Methods: + membership(x): Calculate the membership value of an input 'x' in the fuzzy set. + union(other): Calculate the union of this fuzzy set with another fuzzy set. + intersection(other): Calculate the intersection of this fuzzy set with another. + complement(): Calculate the complement (negation) of this fuzzy set. + plot(): Plot the membership function of the fuzzy set. + + >>> sheru = FuzzySet("Sheru", 0.4, 1, 0.6) + >>> sheru + FuzzySet(name='Sheru', left_boundary=0.4, peak=1, right_boundary=0.6) + >>> str(sheru) + 'Sheru: [0.4, 1, 0.6]' + + >>> siya = FuzzySet("Siya", 0.5, 1, 0.7) + >>> siya + FuzzySet(name='Siya', left_boundary=0.5, peak=1, right_boundary=0.7) + + # Complement Operation + >>> sheru.complement() + FuzzySet(name='¬Sheru', left_boundary=0.4, peak=0.6, right_boundary=0) + >>> siya.complement() # doctest: +NORMALIZE_WHITESPACE + FuzzySet(name='¬Siya', left_boundary=0.30000000000000004, peak=0.5, + right_boundary=0) + + # Intersection Operation + >>> siya.intersection(sheru) + FuzzySet(name='Siya ∩ Sheru', left_boundary=0.5, peak=0.6, right_boundary=1.0) + + # Membership Operation + >>> sheru.membership(0.5) + 0.16666666666666663 + >>> sheru.membership(0.6) + 0.0 + + # Union Operations + >>> siya.union(sheru) + FuzzySet(name='Siya U Sheru', left_boundary=0.4, peak=0.7, right_boundary=1.0) + """ + + name: str + left_boundary: float + peak: float + right_boundary: float + + def __str__(self) -> str: + """ + >>> FuzzySet("fuzzy_set", 0.1, 0.2, 0.3) + FuzzySet(name='fuzzy_set', left_boundary=0.1, peak=0.2, right_boundary=0.3) + """ + return ( + f"{self.name}: [{self.left_boundary}, {self.peak}, {self.right_boundary}]" + ) + + def complement(self) -> FuzzySet: + """ + Calculate the complement (negation) of this fuzzy set. + Returns: + FuzzySet: A new fuzzy set representing the complement. + + >>> FuzzySet("fuzzy_set", 0.1, 0.2, 0.3).complement() + FuzzySet(name='¬fuzzy_set', left_boundary=0.7, peak=0.9, right_boundary=0.8) + """ + return FuzzySet( + f"¬{self.name}", + 1 - self.right_boundary, + 1 - self.left_boundary, + 1 - self.peak, + ) + + def intersection(self, other) -> FuzzySet: + """ + Calculate the intersection of this fuzzy set + with another fuzzy set. + Args: + other: Another fuzzy set to intersect with. + Returns: + A new fuzzy set representing the intersection. + + >>> FuzzySet("a", 0.1, 0.2, 0.3).intersection(FuzzySet("b", 0.4, 0.5, 0.6)) + FuzzySet(name='a ∩ b', left_boundary=0.4, peak=0.3, right_boundary=0.35) + """ + return FuzzySet( + f"{self.name} ∩ {other.name}", + max(self.left_boundary, other.left_boundary), + min(self.right_boundary, other.right_boundary), + (self.peak + other.peak) / 2, + ) + + def membership(self, x: float) -> float: + """ + Calculate the membership value of an input 'x' in the fuzzy set. + Returns: + The membership value of 'x' in the fuzzy set. + + >>> a = FuzzySet("a", 0.1, 0.2, 0.3) + >>> a.membership(0.09) + 0.0 + >>> a.membership(0.1) + 0.0 + >>> a.membership(0.11) + 0.09999999999999995 + >>> a.membership(0.4) + 0.0 + >>> FuzzySet("A", 0, 0.5, 1).membership(0.1) + 0.2 + >>> FuzzySet("B", 0.2, 0.7, 1).membership(0.6) + 0.8 + """ + if x <= self.left_boundary or x >= self.right_boundary: + return 0.0 + elif self.left_boundary < x <= self.peak: + return (x - self.left_boundary) / (self.peak - self.left_boundary) + elif self.peak < x < self.right_boundary: + return (self.right_boundary - x) / (self.right_boundary - self.peak) + msg = f"Invalid value {x} for fuzzy set {self}" + raise ValueError(msg) + + def union(self, other) -> FuzzySet: + """ + Calculate the union of this fuzzy set with another fuzzy set. + Args: + other (FuzzySet): Another fuzzy set to union with. + Returns: + FuzzySet: A new fuzzy set representing the union. + + >>> FuzzySet("a", 0.1, 0.2, 0.3).union(FuzzySet("b", 0.4, 0.5, 0.6)) + FuzzySet(name='a U b', left_boundary=0.1, peak=0.6, right_boundary=0.35) + """ + return FuzzySet( + f"{self.name} U {other.name}", + min(self.left_boundary, other.left_boundary), + max(self.right_boundary, other.right_boundary), + (self.peak + other.peak) / 2, + ) + + def plot(self): + """ + Plot the membership function of the fuzzy set. + """ + x = np.linspace(0, 1, 1000) + y = [self.membership(xi) for xi in x] + + plt.plot(x, y, label=self.name) + if __name__ == "__main__": - # Create universe of discourse in Python using linspace () - X = np.linspace(start=0, stop=75, num=75, endpoint=True, retstep=False) - - # Create two fuzzy sets by defining any membership function - # (trapmf(), gbellmf(), gaussmf(), etc). - abc1 = [0, 25, 50] - abc2 = [25, 50, 75] - young = fuzz.membership.trimf(X, abc1) - middle_aged = fuzz.membership.trimf(X, abc2) - - # Compute the different operations using inbuilt functions. - one = np.ones(75) - zero = np.zeros((75,)) - # 1. Union = max(µA(x), µB(x)) - union = fuzz.fuzzy_or(X, young, X, middle_aged)[1] - # 2. Intersection = min(µA(x), µB(x)) - intersection = fuzz.fuzzy_and(X, young, X, middle_aged)[1] - # 3. Complement (A) = (1- min(µA(x)) - complement_a = fuzz.fuzzy_not(young) - # 4. Difference (A/B) = min(µA(x),(1- µB(x))) - difference = fuzz.fuzzy_and(X, young, X, fuzz.fuzzy_not(middle_aged)[1])[1] - # 5. Algebraic Sum = [µA(x) + µB(x) – (µA(x) * µB(x))] - alg_sum = young + middle_aged - (young * middle_aged) - # 6. Algebraic Product = (µA(x) * µB(x)) - alg_product = young * middle_aged - # 7. Bounded Sum = min[1,(µA(x), µB(x))] - bdd_sum = fuzz.fuzzy_and(X, one, X, young + middle_aged)[1] - # 8. Bounded difference = min[0,(µA(x), µB(x))] - bdd_difference = fuzz.fuzzy_or(X, zero, X, young - middle_aged)[1] - - # max-min composition - # max-product composition - - # Plot each set A, set B and each operation result using plot() and subplot(). - from matplotlib import pyplot as plt - - plt.figure() - - plt.subplot(4, 3, 1) - plt.plot(X, young) - plt.title("Young") - plt.grid(True) - - plt.subplot(4, 3, 2) - plt.plot(X, middle_aged) - plt.title("Middle aged") - plt.grid(True) - - plt.subplot(4, 3, 3) - plt.plot(X, union) - plt.title("union") - plt.grid(True) - - plt.subplot(4, 3, 4) - plt.plot(X, intersection) - plt.title("intersection") - plt.grid(True) - - plt.subplot(4, 3, 5) - plt.plot(X, complement_a) - plt.title("complement_a") - plt.grid(True) - - plt.subplot(4, 3, 6) - plt.plot(X, difference) - plt.title("difference a/b") - plt.grid(True) - - plt.subplot(4, 3, 7) - plt.plot(X, alg_sum) - plt.title("alg_sum") - plt.grid(True) - - plt.subplot(4, 3, 8) - plt.plot(X, alg_product) - plt.title("alg_product") - plt.grid(True) - - plt.subplot(4, 3, 9) - plt.plot(X, bdd_sum) - plt.title("bdd_sum") - plt.grid(True) - - plt.subplot(4, 3, 10) - plt.plot(X, bdd_difference) - plt.title("bdd_difference") - plt.grid(True) - - plt.subplots_adjust(hspace=0.5) + from doctest import testmod + + testmod() + a = FuzzySet("A", 0, 0.5, 1) + b = FuzzySet("B", 0.2, 0.7, 1) + + a.plot() + b.plot() + + plt.xlabel("x") + plt.ylabel("Membership") + plt.legend() + plt.show() + + union_ab = a.union(b) + intersection_ab = a.intersection(b) + complement_a = a.complement() + + union_ab.plot() + intersection_ab.plot() + complement_a.plot() + + plt.xlabel("x") + plt.ylabel("Membership") + plt.legend() plt.show() diff --git a/fuzzy_logic/fuzzy_operations.py.DISABLED.txt b/fuzzy_logic/fuzzy_operations.py.DISABLED.txt new file mode 100644 index 000000000000..67fd587f4baf --- /dev/null +++ b/fuzzy_logic/fuzzy_operations.py.DISABLED.txt @@ -0,0 +1,103 @@ +""" +README, Author - Jigyasa Gandhi(mailto:jigsgandhi97@gmail.com) +Requirements: + - scikit-fuzzy + - numpy + - matplotlib +Python: + - 3.5 +""" +import numpy as np +import skfuzzy as fuzz + +if __name__ == "__main__": + # Create universe of discourse in Python using linspace () + X = np.linspace(start=0, stop=75, num=75, endpoint=True, retstep=False) + + # Create two fuzzy sets by defining any membership function + # (trapmf(), gbellmf(), gaussmf(), etc). + abc1 = [0, 25, 50] + abc2 = [25, 50, 75] + young = fuzz.membership.trimf(X, abc1) + middle_aged = fuzz.membership.trimf(X, abc2) + + # Compute the different operations using inbuilt functions. + one = np.ones(75) + zero = np.zeros((75,)) + # 1. Union = max(µA(x), µB(x)) + union = fuzz.fuzzy_or(X, young, X, middle_aged)[1] + # 2. Intersection = min(µA(x), µB(x)) + intersection = fuzz.fuzzy_and(X, young, X, middle_aged)[1] + # 3. Complement (A) = (1 - min(µA(x))) + complement_a = fuzz.fuzzy_not(young) + # 4. Difference (A/B) = min(µA(x),(1- µB(x))) + difference = fuzz.fuzzy_and(X, young, X, fuzz.fuzzy_not(middle_aged)[1])[1] + # 5. Algebraic Sum = [µA(x) + µB(x) – (µA(x) * µB(x))] + alg_sum = young + middle_aged - (young * middle_aged) + # 6. Algebraic Product = (µA(x) * µB(x)) + alg_product = young * middle_aged + # 7. Bounded Sum = min[1,(µA(x), µB(x))] + bdd_sum = fuzz.fuzzy_and(X, one, X, young + middle_aged)[1] + # 8. Bounded difference = min[0,(µA(x), µB(x))] + bdd_difference = fuzz.fuzzy_or(X, zero, X, young - middle_aged)[1] + + # max-min composition + # max-product composition + + # Plot each set A, set B and each operation result using plot() and subplot(). + from matplotlib import pyplot as plt + + plt.figure() + + plt.subplot(4, 3, 1) + plt.plot(X, young) + plt.title("Young") + plt.grid(True) + + plt.subplot(4, 3, 2) + plt.plot(X, middle_aged) + plt.title("Middle aged") + plt.grid(True) + + plt.subplot(4, 3, 3) + plt.plot(X, union) + plt.title("union") + plt.grid(True) + + plt.subplot(4, 3, 4) + plt.plot(X, intersection) + plt.title("intersection") + plt.grid(True) + + plt.subplot(4, 3, 5) + plt.plot(X, complement_a) + plt.title("complement_a") + plt.grid(True) + + plt.subplot(4, 3, 6) + plt.plot(X, difference) + plt.title("difference a/b") + plt.grid(True) + + plt.subplot(4, 3, 7) + plt.plot(X, alg_sum) + plt.title("alg_sum") + plt.grid(True) + + plt.subplot(4, 3, 8) + plt.plot(X, alg_product) + plt.title("alg_product") + plt.grid(True) + + plt.subplot(4, 3, 9) + plt.plot(X, bdd_sum) + plt.title("bdd_sum") + plt.grid(True) + + plt.subplot(4, 3, 10) + plt.plot(X, bdd_difference) + plt.title("bdd_difference") + plt.grid(True) + + plt.subplots_adjust(hspace=0.5) + plt.show() diff --git a/genetic_algorithm/basic_string.py b/genetic_algorithm/basic_string.py index 45b8be651f6e..a906ce85a779 100644 --- a/genetic_algorithm/basic_string.py +++ b/genetic_algorithm/basic_string.py @@ -21,6 +21,79 @@ random.seed(random.randint(0, 1000)) +def evaluate(item: str, main_target: str) -> tuple[str, float]: + """ + Evaluate how similar the item is with the target by just + counting each char in the right position + >>> evaluate("Helxo Worlx", "Hello World") + ('Helxo Worlx', 9.0) + """ + score = len([g for position, g in enumerate(item) if g == main_target[position]]) + return (item, float(score)) + + +def crossover(parent_1: str, parent_2: str) -> tuple[str, str]: + """ + Slice and combine two strings at a random point. + >>> random.seed(42) + >>> crossover("123456", "abcdef") + ('12345f', 'abcde6') + """ + random_slice = random.randint(0, len(parent_1) - 1) + child_1 = parent_1[:random_slice] + parent_2[random_slice:] + child_2 = parent_2[:random_slice] + parent_1[random_slice:] + return (child_1, child_2) + + +def mutate(child: str, genes: list[str]) -> str: + """ + Mutate a random gene of a child with another one from the list. + >>> random.seed(123) + >>> mutate("123456", list("ABCDEF")) + '12345A' + """ + child_list = list(child) + if random.uniform(0, 1) < MUTATION_PROBABILITY: + child_list[random.randint(0, len(child)) - 1] = random.choice(genes) + return "".join(child_list) + + +# Select, crossover and mutate a new population. +def select( + parent_1: tuple[str, float], + population_score: list[tuple[str, float]], + genes: list[str], +) -> list[str]: + """ + Select the second parent and generate new population + + >>> random.seed(42) + >>> parent_1 = ("123456", 8.0) + >>> population_score = [("abcdef", 4.0), ("ghijkl", 5.0), ("mnopqr", 7.0)] + >>> genes = list("ABCDEF") + >>> child_n = int(min(parent_1[1] + 1, 10)) + >>> population = [] + >>> for _ in range(child_n): + ... parent_2 = population_score[random.randrange(len(population_score))][0] + ... child_1, child_2 = crossover(parent_1[0], parent_2) + ... population.extend((mutate(child_1, genes), mutate(child_2, genes))) + >>> len(population) == (int(parent_1[1]) + 1) * 2 + True + """ + pop = [] + # Generate more children proportionally to the fitness score. + child_n = int(parent_1[1] * 100) + 1 + child_n = 10 if child_n >= 10 else child_n + for _ in range(child_n): + parent_2 = population_score[random.randint(0, N_SELECTED)][0] + + child_1, child_2 = crossover(parent_1[0], parent_2) + # Append new string to the population list. + pop.append(mutate(child_1, genes)) + pop.append(mutate(child_2, genes)) + return pop + + def basic(target: str, genes: list[str], debug: bool = True) -> tuple[int, int, str]: """ Verify that the target contains no genes besides the ones inside genes variable. @@ -48,13 +121,13 @@ def basic(target: str, genes: list[str], debug: bool = True) -> tuple[int, int, # Verify if N_POPULATION is bigger than N_SELECTED if N_POPULATION < N_SELECTED: - raise ValueError(f"{N_POPULATION} must be bigger than {N_SELECTED}") + msg = f"{N_POPULATION} must be bigger than {N_SELECTED}" + raise ValueError(msg) # Verify that the target contains no genes besides the ones inside genes variable. not_in_genes_list = sorted({c for c in target if c not in genes}) if not_in_genes_list: - raise ValueError( - f"{not_in_genes_list} is not in genes list, evolution cannot converge" - ) + msg = f"{not_in_genes_list} is not in genes list, evolution cannot converge" + raise ValueError(msg) # Generate random starting population. population = [] @@ -70,17 +143,6 @@ def basic(target: str, genes: list[str], debug: bool = True) -> tuple[int, int, total_population += len(population) # Random population created. Now it's time to evaluate. - def evaluate(item: str, main_target: str = target) -> tuple[str, float]: - """ - Evaluate how similar the item is with the target by just - counting each char in the right position - >>> evaluate("Helxo Worlx", Hello World) - ["Helxo Worlx", 9] - """ - score = len( - [g for position, g in enumerate(item) if g == main_target[position]] - ) - return (item, float(score)) # Adding a bit of concurrency can make everything faster, # @@ -94,7 +156,7 @@ def evaluate(item: str, main_target: str = target) -> tuple[str, float]: # # but with a simple algorithm like this, it will probably be slower. # We just need to call evaluate for every item inside the population. - population_score = [evaluate(item) for item in population] + population_score = [evaluate(item, target) for item in population] # Check if there is a matching evolution. population_score = sorted(population_score, key=lambda x: x[1], reverse=True) @@ -121,41 +183,9 @@ def evaluate(item: str, main_target: str = target) -> tuple[str, float]: (item, score / len(target)) for item, score in population_score ] - # Select, crossover and mutate a new population. - def select(parent_1: tuple[str, float]) -> list[str]: - """Select the second parent and generate new population""" - pop = [] - # Generate more children proportionally to the fitness score. - child_n = int(parent_1[1] * 100) + 1 - child_n = 10 if child_n >= 10 else child_n - for _ in range(child_n): - parent_2 = population_score[ # noqa: B023 - random.randint(0, N_SELECTED) - ][0] - - child_1, child_2 = crossover(parent_1[0], parent_2) - # Append new string to the population list. - pop.append(mutate(child_1)) - pop.append(mutate(child_2)) - return pop - - def crossover(parent_1: str, parent_2: str) -> tuple[str, str]: - """Slice and combine two string at a random point.""" - random_slice = random.randint(0, len(parent_1) - 1) - child_1 = parent_1[:random_slice] + parent_2[random_slice:] - child_2 = parent_2[:random_slice] + parent_1[random_slice:] - return (child_1, child_2) - - def mutate(child: str) -> str: - """Mutate a random gene of a child with another one from the list.""" - child_list = list(child) - if random.uniform(0, 1) < MUTATION_PROBABILITY: - child_list[random.randint(0, len(child)) - 1] = random.choice(genes) - return "".join(child_list) - # This is selection for i in range(N_SELECTED): - population.extend(select(population_score[int(i)])) + population.extend(select(population_score[int(i)], population_score, genes)) # Check if the population has already reached the maximum value and if so, # break the cycle. If this check is disabled, the algorithm will take # forever to compute large strings, but will also calculate small strings in diff --git a/geodesy/haversine_distance.py b/geodesy/haversine_distance.py index 93e625770f9d..39cd250af965 100644 --- a/geodesy/haversine_distance.py +++ b/geodesy/haversine_distance.py @@ -21,10 +21,11 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl computation like Haversine can be handy for shorter range distances. Args: - lat1, lon1: latitude and longitude of coordinate 1 - lat2, lon2: latitude and longitude of coordinate 2 + * `lat1`, `lon1`: latitude and longitude of coordinate 1 + * `lat2`, `lon2`: latitude and longitude of coordinate 2 Returns: geographical distance between two points in metres + >>> from collections import namedtuple >>> point_2d = namedtuple("point_2d", "lat lon") >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227) diff --git a/geometry/__init__.py b/geometry/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/geometry/geometry.py b/geometry/geometry.py new file mode 100644 index 000000000000..a0be8eb3befc --- /dev/null +++ b/geometry/geometry.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from types import NoneType +from typing import Self + +# Building block classes + + +@dataclass +class Angle: + """ + An Angle in degrees (unit of measurement) + + >>> Angle() + Angle(degrees=90) + >>> Angle(45.5) + Angle(degrees=45.5) + >>> Angle(-1) + Traceback (most recent call last): + ... + TypeError: degrees must be a numeric value between 0 and 360. + >>> Angle(361) + Traceback (most recent call last): + ... + TypeError: degrees must be a numeric value between 0 and 360. + """ + + degrees: float = 90 + + def __post_init__(self) -> None: + if not isinstance(self.degrees, (int, float)) or not 0 <= self.degrees <= 360: + raise TypeError("degrees must be a numeric value between 0 and 360.") + + +@dataclass +class Side: + """ + A side of a two dimensional Shape such as Polygon, etc. + adjacent_sides: a list of sides which are adjacent to the current side + angle: the angle in degrees between each adjacent side + length: the length of the current side in meters + + >>> Side(5) + Side(length=5, angle=Angle(degrees=90), next_side=None) + >>> Side(5, Angle(45.6)) + Side(length=5, angle=Angle(degrees=45.6), next_side=None) + >>> Side(5, Angle(45.6), Side(1, Angle(2))) # doctest: +ELLIPSIS + Side(length=5, angle=Angle(degrees=45.6), next_side=Side(length=1, angle=Angle(d... + >>> Side(-1) + Traceback (most recent call last): + ... + TypeError: length must be a positive numeric value. + >>> Side(5, None) + Traceback (most recent call last): + ... + TypeError: angle must be an Angle object. + >>> Side(5, Angle(90), "Invalid next_side") + Traceback (most recent call last): + ... + TypeError: next_side must be a Side or None. + """ + + length: float + angle: Angle = field(default_factory=Angle) + next_side: Side | None = None + + def __post_init__(self) -> None: + if not isinstance(self.length, (int, float)) or self.length <= 0: + raise TypeError("length must be a positive numeric value.") + if not isinstance(self.angle, Angle): + raise TypeError("angle must be an Angle object.") + if not isinstance(self.next_side, (Side, NoneType)): + raise TypeError("next_side must be a Side or None.") + + +@dataclass +class Ellipse: + """ + A geometric Ellipse on a 2D surface + + >>> Ellipse(5, 10) + Ellipse(major_radius=5, minor_radius=10) + >>> Ellipse(5, 10) is Ellipse(5, 10) + False + >>> Ellipse(5, 10) == Ellipse(5, 10) + True + """ + + major_radius: float + minor_radius: float + + @property + def area(self) -> float: + """ + >>> Ellipse(5, 10).area + 157.07963267948966 + """ + return math.pi * self.major_radius * self.minor_radius + + @property + def perimeter(self) -> float: + """ + >>> Ellipse(5, 10).perimeter + 47.12388980384689 + """ + return math.pi * (self.major_radius + self.minor_radius) + + +class Circle(Ellipse): + """ + A geometric Circle on a 2D surface + + >>> Circle(5) + Circle(radius=5) + >>> Circle(5) is Circle(5) + False + >>> Circle(5) == Circle(5) + True + >>> Circle(5).area + 78.53981633974483 + >>> Circle(5).perimeter + 31.41592653589793 + """ + + def __init__(self, radius: float) -> None: + super().__init__(radius, radius) + self.radius = radius + + def __repr__(self) -> str: + return f"Circle(radius={self.radius})" + + @property + def diameter(self) -> float: + """ + >>> Circle(5).diameter + 10 + """ + return self.radius * 2 + + def max_parts(self, num_cuts: float) -> float: + """ + Return the maximum number of parts that circle can be divided into if cut + 'num_cuts' times. + + >>> circle = Circle(5) + >>> circle.max_parts(0) + 1.0 + >>> circle.max_parts(7) + 29.0 + >>> circle.max_parts(54) + 1486.0 + >>> circle.max_parts(22.5) + 265.375 + >>> circle.max_parts(-222) + Traceback (most recent call last): + ... + TypeError: num_cuts must be a positive numeric value. + >>> circle.max_parts("-222") + Traceback (most recent call last): + ... + TypeError: num_cuts must be a positive numeric value. + """ + if not isinstance(num_cuts, (int, float)) or num_cuts < 0: + raise TypeError("num_cuts must be a positive numeric value.") + return (num_cuts + 2 + num_cuts**2) * 0.5 + + +@dataclass +class Polygon: + """ + An abstract class which represents Polygon on a 2D surface. + + >>> Polygon() + Polygon(sides=[]) + >>> polygon = Polygon() + >>> polygon.add_side(Side(5)).get_side(0) + Side(length=5, angle=Angle(degrees=90), next_side=None) + >>> polygon.get_side(1) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> polygon.set_side(0, Side(10)).get_side(0) + Side(length=10, angle=Angle(degrees=90), next_side=None) + >>> polygon.set_side(1, Side(10)) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + """ + + sides: list[Side] = field(default_factory=list) + + def add_side(self, side: Side) -> Self: + """ + >>> Polygon().add_side(Side(5)) + Polygon(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None)]) + """ + self.sides.append(side) + return self + + def get_side(self, index: int) -> Side: + """ + >>> Polygon().get_side(0) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> Polygon().add_side(Side(5)).get_side(-1) + Side(length=5, angle=Angle(degrees=90), next_side=None) + """ + return self.sides[index] + + def set_side(self, index: int, side: Side) -> Self: + """ + >>> Polygon().set_side(0, Side(5)) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + >>> Polygon().add_side(Side(5)).set_side(0, Side(10)) + Polygon(sides=[Side(length=10, angle=Angle(degrees=90), next_side=None)]) + """ + self.sides[index] = side + return self + + +class Rectangle(Polygon): + """ + A geometric rectangle on a 2D surface. + + >>> rectangle_one = Rectangle(5, 10) + >>> rectangle_one.perimeter() + 30 + >>> rectangle_one.area() + 50 + >>> Rectangle(-5, 10) + Traceback (most recent call last): + ... + TypeError: length must be a positive numeric value. + """ + + def __init__(self, short_side_length: float, long_side_length: float) -> None: + super().__init__() + self.short_side_length = short_side_length + self.long_side_length = long_side_length + self.post_init() + + def post_init(self) -> None: + """ + >>> Rectangle(5, 10) # doctest: +NORMALIZE_WHITESPACE + Rectangle(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None), + Side(length=10, angle=Angle(degrees=90), next_side=None)]) + """ + self.short_side = Side(self.short_side_length) + self.long_side = Side(self.long_side_length) + super().add_side(self.short_side) + super().add_side(self.long_side) + + def perimeter(self) -> float: + return (self.short_side.length + self.long_side.length) * 2 + + def area(self) -> float: + return self.short_side.length * self.long_side.length + + +@dataclass +class Square(Rectangle): + """ + a structure which represents a + geometrical square on a 2D surface + >>> square_one = Square(5) + >>> square_one.perimeter() + 20 + >>> square_one.area() + 25 + """ + + def __init__(self, side_length: float) -> None: + super().__init__(side_length, side_length) + + def perimeter(self) -> float: + return super().perimeter() + + def area(self) -> float: + return super().area() + + +if __name__ == "__main__": + __import__("doctest").testmod() diff --git a/graphics/bezier_curve.py b/graphics/bezier_curve.py index 7c22329ad8b4..6c7dcd4f06e7 100644 --- a/graphics/bezier_curve.py +++ b/graphics/bezier_curve.py @@ -2,7 +2,7 @@ # https://www.tutorialspoint.com/computer_graphics/computer_graphics_curves.htm from __future__ import annotations -from scipy.special import comb # type: ignore +from scipy.special import comb class BezierCurve: @@ -30,9 +30,9 @@ def basis_function(self, t: float) -> list[float]: returns the x, y values of basis function at time t >>> curve = BezierCurve([(1,1), (1,2)]) - >>> curve.basis_function(0) + >>> [float(x) for x in curve.basis_function(0)] [1.0, 0.0] - >>> curve.basis_function(1) + >>> [float(x) for x in curve.basis_function(1)] [0.0, 1.0] """ assert 0 <= t <= 1, "Time t must be between 0 and 1." @@ -55,9 +55,9 @@ def bezier_curve_function(self, t: float) -> tuple[float, float]: The last point in the curve is when t = 1. >>> curve = BezierCurve([(1,1), (1,2)]) - >>> curve.bezier_curve_function(0) + >>> tuple(float(x) for x in curve.bezier_curve_function(0)) (1.0, 1.0) - >>> curve.bezier_curve_function(1) + >>> tuple(float(x) for x in curve.bezier_curve_function(1)) (1.0, 2.0) """ @@ -78,7 +78,7 @@ def plot_curve(self, step_size: float = 0.01): step_size: defines the step(s) at which to evaluate the Bezier curve. The smaller the step size, the finer the curve produced. """ - from matplotlib import pyplot as plt # type: ignore + from matplotlib import pyplot as plt to_plot_x: list[float] = [] # x coordinates of points to plot to_plot_y: list[float] = [] # y coordinates of points to plot diff --git a/graphics/butterfly_pattern.py b/graphics/butterfly_pattern.py new file mode 100644 index 000000000000..7913b03a7e95 --- /dev/null +++ b/graphics/butterfly_pattern.py @@ -0,0 +1,46 @@ +def butterfly_pattern(n: int) -> str: + """ + Creates a butterfly pattern of size n and returns it as a string. + + >>> print(butterfly_pattern(3)) + * * + ** ** + ***** + ** ** + * * + >>> print(butterfly_pattern(5)) + * * + ** ** + *** *** + **** **** + ********* + **** **** + *** *** + ** ** + * * + """ + result = [] + + # Upper part + for i in range(1, n): + left_stars = "*" * i + spaces = " " * (2 * (n - i) - 1) + right_stars = "*" * i + result.append(left_stars + spaces + right_stars) + + # Middle part + result.append("*" * (2 * n - 1)) + + # Lower part + for i in range(n - 1, 0, -1): + left_stars = "*" * i + spaces = " " * (2 * (n - i) - 1) + right_stars = "*" * i + result.append(left_stars + spaces + right_stars) + + return "\n".join(result) + + +if __name__ == "__main__": + n = int(input("Enter the size of the butterfly pattern: ")) + print(butterfly_pattern(n)) diff --git a/graphics/digital_differential_analyzer_line.py b/graphics/digital_differential_analyzer_line.py new file mode 100644 index 000000000000..a51cb0b8dc37 --- /dev/null +++ b/graphics/digital_differential_analyzer_line.py @@ -0,0 +1,52 @@ +import matplotlib.pyplot as plt + + +def digital_differential_analyzer_line( + p1: tuple[int, int], p2: tuple[int, int] +) -> list[tuple[int, int]]: + """ + Draws a line between two points using the DDA algorithm. + + Args: + - p1: Coordinates of the starting point. + - p2: Coordinates of the ending point. + Returns: + - List of coordinate points that form the line. + + >>> digital_differential_analyzer_line((1, 1), (4, 4)) + [(2, 2), (3, 3), (4, 4)] + """ + x1, y1 = p1 + x2, y2 = p2 + dx = x2 - x1 + dy = y2 - y1 + steps = max(abs(dx), abs(dy)) + x_increment = dx / float(steps) + y_increment = dy / float(steps) + coordinates = [] + x: float = x1 + y: float = y1 + for _ in range(steps): + x += x_increment + y += y_increment + coordinates.append((int(round(x)), int(round(y)))) + return coordinates + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + x1 = int(input("Enter the x-coordinate of the starting point: ")) + y1 = int(input("Enter the y-coordinate of the starting point: ")) + x2 = int(input("Enter the x-coordinate of the ending point: ")) + y2 = int(input("Enter the y-coordinate of the ending point: ")) + coordinates = digital_differential_analyzer_line((x1, y1), (x2, y2)) + x_points, y_points = zip(*coordinates) + plt.plot(x_points, y_points, marker="o") + plt.title("Digital Differential Analyzer Line Drawing Algorithm") + plt.xlabel("X-axis") + plt.ylabel("Y-axis") + plt.grid() + plt.show() diff --git a/graphics/vector3_for_2d_rendering.py b/graphics/vector3_for_2d_rendering.py index dfa22262a8d8..a332206e67b6 100644 --- a/graphics/vector3_for_2d_rendering.py +++ b/graphics/vector3_for_2d_rendering.py @@ -28,9 +28,8 @@ def convert_to_2d( TypeError: Input values must either be float or int: ['1', 2, 3, 10, 10] """ if not all(isinstance(val, (float, int)) for val in locals().values()): - raise TypeError( - "Input values must either be float or int: " f"{list(locals().values())}" - ) + msg = f"Input values must either be float or int: {list(locals().values())}" + raise TypeError(msg) projected_x = ((x * distance) / (z + distance)) * scale projected_y = ((y * distance) / (z + distance)) * scale return projected_x, projected_y @@ -71,10 +70,11 @@ def rotate( input_variables = locals() del input_variables["axis"] if not all(isinstance(val, (float, int)) for val in input_variables.values()): - raise TypeError( + msg = ( "Input values except axis must either be float or int: " f"{list(input_variables.values())}" ) + raise TypeError(msg) angle = (angle % 360) / 450 * 180 / math.pi if axis == "z": new_x = x * math.cos(angle) - y * math.sin(angle) diff --git a/graphs/a_star.py b/graphs/a_star.py index e8735179eab9..1d7063ccc55a 100644 --- a/graphs/a_star.py +++ b/graphs/a_star.py @@ -16,6 +16,31 @@ def search( cost: int, heuristic: list[list[int]], ) -> tuple[list[list[int]], list[list[int]]]: + """ + Search for a path on a grid avoiding obstacles. + >>> grid = [[0, 1, 0, 0, 0, 0], + ... [0, 1, 0, 0, 0, 0], + ... [0, 1, 0, 0, 0, 0], + ... [0, 1, 0, 0, 1, 0], + ... [0, 0, 0, 0, 1, 0]] + >>> init = [0, 0] + >>> goal = [len(grid) - 1, len(grid[0]) - 1] + >>> cost = 1 + >>> heuristic = [[0] * len(grid[0]) for _ in range(len(grid))] + >>> heuristic = [[0 for row in range(len(grid[0]))] for col in range(len(grid))] + >>> for i in range(len(grid)): + ... for j in range(len(grid[0])): + ... heuristic[i][j] = abs(i - goal[0]) + abs(j - goal[1]) + ... if grid[i][j] == 1: + ... heuristic[i][j] = 99 + >>> path, action = search(grid, init, goal, cost, heuristic) + >>> path # doctest: +NORMALIZE_WHITESPACE + [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [3, 3], + [2, 3], [2, 4], [2, 5], [3, 5], [4, 5]] + >>> action # doctest: +NORMALIZE_WHITESPACE + [[0, 0, 0, 0, 0, 0], [2, 0, 0, 0, 0, 0], [2, 0, 0, 0, 3, 3], + [2, 0, 0, 0, 0, 2], [2, 3, 3, 3, 0, 2]] + """ closed = [ [0 for col in range(len(grid[0]))] for row in range(len(grid)) ] # the reference grid @@ -50,13 +75,19 @@ def search( for i in range(len(DIRECTIONS)): # to try out different valid actions x2 = x + DIRECTIONS[i][0] y2 = y + DIRECTIONS[i][1] - if x2 >= 0 and x2 < len(grid) and y2 >= 0 and y2 < len(grid[0]): - if closed[x2][y2] == 0 and grid[x2][y2] == 0: - g2 = g + cost - f2 = g2 + heuristic[x2][y2] - cell.append([f2, g2, x2, y2]) - closed[x2][y2] = 1 - action[x2][y2] = i + if ( + x2 >= 0 + and x2 < len(grid) + and y2 >= 0 + and y2 < len(grid[0]) + and closed[x2][y2] == 0 + and grid[x2][y2] == 0 + ): + g2 = g + cost + f2 = g2 + heuristic[x2][y2] + cell.append([f2, g2, x2, y2]) + closed[x2][y2] = 1 + action[x2][y2] = i invpath = [] x = goal[0] y = goal[1] diff --git a/graphs/ant_colony_optimization_algorithms.py b/graphs/ant_colony_optimization_algorithms.py new file mode 100644 index 000000000000..753f4c0962c8 --- /dev/null +++ b/graphs/ant_colony_optimization_algorithms.py @@ -0,0 +1,224 @@ +""" +Use an ant colony optimization algorithm to solve the travelling salesman problem (TSP) +which asks the following question: +"Given a list of cities and the distances between each pair of cities, what is the + shortest possible route that visits each city exactly once and returns to the origin + city?" + +https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms +https://en.wikipedia.org/wiki/Travelling_salesman_problem + +Author: Clark +""" + +import copy +import random + +cities = { + 0: [0, 0], + 1: [0, 5], + 2: [3, 8], + 3: [8, 10], + 4: [12, 8], + 5: [12, 4], + 6: [8, 0], + 7: [6, 2], +} + + +def main( + cities: dict[int, list[int]], + ants_num: int, + iterations_num: int, + pheromone_evaporation: float, + alpha: float, + beta: float, + q: float, # Pheromone system parameters Q, which is a constant +) -> tuple[list[int], float]: + """ + Ant colony algorithm main function + >>> main(cities=cities, ants_num=10, iterations_num=20, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + ([0, 1, 2, 3, 4, 5, 6, 7, 0], 37.909778143828696) + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + ([0, 1, 0], 5.656854249492381) + >>> main(cities={0: [0, 0], 1: [2, 2], 4: [4, 4]}, ants_num=5, iterations_num=5, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> main(cities={}, ants_num=5, iterations_num=5, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + Traceback (most recent call last): + ... + StopIteration + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=0, iterations_num=5, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + ([], inf) + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=0, + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) + ([], inf) + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, + ... pheromone_evaporation=1, alpha=1.0, beta=5.0, q=10) + ([0, 1, 0], 5.656854249492381) + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, + ... pheromone_evaporation=0, alpha=1.0, beta=5.0, q=10) + ([0, 1, 0], 5.656854249492381) + """ + # Initialize the pheromone matrix + cities_num = len(cities) + pheromone = [[1.0] * cities_num] * cities_num + + best_path: list[int] = [] + best_distance = float("inf") + for _ in range(iterations_num): + ants_route = [] + for _ in range(ants_num): + unvisited_cities = copy.deepcopy(cities) + current_city = {next(iter(cities.keys())): next(iter(cities.values()))} + del unvisited_cities[next(iter(current_city.keys()))] + ant_route = [next(iter(current_city.keys()))] + while unvisited_cities: + current_city, unvisited_cities = city_select( + pheromone, current_city, unvisited_cities, alpha, beta + ) + ant_route.append(next(iter(current_city.keys()))) + ant_route.append(0) + ants_route.append(ant_route) + + pheromone, best_path, best_distance = pheromone_update( + pheromone, + cities, + pheromone_evaporation, + ants_route, + q, + best_path, + best_distance, + ) + return best_path, best_distance + + +def distance(city1: list[int], city2: list[int]) -> float: + """ + Calculate the distance between two coordinate points + >>> distance([0, 0], [3, 4] ) + 5.0 + >>> distance([0, 0], [-3, 4] ) + 5.0 + >>> distance([0, 0], [-3, -4] ) + 5.0 + """ + return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5 + + +def pheromone_update( + pheromone: list[list[float]], + cities: dict[int, list[int]], + pheromone_evaporation: float, + ants_route: list[list[int]], + q: float, # Pheromone system parameters Q, which is a constant + best_path: list[int], + best_distance: float, +) -> tuple[list[list[float]], list[int], float]: + """ + Update pheromones on the route and update the best route + >>> + >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]], + ... cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7, + ... ants_route=[[0, 1, 0]], q=10, best_path=[], + ... best_distance=float("inf")) + ([[0.7, 4.235533905932737], [4.235533905932737, 0.7]], [0, 1, 0], 5.656854249492381) + >>> pheromone_update(pheromone=[], + ... cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7, + ... ants_route=[[0, 1, 0]], q=10, best_path=[], + ... best_distance=float("inf")) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]], + ... cities={}, pheromone_evaporation=0.7, + ... ants_route=[[0, 1, 0]], q=10, best_path=[], + ... best_distance=float("inf")) + Traceback (most recent call last): + ... + KeyError: 0 + """ + for a in range(len(cities)): # Update the volatilization of pheromone on all routes + for b in range(len(cities)): + pheromone[a][b] *= pheromone_evaporation + for ant_route in ants_route: + total_distance = 0.0 + for i in range(len(ant_route) - 1): # Calculate total distance + total_distance += distance(cities[ant_route[i]], cities[ant_route[i + 1]]) + delta_pheromone = q / total_distance + for i in range(len(ant_route) - 1): # Update pheromones + pheromone[ant_route[i]][ant_route[i + 1]] += delta_pheromone + pheromone[ant_route[i + 1]][ant_route[i]] = pheromone[ant_route[i]][ + ant_route[i + 1] + ] + + if total_distance < best_distance: + best_path = ant_route + best_distance = total_distance + + return pheromone, best_path, best_distance + + +def city_select( + pheromone: list[list[float]], + current_city: dict[int, list[int]], + unvisited_cities: dict[int, list[int]], + alpha: float, + beta: float, +) -> tuple[dict[int, list[int]], dict[int, list[int]]]: + """ + Choose the next city for ants + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]}, + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) + ({1: [2, 2]}, {}) + >>> city_select(pheromone=[], current_city={0: [0,0]}, + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={}, + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) + Traceback (most recent call last): + ... + StopIteration + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]}, + ... unvisited_cities={}, alpha=1.0, beta=5.0) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + probabilities = [] + for city, value in unvisited_cities.items(): + city_distance = distance(value, next(iter(current_city.values()))) + probability = (pheromone[city][next(iter(current_city.keys()))] ** alpha) * ( + (1 / city_distance) ** beta + ) + probabilities.append(probability) + + chosen_city_i = random.choices( + list(unvisited_cities.keys()), weights=probabilities + )[0] + chosen_city = {chosen_city_i: unvisited_cities[chosen_city_i]} + del unvisited_cities[next(iter(chosen_city.keys()))] + return chosen_city, unvisited_cities + + +if __name__ == "__main__": + best_path, best_distance = main( + cities=cities, + ants_num=10, + iterations_num=20, + pheromone_evaporation=0.7, + alpha=1.0, + beta=5.0, + q=10, + ) + + print(f"{best_path = }") + print(f"{best_distance = }") diff --git a/graphs/articulation_points.py b/graphs/articulation_points.py index d28045282425..0bf16e55bc04 100644 --- a/graphs/articulation_points.py +++ b/graphs/articulation_points.py @@ -1,6 +1,6 @@ # Finding Articulation Points in Undirected Graph -def compute_ap(l): # noqa: E741 - n = len(l) +def compute_ap(graph): + n = len(graph) out_edge_count = 0 low = [0] * n visited = [False] * n @@ -12,7 +12,7 @@ def dfs(root, at, parent, out_edge_count): visited[at] = True low[at] = at - for to in l[at]: + for to in graph[at]: if to == parent: pass elif not visited[to]: @@ -41,7 +41,7 @@ def dfs(root, at, parent, out_edge_count): # Adjacency list of graph -data = { +graph = { 0: [1, 2], 1: [0, 2], 2: [0, 1, 3, 5], @@ -52,4 +52,4 @@ def dfs(root, at, parent, out_edge_count): 7: [6, 8], 8: [5, 7], } -compute_ap(data) +compute_ap(graph) diff --git a/graphs/basic_graphs.py b/graphs/basic_graphs.py index 298a97bf0e17..286e9b195796 100644 --- a/graphs/basic_graphs.py +++ b/graphs/basic_graphs.py @@ -77,6 +77,14 @@ def initialize_weighted_undirected_graph( def dfs(g, s): + """ + >>> dfs({1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}, 1) + 1 + 2 + 4 + 5 + 3 + """ vis, _s = {s}, [s] print(s) while _s: @@ -104,6 +112,17 @@ def dfs(g, s): def bfs(g, s): + """ + >>> bfs({1: [2, 3], 2: [4, 5], 3: [6, 7], 4: [], 5: [8], 6: [], 7: [], 8: []}, 1) + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + """ vis, q = {s}, deque([s]) print(s) while q: @@ -128,24 +147,36 @@ def bfs(g, s): def dijk(g, s): + """ + dijk({1: [(2, 7), (3, 9), (6, 14)], + 2: [(1, 7), (3, 10), (4, 15)], + 3: [(1, 9), (2, 10), (4, 11), (6, 2)], + 4: [(2, 15), (3, 11), (5, 6)], + 5: [(4, 6), (6, 9)], + 6: [(1, 14), (3, 2), (5, 9)]}, 1) + 7 + 9 + 11 + 20 + 20 + """ dist, known, path = {s: 0}, set(), {s: 0} while True: if len(known) == len(g) - 1: break mini = 100000 - for i in dist: - if i not in known and dist[i] < mini: - mini = dist[i] - u = i + for key, value in dist: + if key not in known and value < mini: + mini = value + u = key known.add(u) for v in g[u]: - if v[0] not in known: - if dist[u] + v[1] < dist.get(v[0], 100000): - dist[v[0]] = dist[u] + v[1] - path[v[0]] = u - for i in dist: - if i != s: - print(dist[i]) + if v[0] not in known and dist[u] + v[1] < dist.get(v[0], 100000): + dist[v[0]] = dist[u] + v[1] + path[v[0]] = u + for key, value in dist.items(): + if key != s: + print(value) """ @@ -186,10 +217,29 @@ def topo(g, ind=None, q=None): def adjm(): - n = input().strip() + r""" + Reading an Adjacency matrix + + Parameters: + None + + Returns: + tuple: A tuple containing a list of edges and number of edges + + Example: + >>> # Simulate user input for 3 nodes + >>> input_data = "4\n0 1 0 1\n1 0 1 0\n0 1 0 1\n1 0 1 0\n" + >>> import sys,io + >>> original_input = sys.stdin + >>> sys.stdin = io.StringIO(input_data) # Redirect stdin for testing + >>> adjm() + ([(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)], 4) + >>> sys.stdin = original_input # Restore original stdin + """ + n = int(input().strip()) a = [] for _ in range(n): - a.append(map(int, input().strip().split())) + a.append(tuple(map(int, input().strip().split()))) return a, n @@ -237,16 +287,15 @@ def prim(g, s): if len(known) == len(g) - 1: break mini = 100000 - for i in dist: - if i not in known and dist[i] < mini: - mini = dist[i] - u = i + for key, value in dist.items(): + if key not in known and value < mini: + mini = value + u = key known.add(u) for v in g[u]: - if v[0] not in known: - if v[1] < dist.get(v[0], 100000): - dist[v[0]] = v[1] - path[v[0]] = u + if v[0] not in known and v[1] < dist.get(v[0], 100000): + dist[v[0]] = v[1] + path[v[0]] = u return dist @@ -262,10 +311,29 @@ def prim(g, s): def edglist(): - n, m = map(int, input().split(" ")) + r""" + Get the edges and number of edges from the user + + Parameters: + None + + Returns: + tuple: A tuple containing a list of edges and number of edges + + Example: + >>> # Simulate user input for 3 edges and 4 vertices: (1, 2), (2, 3), (3, 4) + >>> input_data = "4 3\n1 2\n2 3\n3 4\n" + >>> import sys,io + >>> original_input = sys.stdin + >>> sys.stdin = io.StringIO(input_data) # Redirect stdin for testing + >>> edglist() + ([(1, 2), (2, 3), (3, 4)], 4) + >>> sys.stdin = original_input # Restore original stdin + """ + n, m = tuple(map(int, input().split(" "))) edges = [] for _ in range(m): - edges.append(map(int, input().split(" "))) + edges.append(tuple(map(int, input().split(" ")))) return edges, n @@ -280,7 +348,9 @@ def edglist(): def krusk(e_and_n): - # Sort edges on the basis of distance + """ + Sort edges on the basis of distance + """ (e, n) = e_and_n e.sort(reverse=True, key=lambda x: x[2]) s = [{i} for i in range(1, n + 1)] @@ -301,8 +371,37 @@ def krusk(e_and_n): break -# find the isolated node in the graph def find_isolated_nodes(graph): + """ + Find the isolated node in the graph + + Parameters: + graph (dict): A dictionary representing a graph. + + Returns: + list: A list of isolated nodes. + + Examples: + >>> graph1 = {1: [2, 3], 2: [1, 3], 3: [1, 2], 4: []} + >>> find_isolated_nodes(graph1) + [4] + + >>> graph2 = {'A': ['B', 'C'], 'B': ['A'], 'C': ['A'], 'D': []} + >>> find_isolated_nodes(graph2) + ['D'] + + >>> graph3 = {'X': [], 'Y': [], 'Z': []} + >>> find_isolated_nodes(graph3) + ['X', 'Y', 'Z'] + + >>> graph4 = {1: [2, 3], 2: [1, 3], 3: [1, 2]} + >>> find_isolated_nodes(graph4) + [] + + >>> graph5 = {} + >>> find_isolated_nodes(graph5) + [] + """ isolated = [] for node in graph: if not graph[node]: diff --git a/graphs/bi_directional_dijkstra.py b/graphs/bi_directional_dijkstra.py index fc53e2f0d8f3..d2c4030b921b 100644 --- a/graphs/bi_directional_dijkstra.py +++ b/graphs/bi_directional_dijkstra.py @@ -10,13 +10,40 @@ # Author: Swayam Singh (https://github.com/practice404) - from queue import PriorityQueue from typing import Any import numpy as np +def pass_and_relaxation( + graph: dict, + v: str, + visited_forward: set, + visited_backward: set, + cst_fwd: dict, + cst_bwd: dict, + queue: PriorityQueue, + parent: dict, + shortest_distance: float, +) -> float: + for nxt, d in graph[v]: + if nxt in visited_forward: + continue + old_cost_f = cst_fwd.get(nxt, np.inf) + new_cost_f = cst_fwd[v] + d + if new_cost_f < old_cost_f: + queue.put((new_cost_f, nxt)) + cst_fwd[nxt] = new_cost_f + parent[nxt] = v + if ( + nxt in visited_backward + and cst_fwd[v] + d + cst_bwd[nxt] < shortest_distance + ): + shortest_distance = cst_fwd[v] + d + cst_bwd[nxt] + return shortest_distance + + def bidirectional_dij( source: str, destination: str, graph_forward: dict, graph_backward: dict ) -> int: @@ -51,53 +78,36 @@ def bidirectional_dij( if source == destination: return 0 - while queue_forward and queue_backward: - while not queue_forward.empty(): - _, v_fwd = queue_forward.get() - - if v_fwd not in visited_forward: - break - else: - break + while not queue_forward.empty() and not queue_backward.empty(): + _, v_fwd = queue_forward.get() visited_forward.add(v_fwd) - while not queue_backward.empty(): - _, v_bwd = queue_backward.get() - - if v_bwd not in visited_backward: - break - else: - break + _, v_bwd = queue_backward.get() visited_backward.add(v_bwd) - # forward pass and relaxation - for nxt_fwd, d_forward in graph_forward[v_fwd]: - if nxt_fwd in visited_forward: - continue - old_cost_f = cst_fwd.get(nxt_fwd, np.inf) - new_cost_f = cst_fwd[v_fwd] + d_forward - if new_cost_f < old_cost_f: - queue_forward.put((new_cost_f, nxt_fwd)) - cst_fwd[nxt_fwd] = new_cost_f - parent_forward[nxt_fwd] = v_fwd - if nxt_fwd in visited_backward: - if cst_fwd[v_fwd] + d_forward + cst_bwd[nxt_fwd] < shortest_distance: - shortest_distance = cst_fwd[v_fwd] + d_forward + cst_bwd[nxt_fwd] - - # backward pass and relaxation - for nxt_bwd, d_backward in graph_backward[v_bwd]: - if nxt_bwd in visited_backward: - continue - old_cost_b = cst_bwd.get(nxt_bwd, np.inf) - new_cost_b = cst_bwd[v_bwd] + d_backward - if new_cost_b < old_cost_b: - queue_backward.put((new_cost_b, nxt_bwd)) - cst_bwd[nxt_bwd] = new_cost_b - parent_backward[nxt_bwd] = v_bwd - - if nxt_bwd in visited_forward: - if cst_bwd[v_bwd] + d_backward + cst_fwd[nxt_bwd] < shortest_distance: - shortest_distance = cst_bwd[v_bwd] + d_backward + cst_fwd[nxt_bwd] + shortest_distance = pass_and_relaxation( + graph_forward, + v_fwd, + visited_forward, + visited_backward, + cst_fwd, + cst_bwd, + queue_forward, + parent_forward, + shortest_distance, + ) + + shortest_distance = pass_and_relaxation( + graph_backward, + v_bwd, + visited_backward, + visited_forward, + cst_bwd, + cst_fwd, + queue_backward, + parent_backward, + shortest_distance, + ) if cst_fwd[v_fwd] + cst_bwd[v_bwd] >= shortest_distance: break diff --git a/graphs/bidirectional_a_star.py b/graphs/bidirectional_a_star.py index 373d67142aa9..00f623de3493 100644 --- a/graphs/bidirectional_a_star.py +++ b/graphs/bidirectional_a_star.py @@ -1,6 +1,7 @@ """ https://en.wikipedia.org/wiki/Bidirectional_search """ + from __future__ import annotations import time diff --git a/graphs/bidirectional_breadth_first_search.py b/graphs/bidirectional_breadth_first_search.py index 511b080a9add..71c5a9aff08f 100644 --- a/graphs/bidirectional_breadth_first_search.py +++ b/graphs/bidirectional_breadth_first_search.py @@ -1,6 +1,7 @@ """ https://en.wikipedia.org/wiki/Bidirectional_search """ + from __future__ import annotations import time diff --git a/graphs/boruvka.py b/graphs/boruvka.py index 2715a3085948..3dc059ff6a62 100644 --- a/graphs/boruvka.py +++ b/graphs/boruvka.py @@ -1,29 +1,30 @@ """Borůvka's algorithm. - Determines the minimum spanning tree (MST) of a graph using the Borůvka's algorithm. - Borůvka's algorithm is a greedy algorithm for finding a minimum spanning tree in a - connected graph, or a minimum spanning forest if a graph that is not connected. +Determines the minimum spanning tree (MST) of a graph using the Borůvka's algorithm. +Borůvka's algorithm is a greedy algorithm for finding a minimum spanning tree in a +connected graph, or a minimum spanning forest if a graph that is not connected. - The time complexity of this algorithm is O(ELogV), where E represents the number - of edges, while V represents the number of nodes. - O(number_of_edges Log number_of_nodes) +The time complexity of this algorithm is O(ELogV), where E represents the number +of edges, while V represents the number of nodes. +O(number_of_edges Log number_of_nodes) - The space complexity of this algorithm is O(V + E), since we have to keep a couple - of lists whose sizes are equal to the number of nodes, as well as keep all the - edges of a graph inside of the data structure itself. +The space complexity of this algorithm is O(V + E), since we have to keep a couple +of lists whose sizes are equal to the number of nodes, as well as keep all the +edges of a graph inside of the data structure itself. - Borůvka's algorithm gives us pretty much the same result as other MST Algorithms - - they all find the minimum spanning tree, and the time complexity is approximately - the same. +Borůvka's algorithm gives us pretty much the same result as other MST Algorithms - +they all find the minimum spanning tree, and the time complexity is approximately +the same. - One advantage that Borůvka's algorithm has compared to the alternatives is that it - doesn't need to presort the edges or maintain a priority queue in order to find the - minimum spanning tree. - Even though that doesn't help its complexity, since it still passes the edges logE - times, it is a bit simpler to code. +One advantage that Borůvka's algorithm has compared to the alternatives is that it +doesn't need to presort the edges or maintain a priority queue in order to find the +minimum spanning tree. +Even though that doesn't help its complexity, since it still passes the edges logE +times, it is a bit simpler to code. - Details: https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm +Details: https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm """ + from __future__ import annotations from typing import Any diff --git a/graphs/breadth_first_search.py b/graphs/breadth_first_search.py index 171d3875f3c5..cab79be39ed3 100644 --- a/graphs/breadth_first_search.py +++ b/graphs/breadth_first_search.py @@ -1,6 +1,7 @@ #!/usr/bin/python -""" Author: OMKAR PATHAK """ +"""Author: OMKAR PATHAK""" + from __future__ import annotations from queue import Queue diff --git a/graphs/breadth_first_search_2.py b/graphs/breadth_first_search_2.py index a0b92b90b456..ccadfa346bf1 100644 --- a/graphs/breadth_first_search_2.py +++ b/graphs/breadth_first_search_2.py @@ -12,6 +12,7 @@ mark w as explored add w to Q (at the end) """ + from __future__ import annotations from collections import deque diff --git a/graphs/breadth_first_search_shortest_path.py b/graphs/breadth_first_search_shortest_path.py index cb21076f91d2..c06440bccef3 100644 --- a/graphs/breadth_first_search_shortest_path.py +++ b/graphs/breadth_first_search_shortest_path.py @@ -1,6 +1,7 @@ """Breath First Search (BFS) can be used when finding the shortest path from a given source node to a target node in an unweighted graph. """ + from __future__ import annotations graph = { @@ -73,9 +74,10 @@ def shortest_path(self, target_vertex: str) -> str: target_vertex_parent = self.parent.get(target_vertex) if target_vertex_parent is None: - raise ValueError( + msg = ( f"No path from vertex: {self.source_vertex} to vertex: {target_vertex}" ) + raise ValueError(msg) return self.shortest_path(target_vertex_parent) + f"->{target_vertex}" diff --git a/graphs/breadth_first_search_shortest_path_2.py b/graphs/breadth_first_search_shortest_path_2.py index b0c8d353ba04..4f9b6e65bdf3 100644 --- a/graphs/breadth_first_search_shortest_path_2.py +++ b/graphs/breadth_first_search_shortest_path_2.py @@ -1,9 +1,10 @@ """Breadth-first search shortest path implementations. - doctest: - python -m doctest -v bfs_shortest_path.py - Manual test: - python bfs_shortest_path.py +doctest: +python -m doctest -v bfs_shortest_path.py +Manual test: +python bfs_shortest_path.py """ + demo_graph = { "A": ["B", "C", "E"], "B": ["A", "D", "E"], diff --git a/graphs/breadth_first_search_zero_one_shortest_path.py b/graphs/breadth_first_search_zero_one_shortest_path.py index 78047c5d2237..d3a255bac1ef 100644 --- a/graphs/breadth_first_search_zero_one_shortest_path.py +++ b/graphs/breadth_first_search_zero_one_shortest_path.py @@ -3,6 +3,7 @@ 0-1-graph is the weighted graph with the weights equal to 0 or 1. Link: https://codeforces.com/blog/entry/22276 """ + from __future__ import annotations from collections import deque diff --git a/graphs/check_bipartite_graph_bfs.py b/graphs/check_bipartite_graph_bfs.py deleted file mode 100644 index 7fc57cbc78bd..000000000000 --- a/graphs/check_bipartite_graph_bfs.py +++ /dev/null @@ -1,47 +0,0 @@ -# Check whether Graph is Bipartite or Not using BFS - - -# A Bipartite Graph is a graph whose vertices can be divided into two independent sets, -# U and V such that every edge (u, v) either connects a vertex from U to V or a vertex -# from V to U. In other words, for every edge (u, v), either u belongs to U and v to V, -# or u belongs to V and v to U. We can also say that there is no edge that connects -# vertices of same set. -from queue import Queue - - -def check_bipartite(graph): - queue = Queue() - visited = [False] * len(graph) - color = [-1] * len(graph) - - def bfs(): - while not queue.empty(): - u = queue.get() - visited[u] = True - - for neighbour in graph[u]: - if neighbour == u: - return False - - if color[neighbour] == -1: - color[neighbour] = 1 - color[u] - queue.put(neighbour) - - elif color[neighbour] == color[u]: - return False - - return True - - for i in range(len(graph)): - if not visited[i]: - queue.put(i) - color[i] = 0 - if bfs() is False: - return False - - return True - - -if __name__ == "__main__": - # Adjacency List of graph - print(check_bipartite({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]})) diff --git a/graphs/check_bipartite_graph_dfs.py b/graphs/check_bipartite_graph_dfs.py deleted file mode 100644 index fd644230449c..000000000000 --- a/graphs/check_bipartite_graph_dfs.py +++ /dev/null @@ -1,34 +0,0 @@ -# Check whether Graph is Bipartite or Not using DFS - - -# A Bipartite Graph is a graph whose vertices can be divided into two independent sets, -# U and V such that every edge (u, v) either connects a vertex from U to V or a vertex -# from V to U. In other words, for every edge (u, v), either u belongs to U and v to V, -# or u belongs to V and v to U. We can also say that there is no edge that connects -# vertices of same set. -def check_bipartite_dfs(graph): - visited = [False] * len(graph) - color = [-1] * len(graph) - - def dfs(v, c): - visited[v] = True - color[v] = c - for u in graph[v]: - if not visited[u]: - dfs(u, 1 - c) - - for i in range(len(graph)): - if not visited[i]: - dfs(i, 0) - - for i in range(len(graph)): - for j in graph[i]: - if color[i] == color[j]: - return False - - return True - - -# Adjacency list of graph -graph = {0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: []} -print(check_bipartite_dfs(graph)) diff --git a/graphs/check_bipatrite.py b/graphs/check_bipatrite.py new file mode 100644 index 000000000000..213f3f9480b5 --- /dev/null +++ b/graphs/check_bipatrite.py @@ -0,0 +1,183 @@ +from collections import defaultdict, deque + + +def is_bipartite_dfs(graph: defaultdict[int, list[int]]) -> bool: + """ + Check if a graph is bipartite using depth-first search (DFS). + + Args: + `graph`: Adjacency list representing the graph. + + Returns: + ``True`` if bipartite, ``False`` otherwise. + + Checks if the graph can be divided into two sets of vertices, such that no two + vertices within the same set are connected by an edge. + + Examples: + + >>> # FIXME: This test should pass. + >>> is_bipartite_dfs(defaultdict(list, {0: [1, 2], 1: [0, 3], 2: [0, 4]})) + Traceback (most recent call last): + ... + RuntimeError: dictionary changed size during iteration + >>> is_bipartite_dfs(defaultdict(list, {0: [1, 2], 1: [0, 3], 2: [0, 1]})) + False + >>> is_bipartite_dfs({}) + True + >>> is_bipartite_dfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]}) + True + >>> is_bipartite_dfs({0: [1, 2, 3], 1: [0, 2], 2: [0, 1, 3], 3: [0, 2]}) + False + >>> is_bipartite_dfs({0: [4], 1: [], 2: [4], 3: [4], 4: [0, 2, 3]}) + True + >>> is_bipartite_dfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]}) + False + >>> is_bipartite_dfs({7: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]}) + Traceback (most recent call last): + ... + KeyError: 0 + + >>> # FIXME: This test should fails with KeyError: 4. + >>> is_bipartite_dfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]}) + False + >>> is_bipartite_dfs({0: [-1, 3], 1: [0, -2]}) + Traceback (most recent call last): + ... + KeyError: -1 + >>> is_bipartite_dfs({-1: [0, 2], 0: [-1, 1], 1: [0, 2], 2: [-1, 1]}) + True + >>> is_bipartite_dfs({0.9: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]}) + Traceback (most recent call last): + ... + KeyError: 0 + + >>> # FIXME: This test should fails with + >>> # TypeError: list indices must be integers or... + >>> is_bipartite_dfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]}) + True + >>> is_bipartite_dfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]}) + Traceback (most recent call last): + ... + KeyError: 1 + >>> is_bipartite_dfs({0: ["b", "d"], 1: ["a", "c"], 2: ["b", "d"], 3: ["a", "c"]}) + Traceback (most recent call last): + ... + KeyError: 'b' + """ + + def depth_first_search(node: int, color: int) -> bool: + """ + Perform Depth-First Search (DFS) on the graph starting from a node. + + Args: + node: The current node being visited. + color: The color assigned to the current node. + + Returns: + True if the graph is bipartite starting from the current node, + False otherwise. + """ + if visited[node] == -1: + visited[node] = color + for neighbor in graph[node]: + if not depth_first_search(neighbor, 1 - color): + return False + return visited[node] == color + + visited: defaultdict[int, int] = defaultdict(lambda: -1) + for node in graph: + if visited[node] == -1 and not depth_first_search(node, 0): + return False + return True + + +def is_bipartite_bfs(graph: defaultdict[int, list[int]]) -> bool: + """ + Check if a graph is bipartite using a breadth-first search (BFS). + + Args: + `graph`: Adjacency list representing the graph. + + Returns: + ``True`` if bipartite, ``False`` otherwise. + + Check if the graph can be divided into two sets of vertices, such that no two + vertices within the same set are connected by an edge. + + Examples: + + >>> # FIXME: This test should pass. + >>> is_bipartite_bfs(defaultdict(list, {0: [1, 2], 1: [0, 3], 2: [0, 4]})) + Traceback (most recent call last): + ... + RuntimeError: dictionary changed size during iteration + >>> is_bipartite_bfs(defaultdict(list, {0: [1, 2], 1: [0, 2], 2: [0, 1]})) + False + >>> is_bipartite_bfs({}) + True + >>> is_bipartite_bfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]}) + True + >>> is_bipartite_bfs({0: [1, 2, 3], 1: [0, 2], 2: [0, 1, 3], 3: [0, 2]}) + False + >>> is_bipartite_bfs({0: [4], 1: [], 2: [4], 3: [4], 4: [0, 2, 3]}) + True + >>> is_bipartite_bfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]}) + False + >>> is_bipartite_bfs({7: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]}) + Traceback (most recent call last): + ... + KeyError: 0 + + >>> # FIXME: This test should fails with KeyError: 4. + >>> is_bipartite_bfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]}) + False + >>> is_bipartite_bfs({0: [-1, 3], 1: [0, -2]}) + Traceback (most recent call last): + ... + KeyError: -1 + >>> is_bipartite_bfs({-1: [0, 2], 0: [-1, 1], 1: [0, 2], 2: [-1, 1]}) + True + >>> is_bipartite_bfs({0.9: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]}) + Traceback (most recent call last): + ... + KeyError: 0 + + >>> # FIXME: This test should fails with + >>> # TypeError: list indices must be integers or... + >>> is_bipartite_bfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]}) + True + >>> is_bipartite_bfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]}) + Traceback (most recent call last): + ... + KeyError: 1 + >>> is_bipartite_bfs({0: ["b", "d"], 1: ["a", "c"], 2: ["b", "d"], 3: ["a", "c"]}) + Traceback (most recent call last): + ... + KeyError: 'b' + """ + visited: defaultdict[int, int] = defaultdict(lambda: -1) + for node in graph: + if visited[node] == -1: + queue: deque[int] = deque() + queue.append(node) + visited[node] = 0 + while queue: + curr_node = queue.popleft() + for neighbor in graph[curr_node]: + if visited[neighbor] == -1: + visited[neighbor] = 1 - visited[curr_node] + queue.append(neighbor) + elif visited[neighbor] == visited[curr_node]: + return False + return True + + +if __name__ == "__main": + import doctest + + result = doctest.testmod() + if result.failed: + print(f"{result.failed} test(s) failed.") + else: + print("All tests passed!") diff --git a/graphs/check_cycle.py b/graphs/check_cycle.py index dcc864988ca5..9fd1cd80f116 100644 --- a/graphs/check_cycle.py +++ b/graphs/check_cycle.py @@ -15,11 +15,10 @@ def check_cycle(graph: dict) -> bool: visited: set[int] = set() # To detect a back edge, keep track of vertices currently in the recursion stack rec_stk: set[int] = set() - for node in graph: - if node not in visited: - if depth_first_search(graph, node, visited, rec_stk): - return True - return False + return any( + node not in visited and depth_first_search(graph, node, visited, rec_stk) + for node in graph + ) def depth_first_search(graph: dict, vertex: int, visited: set, rec_stk: set) -> bool: diff --git a/graphs/connected_components.py b/graphs/connected_components.py index 4af7803d74a7..15c7633e13e8 100644 --- a/graphs/connected_components.py +++ b/graphs/connected_components.py @@ -27,7 +27,7 @@ def dfs(graph: dict, vert: int, visited: list) -> list: if not visited[neighbour]: connected_verts += dfs(graph, neighbour, visited) - return [vert] + connected_verts + return [vert, *connected_verts] def connected_components(graph: dict) -> list: diff --git a/graphs/deep_clone_graph.py b/graphs/deep_clone_graph.py new file mode 100644 index 000000000000..18ea99c6a52d --- /dev/null +++ b/graphs/deep_clone_graph.py @@ -0,0 +1,78 @@ +""" +LeetCode 133. Clone Graph +https://leetcode.com/problems/clone-graph/ + +Given a reference of a node in a connected undirected graph. + +Return a deep copy (clone) of the graph. + +Each node in the graph contains a value (int) and a list (List[Node]) of its +neighbors. +""" + +from dataclasses import dataclass + + +@dataclass +class Node: + value: int = 0 + neighbors: list["Node"] | None = None + + def __post_init__(self) -> None: + """ + >>> Node(3).neighbors + [] + """ + self.neighbors = self.neighbors or [] + + def __hash__(self) -> int: + """ + >>> hash(Node(3)) != 0 + True + """ + return id(self) + + +def clone_graph(node: Node | None) -> Node | None: + """ + This function returns a clone of a connected undirected graph. + >>> clone_graph(Node(1)) + Node(value=1, neighbors=[]) + >>> clone_graph(Node(1, [Node(2)])) + Node(value=1, neighbors=[Node(value=2, neighbors=[])]) + >>> clone_graph(None) is None + True + """ + if not node: + return None + + originals_to_clones = {} # map nodes to clones + + stack = [node] + + while stack: + original = stack.pop() + + if original in originals_to_clones: + continue + + originals_to_clones[original] = Node(original.value) + + stack.extend(original.neighbors or []) + + for original, clone in originals_to_clones.items(): + for neighbor in original.neighbors or []: + cloned_neighbor = originals_to_clones[neighbor] + + if not clone.neighbors: + clone.neighbors = [] + + clone.neighbors.append(cloned_neighbor) + + return originals_to_clones[node] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphs/depth_first_search.py b/graphs/depth_first_search.py index f20a503ca395..a666e74ce607 100644 --- a/graphs/depth_first_search.py +++ b/graphs/depth_first_search.py @@ -1,4 +1,5 @@ """Non recursive implementation of a DFS algorithm.""" + from __future__ import annotations diff --git a/graphs/depth_first_search_2.py b/graphs/depth_first_search_2.py index 3072d527c1c7..8fe48b7f2b42 100644 --- a/graphs/depth_first_search_2.py +++ b/graphs/depth_first_search_2.py @@ -1,6 +1,6 @@ #!/usr/bin/python -""" Author: OMKAR PATHAK """ +"""Author: OMKAR PATHAK""" class Graph: @@ -9,12 +9,44 @@ def __init__(self): # for printing the Graph vertices def print_graph(self) -> None: + """ + Print the graph vertices. + + Example: + >>> g = Graph() + >>> g.add_edge(0, 1) + >>> g.add_edge(0, 2) + >>> g.add_edge(1, 2) + >>> g.add_edge(2, 0) + >>> g.add_edge(2, 3) + >>> g.add_edge(3, 3) + >>> g.print_graph() + {0: [1, 2], 1: [2], 2: [0, 3], 3: [3]} + 0 -> 1 -> 2 + 1 -> 2 + 2 -> 0 -> 3 + 3 -> 3 + """ print(self.vertex) for i in self.vertex: print(i, " -> ", " -> ".join([str(j) for j in self.vertex[i]])) # for adding the edge between two vertices def add_edge(self, from_vertex: int, to_vertex: int) -> None: + """ + Add an edge between two vertices. + + :param from_vertex: The source vertex. + :param to_vertex: The destination vertex. + + Example: + >>> g = Graph() + >>> g.add_edge(0, 1) + >>> g.add_edge(0, 2) + >>> g.print_graph() + {0: [1, 2]} + 0 -> 1 -> 2 + """ # check if vertex is already present, if from_vertex in self.vertex: self.vertex[from_vertex].append(to_vertex) @@ -23,6 +55,21 @@ def add_edge(self, from_vertex: int, to_vertex: int) -> None: self.vertex[from_vertex] = [to_vertex] def dfs(self) -> None: + """ + Perform depth-first search (DFS) traversal on the graph + and print the visited vertices. + + Example: + >>> g = Graph() + >>> g.add_edge(0, 1) + >>> g.add_edge(0, 2) + >>> g.add_edge(1, 2) + >>> g.add_edge(2, 0) + >>> g.add_edge(2, 3) + >>> g.add_edge(3, 3) + >>> g.dfs() + 0 1 2 3 + """ # visited array for storing already visited nodes visited = [False] * len(self.vertex) @@ -32,18 +79,41 @@ def dfs(self) -> None: self.dfs_recursive(i, visited) def dfs_recursive(self, start_vertex: int, visited: list) -> None: + """ + Perform a recursive depth-first search (DFS) traversal on the graph. + + :param start_vertex: The starting vertex for the traversal. + :param visited: A list to track visited vertices. + + Example: + >>> g = Graph() + >>> g.add_edge(0, 1) + >>> g.add_edge(0, 2) + >>> g.add_edge(1, 2) + >>> g.add_edge(2, 0) + >>> g.add_edge(2, 3) + >>> g.add_edge(3, 3) + >>> visited = [False] * len(g.vertex) + >>> g.dfs_recursive(0, visited) + 0 1 2 3 + """ # mark start vertex as visited visited[start_vertex] = True - print(start_vertex, end=" ") + print(start_vertex, end="") # Recur for all the vertices that are adjacent to this node for i in self.vertex: if not visited[i]: + print(" ", end="") self.dfs_recursive(i, visited) if __name__ == "__main__": + import doctest + + doctest.testmod() + g = Graph() g.add_edge(0, 1) g.add_edge(0, 2) @@ -55,11 +125,3 @@ def dfs_recursive(self, start_vertex: int, visited: list) -> None: g.print_graph() print("DFS:") g.dfs() - - # OUTPUT: - # 0 -> 1 -> 2 - # 1 -> 2 - # 2 -> 0 -> 3 - # 3 -> 3 - # DFS: - # 0 1 2 3 diff --git a/graphs/dijkstra.py b/graphs/dijkstra.py index b0bdfab60649..87e9d2233bb2 100644 --- a/graphs/dijkstra.py +++ b/graphs/dijkstra.py @@ -30,6 +30,7 @@ distance between each vertex that makes up the path from start vertex to target vertex. """ + import heapq diff --git a/graphs/dijkstra_algorithm.py b/graphs/dijkstra_algorithm.py index 1845dad05db2..51412b790bac 100644 --- a/graphs/dijkstra_algorithm.py +++ b/graphs/dijkstra_algorithm.py @@ -11,35 +11,127 @@ class PriorityQueue: # Based on Min Heap def __init__(self): + """ + Priority queue class constructor method. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.cur_size + 0 + >>> priority_queue_test.array + [] + >>> priority_queue_test.pos + {} + """ self.cur_size = 0 self.array = [] self.pos = {} # To store the pos of node in array def is_empty(self): + """ + Conditional boolean method to determine if the priority queue is empty or not. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.is_empty() + True + >>> priority_queue_test.insert((2, 'A')) + >>> priority_queue_test.is_empty() + False + """ return self.cur_size == 0 def min_heapify(self, idx): + """ + Sorts the queue array so that the minimum element is root. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.cur_size = 3 + >>> priority_queue_test.pos = {'A': 0, 'B': 1, 'C': 2} + + >>> priority_queue_test.array = [(5, 'A'), (10, 'B'), (15, 'C')] + >>> priority_queue_test.min_heapify(0) + Traceback (most recent call last): + ... + TypeError: 'list' object is not callable + >>> priority_queue_test.array + [(5, 'A'), (10, 'B'), (15, 'C')] + + >>> priority_queue_test.array = [(10, 'A'), (5, 'B'), (15, 'C')] + >>> priority_queue_test.min_heapify(0) + Traceback (most recent call last): + ... + TypeError: 'list' object is not callable + >>> priority_queue_test.array + [(10, 'A'), (5, 'B'), (15, 'C')] + + >>> priority_queue_test.array = [(10, 'A'), (15, 'B'), (5, 'C')] + >>> priority_queue_test.min_heapify(0) + Traceback (most recent call last): + ... + TypeError: 'list' object is not callable + >>> priority_queue_test.array + [(10, 'A'), (15, 'B'), (5, 'C')] + + >>> priority_queue_test.array = [(10, 'A'), (5, 'B')] + >>> priority_queue_test.cur_size = len(priority_queue_test.array) + >>> priority_queue_test.pos = {'A': 0, 'B': 1} + >>> priority_queue_test.min_heapify(0) + Traceback (most recent call last): + ... + TypeError: 'list' object is not callable + >>> priority_queue_test.array + [(10, 'A'), (5, 'B')] + """ lc = self.left(idx) rc = self.right(idx) - if lc < self.cur_size and self.array(lc)[0] < self.array(idx)[0]: + if lc < self.cur_size and self.array(lc)[0] < self.array[idx][0]: smallest = lc else: smallest = idx - if rc < self.cur_size and self.array(rc)[0] < self.array(smallest)[0]: + if rc < self.cur_size and self.array(rc)[0] < self.array[smallest][0]: smallest = rc if smallest != idx: self.swap(idx, smallest) self.min_heapify(smallest) def insert(self, tup): - # Inserts a node into the Priority Queue + """ + Inserts a node into the Priority Queue. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.insert((10, 'A')) + >>> priority_queue_test.array + [(10, 'A')] + >>> priority_queue_test.insert((15, 'B')) + >>> priority_queue_test.array + [(10, 'A'), (15, 'B')] + >>> priority_queue_test.insert((5, 'C')) + >>> priority_queue_test.array + [(5, 'C'), (10, 'A'), (15, 'B')] + """ self.pos[tup[1]] = self.cur_size self.cur_size += 1 self.array.append((sys.maxsize, tup[1])) self.decrease_key((sys.maxsize, tup[1]), tup[0]) def extract_min(self): - # Removes and returns the min element at top of priority queue + """ + Removes and returns the min element at top of priority queue. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.array = [(10, 'A'), (15, 'B')] + >>> priority_queue_test.cur_size = len(priority_queue_test.array) + >>> priority_queue_test.pos = {'A': 0, 'B': 1} + >>> priority_queue_test.insert((5, 'C')) + >>> priority_queue_test.extract_min() + 'C' + >>> priority_queue_test.array[0] + (15, 'B') + """ min_node = self.array[0][1] self.array[0] = self.array[self.cur_size - 1] self.cur_size -= 1 @@ -48,20 +140,61 @@ def extract_min(self): return min_node def left(self, i): - # returns the index of left child + """ + Returns the index of left child + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.left(0) + 1 + >>> priority_queue_test.left(1) + 3 + """ return 2 * i + 1 def right(self, i): - # returns the index of right child + """ + Returns the index of right child + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.right(0) + 2 + >>> priority_queue_test.right(1) + 4 + """ return 2 * i + 2 def par(self, i): - # returns the index of parent + """ + Returns the index of parent + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.par(1) + 0 + >>> priority_queue_test.par(2) + 1 + >>> priority_queue_test.par(4) + 2 + """ return math.floor(i / 2) def swap(self, i, j): - # swaps array elements at indices i and j - # update the pos{} + """ + Swaps array elements at indices i and j, update the pos{} + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.array = [(10, 'A'), (15, 'B')] + >>> priority_queue_test.cur_size = len(priority_queue_test.array) + >>> priority_queue_test.pos = {'A': 0, 'B': 1} + >>> priority_queue_test.swap(0, 1) + >>> priority_queue_test.array + [(15, 'B'), (10, 'A')] + >>> priority_queue_test.pos + {'A': 1, 'B': 0} + """ self.pos[self.array[i][1]] = j self.pos[self.array[j][1]] = i temp = self.array[i] @@ -69,8 +202,20 @@ def swap(self, i, j): self.array[j] = temp def decrease_key(self, tup, new_d): + """ + Decrease the key value for a given tuple, assuming the new_d is at most old_d. + + Examples: + >>> priority_queue_test = PriorityQueue() + >>> priority_queue_test.array = [(10, 'A'), (15, 'B')] + >>> priority_queue_test.cur_size = len(priority_queue_test.array) + >>> priority_queue_test.pos = {'A': 0, 'B': 1} + >>> priority_queue_test.decrease_key((10, 'A'), 5) + >>> priority_queue_test.array + [(5, 'A'), (15, 'B')] + """ idx = self.pos[tup[1]] - # assuming the new_d is atmost old_d + # assuming the new_d is at most old_d self.array[idx] = (new_d, tup[1]) while idx > 0 and self.array[self.par(idx)][0] > self.array[idx][0]: self.swap(idx, self.par(idx)) @@ -79,6 +224,20 @@ def decrease_key(self, tup, new_d): class Graph: def __init__(self, num): + """ + Graph class constructor + + Examples: + >>> graph_test = Graph(1) + >>> graph_test.num_nodes + 1 + >>> graph_test.dist + [0] + >>> graph_test.par + [-1] + >>> graph_test.adjList + {} + """ self.adjList = {} # To store graph: u -> (v,w) self.num_nodes = num # Number of nodes in graph # To store the distance from source vertex @@ -86,8 +245,16 @@ def __init__(self, num): self.par = [-1] * self.num_nodes # To store the path def add_edge(self, u, v, w): - # Edge going from node u to v and v to u with weight w - # u (w)-> v, v (w) -> u + """ + Add edge going from node u to v and v to u with weight w: u (w)-> v, v (w) -> u + + Examples: + >>> graph_test = Graph(1) + >>> graph_test.add_edge(1, 2, 1) + >>> graph_test.add_edge(2, 3, 2) + >>> graph_test.adjList + {1: [(2, 1)], 2: [(1, 1), (3, 2)], 3: [(2, 2)]} + """ # Check if u already in graph if u in self.adjList: self.adjList[u].append((v, w)) @@ -101,18 +268,106 @@ def add_edge(self, u, v, w): self.adjList[v] = [(u, w)] def show_graph(self): - # u -> v(w) + """ + Show the graph: u -> v(w) + + Examples: + >>> graph_test = Graph(1) + >>> graph_test.add_edge(1, 2, 1) + >>> graph_test.show_graph() + 1 -> 2(1) + 2 -> 1(1) + >>> graph_test.add_edge(2, 3, 2) + >>> graph_test.show_graph() + 1 -> 2(1) + 2 -> 1(1) -> 3(2) + 3 -> 2(2) + """ for u in self.adjList: print(u, "->", " -> ".join(str(f"{v}({w})") for v, w in self.adjList[u])) def dijkstra(self, src): + """ + Dijkstra algorithm + + Examples: + >>> graph_test = Graph(3) + >>> graph_test.add_edge(0, 1, 2) + >>> graph_test.add_edge(1, 2, 2) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 2 + Node 2 has distance: 4 + >>> graph_test.dist + [0, 2, 4] + + >>> graph_test = Graph(2) + >>> graph_test.add_edge(0, 1, 2) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 2 + >>> graph_test.dist + [0, 2] + + >>> graph_test = Graph(3) + >>> graph_test.add_edge(0, 1, 2) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 2 + Node 2 has distance: 0 + >>> graph_test.dist + [0, 2, 0] + + >>> graph_test = Graph(3) + >>> graph_test.add_edge(0, 1, 2) + >>> graph_test.add_edge(1, 2, 2) + >>> graph_test.add_edge(0, 2, 1) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 2 + Node 2 has distance: 1 + >>> graph_test.dist + [0, 2, 1] + + >>> graph_test = Graph(4) + >>> graph_test.add_edge(0, 1, 4) + >>> graph_test.add_edge(1, 2, 2) + >>> graph_test.add_edge(2, 3, 1) + >>> graph_test.add_edge(0, 2, 3) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 4 + Node 2 has distance: 3 + Node 3 has distance: 4 + >>> graph_test.dist + [0, 4, 3, 4] + + >>> graph_test = Graph(4) + >>> graph_test.add_edge(0, 1, 4) + >>> graph_test.add_edge(1, 2, 2) + >>> graph_test.add_edge(2, 3, 1) + >>> graph_test.add_edge(0, 2, 7) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 4 + Node 2 has distance: 6 + Node 3 has distance: 7 + >>> graph_test.dist + [0, 4, 6, 7] + """ # Flush old junk values in par[] self.par = [-1] * self.num_nodes # src is the source node self.dist[src] = 0 q = PriorityQueue() q.insert((0, src)) # (dist from src, node) - for u in self.adjList.keys(): + for u in self.adjList: if u != src: self.dist[u] = sys.maxsize # Infinity self.par[u] = -1 @@ -135,13 +390,40 @@ def dijkstra(self, src): self.show_distances(src) def show_distances(self, src): + """ + Show the distances from src to all other nodes in a graph + + Examples: + >>> graph_test = Graph(1) + >>> graph_test.show_distances(0) + Distance from node: 0 + Node 0 has distance: 0 + """ print(f"Distance from node: {src}") for u in range(self.num_nodes): print(f"Node {u} has distance: {self.dist[u]}") def show_path(self, src, dest): - # To show the shortest path from src to dest - # WARNING: Use it *after* calling dijkstra + """ + Shows the shortest path from src to dest. + WARNING: Use it *after* calling dijkstra. + + Examples: + >>> graph_test = Graph(4) + >>> graph_test.add_edge(0, 1, 1) + >>> graph_test.add_edge(1, 2, 2) + >>> graph_test.add_edge(2, 3, 3) + >>> graph_test.dijkstra(0) + Distance from node: 0 + Node 0 has distance: 0 + Node 1 has distance: 1 + Node 2 has distance: 3 + Node 3 has distance: 6 + >>> graph_test.show_path(0, 3) # doctest: +NORMALIZE_WHITESPACE + ----Path to reach 3 from 0---- + 0 -> 1 -> 2 -> 3 + Total cost of path: 6 + """ path = [] cost = 0 temp = dest @@ -167,6 +449,9 @@ def show_path(self, src, dest): if __name__ == "__main__": + from doctest import testmod + + testmod() graph = Graph(9) graph.add_edge(0, 1, 4) graph.add_edge(0, 7, 8) diff --git a/graphs/dijkstra_binary_grid.py b/graphs/dijkstra_binary_grid.py new file mode 100644 index 000000000000..06293a87da2d --- /dev/null +++ b/graphs/dijkstra_binary_grid.py @@ -0,0 +1,89 @@ +""" +This script implements the Dijkstra algorithm on a binary grid. +The grid consists of 0s and 1s, where 1 represents +a walkable node and 0 represents an obstacle. +The algorithm finds the shortest path from a start node to a destination node. +Diagonal movement can be allowed or disallowed. +""" + +from heapq import heappop, heappush + +import numpy as np + + +def dijkstra( + grid: np.ndarray, + source: tuple[int, int], + destination: tuple[int, int], + allow_diagonal: bool, +) -> tuple[float | int, list[tuple[int, int]]]: + """ + Implements Dijkstra's algorithm on a binary grid. + + Args: + grid (np.ndarray): A 2D numpy array representing the grid. + 1 represents a walkable node and 0 represents an obstacle. + source (Tuple[int, int]): A tuple representing the start node. + destination (Tuple[int, int]): A tuple representing the + destination node. + allow_diagonal (bool): A boolean determining whether + diagonal movements are allowed. + + Returns: + Tuple[Union[float, int], List[Tuple[int, int]]]: + The shortest distance from the start node to the destination node + and the shortest path as a list of nodes. + + >>> dijkstra(np.array([[1, 1, 1], [0, 1, 0], [0, 1, 1]]), (0, 0), (2, 2), False) + (4.0, [(0, 0), (0, 1), (1, 1), (2, 1), (2, 2)]) + + >>> dijkstra(np.array([[1, 1, 1], [0, 1, 0], [0, 1, 1]]), (0, 0), (2, 2), True) + (2.0, [(0, 0), (1, 1), (2, 2)]) + + >>> dijkstra(np.array([[1, 1, 1], [0, 0, 1], [0, 1, 1]]), (0, 0), (2, 2), False) + (4.0, [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]) + """ + rows, cols = grid.shape + dx = [-1, 1, 0, 0] + dy = [0, 0, -1, 1] + if allow_diagonal: + dx += [-1, -1, 1, 1] + dy += [-1, 1, -1, 1] + + queue, visited = [(0, source)], set() + matrix = np.full((rows, cols), np.inf) + matrix[source] = 0 + predecessors = np.empty((rows, cols), dtype=object) + predecessors[source] = None + + while queue: + (dist, (x, y)) = heappop(queue) + if (x, y) in visited: + continue + visited.add((x, y)) + + if (x, y) == destination: + path = [] + while (x, y) != source: + path.append((x, y)) + x, y = predecessors[x, y] + path.append(source) # add the source manually + path.reverse() + return float(matrix[destination]), path + + for i in range(len(dx)): + nx, ny = x + dx[i], y + dy[i] + if 0 <= nx < rows and 0 <= ny < cols: + next_node = grid[nx][ny] + if next_node == 1 and matrix[nx, ny] > dist + 1: + heappush(queue, (dist + 1, (nx, ny))) + matrix[nx, ny] = dist + 1 + predecessors[nx, ny] = (x, y) + + return np.inf, [] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphs/dinic.py b/graphs/dinic.py index aaf3a119525c..7919e6bc060a 100644 --- a/graphs/dinic.py +++ b/graphs/dinic.py @@ -37,7 +37,7 @@ def depth_first_search(self, vertex, sink, flow): # Here we calculate the flow that reaches the sink def max_flow(self, source, sink): flow, self.q[0] = 0, source - for l in range(31): # noqa: E741 l = 30 maybe faster for random data + for l in range(31): # l = 30 maybe faster for random data # noqa: E741 while True: self.lvl, self.ptr = [0] * len(self.q), [0] * len(self.q) qi, qe, self.lvl[source] = 0, 1, 1 diff --git a/graphs/directed_and_undirected_(weighted)_graph.py b/graphs/directed_and_undirected_weighted_graph.py similarity index 97% rename from graphs/directed_and_undirected_(weighted)_graph.py rename to graphs/directed_and_undirected_weighted_graph.py index b29485031083..8ca645fdace8 100644 --- a/graphs/directed_and_undirected_(weighted)_graph.py +++ b/graphs/directed_and_undirected_weighted_graph.py @@ -39,7 +39,7 @@ def dfs(self, s=-2, d=-1): stack = [] visited = [] if s == -2: - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) ss = s @@ -87,7 +87,7 @@ def bfs(self, s=-2): d = deque() visited = [] if s == -2: - s = list(self.graph)[0] + s = next(iter(self.graph)) d.append(s) visited.append(s) while d: @@ -114,7 +114,7 @@ def topological_sort(self, s=-2): stack = [] visited = [] if s == -2: - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) ss = s @@ -146,7 +146,7 @@ def topological_sort(self, s=-2): def cycle_nodes(self): stack = [] visited = [] - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) parent = -2 @@ -199,7 +199,7 @@ def cycle_nodes(self): def has_cycle(self): stack = [] visited = [] - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) parent = -2 @@ -305,7 +305,7 @@ def dfs(self, s=-2, d=-1): stack = [] visited = [] if s == -2: - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) ss = s @@ -353,7 +353,7 @@ def bfs(self, s=-2): d = deque() visited = [] if s == -2: - s = list(self.graph)[0] + s = next(iter(self.graph)) d.append(s) visited.append(s) while d: @@ -371,7 +371,7 @@ def degree(self, u): def cycle_nodes(self): stack = [] visited = [] - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) parent = -2 @@ -424,7 +424,7 @@ def cycle_nodes(self): def has_cycle(self): stack = [] visited = [] - s = list(self.graph)[0] + s = next(iter(self.graph)) stack.append(s) visited.append(s) parent = -2 diff --git a/graphs/edmonds_karp_multiple_source_and_sink.py b/graphs/edmonds_karp_multiple_source_and_sink.py index 070d758e63b6..5c774f4b812b 100644 --- a/graphs/edmonds_karp_multiple_source_and_sink.py +++ b/graphs/edmonds_karp_multiple_source_and_sink.py @@ -113,7 +113,7 @@ def _algorithm(self): vertices_list = [ i for i in range(self.verticies_count) - if i != self.source_index and i != self.sink_index + if i not in {self.source_index, self.sink_index} ] # move through list @@ -163,9 +163,8 @@ def relabel(self, vertex_index): self.graph[vertex_index][to_index] - self.preflow[vertex_index][to_index] > 0 - ): - if min_height is None or self.heights[to_index] < min_height: - min_height = self.heights[to_index] + ) and (min_height is None or self.heights[to_index] < min_height): + min_height = self.heights[to_index] if min_height is not None: self.heights[vertex_index] = min_height + 1 diff --git a/graphs/eulerian_path_and_circuit_for_undirected_graph.py b/graphs/eulerian_path_and_circuit_for_undirected_graph.py index 6c43c5d3e6e3..5b146eaa845b 100644 --- a/graphs/eulerian_path_and_circuit_for_undirected_graph.py +++ b/graphs/eulerian_path_and_circuit_for_undirected_graph.py @@ -20,7 +20,7 @@ def check_circuit_or_path(graph, max_node): odd_degree_nodes = 0 odd_node = -1 for i in range(max_node): - if i not in graph.keys(): + if i not in graph: continue if len(graph[i]) % 2 == 1: odd_degree_nodes += 1 @@ -56,7 +56,7 @@ def main(): g4 = {1: [2, 3], 2: [1, 3], 3: [1, 2]} g5 = { 1: [], - 2: [] + 2: [], # all degree is zero } max_node = 10 diff --git a/graphs/even_tree.py b/graphs/even_tree.py index 92ffb4b232f7..7d47899527a7 100644 --- a/graphs/even_tree.py +++ b/graphs/even_tree.py @@ -12,6 +12,7 @@ Note: The tree input will be such that it can always be decomposed into components containing an even number of nodes. """ + # pylint: disable=invalid-name from collections import defaultdict diff --git a/graphs/frequent_pattern_graph_miner.py b/graphs/frequent_pattern_graph_miner.py index 87d5605a0bc8..f8da73f3438e 100644 --- a/graphs/frequent_pattern_graph_miner.py +++ b/graphs/frequent_pattern_graph_miner.py @@ -8,6 +8,7 @@ URL: https://www.researchgate.net/publication/235255851 """ + # fmt: off edge_array = [ ['ab-e1', 'ac-e3', 'ad-e5', 'bc-e4', 'bd-e2', 'be-e6', 'bh-e12', 'cd-e2', 'ce-e4', @@ -130,11 +131,11 @@ def create_edge(nodes, graph, cluster, c1): """ create edge between the nodes """ - for i in cluster[c1].keys(): + for i in cluster[c1]: count = 0 c2 = c1 + 1 while c2 < max(cluster.keys()): - for j in cluster[c2].keys(): + for j in cluster[c2]: """ creates edge only if the condition satisfies """ @@ -185,7 +186,7 @@ def find_freq_subgraph_given_support(s, cluster, graph): find edges of multiple frequent subgraphs """ k = int(s / 100 * (len(cluster) - 1)) - for i in cluster[k].keys(): + for i in cluster[k]: my_dfs(graph, tuple(cluster[k][i]), (["Header"],)) diff --git a/graphs/graph_adjacency_list.py b/graphs/graph_adjacency_list.py new file mode 100644 index 000000000000..abc75311cd60 --- /dev/null +++ b/graphs/graph_adjacency_list.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python3 +""" +Author: Vikram Nithyanandam + +Description: +The following implementation is a robust unweighted Graph data structure +implemented using an adjacency list. This vertices and edges of this graph can be +effectively initialized and modified while storing your chosen generic +value in each vertex. + +Adjacency List: https://en.wikipedia.org/wiki/Adjacency_list + +Potential Future Ideas: +- Add a flag to set edge weights on and set edge weights +- Make edge weights and vertex values customizable to store whatever the client wants +- Support multigraph functionality if the client wants it +""" + +from __future__ import annotations + +import random +import unittest +from pprint import pformat +from typing import Generic, TypeVar + +import pytest + +T = TypeVar("T") + + +class GraphAdjacencyList(Generic[T]): + def __init__( + self, vertices: list[T], edges: list[list[T]], directed: bool = True + ) -> None: + """ + Parameters: + - vertices: (list[T]) The list of vertex names the client wants to + pass in. Default is empty. + - edges: (list[list[T]]) The list of edges the client wants to + pass in. Each edge is a 2-element list. Default is empty. + - directed: (bool) Indicates if graph is directed or undirected. + Default is True. + """ + self.adj_list: dict[T, list[T]] = {} # dictionary of lists of T + self.directed = directed + + # Falsey checks + edges = edges or [] + vertices = vertices or [] + + for vertex in vertices: + self.add_vertex(vertex) + + for edge in edges: + if len(edge) != 2: + msg = f"Invalid input: {edge} is the wrong length." + raise ValueError(msg) + self.add_edge(edge[0], edge[1]) + + def add_vertex(self, vertex: T) -> None: + """ + Adds a vertex to the graph. If the given vertex already exists, + a ValueError will be thrown. + """ + if self.contains_vertex(vertex): + msg = f"Incorrect input: {vertex} is already in the graph." + raise ValueError(msg) + self.adj_list[vertex] = [] + + def add_edge(self, source_vertex: T, destination_vertex: T) -> None: + """ + Creates an edge from source vertex to destination vertex. If any + given vertex doesn't exist or the edge already exists, a ValueError + will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} or " + f"{destination_vertex} does not exist" + ) + raise ValueError(msg) + if self.contains_edge(source_vertex, destination_vertex): + msg = ( + "Incorrect input: The edge already exists between " + f"{source_vertex} and {destination_vertex}" + ) + raise ValueError(msg) + + # add the destination vertex to the list associated with the source vertex + # and vice versa if not directed + self.adj_list[source_vertex].append(destination_vertex) + if not self.directed: + self.adj_list[destination_vertex].append(source_vertex) + + def remove_vertex(self, vertex: T) -> None: + """ + Removes the given vertex from the graph and deletes all incoming and + outgoing edges from the given vertex as well. If the given vertex + does not exist, a ValueError will be thrown. + """ + if not self.contains_vertex(vertex): + msg = f"Incorrect input: {vertex} does not exist in this graph." + raise ValueError(msg) + + if not self.directed: + # If not directed, find all neighboring vertices and delete all references + # of edges connecting to the given vertex + for neighbor in self.adj_list[vertex]: + self.adj_list[neighbor].remove(vertex) + else: + # If directed, search all neighbors of all vertices and delete all + # references of edges connecting to the given vertex + for edge_list in self.adj_list.values(): + if vertex in edge_list: + edge_list.remove(vertex) + + # Finally, delete the given vertex and all of its outgoing edge references + self.adj_list.pop(vertex) + + def remove_edge(self, source_vertex: T, destination_vertex: T) -> None: + """ + Removes the edge between the two vertices. If any given vertex + doesn't exist or the edge does not exist, a ValueError will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} or " + f"{destination_vertex} does not exist" + ) + raise ValueError(msg) + if not self.contains_edge(source_vertex, destination_vertex): + msg = ( + "Incorrect input: The edge does NOT exist between " + f"{source_vertex} and {destination_vertex}" + ) + raise ValueError(msg) + + # remove the destination vertex from the list associated with the source + # vertex and vice versa if not directed + self.adj_list[source_vertex].remove(destination_vertex) + if not self.directed: + self.adj_list[destination_vertex].remove(source_vertex) + + def contains_vertex(self, vertex: T) -> bool: + """ + Returns True if the graph contains the vertex, False otherwise. + """ + return vertex in self.adj_list + + def contains_edge(self, source_vertex: T, destination_vertex: T) -> bool: + """ + Returns True if the graph contains the edge from the source_vertex to the + destination_vertex, False otherwise. If any given vertex doesn't exist, a + ValueError will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} " + f"or {destination_vertex} does not exist." + ) + raise ValueError(msg) + + return destination_vertex in self.adj_list[source_vertex] + + def clear_graph(self) -> None: + """ + Clears all vertices and edges. + """ + self.adj_list = {} + + def __repr__(self) -> str: + return pformat(self.adj_list) + + +class TestGraphAdjacencyList(unittest.TestCase): + def __assert_graph_edge_exists_check( + self, + undirected_graph: GraphAdjacencyList, + directed_graph: GraphAdjacencyList, + edge: list[int], + ) -> None: + assert undirected_graph.contains_edge(edge[0], edge[1]) + assert undirected_graph.contains_edge(edge[1], edge[0]) + assert directed_graph.contains_edge(edge[0], edge[1]) + + def __assert_graph_edge_does_not_exist_check( + self, + undirected_graph: GraphAdjacencyList, + directed_graph: GraphAdjacencyList, + edge: list[int], + ) -> None: + assert not undirected_graph.contains_edge(edge[0], edge[1]) + assert not undirected_graph.contains_edge(edge[1], edge[0]) + assert not directed_graph.contains_edge(edge[0], edge[1]) + + def __assert_graph_vertex_exists_check( + self, + undirected_graph: GraphAdjacencyList, + directed_graph: GraphAdjacencyList, + vertex: int, + ) -> None: + assert undirected_graph.contains_vertex(vertex) + assert directed_graph.contains_vertex(vertex) + + def __assert_graph_vertex_does_not_exist_check( + self, + undirected_graph: GraphAdjacencyList, + directed_graph: GraphAdjacencyList, + vertex: int, + ) -> None: + assert not undirected_graph.contains_vertex(vertex) + assert not directed_graph.contains_vertex(vertex) + + def __generate_random_edges( + self, vertices: list[int], edge_pick_count: int + ) -> list[list[int]]: + assert edge_pick_count <= len(vertices) + + random_source_vertices: list[int] = random.sample( + vertices[0 : int(len(vertices) / 2)], edge_pick_count + ) + random_destination_vertices: list[int] = random.sample( + vertices[int(len(vertices) / 2) :], edge_pick_count + ) + random_edges: list[list[int]] = [] + + for source in random_source_vertices: + for dest in random_destination_vertices: + random_edges.append([source, dest]) + + return random_edges + + def __generate_graphs( + self, vertex_count: int, min_val: int, max_val: int, edge_pick_count: int + ) -> tuple[GraphAdjacencyList, GraphAdjacencyList, list[int], list[list[int]]]: + if max_val - min_val + 1 < vertex_count: + raise ValueError( + "Will result in duplicate vertices. Either increase range " + "between min_val and max_val or decrease vertex count." + ) + + # generate graph input + random_vertices: list[int] = random.sample( + range(min_val, max_val + 1), vertex_count + ) + random_edges: list[list[int]] = self.__generate_random_edges( + random_vertices, edge_pick_count + ) + + # build graphs + undirected_graph = GraphAdjacencyList( + vertices=random_vertices, edges=random_edges, directed=False + ) + directed_graph = GraphAdjacencyList( + vertices=random_vertices, edges=random_edges, directed=True + ) + + return undirected_graph, directed_graph, random_vertices, random_edges + + def test_init_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # test graph initialization with vertices and edges + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + for edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + assert not undirected_graph.directed + assert directed_graph.directed + + def test_contains_vertex(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # Build graphs WITHOUT edges + undirected_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=True + ) + + # Test contains_vertex + for num in range(101): + assert (num in random_vertices) == undirected_graph.contains_vertex(num) + assert (num in random_vertices) == directed_graph.contains_vertex(num) + + def test_add_vertices(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # build empty graphs + undirected_graph: GraphAdjacencyList = GraphAdjacencyList( + vertices=[], edges=[], directed=False + ) + directed_graph: GraphAdjacencyList = GraphAdjacencyList( + vertices=[], edges=[], directed=True + ) + + # run add_vertex + for num in random_vertices: + undirected_graph.add_vertex(num) + + for num in random_vertices: + directed_graph.add_vertex(num) + + # test add_vertex worked + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + def test_remove_vertices(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=True + ) + + # test remove_vertex worked + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + undirected_graph.remove_vertex(num) + directed_graph.remove_vertex(num) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, num + ) + + def test_add_and_remove_vertices_repeatedly(self) -> None: + random_vertices1: list[int] = random.sample(range(51), 20) + random_vertices2: list[int] = random.sample(range(51, 101), 20) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyList( + vertices=random_vertices1, edges=[], directed=False + ) + directed_graph = GraphAdjacencyList( + vertices=random_vertices1, edges=[], directed=True + ) + + # test adding and removing vertices + for i, _ in enumerate(random_vertices1): + undirected_graph.add_vertex(random_vertices2[i]) + directed_graph.add_vertex(random_vertices2[i]) + + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, random_vertices2[i] + ) + + undirected_graph.remove_vertex(random_vertices1[i]) + directed_graph.remove_vertex(random_vertices1[i]) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, random_vertices1[i] + ) + + # remove all vertices + for i, _ in enumerate(random_vertices1): + undirected_graph.remove_vertex(random_vertices2[i]) + directed_graph.remove_vertex(random_vertices2[i]) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, random_vertices2[i] + ) + + def test_contains_edge(self) -> None: + # generate graphs and graph input + vertex_count = 20 + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(vertex_count, 0, 100, 4) + + # generate all possible edges for testing + all_possible_edges: list[list[int]] = [] + for i in range(vertex_count - 1): + for j in range(i + 1, vertex_count): + all_possible_edges.append([random_vertices[i], random_vertices[j]]) + all_possible_edges.append([random_vertices[j], random_vertices[i]]) + + # test contains_edge function + for edge in all_possible_edges: + if edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + elif [edge[1], edge[0]] in random_edges: + # since this edge exists for undirected but the reverse + # may not exist for directed + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, [edge[1], edge[0]] + ) + else: + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, edge + ) + + def test_add_edge(self) -> None: + # generate graph input + random_vertices: list[int] = random.sample(range(101), 15) + random_edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyList( + vertices=random_vertices, edges=[], directed=True + ) + + # run and test add_edge + for edge in random_edges: + undirected_graph.add_edge(edge[0], edge[1]) + directed_graph.add_edge(edge[0], edge[1]) + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + + def test_remove_edge(self) -> None: + # generate graph input and graphs + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # run and test remove_edge + for edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + undirected_graph.remove_edge(edge[0], edge[1]) + directed_graph.remove_edge(edge[0], edge[1]) + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, edge + ) + + def test_add_and_remove_edges_repeatedly(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # make some more edge options! + more_random_edges: list[list[int]] = [] + + while len(more_random_edges) != len(random_edges): + edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + for edge in edges: + if len(more_random_edges) == len(random_edges): + break + elif edge not in more_random_edges and edge not in random_edges: + more_random_edges.append(edge) + + for i, _ in enumerate(random_edges): + undirected_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1]) + directed_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1]) + + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, more_random_edges[i] + ) + + undirected_graph.remove_edge(random_edges[i][0], random_edges[i][1]) + directed_graph.remove_edge(random_edges[i][0], random_edges[i][1]) + + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, random_edges[i] + ) + + def test_add_vertex_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for vertex in random_vertices: + with pytest.raises(ValueError): + undirected_graph.add_vertex(vertex) + with pytest.raises(ValueError): + directed_graph.add_vertex(vertex) + + def test_remove_vertex_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for i in range(101): + if i not in random_vertices: + with pytest.raises(ValueError): + undirected_graph.remove_vertex(i) + with pytest.raises(ValueError): + directed_graph.remove_vertex(i) + + def test_add_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for edge in random_edges: + with pytest.raises(ValueError): + undirected_graph.add_edge(edge[0], edge[1]) + with pytest.raises(ValueError): + directed_graph.add_edge(edge[0], edge[1]) + + def test_remove_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + more_random_edges: list[list[int]] = [] + + while len(more_random_edges) != len(random_edges): + edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + for edge in edges: + if len(more_random_edges) == len(random_edges): + break + elif edge not in more_random_edges and edge not in random_edges: + more_random_edges.append(edge) + + for edge in more_random_edges: + with pytest.raises(ValueError): + undirected_graph.remove_edge(edge[0], edge[1]) + with pytest.raises(ValueError): + directed_graph.remove_edge(edge[0], edge[1]) + + def test_contains_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for vertex in random_vertices: + with pytest.raises(ValueError): + undirected_graph.contains_edge(vertex, 102) + with pytest.raises(ValueError): + directed_graph.contains_edge(vertex, 102) + + with pytest.raises(ValueError): + undirected_graph.contains_edge(103, 102) + with pytest.raises(ValueError): + directed_graph.contains_edge(103, 102) + + +if __name__ == "__main__": + unittest.main() diff --git a/graphs/graph_adjacency_matrix.py b/graphs/graph_adjacency_matrix.py new file mode 100644 index 000000000000..568c84166e4b --- /dev/null +++ b/graphs/graph_adjacency_matrix.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 +""" +Author: Vikram Nithyanandam + +Description: +The following implementation is a robust unweighted Graph data structure +implemented using an adjacency matrix. This vertices and edges of this graph can be +effectively initialized and modified while storing your chosen generic +value in each vertex. + +Adjacency Matrix: https://mathworld.wolfram.com/AdjacencyMatrix.html + +Potential Future Ideas: +- Add a flag to set edge weights on and set edge weights +- Make edge weights and vertex values customizable to store whatever the client wants +- Support multigraph functionality if the client wants it +""" + +from __future__ import annotations + +import random +import unittest +from pprint import pformat +from typing import Generic, TypeVar + +import pytest + +T = TypeVar("T") + + +class GraphAdjacencyMatrix(Generic[T]): + def __init__( + self, vertices: list[T], edges: list[list[T]], directed: bool = True + ) -> None: + """ + Parameters: + - vertices: (list[T]) The list of vertex names the client wants to + pass in. Default is empty. + - edges: (list[list[T]]) The list of edges the client wants to + pass in. Each edge is a 2-element list. Default is empty. + - directed: (bool) Indicates if graph is directed or undirected. + Default is True. + """ + self.directed = directed + self.vertex_to_index: dict[T, int] = {} + self.adj_matrix: list[list[int]] = [] + + # Falsey checks + edges = edges or [] + vertices = vertices or [] + + for vertex in vertices: + self.add_vertex(vertex) + + for edge in edges: + if len(edge) != 2: + msg = f"Invalid input: {edge} must have length 2." + raise ValueError(msg) + self.add_edge(edge[0], edge[1]) + + def add_edge(self, source_vertex: T, destination_vertex: T) -> None: + """ + Creates an edge from source vertex to destination vertex. If any + given vertex doesn't exist or the edge already exists, a ValueError + will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} or " + f"{destination_vertex} does not exist" + ) + raise ValueError(msg) + if self.contains_edge(source_vertex, destination_vertex): + msg = ( + "Incorrect input: The edge already exists between " + f"{source_vertex} and {destination_vertex}" + ) + raise ValueError(msg) + + # Get the indices of the corresponding vertices and set their edge value to 1. + u: int = self.vertex_to_index[source_vertex] + v: int = self.vertex_to_index[destination_vertex] + self.adj_matrix[u][v] = 1 + if not self.directed: + self.adj_matrix[v][u] = 1 + + def remove_edge(self, source_vertex: T, destination_vertex: T) -> None: + """ + Removes the edge between the two vertices. If any given vertex + doesn't exist or the edge does not exist, a ValueError will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} or " + f"{destination_vertex} does not exist" + ) + raise ValueError(msg) + if not self.contains_edge(source_vertex, destination_vertex): + msg = ( + "Incorrect input: The edge does NOT exist between " + f"{source_vertex} and {destination_vertex}" + ) + raise ValueError(msg) + + # Get the indices of the corresponding vertices and set their edge value to 0. + u: int = self.vertex_to_index[source_vertex] + v: int = self.vertex_to_index[destination_vertex] + self.adj_matrix[u][v] = 0 + if not self.directed: + self.adj_matrix[v][u] = 0 + + def add_vertex(self, vertex: T) -> None: + """ + Adds a vertex to the graph. If the given vertex already exists, + a ValueError will be thrown. + """ + if self.contains_vertex(vertex): + msg = f"Incorrect input: {vertex} already exists in this graph." + raise ValueError(msg) + + # build column for vertex + for row in self.adj_matrix: + row.append(0) + + # build row for vertex and update other data structures + self.adj_matrix.append([0] * (len(self.adj_matrix) + 1)) + self.vertex_to_index[vertex] = len(self.adj_matrix) - 1 + + def remove_vertex(self, vertex: T) -> None: + """ + Removes the given vertex from the graph and deletes all incoming and + outgoing edges from the given vertex as well. If the given vertex + does not exist, a ValueError will be thrown. + """ + if not self.contains_vertex(vertex): + msg = f"Incorrect input: {vertex} does not exist in this graph." + raise ValueError(msg) + + # first slide up the rows by deleting the row corresponding to + # the vertex being deleted. + start_index = self.vertex_to_index[vertex] + self.adj_matrix.pop(start_index) + + # next, slide the columns to the left by deleting the values in + # the column corresponding to the vertex being deleted + for lst in self.adj_matrix: + lst.pop(start_index) + + # final clean up + self.vertex_to_index.pop(vertex) + + # decrement indices for vertices shifted by the deleted vertex in the adj matrix + for inner_vertex in self.vertex_to_index: + if self.vertex_to_index[inner_vertex] >= start_index: + self.vertex_to_index[inner_vertex] = ( + self.vertex_to_index[inner_vertex] - 1 + ) + + def contains_vertex(self, vertex: T) -> bool: + """ + Returns True if the graph contains the vertex, False otherwise. + """ + return vertex in self.vertex_to_index + + def contains_edge(self, source_vertex: T, destination_vertex: T) -> bool: + """ + Returns True if the graph contains the edge from the source_vertex to the + destination_vertex, False otherwise. If any given vertex doesn't exist, a + ValueError will be thrown. + """ + if not ( + self.contains_vertex(source_vertex) + and self.contains_vertex(destination_vertex) + ): + msg = ( + f"Incorrect input: Either {source_vertex} " + f"or {destination_vertex} does not exist." + ) + raise ValueError(msg) + + u = self.vertex_to_index[source_vertex] + v = self.vertex_to_index[destination_vertex] + return self.adj_matrix[u][v] == 1 + + def clear_graph(self) -> None: + """ + Clears all vertices and edges. + """ + self.vertex_to_index = {} + self.adj_matrix = [] + + def __repr__(self) -> str: + first = "Adj Matrix:\n" + pformat(self.adj_matrix) + second = "\nVertex to index mapping:\n" + pformat(self.vertex_to_index) + return first + second + + +class TestGraphMatrix(unittest.TestCase): + def __assert_graph_edge_exists_check( + self, + undirected_graph: GraphAdjacencyMatrix, + directed_graph: GraphAdjacencyMatrix, + edge: list[int], + ) -> None: + assert undirected_graph.contains_edge(edge[0], edge[1]) + assert undirected_graph.contains_edge(edge[1], edge[0]) + assert directed_graph.contains_edge(edge[0], edge[1]) + + def __assert_graph_edge_does_not_exist_check( + self, + undirected_graph: GraphAdjacencyMatrix, + directed_graph: GraphAdjacencyMatrix, + edge: list[int], + ) -> None: + assert not undirected_graph.contains_edge(edge[0], edge[1]) + assert not undirected_graph.contains_edge(edge[1], edge[0]) + assert not directed_graph.contains_edge(edge[0], edge[1]) + + def __assert_graph_vertex_exists_check( + self, + undirected_graph: GraphAdjacencyMatrix, + directed_graph: GraphAdjacencyMatrix, + vertex: int, + ) -> None: + assert undirected_graph.contains_vertex(vertex) + assert directed_graph.contains_vertex(vertex) + + def __assert_graph_vertex_does_not_exist_check( + self, + undirected_graph: GraphAdjacencyMatrix, + directed_graph: GraphAdjacencyMatrix, + vertex: int, + ) -> None: + assert not undirected_graph.contains_vertex(vertex) + assert not directed_graph.contains_vertex(vertex) + + def __generate_random_edges( + self, vertices: list[int], edge_pick_count: int + ) -> list[list[int]]: + assert edge_pick_count <= len(vertices) + + random_source_vertices: list[int] = random.sample( + vertices[0 : int(len(vertices) / 2)], edge_pick_count + ) + random_destination_vertices: list[int] = random.sample( + vertices[int(len(vertices) / 2) :], edge_pick_count + ) + random_edges: list[list[int]] = [] + + for source in random_source_vertices: + for dest in random_destination_vertices: + random_edges.append([source, dest]) + + return random_edges + + def __generate_graphs( + self, vertex_count: int, min_val: int, max_val: int, edge_pick_count: int + ) -> tuple[GraphAdjacencyMatrix, GraphAdjacencyMatrix, list[int], list[list[int]]]: + if max_val - min_val + 1 < vertex_count: + raise ValueError( + "Will result in duplicate vertices. Either increase " + "range between min_val and max_val or decrease vertex count" + ) + + # generate graph input + random_vertices: list[int] = random.sample( + range(min_val, max_val + 1), vertex_count + ) + random_edges: list[list[int]] = self.__generate_random_edges( + random_vertices, edge_pick_count + ) + + # build graphs + undirected_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=random_edges, directed=False + ) + directed_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=random_edges, directed=True + ) + + return undirected_graph, directed_graph, random_vertices, random_edges + + def test_init_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # test graph initialization with vertices and edges + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + for edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + + assert not undirected_graph.directed + assert directed_graph.directed + + def test_contains_vertex(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # Build graphs WITHOUT edges + undirected_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=True + ) + + # Test contains_vertex + for num in range(101): + assert (num in random_vertices) == undirected_graph.contains_vertex(num) + assert (num in random_vertices) == directed_graph.contains_vertex(num) + + def test_add_vertices(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # build empty graphs + undirected_graph: GraphAdjacencyMatrix = GraphAdjacencyMatrix( + vertices=[], edges=[], directed=False + ) + directed_graph: GraphAdjacencyMatrix = GraphAdjacencyMatrix( + vertices=[], edges=[], directed=True + ) + + # run add_vertex + for num in random_vertices: + undirected_graph.add_vertex(num) + + for num in random_vertices: + directed_graph.add_vertex(num) + + # test add_vertex worked + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + def test_remove_vertices(self) -> None: + random_vertices: list[int] = random.sample(range(101), 20) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=True + ) + + # test remove_vertex worked + for num in random_vertices: + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, num + ) + + undirected_graph.remove_vertex(num) + directed_graph.remove_vertex(num) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, num + ) + + def test_add_and_remove_vertices_repeatedly(self) -> None: + random_vertices1: list[int] = random.sample(range(51), 20) + random_vertices2: list[int] = random.sample(range(51, 101), 20) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyMatrix( + vertices=random_vertices1, edges=[], directed=False + ) + directed_graph = GraphAdjacencyMatrix( + vertices=random_vertices1, edges=[], directed=True + ) + + # test adding and removing vertices + for i, _ in enumerate(random_vertices1): + undirected_graph.add_vertex(random_vertices2[i]) + directed_graph.add_vertex(random_vertices2[i]) + + self.__assert_graph_vertex_exists_check( + undirected_graph, directed_graph, random_vertices2[i] + ) + + undirected_graph.remove_vertex(random_vertices1[i]) + directed_graph.remove_vertex(random_vertices1[i]) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, random_vertices1[i] + ) + + # remove all vertices + for i, _ in enumerate(random_vertices1): + undirected_graph.remove_vertex(random_vertices2[i]) + directed_graph.remove_vertex(random_vertices2[i]) + + self.__assert_graph_vertex_does_not_exist_check( + undirected_graph, directed_graph, random_vertices2[i] + ) + + def test_contains_edge(self) -> None: + # generate graphs and graph input + vertex_count = 20 + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(vertex_count, 0, 100, 4) + + # generate all possible edges for testing + all_possible_edges: list[list[int]] = [] + for i in range(vertex_count - 1): + for j in range(i + 1, vertex_count): + all_possible_edges.append([random_vertices[i], random_vertices[j]]) + all_possible_edges.append([random_vertices[j], random_vertices[i]]) + + # test contains_edge function + for edge in all_possible_edges: + if edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + elif [edge[1], edge[0]] in random_edges: + # since this edge exists for undirected but the reverse may + # not exist for directed + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, [edge[1], edge[0]] + ) + else: + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, edge + ) + + def test_add_edge(self) -> None: + # generate graph input + random_vertices: list[int] = random.sample(range(101), 15) + random_edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + + # build graphs WITHOUT edges + undirected_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=False + ) + directed_graph = GraphAdjacencyMatrix( + vertices=random_vertices, edges=[], directed=True + ) + + # run and test add_edge + for edge in random_edges: + undirected_graph.add_edge(edge[0], edge[1]) + directed_graph.add_edge(edge[0], edge[1]) + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + + def test_remove_edge(self) -> None: + # generate graph input and graphs + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # run and test remove_edge + for edge in random_edges: + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, edge + ) + undirected_graph.remove_edge(edge[0], edge[1]) + directed_graph.remove_edge(edge[0], edge[1]) + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, edge + ) + + def test_add_and_remove_edges_repeatedly(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + # make some more edge options! + more_random_edges: list[list[int]] = [] + + while len(more_random_edges) != len(random_edges): + edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + for edge in edges: + if len(more_random_edges) == len(random_edges): + break + elif edge not in more_random_edges and edge not in random_edges: + more_random_edges.append(edge) + + for i, _ in enumerate(random_edges): + undirected_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1]) + directed_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1]) + + self.__assert_graph_edge_exists_check( + undirected_graph, directed_graph, more_random_edges[i] + ) + + undirected_graph.remove_edge(random_edges[i][0], random_edges[i][1]) + directed_graph.remove_edge(random_edges[i][0], random_edges[i][1]) + + self.__assert_graph_edge_does_not_exist_check( + undirected_graph, directed_graph, random_edges[i] + ) + + def test_add_vertex_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for vertex in random_vertices: + with pytest.raises(ValueError): + undirected_graph.add_vertex(vertex) + with pytest.raises(ValueError): + directed_graph.add_vertex(vertex) + + def test_remove_vertex_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for i in range(101): + if i not in random_vertices: + with pytest.raises(ValueError): + undirected_graph.remove_vertex(i) + with pytest.raises(ValueError): + directed_graph.remove_vertex(i) + + def test_add_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for edge in random_edges: + with pytest.raises(ValueError): + undirected_graph.add_edge(edge[0], edge[1]) + with pytest.raises(ValueError): + directed_graph.add_edge(edge[0], edge[1]) + + def test_remove_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + more_random_edges: list[list[int]] = [] + + while len(more_random_edges) != len(random_edges): + edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4) + for edge in edges: + if len(more_random_edges) == len(random_edges): + break + elif edge not in more_random_edges and edge not in random_edges: + more_random_edges.append(edge) + + for edge in more_random_edges: + with pytest.raises(ValueError): + undirected_graph.remove_edge(edge[0], edge[1]) + with pytest.raises(ValueError): + directed_graph.remove_edge(edge[0], edge[1]) + + def test_contains_edge_exception_check(self) -> None: + ( + undirected_graph, + directed_graph, + random_vertices, + random_edges, + ) = self.__generate_graphs(20, 0, 100, 4) + + for vertex in random_vertices: + with pytest.raises(ValueError): + undirected_graph.contains_edge(vertex, 102) + with pytest.raises(ValueError): + directed_graph.contains_edge(vertex, 102) + + with pytest.raises(ValueError): + undirected_graph.contains_edge(103, 102) + with pytest.raises(ValueError): + directed_graph.contains_edge(103, 102) + + +if __name__ == "__main__": + unittest.main() diff --git a/graphs/graph_list.py b/graphs/graph_list.py index e871f3b8a9d6..6563cbb76132 100644 --- a/graphs/graph_list.py +++ b/graphs/graph_list.py @@ -120,29 +120,29 @@ def add_edge( else: self.adj_list[source_vertex] = [destination_vertex] self.adj_list[destination_vertex] = [source_vertex] - else: # For directed graphs - # if both source vertex and destination vertex are present in adjacency - # list, add destination vertex to source vertex list of adjacent vertices. - if source_vertex in self.adj_list and destination_vertex in self.adj_list: - self.adj_list[source_vertex].append(destination_vertex) - # if only source vertex is present in adjacency list, add destination - # vertex to source vertex list of adjacent vertices and create a new vertex - # with destination vertex as key, which has no adjacent vertex - elif source_vertex in self.adj_list: - self.adj_list[source_vertex].append(destination_vertex) - self.adj_list[destination_vertex] = [] - # if only destination vertex is present in adjacency list, create a new - # vertex with source vertex as key and assign a list containing destination - # vertex as first adjacent vertex - elif destination_vertex in self.adj_list: - self.adj_list[source_vertex] = [destination_vertex] - # if both source vertex and destination vertex are not present in adjacency - # list, create a new vertex with source vertex as key and a list containing - # destination vertex as it's first adjacent vertex. Then create a new vertex - # with destination vertex as key, which has no adjacent vertex - else: - self.adj_list[source_vertex] = [destination_vertex] - self.adj_list[destination_vertex] = [] + # For directed graphs + # if both source vertex and destination vertex are present in adjacency + # list, add destination vertex to source vertex list of adjacent vertices. + elif source_vertex in self.adj_list and destination_vertex in self.adj_list: + self.adj_list[source_vertex].append(destination_vertex) + # if only source vertex is present in adjacency list, add destination + # vertex to source vertex list of adjacent vertices and create a new vertex + # with destination vertex as key, which has no adjacent vertex + elif source_vertex in self.adj_list: + self.adj_list[source_vertex].append(destination_vertex) + self.adj_list[destination_vertex] = [] + # if only destination vertex is present in adjacency list, create a new + # vertex with source vertex as key and assign a list containing destination + # vertex as first adjacent vertex + elif destination_vertex in self.adj_list: + self.adj_list[source_vertex] = [destination_vertex] + # if both source vertex and destination vertex are not present in adjacency + # list, create a new vertex with source vertex as key and a list containing + # destination vertex as it's first adjacent vertex. Then create a new vertex + # with destination vertex as key, which has no adjacent vertex + else: + self.adj_list[source_vertex] = [destination_vertex] + self.adj_list[destination_vertex] = [] return self diff --git a/graphs/graph_matrix.py b/graphs/graph_matrix.py deleted file mode 100644 index 4adc6c0bb93b..000000000000 --- a/graphs/graph_matrix.py +++ /dev/null @@ -1,24 +0,0 @@ -class Graph: - def __init__(self, vertex): - self.vertex = vertex - self.graph = [[0] * vertex for i in range(vertex)] - - def add_edge(self, u, v): - self.graph[u - 1][v - 1] = 1 - self.graph[v - 1][u - 1] = 1 - - def show(self): - for i in self.graph: - for j in i: - print(j, end=" ") - print(" ") - - -g = Graph(100) - -g.add_edge(1, 4) -g.add_edge(4, 2) -g.add_edge(4, 5) -g.add_edge(2, 5) -g.add_edge(5, 3) -g.show() diff --git a/graphs/graphs_floyd_warshall.py b/graphs/graphs_floyd_warshall.py index 56cf8b9e382b..aaed9ac5df8b 100644 --- a/graphs/graphs_floyd_warshall.py +++ b/graphs/graphs_floyd_warshall.py @@ -1,7 +1,7 @@ # floyd_warshall.py """ - The problem is to find the shortest distance between all pairs of vertices in a - weighted directed graph that can have negative edge weights. +The problem is to find the shortest distance between all pairs of vertices in a +weighted directed graph that can have negative edge weights. """ diff --git a/graphs/greedy_best_first.py b/graphs/greedy_best_first.py index d49e65b9d814..bb3160047e34 100644 --- a/graphs/greedy_best_first.py +++ b/graphs/greedy_best_first.py @@ -6,14 +6,32 @@ Path = list[tuple[int, int]] -grid = [ - [0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0], # 0 are free path whereas 1's are obstacles - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0], - [1, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0], +# 0's are free path whereas 1's are obstacles +TEST_GRIDS = [ + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [1, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + ], + [ + [0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 1, 0, 0], + [0, 1, 0, 0, 1, 0, 0], + [1, 0, 0, 1, 1, 0, 1], + [0, 0, 0, 0, 0, 0, 0], + ], + [ + [0, 0, 1, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + ], ] delta = ([-1, 0], [0, -1], [1, 0], [0, 1]) # up, left, down, right @@ -58,17 +76,21 @@ def calculate_heuristic(self) -> float: The heuristic here is the Manhattan Distance Could elaborate to offer more than one choice """ - dy = abs(self.pos_x - self.goal_x) - dx = abs(self.pos_y - self.goal_y) + dx = abs(self.pos_x - self.goal_x) + dy = abs(self.pos_y - self.goal_y) return dx + dy def __lt__(self, other) -> bool: return self.f_cost < other.f_cost + def __eq__(self, other) -> bool: + return self.pos == other.pos + class GreedyBestFirst: """ - >>> gbf = GreedyBestFirst((0, 0), (len(grid) - 1, len(grid[0]) - 1)) + >>> grid = TEST_GRIDS[2] + >>> gbf = GreedyBestFirst(grid, (0, 0), (len(grid) - 1, len(grid[0]) - 1)) >>> [x.pos for x in gbf.get_successors(gbf.start)] [(1, 0), (0, 1)] >>> (gbf.start.pos_y + delta[3][0], gbf.start.pos_x + delta[3][1]) @@ -78,11 +100,14 @@ class GreedyBestFirst: >>> gbf.retrace_path(gbf.start) [(0, 0)] >>> gbf.search() # doctest: +NORMALIZE_WHITESPACE - [(0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (4, 1), (5, 1), (6, 1), - (6, 2), (6, 3), (5, 3), (5, 4), (5, 5), (6, 5), (6, 6)] + [(0, 0), (1, 0), (2, 0), (2, 1), (3, 1), (4, 1), (4, 2), (4, 3), + (4, 4)] """ - def __init__(self, start: tuple[int, int], goal: tuple[int, int]): + def __init__( + self, grid: list[list[int]], start: tuple[int, int], goal: tuple[int, int] + ): + self.grid = grid self.start = Node(start[1], start[0], goal[1], goal[0], 0, None) self.target = Node(goal[1], goal[0], goal[1], goal[0], 99999, None) @@ -114,14 +139,6 @@ def search(self) -> Path | None: if child_node not in self.open_nodes: self.open_nodes.append(child_node) - else: - # retrieve the best current path - better_node = self.open_nodes.pop(self.open_nodes.index(child_node)) - - if child_node.g_cost < better_node.g_cost: - self.open_nodes.append(child_node) - else: - self.open_nodes.append(better_node) if not self.reached: return [self.start.pos] @@ -131,28 +148,22 @@ def get_successors(self, parent: Node) -> list[Node]: """ Returns a list of successors (both in the grid and free spaces) """ - successors = [] - for action in delta: - pos_x = parent.pos_x + action[1] - pos_y = parent.pos_y + action[0] - - if not (0 <= pos_x <= len(grid[0]) - 1 and 0 <= pos_y <= len(grid) - 1): - continue - - if grid[pos_y][pos_x] != 0: - continue - - successors.append( - Node( - pos_x, - pos_y, - self.target.pos_y, - self.target.pos_x, - parent.g_cost + 1, - parent, - ) + return [ + Node( + pos_x, + pos_y, + self.target.pos_x, + self.target.pos_y, + parent.g_cost + 1, + parent, + ) + for action in delta + if ( + 0 <= (pos_x := parent.pos_x + action[1]) < len(self.grid[0]) + and 0 <= (pos_y := parent.pos_y + action[0]) < len(self.grid) + and self.grid[pos_y][pos_x] == 0 ) - return successors + ] def retrace_path(self, node: Node | None) -> Path: """ @@ -168,18 +179,21 @@ def retrace_path(self, node: Node | None) -> Path: if __name__ == "__main__": - init = (0, 0) - goal = (len(grid) - 1, len(grid[0]) - 1) - for elem in grid: - print(elem) - - print("------") - - greedy_bf = GreedyBestFirst(init, goal) - path = greedy_bf.search() - if path: - for pos_x, pos_y in path: - grid[pos_x][pos_y] = 2 + for idx, grid in enumerate(TEST_GRIDS): + print(f"==grid-{idx + 1}==") + init = (0, 0) + goal = (len(grid) - 1, len(grid[0]) - 1) for elem in grid: print(elem) + + print("------") + + greedy_bf = GreedyBestFirst(grid, init, goal) + path = greedy_bf.search() + if path: + for pos_x, pos_y in path: + grid[pos_x][pos_y] = 2 + + for elem in grid: + print(elem) diff --git a/graphs/kahns_algorithm_long.py b/graphs/kahns_algorithm_long.py index 63cbeb909a8a..1f16b90c0745 100644 --- a/graphs/kahns_algorithm_long.py +++ b/graphs/kahns_algorithm_long.py @@ -17,8 +17,7 @@ def longest_distance(graph): for x in graph[vertex]: indegree[x] -= 1 - if long_dist[vertex] + 1 > long_dist[x]: - long_dist[x] = long_dist[vertex] + 1 + long_dist[x] = max(long_dist[x], long_dist[vertex] + 1) if indegree[x] == 0: queue.append(x) diff --git a/graphs/kahns_algorithm_topo.py b/graphs/kahns_algorithm_topo.py index b1260bd5bd9b..c956cf9f48fd 100644 --- a/graphs/kahns_algorithm_topo.py +++ b/graphs/kahns_algorithm_topo.py @@ -1,36 +1,61 @@ -def topological_sort(graph): +def topological_sort(graph: dict[int, list[int]]) -> list[int] | None: """ - Kahn's Algorithm is used to find Topological ordering of Directed Acyclic Graph - using BFS + Perform topological sorting of a Directed Acyclic Graph (DAG) + using Kahn's Algorithm via Breadth-First Search (BFS). + + Topological sorting is a linear ordering of vertices in a graph such that for + every directed edge u → v, vertex u comes before vertex v in the ordering. + + Parameters: + graph: Adjacency list representing the directed graph where keys are + vertices, and values are lists of adjacent vertices. + + Returns: + The topologically sorted order of vertices if the graph is a DAG. + Returns None if the graph contains a cycle. + + Example: + >>> graph = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []} + >>> topological_sort(graph) + [0, 1, 2, 3, 4, 5] + + >>> graph_with_cycle = {0: [1], 1: [2], 2: [0]} + >>> topological_sort(graph_with_cycle) """ + indegree = [0] * len(graph) queue = [] - topo = [] - cnt = 0 + topo_order = [] + processed_vertices_count = 0 + # Calculate the indegree of each vertex for values in graph.values(): for i in values: indegree[i] += 1 + # Add all vertices with 0 indegree to the queue for i in range(len(indegree)): if indegree[i] == 0: queue.append(i) + # Perform BFS while queue: vertex = queue.pop(0) - cnt += 1 - topo.append(vertex) - for x in graph[vertex]: - indegree[x] -= 1 - if indegree[x] == 0: - queue.append(x) - - if cnt != len(graph): - print("Cycle exists") - else: - print(topo) - - -# Adjacency List of Graph -graph = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []} -topological_sort(graph) + processed_vertices_count += 1 + topo_order.append(vertex) + + # Traverse neighbors + for neighbor in graph[vertex]: + indegree[neighbor] -= 1 + if indegree[neighbor] == 0: + queue.append(neighbor) + + if processed_vertices_count != len(graph): + return None # no topological ordering exists due to cycle + return topo_order # valid topological ordering + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphs/lanczos_eigenvectors.py b/graphs/lanczos_eigenvectors.py new file mode 100644 index 000000000000..581a81a1127f --- /dev/null +++ b/graphs/lanczos_eigenvectors.py @@ -0,0 +1,206 @@ +""" +Lanczos Method for Finding Eigenvalues and Eigenvectors of a Graph. + +This module demonstrates the Lanczos method to approximate the largest eigenvalues +and corresponding eigenvectors of a symmetric matrix represented as a graph's +adjacency list. The method efficiently handles large, sparse matrices by converting +the graph to a tridiagonal matrix, whose eigenvalues and eigenvectors are then +computed. + +Key Functions: +- `find_lanczos_eigenvectors`: Computes the k largest eigenvalues and vectors. +- `lanczos_iteration`: Constructs the tridiagonal matrix and orthonormal basis vectors. +- `multiply_matrix_vector`: Multiplies an adjacency list graph with a vector. + +Complexity: +- Time: O(k * n), where k is the number of eigenvalues and n is the matrix size. +- Space: O(n), due to sparse representation and tridiagonal matrix structure. + +Further Reading: +- Lanczos Algorithm: https://en.wikipedia.org/wiki/Lanczos_algorithm +- Eigenvector Centrality: https://en.wikipedia.org/wiki/Eigenvector_centrality + +Example Usage: +Given a graph represented by an adjacency list, the `find_lanczos_eigenvectors` +function returns the largest eigenvalues and eigenvectors. This can be used to +analyze graph centrality. +""" + +import numpy as np + + +def validate_adjacency_list(graph: list[list[int | None]]) -> None: + """Validates the adjacency list format for the graph. + + Args: + graph: A list of lists where each sublist contains the neighbors of a node. + + Raises: + ValueError: If the graph is not a list of lists, or if any node has + invalid neighbors (e.g., out-of-range or non-integer values). + + >>> validate_adjacency_list([[1, 2], [0], [0, 1]]) + >>> validate_adjacency_list([[]]) # No neighbors, valid case + >>> validate_adjacency_list([[1], [2], [-1]]) # Invalid neighbor + Traceback (most recent call last): + ... + ValueError: Invalid neighbor -1 in node 2 adjacency list. + """ + if not isinstance(graph, list): + raise ValueError("Graph should be a list of lists.") + + for node_index, neighbors in enumerate(graph): + if not isinstance(neighbors, list): + no_neighbors_message: str = ( + f"Node {node_index} should have a list of neighbors." + ) + raise ValueError(no_neighbors_message) + for neighbor_index in neighbors: + if ( + not isinstance(neighbor_index, int) + or neighbor_index < 0 + or neighbor_index >= len(graph) + ): + invalid_neighbor_message: str = ( + f"Invalid neighbor {neighbor_index} in node {node_index} " + f"adjacency list." + ) + raise ValueError(invalid_neighbor_message) + + +def lanczos_iteration( + graph: list[list[int | None]], num_eigenvectors: int +) -> tuple[np.ndarray, np.ndarray]: + """Constructs the tridiagonal matrix and orthonormal basis vectors using the + Lanczos method. + + Args: + graph: The graph represented as a list of adjacency lists. + num_eigenvectors: The number of largest eigenvalues and eigenvectors + to approximate. + + Returns: + A tuple containing: + - tridiagonal_matrix: A (num_eigenvectors x num_eigenvectors) symmetric + matrix. + - orthonormal_basis: A (num_nodes x num_eigenvectors) matrix of orthonormal + basis vectors. + + Raises: + ValueError: If num_eigenvectors is less than 1 or greater than the number of + nodes. + + >>> graph = [[1, 2], [0, 2], [0, 1]] + >>> T, Q = lanczos_iteration(graph, 2) + >>> T.shape == (2, 2) and Q.shape == (3, 2) + True + """ + num_nodes: int = len(graph) + if not (1 <= num_eigenvectors <= num_nodes): + raise ValueError( + "Number of eigenvectors must be between 1 and the number of " + "nodes in the graph." + ) + + orthonormal_basis: np.ndarray = np.zeros((num_nodes, num_eigenvectors)) + tridiagonal_matrix: np.ndarray = np.zeros((num_eigenvectors, num_eigenvectors)) + + rng = np.random.default_rng() + initial_vector: np.ndarray = rng.random(num_nodes) + initial_vector /= np.sqrt(np.dot(initial_vector, initial_vector)) + orthonormal_basis[:, 0] = initial_vector + + prev_beta: float = 0.0 + for iter_index in range(num_eigenvectors): + result_vector: np.ndarray = multiply_matrix_vector( + graph, orthonormal_basis[:, iter_index] + ) + if iter_index > 0: + result_vector -= prev_beta * orthonormal_basis[:, iter_index - 1] + alpha_value: float = np.dot(orthonormal_basis[:, iter_index], result_vector) + result_vector -= alpha_value * orthonormal_basis[:, iter_index] + + prev_beta = np.sqrt(np.dot(result_vector, result_vector)) + if iter_index < num_eigenvectors - 1 and prev_beta > 1e-10: + orthonormal_basis[:, iter_index + 1] = result_vector / prev_beta + tridiagonal_matrix[iter_index, iter_index] = alpha_value + if iter_index < num_eigenvectors - 1: + tridiagonal_matrix[iter_index, iter_index + 1] = prev_beta + tridiagonal_matrix[iter_index + 1, iter_index] = prev_beta + return tridiagonal_matrix, orthonormal_basis + + +def multiply_matrix_vector( + graph: list[list[int | None]], vector: np.ndarray +) -> np.ndarray: + """Performs multiplication of a graph's adjacency list representation with a vector. + + Args: + graph: The adjacency list of the graph. + vector: A 1D numpy array representing the vector to multiply. + + Returns: + A numpy array representing the product of the adjacency list and the vector. + + Raises: + ValueError: If the vector's length does not match the number of nodes in the + graph. + + >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([1, 1, 1])) + array([2., 2., 2.]) + >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([0, 1, 0])) + array([1., 0., 1.]) + """ + num_nodes: int = len(graph) + if vector.shape[0] != num_nodes: + raise ValueError("Vector length must match the number of nodes in the graph.") + + result: np.ndarray = np.zeros(num_nodes) + for node_index, neighbors in enumerate(graph): + for neighbor_index in neighbors: + result[node_index] += vector[neighbor_index] + return result + + +def find_lanczos_eigenvectors( + graph: list[list[int | None]], num_eigenvectors: int +) -> tuple[np.ndarray, np.ndarray]: + """Computes the largest eigenvalues and their corresponding eigenvectors using the + Lanczos method. + + Args: + graph: The graph as a list of adjacency lists. + num_eigenvectors: Number of largest eigenvalues and eigenvectors to compute. + + Returns: + A tuple containing: + - eigenvalues: 1D array of the largest eigenvalues in descending order. + - eigenvectors: 2D array where each column is an eigenvector corresponding + to an eigenvalue. + + Raises: + ValueError: If the graph format is invalid or num_eigenvectors is out of bounds. + + >>> eigenvalues, eigenvectors = find_lanczos_eigenvectors( + ... [[1, 2], [0, 2], [0, 1]], 2 + ... ) + >>> len(eigenvalues) == 2 and eigenvectors.shape[1] == 2 + True + """ + validate_adjacency_list(graph) + tridiagonal_matrix, orthonormal_basis = lanczos_iteration(graph, num_eigenvectors) + eigenvalues, eigenvectors = np.linalg.eigh(tridiagonal_matrix) + return eigenvalues[::-1], np.dot(orthonormal_basis, eigenvectors[:, ::-1]) + + +def main() -> None: + """ + Main driver function for testing the implementation with doctests. + """ + import doctest + + doctest.testmod() + + +if __name__ == "__main__": + main() diff --git a/graphs/minimum_spanning_tree_boruvka.py b/graphs/minimum_spanning_tree_boruvka.py index 663d8e26cfad..f234d65ab765 100644 --- a/graphs/minimum_spanning_tree_boruvka.py +++ b/graphs/minimum_spanning_tree_boruvka.py @@ -144,6 +144,7 @@ def union(self, item1, item2): self.rank[root1] += 1 self.parent[root2] = root1 return root1 + return None @staticmethod def boruvka_mst(graph): @@ -184,12 +185,12 @@ def boruvka_mst(graph): if cheap_edge[set2] == -1 or cheap_edge[set2][2] > weight: cheap_edge[set2] = [head, tail, weight] - for vertex in cheap_edge: - if cheap_edge[vertex] != -1: - head, tail, weight = cheap_edge[vertex] + for head_tail_weight in cheap_edge.values(): + if head_tail_weight != -1: + head, tail, weight = head_tail_weight if union_find.find(head) != union_find.find(tail): union_find.union(head, tail) - mst_edges.append(cheap_edge[vertex]) + mst_edges.append(head_tail_weight) num_components = num_components - 1 mst = Graph.build(edges=mst_edges) return mst diff --git a/graphs/minimum_spanning_tree_prims.py b/graphs/minimum_spanning_tree_prims.py index f577866f0da6..d0b45d7ef139 100644 --- a/graphs/minimum_spanning_tree_prims.py +++ b/graphs/minimum_spanning_tree_prims.py @@ -16,13 +16,12 @@ def top_to_bottom(self, heap, start, size, positions): if start > size // 2 - 1: return else: - if 2 * start + 2 >= size: + if 2 * start + 2 >= size: # noqa: SIM114 + smallest_child = 2 * start + 1 + elif heap[2 * start + 1] < heap[2 * start + 2]: smallest_child = 2 * start + 1 else: - if heap[2 * start + 1] < heap[2 * start + 2]: - smallest_child = 2 * start + 1 - else: - smallest_child = 2 * start + 2 + smallest_child = 2 * start + 2 if heap[smallest_child] < heap[start]: temp, temp1 = heap[smallest_child], positions[smallest_child] heap[smallest_child], positions[smallest_child] = ( @@ -44,10 +43,7 @@ def bottom_to_top(self, val, index, heap, position): temp = position[index] while index != 0: - if index % 2 == 0: - parent = int((index - 2) / 2) - else: - parent = int((index - 1) / 2) + parent = int((index - 2) / 2) if index % 2 == 0 else int((index - 1) / 2) if val < heap[parent]: heap[index] = heap[parent] diff --git a/graphs/minimum_spanning_tree_prims2.py b/graphs/minimum_spanning_tree_prims2.py index 707be783d087..cc918f81dfe8 100644 --- a/graphs/minimum_spanning_tree_prims2.py +++ b/graphs/minimum_spanning_tree_prims2.py @@ -6,6 +6,7 @@ at a time, from an arbitrary starting vertex, at each step adding the cheapest possible connection from the tree to another vertex. """ + from __future__ import annotations from sys import maxsize @@ -135,14 +136,14 @@ def _bubble_up(self, elem: T) -> None: # only] curr_pos = self.position_map[elem] if curr_pos == 0: - return + return None parent_position = get_parent_position(curr_pos) _, weight = self.heap[curr_pos] _, parent_weight = self.heap[parent_position] if parent_weight > weight: self._swap_nodes(parent_position, curr_pos) return self._bubble_up(elem) - return + return None def _bubble_down(self, elem: T) -> None: # Place a node at the proper position (downward movement) [to be used @@ -154,24 +155,22 @@ def _bubble_down(self, elem: T) -> None: if child_left_position < self.elements and child_right_position < self.elements: _, child_left_weight = self.heap[child_left_position] _, child_right_weight = self.heap[child_right_position] - if child_right_weight < child_left_weight: - if child_right_weight < weight: - self._swap_nodes(child_right_position, curr_pos) - return self._bubble_down(elem) + if child_right_weight < child_left_weight and child_right_weight < weight: + self._swap_nodes(child_right_position, curr_pos) + return self._bubble_down(elem) if child_left_position < self.elements: _, child_left_weight = self.heap[child_left_position] if child_left_weight < weight: self._swap_nodes(child_left_position, curr_pos) return self._bubble_down(elem) else: - return + return None if child_right_position < self.elements: _, child_right_weight = self.heap[child_right_position] if child_right_weight < weight: self._swap_nodes(child_right_position, curr_pos) return self._bubble_down(elem) - else: - return + return None def _swap_nodes(self, node1_pos: int, node2_pos: int) -> None: # Swap the nodes at the given positions diff --git a/graphs/multi_heuristic_astar.py b/graphs/multi_heuristic_astar.py index 0a18ede6ed41..38b07e1ca675 100644 --- a/graphs/multi_heuristic_astar.py +++ b/graphs/multi_heuristic_astar.py @@ -79,7 +79,7 @@ def key(start: TPos, i: int, goal: TPos, g_function: dict[TPos, float]): def do_something(back_pointer, goal, start): - grid = np.chararray((n, n)) + grid = np.char.chararray((n, n)) for i in range(n): for j in range(n): grid[i][j] = "*" @@ -123,9 +123,7 @@ def do_something(back_pointer, goal, start): def valid(p: TPos): if p[0] < 0 or p[0] > n - 1: return False - if p[1] < 0 or p[1] > n - 1: - return False - return True + return not (p[1] < 0 or p[1] > n - 1) def expand_state( @@ -270,24 +268,23 @@ def multi_a_star(start: TPos, goal: TPos, n_heuristic: int): back_pointer, ) close_list_inad.append(get_s) + elif g_function[goal] <= open_list[0].minkey(): + if g_function[goal] < float("inf"): + do_something(back_pointer, goal, start) else: - if g_function[goal] <= open_list[0].minkey(): - if g_function[goal] < float("inf"): - do_something(back_pointer, goal, start) - else: - get_s = open_list[0].top_show() - visited.add(get_s) - expand_state( - get_s, - 0, - visited, - g_function, - close_list_anchor, - close_list_inad, - open_list, - back_pointer, - ) - close_list_anchor.append(get_s) + get_s = open_list[0].top_show() + visited.add(get_s) + expand_state( + get_s, + 0, + visited, + g_function, + close_list_anchor, + close_list_inad, + open_list, + back_pointer, + ) + close_list_anchor.append(get_s) print("No path found to goal") print() for i in range(n - 1, -1, -1): diff --git a/graphs/page_rank.py b/graphs/page_rank.py index b9e4c4a72a93..c0ce3a94c76b 100644 --- a/graphs/page_rank.py +++ b/graphs/page_rank.py @@ -1,6 +1,7 @@ """ Author: https://github.com/bhushan-borole """ + """ The input graph for the algorithm is: diff --git a/graphs/prim.py b/graphs/prim.py index 6cb1a6def359..5b3ce04441ec 100644 --- a/graphs/prim.py +++ b/graphs/prim.py @@ -1,8 +1,8 @@ """Prim's Algorithm. - Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm. +Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm. - Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm +Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm """ import heapq as hq diff --git a/graphs/strongly_connected_components.py b/graphs/strongly_connected_components.py index 325e5c1f33a3..4d4cf88035b5 100644 --- a/graphs/strongly_connected_components.py +++ b/graphs/strongly_connected_components.py @@ -38,7 +38,7 @@ def find_components( reversed_graph: dict[int, list[int]], vert: int, visited: list[bool] ) -> list[int]: """ - Use depth first search to find strongliy connected + Use depth first search to find strongly connected vertices. Now graph is reversed >>> find_components({0: [1], 1: [2], 2: [0]}, 0, 5 * [False]) [0, 1, 2] diff --git a/graphs/tarjans_scc.py b/graphs/tarjans_scc.py index 30f8ca8a204f..b4a3bd5c4c35 100644 --- a/graphs/tarjans_scc.py +++ b/graphs/tarjans_scc.py @@ -1,7 +1,7 @@ from collections import deque -def tarjan(g): +def tarjan(g: list[list[int]]) -> list[list[int]]: """ Tarjan's algo for finding strongly connected components in a directed graph @@ -19,15 +19,30 @@ def tarjan(g): Complexity: strong_connect() is called at most once for each node and has a complexity of O(|E|) as it is DFS. Therefore this has complexity O(|V| + |E|) for a graph G = (V, E) + + >>> tarjan([[2, 3, 4], [2, 3, 4], [0, 1, 3], [0, 1, 2], [1]]) + [[4, 3, 1, 2, 0]] + >>> tarjan([[], [], [], []]) + [[0], [1], [2], [3]] + >>> a = [0, 1, 2, 3, 4, 5, 4] + >>> b = [1, 0, 3, 2, 5, 4, 0] + >>> n = 7 + >>> sorted(tarjan(create_graph(n, list(zip(a, b))))) == sorted( + ... tarjan(create_graph(n, list(zip(a[::-1], b[::-1]))))) + True + >>> a = [0, 1, 2, 3, 4, 5, 6] + >>> b = [0, 1, 2, 3, 4, 5, 6] + >>> sorted(tarjan(create_graph(n, list(zip(a, b))))) + [[0], [1], [2], [3], [4], [5], [6]] """ n = len(g) - stack = deque() + stack: deque[int] = deque() on_stack = [False for _ in range(n)] index_of = [-1 for _ in range(n)] lowlink_of = index_of[:] - def strong_connect(v, index, components): + def strong_connect(v: int, index: int, components: list[list[int]]) -> int: index_of[v] = index # the number when this node is seen lowlink_of[v] = index # lowest rank node reachable from here index += 1 @@ -57,7 +72,7 @@ def strong_connect(v, index, components): components.append(component) return index - components = [] + components: list[list[int]] = [] for v in range(n): if index_of[v] == -1: strong_connect(v, 0, components) @@ -65,8 +80,16 @@ def strong_connect(v, index, components): return components -def create_graph(n, edges): - g = [[] for _ in range(n)] +def create_graph(n: int, edges: list[tuple[int, int]]) -> list[list[int]]: + """ + >>> n = 7 + >>> source = [0, 0, 1, 2, 3, 3, 4, 4, 6] + >>> target = [1, 3, 2, 0, 1, 4, 5, 6, 5] + >>> edges = list(zip(source, target)) + >>> create_graph(n, edges) + [[1, 3], [2], [0], [1, 4], [5, 6], [], [5]] + """ + g: list[list[int]] = [[] for _ in range(n)] for u, v in edges: g[u].append(v) return g @@ -77,7 +100,7 @@ def create_graph(n, edges): n_vertices = 7 source = [0, 0, 1, 2, 3, 3, 4, 4, 6] target = [1, 3, 2, 0, 1, 4, 5, 6, 5] - edges = [(u, v) for u, v in zip(source, target)] + edges = list(zip(source, target)) g = create_graph(n_vertices, edges) - assert [[5], [6], [4], [3, 2, 1, 0]] == tarjan(g) + assert tarjan(g) == [[5], [6], [4], [3, 2, 1, 0]] diff --git a/graphs/tests/__init__.py b/graphs/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/graphs/tests/test_min_spanning_tree_prim.py b/graphs/tests/test_min_spanning_tree_prim.py index 91feab28fc81..66e5706dadb1 100644 --- a/graphs/tests/test_min_spanning_tree_prim.py +++ b/graphs/tests/test_min_spanning_tree_prim.py @@ -22,12 +22,12 @@ def test_prim_successful_result(): [1, 7, 11], ] - adjancency = defaultdict(list) + adjacency = defaultdict(list) for node1, node2, cost in edges: - adjancency[node1].append([node2, cost]) - adjancency[node2].append([node1, cost]) + adjacency[node1].append([node2, cost]) + adjacency[node2].append([node1, cost]) - result = mst(adjancency) + result = mst(adjacency) expected = [ [7, 6, 1], diff --git a/greedy_methods/__init__.py b/greedy_methods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/greedy_methods/best_time_to_buy_and_sell_stock.py b/greedy_methods/best_time_to_buy_and_sell_stock.py new file mode 100644 index 000000000000..4aea19172ece --- /dev/null +++ b/greedy_methods/best_time_to_buy_and_sell_stock.py @@ -0,0 +1,42 @@ +""" +Given a list of stock prices calculate the maximum profit that can be made from a +single buy and sell of one share of stock. We only allowed to complete one buy +transaction and one sell transaction but must buy before we sell. + +Example : prices = [7, 1, 5, 3, 6, 4] +max_profit will return 5 - which is by buying at price 1 and selling at price 6. + +This problem can be solved using the concept of "GREEDY ALGORITHM". + +We iterate over the price array once, keeping track of the lowest price point +(buy) and the maximum profit we can get at each point. The greedy choice at each point +is to either buy at the current price if it's less than our current buying price, or +sell at the current price if the profit is more than our current maximum profit. +""" + + +def max_profit(prices: list[int]) -> int: + """ + >>> max_profit([7, 1, 5, 3, 6, 4]) + 5 + >>> max_profit([7, 6, 4, 3, 1]) + 0 + """ + if not prices: + return 0 + + min_price = prices[0] + max_profit: int = 0 + + for price in prices: + min_price = min(price, min_price) + max_profit = max(price - min_price, max_profit) + + return max_profit + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(max_profit([7, 1, 5, 3, 6, 4])) diff --git a/greedy_methods/fractional_cover_problem.py b/greedy_methods/fractional_cover_problem.py new file mode 100644 index 000000000000..e37c363f1db9 --- /dev/null +++ b/greedy_methods/fractional_cover_problem.py @@ -0,0 +1,102 @@ +# https://en.wikipedia.org/wiki/Set_cover_problem + +from dataclasses import dataclass +from operator import attrgetter + + +@dataclass +class Item: + weight: int + value: int + + @property + def ratio(self) -> float: + """ + Return the value-to-weight ratio for the item. + + Returns: + float: The value-to-weight ratio for the item. + + Examples: + >>> Item(10, 65).ratio + 6.5 + + >>> Item(20, 100).ratio + 5.0 + + >>> Item(30, 120).ratio + 4.0 + """ + return self.value / self.weight + + +def fractional_cover(items: list[Item], capacity: int) -> float: + """ + Solve the Fractional Cover Problem. + + Args: + items: A list of items, where each item has weight and value attributes. + capacity: The maximum weight capacity of the knapsack. + + Returns: + The maximum value that can be obtained by selecting fractions of items to cover + the knapsack's capacity. + + Raises: + ValueError: If capacity is negative. + + Examples: + >>> fractional_cover((Item(10, 60), Item(20, 100), Item(30, 120)), capacity=50) + 240.0 + + >>> fractional_cover([Item(20, 100), Item(30, 120), Item(10, 60)], capacity=25) + 135.0 + + >>> fractional_cover([Item(10, 60), Item(20, 100), Item(30, 120)], capacity=60) + 280.0 + + >>> fractional_cover(items=[Item(5, 30), Item(10, 60), Item(15, 90)], capacity=30) + 180.0 + + >>> fractional_cover(items=[], capacity=50) + 0.0 + + >>> fractional_cover(items=[Item(10, 60)], capacity=5) + 30.0 + + >>> fractional_cover(items=[Item(10, 60)], capacity=1) + 6.0 + + >>> fractional_cover(items=[Item(10, 60)], capacity=0) + 0.0 + + >>> fractional_cover(items=[Item(10, 60)], capacity=-1) + Traceback (most recent call last): + ... + ValueError: Capacity cannot be negative + """ + if capacity < 0: + raise ValueError("Capacity cannot be negative") + + total_value = 0.0 + remaining_capacity = capacity + + # Sort the items by their value-to-weight ratio in descending order + for item in sorted(items, key=attrgetter("ratio"), reverse=True): + if remaining_capacity == 0: + break + + weight_taken = min(item.weight, remaining_capacity) + total_value += weight_taken * item.ratio + remaining_capacity -= weight_taken + + return total_value + + +if __name__ == "__main__": + import doctest + + if result := doctest.testmod().failed: + print(f"{result} test(s) failed") + else: + print("All tests passed") diff --git a/greedy_methods/fractional_knapsack.py b/greedy_methods/fractional_knapsack.py index 58976d40c02b..d52b56f23569 100644 --- a/greedy_methods/fractional_knapsack.py +++ b/greedy_methods/fractional_knapsack.py @@ -6,6 +6,30 @@ def frac_knapsack(vl, wt, w, n): """ >>> frac_knapsack([60, 100, 120], [10, 20, 30], 50, 3) 240.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 10, 4) + 105.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 4) + 95.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6], 8, 4) + 60.0 + >>> frac_knapsack([10, 40, 30], [5, 4, 6, 3], 8, 4) + 60.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 0, 4) + 0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 0) + 95.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], -8, 4) + 0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, -4) + 95.0 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 800, 4) + 130 + >>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 400) + 95.0 + >>> frac_knapsack("ABCD", [5, 4, 6, 3], 8, 400) + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for /: 'str' and 'int' """ r = sorted(zip(vl, wt), key=lambda x: x[0] / x[1], reverse=True) diff --git a/greedy_methods/gas_station.py b/greedy_methods/gas_station.py new file mode 100644 index 000000000000..6391ce379329 --- /dev/null +++ b/greedy_methods/gas_station.py @@ -0,0 +1,98 @@ +""" +Task: +There are n gas stations along a circular route, where the amount of gas +at the ith station is gas_quantities[i]. + +You have a car with an unlimited gas tank and it costs costs[i] of gas +to travel from the ith station to its next (i + 1)th station. +You begin the journey with an empty tank at one of the gas stations. + +Given two integer arrays gas_quantities and costs, return the starting +gas station's index if you can travel around the circuit once +in the clockwise direction otherwise, return -1. +If there exists a solution, it is guaranteed to be unique + +Reference: https://leetcode.com/problems/gas-station/description + +Implementation notes: +First, check whether the total gas is enough to complete the journey. If not, return -1. +However, if there is enough gas, it is guaranteed that there is a valid +starting index to reach the end of the journey. +Greedily calculate the net gain (gas_quantity - cost) at each station. +If the net gain ever goes below 0 while iterating through the stations, +start checking from the next station. + +""" + +from dataclasses import dataclass + + +@dataclass +class GasStation: + gas_quantity: int + cost: int + + +def get_gas_stations( + gas_quantities: list[int], costs: list[int] +) -> tuple[GasStation, ...]: + """ + This function returns a tuple of gas stations. + + Args: + gas_quantities: Amount of gas available at each station + costs: The cost of gas required to move from one station to the next + + Returns: + A tuple of gas stations + + >>> gas_stations = get_gas_stations([1, 2, 3, 4, 5], [3, 4, 5, 1, 2]) + >>> len(gas_stations) + 5 + >>> gas_stations[0] + GasStation(gas_quantity=1, cost=3) + >>> gas_stations[-1] + GasStation(gas_quantity=5, cost=2) + """ + return tuple( + GasStation(quantity, cost) for quantity, cost in zip(gas_quantities, costs) + ) + + +def can_complete_journey(gas_stations: tuple[GasStation, ...]) -> int: + """ + This function returns the index from which to start the journey + in order to reach the end. + + Args: + gas_quantities [list]: Amount of gas available at each station + cost [list]: The cost of gas required to move from one station to the next + + Returns: + start [int]: start index needed to complete the journey + + Examples: + >>> can_complete_journey(get_gas_stations([1, 2, 3, 4, 5], [3, 4, 5, 1, 2])) + 3 + >>> can_complete_journey(get_gas_stations([2, 3, 4], [3, 4, 3])) + -1 + """ + total_gas = sum(gas_station.gas_quantity for gas_station in gas_stations) + total_cost = sum(gas_station.cost for gas_station in gas_stations) + if total_gas < total_cost: + return -1 + + start = 0 + net = 0 + for i, gas_station in enumerate(gas_stations): + net += gas_station.gas_quantity - gas_station.cost + if net < 0: + start = i + 1 + net = 0 + return start + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/greedy_coin_change.py b/greedy_methods/minimum_coin_change.py similarity index 99% rename from maths/greedy_coin_change.py rename to greedy_methods/minimum_coin_change.py index 7cf669bcb8cb..db2c381bc84a 100644 --- a/maths/greedy_coin_change.py +++ b/greedy_methods/minimum_coin_change.py @@ -81,7 +81,7 @@ def find_minimum_change(denominations: list[int], value: str) -> list[int]: ): n = int(input("Enter the number of denominations you want to add: ").strip()) - for i in range(0, n): + for i in range(n): denominations.append(int(input(f"Denomination {i}: ").strip())) value = input("Enter the change you want to make in Indian Currency: ").strip() else: diff --git a/greedy_methods/minimum_waiting_time.py b/greedy_methods/minimum_waiting_time.py new file mode 100644 index 000000000000..aaae8cf8f720 --- /dev/null +++ b/greedy_methods/minimum_waiting_time.py @@ -0,0 +1,48 @@ +""" +Calculate the minimum waiting time using a greedy algorithm. +reference: https://www.youtube.com/watch?v=Sf3eiO12eJs + +For doctests run following command: +python -m doctest -v minimum_waiting_time.py + +The minimum_waiting_time function uses a greedy algorithm to calculate the minimum +time for queries to complete. It sorts the list in non-decreasing order, calculates +the waiting time for each query by multiplying its position in the list with the +sum of all remaining query times, and returns the total waiting time. A doctest +ensures that the function produces the correct output. +""" + + +def minimum_waiting_time(queries: list[int]) -> int: + """ + This function takes a list of query times and returns the minimum waiting time + for all queries to be completed. + + Args: + queries: A list of queries measured in picoseconds + + Returns: + total_waiting_time: Minimum waiting time measured in picoseconds + + Examples: + >>> minimum_waiting_time([3, 2, 1, 2, 6]) + 17 + >>> minimum_waiting_time([3, 2, 1]) + 4 + >>> minimum_waiting_time([1, 2, 3, 4]) + 10 + >>> minimum_waiting_time([5, 5, 5, 5]) + 30 + >>> minimum_waiting_time([]) + 0 + """ + n = len(queries) + if n in (0, 1): + return 0 + return sum(query * (n - i - 1) for i, query in enumerate(sorted(queries))) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/greedy_methods/smallest_range.py b/greedy_methods/smallest_range.py new file mode 100644 index 000000000000..9adb12bf9029 --- /dev/null +++ b/greedy_methods/smallest_range.py @@ -0,0 +1,72 @@ +""" +smallest_range function takes a list of sorted integer lists and finds the smallest +range that includes at least one number from each list, using a min heap for efficiency. +""" + +from heapq import heappop, heappush +from sys import maxsize + + +def smallest_range(nums: list[list[int]]) -> list[int]: + """ + Find the smallest range from each list in nums. + + Uses min heap for efficiency. The range includes at least one number from each list. + + Args: + `nums`: List of k sorted integer lists. + + Returns: + list: Smallest range as a two-element list. + + Examples: + + >>> smallest_range([[4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]]) + [20, 24] + >>> smallest_range([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) + [1, 1] + >>> smallest_range(((1, 2, 3), (1, 2, 3), (1, 2, 3))) + [1, 1] + >>> smallest_range(((-3, -2, -1), (0, 0, 0), (1, 2, 3))) + [-1, 1] + >>> smallest_range([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + [3, 7] + >>> smallest_range([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + [0, 0] + >>> smallest_range([[], [], []]) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + + min_heap: list[tuple[int, int, int]] = [] + current_max = -maxsize - 1 + + for i, items in enumerate(nums): + heappush(min_heap, (items[0], i, 0)) + current_max = max(current_max, items[0]) + + # Initialize smallest_range with large integer values + smallest_range = [-maxsize - 1, maxsize] + + while min_heap: + current_min, list_index, element_index = heappop(min_heap) + + if current_max - current_min < smallest_range[1] - smallest_range[0]: + smallest_range = [current_min, current_max] + + if element_index == len(nums[list_index]) - 1: + break + + next_element = nums[list_index][element_index + 1] + heappush(min_heap, (next_element, list_index, element_index + 1)) + current_max = max(current_max, next_element) + + return smallest_range + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(f"{smallest_range([[1, 2, 3], [1, 2, 3], [1, 2, 3]])}") # Output: [1, 1] diff --git a/hashes/README.md b/hashes/README.md index 6df9a2fb6360..0237260eaa67 100644 --- a/hashes/README.md +++ b/hashes/README.md @@ -7,11 +7,11 @@ Unlike encryption, which is intended to protect data in transit, hashing is inte This is one of the first algorithms that has gained widespread acceptance. MD5 is hashing algorithm made by Ray Rivest that is known to suffer vulnerabilities. It was created in 1992 as the successor to MD4. Currently MD6 is in the works, but as of 2009 Rivest had removed it from NIST consideration for SHA-3. ### SHA -SHA stands for Security Hashing Algorithm and it’s probably best known as the hashing algorithm used in most SSL/TLS cipher suites. A cipher suite is a collection of ciphers and algorithms that are used for SSL/TLS connections. SHA handles the hashing aspects. SHA-1, as we mentioned earlier, is now deprecated. SHA-2 is now mandatory. SHA-2 is sometimes known has SHA-256, though variants with longer bit lengths are also available. +SHA stands for Security Hashing Algorithm and it’s probably best known as the hashing algorithm used in most SSL/TLS cipher suites. A cipher suite is a collection of ciphers and algorithms that are used for SSL/TLS connections. SHA handles the hashing aspects. SHA-1, as we mentioned earlier, is now deprecated. SHA-2 is now mandatory. SHA-2 is sometimes known as SHA-256, though variants with longer bit lengths are also available. ### SHA256 SHA 256 is a member of the SHA 2 algorithm family, under which SHA stands for Secure Hash Algorithm. It was a collaborative effort between both the NSA and NIST to implement a successor to the SHA 1 family, which was beginning to lose potency against brute force attacks. It was published in 2001. The importance of the 256 in the name refers to the final hash digest value, i.e. the hash value will remain 256 bits regardless of the size of the plaintext/cleartext. Other algorithms in the SHA family are similar to SHA 256 in some ways. ### Luhn -The Luhn algorithm, also renowned as the modulus 10 or mod 10 algorithm, is a straightforward checksum formula used to validate a wide range of identification numbers, including credit card numbers, IMEI numbers, and Canadian Social Insurance Numbers. A community of mathematicians developed the LUHN formula in the late 1960s. Companies offering credit cards quickly followed suit. Since the algorithm is in the public interest, anyone can use it. The algorithm is used by most credit cards and many government identification numbers as a simple method of differentiating valid figures from mistyped or otherwise incorrect numbers. It was created to guard against unintentional errors, not malicious attacks. \ No newline at end of file +The Luhn algorithm, also renowned as the modulus 10 or mod 10 algorithm, is a straightforward checksum formula used to validate a wide range of identification numbers, including credit card numbers, IMEI numbers, and Canadian Social Insurance Numbers. A community of mathematicians developed the LUHN formula in the late 1960s. Companies offering credit cards quickly followed suit. Since the algorithm is in the public interest, anyone can use it. The algorithm is used by most credit cards and many government identification numbers as a simple method of differentiating valid figures from mistyped or otherwise incorrect numbers. It was created to guard against unintentional errors, not malicious attacks. diff --git a/hashes/adler32.py b/hashes/adler32.py index 611ebc88b80f..38d76ab12aa0 100644 --- a/hashes/adler32.py +++ b/hashes/adler32.py @@ -1,11 +1,11 @@ """ - Adler-32 is a checksum algorithm which was invented by Mark Adler in 1995. - Compared to a cyclic redundancy check of the same length, it trades reliability for - speed (preferring the latter). - Adler-32 is more reliable than Fletcher-16, and slightly less reliable than - Fletcher-32.[2] +Adler-32 is a checksum algorithm which was invented by Mark Adler in 1995. +Compared to a cyclic redundancy check of the same length, it trades reliability for +speed (preferring the latter). +Adler-32 is more reliable than Fletcher-16, and slightly less reliable than +Fletcher-32.[2] - source: https://en.wikipedia.org/wiki/Adler-32 +source: https://en.wikipedia.org/wiki/Adler-32 """ MOD_ADLER = 65521 diff --git a/hashes/chaos_machine.py b/hashes/chaos_machine.py index 238fdb1c0634..d2fde2f5e371 100644 --- a/hashes/chaos_machine.py +++ b/hashes/chaos_machine.py @@ -53,7 +53,7 @@ def xorshift(x, y): key = machine_time % m # Evolution (Time Length) - for _ in range(0, t): + for _ in range(t): # Variables (Position + Parameters) r = params_space[key] value = buffer_space[key] diff --git a/hashes/fletcher16.py b/hashes/fletcher16.py new file mode 100644 index 000000000000..add8e185bc06 --- /dev/null +++ b/hashes/fletcher16.py @@ -0,0 +1,36 @@ +""" +The Fletcher checksum is an algorithm for computing a position-dependent +checksum devised by John G. Fletcher (1934-2012) at Lawrence Livermore Labs +in the late 1970s.[1] The objective of the Fletcher checksum was to +provide error-detection properties approaching those of a cyclic +redundancy check but with the lower computational effort associated +with summation techniques. + +Source: https://en.wikipedia.org/wiki/Fletcher%27s_checksum +""" + + +def fletcher16(text: str) -> int: + """ + Loop through every character in the data and add to two sums. + + >>> fletcher16('hello world') + 6752 + >>> fletcher16('onethousandfourhundredthirtyfour') + 28347 + >>> fletcher16('The quick brown fox jumps over the lazy dog.') + 5655 + """ + data = bytes(text, "ascii") + sum1 = 0 + sum2 = 0 + for character in data: + sum1 = (sum1 + character) % 255 + sum2 = (sum1 + sum2) % 255 + return (sum2 << 8) | sum1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/hashes/hamming_code.py b/hashes/hamming_code.py index 481a6750773a..b3095852ac51 100644 --- a/hashes/hamming_code.py +++ b/hashes/hamming_code.py @@ -4,44 +4,44 @@ # Black: True """ - * This code implement the Hamming code: - https://en.wikipedia.org/wiki/Hamming_code - In telecommunication, - Hamming codes are a family of linear error-correcting codes. Hamming - codes can detect up to two-bit errors or correct one-bit errors - without detection of uncorrected errors. By contrast, the simple - parity code cannot correct errors, and can detect only an odd number - of bits in error. Hamming codes are perfect codes, that is, they - achieve the highest possible rate for codes with their block length - and minimum distance of three. - - * the implemented code consists of: - * a function responsible for encoding the message (emitterConverter) - * return the encoded message - * a function responsible for decoding the message (receptorConverter) - * return the decoded message and a ack of data integrity - - * how to use: - to be used you must declare how many parity bits (sizePari) - you want to include in the message. - it is desired (for test purposes) to select a bit to be set - as an error. This serves to check whether the code is working correctly. - Lastly, the variable of the message/word that must be desired to be - encoded (text). - - * how this work: - declaration of variables (sizePari, be, text) - - converts the message/word (text) to binary using the - text_to_bits function - encodes the message using the rules of hamming encoding - decodes the message using the rules of hamming encoding - print the original message, the encoded message and the - decoded message - - forces an error in the coded text variable - decodes the message that was forced the error - print the original message, the encoded message, the bit changed - message and the decoded message +* This code implement the Hamming code: + https://en.wikipedia.org/wiki/Hamming_code - In telecommunication, +Hamming codes are a family of linear error-correcting codes. Hamming +codes can detect up to two-bit errors or correct one-bit errors +without detection of uncorrected errors. By contrast, the simple +parity code cannot correct errors, and can detect only an odd number +of bits in error. Hamming codes are perfect codes, that is, they +achieve the highest possible rate for codes with their block length +and minimum distance of three. + +* the implemented code consists of: + * a function responsible for encoding the message (emitterConverter) + * return the encoded message + * a function responsible for decoding the message (receptorConverter) + * return the decoded message and a ack of data integrity + +* how to use: + to be used you must declare how many parity bits (sizePari) + you want to include in the message. + it is desired (for test purposes) to select a bit to be set + as an error. This serves to check whether the code is working correctly. + Lastly, the variable of the message/word that must be desired to be + encoded (text). + +* how this work: + declaration of variables (sizePari, be, text) + + converts the message/word (text) to binary using the + text_to_bits function + encodes the message using the rules of hamming encoding + decodes the message using the rules of hamming encoding + print the original message, the encoded message and the + decoded message + + forces an error in the coded text variable + decodes the message that was forced the error + print the original message, the encoded message, the bit changed + message and the decoded message """ # Imports @@ -77,6 +77,10 @@ def emitter_converter(size_par, data): >>> emitter_converter(4, "101010111111") ['1', '1', '1', '1', '0', '1', '0', '0', '1', '0', '1', '1', '1', '1', '1', '1'] + >>> emitter_converter(5, "101010111111") + Traceback (most recent call last): + ... + ValueError: size of parity don't match with size of data """ if size_par + len(data) <= 2**size_par - (len(data) - 1): raise ValueError("size of parity don't match with size of data") @@ -119,24 +123,21 @@ def emitter_converter(size_par, data): # Bit counter one for a given parity cont_bo = 0 # counter to control the loop reading - cont_loop = 0 - for x in data_ord: + for cont_loop, x in enumerate(data_ord): if x is not None: try: aux = (bin_pos[cont_loop])[-1 * (bp)] except IndexError: aux = "0" - if aux == "1": - if x == "1": - cont_bo += 1 - cont_loop += 1 + if aux == "1" and x == "1": + cont_bo += 1 parity.append(cont_bo % 2) qtd_bp += 1 # Mount the message cont_bp = 0 # parity bit counter - for x in range(0, size_par + len(data)): + for x in range(size_par + len(data)): if data_ord[x] is None: data_out.append(str(parity[cont_bp])) cont_bp += 1 @@ -161,10 +162,10 @@ def receptor_converter(size_par, data): parity_received = [] data_output = [] - for x in range(1, len(data) + 1): + for i, item in enumerate(data, 1): # Performs a template of bit positions - who should be given, # and who should be parity - if qtd_bp < size_par and (np.log(x) / np.log(2)).is_integer(): + if qtd_bp < size_par and (np.log(i) / np.log(2)).is_integer(): data_out_gab.append("P") qtd_bp = qtd_bp + 1 else: @@ -172,10 +173,9 @@ def receptor_converter(size_par, data): # Sorts the data to the new output size if data_out_gab[-1] == "D": - data_output.append(data[cont_data]) + data_output.append(item) else: - parity_received.append(data[cont_data]) - cont_data += 1 + parity_received.append(item) # -----------calculates the parity with the data data_out = [] @@ -212,9 +212,7 @@ def receptor_converter(size_par, data): for bp in range(1, size_par + 1): # Bit counter one for a certain parity cont_bo = 0 - # Counter to control loop reading - cont_loop = 0 - for x in data_ord: + for cont_loop, x in enumerate(data_ord): if x is not None: try: aux = (bin_pos[cont_loop])[-1 * (bp)] @@ -222,14 +220,13 @@ def receptor_converter(size_par, data): aux = "0" if aux == "1" and x == "1": cont_bo += 1 - cont_loop += 1 parity.append(str(cont_bo % 2)) qtd_bp += 1 # Mount the message cont_bp = 0 # Parity bit counter - for x in range(0, size_par + len(data_output)): + for x in range(size_par + len(data_output)): if data_ord[x] is None: data_out.append(str(parity[cont_bp])) cont_bp += 1 diff --git a/hashes/luhn.py b/hashes/luhn.py index bb77fd05c556..a29bf39e3d82 100644 --- a/hashes/luhn.py +++ b/hashes/luhn.py @@ -1,4 +1,5 @@ -""" Luhn Algorithm """ +"""Luhn Algorithm""" + from __future__ import annotations diff --git a/hashes/md5.py b/hashes/md5.py index 2020bf2e53bf..f9d802ff0308 100644 --- a/hashes/md5.py +++ b/hashes/md5.py @@ -1,91 +1,223 @@ -import math +""" +The MD5 algorithm is a hash function that's commonly used as a checksum to +detect data corruption. The algorithm works by processing a given message in +blocks of 512 bits, padding the message as needed. It uses the blocks to operate +a 128-bit state and performs a total of 64 such operations. Note that all values +are little-endian, so inputs are converted as needed. +Although MD5 was used as a cryptographic hash function in the past, it's since +been cracked, so it shouldn't be used for security purposes. -def rearrange(bit_string_32): - """[summary] - Regroups the given binary string. +For more info, see https://en.wikipedia.org/wiki/MD5 +""" + +from collections.abc import Generator +from math import sin + + +def to_little_endian(string_32: bytes) -> bytes: + """ + Converts the given string to little-endian in groups of 8 chars. Arguments: - bitString32 {[string]} -- [32 bit binary] + string_32 {[string]} -- [32-char string] Raises: - ValueError -- [if the given string not are 32 bit binary string] + ValueError -- [input is not 32 char] Returns: - [string] -- [32 bit binary string] - >>> rearrange('1234567890abcdfghijklmnopqrstuvw') - 'pqrstuvwhijklmno90abcdfg12345678' + 32-char little-endian string + >>> to_little_endian(b'1234567890abcdfghijklmnopqrstuvw') + b'pqrstuvwhijklmno90abcdfg12345678' + >>> to_little_endian(b'1234567890') + Traceback (most recent call last): + ... + ValueError: Input must be of length 32 """ + if len(string_32) != 32: + raise ValueError("Input must be of length 32") - if len(bit_string_32) != 32: - raise ValueError("Need length 32") - new_string = "" + little_endian = b"" for i in [3, 2, 1, 0]: - new_string += bit_string_32[8 * i : 8 * i + 8] - return new_string + little_endian += string_32[8 * i : 8 * i + 8] + return little_endian + + +def reformat_hex(i: int) -> bytes: + """ + Converts the given non-negative integer to hex string. + Example: Suppose the input is the following: + i = 1234 -def reformat_hex(i): - """[summary] - Converts the given integer into 8-digit hex number. + The input is 0x000004d2 in hex, so the little-endian hex string is + "d2040000". Arguments: - i {[int]} -- [integer] + i {[int]} -- [integer] + + Raises: + ValueError -- [input is negative] + + Returns: + 8-char little-endian hex string + + >>> reformat_hex(1234) + b'd2040000' >>> reformat_hex(666) - '9a020000' + b'9a020000' + >>> reformat_hex(0) + b'00000000' + >>> reformat_hex(1234567890) + b'd2029649' + >>> reformat_hex(1234567890987654321) + b'b11c6cb1' + >>> reformat_hex(-1) + Traceback (most recent call last): + ... + ValueError: Input must be non-negative """ + if i < 0: + raise ValueError("Input must be non-negative") - hexrep = format(i, "08x") - thing = "" - for i in [3, 2, 1, 0]: - thing += hexrep[2 * i : 2 * i + 2] - return thing + hex_rep = format(i, "08x")[-8:] + little_endian_hex = b"" + for j in [3, 2, 1, 0]: + little_endian_hex += hex_rep[2 * j : 2 * j + 2].encode("utf-8") + return little_endian_hex -def pad(bit_string): - """[summary] - Fills up the binary string to a 512 bit binary string +def preprocess(message: bytes) -> bytes: + """ + Preprocesses the message string: + - Convert message to bit string + - Pad bit string to a multiple of 512 chars: + - Append a 1 + - Append 0's until length = 448 (mod 512) + - Append length of original message (64 chars) + + Example: Suppose the input is the following: + message = "a" + + The message bit string is "01100001", which is 8 bits long. Thus, the + bit string needs 439 bits of padding so that + (bit_string + "1" + padding) = 448 (mod 512). + The message length is "000010000...0" in 64-bit little-endian binary. + The combined bit string is then 512 bits long. Arguments: - bitString {[string]} -- [binary string] + message {[string]} -- [message string] Returns: - [string] -- [binary string] + processed bit string padded to a multiple of 512 chars + + >>> preprocess(b"a") == (b"01100001" + b"1" + + ... (b"0" * 439) + b"00001000" + (b"0" * 56)) + True + >>> preprocess(b"") == b"1" + (b"0" * 447) + (b"0" * 64) + True """ - start_length = len(bit_string) - bit_string += "1" + bit_string = b"" + for char in message: + bit_string += format(char, "08b").encode("utf-8") + start_len = format(len(bit_string), "064b").encode("utf-8") + + # Pad bit_string to a multiple of 512 chars + bit_string += b"1" while len(bit_string) % 512 != 448: - bit_string += "0" - last_part = format(start_length, "064b") - bit_string += rearrange(last_part[32:]) + rearrange(last_part[:32]) + bit_string += b"0" + bit_string += to_little_endian(start_len[32:]) + to_little_endian(start_len[:32]) + return bit_string -def get_block(bit_string): - """[summary] - Iterator: - Returns by each call a list of length 16 with the 32 bit - integer blocks. +def get_block_words(bit_string: bytes) -> Generator[list[int]]: + """ + Splits bit string into blocks of 512 chars and yields each block as a list + of 32-bit words + + Example: Suppose the input is the following: + bit_string = + "000000000...0" + # 0x00 (32 bits, padded to the right) + "000000010...0" + # 0x01 (32 bits, padded to the right) + "000000100...0" + # 0x02 (32 bits, padded to the right) + "000000110...0" + # 0x03 (32 bits, padded to the right) + ... + "000011110...0" # 0x0a (32 bits, padded to the right) + + Then len(bit_string) == 512, so there'll be 1 block. The block is split + into 32-bit words, and each word is converted to little endian. The + first word is interpreted as 0 in decimal, the second word is + interpreted as 1 in decimal, etc. + + Thus, block_words == [[0, 1, 2, 3, ..., 15]]. Arguments: - bit_string {[string]} -- [binary string >= 512] + bit_string {[string]} -- [bit string with multiple of 512 as length] + + Raises: + ValueError -- [length of bit string isn't multiple of 512] + + Yields: + a list of 16 32-bit words + + >>> test_string = ("".join(format(n << 24, "032b") for n in range(16)) + ... .encode("utf-8")) + >>> list(get_block_words(test_string)) + [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]] + >>> list(get_block_words(test_string * 4)) == [list(range(16))] * 4 + True + >>> list(get_block_words(b"1" * 512)) == [[4294967295] * 16] + True + >>> list(get_block_words(b"")) + [] + >>> list(get_block_words(b"1111")) + Traceback (most recent call last): + ... + ValueError: Input must have length that's a multiple of 512 """ + if len(bit_string) % 512 != 0: + raise ValueError("Input must have length that's a multiple of 512") - curr_pos = 0 - while curr_pos < len(bit_string): - curr_part = bit_string[curr_pos : curr_pos + 512] - my_splits = [] - for i in range(16): - my_splits.append(int(rearrange(curr_part[32 * i : 32 * i + 32]), 2)) - yield my_splits - curr_pos += 512 + for pos in range(0, len(bit_string), 512): + block = bit_string[pos : pos + 512] + block_words = [] + for i in range(0, 512, 32): + block_words.append(int(to_little_endian(block[i : i + 32]), 2)) + yield block_words -def not32(i): +def not_32(i: int) -> int: """ - >>> not32(34) + Perform bitwise NOT on given int. + + Arguments: + i {[int]} -- [given int] + + Raises: + ValueError -- [input is negative] + + Returns: + Result of bitwise NOT on i + + >>> not_32(34) 4294967261 + >>> not_32(1234) + 4294966061 + >>> not_32(4294966061) + 1234 + >>> not_32(0) + 4294967295 + >>> not_32(1) + 4294967294 + >>> not_32(-1) + Traceback (most recent call last): + ... + ValueError: Input must be non-negative """ + if i < 0: + raise ValueError("Input must be non-negative") + i_str = format(i, "032b") new_str = "" for c in i_str: @@ -93,35 +225,114 @@ def not32(i): return int(new_str, 2) -def sum32(a, b): +def sum_32(a: int, b: int) -> int: + """ + Add two numbers as 32-bit ints. + + Arguments: + a {[int]} -- [first given int] + b {[int]} -- [second given int] + + Returns: + (a + b) as an unsigned 32-bit int + + >>> sum_32(1, 1) + 2 + >>> sum_32(2, 3) + 5 + >>> sum_32(0, 0) + 0 + >>> sum_32(-1, -1) + 4294967294 + >>> sum_32(4294967295, 1) + 0 + """ return (a + b) % 2**32 -def leftrot32(i, s): - return (i << s) ^ (i >> (32 - s)) +def left_rotate_32(i: int, shift: int) -> int: + """ + Rotate the bits of a given int left by a given amount. + + Arguments: + i {[int]} -- [given int] + shift {[int]} -- [shift amount] + + Raises: + ValueError -- [either given int or shift is negative] + Returns: + `i` rotated to the left by `shift` bits + + >>> left_rotate_32(1234, 1) + 2468 + >>> left_rotate_32(1111, 4) + 17776 + >>> left_rotate_32(2147483648, 1) + 1 + >>> left_rotate_32(2147483648, 3) + 4 + >>> left_rotate_32(4294967295, 4) + 4294967295 + >>> left_rotate_32(1234, 0) + 1234 + >>> left_rotate_32(0, 0) + 0 + >>> left_rotate_32(-1, 0) + Traceback (most recent call last): + ... + ValueError: Input must be non-negative + >>> left_rotate_32(0, -1) + Traceback (most recent call last): + ... + ValueError: Shift must be non-negative + """ + if i < 0: + raise ValueError("Input must be non-negative") + if shift < 0: + raise ValueError("Shift must be non-negative") + return ((i << shift) ^ (i >> (32 - shift))) % 2**32 + + +def md5_me(message: bytes) -> bytes: + """ + Returns the 32-char MD5 hash of a given message. -def md5me(test_string): - """[summary] - Returns a 32-bit hash code of the string 'testString' + Reference: https://en.wikipedia.org/wiki/MD5#Algorithm Arguments: - testString {[string]} -- [message] + message {[string]} -- [message] + + Returns: + 32-char MD5 hash string + + >>> md5_me(b"") + b'd41d8cd98f00b204e9800998ecf8427e' + >>> md5_me(b"The quick brown fox jumps over the lazy dog") + b'9e107d9d372bb6826bd81d3542a419d6' + >>> md5_me(b"The quick brown fox jumps over the lazy dog.") + b'e4d909c290d0fb1ca068ffaddf22cbd0' + + >>> import hashlib + >>> from string import ascii_letters + >>> msgs = [b"", ascii_letters.encode("utf-8"), "Üñîçø∂é".encode("utf-8"), + ... b"The quick brown fox jumps over the lazy dog."] + >>> all(md5_me(msg) == hashlib.md5(msg).hexdigest().encode("utf-8") for msg in msgs) + True """ - bs = "" - for i in test_string: - bs += format(ord(i), "08b") - bs = pad(bs) + # Convert to bit string, add padding and append message length + bit_string = preprocess(message) - tvals = [int(2**32 * abs(math.sin(i + 1))) for i in range(64)] + added_consts = [int(2**32 * abs(sin(i + 1))) for i in range(64)] + # Starting states a0 = 0x67452301 b0 = 0xEFCDAB89 c0 = 0x98BADCFE d0 = 0x10325476 - s = [ + shift_amounts = [ 7, 12, 17, @@ -188,51 +399,46 @@ def md5me(test_string): 21, ] - for m in get_block(bs): + # Process bit string in chunks, each with 16 32-char words + for block_words in get_block_words(bit_string): a = a0 b = b0 c = c0 d = d0 + + # Hash current chunk for i in range(64): if i <= 15: - # f = (B & C) | (not32(B) & D) + # f = (b & c) | (not_32(b) & d) # Alternate definition for f f = d ^ (b & (c ^ d)) g = i elif i <= 31: - # f = (D & B) | (not32(D) & C) + # f = (d & b) | (not_32(d) & c) # Alternate definition for f f = c ^ (d & (b ^ c)) g = (5 * i + 1) % 16 elif i <= 47: f = b ^ c ^ d g = (3 * i + 5) % 16 else: - f = c ^ (b | not32(d)) + f = c ^ (b | not_32(d)) g = (7 * i) % 16 - dtemp = d + f = (f + a + added_consts[i] + block_words[g]) % 2**32 + a = d d = c c = b - b = sum32(b, leftrot32((a + f + tvals[i] + m[g]) % 2**32, s[i])) - a = dtemp - a0 = sum32(a0, a) - b0 = sum32(b0, b) - c0 = sum32(c0, c) - d0 = sum32(d0, d) + b = sum_32(b, left_rotate_32(f, shift_amounts[i])) + + # Add hashed chunk to running total + a0 = sum_32(a0, a) + b0 = sum_32(b0, b) + c0 = sum_32(c0, c) + d0 = sum_32(d0, d) digest = reformat_hex(a0) + reformat_hex(b0) + reformat_hex(c0) + reformat_hex(d0) return digest -def test(): - assert md5me("") == "d41d8cd98f00b204e9800998ecf8427e" - assert ( - md5me("The quick brown fox jumps over the lazy dog") - == "9e107d9d372bb6826bd81d3542a419d6" - ) - print("Success.") - - if __name__ == "__main__": - test() import doctest doctest.testmod() diff --git a/hashes/sdbm.py b/hashes/sdbm.py index a5432874ba7d..a5abc6f3185b 100644 --- a/hashes/sdbm.py +++ b/hashes/sdbm.py @@ -1,21 +1,21 @@ """ - This algorithm was created for sdbm (a public-domain reimplementation of ndbm) - database library. - It was found to do well in scrambling bits, causing better distribution of the keys - and fewer splits. - It also happens to be a good general hashing function with good distribution. - The actual function (pseudo code) is: - for i in i..len(str): - hash(i) = hash(i - 1) * 65599 + str[i]; +This algorithm was created for sdbm (a public-domain reimplementation of ndbm) +database library. +It was found to do well in scrambling bits, causing better distribution of the keys +and fewer splits. +It also happens to be a good general hashing function with good distribution. +The actual function (pseudo code) is: + for i in i..len(str): + hash(i) = hash(i - 1) * 65599 + str[i]; - What is included below is the faster version used in gawk. [there is even a faster, - duff-device version] - The magic constant 65599 was picked out of thin air while experimenting with - different constants. - It turns out to be a prime. - This is one of the algorithms used in berkeley db (see sleepycat) and elsewhere. +What is included below is the faster version used in gawk. [there is even a faster, +duff-device version] +The magic constant 65599 was picked out of thin air while experimenting with +different constants. +It turns out to be a prime. +This is one of the algorithms used in berkeley db (see sleepycat) and elsewhere. - source: http://www.cse.yorku.ca/~oz/hash.html +source: http://www.cse.yorku.ca/~oz/hash.html """ diff --git a/hashes/sha1.py b/hashes/sha1.py index b19e0cfafea3..75a1423e9b5f 100644 --- a/hashes/sha1.py +++ b/hashes/sha1.py @@ -1,48 +1,50 @@ """ -Demonstrates implementation of SHA1 Hash function in a Python class and gives utilities -to find hash of string or hash of text from a file. +Implementation of the SHA1 hash function and gives utilities to find hash of string or +hash of text from a file. Also contains a Test class to verify that the generated hash +matches what is returned by the hashlib library + Usage: python sha1.py --string "Hello World!!" python sha1.py --file "hello_world.txt" When run without any arguments, it prints the hash of the string "Hello World!! Welcome to Cryptography" -Also contains a Test class to verify that the generated Hash is same as that -returned by the hashlib library -SHA1 hash or SHA1 sum of a string is a cryptographic function which means it is easy +SHA1 hash or SHA1 sum of a string is a cryptographic function, which means it is easy to calculate forwards but extremely difficult to calculate backwards. What this means -is, you can easily calculate the hash of a string, but it is extremely difficult to -know the original string if you have its hash. This property is useful to communicate -securely, send encrypted messages and is very useful in payment systems, blockchain -and cryptocurrency etc. -The Algorithm as described in the reference: +is you can easily calculate the hash of a string, but it is extremely difficult to know +the original string if you have its hash. This property is useful for communicating +securely, send encrypted messages and is very useful in payment systems, blockchain and +cryptocurrency etc. + +The algorithm as described in the reference: First we start with a message. The message is padded and the length of the message is added to the end. It is then split into blocks of 512 bits or 64 bytes. The blocks are then processed one at a time. Each block must be expanded and compressed. -The value after each compression is added to a 160bit buffer called the current hash -state. After the last block is processed the current hash state is returned as +The value after each compression is added to a 160-bit buffer called the current hash +state. After the last block is processed, the current hash state is returned as the final hash. + Reference: https://deadhacker.com/2006/02/21/sha-1-illustrated/ """ + import argparse import hashlib # hashlib is only used inside the Test class import struct -import unittest class SHA1Hash: """ - Class to contain the entire pipeline for SHA1 Hashing Algorithm + Class to contain the entire pipeline for SHA1 hashing algorithm >>> SHA1Hash(bytes('Allan', 'utf-8')).final_hash() '872af2d8ac3d8695387e7c804bf0e02c18df9e6e' """ def __init__(self, data): """ - Inititates the variables data and h. h is a list of 5 8-digit Hexadecimal + Initiates the variables data and h. h is a list of 5 8-digit hexadecimal numbers corresponding to (1732584193, 4023233417, 2562383102, 271733878, 3285377520) respectively. We will start with this as a message digest. 0x is how you write - Hexadecimal numbers in Python + hexadecimal numbers in Python """ self.data = data self.h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] @@ -91,14 +93,14 @@ def final_hash(self): For each block, the variable h that was initialized is copied to a,b,c,d,e and these 5 variables a,b,c,d,e undergo several changes. After all the blocks are processed, these 5 variables are pairwise added to h ie a to h[0], b to h[1] - and so on. This h becomes our final hash which is returned. + and so on. This h becomes our final hash which is returned. """ self.padded_data = self.padding() self.blocks = self.split_blocks() for block in self.blocks: expanded_block = self.expand_block(block) a, b, c, d, e = self.h - for i in range(0, 80): + for i in range(80): if 0 <= i < 20: f = (b & c) | ((~b) & d) k = 0x5A827999 @@ -125,23 +127,18 @@ def final_hash(self): self.h[3] + d & 0xFFFFFFFF, self.h[4] + e & 0xFFFFFFFF, ) - return "%08x%08x%08x%08x%08x" % tuple(self.h) + return ("{:08x}" * 5).format(*self.h) -class SHA1HashTest(unittest.TestCase): - """ - Test class for the SHA1Hash class. Inherits the TestCase class from unittest - """ - - def testMatchHashes(self): # noqa: N802 - msg = bytes("Test String", "utf-8") - self.assertEqual(SHA1Hash(msg).final_hash(), hashlib.sha1(msg).hexdigest()) +def test_sha1_hash(): + msg = b"Test String" + assert SHA1Hash(msg).final_hash() == hashlib.sha1(msg).hexdigest() # noqa: S324 def main(): """ Provides option 'string' or 'file' to take input and prints the calculated SHA1 - hash. unittest.main() has been commented because we probably don't want to run + hash. unittest.main() has been commented out because we probably don't want to run the test each time. """ # unittest.main() diff --git a/hashes/sha256.py b/hashes/sha256.py index 98f7c096e3b6..bcc83edca480 100644 --- a/hashes/sha256.py +++ b/hashes/sha256.py @@ -138,7 +138,7 @@ def final_hash(self) -> None: a, b, c, d, e, f, g, h = self.hashes - for index in range(0, 64): + for index in range(64): if index > 15: # modify the zero-ed indexes at the end of the array s0 = ( @@ -203,7 +203,7 @@ def test_match_hashes(self) -> None: import hashlib msg = bytes("Test String", "utf-8") - self.assertEqual(SHA256(msg).hash, hashlib.sha256(msg).hexdigest()) + assert SHA256(msg).hash == hashlib.sha256(msg).hexdigest() def main() -> None: diff --git a/index.md b/index.md new file mode 100644 index 000000000000..134520cb94aa --- /dev/null +++ b/index.md @@ -0,0 +1,10 @@ +# TheAlgorithms/Python +```{toctree} +:maxdepth: 2 +:caption: index.md + + +CONTRIBUTING.md +README.md +LICENSE.md +``` diff --git a/knapsack/knapsack.py b/knapsack/knapsack.py index 18a36c3bcdda..bb507be1ba3c 100644 --- a/knapsack/knapsack.py +++ b/knapsack/knapsack.py @@ -1,6 +1,7 @@ -""" A naive recursive implementation of 0-1 Knapsack Problem - https://en.wikipedia.org/wiki/Knapsack_problem +"""A naive recursive implementation of 0-1 Knapsack Problem +https://en.wikipedia.org/wiki/Knapsack_problem """ + from __future__ import annotations diff --git a/knapsack/tests/test_greedy_knapsack.py b/knapsack/tests/test_greedy_knapsack.py index b7b62d5d80b4..e6a40084109e 100644 --- a/knapsack/tests/test_greedy_knapsack.py +++ b/knapsack/tests/test_greedy_knapsack.py @@ -1,5 +1,7 @@ import unittest +import pytest + from knapsack import greedy_knapsack as kp @@ -16,7 +18,7 @@ def test_sorted(self): profit = [10, 20, 30, 40, 50, 60] weight = [2, 4, 6, 8, 10, 12] max_weight = 100 - self.assertEqual(kp.calc_profit(profit, weight, max_weight), 210) + assert kp.calc_profit(profit, weight, max_weight) == 210 def test_negative_max_weight(self): """ @@ -26,7 +28,7 @@ def test_negative_max_weight(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = -15 - self.assertRaisesRegex(ValueError, "max_weight must greater than zero.") + pytest.raises(ValueError, match="max_weight must greater than zero.") def test_negative_profit_value(self): """ @@ -36,7 +38,7 @@ def test_negative_profit_value(self): # profit = [10, -20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = 15 - self.assertRaisesRegex(ValueError, "Weight can not be negative.") + pytest.raises(ValueError, match="Weight can not be negative.") def test_negative_weight_value(self): """ @@ -46,7 +48,7 @@ def test_negative_weight_value(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, -4, 6, -8, 10, 12] # max_weight = 15 - self.assertRaisesRegex(ValueError, "Profit can not be negative.") + pytest.raises(ValueError, match="Profit can not be negative.") def test_null_max_weight(self): """ @@ -56,7 +58,7 @@ def test_null_max_weight(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = null - self.assertRaisesRegex(ValueError, "max_weight must greater than zero.") + pytest.raises(ValueError, match="max_weight must greater than zero.") def test_unequal_list_length(self): """ @@ -66,9 +68,7 @@ def test_unequal_list_length(self): # profit = [10, 20, 30, 40, 50] # weight = [2, 4, 6, 8, 10, 12] # max_weight = 100 - self.assertRaisesRegex( - IndexError, "The length of profit and weight must be same." - ) + pytest.raises(IndexError, match="The length of profit and weight must be same.") if __name__ == "__main__": diff --git a/knapsack/tests/test_knapsack.py b/knapsack/tests/test_knapsack.py index 248855fbce53..7bfb8780627b 100644 --- a/knapsack/tests/test_knapsack.py +++ b/knapsack/tests/test_knapsack.py @@ -6,6 +6,7 @@ This file contains the test-suite for the knapsack problem. """ + import unittest from knapsack import knapsack as k @@ -20,12 +21,12 @@ def test_base_case(self): val = [0] w = [0] c = len(val) - self.assertEqual(k.knapsack(cap, w, val, c), 0) + assert k.knapsack(cap, w, val, c) == 0 val = [60] w = [10] c = len(val) - self.assertEqual(k.knapsack(cap, w, val, c), 0) + assert k.knapsack(cap, w, val, c) == 0 def test_easy_case(self): """ @@ -35,7 +36,7 @@ def test_easy_case(self): val = [1, 2, 3] w = [3, 2, 1] c = len(val) - self.assertEqual(k.knapsack(cap, w, val, c), 5) + assert k.knapsack(cap, w, val, c) == 5 def test_knapsack(self): """ @@ -45,7 +46,7 @@ def test_knapsack(self): val = [60, 100, 120] w = [10, 20, 30] c = len(val) - self.assertEqual(k.knapsack(cap, w, val, c), 220) + assert k.knapsack(cap, w, val, c) == 220 if __name__ == "__main__": diff --git a/arithmetic_analysis/gaussian_elimination.py b/linear_algebra/gaussian_elimination.py similarity index 74% rename from arithmetic_analysis/gaussian_elimination.py rename to linear_algebra/gaussian_elimination.py index f0f20af8e417..6f4075b710fd 100644 --- a/arithmetic_analysis/gaussian_elimination.py +++ b/linear_algebra/gaussian_elimination.py @@ -1,9 +1,8 @@ """ -Gaussian elimination method for solving a system of linear equations. -Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination +| Gaussian elimination method for solving a system of linear equations. +| Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination """ - import numpy as np from numpy import float64 from numpy.typing import NDArray @@ -14,12 +13,17 @@ def retroactive_resolution( ) -> NDArray[float64]: """ This function performs a retroactive linear system resolution - for triangular matrix + for triangular matrix Examples: - 2x1 + 2x2 - 1x3 = 5 2x1 + 2x2 = -1 - 0x1 - 2x2 - 1x3 = -7 0x1 - 2x2 = -1 - 0x1 + 0x2 + 5x3 = 15 + 1. + * 2x1 + 2x2 - 1x3 = 5 + * 0x1 - 2x2 - 1x3 = -7 + * 0x1 + 0x2 + 5x3 = 15 + 2. + * 2x1 + 2x2 = -1 + * 0x1 - 2x2 = -1 + >>> gaussian_elimination([[2, 2, -1], [0, -2, -1], [0, 0, 5]], [[5], [-7], [15]]) array([[2.], [2.], @@ -33,11 +37,8 @@ def retroactive_resolution( x: NDArray[float64] = np.zeros((rows, 1), dtype=float) for row in reversed(range(rows)): - total = 0 - for col in range(row + 1, columns): - total += coefficients[row, col] * x[col] - - x[row, 0] = (vector[row] - total) / coefficients[row, row] + total = np.dot(coefficients[row, row + 1 :], x[row + 1 :]) + x[row, 0] = (vector[row][0] - total[0]) / coefficients[row, row] return x @@ -49,9 +50,14 @@ def gaussian_elimination( This function performs Gaussian elimination method Examples: - 1x1 - 4x2 - 2x3 = -2 1x1 + 2x2 = 5 - 5x1 + 2x2 - 2x3 = -3 5x1 + 2x2 = 5 - 1x1 - 1x2 + 0x3 = 4 + 1. + * 1x1 - 4x2 - 2x3 = -2 + * 5x1 + 2x2 - 2x3 = -3 + * 1x1 - 1x2 + 0x3 = 4 + 2. + * 1x1 + 2x2 = 5 + * 5x1 + 2x2 = 5 + >>> gaussian_elimination([[1, -4, -2], [5, 2, -2], [1, -1, 0]], [[-2], [-3], [4]]) array([[ 2.3 ], [-1.7 ], diff --git a/arithmetic_analysis/jacobi_iteration_method.py b/linear_algebra/jacobi_iteration_method.py similarity index 66% rename from arithmetic_analysis/jacobi_iteration_method.py rename to linear_algebra/jacobi_iteration_method.py index fe506a94a65d..2cc9c103018b 100644 --- a/arithmetic_analysis/jacobi_iteration_method.py +++ b/linear_algebra/jacobi_iteration_method.py @@ -1,167 +1,204 @@ -""" -Jacobi Iteration Method - https://en.wikipedia.org/wiki/Jacobi_method -""" -from __future__ import annotations - -import numpy as np -from numpy import float64 -from numpy.typing import NDArray - - -# Method to find solution of system of linear equations -def jacobi_iteration_method( - coefficient_matrix: NDArray[float64], - constant_matrix: NDArray[float64], - init_val: list[int], - iterations: int, -) -> list[float]: - """ - Jacobi Iteration Method: - An iterative algorithm to determine the solutions of strictly diagonally dominant - system of linear equations - - 4x1 + x2 + x3 = 2 - x1 + 5x2 + 2x3 = -6 - x1 + 2x2 + 4x3 = -4 - - x_init = [0.5, -0.5 , -0.5] - - Examples: - - >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) - >>> constant = np.array([[2], [-6], [-4]]) - >>> init_val = [0.5, -0.5, -0.5] - >>> iterations = 3 - >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) - [0.909375, -1.14375, -0.7484375] - - - >>> coefficient = np.array([[4, 1, 1], [1, 5, 2]]) - >>> constant = np.array([[2], [-6], [-4]]) - >>> init_val = [0.5, -0.5, -0.5] - >>> iterations = 3 - >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) - Traceback (most recent call last): - ... - ValueError: Coefficient matrix dimensions must be nxn but received 2x3 - - >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) - >>> constant = np.array([[2], [-6]]) - >>> init_val = [0.5, -0.5, -0.5] - >>> iterations = 3 - >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) - Traceback (most recent call last): - ... - ValueError: Coefficient and constant matrices dimensions must be nxn and nx1 but - received 3x3 and 2x1 - - >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) - >>> constant = np.array([[2], [-6], [-4]]) - >>> init_val = [0.5, -0.5] - >>> iterations = 3 - >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) - Traceback (most recent call last): - ... - ValueError: Number of initial values must be equal to number of rows in coefficient - matrix but received 2 and 3 - - >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) - >>> constant = np.array([[2], [-6], [-4]]) - >>> init_val = [0.5, -0.5, -0.5] - >>> iterations = 0 - >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) - Traceback (most recent call last): - ... - ValueError: Iterations must be at least 1 - """ - - rows1, cols1 = coefficient_matrix.shape - rows2, cols2 = constant_matrix.shape - - if rows1 != cols1: - raise ValueError( - f"Coefficient matrix dimensions must be nxn but received {rows1}x{cols1}" - ) - - if cols2 != 1: - raise ValueError(f"Constant matrix must be nx1 but received {rows2}x{cols2}") - - if rows1 != rows2: - raise ValueError( - f"""Coefficient and constant matrices dimensions must be nxn and nx1 but - received {rows1}x{cols1} and {rows2}x{cols2}""" - ) - - if len(init_val) != rows1: - raise ValueError( - f"""Number of initial values must be equal to number of rows in coefficient - matrix but received {len(init_val)} and {rows1}""" - ) - - if iterations <= 0: - raise ValueError("Iterations must be at least 1") - - table: NDArray[float64] = np.concatenate( - (coefficient_matrix, constant_matrix), axis=1 - ) - - rows, cols = table.shape - - strictly_diagonally_dominant(table) - - # Iterates the whole matrix for given number of times - for _ in range(iterations): - new_val = [] - for row in range(rows): - temp = 0 - for col in range(cols): - if col == row: - denom = table[row][col] - elif col == cols - 1: - val = table[row][col] - else: - temp += (-1) * table[row][col] * init_val[col] - temp = (temp + val) / denom - new_val.append(temp) - init_val = new_val - - return [float(i) for i in new_val] - - -# Checks if the given matrix is strictly diagonally dominant -def strictly_diagonally_dominant(table: NDArray[float64]) -> bool: - """ - >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 4, -4]]) - >>> strictly_diagonally_dominant(table) - True - - >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 3, -4]]) - >>> strictly_diagonally_dominant(table) - Traceback (most recent call last): - ... - ValueError: Coefficient matrix is not strictly diagonally dominant - """ - - rows, cols = table.shape - - is_diagonally_dominant = True - - for i in range(0, rows): - total = 0 - for j in range(0, cols - 1): - if i == j: - continue - else: - total += table[i][j] - - if table[i][i] <= total: - raise ValueError("Coefficient matrix is not strictly diagonally dominant") - - return is_diagonally_dominant - - -# Test Cases -if __name__ == "__main__": - import doctest - - doctest.testmod() +""" +Jacobi Iteration Method - https://en.wikipedia.org/wiki/Jacobi_method +""" + +from __future__ import annotations + +import numpy as np +from numpy import float64 +from numpy.typing import NDArray + + +# Method to find solution of system of linear equations +def jacobi_iteration_method( + coefficient_matrix: NDArray[float64], + constant_matrix: NDArray[float64], + init_val: list[float], + iterations: int, +) -> list[float]: + """ + Jacobi Iteration Method: + An iterative algorithm to determine the solutions of strictly diagonally dominant + system of linear equations + + 4x1 + x2 + x3 = 2 + x1 + 5x2 + 2x3 = -6 + x1 + 2x2 + 4x3 = -4 + + x_init = [0.5, -0.5 , -0.5] + + Examples: + + >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) + >>> constant = np.array([[2], [-6], [-4]]) + >>> init_val = [0.5, -0.5, -0.5] + >>> iterations = 3 + >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) + [0.909375, -1.14375, -0.7484375] + + + >>> coefficient = np.array([[4, 1, 1], [1, 5, 2]]) + >>> constant = np.array([[2], [-6], [-4]]) + >>> init_val = [0.5, -0.5, -0.5] + >>> iterations = 3 + >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) + Traceback (most recent call last): + ... + ValueError: Coefficient matrix dimensions must be nxn but received 2x3 + + >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) + >>> constant = np.array([[2], [-6]]) + >>> init_val = [0.5, -0.5, -0.5] + >>> iterations = 3 + >>> jacobi_iteration_method( + ... coefficient, constant, init_val, iterations + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Coefficient and constant matrices dimensions must be nxn and nx1 but + received 3x3 and 2x1 + + >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) + >>> constant = np.array([[2], [-6], [-4]]) + >>> init_val = [0.5, -0.5] + >>> iterations = 3 + >>> jacobi_iteration_method( + ... coefficient, constant, init_val, iterations + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Number of initial values must be equal to number of rows in coefficient + matrix but received 2 and 3 + + >>> coefficient = np.array([[4, 1, 1], [1, 5, 2], [1, 2, 4]]) + >>> constant = np.array([[2], [-6], [-4]]) + >>> init_val = [0.5, -0.5, -0.5] + >>> iterations = 0 + >>> jacobi_iteration_method(coefficient, constant, init_val, iterations) + Traceback (most recent call last): + ... + ValueError: Iterations must be at least 1 + """ + + rows1, cols1 = coefficient_matrix.shape + rows2, cols2 = constant_matrix.shape + + if rows1 != cols1: + msg = f"Coefficient matrix dimensions must be nxn but received {rows1}x{cols1}" + raise ValueError(msg) + + if cols2 != 1: + msg = f"Constant matrix must be nx1 but received {rows2}x{cols2}" + raise ValueError(msg) + + if rows1 != rows2: + msg = ( + "Coefficient and constant matrices dimensions must be nxn and nx1 but " + f"received {rows1}x{cols1} and {rows2}x{cols2}" + ) + raise ValueError(msg) + + if len(init_val) != rows1: + msg = ( + "Number of initial values must be equal to number of rows in coefficient " + f"matrix but received {len(init_val)} and {rows1}" + ) + raise ValueError(msg) + + if iterations <= 0: + raise ValueError("Iterations must be at least 1") + + table: NDArray[float64] = np.concatenate( + (coefficient_matrix, constant_matrix), axis=1 + ) + + rows, cols = table.shape + + strictly_diagonally_dominant(table) + + """ + # Iterates the whole matrix for given number of times + for _ in range(iterations): + new_val = [] + for row in range(rows): + temp = 0 + for col in range(cols): + if col == row: + denom = table[row][col] + elif col == cols - 1: + val = table[row][col] + else: + temp += (-1) * table[row][col] * init_val[col] + temp = (temp + val) / denom + new_val.append(temp) + init_val = new_val + """ + + # denominator - a list of values along the diagonal + denominator = np.diag(coefficient_matrix) + + # val_last - values of the last column of the table array + val_last = table[:, -1] + + # masks - boolean mask of all strings without diagonal + # elements array coefficient_matrix + masks = ~np.eye(coefficient_matrix.shape[0], dtype=bool) + + # no_diagonals - coefficient_matrix array values without diagonal elements + no_diagonals = coefficient_matrix[masks].reshape(-1, rows - 1) + + # Here we get 'i_col' - these are the column numbers, for each row + # without diagonal elements, except for the last column. + i_row, i_col = np.where(masks) + ind = i_col.reshape(-1, rows - 1) + + #'i_col' is converted to a two-dimensional list 'ind', which will be + # used to make selections from 'init_val' ('arr' array see below). + + # Iterates the whole matrix for given number of times + for _ in range(iterations): + arr = np.take(init_val, ind) + sum_product_rows = np.sum((-1) * no_diagonals * arr, axis=1) + new_val = (sum_product_rows + val_last) / denominator + init_val = new_val + + return new_val.tolist() + + +# Checks if the given matrix is strictly diagonally dominant +def strictly_diagonally_dominant(table: NDArray[float64]) -> bool: + """ + >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 4, -4]]) + >>> strictly_diagonally_dominant(table) + True + + >>> table = np.array([[4, 1, 1, 2], [1, 5, 2, -6], [1, 2, 3, -4]]) + >>> strictly_diagonally_dominant(table) + Traceback (most recent call last): + ... + ValueError: Coefficient matrix is not strictly diagonally dominant + """ + + rows, cols = table.shape + + is_diagonally_dominant = True + + for i in range(rows): + total = 0 + for j in range(cols - 1): + if i == j: + continue + else: + total += table[i][j] + + if table[i][i] <= total: + raise ValueError("Coefficient matrix is not strictly diagonally dominant") + + return is_diagonally_dominant + + +# Test Cases +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/linear_algebra/lu_decomposition.py b/linear_algebra/lu_decomposition.py new file mode 100644 index 000000000000..3d89b53a48fb --- /dev/null +++ b/linear_algebra/lu_decomposition.py @@ -0,0 +1,114 @@ +""" +Lower-upper (LU) decomposition factors a matrix as a product of a lower +triangular matrix and an upper triangular matrix. A square matrix has an LU +decomposition under the following conditions: + + - If the matrix is invertible, then it has an LU decomposition if and only + if all of its leading principal minors are non-zero (see + https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of + leading principal minors of a matrix). + - If the matrix is singular (i.e., not invertible) and it has a rank of k + (i.e., it has k linearly independent columns), then it has an LU + decomposition if its first k leading principal minors are non-zero. + +This algorithm will simply attempt to perform LU decomposition on any square +matrix and raise an error if no such decomposition exists. + +Reference: https://en.wikipedia.org/wiki/LU_decomposition +""" + +from __future__ import annotations + +import numpy as np + + +def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """ + Perform LU decomposition on a given matrix and raises an error if the matrix + isn't square or if no such decomposition exists + + >>> matrix = np.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + >>> lower_mat + array([[1. , 0. , 0. ], + [0. , 1. , 0. ], + [2.5, 8. , 1. ]]) + >>> upper_mat + array([[ 2. , -2. , 1. ], + [ 0. , 1. , 2. ], + [ 0. , 0. , -17.5]]) + + >>> matrix = np.array([[4, 3], [6, 3]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + >>> lower_mat + array([[1. , 0. ], + [1.5, 1. ]]) + >>> upper_mat + array([[ 4. , 3. ], + [ 0. , -1.5]]) + + >>> # Matrix is not square + >>> matrix = np.array([[2, -2, 1], [0, 1, 2]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + Traceback (most recent call last): + ... + ValueError: 'table' has to be of square shaped array but got a 2x3 array: + [[ 2 -2 1] + [ 0 1 2]] + + >>> # Matrix is invertible, but its first leading principal minor is 0 + >>> matrix = np.array([[0, 1], [1, 0]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + Traceback (most recent call last): + ... + ArithmeticError: No LU decomposition exists + + >>> # Matrix is singular, but its first leading principal minor is 1 + >>> matrix = np.array([[1, 0], [1, 0]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + >>> lower_mat + array([[1., 0.], + [1., 1.]]) + >>> upper_mat + array([[1., 0.], + [0., 0.]]) + + >>> # Matrix is singular, but its first leading principal minor is 0 + >>> matrix = np.array([[0, 1], [0, 1]]) + >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) + Traceback (most recent call last): + ... + ArithmeticError: No LU decomposition exists + """ + # Ensure that table is a square array + rows, columns = np.shape(table) + if rows != columns: + msg = ( + "'table' has to be of square shaped array but got a " + f"{rows}x{columns} array:\n{table}" + ) + raise ValueError(msg) + + lower = np.zeros((rows, columns)) + upper = np.zeros((rows, columns)) + + # in 'total', the necessary data is extracted through slices + # and the sum of the products is obtained. + + for i in range(columns): + for j in range(i): + total = np.sum(lower[i, :i] * upper[:i, j]) + if upper[j][j] == 0: + raise ArithmeticError("No LU decomposition exists") + lower[i][j] = (table[i][j] - total) / upper[j][j] + lower[i][i] = 1 + for j in range(i, columns): + total = np.sum(lower[i, :i] * upper[:i, j]) + upper[i][j] = table[i][j] - total + return lower, upper + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/linear_algebra/src/conjugate_gradient.py b/linear_algebra/src/conjugate_gradient.py index 4cf566ec9e36..45da35813978 100644 --- a/linear_algebra/src/conjugate_gradient.py +++ b/linear_algebra/src/conjugate_gradient.py @@ -3,6 +3,7 @@ - https://en.wikipedia.org/wiki/Conjugate_gradient_method - https://en.wikipedia.org/wiki/Definite_symmetric_matrix """ + from typing import Any import numpy as np @@ -60,7 +61,8 @@ def _create_spd_matrix(dimension: int) -> Any: >>> _is_matrix_spd(spd_matrix) True """ - random_matrix = np.random.randn(dimension, dimension) + rng = np.random.default_rng() + random_matrix = rng.normal(size=(dimension, dimension)) spd_matrix = np.dot(random_matrix, random_matrix.T) assert _is_matrix_spd(spd_matrix) return spd_matrix @@ -156,7 +158,8 @@ def test_conjugate_gradient() -> None: # Create linear system with SPD matrix and known solution x_true. dimension = 3 spd_matrix = _create_spd_matrix(dimension) - x_true = np.random.randn(dimension, 1) + rng = np.random.default_rng() + x_true = rng.normal(size=(dimension, 1)) b = np.dot(spd_matrix, x_true) # Numpy solution. diff --git a/linear_algebra/src/gaussian_elimination_pivoting.py b/linear_algebra/src/gaussian_elimination_pivoting.py new file mode 100644 index 000000000000..540f57b0cff6 --- /dev/null +++ b/linear_algebra/src/gaussian_elimination_pivoting.py @@ -0,0 +1,88 @@ +import numpy as np + + +def solve_linear_system(matrix: np.ndarray) -> np.ndarray: + """ + Solve a linear system of equations using Gaussian elimination with partial pivoting + + Args: + - `matrix`: Coefficient matrix with the last column representing the constants. + + Returns: + - Solution vector. + + Raises: + - ``ValueError``: If the matrix is not correct (i.e., singular). + + https://courses.engr.illinois.edu/cs357/su2013/lect.htm Lecture 7 + + Example: + + >>> A = np.array([[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], dtype=float) + >>> B = np.array([8, -11, -3], dtype=float) + >>> solution = solve_linear_system(np.column_stack((A, B))) + >>> np.allclose(solution, np.array([2., 3., -1.])) + True + >>> solve_linear_system(np.array([[0, 0, 0]], dtype=float)) + Traceback (most recent call last): + ... + ValueError: Matrix is not square + >>> solve_linear_system(np.array([[0, 0, 0], [0, 0, 0]], dtype=float)) + Traceback (most recent call last): + ... + ValueError: Matrix is singular + """ + ab = np.copy(matrix) + num_of_rows = ab.shape[0] + num_of_columns = ab.shape[1] - 1 + x_lst: list[float] = [] + + if num_of_rows != num_of_columns: + raise ValueError("Matrix is not square") + + for column_num in range(num_of_rows): + # Lead element search + for i in range(column_num, num_of_columns): + if abs(ab[i][column_num]) > abs(ab[column_num][column_num]): + ab[[column_num, i]] = ab[[i, column_num]] + + # Upper triangular matrix + if abs(ab[column_num, column_num]) < 1e-8: + raise ValueError("Matrix is singular") + + if column_num != 0: + for i in range(column_num, num_of_rows): + ab[i, :] -= ( + ab[i, column_num - 1] + / ab[column_num - 1, column_num - 1] + * ab[column_num - 1, :] + ) + + # Find x vector (Back Substitution) + for column_num in range(num_of_rows - 1, -1, -1): + x = ab[column_num, -1] / ab[column_num, column_num] + x_lst.insert(0, x) + for i in range(column_num - 1, -1, -1): + ab[i, -1] -= ab[i, column_num] * x + + # Return the solution vector + return np.asarray(x_lst) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + example_matrix = np.array( + [ + [5.0, -5.0, -3.0, 4.0, -11.0], + [1.0, -4.0, 6.0, -4.0, -10.0], + [-2.0, -5.0, 4.0, -5.0, -12.0], + [-3.0, -3.0, 5.0, -5.0, 8.0], + ], + dtype=float, + ) + + print(f"Matrix:\n{example_matrix}") + print(f"{solve_linear_system(example_matrix) = }") diff --git a/linear_algebra/src/lib.py b/linear_algebra/src/lib.py index ac0398a31a07..0d6a348475cd 100644 --- a/linear_algebra/src/lib.py +++ b/linear_algebra/src/lib.py @@ -18,6 +18,7 @@ - function square_zero_matrix(N) - function random_matrix(W, H, a, b) """ + from __future__ import annotations import math @@ -45,7 +46,6 @@ class Vector: change_component(pos: int, value: float): changes specified component euclidean_length(): returns the euclidean length of the vector angle(other: Vector, deg: bool): returns the angle between two vectors - TODO: compare-operator """ def __init__(self, components: Collection[float] | None = None) -> None: @@ -95,20 +95,28 @@ def __sub__(self, other: Vector) -> Vector: else: # error case raise Exception("must have the same size") + def __eq__(self, other: object) -> bool: + """ + performs the comparison between two vectors + """ + if not isinstance(other, Vector): + return NotImplemented + if len(self) != len(other): + return False + return all(self.component(i) == other.component(i) for i in range(len(self))) + @overload - def __mul__(self, other: float) -> Vector: - ... + def __mul__(self, other: float) -> Vector: ... @overload - def __mul__(self, other: Vector) -> float: - ... + def __mul__(self, other: Vector) -> float: ... def __mul__(self, other: float | Vector) -> float | Vector: """ mul implements the scalar multiplication and the dot-product """ - if isinstance(other, float) or isinstance(other, int): + if isinstance(other, (float, int)): ans = [c * other for c in self.__components] return Vector(ans) elif isinstance(other, Vector) and len(self) == len(other): @@ -200,7 +208,8 @@ def unit_basis_vector(dimension: int, pos: int) -> Vector: at index 'pos' (indexing at 0) """ # precondition - assert isinstance(dimension, int) and (isinstance(pos, int)) + assert isinstance(dimension, int) + assert isinstance(pos, int) ans = [0] * dimension ans[pos] = 1 return Vector(ans) @@ -213,11 +222,9 @@ def axpy(scalar: float, x: Vector, y: Vector) -> Vector: computes the axpy operation """ # precondition - assert ( - isinstance(x, Vector) - and isinstance(y, Vector) - and (isinstance(scalar, int) or isinstance(scalar, float)) - ) + assert isinstance(x, Vector) + assert isinstance(y, Vector) + assert isinstance(scalar, (int, float)) return x * scalar + y @@ -310,12 +317,10 @@ def __sub__(self, other: Matrix) -> Matrix: raise Exception("matrices must have the same dimension!") @overload - def __mul__(self, other: float) -> Matrix: - ... + def __mul__(self, other: float) -> Matrix: ... @overload - def __mul__(self, other: Vector) -> Vector: - ... + def __mul__(self, other: Vector) -> Vector: ... def __mul__(self, other: float | Vector) -> Vector | Matrix: """ @@ -337,12 +342,13 @@ def __mul__(self, other: float | Vector) -> Vector | Matrix: "vector must have the same size as the " "number of columns of the matrix!" ) - elif isinstance(other, int) or isinstance(other, float): # matrix-scalar + elif isinstance(other, (int, float)): # matrix-scalar matrix = [ [self.__matrix[i][j] * other for j in range(self.__width)] for i in range(self.__height) ] return Matrix(matrix, self.__width, self.__height) + return None def height(self) -> int: """ diff --git a/linear_algebra/src/polynom_for_points.py b/linear_algebra/src/polynom_for_points.py index f5e3db0cbb13..452f3edd4aee 100644 --- a/linear_algebra/src/polynom_for_points.py +++ b/linear_algebra/src/polynom_for_points.py @@ -3,30 +3,36 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: coordinates is a two dimensional matrix: [[x, y], [x, y], ...] number of points you want to use - >>> print(points_to_polynomial([])) + >>> points_to_polynomial([]) Traceback (most recent call last): ... ValueError: The program cannot work out a fitting polynomial. - >>> print(points_to_polynomial([[]])) + >>> points_to_polynomial([[]]) + Traceback (most recent call last): + ... + ValueError: The program cannot work out a fitting polynomial. + >>> points_to_polynomial([[1, 0], [2, 0], [3, 0]]) + 'f(x)=x^2*0.0+x^1*-0.0+x^0*0.0' + >>> points_to_polynomial([[1, 1], [2, 1], [3, 1]]) + 'f(x)=x^2*0.0+x^1*-0.0+x^0*1.0' + >>> points_to_polynomial([[1, 3], [2, 3], [3, 3]]) + 'f(x)=x^2*0.0+x^1*-0.0+x^0*3.0' + >>> points_to_polynomial([[1, 1], [2, 2], [3, 3]]) + 'f(x)=x^2*0.0+x^1*1.0+x^0*0.0' + >>> points_to_polynomial([[1, 1], [2, 4], [3, 9]]) + 'f(x)=x^2*1.0+x^1*-0.0+x^0*0.0' + >>> points_to_polynomial([[1, 3], [2, 6], [3, 11]]) + 'f(x)=x^2*1.0+x^1*-0.0+x^0*2.0' + >>> points_to_polynomial([[1, -3], [2, -6], [3, -11]]) + 'f(x)=x^2*-1.0+x^1*-0.0+x^0*-2.0' + >>> points_to_polynomial([[1, 5], [2, 2], [3, 9]]) + 'f(x)=x^2*5.0+x^1*-18.0+x^0*18.0' + >>> points_to_polynomial([[1, 1], [1, 2], [1, 3]]) + 'x=1' + >>> points_to_polynomial([[1, 1], [2, 2], [2, 2]]) Traceback (most recent call last): ... ValueError: The program cannot work out a fitting polynomial. - >>> print(points_to_polynomial([[1, 0], [2, 0], [3, 0]])) - f(x)=x^2*0.0+x^1*-0.0+x^0*0.0 - >>> print(points_to_polynomial([[1, 1], [2, 1], [3, 1]])) - f(x)=x^2*0.0+x^1*-0.0+x^0*1.0 - >>> print(points_to_polynomial([[1, 3], [2, 3], [3, 3]])) - f(x)=x^2*0.0+x^1*-0.0+x^0*3.0 - >>> print(points_to_polynomial([[1, 1], [2, 2], [3, 3]])) - f(x)=x^2*0.0+x^1*1.0+x^0*0.0 - >>> print(points_to_polynomial([[1, 1], [2, 4], [3, 9]])) - f(x)=x^2*1.0+x^1*-0.0+x^0*0.0 - >>> print(points_to_polynomial([[1, 3], [2, 6], [3, 11]])) - f(x)=x^2*1.0+x^1*-0.0+x^0*2.0 - >>> print(points_to_polynomial([[1, -3], [2, -6], [3, -11]])) - f(x)=x^2*-1.0+x^1*-0.0+x^0*-2.0 - >>> print(points_to_polynomial([[1, 5], [2, 2], [3, 9]])) - f(x)=x^2*5.0+x^1*-18.0+x^0*18.0 """ if len(coordinates) == 0 or not all(len(pair) == 2 for pair in coordinates): raise ValueError("The program cannot work out a fitting polynomial.") @@ -43,62 +49,43 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: x = len(coordinates) - count_of_line = 0 - matrix: list[list[float]] = [] # put the x and x to the power values in a matrix - while count_of_line < x: - count_in_line = 0 - a = coordinates[count_of_line][0] - count_line: list[float] = [] - while count_in_line < x: - count_line.append(a ** (x - (count_in_line + 1))) - count_in_line += 1 - matrix.append(count_line) - count_of_line += 1 + matrix: list[list[float]] = [ + [ + coordinates[count_of_line][0] ** (x - (count_in_line + 1)) + for count_in_line in range(x) + ] + for count_of_line in range(x) + ] - count_of_line = 0 # put the y values into a vector - vector: list[float] = [] - while count_of_line < x: - vector.append(coordinates[count_of_line][1]) - count_of_line += 1 - - count = 0 + vector: list[float] = [coordinates[count_of_line][1] for count_of_line in range(x)] - while count < x: - zahlen = 0 - while zahlen < x: - if count == zahlen: - zahlen += 1 - if zahlen == x: - break - bruch = matrix[zahlen][count] / matrix[count][count] + for count in range(x): + for number in range(x): + if count == number: + continue + fraction = matrix[number][count] / matrix[count][count] for counting_columns, item in enumerate(matrix[count]): # manipulating all the values in the matrix - matrix[zahlen][counting_columns] -= item * bruch + matrix[number][counting_columns] -= item * fraction # manipulating the values in the vector - vector[zahlen] -= vector[count] * bruch - zahlen += 1 - count += 1 + vector[number] -= vector[count] * fraction - count = 0 # make solutions - solution: list[str] = [] - while count < x: - solution.append(str(vector[count] / matrix[count][count])) - count += 1 + solution: list[str] = [ + str(vector[count] / matrix[count][count]) for count in range(x) + ] - count = 0 solved = "f(x)=" - while count < x: + for count in range(x): remove_e: list[str] = solution[count].split("E") if len(remove_e) > 1: solution[count] = f"{remove_e[0]}*10^{remove_e[1]}" solved += f"x^{x - (count + 1)}*{solution[count]}" if count + 1 != x: solved += "+" - count += 1 return solved diff --git a/linear_algebra/src/power_iteration.py b/linear_algebra/src/power_iteration.py index 24fbd9a5e002..83c2ce48c3a0 100644 --- a/linear_algebra/src/power_iteration.py +++ b/linear_algebra/src/power_iteration.py @@ -78,7 +78,7 @@ def power_iteration( if is_complex: lambda_ = np.real(lambda_) - return lambda_, vector + return float(lambda_), vector def test_power_iteration() -> None: diff --git a/linear_algebra/src/rank_of_matrix.py b/linear_algebra/src/rank_of_matrix.py new file mode 100644 index 000000000000..2c4fe2a8d1da --- /dev/null +++ b/linear_algebra/src/rank_of_matrix.py @@ -0,0 +1,93 @@ +""" +Calculate the rank of a matrix. + +See: https://en.wikipedia.org/wiki/Rank_(linear_algebra) +""" + + +def rank_of_matrix(matrix: list[list[int | float]]) -> int: + """ + Finds the rank of a matrix. + + Args: + `matrix`: The matrix as a list of lists. + + Returns: + The rank of the matrix. + + Example: + + >>> matrix1 = [[1, 2, 3], + ... [4, 5, 6], + ... [7, 8, 9]] + >>> rank_of_matrix(matrix1) + 2 + >>> matrix2 = [[1, 0, 0], + ... [0, 1, 0], + ... [0, 0, 0]] + >>> rank_of_matrix(matrix2) + 2 + >>> matrix3 = [[1, 2, 3, 4], + ... [5, 6, 7, 8], + ... [9, 10, 11, 12]] + >>> rank_of_matrix(matrix3) + 2 + >>> rank_of_matrix([[2,3,-1,-1], + ... [1,-1,-2,4], + ... [3,1,3,-2], + ... [6,3,0,-7]]) + 4 + >>> rank_of_matrix([[2,1,-3,-6], + ... [3,-3,1,2], + ... [1,1,1,2]]) + 3 + >>> rank_of_matrix([[2,-1,0], + ... [1,3,4], + ... [4,1,-3]]) + 3 + >>> rank_of_matrix([[3,2,1], + ... [-6,-4,-2]]) + 1 + >>> rank_of_matrix([[],[]]) + 0 + >>> rank_of_matrix([[1]]) + 1 + >>> rank_of_matrix([[]]) + 0 + """ + + rows = len(matrix) + columns = len(matrix[0]) + rank = min(rows, columns) + + for row in range(rank): + # Check if diagonal element is not zero + if matrix[row][row] != 0: + # Eliminate all the elements below the diagonal + for col in range(row + 1, rows): + multiplier = matrix[col][row] / matrix[row][row] + for i in range(row, columns): + matrix[col][i] -= multiplier * matrix[row][i] + else: + # Find a non-zero diagonal element to swap rows + reduce = True + for i in range(row + 1, rows): + if matrix[i][row] != 0: + matrix[row], matrix[i] = matrix[i], matrix[row] + reduce = False + break + if reduce: + rank -= 1 + for i in range(rows): + matrix[i][row] = matrix[i][rank] + + # Reduce the row pointer by one to stay on the same row + row -= 1 + + return rank + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/linear_algebra/src/rayleigh_quotient.py b/linear_algebra/src/rayleigh_quotient.py index 4773429cbf1b..46bf1671d2b1 100644 --- a/linear_algebra/src/rayleigh_quotient.py +++ b/linear_algebra/src/rayleigh_quotient.py @@ -1,6 +1,7 @@ """ https://en.wikipedia.org/wiki/Rayleigh_quotient """ + from typing import Any import numpy as np diff --git a/linear_algebra/src/schur_complement.py b/linear_algebra/src/schur_complement.py index 3a5f4443afd3..74ac75e3fce2 100644 --- a/linear_algebra/src/schur_complement.py +++ b/linear_algebra/src/schur_complement.py @@ -1,6 +1,7 @@ import unittest import numpy as np +import pytest def schur_complement( @@ -11,13 +12,14 @@ def schur_complement( ) -> np.ndarray: """ Schur complement of a symmetric matrix X given as a 2x2 block matrix - consisting of matrices A, B and C. - Matrix A must be quadratic and non-singular. - In case A is singular, a pseudo-inverse may be provided using - the pseudo_inv argument. + consisting of matrices `A`, `B` and `C`. + Matrix `A` must be quadratic and non-singular. + In case `A` is singular, a pseudo-inverse may be provided using + the `pseudo_inv` argument. + + | Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement + | See also Convex Optimization - Boyd and Vandenberghe, A.5.5 - Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement - See also Convex Optimization – Boyd and Vandenberghe, A.5.5 >>> import numpy as np >>> a = np.array([[1, 2], [2, 1]]) >>> b = np.array([[0, 3], [3, 0]]) @@ -31,16 +33,18 @@ def schur_complement( shape_c = np.shape(mat_c) if shape_a[0] != shape_b[0]: - raise ValueError( - f"Expected the same number of rows for A and B. \ - Instead found A of size {shape_a} and B of size {shape_b}" + msg = ( + "Expected the same number of rows for A and B. " + f"Instead found A of size {shape_a} and B of size {shape_b}" ) + raise ValueError(msg) if shape_b[1] != shape_c[1]: - raise ValueError( - f"Expected the same number of columns for B and C. \ - Instead found B of size {shape_b} and C of size {shape_c}" + msg = ( + "Expected the same number of columns for B and C. " + f"Instead found B of size {shape_b} and C of size {shape_c}" ) + raise ValueError(msg) a_inv = pseudo_inv if a_inv is None: @@ -68,14 +72,14 @@ def test_schur_complement(self) -> None: det_a = np.linalg.det(a) det_s = np.linalg.det(s) - self.assertAlmostEqual(det_x, det_a * det_s) + assert np.is_close(det_x, det_a * det_s) def test_improper_a_b_dimensions(self) -> None: a = np.array([[1, 2, 1], [2, 1, 2], [3, 2, 4]]) b = np.array([[0, 3], [3, 0], [2, 3]]) c = np.array([[2, 1], [6, 3]]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): schur_complement(a, b, c) def test_improper_b_c_dimensions(self) -> None: @@ -83,7 +87,7 @@ def test_improper_b_c_dimensions(self) -> None: b = np.array([[0, 3], [3, 0], [2, 3]]) c = np.array([[2, 1, 3], [6, 3, 5]]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): schur_complement(a, b, c) diff --git a/linear_algebra/src/test_linear_algebra.py b/linear_algebra/src/test_linear_algebra.py index 50d079572e0f..fc5f90fd5cbe 100644 --- a/linear_algebra/src/test_linear_algebra.py +++ b/linear_algebra/src/test_linear_algebra.py @@ -6,8 +6,11 @@ This file contains the test-suite for the linear algebra library. """ + import unittest +import pytest + from .lib import ( Matrix, Vector, @@ -24,8 +27,8 @@ def test_component(self) -> None: test for method component() """ x = Vector([1, 2, 3]) - self.assertEqual(x.component(0), 1) - self.assertEqual(x.component(2), 3) + assert x.component(0) == 1 + assert x.component(2) == 3 _ = Vector() def test_str(self) -> None: @@ -33,14 +36,14 @@ def test_str(self) -> None: test for method toString() """ x = Vector([0, 0, 0, 0, 0, 1]) - self.assertEqual(str(x), "(0,0,0,0,0,1)") + assert str(x) == "(0,0,0,0,0,1)" def test_size(self) -> None: """ test for method size() """ x = Vector([1, 2, 3, 4]) - self.assertEqual(len(x), 4) + assert len(x) == 4 def test_euclidean_length(self) -> None: """ @@ -50,10 +53,10 @@ def test_euclidean_length(self) -> None: y = Vector([1, 2, 3, 4, 5]) z = Vector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) w = Vector([1, -1, 1, -1, 2, -3, 4, -5]) - self.assertAlmostEqual(x.euclidean_length(), 2.236, 3) - self.assertAlmostEqual(y.euclidean_length(), 7.416, 3) - self.assertEqual(z.euclidean_length(), 0) - self.assertAlmostEqual(w.euclidean_length(), 7.616, 3) + assert x.euclidean_length() == pytest.approx(2.236, abs=1e-3) + assert y.euclidean_length() == pytest.approx(7.416, abs=1e-3) + assert z.euclidean_length() == 0 + assert w.euclidean_length() == pytest.approx(7.616, abs=1e-3) def test_add(self) -> None: """ @@ -61,9 +64,9 @@ def test_add(self) -> None: """ x = Vector([1, 2, 3]) y = Vector([1, 1, 1]) - self.assertEqual((x + y).component(0), 2) - self.assertEqual((x + y).component(1), 3) - self.assertEqual((x + y).component(2), 4) + assert (x + y).component(0) == 2 + assert (x + y).component(1) == 3 + assert (x + y).component(2) == 4 def test_sub(self) -> None: """ @@ -71,9 +74,9 @@ def test_sub(self) -> None: """ x = Vector([1, 2, 3]) y = Vector([1, 1, 1]) - self.assertEqual((x - y).component(0), 0) - self.assertEqual((x - y).component(1), 1) - self.assertEqual((x - y).component(2), 2) + assert (x - y).component(0) == 0 + assert (x - y).component(1) == 1 + assert (x - y).component(2) == 2 def test_mul(self) -> None: """ @@ -82,20 +85,20 @@ def test_mul(self) -> None: x = Vector([1, 2, 3]) a = Vector([2, -1, 4]) # for test of dot product b = Vector([1, -2, -1]) - self.assertEqual(str(x * 3.0), "(3.0,6.0,9.0)") - self.assertEqual((a * b), 0) + assert str(x * 3.0) == "(3.0,6.0,9.0)" + assert a * b == 0 def test_zero_vector(self) -> None: """ test for global function zero_vector() """ - self.assertEqual(str(zero_vector(10)).count("0"), 10) + assert str(zero_vector(10)).count("0") == 10 def test_unit_basis_vector(self) -> None: """ test for global function unit_basis_vector() """ - self.assertEqual(str(unit_basis_vector(3, 1)), "(0,1,0)") + assert str(unit_basis_vector(3, 1)) == "(0,1,0)" def test_axpy(self) -> None: """ @@ -103,7 +106,7 @@ def test_axpy(self) -> None: """ x = Vector([1, 2, 3]) y = Vector([1, 0, 1]) - self.assertEqual(str(axpy(2, x, y)), "(3,4,7)") + assert str(axpy(2, x, y)) == "(3,4,7)" def test_copy(self) -> None: """ @@ -111,7 +114,7 @@ def test_copy(self) -> None: """ x = Vector([1, 0, 0, 0, 0, 0]) y = x.copy() - self.assertEqual(str(x), str(y)) + assert str(x) == str(y) def test_change_component(self) -> None: """ @@ -120,14 +123,14 @@ def test_change_component(self) -> None: x = Vector([1, 0, 0]) x.change_component(0, 0) x.change_component(1, 1) - self.assertEqual(str(x), "(0,1,0)") + assert str(x) == "(0,1,0)" def test_str_matrix(self) -> None: """ test for Matrix method str() """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) - self.assertEqual("|1,2,3|\n|2,4,5|\n|6,7,8|\n", str(a)) + assert str(a) == "|1,2,3|\n|2,4,5|\n|6,7,8|\n" def test_minor(self) -> None: """ @@ -137,7 +140,7 @@ def test_minor(self) -> None: minors = [[-3, -14, -10], [-5, -10, -5], [-2, -1, 0]] for x in range(a.height()): for y in range(a.width()): - self.assertEqual(minors[x][y], a.minor(x, y)) + assert minors[x][y] == a.minor(x, y) def test_cofactor(self) -> None: """ @@ -147,14 +150,14 @@ def test_cofactor(self) -> None: cofactors = [[-3, 14, -10], [5, -10, 5], [-2, 1, 0]] for x in range(a.height()): for y in range(a.width()): - self.assertEqual(cofactors[x][y], a.cofactor(x, y)) + assert cofactors[x][y] == a.cofactor(x, y) def test_determinant(self) -> None: """ test for Matrix method determinant() """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) - self.assertEqual(-5, a.determinant()) + assert a.determinant() == -5 def test__mul__matrix(self) -> None: """ @@ -162,8 +165,8 @@ def test__mul__matrix(self) -> None: """ a = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 3, 3) x = Vector([1, 2, 3]) - self.assertEqual("(14,32,50)", str(a * x)) - self.assertEqual("|2,4,6|\n|8,10,12|\n|14,16,18|\n", str(a * 2)) + assert str(a * x) == "(14,32,50)" + assert str(a * 2) == "|2,4,6|\n|8,10,12|\n|14,16,18|\n" def test_change_component_matrix(self) -> None: """ @@ -171,14 +174,14 @@ def test_change_component_matrix(self) -> None: """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) a.change_component(0, 2, 5) - self.assertEqual("|1,2,5|\n|2,4,5|\n|6,7,8|\n", str(a)) + assert str(a) == "|1,2,5|\n|2,4,5|\n|6,7,8|\n" def test_component_matrix(self) -> None: """ test for Matrix method component() """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) - self.assertEqual(7, a.component(2, 1), 0.01) + assert a.component(2, 1) == 7, 0.01 def test__add__matrix(self) -> None: """ @@ -186,7 +189,7 @@ def test__add__matrix(self) -> None: """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) b = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) - self.assertEqual("|2,4,10|\n|4,8,10|\n|12,14,18|\n", str(a + b)) + assert str(a + b) == "|2,4,10|\n|4,8,10|\n|12,14,18|\n" def test__sub__matrix(self) -> None: """ @@ -194,15 +197,14 @@ def test__sub__matrix(self) -> None: """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) b = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) - self.assertEqual("|0,0,-4|\n|0,0,0|\n|0,0,-2|\n", str(a - b)) + assert str(a - b) == "|0,0,-4|\n|0,0,0|\n|0,0,-2|\n" def test_square_zero_matrix(self) -> None: """ test for global function square_zero_matrix() """ - self.assertEqual( - "|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n", - str(square_zero_matrix(5)), + assert str(square_zero_matrix(5)) == ( + "|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n" ) diff --git a/linear_algebra/src/transformations_2d.py b/linear_algebra/src/transformations_2d.py index cdf42100d5d9..5dee59024752 100644 --- a/linear_algebra/src/transformations_2d.py +++ b/linear_algebra/src/transformations_2d.py @@ -3,14 +3,17 @@ I have added the codes for reflection, projection, scaling and rotation 2D matrices. +.. code-block:: python + scaling(5) = [[5.0, 0.0], [0.0, 5.0]] - rotation(45) = [[0.5253219888177297, -0.8509035245341184], - [0.8509035245341184, 0.5253219888177297]] -projection(45) = [[0.27596319193541496, 0.446998331800279], - [0.446998331800279, 0.7240368080645851]] -reflection(45) = [[0.05064397763545947, 0.893996663600558], - [0.893996663600558, 0.7018070490682369]] + rotation(45) = [[0.5253219888177297, -0.8509035245341184], + [0.8509035245341184, 0.5253219888177297]] + projection(45) = [[0.27596319193541496, 0.446998331800279], + [0.446998331800279, 0.7240368080645851]] + reflection(45) = [[0.05064397763545947, 0.893996663600558], + [0.893996663600558, 0.7018070490682369]] """ + from math import cos, sin diff --git a/linear_programming/__init__.py b/linear_programming/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py new file mode 100644 index 000000000000..a8affe1b72d2 --- /dev/null +++ b/linear_programming/simplex.py @@ -0,0 +1,339 @@ +""" +Python implementation of the simplex algorithm for solving linear programs in +tabular form with +- `>=`, `<=`, and `=` constraints and +- each variable `x1, x2, ...>= 0`. + +See https://gist.github.com/imengus/f9619a568f7da5bc74eaf20169a24d98 for how to +convert linear programs to simplex tableaus, and the steps taken in the simplex +algorithm. + +Resources: +https://en.wikipedia.org/wiki/Simplex_algorithm +https://tinyurl.com/simplex4beginners +""" + +from typing import Any + +import numpy as np + + +class Tableau: + """Operate on simplex tableaus + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4]]), 2, 2) + Traceback (most recent call last): + ... + TypeError: Tableau must have type float64 + + >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2, 2) + Traceback (most recent call last): + ... + ValueError: RHS must be > 0 + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), -2, 2) + Traceback (most recent call last): + ... + ValueError: number of (artificial) variables must be a natural number + """ + + # Max iteration number to prevent cycling + maxiter = 100 + + def __init__( + self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int + ) -> None: + if tableau.dtype != "float64": + raise TypeError("Tableau must have type float64") + + # Check if RHS is negative + if not (tableau[:, -1] >= 0).all(): + raise ValueError("RHS must be > 0") + + if n_vars < 2 or n_artificial_vars < 0: + raise ValueError( + "number of (artificial) variables must be a natural number" + ) + + self.tableau = tableau + self.n_rows, n_cols = tableau.shape + + # Number of decision variables x1, x2, x3... + self.n_vars, self.n_artificial_vars = n_vars, n_artificial_vars + + # 2 if there are >= or == constraints (nonstandard), 1 otherwise (std) + self.n_stages = (self.n_artificial_vars > 0) + 1 + + # Number of slack variables added to make inequalities into equalities + self.n_slack = n_cols - self.n_vars - self.n_artificial_vars - 1 + + # Objectives for each stage + self.objectives = ["max"] + + # In two stage simplex, first minimise then maximise + if self.n_artificial_vars: + self.objectives.append("min") + + self.col_titles = self.generate_col_titles() + + # Index of current pivot row and column + self.row_idx = None + self.col_idx = None + + # Does objective row only contain (non)-negative values? + self.stop_iter = False + + def generate_col_titles(self) -> list[str]: + """Generate column titles for tableau of specific dimensions + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2, 0).generate_col_titles() + ['x1', 'x2', 's1', 's2', 'RHS'] + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2, 2).generate_col_titles() + ['x1', 'x2', 'RHS'] + """ + args = (self.n_vars, self.n_slack) + + # decision | slack + string_starts = ["x", "s"] + titles = [] + for i in range(2): + for j in range(args[i]): + titles.append(string_starts[i] + str(j + 1)) + titles.append("RHS") + return titles + + def find_pivot(self) -> tuple[Any, Any]: + """Finds the pivot row and column. + >>> tuple(int(x) for x in Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], + ... [1,2,0,1,7.]]), 2, 0).find_pivot()) + (1, 0) + """ + objective = self.objectives[-1] + + # Find entries of highest magnitude in objective rows + sign = (objective == "min") - (objective == "max") + col_idx = np.argmax(sign * self.tableau[0, :-1]) + + # Choice is only valid if below 0 for maximise, and above for minimise + if sign * self.tableau[0, col_idx] <= 0: + self.stop_iter = True + return 0, 0 + + # Pivot row is chosen as having the lowest quotient when elements of + # the pivot column divide the right-hand side + + # Slice excluding the objective rows + s = slice(self.n_stages, self.n_rows) + + # RHS + dividend = self.tableau[s, -1] + + # Elements of pivot column within slice + divisor = self.tableau[s, col_idx] + + # Array filled with nans + nans = np.full(self.n_rows - self.n_stages, np.nan) + + # If element in pivot column is greater than zero, return + # quotient or nan otherwise + quotients = np.divide(dividend, divisor, out=nans, where=divisor > 0) + + # Arg of minimum quotient excluding the nan values. n_stages is added + # to compensate for earlier exclusion of objective columns + row_idx = np.nanargmin(quotients) + self.n_stages + return row_idx, col_idx + + def pivot(self, row_idx: int, col_idx: int) -> np.ndarray: + """Pivots on value on the intersection of pivot row and column. + + >>> Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2, 2).pivot(1, 0).tolist() + ... # doctest: +NORMALIZE_WHITESPACE + [[0.0, 3.0, 2.0, 0.0, 8.0], + [1.0, 3.0, 1.0, 0.0, 4.0], + [0.0, -8.0, -3.0, 1.0, -8.0]] + """ + # Avoid changes to original tableau + piv_row = self.tableau[row_idx].copy() + + piv_val = piv_row[col_idx] + + # Entry becomes 1 + piv_row *= 1 / piv_val + + # Variable in pivot column becomes basic, ie the only non-zero entry + for idx, coeff in enumerate(self.tableau[:, col_idx]): + self.tableau[idx] += -coeff * piv_row + self.tableau[row_idx] = piv_row + return self.tableau + + def change_stage(self) -> np.ndarray: + """Exits first phase of the two-stage method by deleting artificial + rows and columns, or completes the algorithm if exiting the standard + case. + + >>> Tableau(np.array([ + ... [3, 3, -1, -1, 0, 0, 4], + ... [2, 1, 0, 0, 0, 0, 0.], + ... [1, 2, -1, 0, 1, 0, 2], + ... [2, 1, 0, -1, 0, 1, 2] + ... ]), 2, 2).change_stage().tolist() + ... # doctest: +NORMALIZE_WHITESPACE + [[2.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 2.0, -1.0, 0.0, 2.0], + [2.0, 1.0, 0.0, -1.0, 2.0]] + """ + # Objective of original objective row remains + self.objectives.pop() + + if not self.objectives: + return self.tableau + + # Slice containing ids for artificial columns + s = slice(-self.n_artificial_vars - 1, -1) + + # Delete the artificial variable columns + self.tableau = np.delete(self.tableau, s, axis=1) + + # Delete the objective row of the first stage + self.tableau = np.delete(self.tableau, 0, axis=0) + + self.n_stages = 1 + self.n_rows -= 1 + self.n_artificial_vars = 0 + self.stop_iter = False + return self.tableau + + def run_simplex(self) -> dict[Any, Any]: + """Operate on tableau until objective function cannot be + improved further. + + # Standard linear program: + Max: x1 + x2 + ST: x1 + 3x2 <= 4 + 3x1 + x2 <= 4 + >>> {key: float(value) for key, value in Tableau(np.array([[-1,-1,0,0,0], + ... [1,3,1,0,4],[3,1,0,1,4.]]), 2, 0).run_simplex().items()} + {'P': 2.0, 'x1': 1.0, 'x2': 1.0} + + # Standard linear program with 3 variables: + Max: 3x1 + x2 + 3x3 + ST: 2x1 + x2 + x3 ≤ 2 + x1 + 2x2 + 3x3 ≤ 5 + 2x1 + 2x2 + x3 ≤ 6 + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [-3,-1,-3,0,0,0,0], + ... [2,1,1,1,0,0,2], + ... [1,2,3,0,1,0,5], + ... [2,2,1,0,0,1,6.] + ... ]),3,0).run_simplex().items()} # doctest: +ELLIPSIS + {'P': 5.4, 'x1': 0.199..., 'x3': 1.6} + + + # Optimal tableau input: + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [0, 0, 0.25, 0.25, 2], + ... [0, 1, 0.375, -0.125, 1], + ... [1, 0, -0.125, 0.375, 1] + ... ]), 2, 0).run_simplex().items()} + {'P': 2.0, 'x1': 1.0, 'x2': 1.0} + + # Non-standard: >= constraints + Max: 2x1 + 3x2 + x3 + ST: x1 + x2 + x3 <= 40 + 2x1 + x2 - x3 >= 10 + - x2 + x3 >= 10 + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [2, 0, 0, 0, -1, -1, 0, 0, 20], + ... [-2, -3, -1, 0, 0, 0, 0, 0, 0], + ... [1, 1, 1, 1, 0, 0, 0, 0, 40], + ... [2, 1, -1, 0, -1, 0, 1, 0, 10], + ... [0, -1, 1, 0, 0, -1, 0, 1, 10.] + ... ]), 3, 2).run_simplex().items()} + {'P': 70.0, 'x1': 10.0, 'x2': 10.0, 'x3': 20.0} + + # Non standard: minimisation and equalities + Min: x1 + x2 + ST: 2x1 + x2 = 12 + 6x1 + 5x2 = 40 + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [8, 6, 0, 0, 52], + ... [1, 1, 0, 0, 0], + ... [2, 1, 1, 0, 12], + ... [6, 5, 0, 1, 40.], + ... ]), 2, 2).run_simplex().items()} + {'P': 7.0, 'x1': 5.0, 'x2': 2.0} + + + # Pivot on slack variables + Max: 8x1 + 6x2 + ST: x1 + 3x2 <= 33 + 4x1 + 2x2 <= 48 + 2x1 + 4x2 <= 48 + x1 + x2 >= 10 + x1 >= 2 + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [2, 1, 0, 0, 0, -1, -1, 0, 0, 12.0], + ... [-8, -6, 0, 0, 0, 0, 0, 0, 0, 0.0], + ... [1, 3, 1, 0, 0, 0, 0, 0, 0, 33.0], + ... [4, 2, 0, 1, 0, 0, 0, 0, 0, 60.0], + ... [2, 4, 0, 0, 1, 0, 0, 0, 0, 48.0], + ... [1, 1, 0, 0, 0, -1, 0, 1, 0, 10.0], + ... [1, 0, 0, 0, 0, 0, -1, 0, 1, 2.0] + ... ]), 2, 2).run_simplex().items()} # doctest: +ELLIPSIS + {'P': 132.0, 'x1': 12.000... 'x2': 5.999...} + """ + # Stop simplex algorithm from cycling. + for _ in range(Tableau.maxiter): + # Completion of each stage removes an objective. If both stages + # are complete, then no objectives are left + if not self.objectives: + # Find the values of each variable at optimal solution + return self.interpret_tableau() + + row_idx, col_idx = self.find_pivot() + + # If there are no more negative values in objective row + if self.stop_iter: + # Delete artificial variable columns and rows. Update attributes + self.tableau = self.change_stage() + else: + self.tableau = self.pivot(row_idx, col_idx) + return {} + + def interpret_tableau(self) -> dict[str, float]: + """Given the final tableau, add the corresponding values of the basic + decision variables to the `output_dict` + >>> {key: float(value) for key, value in Tableau(np.array([ + ... [0,0,0.875,0.375,5], + ... [0,1,0.375,-0.125,1], + ... [1,0,-0.125,0.375,1] + ... ]),2, 0).interpret_tableau().items()} + {'P': 5.0, 'x1': 1.0, 'x2': 1.0} + """ + # P = RHS of final tableau + output_dict = {"P": abs(self.tableau[0, -1])} + + for i in range(self.n_vars): + # Gives indices of nonzero entries in the ith column + nonzero = np.nonzero(self.tableau[:, i]) + n_nonzero = len(nonzero[0]) + + # First entry in the nonzero indices + nonzero_rowidx = nonzero[0][0] + nonzero_val = self.tableau[nonzero_rowidx, i] + + # If there is only one nonzero value in column, which is one + if n_nonzero == 1 and nonzero_val == 1: + rhs_val = self.tableau[nonzero_rowidx, -1] + output_dict[self.col_titles[i]] = rhs_val + return output_dict + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/apriori_algorithm.py b/machine_learning/apriori_algorithm.py new file mode 100644 index 000000000000..09a89ac236bd --- /dev/null +++ b/machine_learning/apriori_algorithm.py @@ -0,0 +1,113 @@ +""" +Apriori Algorithm is a Association rule mining technique, also known as market basket +analysis, aims to discover interesting relationships or associations among a set of +items in a transactional or relational database. + +For example, Apriori Algorithm states: "If a customer buys item A and item B, then they +are likely to buy item C." This rule suggests a relationship between items A, B, and C, +indicating that customers who purchased A and B are more likely to also purchase item C. + +WIKI: https://en.wikipedia.org/wiki/Apriori_algorithm +Examples: https://www.kaggle.com/code/earthian/apriori-association-rules-mining +""" + +from itertools import combinations + + +def load_data() -> list[list[str]]: + """ + Returns a sample transaction dataset. + + >>> load_data() + [['milk'], ['milk', 'butter'], ['milk', 'bread'], ['milk', 'bread', 'chips']] + """ + return [["milk"], ["milk", "butter"], ["milk", "bread"], ["milk", "bread", "chips"]] + + +def prune(itemset: list, candidates: list, length: int) -> list: + """ + Prune candidate itemsets that are not frequent. + The goal of pruning is to filter out candidate itemsets that are not frequent. This + is done by checking if all the (k-1) subsets of a candidate itemset are present in + the frequent itemsets of the previous iteration (valid subsequences of the frequent + itemsets from the previous iteration). + + Prunes candidate itemsets that are not frequent. + + >>> itemset = ['X', 'Y', 'Z'] + >>> candidates = [['X', 'Y'], ['X', 'Z'], ['Y', 'Z']] + >>> prune(itemset, candidates, 2) + [['X', 'Y'], ['X', 'Z'], ['Y', 'Z']] + + >>> itemset = ['1', '2', '3', '4'] + >>> candidates = ['1', '2', '4'] + >>> prune(itemset, candidates, 3) + [] + """ + pruned = [] + for candidate in candidates: + is_subsequence = True + for item in candidate: + if item not in itemset or itemset.count(item) < length - 1: + is_subsequence = False + break + if is_subsequence: + pruned.append(candidate) + return pruned + + +def apriori(data: list[list[str]], min_support: int) -> list[tuple[list[str], int]]: + """ + Returns a list of frequent itemsets and their support counts. + + >>> data = [['A', 'B', 'C'], ['A', 'B'], ['A', 'C'], ['A', 'D'], ['B', 'C']] + >>> apriori(data, 2) + [(['A', 'B'], 1), (['A', 'C'], 2), (['B', 'C'], 2)] + + >>> data = [['1', '2', '3'], ['1', '2'], ['1', '3'], ['1', '4'], ['2', '3']] + >>> apriori(data, 3) + [] + """ + itemset = [list(transaction) for transaction in data] + frequent_itemsets = [] + length = 1 + + while itemset: + # Count itemset support + counts = [0] * len(itemset) + for transaction in data: + for j, candidate in enumerate(itemset): + if all(item in transaction for item in candidate): + counts[j] += 1 + + # Prune infrequent itemsets + itemset = [item for i, item in enumerate(itemset) if counts[i] >= min_support] + + # Append frequent itemsets (as a list to maintain order) + for i, item in enumerate(itemset): + frequent_itemsets.append((sorted(item), counts[i])) + + length += 1 + itemset = prune(itemset, list(combinations(itemset, length)), length) + + return frequent_itemsets + + +if __name__ == "__main__": + """ + Apriori algorithm for finding frequent itemsets. + + Args: + data: A list of transactions, where each transaction is a list of items. + min_support: The minimum support threshold for frequent itemsets. + + Returns: + A list of frequent itemsets along with their support counts. + """ + import doctest + + doctest.testmod() + + # user-defined threshold or minimum support level + frequent_itemsets = apriori(data=load_data(), min_support=2) + print("\n".join(f"{itemset}: {support}" for itemset, support in frequent_itemsets)) diff --git a/machine_learning/astar.py b/machine_learning/astar.py index 7a60ed225a2d..a5859e51fe70 100644 --- a/machine_learning/astar.py +++ b/machine_learning/astar.py @@ -12,6 +12,7 @@ https://en.wikipedia.org/wiki/A*_search_algorithm """ + import numpy as np @@ -57,7 +58,7 @@ def __init__(self, world_size=(5, 5)): def show(self): print(self.w) - def get_neigbours(self, cell): + def get_neighbours(self, cell): """ Return the neighbours of cell """ @@ -110,7 +111,7 @@ def astar(world, start, goal): _closed.append(_open.pop(min_f)) if current == goal: break - for n in world.get_neigbours(current): + for n in world.get_neighbours(current): for c in _closed: if c == n: continue diff --git a/machine_learning/automatic_differentiation.py b/machine_learning/automatic_differentiation.py new file mode 100644 index 000000000000..5c2708247c21 --- /dev/null +++ b/machine_learning/automatic_differentiation.py @@ -0,0 +1,328 @@ +""" +Demonstration of the Automatic Differentiation (Reverse mode). + +Reference: https://en.wikipedia.org/wiki/Automatic_differentiation + +Author: Poojan Smart +Email: smrtpoojan@gmail.com +""" + +from __future__ import annotations + +from collections import defaultdict +from enum import Enum +from types import TracebackType +from typing import Any + +import numpy as np +from typing_extensions import Self # noqa: UP035 + + +class OpType(Enum): + """ + Class represents list of supported operations on Variable for gradient calculation. + """ + + ADD = 0 + SUB = 1 + MUL = 2 + DIV = 3 + MATMUL = 4 + POWER = 5 + NOOP = 6 + + +class Variable: + """ + Class represents n-dimensional object which is used to wrap numpy array on which + operations will be performed and the gradient will be calculated. + + Examples: + >>> Variable(5.0) + Variable(5.0) + >>> Variable([5.0, 2.9]) + Variable([5. 2.9]) + >>> Variable([5.0, 2.9]) + Variable([1.0, 5.5]) + Variable([6. 8.4]) + >>> Variable([[8.0, 10.0]]) + Variable([[ 8. 10.]]) + """ + + def __init__(self, value: Any) -> None: + self.value = np.array(value) + + # pointers to the operations to which the Variable is input + self.param_to: list[Operation] = [] + # pointer to the operation of which the Variable is output of + self.result_of: Operation = Operation(OpType.NOOP) + + def __repr__(self) -> str: + return f"Variable({self.value})" + + def to_ndarray(self) -> np.ndarray: + return self.value + + def __add__(self, other: Variable) -> Variable: + result = Variable(self.value + other.value) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append(OpType.ADD, params=[self, other], output=result) + return result + + def __sub__(self, other: Variable) -> Variable: + result = Variable(self.value - other.value) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append(OpType.SUB, params=[self, other], output=result) + return result + + def __mul__(self, other: Variable) -> Variable: + result = Variable(self.value * other.value) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append(OpType.MUL, params=[self, other], output=result) + return result + + def __truediv__(self, other: Variable) -> Variable: + result = Variable(self.value / other.value) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append(OpType.DIV, params=[self, other], output=result) + return result + + def __matmul__(self, other: Variable) -> Variable: + result = Variable(self.value @ other.value) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append(OpType.MATMUL, params=[self, other], output=result) + return result + + def __pow__(self, power: int) -> Variable: + result = Variable(self.value**power) + + with GradientTracker() as tracker: + # if tracker is enabled, computation graph will be updated + if tracker.enabled: + tracker.append( + OpType.POWER, + params=[self], + output=result, + other_params={"power": power}, + ) + return result + + def add_param_to(self, param_to: Operation) -> None: + self.param_to.append(param_to) + + def add_result_of(self, result_of: Operation) -> None: + self.result_of = result_of + + +class Operation: + """ + Class represents operation between single or two Variable objects. + Operation objects contains type of operation, pointers to input Variable + objects and pointer to resulting Variable from the operation. + """ + + def __init__( + self, + op_type: OpType, + other_params: dict | None = None, + ) -> None: + self.op_type = op_type + self.other_params = {} if other_params is None else other_params + + def add_params(self, params: list[Variable]) -> None: + self.params = params + + def add_output(self, output: Variable) -> None: + self.output = output + + def __eq__(self, value) -> bool: + return self.op_type == value if isinstance(value, OpType) else False + + +class GradientTracker: + """ + Class contains methods to compute partial derivatives of Variable + based on the computation graph. + + Examples: + + >>> with GradientTracker() as tracker: + ... a = Variable([2.0, 5.0]) + ... b = Variable([1.0, 2.0]) + ... m = Variable([1.0, 2.0]) + ... c = a + b + ... d = a * b + ... e = c / d + >>> tracker.gradient(e, a) + array([-0.25, -0.04]) + >>> tracker.gradient(e, b) + array([-1. , -0.25]) + >>> tracker.gradient(e, m) is None + True + + >>> with GradientTracker() as tracker: + ... a = Variable([[2.0, 5.0]]) + ... b = Variable([[1.0], [2.0]]) + ... c = a @ b + >>> tracker.gradient(c, a) + array([[1., 2.]]) + >>> tracker.gradient(c, b) + array([[2.], + [5.]]) + + >>> with GradientTracker() as tracker: + ... a = Variable([[2.0, 5.0]]) + ... b = a ** 3 + >>> tracker.gradient(b, a) + array([[12., 75.]]) + """ + + instance = None + + def __new__(cls) -> Self: + """ + Executes at the creation of class object and returns if + object is already created. This class follows singleton + design pattern. + """ + if cls.instance is None: + cls.instance = super().__new__(cls) + return cls.instance + + def __init__(self) -> None: + self.enabled = False + + def __enter__(self) -> Self: + self.enabled = True + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.enabled = False + + def append( + self, + op_type: OpType, + params: list[Variable], + output: Variable, + other_params: dict | None = None, + ) -> None: + """ + Adds Operation object to the related Variable objects for + creating computational graph for calculating gradients. + + Args: + op_type: Operation type + params: Input parameters to the operation + output: Output variable of the operation + """ + operation = Operation(op_type, other_params=other_params) + param_nodes = [] + for param in params: + param.add_param_to(operation) + param_nodes.append(param) + output.add_result_of(operation) + + operation.add_params(param_nodes) + operation.add_output(output) + + def gradient(self, target: Variable, source: Variable) -> np.ndarray | None: + """ + Reverse accumulation of partial derivatives to calculate gradients + of target variable with respect to source variable. + + Args: + target: target variable for which gradients are calculated. + source: source variable with respect to which the gradients are + calculated. + + Returns: + Gradient of the source variable with respect to the target variable + """ + + # partial derivatives with respect to target + partial_deriv = defaultdict(lambda: 0) + partial_deriv[target] = np.ones_like(target.to_ndarray()) + + # iterating through each operations in the computation graph + operation_queue = [target.result_of] + while len(operation_queue) > 0: + operation = operation_queue.pop() + for param in operation.params: + # as per the chain rule, multiplying partial derivatives + # of variables with respect to the target + dparam_doutput = self.derivative(param, operation) + dparam_dtarget = dparam_doutput * partial_deriv[operation.output] + partial_deriv[param] += dparam_dtarget + + if param.result_of and param.result_of != OpType.NOOP: + operation_queue.append(param.result_of) + + return partial_deriv.get(source) + + def derivative(self, param: Variable, operation: Operation) -> np.ndarray: + """ + Compute the derivative of given operation/function + + Args: + param: variable to be differentiated + operation: function performed on the input variable + + Returns: + Derivative of input variable with respect to the output of + the operation + """ + params = operation.params + + if operation == OpType.ADD: + return np.ones_like(params[0].to_ndarray(), dtype=np.float64) + if operation == OpType.SUB: + if params[0] == param: + return np.ones_like(params[0].to_ndarray(), dtype=np.float64) + return -np.ones_like(params[1].to_ndarray(), dtype=np.float64) + if operation == OpType.MUL: + return ( + params[1].to_ndarray().T + if params[0] == param + else params[0].to_ndarray().T + ) + if operation == OpType.DIV: + if params[0] == param: + return 1 / params[1].to_ndarray() + return -params[0].to_ndarray() / (params[1].to_ndarray() ** 2) + if operation == OpType.MATMUL: + return ( + params[1].to_ndarray().T + if params[0] == param + else params[0].to_ndarray().T + ) + if operation == OpType.POWER: + power = operation.other_params["power"] + return power * (params[0].to_ndarray() ** (power - 1)) + + err_msg = f"invalid operation type: {operation.op_type}" + raise ValueError(err_msg) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/data_transformations.py b/machine_learning/data_transformations.py index ecfd3b9e27c2..a1c28d514fd5 100644 --- a/machine_learning/data_transformations.py +++ b/machine_learning/data_transformations.py @@ -25,6 +25,7 @@ 2. non-gaussian (non-normal) distributions work better with normalization 3. If a column or list of values has extreme values / outliers, use standardization """ + from statistics import mean, stdev diff --git a/machine_learning/decision_tree.py b/machine_learning/decision_tree.py index 7cd1b02c4181..72970431c3fc 100644 --- a/machine_learning/decision_tree.py +++ b/machine_learning/decision_tree.py @@ -3,6 +3,7 @@ Input data set: The input data set must be 1-dimensional with continuous labels. Output: The decision tree maps a real number input to a real number output. """ + import numpy as np @@ -18,22 +19,22 @@ def __init__(self, depth=5, min_leaf_size=5): def mean_squared_error(self, labels, prediction): """ mean_squared_error: - @param labels: a one dimensional numpy array + @param labels: a one-dimensional numpy array @param prediction: a floating point value return value: mean_squared_error calculates the error if prediction is used to estimate the labels >>> tester = DecisionTree() >>> test_labels = np.array([1,2,3,4,5,6,7,8,9,10]) >>> test_prediction = float(6) - >>> tester.mean_squared_error(test_labels, test_prediction) == ( + >>> bool(tester.mean_squared_error(test_labels, test_prediction) == ( ... TestDecisionTree.helper_mean_squared_error_test(test_labels, - ... test_prediction)) + ... test_prediction))) True >>> test_labels = np.array([1,2,3]) >>> test_prediction = float(2) - >>> tester.mean_squared_error(test_labels, test_prediction) == ( + >>> bool(tester.mean_squared_error(test_labels, test_prediction) == ( ... TestDecisionTree.helper_mean_squared_error_test(test_labels, - ... test_prediction)) + ... test_prediction))) True """ if labels.ndim != 1: @@ -44,26 +45,47 @@ def mean_squared_error(self, labels, prediction): def train(self, x, y): """ train: - @param x: a one dimensional numpy array - @param y: a one dimensional numpy array. + @param x: a one-dimensional numpy array + @param y: a one-dimensional numpy array. The contents of y are the labels for the corresponding X values - train does not have a return value - """ - - """ - this section is to check that the inputs conform to our dimensionality + train() does not have a return value + + Examples: + 1. Try to train when x & y are of same length & 1 dimensions (No errors) + >>> dt = DecisionTree() + >>> dt.train(np.array([10,20,30,40,50]),np.array([0,0,0,1,1])) + + 2. Try to train when x is 2 dimensions + >>> dt = DecisionTree() + >>> dt.train(np.array([[1,2,3,4,5],[1,2,3,4,5]]),np.array([0,0,0,1,1])) + Traceback (most recent call last): + ... + ValueError: Input data set must be one-dimensional + + 3. Try to train when x and y are not of the same length + >>> dt = DecisionTree() + >>> dt.train(np.array([1,2,3,4,5]),np.array([[0,0,0,1,1],[0,0,0,1,1]])) + Traceback (most recent call last): + ... + ValueError: x and y have different lengths + + 4. Try to train when x & y are of the same length but different dimensions + >>> dt = DecisionTree() + >>> dt.train(np.array([1,2,3,4,5]),np.array([[1],[2],[3],[4],[5]])) + Traceback (most recent call last): + ... + ValueError: Data set labels must be one-dimensional + + This section is to check that the inputs conform to our dimensionality constraints """ if x.ndim != 1: - print("Error: Input data set must be one dimensional") - return + raise ValueError("Input data set must be one-dimensional") if len(x) != len(y): - print("Error: X and y have different lengths") - return + raise ValueError("x and y have different lengths") if y.ndim != 1: - print("Error: Data set labels must be one dimensional") - return + raise ValueError("Data set labels must be one-dimensional") if len(x) < 2 * self.min_leaf_size: self.prediction = np.mean(y) @@ -83,7 +105,7 @@ def train(self, x, y): the predictor """ for i in range(len(x)): - if len(x[:i]) < self.min_leaf_size: + if len(x[:i]) < self.min_leaf_size: # noqa: SIM114 continue elif len(x[i:]) < self.min_leaf_size: continue @@ -165,7 +187,8 @@ def main(): tree = DecisionTree(depth=10, min_leaf_size=10) tree.train(x, y) - test_cases = (np.random.rand(10) * 2) - 1 + rng = np.random.default_rng() + test_cases = (rng.random(10) * 2) - 1 predictions = np.array([tree.predict(x) for x in test_cases]) avg_error = np.mean((predictions - test_cases) ** 2) diff --git a/machine_learning/dimensionality_reduction.py b/machine_learning/dimensionality_reduction.py new file mode 100644 index 000000000000..50d442ecc3de --- /dev/null +++ b/machine_learning/dimensionality_reduction.py @@ -0,0 +1,198 @@ +# Copyright (c) 2023 Diego Gasco (diego.gasco99@gmail.com), Diegomangasco on GitHub + +""" +Requirements: + - numpy version 1.21 + - scipy version 1.3.3 +Notes: + - Each column of the features matrix corresponds to a class item +""" + +import logging + +import numpy as np +import pytest +from scipy.linalg import eigh + +logging.basicConfig(level=logging.INFO, format="%(message)s") + + +def column_reshape(input_array: np.ndarray) -> np.ndarray: + """Function to reshape a row Numpy array into a column Numpy array + >>> input_array = np.array([1, 2, 3]) + >>> column_reshape(input_array) + array([[1], + [2], + [3]]) + """ + + return input_array.reshape((input_array.size, 1)) + + +def covariance_within_classes( + features: np.ndarray, labels: np.ndarray, classes: int +) -> np.ndarray: + """Function to compute the covariance matrix inside each class. + >>> features = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> labels = np.array([0, 1, 0]) + >>> covariance_within_classes(features, labels, 2) + array([[0.66666667, 0.66666667, 0.66666667], + [0.66666667, 0.66666667, 0.66666667], + [0.66666667, 0.66666667, 0.66666667]]) + """ + + covariance_sum = np.nan + for i in range(classes): + data = features[:, labels == i] + data_mean = data.mean(1) + # Centralize the data of class i + centered_data = data - column_reshape(data_mean) + if i > 0: + # If covariance_sum is not None + covariance_sum += np.dot(centered_data, centered_data.T) + else: + # If covariance_sum is np.nan (i.e. first loop) + covariance_sum = np.dot(centered_data, centered_data.T) + + return covariance_sum / features.shape[1] + + +def covariance_between_classes( + features: np.ndarray, labels: np.ndarray, classes: int +) -> np.ndarray: + """Function to compute the covariance matrix between multiple classes + >>> features = np.array([[9, 2, 3], [4, 3, 6], [1, 8, 9]]) + >>> labels = np.array([0, 1, 0]) + >>> covariance_between_classes(features, labels, 2) + array([[ 3.55555556, 1.77777778, -2.66666667], + [ 1.77777778, 0.88888889, -1.33333333], + [-2.66666667, -1.33333333, 2. ]]) + """ + + general_data_mean = features.mean(1) + covariance_sum = np.nan + for i in range(classes): + data = features[:, labels == i] + device_data = data.shape[1] + data_mean = data.mean(1) + if i > 0: + # If covariance_sum is not None + covariance_sum += device_data * np.dot( + column_reshape(data_mean) - column_reshape(general_data_mean), + (column_reshape(data_mean) - column_reshape(general_data_mean)).T, + ) + else: + # If covariance_sum is np.nan (i.e. first loop) + covariance_sum = device_data * np.dot( + column_reshape(data_mean) - column_reshape(general_data_mean), + (column_reshape(data_mean) - column_reshape(general_data_mean)).T, + ) + + return covariance_sum / features.shape[1] + + +def principal_component_analysis(features: np.ndarray, dimensions: int) -> np.ndarray: + """ + Principal Component Analysis. + + For more details, see: https://en.wikipedia.org/wiki/Principal_component_analysis. + Parameters: + * features: the features extracted from the dataset + * dimensions: to filter the projected data for the desired dimension + + >>> test_principal_component_analysis() + """ + + # Check if the features have been loaded + if features.any(): + data_mean = features.mean(1) + # Center the dataset + centered_data = features - np.reshape(data_mean, (data_mean.size, 1)) + covariance_matrix = np.dot(centered_data, centered_data.T) / features.shape[1] + _, eigenvectors = np.linalg.eigh(covariance_matrix) + # Take all the columns in the reverse order (-1), and then takes only the first + filtered_eigenvectors = eigenvectors[:, ::-1][:, 0:dimensions] + # Project the database on the new space + projected_data = np.dot(filtered_eigenvectors.T, features) + logging.info("Principal Component Analysis computed") + + return projected_data + else: + logging.basicConfig(level=logging.ERROR, format="%(message)s", force=True) + logging.error("Dataset empty") + raise AssertionError + + +def linear_discriminant_analysis( + features: np.ndarray, labels: np.ndarray, classes: int, dimensions: int +) -> np.ndarray: + """ + Linear Discriminant Analysis. + + For more details, see: https://en.wikipedia.org/wiki/Linear_discriminant_analysis. + Parameters: + * features: the features extracted from the dataset + * labels: the class labels of the features + * classes: the number of classes present in the dataset + * dimensions: to filter the projected data for the desired dimension + + >>> test_linear_discriminant_analysis() + """ + + # Check if the dimension desired is less than the number of classes + assert classes > dimensions + + # Check if features have been already loaded + if features.any: + _, eigenvectors = eigh( + covariance_between_classes(features, labels, classes), + covariance_within_classes(features, labels, classes), + ) + filtered_eigenvectors = eigenvectors[:, ::-1][:, :dimensions] + svd_matrix, _, _ = np.linalg.svd(filtered_eigenvectors) + filtered_svd_matrix = svd_matrix[:, 0:dimensions] + projected_data = np.dot(filtered_svd_matrix.T, features) + logging.info("Linear Discriminant Analysis computed") + + return projected_data + else: + logging.basicConfig(level=logging.ERROR, format="%(message)s", force=True) + logging.error("Dataset empty") + raise AssertionError + + +def test_linear_discriminant_analysis() -> None: + # Create dummy dataset with 2 classes and 3 features + features = np.array([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7]]) + labels = np.array([0, 0, 0, 1, 1]) + classes = 2 + dimensions = 2 + + # Assert that the function raises an AssertionError if dimensions > classes + with pytest.raises(AssertionError) as error_info: # noqa: PT012 + projected_data = linear_discriminant_analysis( + features, labels, classes, dimensions + ) + if isinstance(projected_data, np.ndarray): + raise AssertionError( + "Did not raise AssertionError for dimensions > classes" + ) + assert error_info.type is AssertionError + + +def test_principal_component_analysis() -> None: + features = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + dimensions = 2 + expected_output = np.array([[6.92820323, 8.66025404, 10.39230485], [3.0, 3.0, 3.0]]) + + with pytest.raises(AssertionError) as error_info: # noqa: PT012 + output = principal_component_analysis(features, dimensions) + if not np.allclose(expected_output, output): + raise AssertionError + assert error_info.type is AssertionError + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/forecasting/ex_data.csv b/machine_learning/forecasting/ex_data.csv index 1c429e649755..e6e73c4a1ca4 100644 --- a/machine_learning/forecasting/ex_data.csv +++ b/machine_learning/forecasting/ex_data.csv @@ -1,4 +1,4 @@ -total_user,total_events,days +total_users,total_events,days 18231,0.0,1 22621,1.0,2 15675,0.0,3 diff --git a/machine_learning/forecasting/run.py b/machine_learning/forecasting/run.py index 0909b76d8907..9d81b03cd09e 100644 --- a/machine_learning/forecasting/run.py +++ b/machine_learning/forecasting/run.py @@ -1,6 +1,6 @@ """ this is code for forecasting -but i modified it and used it for safety checker of data +but I modified it and used it for safety checker of data for ex: you have an online shop and for some reason some data are missing (the amount of data that u expected are not supposed to be) then we can use it @@ -11,6 +11,8 @@ u can just adjust it for ur own purpose """ +from warnings import simplefilter + import numpy as np import pandas as pd from sklearn.preprocessing import Normalizer @@ -26,7 +28,7 @@ def linear_regression_prediction( input : training data (date, total_user, total_event) in list of float output : list of total user prediction in float >>> n = linear_regression_prediction([2,3,4,5], [5,3,4,6], [3,1,2,4], [2,1], [2,2]) - >>> abs(n - 5.0) < 1e-6 # Checking precision because of floating point errors + >>> bool(abs(n - 5.0) < 1e-6) # Checking precision because of floating point errors True """ x = np.array([[1, item, train_mtch[i]] for i, item in enumerate(train_dt)]) @@ -45,14 +47,16 @@ def sarimax_predictor(train_user: list, train_match: list, test_match: list) -> >>> sarimax_predictor([4,2,6,8], [3,1,2,4], [2]) 6.6666671111109626 """ + # Suppress the User Warning raised by SARIMAX due to insufficient observations + simplefilter("ignore", UserWarning) order = (1, 2, 1) - seasonal_order = (1, 1, 0, 7) + seasonal_order = (1, 1, 1, 7) model = SARIMAX( train_user, exog=train_match, order=order, seasonal_order=seasonal_order ) model_fit = model.fit(disp=False, maxiter=600, method="nm") result = model_fit.predict(1, len(test_match), exog=[test_match]) - return result[0] + return float(result[0]) def support_vector_regressor(x_train: list, x_test: list, train_user: list) -> float: @@ -71,7 +75,7 @@ def support_vector_regressor(x_train: list, x_test: list, train_user: list) -> f regressor = SVR(kernel="rbf", C=1, gamma=0.1, epsilon=0.1) regressor.fit(x_train, train_user) y_pred = regressor.predict(x_test) - return y_pred[0] + return float(y_pred[0]) def interquartile_range_checker(train_user: list) -> float: @@ -88,7 +92,7 @@ def interquartile_range_checker(train_user: list) -> float: q3 = np.percentile(train_user, 75) iqr = q3 - q1 low_lim = q1 - (iqr * 0.1) - return low_lim + return float(low_lim) def data_safety_checker(list_vote: list, actual_result: float) -> bool: @@ -102,28 +106,26 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool: """ safe = 0 not_safe = 0 + + if not isinstance(actual_result, float): + raise TypeError("Actual result should be float. Value passed is a list") + for i in list_vote: if i > actual_result: safe = not_safe + 1 + elif abs(abs(i) - abs(actual_result)) <= 0.1: + safe += 1 else: - if abs(abs(i) - abs(actual_result)) <= 0.1: - safe += 1 - else: - not_safe += 1 + not_safe += 1 return safe > not_safe if __name__ == "__main__": - # data_input_df = pd.read_csv("ex_data.csv", header=None) - data_input = [[18231, 0.0, 1], [22621, 1.0, 2], [15675, 0.0, 3], [23583, 1.0, 4]] - data_input_df = pd.DataFrame( - data_input, columns=["total_user", "total_even", "days"] - ) - """ data column = total user in a day, how much online event held in one day, what day is that(sunday-saturday) """ + data_input_df = pd.read_csv("ex_data.csv") # start normalization normalize_df = Normalizer().fit_transform(data_input_df.values) @@ -138,23 +140,23 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool: x_test = x[len(x) - 1 :] # for linear regression & sarimax - trn_date = total_date[: len(total_date) - 1] - trn_user = total_user[: len(total_user) - 1] - trn_match = total_match[: len(total_match) - 1] + train_date = total_date[: len(total_date) - 1] + train_user = total_user[: len(total_user) - 1] + train_match = total_match[: len(total_match) - 1] - tst_date = total_date[len(total_date) - 1 :] - tst_user = total_user[len(total_user) - 1 :] - tst_match = total_match[len(total_match) - 1 :] + test_date = total_date[len(total_date) - 1 :] + test_user = total_user[len(total_user) - 1 :] + test_match = total_match[len(total_match) - 1 :] # voting system with forecasting res_vote = [ linear_regression_prediction( - trn_date, trn_user, trn_match, tst_date, tst_match + train_date, train_user, train_match, test_date, test_match ), - sarimax_predictor(trn_user, trn_match, tst_match), - support_vector_regressor(x_train, x_test, trn_user), + sarimax_predictor(train_user, train_match, test_match), + support_vector_regressor(x_train, x_test, train_user), ] # check the safety of today's data - not_str = "" if data_safety_checker(res_vote, tst_user) else "not " - print("Today's data is {not_str}safe.") + not_str = "" if data_safety_checker(res_vote, test_user[0]) else "not " + print(f"Today's data is {not_str}safe.") diff --git a/machine_learning/frequent_pattern_growth.py b/machine_learning/frequent_pattern_growth.py new file mode 100644 index 000000000000..fae2df16efb1 --- /dev/null +++ b/machine_learning/frequent_pattern_growth.py @@ -0,0 +1,350 @@ +""" +The Frequent Pattern Growth algorithm (FP-Growth) is a widely used data mining +technique for discovering frequent itemsets in large transaction databases. + +It overcomes some of the limitations of traditional methods such as Apriori by +efficiently constructing the FP-Tree + +WIKI: https://athena.ecs.csus.edu/~mei/associationcw/FpGrowth.html + +Examples: https://www.javatpoint.com/fp-growth-algorithm-in-data-mining +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class TreeNode: + """ + A node in a Frequent Pattern tree. + + Args: + name: The name of this node. + num_occur: The number of occurrences of the node. + parent_node: The parent node. + + Example: + >>> parent = TreeNode("Parent", 1, None) + >>> child = TreeNode("Child", 2, parent) + >>> child.name + 'Child' + >>> child.count + 2 + """ + + name: str + count: int + parent: TreeNode | None = None + children: dict[str, TreeNode] = field(default_factory=dict) + node_link: TreeNode | None = None + + def __repr__(self) -> str: + return f"TreeNode({self.name!r}, {self.count!r}, {self.parent!r})" + + def inc(self, num_occur: int) -> None: + self.count += num_occur + + def disp(self, ind: int = 1) -> None: + print(f"{' ' * ind} {self.name} {self.count}") + for child in self.children.values(): + child.disp(ind + 1) + + +def create_tree(data_set: list, min_sup: int = 1) -> tuple[TreeNode, dict]: + """ + Create Frequent Pattern tree + + Args: + data_set: A list of transactions, where each transaction is a list of items. + min_sup: The minimum support threshold. + Items with support less than this will be pruned. Default is 1. + + Returns: + The root of the FP-Tree. + header_table: The header table dictionary with item information. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> len(header_table) + 4 + >>> header_table["A"] + [[4, None], TreeNode('A', 4, TreeNode('Null Set', 1, None))] + >>> header_table["E"][1] # doctest: +NORMALIZE_WHITESPACE + TreeNode('E', 1, TreeNode('B', 3, TreeNode('A', 4, TreeNode('Null Set', 1, None)))) + >>> sorted(header_table) + ['A', 'B', 'C', 'E'] + >>> fp_tree.name + 'Null Set' + >>> sorted(fp_tree.children) + ['A', 'B'] + >>> fp_tree.children['A'].name + 'A' + >>> sorted(fp_tree.children['A'].children) + ['B', 'C'] + """ + header_table: dict = {} + for trans in data_set: + for item in trans: + header_table[item] = header_table.get(item, [0, None]) + header_table[item][0] += 1 + + for k in list(header_table): + if header_table[k][0] < min_sup: + del header_table[k] + + if not (freq_item_set := set(header_table)): + return TreeNode("Null Set", 1, None), {} + + for key, value in header_table.items(): + header_table[key] = [value, None] + + fp_tree = TreeNode("Null Set", 1, None) # Parent is None for the root node + for tran_set in data_set: + local_d = { + item: header_table[item][0] for item in tran_set if item in freq_item_set + } + if local_d: + sorted_items = sorted( + local_d.items(), key=lambda item_info: item_info[1], reverse=True + ) + ordered_items = [item[0] for item in sorted_items] + update_tree(ordered_items, fp_tree, header_table, 1) + + return fp_tree, header_table + + +def update_tree(items: list, in_tree: TreeNode, header_table: dict, count: int) -> None: + """ + Update the FP-Tree with a transaction. + + Args: + items: List of items in the transaction. + in_tree: The current node in the FP-Tree. + header_table: The header table dictionary with item information. + count: The count of the transaction. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> transaction = ['A', 'B', 'E'] + >>> update_tree(transaction, fp_tree, header_table, 1) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> fp_tree.children['A'].children['B'].children['E'].children + {} + >>> fp_tree.children['A'].children['B'].children['E'].count + 2 + >>> header_table['E'][1].name + 'E' + """ + if items[0] in in_tree.children: + in_tree.children[items[0]].inc(count) + else: + in_tree.children[items[0]] = TreeNode(items[0], count, in_tree) + if header_table[items[0]][1] is None: + header_table[items[0]][1] = in_tree.children[items[0]] + else: + update_header(header_table[items[0]][1], in_tree.children[items[0]]) + if len(items) > 1: + update_tree(items[1:], in_tree.children[items[0]], header_table, count) + + +def update_header(node_to_test: TreeNode, target_node: TreeNode) -> TreeNode: + """ + Update the header table with a node link. + + Args: + node_to_test: The node to be updated in the header table. + target_node: The node to link to. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> node1 = TreeNode("A", 3, None) + >>> node2 = TreeNode("B", 4, None) + >>> node1 + TreeNode('A', 3, None) + >>> node1 = update_header(node1, node2) + >>> node1 + TreeNode('A', 3, None) + >>> node1.node_link + TreeNode('B', 4, None) + >>> node2.node_link is None + True + """ + while node_to_test.node_link is not None: + node_to_test = node_to_test.node_link + if node_to_test.node_link is None: + node_to_test.node_link = target_node + # Return the updated node + return node_to_test + + +def ascend_tree(leaf_node: TreeNode, prefix_path: list[str]) -> None: + """ + Ascend the FP-Tree from a leaf node to its root, adding item names to the prefix + path. + + Args: + leaf_node: The leaf node to start ascending from. + prefix_path: A list to store the item as they are ascended. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + + >>> path = [] + >>> ascend_tree(fp_tree.children['A'], path) + >>> path # ascending from a leaf node 'A' + ['A'] + """ + if leaf_node.parent is not None: + prefix_path.append(leaf_node.name) + ascend_tree(leaf_node.parent, prefix_path) + + +def find_prefix_path(base_pat: frozenset, tree_node: TreeNode | None) -> dict: # noqa: ARG001 + """ + Find the conditional pattern base for a given base pattern. + + Args: + base_pat: The base pattern for which to find the conditional pattern base. + tree_node: The node in the FP-Tree. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> len(header_table) + 4 + >>> base_pattern = frozenset(['A']) + >>> sorted(find_prefix_path(base_pattern, fp_tree.children['A'])) + [] + """ + cond_pats: dict = {} + while tree_node is not None: + prefix_path: list = [] + ascend_tree(tree_node, prefix_path) + if len(prefix_path) > 1: + cond_pats[frozenset(prefix_path[1:])] = tree_node.count + tree_node = tree_node.node_link + return cond_pats + + +def mine_tree( + in_tree: TreeNode, # noqa: ARG001 + header_table: dict, + min_sup: int, + pre_fix: set, + freq_item_list: list, +) -> None: + """ + Mine the FP-Tree recursively to discover frequent itemsets. + + Args: + in_tree: The FP-Tree to mine. + header_table: The header table dictionary with item information. + min_sup: The minimum support threshold. + pre_fix: A set of items as a prefix for the itemsets being mined. + freq_item_list: A list to store the frequent itemsets. + + Example: + >>> data_set = [ + ... ['A', 'B', 'C'], + ... ['A', 'C'], + ... ['A', 'B', 'E'], + ... ['A', 'B', 'C', 'E'], + ... ['B', 'E'] + ... ] + >>> min_sup = 2 + >>> fp_tree, header_table = create_tree(data_set, min_sup) + >>> fp_tree + TreeNode('Null Set', 1, None) + >>> frequent_itemsets = [] + >>> mine_tree(fp_tree, header_table, min_sup, set([]), frequent_itemsets) + >>> expe_itm = [{'C'}, {'C', 'A'}, {'E'}, {'A', 'E'}, {'E', 'B'}, {'A'}, {'B'}] + >>> all(expected in frequent_itemsets for expected in expe_itm) + True + """ + sorted_items = sorted(header_table.items(), key=lambda item_info: item_info[1][0]) + big_l = [item[0] for item in sorted_items] + for base_pat in big_l: + new_freq_set = pre_fix.copy() + new_freq_set.add(base_pat) + freq_item_list.append(new_freq_set) + cond_patt_bases = find_prefix_path(base_pat, header_table[base_pat][1]) + my_cond_tree, my_head = create_tree(list(cond_patt_bases), min_sup) + if my_head is not None: + # Pass header_table[base_pat][1] as node_to_test to update_header + header_table[base_pat][1] = update_header( + header_table[base_pat][1], my_cond_tree + ) + mine_tree(my_cond_tree, my_head, min_sup, new_freq_set, freq_item_list) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + data_set: list[frozenset] = [ + frozenset(["bread", "milk", "cheese"]), + frozenset(["bread", "milk"]), + frozenset(["bread", "diapers"]), + frozenset(["bread", "milk", "diapers"]), + frozenset(["milk", "diapers"]), + frozenset(["milk", "cheese"]), + frozenset(["diapers", "cheese"]), + frozenset(["bread", "milk", "cheese", "diapers"]), + ] + print(f"{len(data_set) = }") + fp_tree, header_table = create_tree(data_set, min_sup=3) + print(f"{fp_tree = }") + print(f"{len(header_table) = }") + freq_items: list = [] + mine_tree(fp_tree, header_table, 3, set(), freq_items) + print(f"{freq_items = }") diff --git a/machine_learning/gradient_boosting_classifier.py b/machine_learning/gradient_boosting_classifier.py new file mode 100644 index 000000000000..2902394d8226 --- /dev/null +++ b/machine_learning/gradient_boosting_classifier.py @@ -0,0 +1,118 @@ +import numpy as np +from sklearn.datasets import load_iris +from sklearn.metrics import accuracy_score +from sklearn.model_selection import train_test_split +from sklearn.tree import DecisionTreeRegressor + + +class GradientBoostingClassifier: + def __init__(self, n_estimators: int = 100, learning_rate: float = 0.1) -> None: + """ + Initialize a GradientBoostingClassifier. + + Parameters: + - n_estimators (int): The number of weak learners to train. + - learning_rate (float): The learning rate for updating the model. + + Attributes: + - n_estimators (int): The number of weak learners. + - learning_rate (float): The learning rate. + - models (list): A list to store the trained weak learners. + """ + self.n_estimators = n_estimators + self.learning_rate = learning_rate + self.models: list[tuple[DecisionTreeRegressor, float]] = [] + + def fit(self, features: np.ndarray, target: np.ndarray) -> None: + """ + Fit the GradientBoostingClassifier to the training data. + + Parameters: + - features (np.ndarray): The training features. + - target (np.ndarray): The target values. + + Returns: + None + + >>> import numpy as np + >>> from sklearn.datasets import load_iris + >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1) + >>> iris = load_iris() + >>> X, y = iris.data, iris.target + >>> clf.fit(X, y) + >>> # Check if the model is trained + >>> len(clf.models) == 100 + True + """ + for _ in range(self.n_estimators): + # Calculate the pseudo-residuals + residuals = -self.gradient(target, self.predict(features)) + # Fit a weak learner (e.g., decision tree) to the residuals + model = DecisionTreeRegressor(max_depth=1) + model.fit(features, residuals) + # Update the model by adding the weak learner with a learning rate + self.models.append((model, self.learning_rate)) + + def predict(self, features: np.ndarray) -> np.ndarray: + """ + Make predictions on input data. + + Parameters: + - features (np.ndarray): The input data for making predictions. + + Returns: + - np.ndarray: An array of binary predictions (-1 or 1). + + >>> import numpy as np + >>> from sklearn.datasets import load_iris + >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1) + >>> iris = load_iris() + >>> X, y = iris.data, iris.target + >>> clf.fit(X, y) + >>> y_pred = clf.predict(X) + >>> # Check if the predictions have the correct shape + >>> y_pred.shape == y.shape + True + """ + # Initialize predictions with zeros + predictions = np.zeros(features.shape[0]) + for model, learning_rate in self.models: + predictions += learning_rate * model.predict(features) + return np.sign(predictions) # Convert to binary predictions (-1 or 1) + + def gradient(self, target: np.ndarray, y_pred: np.ndarray) -> np.ndarray: + """ + Calculate the negative gradient (pseudo-residuals) for logistic loss. + + Parameters: + - target (np.ndarray): The target values. + - y_pred (np.ndarray): The predicted values. + + Returns: + - np.ndarray: An array of pseudo-residuals. + + >>> import numpy as np + >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1) + >>> target = np.array([0, 1, 0, 1]) + >>> y_pred = np.array([0.2, 0.8, 0.3, 0.7]) + >>> residuals = clf.gradient(target, y_pred) + >>> # Check if residuals have the correct shape + >>> residuals.shape == target.shape + True + """ + return -target / (1 + np.exp(target * y_pred)) + + +if __name__ == "__main__": + iris = load_iris() + X, y = iris.data, iris.target + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.2, random_state=42 + ) + + clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1) + clf.fit(X_train, y_train) + + y_pred = clf.predict(X_test) + accuracy = accuracy_score(y_test, y_pred) + print(f"Accuracy: {accuracy:.2f}") diff --git a/machine_learning/gradient_descent.py b/machine_learning/gradient_descent.py index 9fa460a07562..95463faf5635 100644 --- a/machine_learning/gradient_descent.py +++ b/machine_learning/gradient_descent.py @@ -2,7 +2,8 @@ Implementation of gradient descent algorithm for minimizing cost of a linear hypothesis function. """ -import numpy + +import numpy as np # List of input, output pairs train_data = ( @@ -55,6 +56,7 @@ def output(example_no, data_set): return train_data[example_no][1] elif data_set == "test": return test_data[example_no][1] + return None def calculate_hypothesis_value(example_no, data_set): @@ -68,6 +70,7 @@ def calculate_hypothesis_value(example_no, data_set): return _hypothesis_value(train_data[example_no][0]) elif data_set == "test": return _hypothesis_value(test_data[example_no][0]) + return None def summation_of_cost_derivative(index, end=m): @@ -108,12 +111,12 @@ def run_gradient_descent(): while True: j += 1 temp_parameter_vector = [0, 0, 0, 0] - for i in range(0, len(parameter_vector)): + for i in range(len(parameter_vector)): cost_derivative = get_cost_derivative(i - 1) temp_parameter_vector[i] = ( parameter_vector[i] - LEARNING_RATE * cost_derivative ) - if numpy.allclose( + if np.allclose( parameter_vector, temp_parameter_vector, atol=absolute_error_limit, diff --git a/machine_learning/k_means_clust.py b/machine_learning/k_means_clust.py index b6305469ed7d..a926362fc18b 100644 --- a/machine_learning/k_means_clust.py +++ b/machine_learning/k_means_clust.py @@ -11,10 +11,10 @@ - initial_centroids , initial centroid values generated by utility function(mentioned in usage). - maxiter , maximum number of iterations to process. - - heterogeneity , empty list that will be filled with hetrogeneity values if passed + - heterogeneity , empty list that will be filled with heterogeneity values if passed to kmeans func. Usage: - 1. define 'k' value, 'X' features array and 'hetrogeneity' empty list + 1. define 'k' value, 'X' features array and 'heterogeneity' empty list 2. create initial_centroids, initial_centroids = get_initial_centroids( X, @@ -31,8 +31,8 @@ record_heterogeneity=heterogeneity, verbose=True # whether to print logs in console or not.(default=False) ) - 4. Plot the loss function, hetrogeneity values for every iteration saved in - hetrogeneity list. + 4. Plot the loss function and heterogeneity values for every iteration saved in + heterogeneity list. plot_heterogeneity( heterogeneity, k @@ -40,6 +40,7 @@ 5. Transfers Dataframe into excel format it must have feature called 'Clust' with k means clustering numbers in it. """ + import warnings import numpy as np @@ -54,12 +55,12 @@ def get_initial_centroids(data, k, seed=None): """Randomly choose k data points as initial centroids""" - if seed is not None: # useful for obtaining consistent results - np.random.seed(seed) + # useful for obtaining consistent results + rng = np.random.default_rng(seed) n = data.shape[0] # number of data points # Pick K indices from range [0, N). - rand_indices = np.random.randint(0, n, k) + rand_indices = rng.integers(0, n, k) # Keep centroids as dense format, as many entries will be nonzero due to averaging. # As long as at least one document in a cluster contains a word, @@ -128,7 +129,7 @@ def plot_heterogeneity(heterogeneity, k): def kmeans( data, k, initial_centroids, maxiter=500, record_heterogeneity=None, verbose=False ): - """This function runs k-means on given data and initial set of centroids. + """Runs k-means on given data and initial set of centroids. maxiter: maximum number of iterations to run.(default=500) record_heterogeneity: (optional) a list, to store the history of heterogeneity as function of iterations @@ -195,23 +196,20 @@ def kmeans( def report_generator( - df: pd.DataFrame, clustering_variables: np.ndarray, fill_missing_report=None + predicted: pd.DataFrame, clustering_variables: np.ndarray, fill_missing_report=None ) -> pd.DataFrame: """ - Function generates easy-erading clustering report. It takes 2 arguments as an input: - DataFrame - dataframe with predicted cluester column; - FillMissingReport - dictionary of rules how we are going to fill missing - values of for final report generate (not included in modeling); - in order to run the function following libraries must be imported: - import pandas as pd - import numpy as np - >>> data = pd.DataFrame() - >>> data['numbers'] = [1, 2, 3] - >>> data['col1'] = [0.5, 2.5, 4.5] - >>> data['col2'] = [100, 200, 300] - >>> data['col3'] = [10, 20, 30] - >>> data['Cluster'] = [1, 1, 2] - >>> report_generator(data, ['col1', 'col2'], 0) + Generate a clustering report given these two arguments: + predicted - dataframe with predicted cluster column + fill_missing_report - dictionary of rules on how we are going to fill in missing + values for final generated report (not included in modelling); + >>> predicted = pd.DataFrame() + >>> predicted['numbers'] = [1, 2, 3] + >>> predicted['col1'] = [0.5, 2.5, 4.5] + >>> predicted['col2'] = [100, 200, 300] + >>> predicted['col3'] = [10, 20, 30] + >>> predicted['Cluster'] = [1, 1, 2] + >>> report_generator(predicted, ['col1', 'col2'], 0) Features Type Mark 1 2 0 # of Customers ClusterSize False 2.000000 1.000000 1 % of Customers ClusterProportion False 0.666667 0.333333 @@ -229,18 +227,18 @@ def report_generator( """ # Fill missing values with given rules if fill_missing_report: - df.fillna(value=fill_missing_report, inplace=True) - df["dummy"] = 1 - numeric_cols = df.select_dtypes(np.number).columns + predicted = predicted.fillna(value=fill_missing_report) + predicted["dummy"] = 1 + numeric_cols = predicted.select_dtypes(np.number).columns report = ( - df.groupby(["Cluster"])[ # construct report dataframe + predicted.groupby(["Cluster"])[ # construct report dataframe numeric_cols ] # group by cluster number .agg( [ - ("sum", np.sum), + ("sum", "sum"), ("mean_with_zeros", lambda x: np.mean(np.nan_to_num(x))), - ("mean_without_zeros", lambda x: x.replace(0, np.NaN).mean()), + ("mean_without_zeros", lambda x: x.replace(0, np.nan).mean()), ( "mean_25-75", lambda x: np.mean( @@ -251,7 +249,7 @@ def report_generator( ) ), ), - ("mean_with_na", np.mean), + ("mean_with_na", "mean"), ("min", lambda x: x.min()), ("5%", lambda x: x.quantile(0.05)), ("25%", lambda x: x.quantile(0.25)), @@ -270,46 +268,43 @@ def report_generator( .rename(index=str, columns={"level_0": "Features", "level_1": "Type"}) ) # rename columns # calculate the size of cluster(count of clientID's) + # avoid SettingWithCopyWarning clustersize = report[ (report["Features"] == "dummy") & (report["Type"] == "count") - ].copy() # avoid SettingWithCopyWarning - clustersize.Type = ( - "ClusterSize" # rename created cluster df to match report column names - ) + ].copy() + # rename created predicted cluster to match report column names + clustersize.Type = "ClusterSize" clustersize.Features = "# of Customers" + # calculating the proportion of cluster clusterproportion = pd.DataFrame( - clustersize.iloc[:, 2:].values - / clustersize.iloc[:, 2:].values.sum() # calculating the proportion of cluster + clustersize.iloc[:, 2:].to_numpy() / clustersize.iloc[:, 2:].to_numpy().sum() ) - clusterproportion[ - "Type" - ] = "% of Customers" # rename created cluster df to match report column names + # rename created predicted cluster to match report column names + clusterproportion["Type"] = "% of Customers" clusterproportion["Features"] = "ClusterProportion" cols = clusterproportion.columns.tolist() cols = cols[-2:] + cols[:-2] clusterproportion = clusterproportion[cols] # rearrange columns to match report clusterproportion.columns = report.columns + # generating dataframe with count of nan values a = pd.DataFrame( abs( - report[report["Type"] == "count"].iloc[:, 2:].values - - clustersize.iloc[:, 2:].values + report[report["Type"] == "count"].iloc[:, 2:].to_numpy() + - clustersize.iloc[:, 2:].to_numpy() ) - ) # generating df with count of nan values + ) a["Features"] = 0 a["Type"] = "# of nan" - a.Features = report[ - report["Type"] == "count" - ].Features.tolist() # filling values in order to match report + # filling values in order to match report + a.Features = report[report["Type"] == "count"].Features.tolist() cols = a.columns.tolist() cols = cols[-2:] + cols[:-2] a = a[cols] # rearrange columns to match report a.columns = report.columns # rename columns to match report - report = report.drop( - report[report.Type == "count"].index - ) # drop count values except cluster size - report = pd.concat( - [report, a, clustersize, clusterproportion], axis=0 - ) # concat report with clustert size and nan values + # drop count values except for cluster size + report = report.drop(report[report.Type == "count"].index) + # concat report with cluster size and nan values + report = pd.concat([report, a, clustersize, clusterproportion], axis=0) report["Mark"] = report["Features"].isin(clustering_variables) cols = report.columns.tolist() cols = cols[0:2] + cols[-1:] + cols[2:-1] @@ -338,7 +333,7 @@ def report_generator( ) report.columns.name = "" report = report.reset_index() - report.drop(columns=["index"], inplace=True) + report = report.drop(columns=["index"]) return report diff --git a/machine_learning/k_nearest_neighbours.py b/machine_learning/k_nearest_neighbours.py index 2a90cfe5987a..fbc1b8bd227e 100644 --- a/machine_learning/k_nearest_neighbours.py +++ b/machine_learning/k_nearest_neighbours.py @@ -1,58 +1,88 @@ +""" +k-Nearest Neighbours (kNN) is a simple non-parametric supervised learning +algorithm used for classification. Given some labelled training data, a given +point is classified using its k nearest neighbours according to some distance +metric. The most commonly occurring label among the neighbours becomes the label +of the given point. In effect, the label of the given point is decided by a +majority vote. + +This implementation uses the commonly used Euclidean distance metric, but other +distance metrics can also be used. + +Reference: https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm +""" + from collections import Counter +from heapq import nsmallest import numpy as np from sklearn import datasets from sklearn.model_selection import train_test_split -data = datasets.load_iris() - -X = np.array(data["data"]) -y = np.array(data["target"]) -classes = data["target_names"] - -X_train, X_test, y_train, y_test = train_test_split(X, y) - - -def euclidean_distance(a, b): - """ - Gives the euclidean distance between two points - >>> euclidean_distance([0, 0], [3, 4]) - 5.0 - >>> euclidean_distance([1, 2, 3], [1, 8, 11]) - 10.0 - """ - return np.linalg.norm(np.array(a) - np.array(b)) - - -def classifier(train_data, train_target, classes, point, k=5): - """ - Classifies the point using the KNN algorithm - k closest points are found (ranked in ascending order of euclidean distance) - Params: - :train_data: Set of points that are classified into two or more classes - :train_target: List of classes in the order of train_data points - :classes: Labels of the classes - :point: The data point that needs to be classified - - >>> X_train = [[0, 0], [1, 0], [0, 1], [0.5, 0.5], [3, 3], [2, 3], [3, 2]] - >>> y_train = [0, 0, 0, 0, 1, 1, 1] - >>> classes = ['A','B']; point = [1.2,1.2] - >>> classifier(X_train, y_train, classes,point) - 'A' - """ - data = zip(train_data, train_target) - # List of distances of all points from the point to be classified - distances = [] - for data_point in data: - distance = euclidean_distance(data_point[0], point) - distances.append((distance, data_point[1])) - # Choosing 'k' points with the least distances. - votes = [i[1] for i in sorted(distances)[:k]] - # Most commonly occurring class among them - # is the class into which the point is classified - result = Counter(votes).most_common(1)[0][0] - return classes[result] + +class KNN: + def __init__( + self, + train_data: np.ndarray[float], + train_target: np.ndarray[int], + class_labels: list[str], + ) -> None: + """ + Create a kNN classifier using the given training data and class labels + """ + self.data = zip(train_data, train_target) + self.labels = class_labels + + @staticmethod + def _euclidean_distance(a: np.ndarray[float], b: np.ndarray[float]) -> float: + """ + Calculate the Euclidean distance between two points + >>> KNN._euclidean_distance(np.array([0, 0]), np.array([3, 4])) + 5.0 + >>> KNN._euclidean_distance(np.array([1, 2, 3]), np.array([1, 8, 11])) + 10.0 + """ + return float(np.linalg.norm(a - b)) + + def classify(self, pred_point: np.ndarray[float], k: int = 5) -> str: + """ + Classify a given point using the kNN algorithm + >>> train_X = np.array( + ... [[0, 0], [1, 0], [0, 1], [0.5, 0.5], [3, 3], [2, 3], [3, 2]] + ... ) + >>> train_y = np.array([0, 0, 0, 0, 1, 1, 1]) + >>> classes = ['A', 'B'] + >>> knn = KNN(train_X, train_y, classes) + >>> point = np.array([1.2, 1.2]) + >>> knn.classify(point) + 'A' + """ + # Distances of all points from the point to be classified + distances = ( + (self._euclidean_distance(data_point[0], pred_point), data_point[1]) + for data_point in self.data + ) + + # Choosing k points with the shortest distances + votes = (i[1] for i in nsmallest(k, distances)) + + # Most commonly occurring class is the one into which the point is classified + result = Counter(votes).most_common(1)[0][0] + return self.labels[result] if __name__ == "__main__": - print(classifier(X_train, y_train, classes, [4.4, 3.1, 1.3, 1.4])) + import doctest + + doctest.testmod() + + iris = datasets.load_iris() + + X = np.array(iris["data"]) + y = np.array(iris["target"]) + iris_classes = iris["target_names"] + + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) + iris_point = np.array([4.4, 3.1, 1.3, 1.4]) + classifier = KNN(X_train, y_train, iris_classes) + print(classifier.classify(iris_point, k=3)) diff --git a/machine_learning/knn_sklearn.py b/machine_learning/knn_sklearn.py deleted file mode 100644 index 4a621a4244b6..000000000000 --- a/machine_learning/knn_sklearn.py +++ /dev/null @@ -1,31 +0,0 @@ -from sklearn.datasets import load_iris -from sklearn.model_selection import train_test_split -from sklearn.neighbors import KNeighborsClassifier - -# Load iris file -iris = load_iris() -iris.keys() - - -print(f"Target names: \n {iris.target_names} ") -print(f"\n Features: \n {iris.feature_names}") - -# Train set e Test set -X_train, X_test, y_train, y_test = train_test_split( - iris["data"], iris["target"], random_state=4 -) - -# KNN - -knn = KNeighborsClassifier(n_neighbors=1) -knn.fit(X_train, y_train) - -# new array to test -X_new = [[1, 2, 1, 4], [2, 3, 4, 5]] - -prediction = knn.predict(X_new) - -print( - f"\nNew array: \n {X_new}\n\nTarget Names Prediction: \n" - f" {iris['target_names'][prediction]}" -) diff --git a/machine_learning/linear_discriminant_analysis.py b/machine_learning/linear_discriminant_analysis.py index f4fb5ba76b64..8528ccbbae51 100644 --- a/machine_learning/linear_discriminant_analysis.py +++ b/machine_learning/linear_discriminant_analysis.py @@ -1,47 +1,48 @@ """ - Linear Discriminant Analysis +Linear Discriminant Analysis - Assumptions About Data : - 1. The input variables has a gaussian distribution. - 2. The variance calculated for each input variables by class grouping is the - same. - 3. The mix of classes in your training set is representative of the problem. +Assumptions About Data : + 1. The input variables has a gaussian distribution. + 2. The variance calculated for each input variables by class grouping is the + same. + 3. The mix of classes in your training set is representative of the problem. - Learning The Model : - The LDA model requires the estimation of statistics from the training data : - 1. Mean of each input value for each class. - 2. Probability of an instance belong to each class. - 3. Covariance for the input data for each class +Learning The Model : + The LDA model requires the estimation of statistics from the training data : + 1. Mean of each input value for each class. + 2. Probability of an instance belong to each class. + 3. Covariance for the input data for each class - Calculate the class means : - mean(x) = 1/n ( for i = 1 to i = n --> sum(xi)) + Calculate the class means : + mean(x) = 1/n ( for i = 1 to i = n --> sum(xi)) - Calculate the class probabilities : - P(y = 0) = count(y = 0) / (count(y = 0) + count(y = 1)) - P(y = 1) = count(y = 1) / (count(y = 0) + count(y = 1)) + Calculate the class probabilities : + P(y = 0) = count(y = 0) / (count(y = 0) + count(y = 1)) + P(y = 1) = count(y = 1) / (count(y = 0) + count(y = 1)) - Calculate the variance : - We can calculate the variance for dataset in two steps : - 1. Calculate the squared difference for each input variable from the - group mean. - 2. Calculate the mean of the squared difference. - ------------------------------------------------ - Squared_Difference = (x - mean(k)) ** 2 - Variance = (1 / (count(x) - count(classes))) * - (for i = 1 to i = n --> sum(Squared_Difference(xi))) + Calculate the variance : + We can calculate the variance for dataset in two steps : + 1. Calculate the squared difference for each input variable from the + group mean. + 2. Calculate the mean of the squared difference. + ------------------------------------------------ + Squared_Difference = (x - mean(k)) ** 2 + Variance = (1 / (count(x) - count(classes))) * + (for i = 1 to i = n --> sum(Squared_Difference(xi))) - Making Predictions : - discriminant(x) = x * (mean / variance) - - ((mean ** 2) / (2 * variance)) + Ln(probability) - --------------------------------------------------------------------------- - After calculating the discriminant value for each class, the class with the - largest discriminant value is taken as the prediction. +Making Predictions : + discriminant(x) = x * (mean / variance) - + ((mean ** 2) / (2 * variance)) + Ln(probability) + --------------------------------------------------------------------------- + After calculating the discriminant value for each class, the class with the + largest discriminant value is taken as the prediction. - Author: @EverLookNeverSee +Author: @EverLookNeverSee """ + from collections.abc import Callable from math import log from os import name, system @@ -255,7 +256,7 @@ def valid_input( input_type: Callable[[object], num], # Usually float or int input_msg: str, err_msg: str, - condition: Callable[[num], bool] = lambda x: True, + condition: Callable[[num], bool] = lambda _: True, default: str | None = None, ) -> num: """ @@ -321,7 +322,7 @@ def main(): user_count = valid_input( input_type=int, condition=lambda x: x > 0, - input_msg=(f"Enter The number of instances for class_{i+1}: "), + input_msg=(f"Enter The number of instances for class_{i + 1}: "), err_msg="Number of instances should be positive!", ) counts.append(user_count) @@ -332,7 +333,7 @@ def main(): for a in range(n_classes): user_mean = valid_input( input_type=float, - input_msg=(f"Enter the value of mean for class_{a+1}: "), + input_msg=(f"Enter the value of mean for class_{a + 1}: "), err_msg="This is an invalid value.", ) user_means.append(user_mean) @@ -399,7 +400,7 @@ def main(): if input("Press any key to restart or 'q' for quit: ").strip().lower() == "q": print("\n" + "GoodBye!".center(100, "-") + "\n") break - system("cls" if name == "nt" else "clear") + system("cls" if name == "nt" else "clear") # noqa: S605 if __name__ == "__main__": diff --git a/machine_learning/linear_regression.py b/machine_learning/linear_regression.py index 75943ac9f2ad..1d11e5a9cc2b 100644 --- a/machine_learning/linear_regression.py +++ b/machine_learning/linear_regression.py @@ -7,6 +7,7 @@ fit our dataset. In this particular code, I had used a CSGO dataset (ADR vs Rating). We try to best fit a line through dataset and estimate the parameters. """ + import numpy as np import requests @@ -18,7 +19,8 @@ def collect_dataset(): """ response = requests.get( "https://raw.githubusercontent.com/yashLadha/The_Math_of_Intelligence/" - "master/Week1/ADRvsRating.csv" + "master/Week1/ADRvsRating.csv", + timeout=10, ) lines = response.text.splitlines() data = [] @@ -39,6 +41,14 @@ def run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta): :param theta : Feature vector (weight's for our model) ;param return : Updated Feature's, using curr_features - alpha_ * gradient(w.r.t. feature) + >>> import numpy as np + >>> data_x = np.array([[1, 2], [3, 4]]) + >>> data_y = np.array([5, 6]) + >>> len_data = len(data_x) + >>> alpha = 0.01 + >>> theta = np.array([0.1, 0.2]) + >>> run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta) + array([0.196, 0.343]) """ n = len_data @@ -56,6 +66,12 @@ def sum_of_square_error(data_x, data_y, len_data, theta): :param len_data : len of the dataset :param theta : contains the feature vector :return : sum of square error computed from given feature's + + Example: + >>> vc_x = np.array([[1.1], [2.1], [3.1]]) + >>> vc_y = np.array([1.2, 2.2, 3.2]) + >>> round(sum_of_square_error(vc_x, vc_y, 3, np.array([1])),3) + np.float64(0.005) """ prod = np.dot(theta, data_x.transpose()) prod -= data_y.transpose() @@ -78,7 +94,7 @@ def run_linear_regression(data_x, data_y): theta = np.zeros((1, no_features)) - for i in range(0, iterations): + for i in range(iterations): theta = run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta) error = sum_of_square_error(data_x, data_y, len_data, theta) print(f"At Iteration {i + 1} - Error is {error:.5f}") @@ -91,6 +107,11 @@ def mean_absolute_error(predicted_y, original_y): :param predicted_y : contains the output of prediction (result vector) :param original_y : contains values of expected outcome :return : mean absolute error computed from given feature's + + >>> predicted_y = [3, -0.5, 2, 7] + >>> original_y = [2.5, 0.0, 2, 8] + >>> mean_absolute_error(predicted_y, original_y) + 0.5 """ total = sum(abs(y - predicted_y[i]) for i, y in enumerate(original_y)) return total / len(original_y) @@ -107,9 +128,12 @@ def main(): theta = run_linear_regression(data_x, data_y) len_result = theta.shape[1] print("Resultant Feature vector : ") - for i in range(0, len_result): + for i in range(len_result): print(f"{theta[0, i]:.5f}") if __name__ == "__main__": + import doctest + + doctest.testmod() main() diff --git a/machine_learning/local_weighted_learning/local_weighted_learning.md b/machine_learning/local_weighted_learning/README.md similarity index 100% rename from machine_learning/local_weighted_learning/local_weighted_learning.md rename to machine_learning/local_weighted_learning/README.md diff --git a/machine_learning/local_weighted_learning/local_weighted_learning.py b/machine_learning/local_weighted_learning/local_weighted_learning.py index 6260e9ac6bfe..f3056da40e24 100644 --- a/machine_learning/local_weighted_learning/local_weighted_learning.py +++ b/machine_learning/local_weighted_learning/local_weighted_learning.py @@ -1,14 +1,55 @@ +""" +Locally weighted linear regression, also called local regression, is a type of +non-parametric linear regression that prioritizes data closest to a given +prediction point. The algorithm estimates the vector of model coefficients β +using weighted least squares regression: + +β = (Xáµ€WX)â»Â¹(Xáµ€Wy), + +where X is the design matrix, y is the response vector, and W is the diagonal +weight matrix. + +This implementation calculates wáµ¢, the weight of the ith training sample, using +the Gaussian weight: + +wáµ¢ = exp(-‖xáµ¢ - x‖²/(2τ²)), + +where xáµ¢ is the ith training sample, x is the prediction point, Ï„ is the +"bandwidth", and ‖x‖ is the Euclidean norm (also called the 2-norm or the L² +norm). The bandwidth Ï„ controls how quickly the weight of a training sample +decreases as its distance from the prediction point increases. One can think of +the Gaussian weight as a bell curve centered around the prediction point: a +training sample is weighted lower if it's farther from the center, and Ï„ +controls the spread of the bell curve. + +Other types of locally weighted regression such as locally estimated scatterplot +smoothing (LOESS) typically use different weight functions. + +References: + - https://en.wikipedia.org/wiki/Local_regression + - https://en.wikipedia.org/wiki/Weighted_least_squares + - https://cs229.stanford.edu/notes2022fall/main_notes.pdf +""" + import matplotlib.pyplot as plt import numpy as np -def weighted_matrix( - point: np.array, training_data_x: np.array, bandwidth: float -) -> np.array: +def weight_matrix(point: np.ndarray, x_train: np.ndarray, tau: float) -> np.ndarray: """ - Calculate the weight for every point in the data set. - point --> the x value at which we want to make predictions - >>> weighted_matrix( + Calculate the weight of every point in the training data around a given + prediction point + + Args: + point: x-value at which the prediction is being made + x_train: ndarray of x-values for training + tau: bandwidth value, controls how quickly the weight of training values + decreases as the distance from the prediction point increases + + Returns: + m x m weight matrix around the prediction point, where m is the size of + the training set + >>> weight_matrix( ... np.array([1., 1.]), ... np.array([[16.99, 10.34], [21.01,23.68], [24.59,25.69]]), ... 0.6 @@ -17,25 +58,30 @@ def weighted_matrix( [0.00000000e+000, 0.00000000e+000, 0.00000000e+000], [0.00000000e+000, 0.00000000e+000, 0.00000000e+000]]) """ - m, _ = np.shape(training_data_x) # m is the number of training samples - weights = np.eye(m) # Initializing weights as identity matrix - - # calculating weights for all training examples [x(i)'s] + m = len(x_train) # Number of training samples + weights = np.eye(m) # Initialize weights as identity matrix for j in range(m): - diff = point - training_data_x[j] - weights[j, j] = np.exp(diff @ diff.T / (-2.0 * bandwidth**2)) + diff = point - x_train[j] + weights[j, j] = np.exp(diff @ diff.T / (-2.0 * tau**2)) + return weights def local_weight( - point: np.array, - training_data_x: np.array, - training_data_y: np.array, - bandwidth: float, -) -> np.array: + point: np.ndarray, x_train: np.ndarray, y_train: np.ndarray, tau: float +) -> np.ndarray: """ - Calculate the local weights using the weight_matrix function on training data. - Return the weighted matrix. + Calculate the local weights at a given prediction point using the weight + matrix for that point + + Args: + point: x-value at which the prediction is being made + x_train: ndarray of x-values for training + y_train: ndarray of y-values for training + tau: bandwidth value, controls how quickly the weight of training values + decreases as the distance from the prediction point increases + Returns: + ndarray of local weights >>> local_weight( ... np.array([1., 1.]), ... np.array([[16.99, 10.34], [21.01,23.68], [24.59,25.69]]), @@ -45,19 +91,28 @@ def local_weight( array([[0.00873174], [0.08272556]]) """ - weight = weighted_matrix(point, training_data_x, bandwidth) - w = np.linalg.inv(training_data_x.T @ (weight @ training_data_x)) @ ( - training_data_x.T @ weight @ training_data_y.T + weight_mat = weight_matrix(point, x_train, tau) + weight = np.linalg.inv(x_train.T @ weight_mat @ x_train) @ ( + x_train.T @ weight_mat @ y_train.T ) - return w + return weight def local_weight_regression( - training_data_x: np.array, training_data_y: np.array, bandwidth: float -) -> np.array: + x_train: np.ndarray, y_train: np.ndarray, tau: float +) -> np.ndarray: """ - Calculate predictions for each data point on axis + Calculate predictions for each point in the training data + + Args: + x_train: ndarray of x-values for training + y_train: ndarray of y-values for training + tau: bandwidth value, controls how quickly the weight of training values + decreases as the distance from the prediction point increases + + Returns: + ndarray of predictions >>> local_weight_regression( ... np.array([[16.99, 10.34], [21.01, 23.68], [24.59, 25.69]]), ... np.array([[1.01, 1.66, 3.5]]), @@ -65,77 +120,57 @@ def local_weight_regression( ... ) array([1.07173261, 1.65970737, 3.50160179]) """ - m, _ = np.shape(training_data_x) - ypred = np.zeros(m) + y_pred = np.zeros(len(x_train)) # Initialize array of predictions + for i, item in enumerate(x_train): + y_pred[i] = np.dot(item, local_weight(item, x_train, y_train, tau)).item() - for i, item in enumerate(training_data_x): - ypred[i] = item @ local_weight( - item, training_data_x, training_data_y, bandwidth - ) - - return ypred + return y_pred def load_data( - dataset_name: str, cola_name: str, colb_name: str -) -> tuple[np.array, np.array, np.array, np.array]: + dataset_name: str, x_name: str, y_name: str +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """ Load data from seaborn and split it into x and y points + >>> pass # No doctests, function is for demo purposes only """ import seaborn as sns data = sns.load_dataset(dataset_name) - col_a = np.array(data[cola_name]) # total_bill - col_b = np.array(data[colb_name]) # tip - - mcol_a = col_a.copy() - mcol_b = col_b.copy() - - one = np.ones(np.shape(mcol_b)[0], dtype=int) + x_data = np.array(data[x_name]) + y_data = np.array(data[y_name]) - # pairing elements of one and mcol_a - training_data_x = np.column_stack((one, mcol_a)) + one = np.ones(len(y_data)) - return training_data_x, mcol_b, col_a, col_b + # pairing elements of one and x_data + x_train = np.column_stack((one, x_data)) - -def get_preds(training_data_x: np.array, mcol_b: np.array, tau: float) -> np.array: - """ - Get predictions with minimum error for each training data - >>> get_preds( - ... np.array([[16.99, 10.34], [21.01, 23.68], [24.59, 25.69]]), - ... np.array([[1.01, 1.66, 3.5]]), - ... 0.6 - ... ) - array([1.07173261, 1.65970737, 3.50160179]) - """ - ypred = local_weight_regression(training_data_x, mcol_b, tau) - return ypred + return x_train, x_data, y_data def plot_preds( - training_data_x: np.array, - predictions: np.array, - col_x: np.array, - col_y: np.array, - cola_name: str, - colb_name: str, -) -> plt.plot: + x_train: np.ndarray, + preds: np.ndarray, + x_data: np.ndarray, + y_data: np.ndarray, + x_name: str, + y_name: str, +) -> None: """ Plot predictions and display the graph + >>> pass # No doctests, function is for demo purposes only """ - xsort = training_data_x.copy() - xsort.sort(axis=0) - plt.scatter(col_x, col_y, color="blue") + x_train_sorted = np.sort(x_train, axis=0) + plt.scatter(x_data, y_data, color="blue") plt.plot( - xsort[:, 1], - predictions[training_data_x[:, 1].argsort(0)], + x_train_sorted[:, 1], + preds[x_train[:, 1].argsort(0)], color="yellow", linewidth=5, ) plt.title("Local Weighted Regression") - plt.xlabel(cola_name) - plt.ylabel(colb_name) + plt.xlabel(x_name) + plt.ylabel(y_name) plt.show() @@ -144,6 +179,7 @@ def plot_preds( doctest.testmod() - training_data_x, mcol_b, col_a, col_b = load_data("tips", "total_bill", "tip") - predictions = get_preds(training_data_x, mcol_b, 0.5) - plot_preds(training_data_x, predictions, col_a, col_b, "total_bill", "tip") + # Demo with a dataset from the seaborn module + training_data_x, total_bill, tip = load_data("tips", "total_bill", "tip") + predictions = local_weight_regression(training_data_x, tip, 5) + plot_preds(training_data_x, predictions, total_bill, tip, "total_bill", "tip") diff --git a/machine_learning/logistic_regression.py b/machine_learning/logistic_regression.py index 87bc8f6681cc..496026631fbe 100644 --- a/machine_learning/logistic_regression.py +++ b/machine_learning/logistic_regression.py @@ -14,6 +14,7 @@ Coursera ML course https://medium.com/@martinpella/logistic-regression-from-scratch-in-python-124c5636b8ac """ + import numpy as np from matplotlib import pyplot as plt from sklearn import datasets @@ -27,12 +28,79 @@ # classification problems -def sigmoid_function(z): +def sigmoid_function(z: float | np.ndarray) -> float | np.ndarray: + """ + Also known as Logistic Function. + + 1 + f(x) = ------- + 1 + eâ»Ë£ + + The sigmoid function approaches a value of 1 as its input 'x' becomes + increasing positive. Opposite for negative values. + + Reference: https://en.wikipedia.org/wiki/Sigmoid_function + + @param z: input to the function + @returns: returns value in the range 0 to 1 + + Examples: + >>> float(sigmoid_function(4)) + 0.9820137900379085 + >>> sigmoid_function(np.array([-3, 3])) + array([0.04742587, 0.95257413]) + >>> sigmoid_function(np.array([-3, 3, 1])) + array([0.04742587, 0.95257413, 0.73105858]) + >>> sigmoid_function(np.array([-0.01, -2, -1.9])) + array([0.49750002, 0.11920292, 0.13010847]) + >>> sigmoid_function(np.array([-1.3, 5.3, 12])) + array([0.21416502, 0.9950332 , 0.99999386]) + >>> sigmoid_function(np.array([0.01, 0.02, 4.1])) + array([0.50249998, 0.50499983, 0.9836975 ]) + >>> sigmoid_function(np.array([0.8])) + array([0.68997448]) + """ return 1 / (1 + np.exp(-z)) -def cost_function(h, y): - return (-y * np.log(h) - (1 - y) * np.log(1 - h)).mean() +def cost_function(h: np.ndarray, y: np.ndarray) -> float: + """ + Cost function quantifies the error between predicted and expected values. + The cost function used in Logistic Regression is called Log Loss + or Cross Entropy Function. + + J(θ) = (1/m) * Σ [ -y * log(hθ(x)) - (1 - y) * log(1 - hθ(x)) ] + + Where: + - J(θ) is the cost that we want to minimize during training + - m is the number of training examples + - Σ represents the summation over all training examples + - y is the actual binary label (0 or 1) for a given example + - hθ(x) is the predicted probability that x belongs to the positive class + + @param h: the output of sigmoid function. It is the estimated probability + that the input example 'x' belongs to the positive class + + @param y: the actual binary label associated with input example 'x' + + Examples: + >>> estimations = sigmoid_function(np.array([0.3, -4.3, 8.1])) + >>> cost_function(h=estimations,y=np.array([1, 0, 1])) + 0.18937868932131605 + >>> estimations = sigmoid_function(np.array([4, 3, 1])) + >>> cost_function(h=estimations,y=np.array([1, 0, 0])) + 1.459999655669926 + >>> estimations = sigmoid_function(np.array([4, -3, -1])) + >>> cost_function(h=estimations,y=np.array([1,0,0])) + 0.1266663223365915 + >>> estimations = sigmoid_function(0) + >>> cost_function(h=estimations,y=np.array([1])) + 0.6931471805599453 + + References: + - https://en.wikipedia.org/wiki/Logistic_regression + """ + return float((-y * np.log(h) - (1 - y) * np.log(1 - h)).mean()) def log_likelihood(x, y, weights): @@ -60,6 +128,10 @@ def logistic_reg(alpha, x, y, max_iterations=70000): # In[68]: if __name__ == "__main__": + import doctest + + doctest.testmod() + iris = datasets.load_iris() x = iris.data[:, :2] y = (iris.target != 0) * 1 diff --git a/machine_learning/loss_functions.py b/machine_learning/loss_functions.py new file mode 100644 index 000000000000..0bd9aa8b5401 --- /dev/null +++ b/machine_learning/loss_functions.py @@ -0,0 +1,669 @@ +import numpy as np + + +def binary_cross_entropy( + y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15 +) -> float: + """ + Calculate the mean binary cross-entropy (BCE) loss between true labels and predicted + probabilities. + + BCE loss quantifies dissimilarity between true labels (0 or 1) and predicted + probabilities. It's widely used in binary classification tasks. + + BCE = -Σ(y_true * ln(y_pred) + (1 - y_true) * ln(1 - y_pred)) + + Reference: https://en.wikipedia.org/wiki/Cross_entropy + + Parameters: + - y_true: True binary labels (0 or 1) + - y_pred: Predicted probabilities for class 1 + - epsilon: Small constant to avoid numerical instability + + >>> true_labels = np.array([0, 1, 1, 0, 1]) + >>> predicted_probs = np.array([0.2, 0.7, 0.9, 0.3, 0.8]) + >>> float(binary_cross_entropy(true_labels, predicted_probs)) + 0.2529995012327421 + >>> true_labels = np.array([0, 1, 1, 0, 1]) + >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2]) + >>> binary_cross_entropy(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # Clip predictions to avoid log(0) + bce_loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) + return np.mean(bce_loss) + + +def binary_focal_cross_entropy( + y_true: np.ndarray, + y_pred: np.ndarray, + gamma: float = 2.0, + alpha: float = 0.25, + epsilon: float = 1e-15, +) -> float: + """ + Calculate the mean binary focal cross-entropy (BFCE) loss between true labels + and predicted probabilities. + + BFCE loss quantifies dissimilarity between true labels (0 or 1) and predicted + probabilities. It's a variation of binary cross-entropy that addresses class + imbalance by focusing on hard examples. + + BCFE = -Σ(alpha * (1 - y_pred)**gamma * y_true * log(y_pred) + + (1 - alpha) * y_pred**gamma * (1 - y_true) * log(1 - y_pred)) + + Reference: [Lin et al., 2018](https://arxiv.org/pdf/1708.02002.pdf) + + Parameters: + - y_true: True binary labels (0 or 1). + - y_pred: Predicted probabilities for class 1. + - gamma: Focusing parameter for modulating the loss (default: 2.0). + - alpha: Weighting factor for class 1 (default: 0.25). + - epsilon: Small constant to avoid numerical instability. + + >>> true_labels = np.array([0, 1, 1, 0, 1]) + >>> predicted_probs = np.array([0.2, 0.7, 0.9, 0.3, 0.8]) + >>> float(binary_focal_cross_entropy(true_labels, predicted_probs)) + 0.008257977659239775 + >>> true_labels = np.array([0, 1, 1, 0, 1]) + >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2]) + >>> binary_focal_cross_entropy(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + # Clip predicted probabilities to avoid log(0) + y_pred = np.clip(y_pred, epsilon, 1 - epsilon) + + bcfe_loss = -( + alpha * (1 - y_pred) ** gamma * y_true * np.log(y_pred) + + (1 - alpha) * y_pred**gamma * (1 - y_true) * np.log(1 - y_pred) + ) + + return np.mean(bcfe_loss) + + +def categorical_cross_entropy( + y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15 +) -> float: + """ + Calculate categorical cross-entropy (CCE) loss between true class labels and + predicted class probabilities. + + CCE = -Σ(y_true * ln(y_pred)) + + Reference: https://en.wikipedia.org/wiki/Cross_entropy + + Parameters: + - y_true: True class labels (one-hot encoded) + - y_pred: Predicted class probabilities + - epsilon: Small constant to avoid numerical instability + + >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]]) + >>> float(categorical_cross_entropy(true_labels, pred_probs)) + 0.567395975254385 + >>> true_labels = np.array([[1, 0], [0, 1]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same shape. + >>> true_labels = np.array([[2, 0, 1], [1, 0, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: y_true must be one-hot encoded. + >>> true_labels = np.array([[1, 0, 1], [1, 0, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: y_true must be one-hot encoded. + >>> true_labels = np.array([[1, 0, 0], [0, 1, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.1], [0.2, 0.7, 0.1]]) + >>> categorical_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: Predicted probabilities must sum to approximately 1. + """ + if y_true.shape != y_pred.shape: + raise ValueError("Input arrays must have the same shape.") + + if np.any((y_true != 0) & (y_true != 1)) or np.any(y_true.sum(axis=1) != 1): + raise ValueError("y_true must be one-hot encoded.") + + if not np.all(np.isclose(np.sum(y_pred, axis=1), 1, rtol=epsilon, atol=epsilon)): + raise ValueError("Predicted probabilities must sum to approximately 1.") + + y_pred = np.clip(y_pred, epsilon, 1) # Clip predictions to avoid log(0) + return -np.sum(y_true * np.log(y_pred)) + + +def categorical_focal_cross_entropy( + y_true: np.ndarray, + y_pred: np.ndarray, + alpha: np.ndarray = None, + gamma: float = 2.0, + epsilon: float = 1e-15, +) -> float: + """ + Calculate the mean categorical focal cross-entropy (CFCE) loss between true + labels and predicted probabilities for multi-class classification. + + CFCE loss is a generalization of binary focal cross-entropy for multi-class + classification. It addresses class imbalance by focusing on hard examples. + + CFCE = -Σ alpha * (1 - y_pred)**gamma * y_true * log(y_pred) + + Reference: [Lin et al., 2018](https://arxiv.org/pdf/1708.02002.pdf) + + Parameters: + - y_true: True labels in one-hot encoded form. + - y_pred: Predicted probabilities for each class. + - alpha: Array of weighting factors for each class. + - gamma: Focusing parameter for modulating the loss (default: 2.0). + - epsilon: Small constant to avoid numerical instability. + + Returns: + - The mean categorical focal cross-entropy loss. + + >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]]) + >>> alpha = np.array([0.6, 0.2, 0.7]) + >>> float(categorical_focal_cross_entropy(true_labels, pred_probs, alpha)) + 0.0025966118981496423 + + >>> true_labels = np.array([[0, 1, 0], [0, 0, 1]]) + >>> pred_probs = np.array([[0.05, 0.95, 0], [0.1, 0.8, 0.1]]) + >>> alpha = np.array([0.25, 0.25, 0.25]) + >>> float(categorical_focal_cross_entropy(true_labels, pred_probs, alpha)) + 0.23315276982014324 + + >>> true_labels = np.array([[1, 0], [0, 1]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same shape. + + >>> true_labels = np.array([[2, 0, 1], [1, 0, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_focal_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: y_true must be one-hot encoded. + + >>> true_labels = np.array([[1, 0, 1], [1, 0, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1]]) + >>> categorical_focal_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: y_true must be one-hot encoded. + + >>> true_labels = np.array([[1, 0, 0], [0, 1, 0]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.1], [0.2, 0.7, 0.1]]) + >>> categorical_focal_cross_entropy(true_labels, pred_probs) + Traceback (most recent call last): + ... + ValueError: Predicted probabilities must sum to approximately 1. + + >>> true_labels = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + >>> pred_probs = np.array([[0.9, 0.1, 0.0], [0.2, 0.7, 0.1], [0.0, 0.1, 0.9]]) + >>> alpha = np.array([0.6, 0.2]) + >>> categorical_focal_cross_entropy(true_labels, pred_probs, alpha) + Traceback (most recent call last): + ... + ValueError: Length of alpha must match the number of classes. + """ + if y_true.shape != y_pred.shape: + raise ValueError("Shape of y_true and y_pred must be the same.") + + if alpha is None: + alpha = np.ones(y_true.shape[1]) + + if np.any((y_true != 0) & (y_true != 1)) or np.any(y_true.sum(axis=1) != 1): + raise ValueError("y_true must be one-hot encoded.") + + if len(alpha) != y_true.shape[1]: + raise ValueError("Length of alpha must match the number of classes.") + + if not np.all(np.isclose(np.sum(y_pred, axis=1), 1, rtol=epsilon, atol=epsilon)): + raise ValueError("Predicted probabilities must sum to approximately 1.") + + # Clip predicted probabilities to avoid log(0) + y_pred = np.clip(y_pred, epsilon, 1 - epsilon) + + # Calculate loss for each class and sum across classes + cfce_loss = -np.sum( + alpha * np.power(1 - y_pred, gamma) * y_true * np.log(y_pred), axis=1 + ) + + return np.mean(cfce_loss) + + +def hinge_loss(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Calculate the mean hinge loss for between true labels and predicted probabilities + for training support vector machines (SVMs). + + Hinge loss = max(0, 1 - true * pred) + + Reference: https://en.wikipedia.org/wiki/Hinge_loss + + Args: + - y_true: actual values (ground truth) encoded as -1 or 1 + - y_pred: predicted values + + >>> true_labels = np.array([-1, 1, 1, -1, 1]) + >>> pred = np.array([-4, -0.3, 0.7, 5, 10]) + >>> float(hinge_loss(true_labels, pred)) + 1.52 + >>> true_labels = np.array([-1, 1, 1, -1, 1, 1]) + >>> pred = np.array([-4, -0.3, 0.7, 5, 10]) + >>> hinge_loss(true_labels, pred) + Traceback (most recent call last): + ... + ValueError: Length of predicted and actual array must be same. + >>> true_labels = np.array([-1, 1, 10, -1, 1]) + >>> pred = np.array([-4, -0.3, 0.7, 5, 10]) + >>> hinge_loss(true_labels, pred) + Traceback (most recent call last): + ... + ValueError: y_true can have values -1 or 1 only. + """ + if len(y_true) != len(y_pred): + raise ValueError("Length of predicted and actual array must be same.") + + if np.any((y_true != -1) & (y_true != 1)): + raise ValueError("y_true can have values -1 or 1 only.") + + hinge_losses = np.maximum(0, 1.0 - (y_true * y_pred)) + return np.mean(hinge_losses) + + +def huber_loss(y_true: np.ndarray, y_pred: np.ndarray, delta: float) -> float: + """ + Calculate the mean Huber loss between the given ground truth and predicted values. + + The Huber loss describes the penalty incurred by an estimation procedure, and it + serves as a measure of accuracy for regression models. + + Huber loss = + 0.5 * (y_true - y_pred)^2 if |y_true - y_pred| <= delta + delta * |y_true - y_pred| - 0.5 * delta^2 otherwise + + Reference: https://en.wikipedia.org/wiki/Huber_loss + + Parameters: + - y_true: The true values (ground truth) + - y_pred: The predicted values + + >>> true_values = np.array([0.9, 10.0, 2.0, 1.0, 5.2]) + >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2]) + >>> bool(np.isclose(huber_loss(true_values, predicted_values, 1.0), 2.102)) + True + >>> true_labels = np.array([11.0, 21.0, 3.32, 4.0, 5.0]) + >>> predicted_probs = np.array([8.3, 20.8, 2.9, 11.2, 5.0]) + >>> bool(np.isclose(huber_loss(true_labels, predicted_probs, 1.0), 1.80164)) + True + >>> true_labels = np.array([11.0, 21.0, 3.32, 4.0]) + >>> predicted_probs = np.array([8.3, 20.8, 2.9, 11.2, 5.0]) + >>> huber_loss(true_labels, predicted_probs, 1.0) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + huber_mse = 0.5 * (y_true - y_pred) ** 2 + huber_mae = delta * (np.abs(y_true - y_pred) - 0.5 * delta) + return np.where(np.abs(y_true - y_pred) <= delta, huber_mse, huber_mae).mean() + + +def mean_squared_error(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Calculate the mean squared error (MSE) between ground truth and predicted values. + + MSE measures the squared difference between true values and predicted values, and it + serves as a measure of accuracy for regression models. + + MSE = (1/n) * Σ(y_true - y_pred)^2 + + Reference: https://en.wikipedia.org/wiki/Mean_squared_error + + Parameters: + - y_true: The true values (ground truth) + - y_pred: The predicted values + + >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2]) + >>> bool(np.isclose(mean_squared_error(true_values, predicted_values), 0.028)) + True + >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2]) + >>> mean_squared_error(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + squared_errors = (y_true - y_pred) ** 2 + return np.mean(squared_errors) + + +def mean_absolute_error(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Calculates the Mean Absolute Error (MAE) between ground truth (observed) + and predicted values. + + MAE measures the absolute difference between true values and predicted values. + + Equation: + MAE = (1/n) * Σ(abs(y_true - y_pred)) + + Reference: https://en.wikipedia.org/wiki/Mean_absolute_error + + Parameters: + - y_true: The true values (ground truth) + - y_pred: The predicted values + + >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2]) + >>> bool(np.isclose(mean_absolute_error(true_values, predicted_values), 0.16)) + True + >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2]) + >>> bool(np.isclose(mean_absolute_error(true_values, predicted_values), 2.16)) + False + >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_probs = np.array([0.3, 0.8, 0.9, 5.2]) + >>> mean_absolute_error(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + return np.mean(abs(y_true - y_pred)) + + +def mean_squared_logarithmic_error(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Calculate the mean squared logarithmic error (MSLE) between ground truth and + predicted values. + + MSLE measures the squared logarithmic difference between true values and predicted + values for regression models. It's particularly useful for dealing with skewed or + large-value data, and it's often used when the relative differences between + predicted and true values are more important than absolute differences. + + MSLE = (1/n) * Σ(log(1 + y_true) - log(1 + y_pred))^2 + + Reference: https://insideaiml.com/blog/MeanSquared-Logarithmic-Error-Loss-1035 + + Parameters: + - y_true: The true values (ground truth) + - y_pred: The predicted values + + >>> true_values = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_values = np.array([0.8, 2.1, 2.9, 4.2, 5.2]) + >>> float(mean_squared_logarithmic_error(true_values, predicted_values)) + 0.0030860877925181344 + >>> true_labels = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + >>> predicted_probs = np.array([0.3, 0.8, 0.9, 0.2]) + >>> mean_squared_logarithmic_error(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + squared_logarithmic_errors = (np.log1p(y_true) - np.log1p(y_pred)) ** 2 + return np.mean(squared_logarithmic_errors) + + +def mean_absolute_percentage_error( + y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-15 +) -> float: + """ + Calculate the Mean Absolute Percentage Error between y_true and y_pred. + + Mean Absolute Percentage Error calculates the average of the absolute + percentage differences between the predicted and true values. + + Formula = (Σ|y_true[i]-Y_pred[i]/y_true[i]|)/n + + Source: https://stephenallwright.com/good-mape-score/ + + Parameters: + y_true (np.ndarray): Numpy array containing true/target values. + y_pred (np.ndarray): Numpy array containing predicted values. + + Returns: + float: The Mean Absolute Percentage error between y_true and y_pred. + + Examples: + >>> y_true = np.array([10, 20, 30, 40]) + >>> y_pred = np.array([12, 18, 33, 45]) + >>> float(mean_absolute_percentage_error(y_true, y_pred)) + 0.13125 + + >>> y_true = np.array([1, 2, 3, 4]) + >>> y_pred = np.array([2, 3, 4, 5]) + >>> float(mean_absolute_percentage_error(y_true, y_pred)) + 0.5208333333333333 + + >>> y_true = np.array([34, 37, 44, 47, 48, 48, 46, 43, 32, 27, 26, 24]) + >>> y_pred = np.array([37, 40, 46, 44, 46, 50, 45, 44, 34, 30, 22, 23]) + >>> float(mean_absolute_percentage_error(y_true, y_pred)) + 0.064671076436071 + """ + if len(y_true) != len(y_pred): + raise ValueError("The length of the two arrays should be the same.") + + y_true = np.where(y_true == 0, epsilon, y_true) + absolute_percentage_diff = np.abs((y_true - y_pred) / y_true) + + return np.mean(absolute_percentage_diff) + + +def perplexity_loss( + y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1e-7 +) -> float: + """ + Calculate the perplexity for the y_true and y_pred. + + Compute the Perplexity which useful in predicting language model + accuracy in Natural Language Processing (NLP.) + Perplexity is measure of how certain the model in its predictions. + + Perplexity Loss = exp(-1/N (Σ ln(p(x))) + + Reference: + https://en.wikipedia.org/wiki/Perplexity + + Args: + y_true: Actual label encoded sentences of shape (batch_size, sentence_length) + y_pred: Predicted sentences of shape (batch_size, sentence_length, vocab_size) + epsilon: Small floating point number to avoid getting inf for log(0) + + Returns: + Perplexity loss between y_true and y_pred. + + >>> y_true = np.array([[1, 4], [2, 3]]) + >>> y_pred = np.array( + ... [[[0.28, 0.19, 0.21 , 0.15, 0.15], + ... [0.24, 0.19, 0.09, 0.18, 0.27]], + ... [[0.03, 0.26, 0.21, 0.18, 0.30], + ... [0.28, 0.10, 0.33, 0.15, 0.12]]] + ... ) + >>> float(perplexity_loss(y_true, y_pred)) + 5.0247347775367945 + >>> y_true = np.array([[1, 4], [2, 3]]) + >>> y_pred = np.array( + ... [[[0.28, 0.19, 0.21 , 0.15, 0.15], + ... [0.24, 0.19, 0.09, 0.18, 0.27], + ... [0.30, 0.10, 0.20, 0.15, 0.25]], + ... [[0.03, 0.26, 0.21, 0.18, 0.30], + ... [0.28, 0.10, 0.33, 0.15, 0.12], + ... [0.30, 0.10, 0.20, 0.15, 0.25]],] + ... ) + >>> perplexity_loss(y_true, y_pred) + Traceback (most recent call last): + ... + ValueError: Sentence length of y_true and y_pred must be equal. + >>> y_true = np.array([[1, 4], [2, 11]]) + >>> y_pred = np.array( + ... [[[0.28, 0.19, 0.21 , 0.15, 0.15], + ... [0.24, 0.19, 0.09, 0.18, 0.27]], + ... [[0.03, 0.26, 0.21, 0.18, 0.30], + ... [0.28, 0.10, 0.33, 0.15, 0.12]]] + ... ) + >>> perplexity_loss(y_true, y_pred) + Traceback (most recent call last): + ... + ValueError: Label value must not be greater than vocabulary size. + >>> y_true = np.array([[1, 4]]) + >>> y_pred = np.array( + ... [[[0.28, 0.19, 0.21 , 0.15, 0.15], + ... [0.24, 0.19, 0.09, 0.18, 0.27]], + ... [[0.03, 0.26, 0.21, 0.18, 0.30], + ... [0.28, 0.10, 0.33, 0.15, 0.12]]] + ... ) + >>> perplexity_loss(y_true, y_pred) + Traceback (most recent call last): + ... + ValueError: Batch size of y_true and y_pred must be equal. + """ + + vocab_size = y_pred.shape[2] + + if y_true.shape[0] != y_pred.shape[0]: + raise ValueError("Batch size of y_true and y_pred must be equal.") + if y_true.shape[1] != y_pred.shape[1]: + raise ValueError("Sentence length of y_true and y_pred must be equal.") + if np.max(y_true) > vocab_size: + raise ValueError("Label value must not be greater than vocabulary size.") + + # Matrix to select prediction value only for true class + filter_matrix = np.array( + [[list(np.eye(vocab_size)[word]) for word in sentence] for sentence in y_true] + ) + + # Getting the matrix containing prediction for only true class + true_class_pred = np.sum(y_pred * filter_matrix, axis=2).clip(epsilon, 1) + + # Calculating perplexity for each sentence + perp_losses = np.exp(np.negative(np.mean(np.log(true_class_pred), axis=1))) + + return np.mean(perp_losses) + + +def smooth_l1_loss(y_true: np.ndarray, y_pred: np.ndarray, beta: float = 1.0) -> float: + """ + Calculate the Smooth L1 Loss between y_true and y_pred. + + The Smooth L1 Loss is less sensitive to outliers than the L2 Loss and is often used + in regression problems, such as object detection. + + Smooth L1 Loss = + 0.5 * (x - y)^2 / beta, if |x - y| < beta + |x - y| - 0.5 * beta, otherwise + + Reference: + https://pytorch.org/docs/stable/generated/torch.nn.SmoothL1Loss.html + + Args: + y_true: Array of true values. + y_pred: Array of predicted values. + beta: Specifies the threshold at which to change between L1 and L2 loss. + + Returns: + The calculated Smooth L1 Loss between y_true and y_pred. + + Raises: + ValueError: If the length of the two arrays is not the same. + + >>> y_true = np.array([3, 5, 2, 7]) + >>> y_pred = np.array([2.9, 4.8, 2.1, 7.2]) + >>> float(smooth_l1_loss(y_true, y_pred, 1.0)) + 0.012500000000000022 + + >>> y_true = np.array([2, 4, 6]) + >>> y_pred = np.array([1, 5, 7]) + >>> float(smooth_l1_loss(y_true, y_pred, 1.0)) + 0.5 + + >>> y_true = np.array([1, 3, 5, 7]) + >>> y_pred = np.array([1, 3, 5, 7]) + >>> float(smooth_l1_loss(y_true, y_pred, 1.0)) + 0.0 + + >>> y_true = np.array([1, 3, 5]) + >>> y_pred = np.array([1, 3, 5, 7]) + >>> smooth_l1_loss(y_true, y_pred, 1.0) + Traceback (most recent call last): + ... + ValueError: The length of the two arrays should be the same. + """ + + if len(y_true) != len(y_pred): + raise ValueError("The length of the two arrays should be the same.") + + diff = np.abs(y_true - y_pred) + loss = np.where(diff < beta, 0.5 * diff**2 / beta, diff - 0.5 * beta) + return np.mean(loss) + + +def kullback_leibler_divergence(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Calculate the Kullback-Leibler divergence (KL divergence) loss between true labels + and predicted probabilities. + + KL divergence loss quantifies dissimilarity between true labels and predicted + probabilities. It's often used in training generative models. + + KL = Σ(y_true * ln(y_true / y_pred)) + + Reference: https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence + + Parameters: + - y_true: True class probabilities + - y_pred: Predicted class probabilities + + >>> true_labels = np.array([0.2, 0.3, 0.5]) + >>> predicted_probs = np.array([0.3, 0.3, 0.4]) + >>> float(kullback_leibler_divergence(true_labels, predicted_probs)) + 0.030478754035472025 + >>> true_labels = np.array([0.2, 0.3, 0.5]) + >>> predicted_probs = np.array([0.3, 0.3, 0.4, 0.5]) + >>> kullback_leibler_divergence(true_labels, predicted_probs) + Traceback (most recent call last): + ... + ValueError: Input arrays must have the same length. + """ + if len(y_true) != len(y_pred): + raise ValueError("Input arrays must have the same length.") + + kl_loss = y_true * np.log(y_true / y_pred) + return np.sum(kl_loss) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/lstm/lstm_prediction.py b/machine_learning/lstm/lstm_prediction.py index 74197c46a0ad..81ac5f01d3d6 100644 --- a/machine_learning/lstm/lstm_prediction.py +++ b/machine_learning/lstm/lstm_prediction.py @@ -1,14 +1,15 @@ """ - Create a Long Short Term Memory (LSTM) network model - An LSTM is a type of Recurrent Neural Network (RNN) as discussed at: - * https://colah.github.io/posts/2015-08-Understanding-LSTMs - * https://en.wikipedia.org/wiki/Long_short-term_memory +Create a Long Short Term Memory (LSTM) network model +An LSTM is a type of Recurrent Neural Network (RNN) as discussed at: +* https://colah.github.io/posts/2015-08-Understanding-LSTMs +* https://en.wikipedia.org/wiki/Long_short-term_memory """ + import numpy as np import pandas as pd +from keras.layers import LSTM, Dense +from keras.models import Sequential from sklearn.preprocessing import MinMaxScaler -from tensorflow.keras.layers import LSTM, Dense -from tensorflow.keras.models import Sequential if __name__ == "__main__": """ @@ -17,11 +18,11 @@ make sure you set the price column on line number 21. Here we use a dataset which have the price on 3rd column. """ - df = pd.read_csv("sample_data.csv", header=None) - len_data = df.shape[:1][0] + sample_data = pd.read_csv("sample_data.csv", header=None) + len_data = sample_data.shape[:1][0] # If you're using some other dataset input the target column - actual_data = df.iloc[:, 1:2] - actual_data = actual_data.values.reshape(len_data, 1) + actual_data = sample_data.iloc[:, 1:2] + actual_data = actual_data.to_numpy().reshape(len_data, 1) actual_data = MinMaxScaler().fit_transform(actual_data) look_back = 10 forward_days = 5 @@ -32,10 +33,10 @@ train_x, train_y = [], [] test_x, test_y = [], [] - for i in range(0, len(train_data) - forward_days - look_back + 1): + for i in range(len(train_data) - forward_days - look_back + 1): train_x.append(train_data[i : i + look_back]) train_y.append(train_data[i + look_back : i + look_back + forward_days]) - for i in range(0, len(test_data) - forward_days - look_back + 1): + for i in range(len(test_data) - forward_days - look_back + 1): test_x.append(test_data[i : i + look_back]) test_y.append(test_data[i + look_back : i + look_back + forward_days]) x_train = np.array(train_x) diff --git a/machine_learning/mfcc.py b/machine_learning/mfcc.py new file mode 100644 index 000000000000..dcc3151d5a1a --- /dev/null +++ b/machine_learning/mfcc.py @@ -0,0 +1,479 @@ +""" +Mel Frequency Cepstral Coefficients (MFCC) Calculation + +MFCC is an algorithm widely used in audio and speech processing to represent the +short-term power spectrum of a sound signal in a more compact and +discriminative way. It is particularly popular in speech and audio processing +tasks such as speech recognition and speaker identification. + +How Mel Frequency Cepstral Coefficients are Calculated: +1. Preprocessing: + - Load an audio signal and normalize it to ensure that the values fall + within a specific range (e.g., between -1 and 1). + - Frame the audio signal into overlapping, fixed-length segments, typically + using a technique like windowing to reduce spectral leakage. + +2. Fourier Transform: + - Apply a Fast Fourier Transform (FFT) to each audio frame to convert it + from the time domain to the frequency domain. This results in a + representation of the audio frame as a sequence of frequency components. + +3. Power Spectrum: + - Calculate the power spectrum by taking the squared magnitude of each + frequency component obtained from the FFT. This step measures the energy + distribution across different frequency bands. + +4. Mel Filterbank: + - Apply a set of triangular filterbanks spaced in the Mel frequency scale + to the power spectrum. These filters mimic the human auditory system's + frequency response. Each filterbank sums the power spectrum values within + its band. + +5. Logarithmic Compression: + - Take the logarithm (typically base 10) of the filterbank values to + compress the dynamic range. This step mimics the logarithmic response of + the human ear to sound intensity. + +6. Discrete Cosine Transform (DCT): + - Apply the Discrete Cosine Transform to the log filterbank energies to + obtain the MFCC coefficients. This transformation helps decorrelate the + filterbank energies and captures the most important features of the audio + signal. + +7. Feature Extraction: + - Select a subset of the DCT coefficients to form the feature vector. + Often, the first few coefficients (e.g., 12-13) are used for most + applications. + +References: +- Mel-Frequency Cepstral Coefficients (MFCCs): + https://en.wikipedia.org/wiki/Mel-frequency_cepstrum +- Speech and Language Processing by Daniel Jurafsky & James H. Martin: + https://web.stanford.edu/~jurafsky/slp3/ +- Mel Frequency Cepstral Coefficient (MFCC) tutorial + http://practicalcryptography.com/miscellaneous/machine-learning + /guide-mel-frequency-cepstral-coefficients-mfccs/ + +Author: Amir Lavasani +""" + +import logging + +import numpy as np +import scipy.fftpack as fft +from scipy.signal import get_window + +logging.basicConfig(filename=f"{__file__}.log", level=logging.INFO) + + +def mfcc( + audio: np.ndarray, + sample_rate: int, + ftt_size: int = 1024, + hop_length: int = 20, + mel_filter_num: int = 10, + dct_filter_num: int = 40, +) -> np.ndarray: + """ + Calculate Mel Frequency Cepstral Coefficients (MFCCs) from an audio signal. + + Args: + audio: The input audio signal. + sample_rate: The sample rate of the audio signal (in Hz). + ftt_size: The size of the FFT window (default is 1024). + hop_length: The hop length for frame creation (default is 20ms). + mel_filter_num: The number of Mel filters (default is 10). + dct_filter_num: The number of DCT filters (default is 40). + + Returns: + A matrix of MFCCs for the input audio. + + Raises: + ValueError: If the input audio is empty. + + Example: + >>> sample_rate = 44100 # Sample rate of 44.1 kHz + >>> duration = 2.0 # Duration of 1 second + >>> t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) + >>> audio = 0.5 * np.sin(2 * np.pi * 440.0 * t) # Generate a 440 Hz sine wave + >>> mfccs = mfcc(audio, sample_rate) + >>> mfccs.shape + (40, 101) + """ + logging.info(f"Sample rate: {sample_rate}Hz") + logging.info(f"Audio duration: {len(audio) / sample_rate}s") + logging.info(f"Audio min: {np.min(audio)}") + logging.info(f"Audio max: {np.max(audio)}") + + # normalize audio + audio_normalized = normalize(audio) + + logging.info(f"Normalized audio min: {np.min(audio_normalized)}") + logging.info(f"Normalized audio max: {np.max(audio_normalized)}") + + # frame audio into + audio_framed = audio_frames( + audio_normalized, sample_rate, ftt_size=ftt_size, hop_length=hop_length + ) + + logging.info(f"Framed audio shape: {audio_framed.shape}") + logging.info(f"First frame: {audio_framed[0]}") + + # convert to frequency domain + # For simplicity we will choose the Hanning window. + window = get_window("hann", ftt_size, fftbins=True) + audio_windowed = audio_framed * window + + logging.info(f"Windowed audio shape: {audio_windowed.shape}") + logging.info(f"First frame: {audio_windowed[0]}") + + audio_fft = calculate_fft(audio_windowed, ftt_size) + logging.info(f"fft audio shape: {audio_fft.shape}") + logging.info(f"First frame: {audio_fft[0]}") + + audio_power = calculate_signal_power(audio_fft) + logging.info(f"power audio shape: {audio_power.shape}") + logging.info(f"First frame: {audio_power[0]}") + + filters = mel_spaced_filterbank(sample_rate, mel_filter_num, ftt_size) + logging.info(f"filters shape: {filters.shape}") + + audio_filtered = np.dot(filters, np.transpose(audio_power)) + audio_log = 10.0 * np.log10(audio_filtered) + logging.info(f"audio_log shape: {audio_log.shape}") + + dct_filters = discrete_cosine_transform(dct_filter_num, mel_filter_num) + cepstral_coefficents = np.dot(dct_filters, audio_log) + + logging.info(f"cepstral_coefficents shape: {cepstral_coefficents.shape}") + return cepstral_coefficents + + +def normalize(audio: np.ndarray) -> np.ndarray: + """ + Normalize an audio signal by scaling it to have values between -1 and 1. + + Args: + audio: The input audio signal. + + Returns: + The normalized audio signal. + + Examples: + >>> audio = np.array([1, 2, 3, 4, 5]) + >>> normalized_audio = normalize(audio) + >>> float(np.max(normalized_audio)) + 1.0 + >>> float(np.min(normalized_audio)) + 0.2 + """ + # Divide the entire audio signal by the maximum absolute value + return audio / np.max(np.abs(audio)) + + +def audio_frames( + audio: np.ndarray, + sample_rate: int, + hop_length: int = 20, + ftt_size: int = 1024, +) -> np.ndarray: + """ + Split an audio signal into overlapping frames. + + Args: + audio: The input audio signal. + sample_rate: The sample rate of the audio signal. + hop_length: The length of the hopping (default is 20ms). + ftt_size: The size of the FFT window (default is 1024). + + Returns: + An array of overlapping frames. + + Examples: + >>> audio = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]*1000) + >>> sample_rate = 8000 + >>> frames = audio_frames(audio, sample_rate, hop_length=10, ftt_size=512) + >>> frames.shape + (126, 512) + """ + + hop_size = np.round(sample_rate * hop_length / 1000).astype(int) + + # Pad the audio signal to handle edge cases + audio = np.pad(audio, int(ftt_size / 2), mode="reflect") + + # Calculate the number of frames + frame_count = int((len(audio) - ftt_size) / hop_size) + 1 + + # Initialize an array to store the frames + frames = np.zeros((frame_count, ftt_size)) + + # Split the audio signal into frames + for n in range(frame_count): + frames[n] = audio[n * hop_size : n * hop_size + ftt_size] + + return frames + + +def calculate_fft(audio_windowed: np.ndarray, ftt_size: int = 1024) -> np.ndarray: + """ + Calculate the Fast Fourier Transform (FFT) of windowed audio data. + + Args: + audio_windowed: The windowed audio signal. + ftt_size: The size of the FFT (default is 1024). + + Returns: + The FFT of the audio data. + + Examples: + >>> audio_windowed = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + >>> audio_fft = calculate_fft(audio_windowed, ftt_size=4) + >>> bool(np.allclose(audio_fft[0], np.array([6.0+0.j, -1.5+0.8660254j, + ... -1.5-0.8660254j]))) + True + """ + # Transpose the audio data to have time in rows and channels in columns + audio_transposed = np.transpose(audio_windowed) + + # Initialize an array to store the FFT results + audio_fft = np.empty( + (int(1 + ftt_size // 2), audio_transposed.shape[1]), + dtype=np.complex64, + order="F", + ) + + # Compute FFT for each channel + for n in range(audio_fft.shape[1]): + audio_fft[:, n] = fft.fft(audio_transposed[:, n], axis=0)[: audio_fft.shape[0]] + + # Transpose the FFT results back to the original shape + return np.transpose(audio_fft) + + +def calculate_signal_power(audio_fft: np.ndarray) -> np.ndarray: + """ + Calculate the power of the audio signal from its FFT. + + Args: + audio_fft: The FFT of the audio signal. + + Returns: + The power of the audio signal. + + Examples: + >>> audio_fft = np.array([1+2j, 2+3j, 3+4j, 4+5j]) + >>> power = calculate_signal_power(audio_fft) + >>> np.allclose(power, np.array([5, 13, 25, 41])) + True + """ + # Calculate the power by squaring the absolute values of the FFT coefficients + return np.square(np.abs(audio_fft)) + + +def freq_to_mel(freq: float) -> float: + """ + Convert a frequency in Hertz to the mel scale. + + Args: + freq: The frequency in Hertz. + + Returns: + The frequency in mel scale. + + Examples: + >>> float(round(freq_to_mel(1000), 2)) + 999.99 + """ + # Use the formula to convert frequency to the mel scale + return 2595.0 * np.log10(1.0 + freq / 700.0) + + +def mel_to_freq(mels: float) -> float: + """ + Convert a frequency in the mel scale to Hertz. + + Args: + mels: The frequency in mel scale. + + Returns: + The frequency in Hertz. + + Examples: + >>> round(mel_to_freq(999.99), 2) + 1000.01 + """ + # Use the formula to convert mel scale to frequency + return 700.0 * (10.0 ** (mels / 2595.0) - 1.0) + + +def mel_spaced_filterbank( + sample_rate: int, mel_filter_num: int = 10, ftt_size: int = 1024 +) -> np.ndarray: + """ + Create a Mel-spaced filter bank for audio processing. + + Args: + sample_rate: The sample rate of the audio. + mel_filter_num: The number of mel filters (default is 10). + ftt_size: The size of the FFT (default is 1024). + + Returns: + Mel-spaced filter bank. + + Examples: + >>> float(round(mel_spaced_filterbank(8000, 10, 1024)[0][1], 10)) + 0.0004603981 + """ + freq_min = 0 + freq_high = sample_rate // 2 + + logging.info(f"Minimum frequency: {freq_min}") + logging.info(f"Maximum frequency: {freq_high}") + + # Calculate filter points and mel frequencies + filter_points, mel_freqs = get_filter_points( + sample_rate, + freq_min, + freq_high, + mel_filter_num, + ftt_size, + ) + + filters = get_filters(filter_points, ftt_size) + + # normalize filters + # taken from the librosa library + enorm = 2.0 / (mel_freqs[2 : mel_filter_num + 2] - mel_freqs[:mel_filter_num]) + return filters * enorm[:, np.newaxis] + + +def get_filters(filter_points: np.ndarray, ftt_size: int) -> np.ndarray: + """ + Generate filters for audio processing. + + Args: + filter_points: A list of filter points. + ftt_size: The size of the FFT. + + Returns: + A matrix of filters. + + Examples: + >>> get_filters(np.array([0, 20, 51, 95, 161, 256], dtype=int), 512).shape + (4, 257) + """ + num_filters = len(filter_points) - 2 + filters = np.zeros((num_filters, int(ftt_size / 2) + 1)) + + for n in range(num_filters): + start = filter_points[n] + mid = filter_points[n + 1] + end = filter_points[n + 2] + + # Linearly increase values from 0 to 1 + filters[n, start:mid] = np.linspace(0, 1, mid - start) + + # Linearly decrease values from 1 to 0 + filters[n, mid:end] = np.linspace(1, 0, end - mid) + + return filters + + +def get_filter_points( + sample_rate: int, + freq_min: int, + freq_high: int, + mel_filter_num: int = 10, + ftt_size: int = 1024, +) -> tuple[np.ndarray, np.ndarray]: + """ + Calculate the filter points and frequencies for mel frequency filters. + + Args: + sample_rate: The sample rate of the audio. + freq_min: The minimum frequency in Hertz. + freq_high: The maximum frequency in Hertz. + mel_filter_num: The number of mel filters (default is 10). + ftt_size: The size of the FFT (default is 1024). + + Returns: + Filter points and corresponding frequencies. + + Examples: + >>> filter_points = get_filter_points(8000, 0, 4000, mel_filter_num=4, ftt_size=512) + >>> filter_points[0] + array([ 0, 20, 51, 95, 161, 256]) + >>> filter_points[1] + array([ 0. , 324.46707094, 799.33254207, 1494.30973963, + 2511.42581671, 4000. ]) + """ + # Convert minimum and maximum frequencies to mel scale + fmin_mel = freq_to_mel(freq_min) + fmax_mel = freq_to_mel(freq_high) + + logging.info(f"MEL min: {fmin_mel}") + logging.info(f"MEL max: {fmax_mel}") + + # Generate equally spaced mel frequencies + mels = np.linspace(fmin_mel, fmax_mel, num=mel_filter_num + 2) + + # Convert mel frequencies back to Hertz + freqs = mel_to_freq(mels) + + # Calculate filter points as integer values + filter_points = np.floor((ftt_size + 1) / sample_rate * freqs).astype(int) + + return filter_points, freqs + + +def discrete_cosine_transform(dct_filter_num: int, filter_num: int) -> np.ndarray: + """ + Compute the Discrete Cosine Transform (DCT) basis matrix. + + Args: + dct_filter_num: The number of DCT filters to generate. + filter_num: The number of the fbank filters. + + Returns: + The DCT basis matrix. + + Examples: + >>> float(round(discrete_cosine_transform(3, 5)[0][0], 5)) + 0.44721 + """ + basis = np.empty((dct_filter_num, filter_num)) + basis[0, :] = 1.0 / np.sqrt(filter_num) + + samples = np.arange(1, 2 * filter_num, 2) * np.pi / (2.0 * filter_num) + + for i in range(1, dct_filter_num): + basis[i, :] = np.cos(i * samples) * np.sqrt(2.0 / filter_num) + + return basis + + +def example(wav_file_path: str = "./path-to-file/sample.wav") -> np.ndarray: + """ + Example function to calculate Mel Frequency Cepstral Coefficients + (MFCCs) from an audio file. + + Args: + wav_file_path: The path to the WAV audio file. + + Returns: + np.ndarray: The computed MFCCs for the audio. + """ + from scipy.io import wavfile + + # Load the audio from the WAV file + sample_rate, audio = wavfile.read(wav_file_path) + + # Calculate MFCCs + return mfcc(audio, sample_rate) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/multilayer_perceptron_classifier.py b/machine_learning/multilayer_perceptron_classifier.py index e99a4131e972..40f998c7dfa2 100644 --- a/machine_learning/multilayer_perceptron_classifier.py +++ b/machine_learning/multilayer_perceptron_classifier.py @@ -17,7 +17,7 @@ def wrapper(y): """ - >>> wrapper(Y) + >>> [int(x) for x in wrapper(Y)] [0, 0, 1] """ return list(y) diff --git a/machine_learning/polymonial_regression.py b/machine_learning/polymonial_regression.py deleted file mode 100644 index 374c35f7f905..000000000000 --- a/machine_learning/polymonial_regression.py +++ /dev/null @@ -1,45 +0,0 @@ -import pandas as pd -from matplotlib import pyplot as plt -from sklearn.linear_model import LinearRegression - -# Splitting the dataset into the Training set and Test set -from sklearn.model_selection import train_test_split - -# Fitting Polynomial Regression to the dataset -from sklearn.preprocessing import PolynomialFeatures - -# Importing the dataset -dataset = pd.read_csv( - "https://s3.us-west-2.amazonaws.com/public.gamelab.fun/dataset/" - "position_salaries.csv" -) -X = dataset.iloc[:, 1:2].values -y = dataset.iloc[:, 2].values - - -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) - - -poly_reg = PolynomialFeatures(degree=4) -X_poly = poly_reg.fit_transform(X) -pol_reg = LinearRegression() -pol_reg.fit(X_poly, y) - - -# Visualizing the Polymonial Regression results -def viz_polymonial(): - plt.scatter(X, y, color="red") - plt.plot(X, pol_reg.predict(poly_reg.fit_transform(X)), color="blue") - plt.title("Truth or Bluff (Linear Regression)") - plt.xlabel("Position level") - plt.ylabel("Salary") - plt.show() - return - - -if __name__ == "__main__": - viz_polymonial() - - # Predicting a new result with Polymonial Regression - pol_reg.predict(poly_reg.fit_transform([[5.5]])) - # output should be 132148.43750003 diff --git a/machine_learning/polynomial_regression.py b/machine_learning/polynomial_regression.py new file mode 100644 index 000000000000..212f40bea197 --- /dev/null +++ b/machine_learning/polynomial_regression.py @@ -0,0 +1,213 @@ +""" +Polynomial regression is a type of regression analysis that models the relationship +between a predictor x and the response y as an mth-degree polynomial: + +y = β₀ + βâ‚x + β₂x² + ... + βₘxáµ + ε + +By treating x, x², ..., xáµ as distinct variables, we see that polynomial regression is a +special case of multiple linear regression. Therefore, we can use ordinary least squares +(OLS) estimation to estimate the vector of model parameters β = (β₀, βâ‚, β₂, ..., βₘ) +for polynomial regression: + +β = (Xáµ€X)â»Â¹Xáµ€y = Xâºy + +where X is the design matrix, y is the response vector, and X⺠denotes the Moore-Penrose +pseudoinverse of X. In the case of polynomial regression, the design matrix is + + |1 xâ‚ x₲ ⋯ xâ‚áµ| +X = |1 xâ‚‚ x₂² ⋯ xâ‚‚áµ| + |â‹® â‹® â‹® ⋱ â‹® | + |1 xâ‚™ xₙ² ⋯ xâ‚™áµ| + +In OLS estimation, inverting Xáµ€X to compute X⺠can be very numerically unstable. This +implementation sidesteps this need to invert Xáµ€X by computing X⺠using singular value +decomposition (SVD): + +β = VΣâºUáµ€y + +where UΣVáµ€ is an SVD of X. + +References: + - https://en.wikipedia.org/wiki/Polynomial_regression + - https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse + - https://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares + - https://en.wikipedia.org/wiki/Singular_value_decomposition +""" + +import matplotlib.pyplot as plt +import numpy as np + + +class PolynomialRegression: + __slots__ = "degree", "params" + + def __init__(self, degree: int) -> None: + """ + @raises ValueError: if the polynomial degree is negative + """ + if degree < 0: + raise ValueError("Polynomial degree must be non-negative") + + self.degree = degree + self.params = None + + @staticmethod + def _design_matrix(data: np.ndarray, degree: int) -> np.ndarray: + """ + Constructs a polynomial regression design matrix for the given input data. For + input data x = (xâ‚, xâ‚‚, ..., xâ‚™) and polynomial degree m, the design matrix is + the Vandermonde matrix + + |1 xâ‚ x₲ ⋯ xâ‚áµ| + X = |1 xâ‚‚ x₂² ⋯ xâ‚‚áµ| + |â‹® â‹® â‹® ⋱ â‹® | + |1 xâ‚™ xₙ² ⋯ xâ‚™áµ| + + Reference: https://en.wikipedia.org/wiki/Vandermonde_matrix + + @param data: the input predictor values x, either for model fitting or for + prediction + @param degree: the polynomial degree m + @returns: the Vandermonde matrix X (see above) + @raises ValueError: if input data is not N x 1 + + >>> x = np.array([0, 1, 2]) + >>> PolynomialRegression._design_matrix(x, degree=0) + array([[1], + [1], + [1]]) + >>> PolynomialRegression._design_matrix(x, degree=1) + array([[1, 0], + [1, 1], + [1, 2]]) + >>> PolynomialRegression._design_matrix(x, degree=2) + array([[1, 0, 0], + [1, 1, 1], + [1, 2, 4]]) + >>> PolynomialRegression._design_matrix(x, degree=3) + array([[1, 0, 0, 0], + [1, 1, 1, 1], + [1, 2, 4, 8]]) + >>> PolynomialRegression._design_matrix(np.array([[0, 0], [0 , 0]]), degree=3) + Traceback (most recent call last): + ... + ValueError: Data must have dimensions N x 1 + """ + rows, *remaining = data.shape + if remaining: + raise ValueError("Data must have dimensions N x 1") + + return np.vander(data, N=degree + 1, increasing=True) + + def fit(self, x_train: np.ndarray, y_train: np.ndarray) -> None: + """ + Computes the polynomial regression model parameters using ordinary least squares + (OLS) estimation: + + β = (Xáµ€X)â»Â¹Xáµ€y = Xâºy + + where X⺠denotes the Moore-Penrose pseudoinverse of the design matrix X. This + function computes X⺠using singular value decomposition (SVD). + + References: + - https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse + - https://en.wikipedia.org/wiki/Singular_value_decomposition + - https://en.wikipedia.org/wiki/Multicollinearity + + @param x_train: the predictor values x for model fitting + @param y_train: the response values y for model fitting + @raises ArithmeticError: if X isn't full rank, then Xáµ€X is singular and β + doesn't exist + + >>> x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + >>> y = x**3 - 2 * x**2 + 3 * x - 5 + >>> poly_reg = PolynomialRegression(degree=3) + >>> poly_reg.fit(x, y) + >>> poly_reg.params + array([-5., 3., -2., 1.]) + >>> poly_reg = PolynomialRegression(degree=20) + >>> poly_reg.fit(x, y) + Traceback (most recent call last): + ... + ArithmeticError: Design matrix is not full rank, can't compute coefficients + + Make sure errors don't grow too large: + >>> coefs = np.array([-250, 50, -2, 36, 20, -12, 10, 2, -1, -15, 1]) + >>> y = PolynomialRegression._design_matrix(x, len(coefs) - 1) @ coefs + >>> poly_reg = PolynomialRegression(degree=len(coefs) - 1) + >>> poly_reg.fit(x, y) + >>> np.allclose(poly_reg.params, coefs, atol=10e-3) + True + """ + X = PolynomialRegression._design_matrix(x_train, self.degree) # noqa: N806 + _, cols = X.shape + if np.linalg.matrix_rank(X) < cols: + raise ArithmeticError( + "Design matrix is not full rank, can't compute coefficients" + ) + + # np.linalg.pinv() computes the Moore-Penrose pseudoinverse using SVD + self.params = np.linalg.pinv(X) @ y_train + + def predict(self, data: np.ndarray) -> np.ndarray: + """ + Computes the predicted response values y for the given input data by + constructing the design matrix X and evaluating y = Xβ. + + @param data: the predictor values x for prediction + @returns: the predicted response values y = Xβ + @raises ArithmeticError: if this function is called before the model + parameters are fit + + >>> x = np.array([0, 1, 2, 3, 4]) + >>> y = x**3 - 2 * x**2 + 3 * x - 5 + >>> poly_reg = PolynomialRegression(degree=3) + >>> poly_reg.fit(x, y) + >>> poly_reg.predict(np.array([-1])) + array([-11.]) + >>> poly_reg.predict(np.array([-2])) + array([-27.]) + >>> poly_reg.predict(np.array([6])) + array([157.]) + >>> PolynomialRegression(degree=3).predict(x) + Traceback (most recent call last): + ... + ArithmeticError: Predictor hasn't been fit yet + """ + if self.params is None: + raise ArithmeticError("Predictor hasn't been fit yet") + + return PolynomialRegression._design_matrix(data, self.degree) @ self.params + + +def main() -> None: + """ + Fit a polynomial regression model to predict fuel efficiency using seaborn's mpg + dataset + + >>> pass # Placeholder, function is only for demo purposes + """ + import seaborn as sns + + mpg_data = sns.load_dataset("mpg") + + poly_reg = PolynomialRegression(degree=2) + poly_reg.fit(mpg_data.weight, mpg_data.mpg) + + weight_sorted = np.sort(mpg_data.weight) + predictions = poly_reg.predict(weight_sorted) + + plt.scatter(mpg_data.weight, mpg_data.mpg, color="gray", alpha=0.5) + plt.plot(weight_sorted, predictions, color="red", linewidth=3) + plt.title("Predicting Fuel Efficiency Using Polynomial Regression") + plt.xlabel("Weight (lbs)") + plt.ylabel("Fuel Efficiency (mpg)") + plt.show() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + main() diff --git a/machine_learning/scoring_functions.py b/machine_learning/scoring_functions.py index 08b969a95c3b..f6b685f4f98a 100644 --- a/machine_learning/scoring_functions.py +++ b/machine_learning/scoring_functions.py @@ -20,11 +20,11 @@ def mae(predict, actual): """ Examples(rounded for precision): >>> actual = [1,2,3];predict = [1,4,3] - >>> np.around(mae(predict,actual),decimals = 2) + >>> float(np.around(mae(predict,actual),decimals = 2)) 0.67 >>> actual = [1,1,1];predict = [1,1,1] - >>> mae(predict,actual) + >>> float(mae(predict,actual)) 0.0 """ predict = np.array(predict) @@ -41,11 +41,11 @@ def mse(predict, actual): """ Examples(rounded for precision): >>> actual = [1,2,3];predict = [1,4,3] - >>> np.around(mse(predict,actual),decimals = 2) + >>> float(np.around(mse(predict,actual),decimals = 2)) 1.33 >>> actual = [1,1,1];predict = [1,1,1] - >>> mse(predict,actual) + >>> float(mse(predict,actual)) 0.0 """ predict = np.array(predict) @@ -63,11 +63,11 @@ def rmse(predict, actual): """ Examples(rounded for precision): >>> actual = [1,2,3];predict = [1,4,3] - >>> np.around(rmse(predict,actual),decimals = 2) + >>> float(np.around(rmse(predict,actual),decimals = 2)) 1.15 >>> actual = [1,1,1];predict = [1,1,1] - >>> rmse(predict,actual) + >>> float(rmse(predict,actual)) 0.0 """ predict = np.array(predict) @@ -84,12 +84,10 @@ def rmse(predict, actual): def rmsle(predict, actual): """ Examples(rounded for precision): - >>> actual = [10,10,30];predict = [10,2,30] - >>> np.around(rmsle(predict,actual),decimals = 2) + >>> float(np.around(rmsle(predict=[10, 2, 30], actual=[10, 10, 30]), decimals=2)) 0.75 - >>> actual = [1,1,1];predict = [1,1,1] - >>> rmsle(predict,actual) + >>> float(rmsle(predict=[1, 1, 1], actual=[1, 1, 1])) 0.0 """ predict = np.array(predict) @@ -117,12 +115,12 @@ def mbd(predict, actual): Here the model overpredicts >>> actual = [1,2,3];predict = [2,3,4] - >>> np.around(mbd(predict,actual),decimals = 2) + >>> float(np.around(mbd(predict,actual),decimals = 2)) 50.0 Here the model underpredicts >>> actual = [1,2,3];predict = [0,1,1] - >>> np.around(mbd(predict,actual),decimals = 2) + >>> float(np.around(mbd(predict,actual),decimals = 2)) -66.67 """ predict = np.array(predict) diff --git a/machine_learning/self_organizing_map.py b/machine_learning/self_organizing_map.py index 32fdf1d2b41d..fb9d0074e791 100644 --- a/machine_learning/self_organizing_map.py +++ b/machine_learning/self_organizing_map.py @@ -1,6 +1,7 @@ """ https://en.wikipedia.org/wiki/Self-organizing_map """ + import math diff --git a/machine_learning/sequential_minimum_optimization.py b/machine_learning/sequential_minimum_optimization.py index 9c45c351272f..625fc28fe60c 100644 --- a/machine_learning/sequential_minimum_optimization.py +++ b/machine_learning/sequential_minimum_optimization.py @@ -1,11 +1,9 @@ """ - Implementation of sequential minimal optimization (SMO) for support vector machines - (SVM). +Sequential minimal optimization (SMO) for support vector machines (SVM) - Sequential minimal optimization (SMO) is an algorithm for solving the quadratic - programming (QP) problem that arises during the training of support vector - machines. - It was invented by John Platt in 1998. +Sequential minimal optimization (SMO) is an algorithm for solving the quadratic +programming (QP) problem that arises during the training of SVMs. It was invented by +John Platt in 1998. Input: 0: type: numpy.ndarray. @@ -30,7 +28,6 @@ https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-98-14.pdf """ - import os import sys import urllib.request @@ -125,11 +122,10 @@ def fit(self): b_old = self._b self._b = b - # 4: update error value,here we only calculate those non-bound samples' - # error + # 4: update error, here we only calculate the error for non-bound samples self._unbound = [i for i in self._all_samples if self._is_unbound(i)] for s in self.unbound: - if s == i1 or s == i2: + if s in (i1, i2): continue self._error[s] += ( y1 * (a1_new - a1) * k(i1, s) @@ -137,7 +133,7 @@ def fit(self): + (self._b - b_old) ) - # if i1 or i2 is non-bound,update there error value to zero + # if i1 or i2 is non-bound, update their error value to zero if self._is_unbound(i1): self._error[i1] = 0 if self._is_unbound(i2): @@ -162,7 +158,7 @@ def predict(self, test_samples, classify=True): results.append(result) return np.array(results) - # Check if alpha violate KKT condition + # Check if alpha violates the KKT condition def _check_obey_kkt(self, index): alphas = self.alphas tol = self._tol @@ -173,20 +169,19 @@ def _check_obey_kkt(self, index): # Get value calculated from kernel function def _k(self, i1, i2): - # for test samples,use Kernel function + # for test samples, use kernel function if isinstance(i2, np.ndarray): return self.Kernel(self.samples[i1], i2) - # for train samples,Kernel values have been saved in matrix + # for training samples, kernel values have been saved in matrix else: return self._K_matrix[i1, i2] - # Get sample's error + # Get error for sample def _e(self, index): """ Two cases: - 1:Sample[index] is non-bound,Fetch error from list: _error - 2:sample[index] is bound,Use predicted value deduct true value: g(xi) - yi - + 1: Sample[index] is non-bound, fetch error from list: _error + 2: sample[index] is bound, use predicted value minus true value: g(xi) - yi """ # get from error data if self._is_unbound(index): @@ -197,7 +192,7 @@ def _e(self, index): yi = self.tags[index] return gx - yi - # Calculate Kernel matrix of all possible i1,i2 ,saving time + # Calculate kernel matrix of all possible i1, i2, saving time def _calculate_k_matrix(self): k_matrix = np.zeros([self.length, self.length]) for i in self._all_samples: @@ -207,7 +202,7 @@ def _calculate_k_matrix(self): ) return k_matrix - # Predict test sample's tag + # Predict tag for test sample def _predict(self, sample): k = self._k predicted_value = ( @@ -223,30 +218,31 @@ def _predict(self, sample): # Choose alpha1 and alpha2 def _choose_alphas(self): - locis = yield from self._choose_a1() - if not locis: - return - return locis + loci = yield from self._choose_a1() + if not loci: + return None + return loci def _choose_a1(self): """ - Choose first alpha ;steps: - 1:First loop over all sample - 2:Second loop over all non-bound samples till all non-bound samples does not - voilate kkt condition. - 3:Repeat this two process endlessly,till all samples does not voilate kkt - condition samples after first loop. + Choose first alpha + Steps: + 1: First loop over all samples + 2: Second loop over all non-bound samples until no non-bound samples violate + the KKT condition. + 3: Repeat these two processes until no samples violate the KKT condition + after the first loop. """ while True: all_not_obey = True # all sample - print("scanning all sample!") + print("Scanning all samples!") for i1 in [i for i in self._all_samples if self._check_obey_kkt(i)]: all_not_obey = False yield from self._choose_a2(i1) # non-bound sample - print("scanning non-bound sample!") + print("Scanning non-bound samples!") while True: not_obey = True for i1 in [ @@ -257,20 +253,21 @@ def _choose_a1(self): not_obey = False yield from self._choose_a2(i1) if not_obey: - print("all non-bound samples fit the KKT condition!") + print("All non-bound samples satisfy the KKT condition!") break if all_not_obey: - print("all samples fit the KKT condition! Optimization done!") + print("All samples satisfy the KKT condition!") break return False def _choose_a2(self, i1): """ - Choose the second alpha by using heuristic algorithm ;steps: - 1: Choose alpha2 which gets the maximum step size (|E1 - E2|). - 2: Start in a random point,loop over all non-bound samples till alpha1 and + Choose the second alpha using a heuristic algorithm + Steps: + 1: Choose alpha2 that maximizes the step size (|E1 - E2|). + 2: Start in a random point, loop over all non-bound samples till alpha1 and alpha2 are optimized. - 3: Start in a random point,loop over all samples till alpha1 and alpha2 are + 3: Start in a random point, loop over all samples till alpha1 and alpha2 are optimized. """ self._unbound = [i for i in self._all_samples if self._is_unbound(i)] @@ -290,12 +287,13 @@ def _choose_a2(self, i1): if cmd is None: return - for i2 in np.roll(self.unbound, np.random.choice(self.length)): + rng = np.random.default_rng() + for i2 in np.roll(self.unbound, rng.choice(self.length)): cmd = yield i1, i2 if cmd is None: return - for i2 in np.roll(self._all_samples, np.random.choice(self.length)): + for i2 in np.roll(self._all_samples, rng.choice(self.length)): cmd = yield i1, i2 if cmd is None: return @@ -306,12 +304,12 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): if i1 == i2: return None, None - # calculate L and H which bound the new alpha2 + # calculate L and H which bound the new alpha2 s = y1 * y2 if s == -1: - l, h = max(0.0, a2 - a1), min(self._c, self._c + a2 - a1) + l, h = max(0.0, a2 - a1), min(self._c, self._c + a2 - a1) # noqa: E741 else: - l, h = max(0.0, a2 + a1 - self._c), min(self._c, a2 + a1) + l, h = max(0.0, a2 + a1 - self._c), min(self._c, a2 + a1) # noqa: E741 if l == h: return None, None @@ -320,7 +318,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): k22 = k(i2, i2) k12 = k(i1, i2) - # select the new alpha2 which could get the minimal objectives + # select the new alpha2 which could achieve the minimal objectives if (eta := k11 + k22 - 2.0 * k12) > 0.0: a2_new_unc = a2 + (y2 * (e1 - e2)) / eta # a2_new has a boundary @@ -335,7 +333,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): l1 = a1 + s * (a2 - l) h1 = a1 + s * (a2 - h) - # way 1 + # Method 1 f1 = y1 * (e1 + b) - a1 * k(i1, i1) - s * a2 * k(i1, i2) f2 = y2 * (e2 + b) - a2 * k(i2, i2) - s * a1 * k(i1, i2) ol = ( @@ -353,9 +351,8 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): + s * h * h1 * k(i1, i2) ) """ - # way 2 - Use objective function check which alpha2 new could get the minimal - objectives + Method 2: Use objective function to check which alpha2_new could achieve the + minimal objectives """ if ol < (oh - self._eps): a2_new = l @@ -375,7 +372,7 @@ def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): return a1_new, a2_new - # Normalise data using min_max way + # Normalize data using min-max method def _norm(self, data): if self._init: self._min = np.min(data, axis=0) @@ -423,9 +420,8 @@ def _rbf(self, v1, v2): return np.exp(-1 * (self.gamma * np.linalg.norm(v1 - v2) ** 2)) def _check(self): - if self._kernel == self._rbf: - if self.gamma < 0: - raise ValueError("gamma value must greater than 0") + if self._kernel == self._rbf and self.gamma < 0: + raise ValueError("gamma value must be non-negative") def _get_kernel(self, kernel_name): maps = {"linear": self._linear, "poly": self._polynomial, "rbf": self._rbf} @@ -445,26 +441,30 @@ def call_func(*args, **kwargs): start_time = time.time() func(*args, **kwargs) end_time = time.time() - print(f"smo algorithm cost {end_time - start_time} seconds") + print(f"SMO algorithm cost {end_time - start_time} seconds") return call_func @count_time -def test_cancel_data(): - print("Hello!\nStart test svm by smo algorithm!") +def test_cancer_data(): + print("Hello!\nStart test SVM using the SMO algorithm!") # 0: download dataset and load into pandas' dataframe - if not os.path.exists(r"cancel_data.csv"): - request = urllib.request.Request( + if not os.path.exists(r"cancer_data.csv"): + request = urllib.request.Request( # noqa: S310 CANCER_DATASET_URL, headers={"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"}, ) - response = urllib.request.urlopen(request) + response = urllib.request.urlopen(request) # noqa: S310 content = response.read().decode("utf-8") - with open(r"cancel_data.csv", "w") as f: + with open(r"cancer_data.csv", "w") as f: f.write(content) - data = pd.read_csv(r"cancel_data.csv", header=None) + data = pd.read_csv( + "cancer_data.csv", + header=None, + dtype={0: str}, # Assuming the first column contains string data + ) # 1: pre-processing data del data[data.columns.tolist()[0]] @@ -476,14 +476,14 @@ def test_cancel_data(): train_data, test_data = samples[:328, :], samples[328:, :] test_tags, test_samples = test_data[:, 0], test_data[:, 1:] - # 3: choose kernel function,and set initial alphas to zero(optional) - mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) + # 3: choose kernel function, and set initial alphas to zero (optional) + my_kernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) al = np.zeros(train_data.shape[0]) # 4: calculating best alphas using SMO algorithm and predict test_data samples mysvm = SmoSVM( train=train_data, - kernel_func=mykernel, + kernel_func=my_kernel, alpha_list=al, cost=0.4, b=0.0, @@ -498,30 +498,30 @@ def test_cancel_data(): for i in range(test_tags.shape[0]): if test_tags[i] == predict[i]: score += 1 - print(f"\nall: {test_num}\nright: {score}\nfalse: {test_num - score}") + print(f"\nAll: {test_num}\nCorrect: {score}\nIncorrect: {test_num - score}") print(f"Rough Accuracy: {score / test_tags.shape[0]}") def test_demonstration(): # change stdout - print("\nStart plot,please wait!!!") + print("\nStarting plot, please wait!") sys.stdout = open(os.devnull, "w") ax1 = plt.subplot2grid((2, 2), (0, 0)) ax2 = plt.subplot2grid((2, 2), (0, 1)) ax3 = plt.subplot2grid((2, 2), (1, 0)) ax4 = plt.subplot2grid((2, 2), (1, 1)) - ax1.set_title("linear svm,cost:0.1") + ax1.set_title("Linear SVM, cost = 0.1") test_linear_kernel(ax1, cost=0.1) - ax2.set_title("linear svm,cost:500") + ax2.set_title("Linear SVM, cost = 500") test_linear_kernel(ax2, cost=500) - ax3.set_title("rbf kernel svm,cost:0.1") + ax3.set_title("RBF kernel SVM, cost = 0.1") test_rbf_kernel(ax3, cost=0.1) - ax4.set_title("rbf kernel svm,cost:500") + ax4.set_title("RBF kernel SVM, cost = 500") test_rbf_kernel(ax4, cost=500) sys.stdout = sys.__stdout__ - print("Plot done!!!") + print("Plot done!") def test_linear_kernel(ax, cost): @@ -532,10 +532,10 @@ def test_linear_kernel(ax, cost): scaler = StandardScaler() train_x_scaled = scaler.fit_transform(train_x, train_y) train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled)) - mykernel = Kernel(kernel="linear", degree=5, coef0=1, gamma=0.5) + my_kernel = Kernel(kernel="linear", degree=5, coef0=1, gamma=0.5) mysvm = SmoSVM( train=train_data, - kernel_func=mykernel, + kernel_func=my_kernel, cost=cost, tolerance=0.001, auto_norm=False, @@ -552,10 +552,10 @@ def test_rbf_kernel(ax, cost): scaler = StandardScaler() train_x_scaled = scaler.fit_transform(train_x, train_y) train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled)) - mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) + my_kernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) mysvm = SmoSVM( train=train_data, - kernel_func=mykernel, + kernel_func=my_kernel, cost=cost, tolerance=0.001, auto_norm=False, @@ -568,11 +568,11 @@ def plot_partition_boundary( model, train_data, ax, resolution=100, colors=("b", "k", "r") ): """ - We can not get the optimum w of our kernel svm model which is different from linear - svm. For this reason, we generate randomly distributed points with high desity and - prediced values of these points are calculated by using our tained model. Then we - could use this prediced values to draw contour map. - And this contour map can represent svm's partition boundary. + We cannot get the optimal w of our kernel SVM model, which is different from a + linear SVM. For this reason, we generate randomly distributed points with high + density, and predicted values of these points are calculated using our trained + model. Then we could use this predicted values to draw contour map, and this contour + map represents the SVM's partition boundary. """ train_data_x = train_data[:, 1] train_data_y = train_data[:, 2] @@ -590,7 +590,7 @@ def plot_partition_boundary( ax.contour( xrange, yrange, - np.mat(grid).T, + np.asmatrix(grid).T, levels=(-1, 0, 1), linestyles=("--", "-", "--"), linewidths=(1, 1, 1), @@ -617,6 +617,6 @@ def plot_partition_boundary( if __name__ == "__main__": - test_cancel_data() + test_cancer_data() test_demonstration() plt.show() diff --git a/machine_learning/similarity_search.py b/machine_learning/similarity_search.py index 72979181f67c..c8a573796882 100644 --- a/machine_learning/similarity_search.py +++ b/machine_learning/similarity_search.py @@ -7,6 +7,7 @@ 1. the nearest vector 2. distance between the vector and the nearest vector (float) """ + from __future__ import annotations import math @@ -97,26 +98,29 @@ def similarity_search( """ if dataset.ndim != value_array.ndim: - raise ValueError( - f"Wrong input data's dimensions... dataset : {dataset.ndim}, " - f"value_array : {value_array.ndim}" + msg = ( + "Wrong input data's dimensions... " + f"dataset : {dataset.ndim}, value_array : {value_array.ndim}" ) + raise ValueError(msg) try: if dataset.shape[1] != value_array.shape[1]: - raise ValueError( - f"Wrong input data's shape... dataset : {dataset.shape[1]}, " - f"value_array : {value_array.shape[1]}" + msg = ( + "Wrong input data's shape... " + f"dataset : {dataset.shape[1]}, value_array : {value_array.shape[1]}" ) + raise ValueError(msg) except IndexError: if dataset.ndim != value_array.ndim: raise TypeError("Wrong shape") if dataset.dtype != value_array.dtype: - raise TypeError( - f"Input data have different datatype... dataset : {dataset.dtype}, " - f"value_array : {value_array.dtype}" + msg = ( + "Input data have different datatype... " + f"dataset : {dataset.dtype}, value_array : {value_array.dtype}" ) + raise TypeError(msg) answer = [] @@ -149,7 +153,7 @@ def cosine_similarity(input_a: np.ndarray, input_b: np.ndarray) -> float: >>> cosine_similarity(np.array([1, 2]), np.array([6, 32])) 0.9615239476408232 """ - return np.dot(input_a, input_b) / (norm(input_a) * norm(input_b)) + return float(np.dot(input_a, input_b) / (norm(input_a) * norm(input_b))) if __name__ == "__main__": diff --git a/machine_learning/support_vector_machines.py b/machine_learning/support_vector_machines.py index caec10175c50..d17c9044a3e9 100644 --- a/machine_learning/support_vector_machines.py +++ b/machine_learning/support_vector_machines.py @@ -14,11 +14,11 @@ def norm_squared(vector: ndarray) -> float: Returns: float: squared second norm of vector - >>> norm_squared([1, 2]) + >>> int(norm_squared([1, 2])) 5 - >>> norm_squared(np.asarray([1, 2])) + >>> int(norm_squared(np.asarray([1, 2]))) 5 - >>> norm_squared([0, 0]) + >>> int(norm_squared([0, 0])) 0 """ return np.dot(vector, vector) @@ -56,7 +56,7 @@ def __init__( *, regularization: float = np.inf, kernel: str = "linear", - gamma: float = 0, + gamma: float = 0.0, ) -> None: self.regularization = regularization self.gamma = gamma @@ -65,7 +65,7 @@ def __init__( elif kernel == "rbf": if self.gamma == 0: raise ValueError("rbf kernel requires gamma") - if not (isinstance(self.gamma, float) or isinstance(self.gamma, int)): + if not isinstance(self.gamma, (float, int)): raise ValueError("gamma must be float or int") if not self.gamma > 0: raise ValueError("gamma must be > 0") @@ -74,7 +74,8 @@ def __init__( # sklear: def_gamma = 1/(n_features * X.var()) (wiki) # previously it was 1/(n_features) else: - raise ValueError(f"Unknown kernel: {kernel}") + msg = f"Unknown kernel: {kernel}" + raise ValueError(msg) # kernels def __linear(self, vector1: ndarray, vector2: ndarray) -> float: diff --git a/machine_learning/xgboost_regressor.py b/machine_learning/xgboost_regressor.py index 023984fc1f59..52e041c55ea2 100644 --- a/machine_learning/xgboost_regressor.py +++ b/machine_learning/xgboost_regressor.py @@ -27,7 +27,9 @@ def xgboost( ... 1.14300000e+03, 2.60958904e+00, 3.67800000e+01, -1.19780000e+02]])) array([[1.1139996]], dtype=float32) """ - xgb = XGBRegressor(verbosity=0, random_state=42) + xgb = XGBRegressor( + verbosity=0, random_state=42, tree_method="exact", base_score=0.5 + ) xgb.fit(features, target) # Predict target for test data predictions = xgb.predict(test_features) @@ -37,13 +39,13 @@ def xgboost( def main() -> None: """ - >>> main() - Mean Absolute Error : 0.30957163379906033 - Mean Square Error : 0.22611560196662744 - The URL for this algorithm https://xgboost.readthedocs.io/en/stable/ California house price dataset is used to demonstrate the algorithm. + + Expected error values: + Mean Absolute Error: 0.30957163379906033 + Mean Square Error: 0.22611560196662744 """ # Load California house price dataset california = fetch_california_housing() @@ -53,8 +55,8 @@ def main() -> None: ) predictions = xgboost(x_train, y_train, x_test) # Error printing - print(f"Mean Absolute Error : {mean_absolute_error(y_test, predictions)}") - print(f"Mean Square Error : {mean_squared_error(y_test, predictions)}") + print(f"Mean Absolute Error: {mean_absolute_error(y_test, predictions)}") + print(f"Mean Square Error: {mean_squared_error(y_test, predictions)}") if __name__ == "__main__": diff --git a/maths/3n_plus_1.py b/maths/3n_plus_1.py deleted file mode 100644 index 59fdec48e100..000000000000 --- a/maths/3n_plus_1.py +++ /dev/null @@ -1,149 +0,0 @@ -from __future__ import annotations - - -def n31(a: int) -> tuple[list[int], int]: - """ - Returns the Collatz sequence and its length of any positive integer. - >>> n31(4) - ([4, 2, 1], 3) - """ - - if not isinstance(a, int): - raise TypeError(f"Must be int, not {type(a).__name__}") - if a < 1: - raise ValueError(f"Given integer must be positive, not {a}") - - path = [a] - while a != 1: - if a % 2 == 0: - a //= 2 - else: - a = 3 * a + 1 - path.append(a) - return path, len(path) - - -def test_n31(): - """ - >>> test_n31() - """ - assert n31(4) == ([4, 2, 1], 3) - assert n31(11) == ([11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1], 15) - assert n31(31) == ( - [ - 31, - 94, - 47, - 142, - 71, - 214, - 107, - 322, - 161, - 484, - 242, - 121, - 364, - 182, - 91, - 274, - 137, - 412, - 206, - 103, - 310, - 155, - 466, - 233, - 700, - 350, - 175, - 526, - 263, - 790, - 395, - 1186, - 593, - 1780, - 890, - 445, - 1336, - 668, - 334, - 167, - 502, - 251, - 754, - 377, - 1132, - 566, - 283, - 850, - 425, - 1276, - 638, - 319, - 958, - 479, - 1438, - 719, - 2158, - 1079, - 3238, - 1619, - 4858, - 2429, - 7288, - 3644, - 1822, - 911, - 2734, - 1367, - 4102, - 2051, - 6154, - 3077, - 9232, - 4616, - 2308, - 1154, - 577, - 1732, - 866, - 433, - 1300, - 650, - 325, - 976, - 488, - 244, - 122, - 61, - 184, - 92, - 46, - 23, - 70, - 35, - 106, - 53, - 160, - 80, - 40, - 20, - 10, - 5, - 16, - 8, - 4, - 2, - 1, - ], - 107, - ) - - -if __name__ == "__main__": - num = 4 - path, length = n31(num) - print(f"The Collatz sequence of {num} took {length} steps. \nPath: {path}") diff --git a/maths/abs.py b/maths/abs.py index cb0ffc8a5b61..b357e98d8680 100644 --- a/maths/abs.py +++ b/maths/abs.py @@ -75,9 +75,9 @@ def test_abs_val(): """ >>> test_abs_val() """ - assert 0 == abs_val(0) - assert 34 == abs_val(34) - assert 100000000000 == abs_val(-100000000000) + assert abs_val(0) == 0 + assert abs_val(34) == 34 + assert abs_val(-100000000000) == 100000000000 a = [-3, -1, 2, -11] assert abs_max(a) == -11 diff --git a/maths/add.py b/maths/add.py deleted file mode 100644 index c89252c645ea..000000000000 --- a/maths/add.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Just to check -""" - - -def add(a: float, b: float) -> float: - """ - >>> add(2, 2) - 4 - >>> add(2, -2) - 0 - """ - return a + b - - -if __name__ == "__main__": - a = 5 - b = 6 - print(f"The sum of {a} + {b} is {add(a, b)}") diff --git a/maths/allocation_number.py b/maths/allocation_number.py index d419e74d01ff..52f1ac4bdb23 100644 --- a/maths/allocation_number.py +++ b/maths/allocation_number.py @@ -5,6 +5,7 @@ for i in allocation_list: requests.get(url,headers={'Range':f'bytes={i}'}) """ + from __future__ import annotations diff --git a/maths/arc_length.py b/maths/arc_length.py index 9e87ca38cc7d..4c518f321dc7 100644 --- a/maths/arc_length.py +++ b/maths/arc_length.py @@ -7,6 +7,8 @@ def arc_length(angle: int, radius: int) -> float: 3.9269908169872414 >>> arc_length(120, 15) 31.415926535897928 + >>> arc_length(90, 10) + 15.707963267948966 """ return 2 * pi * radius * (angle / 360) diff --git a/maths/area.py b/maths/area.py index ea7216c8fe3f..31a654206977 100644 --- a/maths/area.py +++ b/maths/area.py @@ -2,6 +2,7 @@ Find the area of various geometric shapes Wikipedia reference: https://en.wikipedia.org/wiki/Area """ + from math import pi, sqrt, tan diff --git a/maths/area_under_curve.py b/maths/area_under_curve.py index b557b2029657..10aec768fa09 100644 --- a/maths/area_under_curve.py +++ b/maths/area_under_curve.py @@ -1,15 +1,16 @@ """ Approximates the area under the curve using the trapezoidal rule """ + from __future__ import annotations from collections.abc import Callable def trapezoidal_area( - fnc: Callable[[int | float], int | float], - x_start: int | float, - x_end: int | float, + fnc: Callable[[float], float], + x_start: float, + x_end: float, steps: int = 100, ) -> float: """ diff --git a/maths/average_median.py b/maths/average_median.py index cd1ec1574893..f24e525736b3 100644 --- a/maths/average_median.py +++ b/maths/average_median.py @@ -19,7 +19,9 @@ def median(nums: list) -> int | float: Returns: Median. """ - sorted_list = sorted(nums) + # The sorted function returns list[SupportsRichComparisonT@sorted] + # which does not support `+` + sorted_list: list[int] = sorted(nums) length = len(sorted_list) mid_index = length >> 1 return ( diff --git a/maths/base_neg2_conversion.py b/maths/base_neg2_conversion.py new file mode 100644 index 000000000000..81d40d37e79d --- /dev/null +++ b/maths/base_neg2_conversion.py @@ -0,0 +1,37 @@ +def decimal_to_negative_base_2(num: int) -> int: + """ + This function returns the number negative base 2 + of the decimal number of the input data. + + Args: + int: The decimal number to convert. + + Returns: + int: The negative base 2 number. + + Examples: + >>> decimal_to_negative_base_2(0) + 0 + >>> decimal_to_negative_base_2(-19) + 111101 + >>> decimal_to_negative_base_2(4) + 100 + >>> decimal_to_negative_base_2(7) + 11011 + """ + if num == 0: + return 0 + ans = "" + while num != 0: + num, rem = divmod(num, -2) + if rem < 0: + rem += 2 + num += 1 + ans = str(rem) + ans + return int(ans) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/basic_maths.py b/maths/basic_maths.py index 26c52c54983e..833f31c18b9e 100644 --- a/maths/basic_maths.py +++ b/maths/basic_maths.py @@ -1,4 +1,5 @@ """Implementation of Basic Math in Python.""" + import math @@ -98,7 +99,17 @@ def euler_phi(n: int) -> int: """Calculate Euler's Phi Function. >>> euler_phi(100) 40 + >>> euler_phi(0) + Traceback (most recent call last): + ... + ValueError: Only positive numbers are accepted + >>> euler_phi(-10) + Traceback (most recent call last): + ... + ValueError: Only positive numbers are accepted """ + if n <= 0: + raise ValueError("Only positive numbers are accepted") s = n for x in set(prime_factors(n)): s *= (x - 1) / x diff --git a/maths/binary_exp_mod.py b/maths/binary_exp_mod.py deleted file mode 100644 index 67dd1e728b18..000000000000 --- a/maths/binary_exp_mod.py +++ /dev/null @@ -1,28 +0,0 @@ -def bin_exp_mod(a, n, b): - """ - >>> bin_exp_mod(3, 4, 5) - 1 - >>> bin_exp_mod(7, 13, 10) - 7 - """ - # mod b - assert not (b == 0), "This cannot accept modulo that is == 0" - if n == 0: - return 1 - - if n % 2 == 1: - return (bin_exp_mod(a, n - 1, b) * a) % b - - r = bin_exp_mod(a, n / 2, b) - return (r * r) % b - - -if __name__ == "__main__": - try: - BASE = int(input("Enter Base : ").strip()) - POWER = int(input("Enter Power : ").strip()) - MODULO = int(input("Enter Modulo : ").strip()) - except ValueError: - print("Invalid literal for integer") - - print(bin_exp_mod(BASE, POWER, MODULO)) diff --git a/maths/binary_exponentiation.py b/maths/binary_exponentiation.py index 147b4285ffa1..51ce86d26c41 100644 --- a/maths/binary_exponentiation.py +++ b/maths/binary_exponentiation.py @@ -1,27 +1,196 @@ -"""Binary Exponentiation.""" +""" +Binary Exponentiation -# Author : Junth Basnet -# Time Complexity : O(logn) +This is a method to find a^b in O(log b) time complexity and is one of the most commonly +used methods of exponentiation. The method is also useful for modular exponentiation, +when the solution to (a^b) % c is required. +To calculate a^b: +- If b is even, then a^b = (a * a)^(b / 2) +- If b is odd, then a^b = a * a^(b - 1) +Repeat until b = 1 or b = 0 -def binary_exponentiation(a, n): - if n == 0: +For modular exponentiation, we use the fact that (a * b) % c = ((a % c) * (b % c)) % c +""" + + +def binary_exp_recursive(base: float, exponent: int) -> float: + """ + Computes a^b recursively, where a is the base and b is the exponent + + >>> binary_exp_recursive(3, 5) + 243 + >>> binary_exp_recursive(11, 13) + 34522712143931 + >>> binary_exp_recursive(-1, 3) + -1 + >>> binary_exp_recursive(0, 5) + 0 + >>> binary_exp_recursive(3, 1) + 3 + >>> binary_exp_recursive(3, 0) + 1 + >>> binary_exp_recursive(1.5, 4) + 5.0625 + >>> binary_exp_recursive(3, -1) + Traceback (most recent call last): + ... + ValueError: Exponent must be a non-negative integer + """ + if exponent < 0: + raise ValueError("Exponent must be a non-negative integer") + + if exponent == 0: return 1 - elif n % 2 == 1: - return binary_exponentiation(a, n - 1) * a + if exponent % 2 == 1: + return binary_exp_recursive(base, exponent - 1) * base + + b = binary_exp_recursive(base, exponent // 2) + return b * b + + +def binary_exp_iterative(base: float, exponent: int) -> float: + """ + Computes a^b iteratively, where a is the base and b is the exponent - else: - b = binary_exponentiation(a, n / 2) - return b * b + >>> binary_exp_iterative(3, 5) + 243 + >>> binary_exp_iterative(11, 13) + 34522712143931 + >>> binary_exp_iterative(-1, 3) + -1 + >>> binary_exp_iterative(0, 5) + 0 + >>> binary_exp_iterative(3, 1) + 3 + >>> binary_exp_iterative(3, 0) + 1 + >>> binary_exp_iterative(1.5, 4) + 5.0625 + >>> binary_exp_iterative(3, -1) + Traceback (most recent call last): + ... + ValueError: Exponent must be a non-negative integer + """ + if exponent < 0: + raise ValueError("Exponent must be a non-negative integer") + + res: int | float = 1 + while exponent > 0: + if exponent & 1: + res *= base + + base *= base + exponent >>= 1 + + return res + + +def binary_exp_mod_recursive(base: float, exponent: int, modulus: int) -> float: + """ + Computes a^b % c recursively, where a is the base, b is the exponent, and c is the + modulus + + >>> binary_exp_mod_recursive(3, 4, 5) + 1 + >>> binary_exp_mod_recursive(11, 13, 7) + 4 + >>> binary_exp_mod_recursive(1.5, 4, 3) + 2.0625 + >>> binary_exp_mod_recursive(7, -1, 10) + Traceback (most recent call last): + ... + ValueError: Exponent must be a non-negative integer + >>> binary_exp_mod_recursive(7, 13, 0) + Traceback (most recent call last): + ... + ValueError: Modulus must be a positive integer + """ + if exponent < 0: + raise ValueError("Exponent must be a non-negative integer") + if modulus <= 0: + raise ValueError("Modulus must be a positive integer") + + if exponent == 0: + return 1 + + if exponent % 2 == 1: + return (binary_exp_mod_recursive(base, exponent - 1, modulus) * base) % modulus + + r = binary_exp_mod_recursive(base, exponent // 2, modulus) + return (r * r) % modulus + + +def binary_exp_mod_iterative(base: float, exponent: int, modulus: int) -> float: + """ + Computes a^b % c iteratively, where a is the base, b is the exponent, and c is the + modulus + + >>> binary_exp_mod_iterative(3, 4, 5) + 1 + >>> binary_exp_mod_iterative(11, 13, 7) + 4 + >>> binary_exp_mod_iterative(1.5, 4, 3) + 2.0625 + >>> binary_exp_mod_iterative(7, -1, 10) + Traceback (most recent call last): + ... + ValueError: Exponent must be a non-negative integer + >>> binary_exp_mod_iterative(7, 13, 0) + Traceback (most recent call last): + ... + ValueError: Modulus must be a positive integer + """ + if exponent < 0: + raise ValueError("Exponent must be a non-negative integer") + if modulus <= 0: + raise ValueError("Modulus must be a positive integer") + + res: int | float = 1 + while exponent > 0: + if exponent & 1: + res = ((res % modulus) * (base % modulus)) % modulus + + base *= base + exponent >>= 1 + + return res if __name__ == "__main__": - try: - BASE = int(input("Enter Base : ").strip()) - POWER = int(input("Enter Power : ").strip()) - except ValueError: - print("Invalid literal for integer") - - RESULT = binary_exponentiation(BASE, POWER) - print(f"{BASE}^({POWER}) : {RESULT}") + from timeit import timeit + + a = 1269380576 + b = 374 + c = 34 + + runs = 100_000 + print( + timeit( + f"binary_exp_recursive({a}, {b})", + setup="from __main__ import binary_exp_recursive", + number=runs, + ) + ) + print( + timeit( + f"binary_exp_iterative({a}, {b})", + setup="from __main__ import binary_exp_iterative", + number=runs, + ) + ) + print( + timeit( + f"binary_exp_mod_recursive({a}, {b}, {c})", + setup="from __main__ import binary_exp_mod_recursive", + number=runs, + ) + ) + print( + timeit( + f"binary_exp_mod_iterative({a}, {b}, {c})", + setup="from __main__ import binary_exp_mod_iterative", + number=runs, + ) + ) diff --git a/maths/binary_exponentiation_2.py b/maths/binary_exponentiation_2.py deleted file mode 100644 index 51ec4baf2598..000000000000 --- a/maths/binary_exponentiation_2.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -* Binary Exponentiation with Multiplication -* This is a method to find a*b in a time complexity of O(log b) -* This is one of the most commonly used methods of finding result of multiplication. -* Also useful in cases where solution to (a*b)%c is required, -* where a,b,c can be numbers over the computers calculation limits. -* Done using iteration, can also be done using recursion - -* @author chinmoy159 -* @version 1.0 dated 10/08/2017 -""" - - -def b_expo(a, b): - res = 0 - while b > 0: - if b & 1: - res += a - - a += a - b >>= 1 - - return res - - -def b_expo_mod(a, b, c): - res = 0 - while b > 0: - if b & 1: - res = ((res % c) + (a % c)) % c - - a += a - b >>= 1 - - return res - - -""" -* Wondering how this method works ! -* It's pretty simple. -* Let's say you need to calculate a ^ b -* RULE 1 : a * b = (a+a) * (b/2) ---- example : 4 * 4 = (4+4) * (4/2) = 8 * 2 -* RULE 2 : IF b is ODD, then ---- a * b = a + (a * (b - 1)) :: where (b - 1) is even. -* Once b is even, repeat the process to get a * b -* Repeat the process till b = 1 OR b = 0, because a*1 = a AND a*0 = 0 -* -* As far as the modulo is concerned, -* the fact : (a+b) % c = ((a%c) + (b%c)) % c -* Now apply RULE 1 OR 2, whichever is required. -""" diff --git a/maths/binary_exponentiation_3.py b/maths/binary_exponentiation_3.py deleted file mode 100644 index dd4e70e74129..000000000000 --- a/maths/binary_exponentiation_3.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -* Binary Exponentiation for Powers -* This is a method to find a^b in a time complexity of O(log b) -* This is one of the most commonly used methods of finding powers. -* Also useful in cases where solution to (a^b)%c is required, -* where a,b,c can be numbers over the computers calculation limits. -* Done using iteration, can also be done using recursion - -* @author chinmoy159 -* @version 1.0 dated 10/08/2017 -""" - - -def b_expo(a, b): - res = 1 - while b > 0: - if b & 1: - res *= a - - a *= a - b >>= 1 - - return res - - -def b_expo_mod(a, b, c): - res = 1 - while b > 0: - if b & 1: - res = ((res % c) * (a % c)) % c - - a *= a - b >>= 1 - - return res - - -""" -* Wondering how this method works ! -* It's pretty simple. -* Let's say you need to calculate a ^ b -* RULE 1 : a ^ b = (a*a) ^ (b/2) ---- example : 4 ^ 4 = (4*4) ^ (4/2) = 16 ^ 2 -* RULE 2 : IF b is ODD, then ---- a ^ b = a * (a ^ (b - 1)) :: where (b - 1) is even. -* Once b is even, repeat the process to get a ^ b -* Repeat the process till b = 1 OR b = 0, because a^1 = a AND a^0 = 1 -* -* As far as the modulo is concerned, -* the fact : (a*b) % c = ((a%c) * (b%c)) % c -* Now apply RULE 1 OR 2 whichever is required. -""" diff --git a/maths/binary_multiplication.py b/maths/binary_multiplication.py new file mode 100644 index 000000000000..0cc5a575f445 --- /dev/null +++ b/maths/binary_multiplication.py @@ -0,0 +1,101 @@ +""" +Binary Multiplication +This is a method to find a*b in a time complexity of O(log b) +This is one of the most commonly used methods of finding result of multiplication. +Also useful in cases where solution to (a*b)%c is required, +where a,b,c can be numbers over the computers calculation limits. +Done using iteration, can also be done using recursion + +Let's say you need to calculate a * b +RULE 1 : a * b = (a+a) * (b/2) ---- example : 4 * 4 = (4+4) * (4/2) = 8 * 2 +RULE 2 : IF b is odd, then ---- a * b = a + (a * (b - 1)), where (b - 1) is even. +Once b is even, repeat the process to get a * b +Repeat the process until b = 1 or b = 0, because a*1 = a and a*0 = 0 + +As far as the modulo is concerned, +the fact : (a+b) % c = ((a%c) + (b%c)) % c +Now apply RULE 1 or 2, whichever is required. + +@author chinmoy159 +""" + + +def binary_multiply(a: int, b: int) -> int: + """ + Multiply 'a' and 'b' using bitwise multiplication. + + Parameters: + a (int): The first number. + b (int): The second number. + + Returns: + int: a * b + + Examples: + >>> binary_multiply(2, 3) + 6 + >>> binary_multiply(5, 0) + 0 + >>> binary_multiply(3, 4) + 12 + >>> binary_multiply(10, 5) + 50 + >>> binary_multiply(0, 5) + 0 + >>> binary_multiply(2, 1) + 2 + >>> binary_multiply(1, 10) + 10 + """ + res = 0 + while b > 0: + if b & 1: + res += a + + a += a + b >>= 1 + + return res + + +def binary_mod_multiply(a: int, b: int, modulus: int) -> int: + """ + Calculate (a * b) % c using binary multiplication and modular arithmetic. + + Parameters: + a (int): The first number. + b (int): The second number. + modulus (int): The modulus. + + Returns: + int: (a * b) % modulus. + + Examples: + >>> binary_mod_multiply(2, 3, 5) + 1 + >>> binary_mod_multiply(5, 0, 7) + 0 + >>> binary_mod_multiply(3, 4, 6) + 0 + >>> binary_mod_multiply(10, 5, 13) + 11 + >>> binary_mod_multiply(2, 1, 5) + 2 + >>> binary_mod_multiply(1, 10, 3) + 1 + """ + res = 0 + while b > 0: + if b & 1: + res = ((res % modulus) + (a % modulus)) % modulus + + a += a + b >>= 1 + + return res + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/binomial_coefficient.py b/maths/binomial_coefficient.py index 0d4b3d1a8d9a..24c54326e305 100644 --- a/maths/binomial_coefficient.py +++ b/maths/binomial_coefficient.py @@ -1,10 +1,48 @@ -def binomial_coefficient(n, r): +def binomial_coefficient(n: int, r: int) -> int: """ - Find binomial coefficient using pascals triangle. + Find binomial coefficient using Pascal's triangle. + + Calculate C(n, r) using Pascal's triangle. + + :param n: The total number of items. + :param r: The number of items to choose. + :return: The binomial coefficient C(n, r). >>> binomial_coefficient(10, 5) 252 + >>> binomial_coefficient(10, 0) + 1 + >>> binomial_coefficient(0, 10) + 1 + >>> binomial_coefficient(10, 10) + 1 + >>> binomial_coefficient(5, 2) + 10 + >>> binomial_coefficient(5, 6) + 0 + >>> binomial_coefficient(3, 5) + 0 + >>> binomial_coefficient(-2, 3) + Traceback (most recent call last): + ... + ValueError: n and r must be non-negative integers + >>> binomial_coefficient(5, -1) + Traceback (most recent call last): + ... + ValueError: n and r must be non-negative integers + >>> binomial_coefficient(10.1, 5) + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + >>> binomial_coefficient(10, 5.1) + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer """ + if n < 0 or r < 0: + raise ValueError("n and r must be non-negative integers") + if 0 in (n, r): + return 1 c = [0 for i in range(r + 1)] # nc0 = 1 c[0] = 1 @@ -17,4 +55,8 @@ def binomial_coefficient(n, r): return c[r] -print(binomial_coefficient(n=10, r=5)) +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(binomial_coefficient(n=10, r=5)) diff --git a/maths/binomial_distribution.py b/maths/binomial_distribution.py index 5b56f2d59244..eabcaea0d1b2 100644 --- a/maths/binomial_distribution.py +++ b/maths/binomial_distribution.py @@ -1,5 +1,6 @@ """For more information about the Binomial Distribution - - https://en.wikipedia.org/wiki/Binomial_distribution""" +https://en.wikipedia.org/wiki/Binomial_distribution""" + from math import factorial diff --git a/maths/carmichael_number.py b/maths/carmichael_number.py deleted file mode 100644 index c9c144759246..000000000000 --- a/maths/carmichael_number.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -== Carmichael Numbers == -A number n is said to be a Carmichael number if it -satisfies the following modular arithmetic condition: - - power(b, n-1) MOD n = 1, - for all b ranging from 1 to n such that b and - n are relatively prime, i.e, gcd(b, n) = 1 - -Examples of Carmichael Numbers: 561, 1105, ... -https://en.wikipedia.org/wiki/Carmichael_number -""" - - -def gcd(a: int, b: int) -> int: - if a < b: - return gcd(b, a) - if a % b == 0: - return b - return gcd(b, a % b) - - -def power(x: int, y: int, mod: int) -> int: - if y == 0: - return 1 - temp = power(x, y // 2, mod) % mod - temp = (temp * temp) % mod - if y % 2 == 1: - temp = (temp * x) % mod - return temp - - -def is_carmichael_number(n: int) -> bool: - b = 2 - while b < n: - if gcd(b, n) == 1 and power(b, n - 1, n) != 1: - return False - b += 1 - return True - - -if __name__ == "__main__": - number = int(input("Enter number: ").strip()) - if is_carmichael_number(number): - print(f"{number} is a Carmichael Number.") - else: - print(f"{number} is not a Carmichael Number.") diff --git a/maths/chebyshev_distance.py b/maths/chebyshev_distance.py new file mode 100644 index 000000000000..4801d391621f --- /dev/null +++ b/maths/chebyshev_distance.py @@ -0,0 +1,20 @@ +def chebyshev_distance(point_a: list[float], point_b: list[float]) -> float: + """ + This function calculates the Chebyshev distance (also known as the + Chessboard distance) between two n-dimensional points represented as lists. + + https://en.wikipedia.org/wiki/Chebyshev_distance + + >>> chebyshev_distance([1.0, 1.0], [2.0, 2.0]) + 1.0 + >>> chebyshev_distance([1.0, 1.0, 9.0], [2.0, 2.0, -5.2]) + 14.2 + >>> chebyshev_distance([1.0], [2.0, 2.0]) + Traceback (most recent call last): + ... + ValueError: Both points must have the same dimension. + """ + if len(point_a) != len(point_b): + raise ValueError("Both points must have the same dimension.") + + return max(abs(a - b) for a, b in zip(point_a, point_b)) diff --git a/blockchain/chinese_remainder_theorem.py b/maths/chinese_remainder_theorem.py similarity index 99% rename from blockchain/chinese_remainder_theorem.py rename to maths/chinese_remainder_theorem.py index d3e75e77922a..18af63d106e8 100644 --- a/blockchain/chinese_remainder_theorem.py +++ b/maths/chinese_remainder_theorem.py @@ -11,6 +11,7 @@ 1. Use extended euclid algorithm to find x,y such that a*x + b*y = 1 2. Take n = ra*by + rb*ax """ + from __future__ import annotations diff --git a/maths/chudnovsky_algorithm.py b/maths/chudnovsky_algorithm.py index aaee7462822e..d122bf0756f7 100644 --- a/maths/chudnovsky_algorithm.py +++ b/maths/chudnovsky_algorithm.py @@ -5,7 +5,7 @@ def pi(precision: int) -> str: """ The Chudnovsky algorithm is a fast method for calculating the digits of PI, - based on Ramanujan’s PI formulae. + based on Ramanujan's PI formulae. https://en.wikipedia.org/wiki/Chudnovsky_algorithm diff --git a/maths/collatz_sequence.py b/maths/collatz_sequence.py index 7b3636de69f4..b00dca8d70b7 100644 --- a/maths/collatz_sequence.py +++ b/maths/collatz_sequence.py @@ -1,43 +1,66 @@ +""" +The Collatz conjecture is a famous unsolved problem in mathematics. Given a starting +positive integer, define the following sequence: +- If the current term n is even, then the next term is n/2. +- If the current term n is odd, then the next term is 3n + 1. +The conjecture claims that this sequence will always reach 1 for any starting number. + +Other names for this problem include the 3n + 1 problem, the Ulam conjecture, Kakutani's +problem, the Thwaites conjecture, Hasse's algorithm, the Syracuse problem, and the +hailstone sequence. + +Reference: https://en.wikipedia.org/wiki/Collatz_conjecture +""" + from __future__ import annotations +from collections.abc import Generator -def collatz_sequence(n: int) -> list[int]: + +def collatz_sequence(n: int) -> Generator[int]: """ - Collatz conjecture: start with any positive integer n. The next term is - obtained as follows: - If n term is even, the next term is: n / 2 . - If n is odd, the next term is: 3 * n + 1. - - The conjecture states the sequence will always reach 1 for any starting value n. - Example: - >>> collatz_sequence(2.1) + Generate the Collatz sequence starting at n. + >>> tuple(collatz_sequence(2.1)) Traceback (most recent call last): ... - Exception: Sequence only defined for natural numbers - >>> collatz_sequence(0) + Exception: Sequence only defined for positive integers + >>> tuple(collatz_sequence(0)) Traceback (most recent call last): ... - Exception: Sequence only defined for natural numbers - >>> collatz_sequence(43) # doctest: +NORMALIZE_WHITESPACE - [43, 130, 65, 196, 98, 49, 148, 74, 37, 112, 56, 28, 14, 7, - 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1] + Exception: Sequence only defined for positive integers + >>> tuple(collatz_sequence(4)) + (4, 2, 1) + >>> tuple(collatz_sequence(11)) + (11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1) + >>> tuple(collatz_sequence(31)) # doctest: +NORMALIZE_WHITESPACE + (31, 94, 47, 142, 71, 214, 107, 322, 161, 484, 242, 121, 364, 182, 91, 274, 137, + 412, 206, 103, 310, 155, 466, 233, 700, 350, 175, 526, 263, 790, 395, 1186, 593, + 1780, 890, 445, 1336, 668, 334, 167, 502, 251, 754, 377, 1132, 566, 283, 850, 425, + 1276, 638, 319, 958, 479, 1438, 719, 2158, 1079, 3238, 1619, 4858, 2429, 7288, 3644, + 1822, 911, 2734, 1367, 4102, 2051, 6154, 3077, 9232, 4616, 2308, 1154, 577, 1732, + 866, 433, 1300, 650, 325, 976, 488, 244, 122, 61, 184, 92, 46, 23, 70, 35, 106, 53, + 160, 80, 40, 20, 10, 5, 16, 8, 4, 2, 1) + >>> tuple(collatz_sequence(43)) # doctest: +NORMALIZE_WHITESPACE + (43, 130, 65, 196, 98, 49, 148, 74, 37, 112, 56, 28, 14, 7, 22, 11, 34, 17, 52, 26, + 13, 40, 20, 10, 5, 16, 8, 4, 2, 1) """ - if not isinstance(n, int) or n < 1: - raise Exception("Sequence only defined for natural numbers") + raise Exception("Sequence only defined for positive integers") - sequence = [n] + yield n while n != 1: - n = 3 * n + 1 if n & 1 else n // 2 - sequence.append(n) - return sequence + if n % 2 == 0: + n //= 2 + else: + n = 3 * n + 1 + yield n def main(): - n = 43 - sequence = collatz_sequence(n) + n = int(input("Your number: ")) + sequence = tuple(collatz_sequence(n)) print(sequence) - print(f"collatz sequence from {n} took {len(sequence)} steps.") + print(f"Collatz sequence from {n} took {len(sequence)} steps.") if __name__ == "__main__": diff --git a/maths/combinations.py b/maths/combinations.py index a2324012c01f..6e9e1a807067 100644 --- a/maths/combinations.py +++ b/maths/combinations.py @@ -1,7 +1,6 @@ """ https://en.wikipedia.org/wiki/Combination """ -from math import factorial def combinations(n: int, k: int) -> int: @@ -35,7 +34,11 @@ def combinations(n: int, k: int) -> int: # to calculate a factorial of a negative number, which is not possible if n < k or k < 0: raise ValueError("Please enter positive integers for n and k where n >= k") - return factorial(n) // (factorial(k) * factorial(n - k)) + res = 1 + for i in range(k): + res *= n - i + res //= i + 1 + return res if __name__ == "__main__": diff --git a/maths/continued_fraction.py b/maths/continued_fraction.py new file mode 100644 index 000000000000..2c38bf88b1e9 --- /dev/null +++ b/maths/continued_fraction.py @@ -0,0 +1,57 @@ +""" +Finding the continuous fraction for a rational number using python + +https://en.wikipedia.org/wiki/Continued_fraction +""" + +from fractions import Fraction +from math import floor + + +def continued_fraction(num: Fraction) -> list[int]: + """ + :param num: + Fraction of the number whose continued fractions to be found. + Use Fraction(str(number)) for more accurate results due to + float inaccuracies. + + :return: + The continued fraction of rational number. + It is the all commas in the (n + 1)-tuple notation. + + >>> continued_fraction(Fraction(2)) + [2] + >>> continued_fraction(Fraction("3.245")) + [3, 4, 12, 4] + >>> continued_fraction(Fraction("2.25")) + [2, 4] + >>> continued_fraction(1/Fraction("2.25")) + [0, 2, 4] + >>> continued_fraction(Fraction("415/93")) + [4, 2, 6, 7] + >>> continued_fraction(Fraction(0)) + [0] + >>> continued_fraction(Fraction(0.75)) + [0, 1, 3] + >>> continued_fraction(Fraction("-2.25")) # -2.25 = -3 + 0.75 + [-3, 1, 3] + """ + numerator, denominator = num.as_integer_ratio() + continued_fraction_list: list[int] = [] + while True: + integer_part = floor(numerator / denominator) + continued_fraction_list.append(integer_part) + numerator -= integer_part * denominator + if numerator == 0: + break + numerator, denominator = denominator, numerator + + return continued_fraction_list + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("Continued Fraction of 0.84375 is: ", continued_fraction(Fraction("0.84375"))) diff --git a/maths/decimal_to_fraction.py b/maths/decimal_to_fraction.py index 9462bafe0171..2aa8e3c3dfd6 100644 --- a/maths/decimal_to_fraction.py +++ b/maths/decimal_to_fraction.py @@ -1,4 +1,4 @@ -def decimal_to_fraction(decimal: int | float | str) -> tuple[int, int]: +def decimal_to_fraction(decimal: float | str) -> tuple[int, int]: """ Return a decimal number in its simplest fraction form >>> decimal_to_fraction(2) diff --git a/maths/double_factorial.py b/maths/double_factorial.py new file mode 100644 index 000000000000..3c3a28304e95 --- /dev/null +++ b/maths/double_factorial.py @@ -0,0 +1,60 @@ +def double_factorial_recursive(n: int) -> int: + """ + Compute double factorial using recursive method. + Recursion can be costly for large numbers. + + To learn about the theory behind this algorithm: + https://en.wikipedia.org/wiki/Double_factorial + + >>> from math import prod + >>> all(double_factorial_recursive(i) == prod(range(i, 0, -2)) for i in range(20)) + True + >>> double_factorial_recursive(0.1) + Traceback (most recent call last): + ... + ValueError: double_factorial_recursive() only accepts integral values + >>> double_factorial_recursive(-1) + Traceback (most recent call last): + ... + ValueError: double_factorial_recursive() not defined for negative values + """ + if not isinstance(n, int): + raise ValueError("double_factorial_recursive() only accepts integral values") + if n < 0: + raise ValueError("double_factorial_recursive() not defined for negative values") + return 1 if n <= 1 else n * double_factorial_recursive(n - 2) + + +def double_factorial_iterative(num: int) -> int: + """ + Compute double factorial using iterative method. + + To learn about the theory behind this algorithm: + https://en.wikipedia.org/wiki/Double_factorial + + >>> from math import prod + >>> all(double_factorial_iterative(i) == prod(range(i, 0, -2)) for i in range(20)) + True + >>> double_factorial_iterative(0.1) + Traceback (most recent call last): + ... + ValueError: double_factorial_iterative() only accepts integral values + >>> double_factorial_iterative(-1) + Traceback (most recent call last): + ... + ValueError: double_factorial_iterative() not defined for negative values + """ + if not isinstance(num, int): + raise ValueError("double_factorial_iterative() only accepts integral values") + if num < 0: + raise ValueError("double_factorial_iterative() not defined for negative values") + value = 1 + for i in range(num, 0, -2): + value *= i + return value + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/double_factorial_iterative.py b/maths/double_factorial_iterative.py deleted file mode 100644 index b2b58aa04c28..000000000000 --- a/maths/double_factorial_iterative.py +++ /dev/null @@ -1,33 +0,0 @@ -def double_factorial(num: int) -> int: - """ - Compute double factorial using iterative method. - - To learn about the theory behind this algorithm: - https://en.wikipedia.org/wiki/Double_factorial - - >>> import math - >>> all(double_factorial(i) == math.prod(range(i, 0, -2)) for i in range(20)) - True - >>> double_factorial(0.1) - Traceback (most recent call last): - ... - ValueError: double_factorial() only accepts integral values - >>> double_factorial(-1) - Traceback (most recent call last): - ... - ValueError: double_factorial() not defined for negative values - """ - if not isinstance(num, int): - raise ValueError("double_factorial() only accepts integral values") - if num < 0: - raise ValueError("double_factorial() not defined for negative values") - value = 1 - for i in range(num, 0, -2): - value *= i - return value - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/maths/double_factorial_recursive.py b/maths/double_factorial_recursive.py deleted file mode 100644 index 05c9b29680a7..000000000000 --- a/maths/double_factorial_recursive.py +++ /dev/null @@ -1,31 +0,0 @@ -def double_factorial(n: int) -> int: - """ - Compute double factorial using recursive method. - Recursion can be costly for large numbers. - - To learn about the theory behind this algorithm: - https://en.wikipedia.org/wiki/Double_factorial - - >>> import math - >>> all(double_factorial(i) == math.prod(range(i, 0, -2)) for i in range(20)) - True - >>> double_factorial(0.1) - Traceback (most recent call last): - ... - ValueError: double_factorial() only accepts integral values - >>> double_factorial(-1) - Traceback (most recent call last): - ... - ValueError: double_factorial() not defined for negative values - """ - if not isinstance(n, int): - raise ValueError("double_factorial() only accepts integral values") - if n < 0: - raise ValueError("double_factorial() not defined for negative values") - return 1 if n <= 1 else n * double_factorial(n - 2) - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/maths/dual_number_automatic_differentiation.py b/maths/dual_number_automatic_differentiation.py new file mode 100644 index 000000000000..09aeb17a4aea --- /dev/null +++ b/maths/dual_number_automatic_differentiation.py @@ -0,0 +1,139 @@ +from math import factorial + +""" +https://en.wikipedia.org/wiki/Automatic_differentiation#Automatic_differentiation_using_dual_numbers +https://blog.jliszka.org/2013/10/24/exact-numeric-nth-derivatives.html + +Note this only works for basic functions, f(x) where the power of x is positive. +""" + + +class Dual: + def __init__(self, real, rank): + self.real = real + if isinstance(rank, int): + self.duals = [1] * rank + else: + self.duals = rank + + def __repr__(self): + s = "+".join(f"{dual}E{n}" for n, dual in enumerate(self.duals, 1)) + return f"{self.real}+{s}" + + def reduce(self): + cur = self.duals.copy() + while cur[-1] == 0: + cur.pop(-1) + return Dual(self.real, cur) + + def __add__(self, other): + if not isinstance(other, Dual): + return Dual(self.real + other, self.duals) + s_dual = self.duals.copy() + o_dual = other.duals.copy() + if len(s_dual) > len(o_dual): + o_dual.extend([1] * (len(s_dual) - len(o_dual))) + elif len(s_dual) < len(o_dual): + s_dual.extend([1] * (len(o_dual) - len(s_dual))) + new_duals = [] + for i in range(len(s_dual)): + new_duals.append(s_dual[i] + o_dual[i]) + return Dual(self.real + other.real, new_duals) + + __radd__ = __add__ + + def __sub__(self, other): + return self + other * -1 + + def __mul__(self, other): + if not isinstance(other, Dual): + new_duals = [] + for i in self.duals: + new_duals.append(i * other) + return Dual(self.real * other, new_duals) + new_duals = [0] * (len(self.duals) + len(other.duals) + 1) + for i, item in enumerate(self.duals): + for j, jtem in enumerate(other.duals): + new_duals[i + j + 1] += item * jtem + for k in range(len(self.duals)): + new_duals[k] += self.duals[k] * other.real + for index in range(len(other.duals)): + new_duals[index] += other.duals[index] * self.real + return Dual(self.real * other.real, new_duals) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if not isinstance(other, Dual): + new_duals = [] + for i in self.duals: + new_duals.append(i / other) + return Dual(self.real / other, new_duals) + raise ValueError + + def __floordiv__(self, other): + if not isinstance(other, Dual): + new_duals = [] + for i in self.duals: + new_duals.append(i // other) + return Dual(self.real // other, new_duals) + raise ValueError + + def __pow__(self, n): + if n < 0 or isinstance(n, float): + raise ValueError("power must be a positive integer") + if n == 0: + return 1 + if n == 1: + return self + x = self + for _ in range(n - 1): + x *= self + return x + + +def differentiate(func, position, order): + """ + >>> differentiate(lambda x: x**2, 2, 2) + 2 + >>> differentiate(lambda x: x**2 * x**4, 9, 2) + 196830 + >>> differentiate(lambda y: 0.5 * (y + 3) ** 6, 3.5, 4) + 7605.0 + >>> differentiate(lambda y: y ** 2, 4, 3) + 0 + >>> differentiate(8, 8, 8) + Traceback (most recent call last): + ... + ValueError: differentiate() requires a function as input for func + >>> differentiate(lambda x: x **2, "", 1) + Traceback (most recent call last): + ... + ValueError: differentiate() requires a float as input for position + >>> differentiate(lambda x: x**2, 3, "") + Traceback (most recent call last): + ... + ValueError: differentiate() requires an int as input for order + """ + if not callable(func): + raise ValueError("differentiate() requires a function as input for func") + if not isinstance(position, (float, int)): + raise ValueError("differentiate() requires a float as input for position") + if not isinstance(order, int): + raise ValueError("differentiate() requires an int as input for order") + d = Dual(position, 1) + result = func(d) + if order == 0: + return result.real + return result.duals[order - 1] * factorial(order) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + def f(y): + return y**2 * y**4 + + print(differentiate(f, 9, 2)) diff --git a/maths/entropy.py b/maths/entropy.py index 498c28f31bc4..b816f1d193f7 100644 --- a/maths/entropy.py +++ b/maths/entropy.py @@ -4,6 +4,7 @@ Implementation of entropy of information https://en.wikipedia.org/wiki/Entropy_(information_theory) """ + from __future__ import annotations import math @@ -20,10 +21,10 @@ def calculate_prob(text: str) -> None: :return: Prints 1) Entropy of information based on 1 alphabet 2) Entropy of information based on couples of 2 alphabet - 3) print Entropy of H(X n∣Xn−1) + 3) print Entropy of H(X n|Xn-1) Text from random books. Also, random quotes. - >>> text = ("Behind Winston’s back the voice " + >>> text = ("Behind Winston's back the voice " ... "from the telescreen was still " ... "babbling and the overfulfilment") >>> calculate_prob(text) @@ -95,13 +96,13 @@ def analyze_text(text: str) -> tuple[dict, dict]: The first dictionary stores the frequency of single character strings. The second dictionary stores the frequency of two character strings. """ - single_char_strings = Counter() # type: ignore - two_char_strings = Counter() # type: ignore + single_char_strings = Counter() # type: ignore[var-annotated] + two_char_strings = Counter() # type: ignore[var-annotated] single_char_strings[text[-1]] += 1 # first case when we have space at start. two_char_strings[" " + text[0]] += 1 - for i in range(0, len(text) - 1): + for i in range(len(text) - 1): single_char_strings[text[i]] += 1 two_char_strings[text[i : i + 2]] += 1 return single_char_strings, two_char_strings diff --git a/maths/euclidean_distance.py b/maths/euclidean_distance.py index 22012e92c9cf..aa7f3efc7684 100644 --- a/maths/euclidean_distance.py +++ b/maths/euclidean_distance.py @@ -1,25 +1,25 @@ from __future__ import annotations +import typing from collections.abc import Iterable -from typing import Union import numpy as np -Vector = Union[Iterable[float], Iterable[int], np.ndarray] -VectorOut = Union[np.float64, int, float] +Vector = typing.Union[Iterable[float], Iterable[int], np.ndarray] # noqa: UP007 +VectorOut = typing.Union[np.float64, int, float] # noqa: UP007 def euclidean_distance(vector_1: Vector, vector_2: Vector) -> VectorOut: """ Calculate the distance between the two endpoints of two vectors. A vector is defined as a list, tuple, or numpy 1D array. - >>> euclidean_distance((0, 0), (2, 2)) + >>> float(euclidean_distance((0, 0), (2, 2))) 2.8284271247461903 - >>> euclidean_distance(np.array([0, 0, 0]), np.array([2, 2, 2])) + >>> float(euclidean_distance(np.array([0, 0, 0]), np.array([2, 2, 2]))) 3.4641016151377544 - >>> euclidean_distance(np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])) + >>> float(euclidean_distance(np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8]))) 8.0 - >>> euclidean_distance([1, 2, 3, 4], [5, 6, 7, 8]) + >>> float(euclidean_distance([1, 2, 3, 4], [5, 6, 7, 8])) 8.0 """ return np.sqrt(np.sum((np.asarray(vector_1) - np.asarray(vector_2)) ** 2)) diff --git a/maths/euclidean_gcd.py b/maths/euclidean_gcd.py deleted file mode 100644 index de4b250243db..000000000000 --- a/maths/euclidean_gcd.py +++ /dev/null @@ -1,47 +0,0 @@ -""" https://en.wikipedia.org/wiki/Euclidean_algorithm """ - - -def euclidean_gcd(a: int, b: int) -> int: - """ - Examples: - >>> euclidean_gcd(3, 5) - 1 - - >>> euclidean_gcd(6, 3) - 3 - """ - while b: - a, b = b, a % b - return a - - -def euclidean_gcd_recursive(a: int, b: int) -> int: - """ - Recursive method for euclicedan gcd algorithm - - Examples: - >>> euclidean_gcd_recursive(3, 5) - 1 - - >>> euclidean_gcd_recursive(6, 3) - 3 - """ - return a if b == 0 else euclidean_gcd_recursive(b, a % b) - - -def main(): - print(f"euclidean_gcd(3, 5) = {euclidean_gcd(3, 5)}") - print(f"euclidean_gcd(5, 3) = {euclidean_gcd(5, 3)}") - print(f"euclidean_gcd(1, 3) = {euclidean_gcd(1, 3)}") - print(f"euclidean_gcd(3, 6) = {euclidean_gcd(3, 6)}") - print(f"euclidean_gcd(6, 3) = {euclidean_gcd(6, 3)}") - - print(f"euclidean_gcd_recursive(3, 5) = {euclidean_gcd_recursive(3, 5)}") - print(f"euclidean_gcd_recursive(5, 3) = {euclidean_gcd_recursive(5, 3)}") - print(f"euclidean_gcd_recursive(1, 3) = {euclidean_gcd_recursive(1, 3)}") - print(f"euclidean_gcd_recursive(3, 6) = {euclidean_gcd_recursive(3, 6)}") - print(f"euclidean_gcd_recursive(6, 3) = {euclidean_gcd_recursive(6, 3)}") - - -if __name__ == "__main__": - main() diff --git a/maths/euler_method.py b/maths/euler_method.py index 30f193e6daa5..c6adb07e2d3d 100644 --- a/maths/euler_method.py +++ b/maths/euler_method.py @@ -26,7 +26,7 @@ def explicit_euler( ... return y >>> y0 = 1 >>> y = explicit_euler(f, y0, 0.0, 0.01, 5) - >>> y[-1] + >>> float(y[-1]) 144.77277243257308 """ n = int(np.ceil((x_end - x0) / step_size)) diff --git a/maths/euler_modified.py b/maths/euler_modified.py index 14bddadf4c53..bb282e9f0ab9 100644 --- a/maths/euler_modified.py +++ b/maths/euler_modified.py @@ -5,7 +5,7 @@ def euler_modified( ode_func: Callable, y0: float, x0: float, step_size: float, x_end: float -) -> np.array: +) -> np.ndarray: """ Calculate solution at each step to an ODE using Euler's Modified Method The Euler Method is straightforward to implement, but can't give accurate solutions. @@ -24,13 +24,13 @@ def euler_modified( >>> def f1(x, y): ... return -2*x*(y**2) >>> y = euler_modified(f1, 1.0, 0.0, 0.2, 1.0) - >>> y[-1] + >>> float(y[-1]) 0.503338255442106 >>> import math >>> def f2(x, y): ... return -2*y + (x**3)*math.exp(-2*x) >>> y = euler_modified(f2, 1.0, 0.0, 0.1, 0.3) - >>> y[-1] + >>> float(y[-1]) 0.5525976431951775 """ n = int(np.ceil((x_end - x0) / step_size)) diff --git a/maths/eulers_totient.py b/maths/eulers_totient.py index 6a35e69bde0b..00f0254c215a 100644 --- a/maths/eulers_totient.py +++ b/maths/eulers_totient.py @@ -1,12 +1,27 @@ # Eulers Totient function finds the number of relative primes of a number n from 1 to n def totient(n: int) -> list: + """ + >>> n = 10 + >>> totient_calculation = totient(n) + >>> for i in range(1, n): + ... print(f"{i} has {totient_calculation[i]} relative primes.") + 1 has 0 relative primes. + 2 has 1 relative primes. + 3 has 2 relative primes. + 4 has 2 relative primes. + 5 has 4 relative primes. + 6 has 2 relative primes. + 7 has 6 relative primes. + 8 has 4 relative primes. + 9 has 6 relative primes. + """ is_prime = [True for i in range(n + 1)] totients = [i - 1 for i in range(n + 1)] primes = [] for i in range(2, n + 1): if is_prime[i]: primes.append(i) - for j in range(0, len(primes)): + for j in range(len(primes)): if i * primes[j] >= n: break is_prime[i * primes[j]] = False @@ -20,25 +35,6 @@ def totient(n: int) -> list: return totients -def test_totient() -> None: - """ - >>> n = 10 - >>> totient_calculation = totient(n) - >>> for i in range(1, n): - ... print(f"{i} has {totient_calculation[i]} relative primes.") - 1 has 0 relative primes. - 2 has 1 relative primes. - 3 has 2 relative primes. - 4 has 2 relative primes. - 5 has 4 relative primes. - 6 has 2 relative primes. - 7 has 6 relative primes. - 8 has 4 relative primes. - 9 has 6 relative primes. - """ - pass - - if __name__ == "__main__": import doctest diff --git a/maths/factorial.py b/maths/factorial.py index bbf0efc011d8..aaf90f384bb9 100644 --- a/maths/factorial.py +++ b/maths/factorial.py @@ -1,4 +1,5 @@ -"""Factorial of a positive integer -- https://en.wikipedia.org/wiki/Factorial +""" +Factorial of a positive integer -- https://en.wikipedia.org/wiki/Factorial """ @@ -55,7 +56,7 @@ def factorial_recursive(n: int) -> int: raise ValueError("factorial() only accepts integral values") if n < 0: raise ValueError("factorial() not defined for negative values") - return 1 if n == 0 or n == 1 else n * factorial(n - 1) + return 1 if n in {0, 1} else n * factorial(n - 1) if __name__ == "__main__": diff --git a/maths/fast_inverse_sqrt.py b/maths/fast_inverse_sqrt.py new file mode 100644 index 000000000000..79385bb84877 --- /dev/null +++ b/maths/fast_inverse_sqrt.py @@ -0,0 +1,54 @@ +""" +Fast inverse square root (1/sqrt(x)) using the Quake III algorithm. +Reference: https://en.wikipedia.org/wiki/Fast_inverse_square_root +Accuracy: https://en.wikipedia.org/wiki/Fast_inverse_square_root#Accuracy +""" + +import struct + + +def fast_inverse_sqrt(number: float) -> float: + """ + Compute the fast inverse square root of a floating-point number using the famous + Quake III algorithm. + + :param float number: Input number for which to calculate the inverse square root. + :return float: The fast inverse square root of the input number. + + Example: + >>> fast_inverse_sqrt(10) + 0.3156857923527257 + >>> fast_inverse_sqrt(4) + 0.49915357479239103 + >>> fast_inverse_sqrt(4.1) + 0.4932849504615651 + >>> fast_inverse_sqrt(0) + Traceback (most recent call last): + ... + ValueError: Input must be a positive number. + >>> fast_inverse_sqrt(-1) + Traceback (most recent call last): + ... + ValueError: Input must be a positive number. + >>> from math import isclose, sqrt + >>> all(isclose(fast_inverse_sqrt(i), 1 / sqrt(i), rel_tol=0.00132) + ... for i in range(50, 60)) + True + """ + if number <= 0: + raise ValueError("Input must be a positive number.") + i = struct.unpack(">i", struct.pack(">f", number))[0] + i = 0x5F3759DF - (i >> 1) + y = struct.unpack(">f", struct.pack(">i", i))[0] + return y * (1.5 - 0.5 * number * y * y) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + # https://en.wikipedia.org/wiki/Fast_inverse_square_root#Accuracy + from math import sqrt + + for i in range(5, 101, 5): + print(f"{i:>3}: {(1 / sqrt(i)) - fast_inverse_sqrt(i):.5f}") diff --git a/maths/fermat_little_theorem.py b/maths/fermat_little_theorem.py index eea03be245cb..4a3ecd05ce91 100644 --- a/maths/fermat_little_theorem.py +++ b/maths/fermat_little_theorem.py @@ -5,7 +5,7 @@ # Wikipedia reference: https://en.wikipedia.org/wiki/Fermat%27s_little_theorem -def binary_exponentiation(a, n, mod): +def binary_exponentiation(a: int, n: float, mod: int) -> int: if n == 0: return 1 diff --git a/maths/fibonacci.py b/maths/fibonacci.py index d58c9fc68c67..24b2d7ae449e 100644 --- a/maths/fibonacci.py +++ b/maths/fibonacci.py @@ -1,4 +1,3 @@ -# fibonacci.py """ Calculates the Fibonacci sequence using iteration, recursion, memoization, and a simplified form of Binet's formula @@ -8,18 +7,21 @@ NOTE 2: the Binet's formula function is much more limited in the size of inputs that it can handle due to the size limitations of Python floats +NOTE 3: the matrix function is the fastest and most memory efficient for large n -RESULTS: (n = 20) -fib_iterative runtime: 0.0055 ms -fib_recursive runtime: 6.5627 ms -fib_memoization runtime: 0.0107 ms -fib_binet runtime: 0.0174 ms + +See benchmark numbers in __main__ for performance comparisons/ +https://en.wikipedia.org/wiki/Fibonacci_number for more information """ -from functools import lru_cache +import functools +from collections.abc import Iterator from math import sqrt from time import time +import numpy as np +from numpy import ndarray + def time_func(func, *args, **kwargs): """ @@ -35,6 +37,31 @@ def time_func(func, *args, **kwargs): return output +def fib_iterative_yield(n: int) -> Iterator[int]: + """ + Calculates the first n (1-indexed) Fibonacci numbers using iteration with yield + >>> list(fib_iterative_yield(0)) + [0] + >>> tuple(fib_iterative_yield(1)) + (0, 1) + >>> tuple(fib_iterative_yield(5)) + (0, 1, 1, 2, 3, 5) + >>> tuple(fib_iterative_yield(10)) + (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55) + >>> tuple(fib_iterative_yield(-1)) + Traceback (most recent call last): + ... + ValueError: n is negative + """ + if n < 0: + raise ValueError("n is negative") + a, b = 0, 1 + yield a + for _ in range(n): + yield b + a, b = b, a + b + + def fib_iterative(n: int) -> list[int]: """ Calculates the first n (0-indexed) Fibonacci numbers using iteration @@ -49,10 +76,10 @@ def fib_iterative(n: int) -> list[int]: >>> fib_iterative(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative """ if n < 0: - raise Exception("n is negative") + raise ValueError("n is negative") if n == 0: return [0] fib = [0, 1] @@ -75,21 +102,33 @@ def fib_recursive(n: int) -> list[int]: >>> fib_iterative(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative """ def fib_recursive_term(i: int) -> int: """ Calculates the i-th (0-indexed) Fibonacci number using recursion + >>> fib_recursive_term(0) + 0 + >>> fib_recursive_term(1) + 1 + >>> fib_recursive_term(5) + 5 + >>> fib_recursive_term(10) + 55 + >>> fib_recursive_term(-1) + Traceback (most recent call last): + ... + Exception: n is negative """ if i < 0: - raise Exception("n is negative") + raise ValueError("n is negative") if i < 2: return i return fib_recursive_term(i - 1) + fib_recursive_term(i - 2) if n < 0: - raise Exception("n is negative") + raise ValueError("n is negative") return [fib_recursive_term(i) for i in range(n + 1)] @@ -107,22 +146,22 @@ def fib_recursive_cached(n: int) -> list[int]: >>> fib_iterative(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative """ - @lru_cache(maxsize=None) + @functools.cache def fib_recursive_term(i: int) -> int: """ Calculates the i-th (0-indexed) Fibonacci number using recursion """ if i < 0: - raise Exception("n is negative") + raise ValueError("n is negative") if i < 2: return i return fib_recursive_term(i - 1) + fib_recursive_term(i - 2) if n < 0: - raise Exception("n is negative") + raise ValueError("n is negative") return [fib_recursive_term(i) for i in range(n + 1)] @@ -140,10 +179,10 @@ def fib_memoization(n: int) -> list[int]: >>> fib_iterative(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative """ if n < 0: - raise Exception("n is negative") + raise ValueError("n is negative") # Cache must be outside recursuive function # other it will reset every time it calls itself. cache: dict[int, int] = {0: 0, 1: 1, 2: 1} # Prefilled cache @@ -181,25 +220,113 @@ def fib_binet(n: int) -> list[int]: >>> fib_binet(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative >>> fib_binet(1475) Traceback (most recent call last): ... - Exception: n is too large + ValueError: n is too large """ if n < 0: - raise Exception("n is negative") + raise ValueError("n is negative") if n >= 1475: - raise Exception("n is too large") + raise ValueError("n is too large") sqrt_5 = sqrt(5) phi = (1 + sqrt_5) / 2 return [round(phi**i / sqrt_5) for i in range(n + 1)] +def matrix_pow_np(m: ndarray, power: int) -> ndarray: + """ + Raises a matrix to the power of 'power' using binary exponentiation. + + Args: + m: Matrix as a numpy array. + power: The power to which the matrix is to be raised. + + Returns: + The matrix raised to the power. + + Raises: + ValueError: If power is negative. + + >>> m = np.array([[1, 1], [1, 0]], dtype=int) + >>> matrix_pow_np(m, 0) # Identity matrix when raised to the power of 0 + array([[1, 0], + [0, 1]]) + + >>> matrix_pow_np(m, 1) # Same matrix when raised to the power of 1 + array([[1, 1], + [1, 0]]) + + >>> matrix_pow_np(m, 5) + array([[8, 5], + [5, 3]]) + + >>> matrix_pow_np(m, -1) + Traceback (most recent call last): + ... + ValueError: power is negative + """ + result = np.array([[1, 0], [0, 1]], dtype=int) # Identity Matrix + base = m + if power < 0: # Negative power is not allowed + raise ValueError("power is negative") + while power: + if power % 2 == 1: + result = np.dot(result, base) + base = np.dot(base, base) + power //= 2 + return result + + +def fib_matrix_np(n: int) -> int: + """ + Calculates the n-th Fibonacci number using matrix exponentiation. + https://www.nayuki.io/page/fast-fibonacci-algorithms#:~:text= + Summary:%20The%20two%20fast%20Fibonacci%20algorithms%20are%20matrix + + Args: + n: Fibonacci sequence index + + Returns: + The n-th Fibonacci number. + + Raises: + ValueError: If n is negative. + + >>> fib_matrix_np(0) + 0 + >>> fib_matrix_np(1) + 1 + >>> fib_matrix_np(5) + 5 + >>> fib_matrix_np(10) + 55 + >>> fib_matrix_np(-1) + Traceback (most recent call last): + ... + ValueError: n is negative + """ + if n < 0: + raise ValueError("n is negative") + if n == 0: + return 0 + + m = np.array([[1, 1], [1, 0]], dtype=int) + result = matrix_pow_np(m, n - 1) + return int(result[0, 0]) + + if __name__ == "__main__": + from doctest import testmod + + testmod() + # Time on an M1 MacBook Pro -- Fastest to slowest num = 30 - time_func(fib_iterative, num) - time_func(fib_recursive, num) # Around 3s runtime - time_func(fib_recursive_cached, num) # Around 0ms runtime - time_func(fib_memoization, num) - time_func(fib_binet, num) + time_func(fib_iterative_yield, num) # 0.0012 ms + time_func(fib_iterative, num) # 0.0031 ms + time_func(fib_binet, num) # 0.0062 ms + time_func(fib_memoization, num) # 0.0100 ms + time_func(fib_recursive_cached, num) # 0.0153 ms + time_func(fib_recursive, num) # 257.0910 ms + time_func(fib_matrix_np, num) # 0.0000 ms diff --git a/maths/find_max.py b/maths/find_max.py index 684fbe8161e8..4765d300634e 100644 --- a/maths/find_max.py +++ b/maths/find_max.py @@ -1,30 +1,83 @@ from __future__ import annotations -def find_max(nums: list[int | float]) -> int | float: +def find_max_iterative(nums: list[int | float]) -> int | float: """ >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_max(nums) == max(nums) + ... find_max_iterative(nums) == max(nums) True True True True - >>> find_max([2, 4, 9, 7, 19, 94, 5]) + >>> find_max_iterative([2, 4, 9, 7, 19, 94, 5]) 94 - >>> find_max([]) + >>> find_max_iterative([]) Traceback (most recent call last): ... - ValueError: find_max() arg is an empty sequence + ValueError: find_max_iterative() arg is an empty sequence """ if len(nums) == 0: - raise ValueError("find_max() arg is an empty sequence") + raise ValueError("find_max_iterative() arg is an empty sequence") max_num = nums[0] for x in nums: - if x > max_num: + if x > max_num: # noqa: PLR1730 max_num = x return max_num +# Divide and Conquer algorithm +def find_max_recursive(nums: list[int | float], left: int, right: int) -> int | float: + """ + find max value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: max in nums + + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_max_recursive(nums, 0, len(nums) - 1) == max(nums) + True + True + True + True + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_max_recursive(nums, 0, len(nums) - 1) == max(nums) + True + >>> find_max_recursive([], 0, 0) + Traceback (most recent call last): + ... + ValueError: find_max_recursive() arg is an empty sequence + >>> find_max_recursive(nums, 0, len(nums)) == max(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> find_max_recursive(nums, -len(nums), -1) == max(nums) + True + >>> find_max_recursive(nums, -len(nums) - 1, -1) == max(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + if len(nums) == 0: + raise ValueError("find_max_recursive() arg is an empty sequence") + if ( + left >= len(nums) + or left < -len(nums) + or right >= len(nums) + or right < -len(nums) + ): + raise IndexError("list index out of range") + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_max = find_max_recursive(nums, left, mid) # find max in range[left, mid] + right_max = find_max_recursive( + nums, mid + 1, right + ) # find max in range[mid + 1, right] + + return left_max if left_max >= right_max else right_max + + if __name__ == "__main__": import doctest diff --git a/maths/find_max_recursion.py b/maths/find_max_recursion.py deleted file mode 100644 index 629932e0818f..000000000000 --- a/maths/find_max_recursion.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - - -# Divide and Conquer algorithm -def find_max(nums: list[int | float], left: int, right: int) -> int | float: - """ - find max value in list - :param nums: contains elements - :param left: index of first element - :param right: index of last element - :return: max in nums - - >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_max(nums, 0, len(nums) - 1) == max(nums) - True - True - True - True - >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] - >>> find_max(nums, 0, len(nums) - 1) == max(nums) - True - >>> find_max([], 0, 0) - Traceback (most recent call last): - ... - ValueError: find_max() arg is an empty sequence - >>> find_max(nums, 0, len(nums)) == max(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - >>> find_max(nums, -len(nums), -1) == max(nums) - True - >>> find_max(nums, -len(nums) - 1, -1) == max(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - """ - if len(nums) == 0: - raise ValueError("find_max() arg is an empty sequence") - if ( - left >= len(nums) - or left < -len(nums) - or right >= len(nums) - or right < -len(nums) - ): - raise IndexError("list index out of range") - if left == right: - return nums[left] - mid = (left + right) >> 1 # the middle - left_max = find_max(nums, left, mid) # find max in range[left, mid] - right_max = find_max(nums, mid + 1, right) # find max in range[mid + 1, right] - - return left_max if left_max >= right_max else right_max - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) diff --git a/maths/find_min.py b/maths/find_min.py index 2eac087c6388..762562e36ef9 100644 --- a/maths/find_min.py +++ b/maths/find_min.py @@ -1,33 +1,86 @@ from __future__ import annotations -def find_min(nums: list[int | float]) -> int | float: +def find_min_iterative(nums: list[int | float]) -> int | float: """ Find Minimum Number in a List :param nums: contains elements :return: min number in list >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_min(nums) == min(nums) + ... find_min_iterative(nums) == min(nums) True True True True - >>> find_min([0, 1, 2, 3, 4, 5, -3, 24, -56]) + >>> find_min_iterative([0, 1, 2, 3, 4, 5, -3, 24, -56]) -56 - >>> find_min([]) + >>> find_min_iterative([]) Traceback (most recent call last): ... - ValueError: find_min() arg is an empty sequence + ValueError: find_min_iterative() arg is an empty sequence """ if len(nums) == 0: - raise ValueError("find_min() arg is an empty sequence") + raise ValueError("find_min_iterative() arg is an empty sequence") min_num = nums[0] for num in nums: min_num = min(min_num, num) return min_num +# Divide and Conquer algorithm +def find_min_recursive(nums: list[int | float], left: int, right: int) -> int | float: + """ + find min value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: min in nums + + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_min_recursive(nums, 0, len(nums) - 1) == min(nums) + True + True + True + True + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_min_recursive(nums, 0, len(nums) - 1) == min(nums) + True + >>> find_min_recursive([], 0, 0) + Traceback (most recent call last): + ... + ValueError: find_min_recursive() arg is an empty sequence + >>> find_min_recursive(nums, 0, len(nums)) == min(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> find_min_recursive(nums, -len(nums), -1) == min(nums) + True + >>> find_min_recursive(nums, -len(nums) - 1, -1) == min(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + if len(nums) == 0: + raise ValueError("find_min_recursive() arg is an empty sequence") + if ( + left >= len(nums) + or left < -len(nums) + or right >= len(nums) + or right < -len(nums) + ): + raise IndexError("list index out of range") + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_min = find_min_recursive(nums, left, mid) # find min in range[left, mid] + right_min = find_min_recursive( + nums, mid + 1, right + ) # find min in range[mid + 1, right] + + return left_min if left_min <= right_min else right_min + + if __name__ == "__main__": import doctest diff --git a/maths/find_min_recursion.py b/maths/find_min_recursion.py deleted file mode 100644 index 4d11015efcd5..000000000000 --- a/maths/find_min_recursion.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - - -# Divide and Conquer algorithm -def find_min(nums: list[int | float], left: int, right: int) -> int | float: - """ - find min value in list - :param nums: contains elements - :param left: index of first element - :param right: index of last element - :return: min in nums - - >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_min(nums, 0, len(nums) - 1) == min(nums) - True - True - True - True - >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] - >>> find_min(nums, 0, len(nums) - 1) == min(nums) - True - >>> find_min([], 0, 0) - Traceback (most recent call last): - ... - ValueError: find_min() arg is an empty sequence - >>> find_min(nums, 0, len(nums)) == min(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - >>> find_min(nums, -len(nums), -1) == min(nums) - True - >>> find_min(nums, -len(nums) - 1, -1) == min(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - """ - if len(nums) == 0: - raise ValueError("find_min() arg is an empty sequence") - if ( - left >= len(nums) - or left < -len(nums) - or right >= len(nums) - or right < -len(nums) - ): - raise IndexError("list index out of range") - if left == right: - return nums[left] - mid = (left + right) >> 1 # the middle - left_min = find_min(nums, left, mid) # find min in range[left, mid] - right_min = find_min(nums, mid + 1, right) # find min in range[mid + 1, right] - - return left_min if left_min <= right_min else right_min - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) diff --git a/maths/gamma.py b/maths/gamma.py index d5debc58764b..e328cd8b22b7 100644 --- a/maths/gamma.py +++ b/maths/gamma.py @@ -1,35 +1,44 @@ +""" +Gamma function is a very useful tool in math and physics. +It helps calculating complex integral in a convenient way. +for more info: https://en.wikipedia.org/wiki/Gamma_function +In mathematics, the gamma function is one commonly +used extension of the factorial function to complex numbers. +The gamma function is defined for all complex numbers except +the non-positive integers +Python's Standard Library math.gamma() function overflows around gamma(171.624). +""" + import math from numpy import inf from scipy.integrate import quad -def gamma(num: float) -> float: +def gamma_iterative(num: float) -> float: """ - https://en.wikipedia.org/wiki/Gamma_function - In mathematics, the gamma function is one commonly - used extension of the factorial function to complex numbers. - The gamma function is defined for all complex numbers except the non-positive - integers - >>> gamma(-1) + Calculates the value of Gamma function of num + where num is either an integer (1, 2, 3..) or a half-integer (0.5, 1.5, 2.5 ...). + + >>> gamma_iterative(-1) Traceback (most recent call last): ... ValueError: math domain error - >>> gamma(0) + >>> gamma_iterative(0) Traceback (most recent call last): ... ValueError: math domain error - >>> gamma(9) + >>> gamma_iterative(9) 40320.0 >>> from math import gamma as math_gamma - >>> all(.99999999 < gamma(i) / math_gamma(i) <= 1.000000001 + >>> all(.99999999 < gamma_iterative(i) / math_gamma(i) <= 1.000000001 ... for i in range(1, 50)) True - >>> gamma(-1)/math_gamma(-1) <= 1.000000001 + >>> gamma_iterative(-1)/math_gamma(-1) <= 1.000000001 Traceback (most recent call last): ... ValueError: math domain error - >>> gamma(3.3) - math_gamma(3.3) <= 0.00000001 + >>> gamma_iterative(3.3) - math_gamma(3.3) <= 0.00000001 True """ if num <= 0: @@ -42,7 +51,66 @@ def integrand(x: float, z: float) -> float: return math.pow(x, z - 1) * math.exp(-x) +def gamma_recursive(num: float) -> float: + """ + Calculates the value of Gamma function of num + where num is either an integer (1, 2, 3..) or a half-integer (0.5, 1.5, 2.5 ...). + Implemented using recursion + Examples: + >>> from math import isclose, gamma as math_gamma + >>> gamma_recursive(0.5) + 1.7724538509055159 + >>> gamma_recursive(1) + 1.0 + >>> gamma_recursive(2) + 1.0 + >>> gamma_recursive(3.5) + 3.3233509704478426 + >>> gamma_recursive(171.5) + 9.483367566824795e+307 + >>> all(isclose(gamma_recursive(num), math_gamma(num)) + ... for num in (0.5, 2, 3.5, 171.5)) + True + >>> gamma_recursive(0) + Traceback (most recent call last): + ... + ValueError: math domain error + >>> gamma_recursive(-1.1) + Traceback (most recent call last): + ... + ValueError: math domain error + >>> gamma_recursive(-4) + Traceback (most recent call last): + ... + ValueError: math domain error + >>> gamma_recursive(172) + Traceback (most recent call last): + ... + OverflowError: math range error + >>> gamma_recursive(1.1) + Traceback (most recent call last): + ... + NotImplementedError: num must be an integer or a half-integer + """ + if num <= 0: + raise ValueError("math domain error") + if num > 171.5: + raise OverflowError("math range error") + elif num - int(num) not in (0, 0.5): + raise NotImplementedError("num must be an integer or a half-integer") + elif num == 0.5: + return math.sqrt(math.pi) + else: + return 1.0 if num == 1 else (num - 1) * gamma_recursive(num - 1) + + if __name__ == "__main__": from doctest import testmod testmod() + num = 1.0 + while num: + num = float(input("Gamma of: ")) + print(f"gamma_iterative({num}) = {gamma_iterative(num)}") + print(f"gamma_recursive({num}) = {gamma_recursive(num)}") + print("\nEnter 0 to exit...") diff --git a/maths/gamma_recursive.py b/maths/gamma_recursive.py deleted file mode 100644 index 3d6b8c5e8138..000000000000 --- a/maths/gamma_recursive.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Gamma function is a very useful tool in math and physics. -It helps calculating complex integral in a convenient way. -for more info: https://en.wikipedia.org/wiki/Gamma_function -Python's Standard Library math.gamma() function overflows around gamma(171.624). -""" -from math import pi, sqrt - - -def gamma(num: float) -> float: - """ - Calculates the value of Gamma function of num - where num is either an integer (1, 2, 3..) or a half-integer (0.5, 1.5, 2.5 ...). - Implemented using recursion - Examples: - >>> from math import isclose, gamma as math_gamma - >>> gamma(0.5) - 1.7724538509055159 - >>> gamma(2) - 1.0 - >>> gamma(3.5) - 3.3233509704478426 - >>> gamma(171.5) - 9.483367566824795e+307 - >>> all(isclose(gamma(num), math_gamma(num)) for num in (0.5, 2, 3.5, 171.5)) - True - >>> gamma(0) - Traceback (most recent call last): - ... - ValueError: math domain error - >>> gamma(-1.1) - Traceback (most recent call last): - ... - ValueError: math domain error - >>> gamma(-4) - Traceback (most recent call last): - ... - ValueError: math domain error - >>> gamma(172) - Traceback (most recent call last): - ... - OverflowError: math range error - >>> gamma(1.1) - Traceback (most recent call last): - ... - NotImplementedError: num must be an integer or a half-integer - """ - if num <= 0: - raise ValueError("math domain error") - if num > 171.5: - raise OverflowError("math range error") - elif num - int(num) not in (0, 0.5): - raise NotImplementedError("num must be an integer or a half-integer") - elif num == 0.5: - return sqrt(pi) - else: - return 1.0 if num == 1 else (num - 1) * gamma(num - 1) - - -def test_gamma() -> None: - """ - >>> test_gamma() - """ - assert gamma(0.5) == sqrt(pi) - assert gamma(1) == 1.0 - assert gamma(2) == 1.0 - - -if __name__ == "__main__": - from doctest import testmod - - testmod() - num = 1.0 - while num: - num = float(input("Gamma of: ")) - print(f"gamma({num}) = {gamma(num)}") - print("\nEnter 0 to exit...") diff --git a/maths/gaussian.py b/maths/gaussian.py index 51ebc2e25849..b1e62ea77fe2 100644 --- a/maths/gaussian.py +++ b/maths/gaussian.py @@ -1,21 +1,22 @@ """ Reference: https://en.wikipedia.org/wiki/Gaussian_function """ + from numpy import exp, pi, sqrt -def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int: +def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> float: """ - >>> gaussian(1) + >>> float(gaussian(1)) 0.24197072451914337 - >>> gaussian(24) + >>> float(gaussian(24)) 3.342714441794458e-126 - >>> gaussian(1, 4, 2) + >>> float(gaussian(1, 4, 2)) 0.06475879783294587 - >>> gaussian(1, 5, 3) + >>> float(gaussian(1, 5, 3)) 0.05467002489199788 Supports NumPy Arrays @@ -28,7 +29,7 @@ def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int: 5.05227108e-15, 1.02797736e-18, 7.69459863e-23, 2.11881925e-27, 2.14638374e-32, 7.99882776e-38, 1.09660656e-43]) - >>> gaussian(15) + >>> float(gaussian(15)) 5.530709549844416e-50 >>> gaussian([1,2, 'string']) @@ -46,10 +47,10 @@ def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int: ... OverflowError: (34, 'Result too large') - >>> gaussian(10**-326) + >>> float(gaussian(10**-326)) 0.3989422804014327 - >>> gaussian(2523, mu=234234, sigma=3425) + >>> float(gaussian(2523, mu=234234, sigma=3425)) 0.0 """ return 1 / sqrt(2 * pi * sigma**2) * exp(-((x - mu) ** 2) / (2 * sigma**2)) diff --git a/maths/geometric_mean.py b/maths/geometric_mean.py new file mode 100644 index 000000000000..240d519ad398 --- /dev/null +++ b/maths/geometric_mean.py @@ -0,0 +1,55 @@ +""" +The Geometric Mean of n numbers is defined as the n-th root of the product +of those numbers. It is used to measure the central tendency of the numbers. +https://en.wikipedia.org/wiki/Geometric_mean +""" + + +def compute_geometric_mean(*args: int) -> float: + """ + Return the geometric mean of the argument numbers. + >>> compute_geometric_mean(2,8) + 4.0 + >>> compute_geometric_mean('a', 4) + Traceback (most recent call last): + ... + TypeError: Not a Number + >>> compute_geometric_mean(5, 125) + 25.0 + >>> compute_geometric_mean(1, 0) + 0.0 + >>> compute_geometric_mean(1, 5, 25, 5) + 5.0 + >>> compute_geometric_mean(2, -2) + Traceback (most recent call last): + ... + ArithmeticError: Cannot Compute Geometric Mean for these numbers. + >>> compute_geometric_mean(-5, 25, 1) + -5.0 + """ + product = 1 + for number in args: + if not isinstance(number, int) and not isinstance(number, float): + raise TypeError("Not a Number") + product *= number + # Cannot calculate the even root for negative product. + # Frequently they are restricted to being positive. + if product < 0 and len(args) % 2 == 0: + raise ArithmeticError("Cannot Compute Geometric Mean for these numbers.") + mean = abs(product) ** (1 / len(args)) + # Since python calculates complex roots for negative products with odd roots. + if product < 0: + mean = -mean + # Since it does floating point arithmetic, it gives 64**(1/3) as 3.99999996 + possible_mean = float(round(mean)) + # To check if the rounded number is actually the mean. + if possible_mean ** len(args) == product: + mean = possible_mean + return mean + + +if __name__ == "__main__": + from doctest import testmod + + testmod(name="compute_geometric_mean") + print(compute_geometric_mean(-3, -27)) diff --git a/maths/germain_primes.py b/maths/germain_primes.py new file mode 100644 index 000000000000..078d1967f41a --- /dev/null +++ b/maths/germain_primes.py @@ -0,0 +1,72 @@ +""" +A Sophie Germain prime is any prime p, where 2p + 1 is also prime. +The second number, 2p + 1 is called a safe prime. + +Examples of Germain primes include: 2, 3, 5, 11, 23 + +Their corresponding safe primes: 5, 7, 11, 23, 47 +https://en.wikipedia.org/wiki/Safe_and_Sophie_Germain_primes +""" + +from maths.prime_check import is_prime + + +def is_germain_prime(number: int) -> bool: + """Checks if input number and 2*number + 1 are prime. + + >>> is_germain_prime(3) + True + >>> is_germain_prime(11) + True + >>> is_germain_prime(4) + False + >>> is_germain_prime(23) + True + >>> is_germain_prime(13) + False + >>> is_germain_prime(20) + False + >>> is_germain_prime('abc') + Traceback (most recent call last): + ... + TypeError: Input value must be a positive integer. Input value: abc + """ + if not isinstance(number, int) or number < 1: + msg = f"Input value must be a positive integer. Input value: {number}" + raise TypeError(msg) + + return is_prime(number) and is_prime(2 * number + 1) + + +def is_safe_prime(number: int) -> bool: + """Checks if input number and (number - 1)/2 are prime. + The smallest safe prime is 5, with the Germain prime is 2. + + >>> is_safe_prime(5) + True + >>> is_safe_prime(11) + True + >>> is_safe_prime(1) + False + >>> is_safe_prime(2) + False + >>> is_safe_prime(3) + False + >>> is_safe_prime(47) + True + >>> is_safe_prime('abc') + Traceback (most recent call last): + ... + TypeError: Input value must be a positive integer. Input value: abc + """ + if not isinstance(number, int) or number < 1: + msg = f"Input value must be a positive integer. Input value: {number}" + raise TypeError(msg) + + return (number - 1) % 2 == 0 and is_prime(number) and is_prime((number - 1) // 2) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/hardy_ramanujanalgo.py b/maths/hardy_ramanujanalgo.py index 6929533fc389..31ec76fbe10b 100644 --- a/maths/hardy_ramanujanalgo.py +++ b/maths/hardy_ramanujanalgo.py @@ -4,7 +4,7 @@ import math -def exact_prime_factor_count(n): +def exact_prime_factor_count(n: int) -> int: """ >>> exact_prime_factor_count(51242183) 3 diff --git a/maths/integer_square_root.py b/maths/integer_square_root.py new file mode 100644 index 000000000000..27e874a43c79 --- /dev/null +++ b/maths/integer_square_root.py @@ -0,0 +1,73 @@ +""" +Integer Square Root Algorithm -- An efficient method to calculate the square root of a +non-negative integer 'num' rounded down to the nearest integer. It uses a binary search +approach to find the integer square root without using any built-in exponent functions +or operators. +* https://en.wikipedia.org/wiki/Integer_square_root +* https://docs.python.org/3/library/math.html#math.isqrt +Note: + - This algorithm is designed for non-negative integers only. + - The result is rounded down to the nearest integer. + - The algorithm has a time complexity of O(log(x)). + - Original algorithm idea based on binary search. +""" + + +def integer_square_root(num: int) -> int: + """ + Returns the integer square root of a non-negative integer num. + Args: + num: A non-negative integer. + Returns: + The integer square root of num. + Raises: + ValueError: If num is not an integer or is negative. + >>> [integer_square_root(i) for i in range(18)] + [0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4] + >>> integer_square_root(625) + 25 + >>> integer_square_root(2_147_483_647) + 46340 + >>> from math import isqrt + >>> all(integer_square_root(i) == isqrt(i) for i in range(20)) + True + >>> integer_square_root(-1) + Traceback (most recent call last): + ... + ValueError: num must be non-negative integer + >>> integer_square_root(1.5) + Traceback (most recent call last): + ... + ValueError: num must be non-negative integer + >>> integer_square_root("0") + Traceback (most recent call last): + ... + ValueError: num must be non-negative integer + """ + if not isinstance(num, int) or num < 0: + raise ValueError("num must be non-negative integer") + + if num < 2: + return num + + left_bound = 0 + right_bound = num // 2 + + while left_bound <= right_bound: + mid = left_bound + (right_bound - left_bound) // 2 + mid_squared = mid * mid + if mid_squared == num: + return mid + + if mid_squared < num: + left_bound = mid + 1 + else: + right_bound = mid - 1 + + return right_bound + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/interquartile_range.py b/maths/interquartile_range.py new file mode 100644 index 000000000000..e91a651647d4 --- /dev/null +++ b/maths/interquartile_range.py @@ -0,0 +1,67 @@ +""" +An implementation of interquartile range (IQR) which is a measure of statistical +dispersion, which is the spread of the data. + +The function takes the list of numeric values as input and returns the IQR. + +Script inspired by this Wikipedia article: +https://en.wikipedia.org/wiki/Interquartile_range +""" + +from __future__ import annotations + + +def find_median(nums: list[int | float]) -> float: + """ + This is the implementation of the median. + :param nums: The list of numeric nums + :return: Median of the list + >>> find_median(nums=([1, 2, 2, 3, 4])) + 2 + >>> find_median(nums=([1, 2, 2, 3, 4, 4])) + 2.5 + >>> find_median(nums=([-1, 2, 0, 3, 4, -4])) + 1.5 + >>> find_median(nums=([1.1, 2.2, 2, 3.3, 4.4, 4])) + 2.65 + """ + div, mod = divmod(len(nums), 2) + if mod: + return nums[div] + return (nums[div] + nums[(div) - 1]) / 2 + + +def interquartile_range(nums: list[int | float]) -> float: + """ + Return the interquartile range for a list of numeric values. + :param nums: The list of numeric values. + :return: interquartile range + + >>> interquartile_range(nums=[4, 1, 2, 3, 2]) + 2.0 + >>> interquartile_range(nums = [-2, -7, -10, 9, 8, 4, -67, 45]) + 17.0 + >>> interquartile_range(nums = [-2.1, -7.1, -10.1, 9.1, 8.1, 4.1, -67.1, 45.1]) + 17.2 + >>> interquartile_range(nums = [0, 0, 0, 0, 0]) + 0.0 + >>> interquartile_range(nums=[]) + Traceback (most recent call last): + ... + ValueError: The list is empty. Provide a non-empty list. + """ + if not nums: + raise ValueError("The list is empty. Provide a non-empty list.") + nums.sort() + length = len(nums) + div, mod = divmod(length, 2) + q1 = find_median(nums[:div]) + half_length = sum((div, mod)) + q3 = find_median(nums[half_length:length]) + return q3 - q1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/is_int_palindrome.py b/maths/is_int_palindrome.py new file mode 100644 index 000000000000..63dc9e2138e8 --- /dev/null +++ b/maths/is_int_palindrome.py @@ -0,0 +1,34 @@ +def is_int_palindrome(num: int) -> bool: + """ + Returns whether `num` is a palindrome or not + (see for reference https://en.wikipedia.org/wiki/Palindromic_number). + + >>> is_int_palindrome(-121) + False + >>> is_int_palindrome(0) + True + >>> is_int_palindrome(10) + False + >>> is_int_palindrome(11) + True + >>> is_int_palindrome(101) + True + >>> is_int_palindrome(120) + False + """ + if num < 0: + return False + + num_copy: int = num + rev_num: int = 0 + while num > 0: + rev_num = rev_num * 10 + (num % 10) + num //= 10 + + return num_copy == rev_num + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/is_ip_v4_address_valid.py b/maths/is_ip_v4_address_valid.py index 0ae8e021ead1..305afabffed3 100644 --- a/maths/is_ip_v4_address_valid.py +++ b/maths/is_ip_v4_address_valid.py @@ -1,13 +1,15 @@ """ +wiki: https://en.wikipedia.org/wiki/IPv4 + Is IP v4 address valid? A valid IP address must be four octets in the form of A.B.C.D, -where A,B,C and D are numbers from 0-254 -for example: 192.168.23.1, 172.254.254.254 are valid IP address - 192.168.255.0, 255.192.3.121 are invalid IP address +where A, B, C and D are numbers from 0-255 +for example: 192.168.23.1, 172.255.255.255 are valid IP address + 192.168.256.0, 256.192.3.121 are invalid IP address """ -def is_ip_v4_address_valid(ip_v4_address: str) -> bool: +def is_ip_v4_address_valid(ip: str) -> bool: """ print "Valid IP address" If IP is valid. or @@ -16,13 +18,13 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool: >>> is_ip_v4_address_valid("192.168.0.23") True - >>> is_ip_v4_address_valid("192.255.15.8") + >>> is_ip_v4_address_valid("192.256.15.8") False >>> is_ip_v4_address_valid("172.100.0.8") True - >>> is_ip_v4_address_valid("254.255.0.255") + >>> is_ip_v4_address_valid("255.256.0.256") False >>> is_ip_v4_address_valid("1.2.33333333.4") @@ -45,12 +47,29 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool: >>> is_ip_v4_address_valid("1.2.3.") False + + >>> is_ip_v4_address_valid("1.2.3.05") + False """ - octets = [int(i) for i in ip_v4_address.split(".") if i.isdigit()] - return len(octets) == 4 and all(0 <= int(octet) <= 254 for octet in octets) + octets = ip.split(".") + if len(octets) != 4: + return False + + for octet in octets: + if not octet.isdigit(): + return False + + number = int(octet) + if len(str(number)) != len(octet): + return False + + if not 0 <= number <= 255: + return False + + return True if __name__ == "__main__": ip = input().strip() valid_or_invalid = "valid" if is_ip_v4_address_valid(ip) else "invalid" - print(f"{ip} is a {valid_or_invalid} IP v4 address.") + print(f"{ip} is a {valid_or_invalid} IPv4 address.") diff --git a/maths/is_square_free.py b/maths/is_square_free.py index 4134398d258b..a336c37e8dbc 100644 --- a/maths/is_square_free.py +++ b/maths/is_square_free.py @@ -1,8 +1,9 @@ """ References: wikipedia:square free number -python/black : True -flake8 : True +psf/black : True +ruff : True """ + from __future__ import annotations diff --git a/maths/jaccard_similarity.py b/maths/jaccard_similarity.py index eab25188b2fd..6b6243458fa8 100644 --- a/maths/jaccard_similarity.py +++ b/maths/jaccard_similarity.py @@ -14,7 +14,11 @@ """ -def jaccard_similarity(set_a, set_b, alternative_union=False): +def jaccard_similarity( + set_a: set[str] | list[str] | tuple[str], + set_b: set[str] | list[str] | tuple[str], + alternative_union=False, +): """ Finds the jaccard similarity between two sets. Essentially, its intersection over union. @@ -37,40 +41,52 @@ def jaccard_similarity(set_a, set_b, alternative_union=False): >>> set_b = {'c', 'd', 'e', 'f', 'h', 'i'} >>> jaccard_similarity(set_a, set_b) 0.375 - >>> jaccard_similarity(set_a, set_a) 1.0 - >>> jaccard_similarity(set_a, set_a, True) 0.5 - >>> set_a = ['a', 'b', 'c', 'd', 'e'] >>> set_b = ('c', 'd', 'e', 'f', 'h', 'i') >>> jaccard_similarity(set_a, set_b) 0.375 + >>> set_a = ('c', 'd', 'e', 'f', 'h', 'i') + >>> set_b = ['a', 'b', 'c', 'd', 'e'] + >>> jaccard_similarity(set_a, set_b) + 0.375 + >>> set_a = ('c', 'd', 'e', 'f', 'h', 'i') + >>> set_b = ['a', 'b', 'c', 'd'] + >>> jaccard_similarity(set_a, set_b, True) + 0.2 + >>> set_a = {'a', 'b'} + >>> set_b = ['c', 'd'] + >>> jaccard_similarity(set_a, set_b) + Traceback (most recent call last): + ... + ValueError: Set a and b must either both be sets or be either a list or a tuple. """ if isinstance(set_a, set) and isinstance(set_b, set): - intersection = len(set_a.intersection(set_b)) + intersection_length = len(set_a.intersection(set_b)) if alternative_union: - union = len(set_a) + len(set_b) + union_length = len(set_a) + len(set_b) else: - union = len(set_a.union(set_b)) + union_length = len(set_a.union(set_b)) - return intersection / union + return intersection_length / union_length - if isinstance(set_a, (list, tuple)) and isinstance(set_b, (list, tuple)): + elif isinstance(set_a, (list, tuple)) and isinstance(set_b, (list, tuple)): intersection = [element for element in set_a if element in set_b] if alternative_union: - union = len(set_a) + len(set_b) - return len(intersection) / union + return len(intersection) / (len(set_a) + len(set_b)) else: - union = set_a + [element for element in set_b if element not in set_a] + # Cast set_a to list because tuples cannot be mutated + union = list(set_a) + [element for element in set_b if element not in set_a] return len(intersection) / len(union) - - return len(intersection) / len(union) + raise ValueError( + "Set a and b must either both be sets or be either a list or a tuple." + ) if __name__ == "__main__": diff --git a/maths/joint_probability_distribution.py b/maths/joint_probability_distribution.py new file mode 100644 index 000000000000..6fbcea40c358 --- /dev/null +++ b/maths/joint_probability_distribution.py @@ -0,0 +1,124 @@ +""" +Calculate joint probability distribution +https://en.wikipedia.org/wiki/Joint_probability_distribution +""" + + +def joint_probability_distribution( + x_values: list[int], + y_values: list[int], + x_probabilities: list[float], + y_probabilities: list[float], +) -> dict: + """ + >>> joint_distribution = joint_probability_distribution( + ... [1, 2], [-2, 5, 8], [0.7, 0.3], [0.3, 0.5, 0.2] + ... ) + >>> from math import isclose + >>> isclose(joint_distribution.pop((1, 8)), 0.14) + True + >>> joint_distribution + {(1, -2): 0.21, (1, 5): 0.35, (2, -2): 0.09, (2, 5): 0.15, (2, 8): 0.06} + """ + return { + (x, y): x_prob * y_prob + for x, x_prob in zip(x_values, x_probabilities) + for y, y_prob in zip(y_values, y_probabilities) + } + + +# Function to calculate the expectation (mean) +def expectation(values: list, probabilities: list) -> float: + """ + >>> from math import isclose + >>> isclose(expectation([1, 2], [0.7, 0.3]), 1.3) + True + """ + return sum(x * p for x, p in zip(values, probabilities)) + + +# Function to calculate the variance +def variance(values: list[int], probabilities: list[float]) -> float: + """ + >>> from math import isclose + >>> isclose(variance([1,2],[0.7,0.3]), 0.21) + True + """ + mean = expectation(values, probabilities) + return sum((x - mean) ** 2 * p for x, p in zip(values, probabilities)) + + +# Function to calculate the covariance +def covariance( + x_values: list[int], + y_values: list[int], + x_probabilities: list[float], + y_probabilities: list[float], +) -> float: + """ + >>> covariance([1, 2], [-2, 5, 8], [0.7, 0.3], [0.3, 0.5, 0.2]) + -2.7755575615628914e-17 + """ + mean_x = expectation(x_values, x_probabilities) + mean_y = expectation(y_values, y_probabilities) + return sum( + (x - mean_x) * (y - mean_y) * px * py + for x, px in zip(x_values, x_probabilities) + for y, py in zip(y_values, y_probabilities) + ) + + +# Function to calculate the standard deviation +def standard_deviation(variance: float) -> float: + """ + >>> standard_deviation(0.21) + 0.458257569495584 + """ + return variance**0.5 + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + # Input values for X and Y + x_vals = input("Enter values of X separated by spaces: ").split() + y_vals = input("Enter values of Y separated by spaces: ").split() + + # Convert input values to integers + x_values = [int(x) for x in x_vals] + y_values = [int(y) for y in y_vals] + + # Input probabilities for X and Y + x_probs = input("Enter probabilities for X separated by spaces: ").split() + y_probs = input("Enter probabilities for Y separated by spaces: ").split() + assert len(x_values) == len(x_probs) + assert len(y_values) == len(y_probs) + + # Convert input probabilities to floats + x_probabilities = [float(p) for p in x_probs] + y_probabilities = [float(p) for p in y_probs] + + # Calculate the joint probability distribution + jpd = joint_probability_distribution( + x_values, y_values, x_probabilities, y_probabilities + ) + + # Print the joint probability distribution + print( + "\n".join( + f"P(X={x}, Y={y}) = {probability}" for (x, y), probability in jpd.items() + ) + ) + mean_xy = expectation( + [x * y for x in x_values for y in y_values], + [px * py for px in x_probabilities for py in y_probabilities], + ) + print(f"x mean: {expectation(x_values, x_probabilities) = }") + print(f"y mean: {expectation(y_values, y_probabilities) = }") + print(f"xy mean: {mean_xy}") + print(f"x: {variance(x_values, x_probabilities) = }") + print(f"y: {variance(y_values, y_probabilities) = }") + print(f"{covariance(x_values, y_values, x_probabilities, y_probabilities) = }") + print(f"x: {standard_deviation(variance(x_values, x_probabilities)) = }") + print(f"y: {standard_deviation(variance(y_values, y_probabilities)) = }") diff --git a/maths/josephus_problem.py b/maths/josephus_problem.py new file mode 100644 index 000000000000..271292ba1d9f --- /dev/null +++ b/maths/josephus_problem.py @@ -0,0 +1,130 @@ +""" +The Josephus problem is a famous theoretical problem related to a certain +counting-out game. This module provides functions to solve the Josephus problem +for num_people and a step_size. + +The Josephus problem is defined as follows: +- num_people are standing in a circle. +- Starting with a specified person, you count around the circle, + skipping a fixed number of people (step_size). +- The person at which you stop counting is eliminated from the circle. +- The counting continues until only one person remains. + +For more information about the Josephus problem, refer to: +https://en.wikipedia.org/wiki/Josephus_problem +""" + + +def josephus_recursive(num_people: int, step_size: int) -> int: + """ + Solve the Josephus problem for num_people and a step_size recursively. + + Args: + num_people: A positive integer representing the number of people. + step_size: A positive integer representing the step size for elimination. + + Returns: + The position of the last person remaining. + + Raises: + ValueError: If num_people or step_size is not a positive integer. + + Examples: + >>> josephus_recursive(7, 3) + 3 + >>> josephus_recursive(10, 2) + 4 + >>> josephus_recursive(0, 2) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive(1.9, 2) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive(-2, 2) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive(7, 0) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive(7, -2) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive(1_000, 0.01) + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + >>> josephus_recursive("cat", "dog") + Traceback (most recent call last): + ... + ValueError: num_people or step_size is not a positive integer. + """ + if ( + not isinstance(num_people, int) + or not isinstance(step_size, int) + or num_people <= 0 + or step_size <= 0 + ): + raise ValueError("num_people or step_size is not a positive integer.") + + if num_people == 1: + return 0 + + return (josephus_recursive(num_people - 1, step_size) + step_size) % num_people + + +def find_winner(num_people: int, step_size: int) -> int: + """ + Find the winner of the Josephus problem for num_people and a step_size. + + Args: + num_people (int): Number of people. + step_size (int): Step size for elimination. + + Returns: + int: The position of the last person remaining (1-based index). + + Examples: + >>> find_winner(7, 3) + 4 + >>> find_winner(10, 2) + 5 + """ + return josephus_recursive(num_people, step_size) + 1 + + +def josephus_iterative(num_people: int, step_size: int) -> int: + """ + Solve the Josephus problem for num_people and a step_size iteratively. + + Args: + num_people (int): The number of people in the circle. + step_size (int): The number of steps to take before eliminating someone. + + Returns: + int: The position of the last person standing. + + Examples: + >>> josephus_iterative(5, 2) + 3 + >>> josephus_iterative(7, 3) + 4 + """ + circle = list(range(1, num_people + 1)) + current = 0 + + while len(circle) > 1: + current = (current + step_size - 1) % len(circle) + circle.pop(current) + + return circle[0] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/juggler_sequence.py b/maths/juggler_sequence.py index 9daba8bc0e8a..7f65d1dff925 100644 --- a/maths/juggler_sequence.py +++ b/maths/juggler_sequence.py @@ -40,9 +40,11 @@ def juggler_sequence(number: int) -> list[int]: ValueError: Input value of [number=-1] must be a positive integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 1: - raise ValueError(f"Input value of [number={number}] must be a positive integer") + msg = f"Input value of [number={number}] must be a positive integer" + raise ValueError(msg) sequence = [number] while number != 1: if number % 2 == 0: diff --git a/maths/kadanes.py b/maths/kadanes.py deleted file mode 100644 index c2ea53a6cc84..000000000000 --- a/maths/kadanes.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Kadane's algorithm to get maximum subarray sum -https://medium.com/@rsinghal757/kadanes-algorithm-dynamic-programming-how-and-why-does-it-work-3fd8849ed73d -https://en.wikipedia.org/wiki/Maximum_subarray_problem -""" -test_data: tuple = ([-2, -8, -9], [2, 8, 9], [-1, 0, 1], [0, 0], []) - - -def negative_exist(arr: list) -> int: - """ - >>> negative_exist([-2,-8,-9]) - -2 - >>> [negative_exist(arr) for arr in test_data] - [-2, 0, 0, 0, 0] - """ - arr = arr or [0] - max_number = arr[0] - for i in arr: - if i >= 0: - return 0 - elif max_number <= i: - max_number = i - return max_number - - -def kadanes(arr: list) -> int: - """ - If negative_exist() returns 0 than this function will execute - else it will return the value return by negative_exist function - - For example: arr = [2, 3, -9, 8, -2] - Initially we set value of max_sum to 0 and max_till_element to 0 than when - max_sum is less than max_till particular element it will assign that value to - max_sum and when value of max_till_sum is less than 0 it will assign 0 to i - and after that whole process, return the max_sum - So the output for above arr is 8 - - >>> kadanes([2, 3, -9, 8, -2]) - 8 - >>> [kadanes(arr) for arr in test_data] - [-2, 19, 1, 0, 0] - """ - max_sum = negative_exist(arr) - if max_sum < 0: - return max_sum - - max_sum = 0 - max_till_element = 0 - - for i in arr: - max_till_element += i - max_sum = max(max_sum, max_till_element) - max_till_element = max(max_till_element, 0) - return max_sum - - -if __name__ == "__main__": - try: - print("Enter integer values sepatated by spaces") - arr = [int(x) for x in input().split()] - print(f"Maximum subarray sum of {arr} is {kadanes(arr)}") - except ValueError: - print("Please enter integer values.") diff --git a/maths/karatsuba.py b/maths/karatsuba.py index 4bf4aecdc068..0e063fb44b83 100644 --- a/maths/karatsuba.py +++ b/maths/karatsuba.py @@ -1,7 +1,7 @@ -""" Multiply two numbers using Karatsuba algorithm """ +"""Multiply two numbers using Karatsuba algorithm""" -def karatsuba(a, b): +def karatsuba(a: int, b: int) -> int: """ >>> karatsuba(15463, 23489) == 15463 * 23489 True diff --git a/maths/largest_of_very_large_numbers.py b/maths/largest_of_very_large_numbers.py index d2dc0af18126..edee50371e02 100644 --- a/maths/largest_of_very_large_numbers.py +++ b/maths/largest_of_very_large_numbers.py @@ -4,14 +4,27 @@ def res(x, y): + """ + Reduces large number to a more manageable number + >>> res(5, 7) + 4.892790030352132 + >>> res(0, 5) + 0 + >>> res(3, 0) + 1 + >>> res(-1, 5) + Traceback (most recent call last): + ... + ValueError: math domain error + """ if 0 not in (x, y): # We use the relation x^y = y*log10(x), where 10 is the base. return y * math.log10(x) - else: - if x == 0: # 0 raised to any number is 0 - return 0 - elif y == 0: - return 1 # any number raised to 0 is 1 + elif x == 0: # 0 raised to any number is 0 + return 0 + elif y == 0: + return 1 # any number raised to 0 is 1 + raise AssertionError("This should never happen") if __name__ == "__main__": # Main function diff --git a/maths/largest_subarray_sum.py b/maths/largest_subarray_sum.py deleted file mode 100644 index 90f92c7127bf..000000000000 --- a/maths/largest_subarray_sum.py +++ /dev/null @@ -1,21 +0,0 @@ -from sys import maxsize - - -def max_sub_array_sum(a: list, size: int = 0): - """ - >>> max_sub_array_sum([-13, -3, -25, -20, -3, -16, -23, -12, -5, -22, -15, -4, -7]) - -3 - """ - size = size or len(a) - max_so_far = -maxsize - 1 - max_ending_here = 0 - for i in range(0, size): - max_ending_here = max_ending_here + a[i] - max_so_far = max(max_so_far, max_ending_here) - max_ending_here = max(max_ending_here, 0) - return max_so_far - - -if __name__ == "__main__": - a = [-13, -3, -25, -20, 1, -16, -23, -12, -5, -22, -15, -4, -7] - print(("Maximum contiguous sum is", max_sub_array_sum(a, len(a)))) diff --git a/maths/least_common_multiple.py b/maths/least_common_multiple.py index 621d93720c41..a5c4bf8e3625 100644 --- a/maths/least_common_multiple.py +++ b/maths/least_common_multiple.py @@ -1,6 +1,8 @@ import unittest from timeit import timeit +from maths.greatest_common_divisor import greatest_common_divisor + def least_common_multiple_slow(first_num: int, second_num: int) -> int: """ @@ -20,26 +22,6 @@ def least_common_multiple_slow(first_num: int, second_num: int) -> int: return common_mult -def greatest_common_divisor(a: int, b: int) -> int: - """ - Calculate Greatest Common Divisor (GCD). - see greatest_common_divisor.py - >>> greatest_common_divisor(24, 40) - 8 - >>> greatest_common_divisor(1, 1) - 1 - >>> greatest_common_divisor(1, 800) - 1 - >>> greatest_common_divisor(11, 37) - 1 - >>> greatest_common_divisor(3, 5) - 1 - >>> greatest_common_divisor(16, 4) - 4 - """ - return b if a == 0 else greatest_common_divisor(b % a, a) - - def least_common_multiple_fast(first_num: int, second_num: int) -> int: """ Find the least common multiple of two numbers. @@ -67,7 +49,7 @@ def benchmark(): class TestLeastCommonMultiple(unittest.TestCase): - test_inputs = [ + test_inputs = ( (10, 20), (13, 15), (4, 31), @@ -77,16 +59,16 @@ class TestLeastCommonMultiple(unittest.TestCase): (12, 25), (10, 25), (6, 9), - ] - expected_results = [20, 195, 124, 210, 1462, 60, 300, 50, 18] + ) + expected_results = (20, 195, 124, 210, 1462, 60, 300, 50, 18) def test_lcm_function(self): for i, (first_num, second_num) in enumerate(self.test_inputs): slow_result = least_common_multiple_slow(first_num, second_num) fast_result = least_common_multiple_fast(first_num, second_num) with self.subTest(i=i): - self.assertEqual(slow_result, self.expected_results[i]) - self.assertEqual(fast_result, self.expected_results[i]) + assert slow_result == self.expected_results[i] + assert fast_result == self.expected_results[i] if __name__ == "__main__": diff --git a/maths/line_length.py b/maths/line_length.py index b810f2d9ad1f..ed2efc31e96e 100644 --- a/maths/line_length.py +++ b/maths/line_length.py @@ -5,9 +5,9 @@ def line_length( - fnc: Callable[[int | float], int | float], - x_start: int | float, - x_end: int | float, + fnc: Callable[[float], float], + x_start: float, + x_end: float, steps: int = 100, ) -> float: """ diff --git a/maths/liouville_lambda.py b/maths/liouville_lambda.py index 5993efa42d66..1ed228dd5434 100644 --- a/maths/liouville_lambda.py +++ b/maths/liouville_lambda.py @@ -33,7 +33,8 @@ def liouville_lambda(number: int) -> int: TypeError: Input value of [number=11.0] must be an integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 1: raise ValueError("Input must be a positive integer") return -1 if len(prime_factors(number)) % 2 else 1 diff --git a/maths/lucas_lehmer_primality_test.py b/maths/lucas_lehmer_primality_test.py index 0a5621aacd79..af5c81133044 100644 --- a/maths/lucas_lehmer_primality_test.py +++ b/maths/lucas_lehmer_primality_test.py @@ -1,13 +1,13 @@ """ - In mathematics, the Lucas–Lehmer test (LLT) is a primality test for Mersenne - numbers. https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test +In mathematics, the Lucas-Lehmer test (LLT) is a primality test for Mersenne +numbers. https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test - A Mersenne number is a number that is one less than a power of two. - That is M_p = 2^p - 1 - https://en.wikipedia.org/wiki/Mersenne_prime +A Mersenne number is a number that is one less than a power of two. +That is M_p = 2^p - 1 +https://en.wikipedia.org/wiki/Mersenne_prime - The Lucas–Lehmer test is the primality test used by the - Great Internet Mersenne Prime Search (GIMPS) to locate large primes. +The Lucas-Lehmer test is the primality test used by the +Great Internet Mersenne Prime Search (GIMPS) to locate large primes. """ diff --git a/maths/maclaurin_series.py b/maths/maclaurin_series.py index e55839bc15ba..6ec5551a5e6e 100644 --- a/maths/maclaurin_series.py +++ b/maths/maclaurin_series.py @@ -1,6 +1,7 @@ """ https://en.wikipedia.org/wiki/Taylor_series#Trigonometric_functions """ + from math import factorial, pi @@ -17,13 +18,13 @@ def maclaurin_sin(theta: float, accuracy: int = 30) -> float: >>> all(isclose(maclaurin_sin(x, 50), sin(x)) for x in range(-25, 25)) True >>> maclaurin_sin(10) - -0.544021110889369 + -0.5440211108893691 >>> maclaurin_sin(-10) - 0.5440211108893703 + 0.5440211108893704 >>> maclaurin_sin(10, 15) - -0.5440211108893689 + -0.544021110889369 >>> maclaurin_sin(-10, 15) - 0.5440211108893703 + 0.5440211108893704 >>> maclaurin_sin("10") Traceback (most recent call last): ... @@ -69,11 +70,11 @@ def maclaurin_cos(theta: float, accuracy: int = 30) -> float: >>> all(isclose(maclaurin_cos(x, 50), cos(x)) for x in range(-25, 25)) True >>> maclaurin_cos(5) - 0.28366218546322675 + 0.2836621854632268 >>> maclaurin_cos(-5) - 0.2836621854632266 + 0.2836621854632265 >>> maclaurin_cos(10, 15) - -0.8390715290764525 + -0.8390715290764524 >>> maclaurin_cos(-10, 15) -0.8390715290764521 >>> maclaurin_cos("10") diff --git a/maths/manhattan_distance.py b/maths/manhattan_distance.py index 2711d4c8ccd6..413991468a49 100644 --- a/maths/manhattan_distance.py +++ b/maths/manhattan_distance.py @@ -15,15 +15,15 @@ def manhattan_distance(point_a: list, point_b: list) -> float: 9.0 >>> manhattan_distance([1,1], None) Traceback (most recent call last): - ... + ... ValueError: Missing an input >>> manhattan_distance([1,1], [2, 2, 2]) Traceback (most recent call last): - ... + ... ValueError: Both points must be in the same n-dimensional space >>> manhattan_distance([1,"one"], [2, 2, 2]) Traceback (most recent call last): - ... + ... TypeError: Expected a list of numbers as input, found str >>> manhattan_distance(1, [2, 2, 2]) Traceback (most recent call last): @@ -66,14 +66,14 @@ def _validate_point(point: list[float]) -> None: if isinstance(point, list): for item in point: if not isinstance(item, (int, float)): - raise TypeError( - f"Expected a list of numbers as input, " - f"found {type(item).__name__}" + msg = ( + "Expected a list of numbers as input, found " + f"{type(item).__name__}" ) + raise TypeError(msg) else: - raise TypeError( - f"Expected a list of numbers as input, found {type(point).__name__}" - ) + msg = f"Expected a list of numbers as input, found {type(point).__name__}" + raise TypeError(msg) else: raise ValueError("Missing an input") diff --git a/maths/matrix_exponentiation.py b/maths/matrix_exponentiation.py index 7c37151c87ca..7cdac9d34674 100644 --- a/maths/matrix_exponentiation.py +++ b/maths/matrix_exponentiation.py @@ -39,6 +39,21 @@ def modular_exponentiation(a, b): def fibonacci_with_matrix_exponentiation(n, f1, f2): + """ + Returns the nth number of the Fibonacci sequence that + starts with f1 and f2 + Uses the matrix exponentiation + >>> fibonacci_with_matrix_exponentiation(1, 5, 6) + 5 + >>> fibonacci_with_matrix_exponentiation(2, 10, 11) + 11 + >>> fibonacci_with_matrix_exponentiation(13, 0, 1) + 144 + >>> fibonacci_with_matrix_exponentiation(10, 5, 9) + 411 + >>> fibonacci_with_matrix_exponentiation(9, 2, 3) + 89 + """ # Trivial Cases if n == 1: return f1 @@ -50,21 +65,34 @@ def fibonacci_with_matrix_exponentiation(n, f1, f2): def simple_fibonacci(n, f1, f2): + """ + Returns the nth number of the Fibonacci sequence that + starts with f1 and f2 + Uses the definition + >>> simple_fibonacci(1, 5, 6) + 5 + >>> simple_fibonacci(2, 10, 11) + 11 + >>> simple_fibonacci(13, 0, 1) + 144 + >>> simple_fibonacci(10, 5, 9) + 411 + >>> simple_fibonacci(9, 2, 3) + 89 + """ # Trivial Cases if n == 1: return f1 elif n == 2: return f2 - fn_1 = f1 - fn_2 = f2 n -= 2 while n > 0: - fn_1, fn_2 = fn_1 + fn_2, fn_1 + f2, f1 = f1 + f2, f2 n -= 1 - return fn_1 + return f2 def matrix_exponentiation_time(): diff --git a/maths/max_sum_sliding_window.py b/maths/max_sum_sliding_window.py index c6f9b4ed0ad7..c7492978a6c9 100644 --- a/maths/max_sum_sliding_window.py +++ b/maths/max_sum_sliding_window.py @@ -6,6 +6,7 @@ called 'Window sliding technique' where the nested loops can be converted to a single loop to reduce time complexity. """ + from __future__ import annotations @@ -42,4 +43,6 @@ def max_sum_in_array(array: list[int], k: int) -> int: testmod() array = [randint(-1000, 1000) for i in range(100)] k = randint(0, 110) - print(f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array,k)}") + print( + f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array, k)}" + ) diff --git a/maths/median_of_two_arrays.py b/maths/median_of_two_arrays.py deleted file mode 100644 index 55aa587a9c4b..000000000000 --- a/maths/median_of_two_arrays.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - - -def median_of_two_arrays(nums1: list[float], nums2: list[float]) -> float: - """ - >>> median_of_two_arrays([1, 2], [3]) - 2 - >>> median_of_two_arrays([0, -1.1], [2.5, 1]) - 0.5 - >>> median_of_two_arrays([], [2.5, 1]) - 1.75 - >>> median_of_two_arrays([], [0]) - 0 - >>> median_of_two_arrays([], []) - Traceback (most recent call last): - ... - IndexError: list index out of range - """ - all_numbers = sorted(nums1 + nums2) - div, mod = divmod(len(all_numbers), 2) - if mod == 1: - return all_numbers[div] - else: - return (all_numbers[div] + all_numbers[div - 1]) / 2 - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - array_1 = [float(x) for x in input("Enter the elements of first array: ").split()] - array_2 = [float(x) for x in input("Enter the elements of second array: ").split()] - print(f"The median of two arrays is: {median_of_two_arrays(array_1, array_2)}") diff --git a/maths/miller_rabin.py b/maths/miller_rabin.py deleted file mode 100644 index 9f2668dbab14..000000000000 --- a/maths/miller_rabin.py +++ /dev/null @@ -1,51 +0,0 @@ -import random - -from .binary_exp_mod import bin_exp_mod - - -# This is a probabilistic check to test primality, useful for big numbers! -# if it's a prime, it will return true -# if it's not a prime, the chance of it returning true is at most 1/4**prec -def is_prime_big(n, prec=1000): - """ - >>> from maths.prime_check import is_prime - >>> # all(is_prime_big(i) == is_prime(i) for i in range(1000)) # 3.45s - >>> all(is_prime_big(i) == is_prime(i) for i in range(256)) - True - """ - if n < 2: - return False - - if n % 2 == 0: - return n == 2 - - # this means n is odd - d = n - 1 - exp = 0 - while d % 2 == 0: - d /= 2 - exp += 1 - - # n - 1=d*(2**exp) - count = 0 - while count < prec: - a = random.randint(2, n - 1) - b = bin_exp_mod(a, d, n) - if b != 1: - flag = True - for _ in range(exp): - if b == n - 1: - flag = False - break - b = b * b - b %= n - if flag: - return False - count += 1 - return True - - -if __name__ == "__main__": - n = abs(int(input("Enter bound : ").strip())) - print("Here's the list of primes:") - print(", ".join(str(i) for i in range(n + 1) if is_prime_big(i))) diff --git a/maths/minkowski_distance.py b/maths/minkowski_distance.py new file mode 100644 index 000000000000..99f02e31e417 --- /dev/null +++ b/maths/minkowski_distance.py @@ -0,0 +1,45 @@ +def minkowski_distance( + point_a: list[float], + point_b: list[float], + order: int, +) -> float: + """ + This function calculates the Minkowski distance for a given order between + two n-dimensional points represented as lists. For the case of order = 1, + the Minkowski distance degenerates to the Manhattan distance. For + order = 2, the usual Euclidean distance is obtained. + + https://en.wikipedia.org/wiki/Minkowski_distance + + Note: due to floating point calculation errors the output of this + function may be inaccurate. + + >>> minkowski_distance([1.0, 1.0], [2.0, 2.0], 1) + 2.0 + >>> minkowski_distance([1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], 2) + 8.0 + >>> import numpy as np + >>> bool(np.isclose(5.0, minkowski_distance([5.0], [0.0], 3))) + True + >>> minkowski_distance([1.0], [2.0], -1) + Traceback (most recent call last): + ... + ValueError: The order must be greater than or equal to 1. + >>> minkowski_distance([1.0], [1.0, 2.0], 1) + Traceback (most recent call last): + ... + ValueError: Both points must have the same dimension. + """ + if order < 1: + raise ValueError("The order must be greater than or equal to 1.") + + if len(point_a) != len(point_b): + raise ValueError("Both points must have the same dimension.") + + return sum(abs(a - b) ** order for a, b in zip(point_a, point_b)) ** (1 / order) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/mobius_function.py b/maths/mobius_function.py index 4fcf35f21813..8abdc4cafcb4 100644 --- a/maths/mobius_function.py +++ b/maths/mobius_function.py @@ -1,8 +1,8 @@ """ References: https://en.wikipedia.org/wiki/M%C3%B6bius_function References: wikipedia:square free number -python/black : True -flake8 : True +psf/black : True +ruff : True """ from maths.is_square_free import is_square_free diff --git a/blockchain/modular_division.py b/maths/modular_division.py similarity index 92% rename from blockchain/modular_division.py rename to maths/modular_division.py index a9d0f65c5b27..2f8f4479b27d 100644 --- a/blockchain/modular_division.py +++ b/maths/modular_division.py @@ -9,7 +9,7 @@ def modular_division(a: int, b: int, n: int) -> int: GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) Given three integers a, b, and n, such that gcd(a,n)=1 and n>1, the algorithm should - return an integer x such that 0≤x≤n−1, and b/a=x(modn) (that is, b=ax(modn)). + return an integer x such that 0≤x≤n-1, and b/a=x(modn) (that is, b=ax(modn)). Theorem: a has a multiplicative inverse modulo n iff gcd(a,n) = 1 @@ -28,7 +28,9 @@ def modular_division(a: int, b: int, n: int) -> int: 4 """ - assert n > 1 and a > 0 and greatest_common_divisor(a, n) == 1 + assert n > 1 + assert a > 0 + assert greatest_common_divisor(a, n) == 1 (d, t, s) = extended_gcd(n, a) # Implemented below x = (b * s) % n return x @@ -86,7 +88,8 @@ def extended_gcd(a: int, b: int) -> tuple[int, int, int]: ** extended_gcd function is used when d = gcd(a,b) is required in output """ - assert a >= 0 and b >= 0 + assert a >= 0 + assert b >= 0 if b == 0: d, x, y = a, 1, 0 @@ -95,7 +98,8 @@ def extended_gcd(a: int, b: int) -> tuple[int, int, int]: x = q y = p - q * (a // b) - assert a % d == 0 and b % d == 0 + assert a % d == 0 + assert b % d == 0 assert d == a * x + b * y return (d, x, y) diff --git a/maths/modular_exponential.py b/maths/modular_exponential.py index 42987dbf3a24..a27e29ebc02a 100644 --- a/maths/modular_exponential.py +++ b/maths/modular_exponential.py @@ -1,8 +1,8 @@ """ - Modular Exponential. - Modular exponentiation is a type of exponentiation performed over a modulus. - For more explanation, please check - https://en.wikipedia.org/wiki/Modular_exponentiation +Modular Exponential. +Modular exponentiation is a type of exponentiation performed over a modulus. +For more explanation, please check +https://en.wikipedia.org/wiki/Modular_exponentiation """ """Calculate Modular Exponential.""" diff --git a/maths/monte_carlo.py b/maths/monte_carlo.py index 474f1f65deb4..d174a0b188a2 100644 --- a/maths/monte_carlo.py +++ b/maths/monte_carlo.py @@ -1,6 +1,7 @@ """ @author: MatteoRaso """ + from collections.abc import Callable from math import pi, sqrt from random import uniform diff --git a/maths/newton_raphson.py b/maths/newton_raphson.py deleted file mode 100644 index 2c9cd1de95b0..000000000000 --- a/maths/newton_raphson.py +++ /dev/null @@ -1,53 +0,0 @@ -""" - Author: P Shreyas Shetty - Implementation of Newton-Raphson method for solving equations of kind - f(x) = 0. It is an iterative method where solution is found by the expression - x[n+1] = x[n] + f(x[n])/f'(x[n]) - If no solution exists, then either the solution will not be found when iteration - limit is reached or the gradient f'(x[n]) approaches zero. In both cases, exception - is raised. If iteration limit is reached, try increasing maxiter. - """ -import math as m - - -def calc_derivative(f, a, h=0.001): - """ - Calculates derivative at point a for function f using finite difference - method - """ - return (f(a + h) - f(a - h)) / (2 * h) - - -def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6, logsteps=False): - a = x0 # set the initial guess - steps = [a] - error = abs(f(a)) - f1 = lambda x: calc_derivative(f, x, h=step) # noqa: E731 Derivative of f(x) - for _ in range(maxiter): - if f1(a) == 0: - raise ValueError("No converging solution found") - a = a - f(a) / f1(a) # Calculate the next estimate - if logsteps: - steps.append(a) - if error < maxerror: - break - else: - raise ValueError("Iteration limit reached, no converging solution found") - if logsteps: - # If logstep is true, then log intermediate steps - return a, error, steps - return a, error - - -if __name__ == "__main__": - from matplotlib import pyplot as plt - - f = lambda x: m.tanh(x) ** 2 - m.exp(3 * x) # noqa: E731 - solution, error, steps = newton_raphson( - f, x0=10, maxiter=1000, step=1e-6, logsteps=True - ) - plt.plot([abs(f(x)) for x in steps]) - plt.xlabel("step") - plt.ylabel("error") - plt.show() - print(f"solution = {{{solution:f}}}, error = {{{error:f}}}") diff --git a/maths/number_of_digits.py b/maths/number_of_digits.py index 86bc67f72490..bb9c0d248fd1 100644 --- a/maths/number_of_digits.py +++ b/maths/number_of_digits.py @@ -16,7 +16,15 @@ def num_digits(n: int) -> int: 1 >>> num_digits(-123456) 6 + >>> num_digits('123') # Raises a TypeError for non-integer input + Traceback (most recent call last): + ... + TypeError: Input must be an integer """ + + if not isinstance(n, int): + raise TypeError("Input must be an integer") + digits = 0 n = abs(n) while True: @@ -42,7 +50,15 @@ def num_digits_fast(n: int) -> int: 1 >>> num_digits_fast(-123456) 6 + >>> num_digits('123') # Raises a TypeError for non-integer input + Traceback (most recent call last): + ... + TypeError: Input must be an integer """ + + if not isinstance(n, int): + raise TypeError("Input must be an integer") + return 1 if n == 0 else math.floor(math.log(abs(n), 10) + 1) @@ -61,7 +77,15 @@ def num_digits_faster(n: int) -> int: 1 >>> num_digits_faster(-123456) 6 + >>> num_digits('123') # Raises a TypeError for non-integer input + Traceback (most recent call last): + ... + TypeError: Input must be an integer """ + + if not isinstance(n, int): + raise TypeError("Input must be an integer") + return len(str(abs(n))) diff --git a/maths/numerical_analysis/__init__.py b/maths/numerical_analysis/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/maths/numerical_analysis/adams_bashforth.py b/maths/numerical_analysis/adams_bashforth.py new file mode 100644 index 000000000000..26244a58552f --- /dev/null +++ b/maths/numerical_analysis/adams_bashforth.py @@ -0,0 +1,231 @@ +""" +Use the Adams-Bashforth methods to solve Ordinary Differential Equations. + +https://en.wikipedia.org/wiki/Linear_multistep_method +Author : Ravi Kumar +""" + +from collections.abc import Callable +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class AdamsBashforth: + """ + args: + func: An ordinary differential equation (ODE) as function of x and y. + x_initials: List containing initial required values of x. + y_initials: List containing initial required values of y. + step_size: The increment value of x. + x_final: The final value of x. + + Returns: Solution of y at each nodal point + + >>> def f(x, y): + ... return x + y + >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0.2, 1], 0.2, 1) # doctest: +ELLIPSIS + AdamsBashforth(func=..., x_initials=[0, 0.2, 0.4], y_initials=[0, 0.2, 1], step...) + >>> AdamsBashforth(f, [0, 0.2, 1], [0, 0, 0.04], 0.2, 1).step_2() + Traceback (most recent call last): + ... + ValueError: The final value of x must be greater than the initial values of x. + + >>> AdamsBashforth(f, [0, 0.2, 0.3], [0, 0, 0.04], 0.2, 1).step_3() + Traceback (most recent call last): + ... + ValueError: x-values must be equally spaced according to step size. + + >>> AdamsBashforth(f,[0,0.2,0.4,0.6,0.8],[0,0,0.04,0.128,0.307],-0.2,1).step_5() + Traceback (most recent call last): + ... + ValueError: Step size must be positive. + """ + + func: Callable[[float, float], float] + x_initials: list[float] + y_initials: list[float] + step_size: float + x_final: float + + def __post_init__(self) -> None: + if self.x_initials[-1] >= self.x_final: + raise ValueError( + "The final value of x must be greater than the initial values of x." + ) + + if self.step_size <= 0: + raise ValueError("Step size must be positive.") + + if not all( + round(x1 - x0, 10) == self.step_size + for x0, x1 in zip(self.x_initials, self.x_initials[1:]) + ): + raise ValueError("x-values must be equally spaced according to step size.") + + def step_2(self) -> np.ndarray: + """ + >>> def f(x, y): + ... return x + >>> AdamsBashforth(f, [0, 0.2], [0, 0], 0.2, 1).step_2() + array([0. , 0. , 0.06, 0.16, 0.3 , 0.48]) + + >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_2() + Traceback (most recent call last): + ... + ValueError: Insufficient initial points information. + """ + + if len(self.x_initials) != 2 or len(self.y_initials) != 2: + raise ValueError("Insufficient initial points information.") + + x_0, x_1 = self.x_initials[:2] + y_0, y_1 = self.y_initials[:2] + + n = int((self.x_final - x_1) / self.step_size) + y = np.zeros(n + 2) + y[0] = y_0 + y[1] = y_1 + + for i in range(n): + y[i + 2] = y[i + 1] + (self.step_size / 2) * ( + 3 * self.func(x_1, y[i + 1]) - self.func(x_0, y[i]) + ) + x_0 = x_1 + x_1 += self.step_size + + return y + + def step_3(self) -> np.ndarray: + """ + >>> def f(x, y): + ... return x + y + >>> y = AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_3() + >>> float(y[3]) + 0.15533333333333332 + + >>> AdamsBashforth(f, [0, 0.2], [0, 0], 0.2, 1).step_3() + Traceback (most recent call last): + ... + ValueError: Insufficient initial points information. + """ + if len(self.x_initials) != 3 or len(self.y_initials) != 3: + raise ValueError("Insufficient initial points information.") + + x_0, x_1, x_2 = self.x_initials[:3] + y_0, y_1, y_2 = self.y_initials[:3] + + n = int((self.x_final - x_2) / self.step_size) + y = np.zeros(n + 4) + y[0] = y_0 + y[1] = y_1 + y[2] = y_2 + + for i in range(n + 1): + y[i + 3] = y[i + 2] + (self.step_size / 12) * ( + 23 * self.func(x_2, y[i + 2]) + - 16 * self.func(x_1, y[i + 1]) + + 5 * self.func(x_0, y[i]) + ) + x_0 = x_1 + x_1 = x_2 + x_2 += self.step_size + + return y + + def step_4(self) -> np.ndarray: + """ + >>> def f(x,y): + ... return x + y + >>> y = AdamsBashforth( + ... f, [0, 0.2, 0.4, 0.6], [0, 0, 0.04, 0.128], 0.2, 1).step_4() + >>> float(y[4]) + 0.30699999999999994 + >>> float(y[5]) + 0.5771083333333333 + + >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_4() + Traceback (most recent call last): + ... + ValueError: Insufficient initial points information. + """ + + if len(self.x_initials) != 4 or len(self.y_initials) != 4: + raise ValueError("Insufficient initial points information.") + + x_0, x_1, x_2, x_3 = self.x_initials[:4] + y_0, y_1, y_2, y_3 = self.y_initials[:4] + + n = int((self.x_final - x_3) / self.step_size) + y = np.zeros(n + 4) + y[0] = y_0 + y[1] = y_1 + y[2] = y_2 + y[3] = y_3 + + for i in range(n): + y[i + 4] = y[i + 3] + (self.step_size / 24) * ( + 55 * self.func(x_3, y[i + 3]) + - 59 * self.func(x_2, y[i + 2]) + + 37 * self.func(x_1, y[i + 1]) + - 9 * self.func(x_0, y[i]) + ) + x_0 = x_1 + x_1 = x_2 + x_2 = x_3 + x_3 += self.step_size + + return y + + def step_5(self) -> np.ndarray: + """ + >>> def f(x,y): + ... return x + y + >>> y = AdamsBashforth( + ... f, [0, 0.2, 0.4, 0.6, 0.8], [0, 0.02140, 0.02140, 0.22211, 0.42536], + ... 0.2, 1).step_5() + >>> float(y[-1]) + 0.05436839444444452 + + >>> AdamsBashforth(f, [0, 0.2, 0.4], [0, 0, 0.04], 0.2, 1).step_5() + Traceback (most recent call last): + ... + ValueError: Insufficient initial points information. + """ + + if len(self.x_initials) != 5 or len(self.y_initials) != 5: + raise ValueError("Insufficient initial points information.") + + x_0, x_1, x_2, x_3, x_4 = self.x_initials[:5] + y_0, y_1, y_2, y_3, y_4 = self.y_initials[:5] + + n = int((self.x_final - x_4) / self.step_size) + y = np.zeros(n + 6) + y[0] = y_0 + y[1] = y_1 + y[2] = y_2 + y[3] = y_3 + y[4] = y_4 + + for i in range(n + 1): + y[i + 5] = y[i + 4] + (self.step_size / 720) * ( + 1901 * self.func(x_4, y[i + 4]) + - 2774 * self.func(x_3, y[i + 3]) + - 2616 * self.func(x_2, y[i + 2]) + - 1274 * self.func(x_1, y[i + 1]) + + 251 * self.func(x_0, y[i]) + ) + x_0 = x_1 + x_1 = x_2 + x_2 = x_3 + x_3 = x_4 + x_4 += self.step_size + + return y + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/arithmetic_analysis/bisection.py b/maths/numerical_analysis/bisection.py similarity index 100% rename from arithmetic_analysis/bisection.py rename to maths/numerical_analysis/bisection.py diff --git a/maths/bisection.py b/maths/numerical_analysis/bisection_2.py similarity index 97% rename from maths/bisection.py rename to maths/numerical_analysis/bisection_2.py index 45f26d8d88e4..68ba6577ce29 100644 --- a/maths/bisection.py +++ b/maths/numerical_analysis/bisection_2.py @@ -1,5 +1,5 @@ """ -Given a function on floating number f(x) and two floating numbers ‘a’ and ‘b’ such that +Given a function on floating number f(x) and two floating numbers `a` and `b` such that f(a) * f(b) < 0 and f(x) is continuous in [a, b]. Here f(x) represents algebraic or transcendental equation. Find root of function in interval [a, b] (Or find a value of x such that f(x) is 0) diff --git a/maths/integration_by_simpson_approx.py b/maths/numerical_analysis/integration_by_simpson_approx.py similarity index 91% rename from maths/integration_by_simpson_approx.py rename to maths/numerical_analysis/integration_by_simpson_approx.py index f77ae76135ee..043f3a9a72af 100644 --- a/maths/integration_by_simpson_approx.py +++ b/maths/numerical_analysis/integration_by_simpson_approx.py @@ -4,7 +4,7 @@ Purpose : You have one function f(x) which takes float integer and returns float you have to integrate the function in limits a to b. -The approximation proposed by Thomas Simpsons in 1743 is one way to calculate +The approximation proposed by Thomas Simpson in 1743 is one way to calculate integration. ( read article : https://cp-algorithms.com/num_methods/simpson-integration.html ) @@ -88,18 +88,18 @@ def simpson_integration(function, a: float, b: float, precision: int = 4) -> flo AssertionError: precision should be positive integer your input : -1 """ - assert callable( - function - ), f"the function(object) passed should be callable your input : {function}" + assert callable(function), ( + f"the function(object) passed should be callable your input : {function}" + ) assert isinstance(a, (float, int)), f"a should be float or integer your input : {a}" assert isinstance(function(a), (float, int)), ( "the function should return integer or float return type of your function, " f"{type(a)}" ) assert isinstance(b, (float, int)), f"b should be float or integer your input : {b}" - assert ( - isinstance(precision, int) and precision > 0 - ), f"precision should be positive integer your input : {precision}" + assert isinstance(precision, int) and precision > 0, ( + f"precision should be positive integer your input : {precision}" + ) # just applying the formula of simpson for approximate integration written in # mentioned article in first comment of this file and above this function diff --git a/arithmetic_analysis/intersection.py b/maths/numerical_analysis/intersection.py similarity index 95% rename from arithmetic_analysis/intersection.py rename to maths/numerical_analysis/intersection.py index 826c0ead0a00..325abeaca996 100644 --- a/arithmetic_analysis/intersection.py +++ b/maths/numerical_analysis/intersection.py @@ -42,6 +42,11 @@ def intersection(function: Callable[[float], float], x0: float, x1: float) -> fl def f(x: float) -> float: + """ + function is f(x) = x^3 - 2x - 5 + >>> f(2) + -1.0 + """ return math.pow(x, 3) - (2 * x) - 5 diff --git a/maths/nevilles_method.py b/maths/numerical_analysis/nevilles_method.py similarity index 80% rename from maths/nevilles_method.py rename to maths/numerical_analysis/nevilles_method.py index 1f48b43fbd22..25c93ac6c531 100644 --- a/maths/nevilles_method.py +++ b/maths/numerical_analysis/nevilles_method.py @@ -1,11 +1,11 @@ """ - Python program to show how to interpolate and evaluate a polynomial - using Neville's method. - Neville’s method evaluates a polynomial that passes through a - given set of x and y points for a particular x value (x0) using the - Newton polynomial form. - Reference: - https://rpubs.com/aaronsc32/nevilles-method-polynomial-interpolation +Python program to show how to interpolate and evaluate a polynomial +using Neville's method. +Neville's method evaluates a polynomial that passes through a +given set of x and y points for a particular x value (x0) using the +Newton polynomial form. +Reference: + https://rpubs.com/aaronsc32/nevilles-method-polynomial-interpolation """ diff --git a/arithmetic_analysis/newton_forward_interpolation.py b/maths/numerical_analysis/newton_forward_interpolation.py similarity index 100% rename from arithmetic_analysis/newton_forward_interpolation.py rename to maths/numerical_analysis/newton_forward_interpolation.py diff --git a/maths/numerical_analysis/newton_raphson.py b/maths/numerical_analysis/newton_raphson.py new file mode 100644 index 000000000000..10fb244bf426 --- /dev/null +++ b/maths/numerical_analysis/newton_raphson.py @@ -0,0 +1,114 @@ +""" +The Newton-Raphson method (aka the Newton method) is a root-finding algorithm that +approximates a root of a given real-valued function f(x). It is an iterative method +given by the formula + +x_{n + 1} = x_n + f(x_n) / f'(x_n) + +with the precision of the approximation increasing as the number of iterations increase. + +Reference: https://en.wikipedia.org/wiki/Newton%27s_method +""" + +from collections.abc import Callable + +RealFunc = Callable[[float], float] + + +def calc_derivative(f: RealFunc, x: float, delta_x: float = 1e-3) -> float: + """ + Approximate the derivative of a function f(x) at a point x using the finite + difference method + + >>> import math + >>> tolerance = 1e-5 + >>> derivative = calc_derivative(lambda x: x**2, 2) + >>> math.isclose(derivative, 4, abs_tol=tolerance) + True + >>> derivative = calc_derivative(math.sin, 0) + >>> math.isclose(derivative, 1, abs_tol=tolerance) + True + """ + return (f(x + delta_x / 2) - f(x - delta_x / 2)) / delta_x + + +def newton_raphson( + f: RealFunc, + x0: float = 0, + max_iter: int = 100, + step: float = 1e-6, + max_error: float = 1e-6, + log_steps: bool = False, +) -> tuple[float, float, list[float]]: + """ + Find a root of the given function f using the Newton-Raphson method. + + :param f: A real-valued single-variable function + :param x0: Initial guess + :param max_iter: Maximum number of iterations + :param step: Step size of x, used to approximate f'(x) + :param max_error: Maximum approximation error + :param log_steps: bool denoting whether to log intermediate steps + + :return: A tuple containing the approximation, the error, and the intermediate + steps. If log_steps is False, then an empty list is returned for the third + element of the tuple. + + :raises ZeroDivisionError: The derivative approaches 0. + :raises ArithmeticError: No solution exists, or the solution isn't found before the + iteration limit is reached. + + >>> import math + >>> tolerance = 1e-15 + >>> root, *_ = newton_raphson(lambda x: x**2 - 5*x + 2, 0.4, max_error=tolerance) + >>> math.isclose(root, (5 - math.sqrt(17)) / 2, abs_tol=tolerance) + True + >>> root, *_ = newton_raphson(lambda x: math.log(x) - 1, 2, max_error=tolerance) + >>> math.isclose(root, math.e, abs_tol=tolerance) + True + >>> root, *_ = newton_raphson(math.sin, 1, max_error=tolerance) + >>> math.isclose(root, 0, abs_tol=tolerance) + True + >>> newton_raphson(math.cos, 0) + Traceback (most recent call last): + ... + ZeroDivisionError: No converging solution found, zero derivative + >>> newton_raphson(lambda x: x**2 + 1, 2) + Traceback (most recent call last): + ... + ArithmeticError: No converging solution found, iteration limit reached + """ + + def f_derivative(x: float) -> float: + return calc_derivative(f, x, step) + + a = x0 # Set initial guess + steps = [] + for _ in range(max_iter): + if log_steps: # Log intermediate steps + steps.append(a) + + error = abs(f(a)) + if error < max_error: + return a, error, steps + + if f_derivative(a) == 0: + raise ZeroDivisionError("No converging solution found, zero derivative") + a -= f(a) / f_derivative(a) # Calculate next estimate + raise ArithmeticError("No converging solution found, iteration limit reached") + + +if __name__ == "__main__": + import doctest + from math import exp, tanh + + doctest.testmod() + + def func(x: float) -> float: + return tanh(x) ** 2 - exp(3 * x) + + solution, err, steps = newton_raphson( + func, x0=10, max_iter=100, step=1e-6, log_steps=True + ) + print(f"{solution=}, {err=}") + print("\n".join(str(x) for x in steps)) diff --git a/maths/numerical_integration.py b/maths/numerical_analysis/numerical_integration.py similarity index 94% rename from maths/numerical_integration.py rename to maths/numerical_analysis/numerical_integration.py index f2d65f89e390..f64436ec48c1 100644 --- a/maths/numerical_integration.py +++ b/maths/numerical_analysis/numerical_integration.py @@ -1,15 +1,16 @@ """ Approximates the area under the curve using the trapezoidal rule """ + from __future__ import annotations from collections.abc import Callable def trapezoidal_area( - fnc: Callable[[int | float], int | float], - x_start: int | float, - x_end: int | float, + fnc: Callable[[float], float], + x_start: float, + x_end: float, steps: int = 100, ) -> float: """ diff --git a/maths/numerical_analysis/proper_fractions.py b/maths/numerical_analysis/proper_fractions.py new file mode 100644 index 000000000000..774ce9a24876 --- /dev/null +++ b/maths/numerical_analysis/proper_fractions.py @@ -0,0 +1,40 @@ +from math import gcd + + +def proper_fractions(denominator: int) -> list[str]: + """ + this algorithm returns a list of proper fractions, in the + range between 0 and 1, which can be formed with the given denominator + https://en.wikipedia.org/wiki/Fraction#Proper_and_improper_fractions + + >>> proper_fractions(10) + ['1/10', '3/10', '7/10', '9/10'] + >>> proper_fractions(5) + ['1/5', '2/5', '3/5', '4/5'] + >>> proper_fractions(-15) + Traceback (most recent call last): + ... + ValueError: The Denominator Cannot be less than 0 + >>> proper_fractions(0) + [] + >>> proper_fractions(1.2) + Traceback (most recent call last): + ... + ValueError: The Denominator must be an integer + """ + + if denominator < 0: + raise ValueError("The Denominator Cannot be less than 0") + elif isinstance(denominator, float): + raise ValueError("The Denominator must be an integer") + return [ + f"{numerator}/{denominator}" + for numerator in range(1, denominator) + if gcd(numerator, denominator) == 1 + ] + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/runge_kutta.py b/maths/numerical_analysis/runge_kutta.py similarity index 97% rename from maths/runge_kutta.py rename to maths/numerical_analysis/runge_kutta.py index 4cac017ee89e..3a25b0fb0173 100644 --- a/maths/runge_kutta.py +++ b/maths/numerical_analysis/runge_kutta.py @@ -19,7 +19,7 @@ def runge_kutta(f, y0, x0, h, x_end): ... return y >>> y0 = 1 >>> y = runge_kutta(f, y0, 0.0, 0.01, 5) - >>> y[-1] + >>> float(y[-1]) 148.41315904125113 """ n = int(np.ceil((x_end - x0) / h)) diff --git a/maths/numerical_analysis/runge_kutta_fehlberg_45.py b/maths/numerical_analysis/runge_kutta_fehlberg_45.py new file mode 100644 index 000000000000..0fbd60a35c1a --- /dev/null +++ b/maths/numerical_analysis/runge_kutta_fehlberg_45.py @@ -0,0 +1,114 @@ +""" +Use the Runge-Kutta-Fehlberg method to solve Ordinary Differential Equations. +""" + +from collections.abc import Callable + +import numpy as np + + +def runge_kutta_fehlberg_45( + func: Callable, + x_initial: float, + y_initial: float, + step_size: float, + x_final: float, +) -> np.ndarray: + """ + Solve an Ordinary Differential Equations using Runge-Kutta-Fehlberg Method (rkf45) + of order 5. + + https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta%E2%80%93Fehlberg_method + + args: + func: An ordinary differential equation (ODE) as function of x and y. + x_initial: The initial value of x. + y_initial: The initial value of y. + step_size: The increment value of x. + x_final: The final value of x. + + Returns: + Solution of y at each nodal point + + # exact value of y[1] is tan(0.2) = 0.2027100937470787 + >>> def f(x, y): + ... return 1 + y**2 + >>> y = runge_kutta_fehlberg_45(f, 0, 0, 0.2, 1) + >>> float(y[1]) + 0.2027100937470787 + >>> def f(x,y): + ... return x + >>> y = runge_kutta_fehlberg_45(f, -1, 0, 0.2, 0) + >>> float(y[1]) + -0.18000000000000002 + >>> y = runge_kutta_fehlberg_45(5, 0, 0, 0.1, 1) + Traceback (most recent call last): + ... + TypeError: 'int' object is not callable + >>> def f(x, y): + ... return x + y + >>> y = runge_kutta_fehlberg_45(f, 0, 0, 0.2, -1) + Traceback (most recent call last): + ... + ValueError: The final value of x must be greater than initial value of x. + >>> def f(x, y): + ... return x + >>> y = runge_kutta_fehlberg_45(f, -1, 0, -0.2, 0) + Traceback (most recent call last): + ... + ValueError: Step size must be positive. + """ + if x_initial >= x_final: + raise ValueError( + "The final value of x must be greater than initial value of x." + ) + + if step_size <= 0: + raise ValueError("Step size must be positive.") + + n = int((x_final - x_initial) / step_size) + y = np.zeros( + (n + 1), + ) + x = np.zeros(n + 1) + y[0] = y_initial + x[0] = x_initial + for i in range(n): + k1 = step_size * func(x[i], y[i]) + k2 = step_size * func(x[i] + step_size / 4, y[i] + k1 / 4) + k3 = step_size * func( + x[i] + (3 / 8) * step_size, y[i] + (3 / 32) * k1 + (9 / 32) * k2 + ) + k4 = step_size * func( + x[i] + (12 / 13) * step_size, + y[i] + (1932 / 2197) * k1 - (7200 / 2197) * k2 + (7296 / 2197) * k3, + ) + k5 = step_size * func( + x[i] + step_size, + y[i] + (439 / 216) * k1 - 8 * k2 + (3680 / 513) * k3 - (845 / 4104) * k4, + ) + k6 = step_size * func( + x[i] + step_size / 2, + y[i] + - (8 / 27) * k1 + + 2 * k2 + - (3544 / 2565) * k3 + + (1859 / 4104) * k4 + - (11 / 40) * k5, + ) + y[i + 1] = ( + y[i] + + (16 / 135) * k1 + + (6656 / 12825) * k3 + + (28561 / 56430) * k4 + - (9 / 50) * k5 + + (2 / 55) * k6 + ) + x[i + 1] = step_size + x[i] + return y + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/numerical_analysis/runge_kutta_gills.py b/maths/numerical_analysis/runge_kutta_gills.py new file mode 100644 index 000000000000..5d9672679813 --- /dev/null +++ b/maths/numerical_analysis/runge_kutta_gills.py @@ -0,0 +1,90 @@ +""" +Use the Runge-Kutta-Gill's method of order 4 to solve Ordinary Differential Equations. + +https://www.geeksforgeeks.org/gills-4th-order-method-to-solve-differential-equations/ +Author : Ravi Kumar +""" + +from collections.abc import Callable +from math import sqrt + +import numpy as np + + +def runge_kutta_gills( + func: Callable[[float, float], float], + x_initial: float, + y_initial: float, + step_size: float, + x_final: float, +) -> np.ndarray: + """ + Solve an Ordinary Differential Equations using Runge-Kutta-Gills Method of order 4. + + args: + func: An ordinary differential equation (ODE) as function of x and y. + x_initial: The initial value of x. + y_initial: The initial value of y. + step_size: The increment value of x. + x_final: The final value of x. + + Returns: + Solution of y at each nodal point + + >>> def f(x, y): + ... return (x-y)/2 + >>> y = runge_kutta_gills(f, 0, 3, 0.2, 5) + >>> float(y[-1]) + 3.4104259225717537 + + >>> def f(x,y): + ... return x + >>> y = runge_kutta_gills(f, -1, 0, 0.2, 0) + >>> y + array([ 0. , -0.18, -0.32, -0.42, -0.48, -0.5 ]) + + >>> def f(x, y): + ... return x + y + >>> y = runge_kutta_gills(f, 0, 0, 0.2, -1) + Traceback (most recent call last): + ... + ValueError: The final value of x must be greater than initial value of x. + + >>> def f(x, y): + ... return x + >>> y = runge_kutta_gills(f, -1, 0, -0.2, 0) + Traceback (most recent call last): + ... + ValueError: Step size must be positive. + """ + if x_initial >= x_final: + raise ValueError( + "The final value of x must be greater than initial value of x." + ) + + if step_size <= 0: + raise ValueError("Step size must be positive.") + + n = int((x_final - x_initial) / step_size) + y = np.zeros(n + 1) + y[0] = y_initial + for i in range(n): + k1 = step_size * func(x_initial, y[i]) + k2 = step_size * func(x_initial + step_size / 2, y[i] + k1 / 2) + k3 = step_size * func( + x_initial + step_size / 2, + y[i] + (-0.5 + 1 / sqrt(2)) * k1 + (1 - 1 / sqrt(2)) * k2, + ) + k4 = step_size * func( + x_initial + step_size, y[i] - (1 / sqrt(2)) * k2 + (1 + 1 / sqrt(2)) * k3 + ) + + y[i + 1] = y[i] + (k1 + (2 - sqrt(2)) * k2 + (2 + sqrt(2)) * k3 + k4) / 6 + x_initial += step_size + return y + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/arithmetic_analysis/secant_method.py b/maths/numerical_analysis/secant_method.py similarity index 94% rename from arithmetic_analysis/secant_method.py rename to maths/numerical_analysis/secant_method.py index d28a46206d40..9fff8222cdde 100644 --- a/arithmetic_analysis/secant_method.py +++ b/maths/numerical_analysis/secant_method.py @@ -2,6 +2,7 @@ Implementing Secant method in Python Author: dimgrichr """ + from math import exp @@ -20,7 +21,7 @@ def secant_method(lower_bound: float, upper_bound: float, repeats: int) -> float """ x0 = lower_bound x1 = upper_bound - for _ in range(0, repeats): + for _ in range(repeats): x0, x1 = x1, x1 - (f(x1) * (x1 - x0)) / (f(x1) - f(x0)) return x1 diff --git a/maths/numerical_analysis/simpson_rule.py b/maths/numerical_analysis/simpson_rule.py new file mode 100644 index 000000000000..e75fb557a2f5 --- /dev/null +++ b/maths/numerical_analysis/simpson_rule.py @@ -0,0 +1,86 @@ +""" +Numerical integration or quadrature for a smooth function f with known values at x_i + +This method is the classical approach of summing 'Equally Spaced Abscissas' + +method 2: +"Simpson Rule" + +""" + + +def method_2(boundary: list[int], steps: int) -> float: + # "Simpson Rule" + # int(f) = delta_x/2 * (b-a)/3*(f1 + 4f2 + 2f_3 + ... + fn) + """ + Calculate the definite integral of a function using Simpson's Rule. + :param boundary: A list containing the lower and upper bounds of integration. + :param steps: The number of steps or resolution for the integration. + :return: The approximate integral value. + + >>> round(method_2([0, 2, 4], 10), 10) + 2.6666666667 + >>> round(method_2([2, 0], 10), 10) + -0.2666666667 + >>> round(method_2([-2, -1], 10), 10) + 2.172 + >>> round(method_2([0, 1], 10), 10) + 0.3333333333 + >>> round(method_2([0, 2], 10), 10) + 2.6666666667 + >>> round(method_2([0, 2], 100), 10) + 2.5621226667 + >>> round(method_2([0, 1], 1000), 10) + 0.3320026653 + >>> round(method_2([0, 2], 0), 10) + Traceback (most recent call last): + ... + ZeroDivisionError: Number of steps must be greater than zero + >>> round(method_2([0, 2], -10), 10) + Traceback (most recent call last): + ... + ZeroDivisionError: Number of steps must be greater than zero + """ + if steps <= 0: + raise ZeroDivisionError("Number of steps must be greater than zero") + + h = (boundary[1] - boundary[0]) / steps + a = boundary[0] + b = boundary[1] + x_i = make_points(a, b, h) + y = 0.0 + y += (h / 3.0) * f(a) + cnt = 2 + for i in x_i: + y += (h / 3) * (4 - 2 * (cnt % 2)) * f(i) + cnt += 1 + y += (h / 3.0) * f(b) + return y + + +def make_points(a, b, h): + x = a + h + while x < (b - h): + yield x + x = x + h + + +def f(x): # enter your function here + y = (x - 0) * (x - 0) + return y + + +def main(): + a = 0.0 # Lower bound of integration + b = 1.0 # Upper bound of integration + steps = 10.0 # number of steps or resolution + boundary = [a, b] # boundary of integration + y = method_2(boundary, steps) + print(f"y = {y}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/maths/square_root.py b/maths/numerical_analysis/square_root.py similarity index 81% rename from maths/square_root.py rename to maths/numerical_analysis/square_root.py index 2cbf14beae18..4462ccb75261 100644 --- a/maths/square_root.py +++ b/maths/numerical_analysis/square_root.py @@ -19,14 +19,13 @@ def get_initial_point(a: float) -> float: def square_root_iterative( - a: float, max_iter: int = 9999, tolerance: float = 0.00000000000001 + a: float, max_iter: int = 9999, tolerance: float = 1e-14 ) -> float: """ - Square root is aproximated using Newtons method. + Square root approximated using Newton's method. https://en.wikipedia.org/wiki/Newton%27s_method - >>> all(abs(square_root_iterative(i)-math.sqrt(i)) <= .00000000000001 - ... for i in range(500)) + >>> all(abs(square_root_iterative(i) - math.sqrt(i)) <= 1e-14 for i in range(500)) True >>> square_root_iterative(-1) diff --git a/maths/odd_sieve.py b/maths/odd_sieve.py new file mode 100644 index 000000000000..06605ca54296 --- /dev/null +++ b/maths/odd_sieve.py @@ -0,0 +1,42 @@ +from itertools import compress, repeat +from math import ceil, sqrt + + +def odd_sieve(num: int) -> list[int]: + """ + Returns the prime numbers < `num`. The prime numbers are calculated using an + odd sieve implementation of the Sieve of Eratosthenes algorithm + (see for reference https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes). + + >>> odd_sieve(2) + [] + >>> odd_sieve(3) + [2] + >>> odd_sieve(10) + [2, 3, 5, 7] + >>> odd_sieve(20) + [2, 3, 5, 7, 11, 13, 17, 19] + """ + + if num <= 2: + return [] + if num == 3: + return [2] + + # Odd sieve for numbers in range [3, num - 1] + sieve = bytearray(b"\x01") * ((num >> 1) - 1) + + for i in range(3, int(sqrt(num)) + 1, 2): + if sieve[(i >> 1) - 1]: + i_squared = i**2 + sieve[(i_squared >> 1) - 1 :: i] = repeat( + 0, ceil((num - i_squared) / (i << 1)) + ) + + return [2, *list(compress(range(3, num, 2), sieve))] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/perfect_cube.py b/maths/perfect_cube.py index 9ad287e41e75..a732b7cce6c8 100644 --- a/maths/perfect_cube.py +++ b/maths/perfect_cube.py @@ -11,6 +11,45 @@ def perfect_cube(n: int) -> bool: return (val * val * val) == n +def perfect_cube_binary_search(n: int) -> bool: + """ + Check if a number is a perfect cube or not using binary search. + Time complexity : O(Log(n)) + Space complexity: O(1) + + >>> perfect_cube_binary_search(27) + True + >>> perfect_cube_binary_search(64) + True + >>> perfect_cube_binary_search(4) + False + >>> perfect_cube_binary_search("a") + Traceback (most recent call last): + ... + TypeError: perfect_cube_binary_search() only accepts integers + >>> perfect_cube_binary_search(0.1) + Traceback (most recent call last): + ... + TypeError: perfect_cube_binary_search() only accepts integers + """ + if not isinstance(n, int): + raise TypeError("perfect_cube_binary_search() only accepts integers") + if n < 0: + n = -n + left = 0 + right = n + while left <= right: + mid = left + (right - left) // 2 + if mid * mid * mid == n: + return True + elif mid * mid * mid < n: + left = mid + 1 + else: + right = mid - 1 + return False + + if __name__ == "__main__": - print(perfect_cube(27)) - print(perfect_cube(4)) + import doctest + + doctest.testmod() diff --git a/maths/perfect_number.py b/maths/perfect_number.py index 148e988fb4c5..52c816cc7895 100644 --- a/maths/perfect_number.py +++ b/maths/perfect_number.py @@ -14,21 +14,73 @@ def perfect(number: int) -> bool: """ + Check if a number is a perfect number. + + A perfect number is a positive integer that is equal to the sum of its proper + divisors (excluding itself). + + Args: + number: The number to be checked. + + Returns: + True if the number is a perfect number otherwise, False. + Start from 1 because dividing by 0 will raise ZeroDivisionError. + A number at most can be divisible by the half of the number except the number + itself. For example, 6 is at most can be divisible by 3 except by 6 itself. + Examples: >>> perfect(27) False >>> perfect(28) True >>> perfect(29) False - - Start from 1 because dividing by 0 will raise ZeroDivisionError. - A number at most can be divisible by the half of the number except the number - itself. For example, 6 is at most can be divisible by 3 except by 6 itself. + >>> perfect(6) + True + >>> perfect(12) + False + >>> perfect(496) + True + >>> perfect(8128) + True + >>> perfect(0) + False + >>> perfect(-1) + False + >>> perfect(33550336) # Large perfect number + True + >>> perfect(33550337) # Just above a large perfect number + False + >>> perfect(1) # Edge case: 1 is not a perfect number + False + >>> perfect("123") # String representation of a number + Traceback (most recent call last): + ... + ValueError: number must be an integer + >>> perfect(12.34) + Traceback (most recent call last): + ... + ValueError: number must be an integer + >>> perfect("Hello") + Traceback (most recent call last): + ... + ValueError: number must be an integer """ + if not isinstance(number, int): + raise ValueError("number must be an integer") + if number <= 0: + return False return sum(i for i in range(1, number // 2 + 1) if number % i == 0) == number if __name__ == "__main__": + from doctest import testmod + + testmod() print("Program to check whether a number is a Perfect number or not...") - number = int(input("Enter number: ").strip()) + try: + number = int(input("Enter a positive integer: ").strip()) + except ValueError: + msg = "number must be an integer" + raise ValueError(msg) + print(f"{number} is {'' if perfect(number) else 'not '}a Perfect Number.") diff --git a/maths/persistence.py b/maths/persistence.py index 607641e67200..c61a69a7c27d 100644 --- a/maths/persistence.py +++ b/maths/persistence.py @@ -28,7 +28,7 @@ def multiplicative_persistence(num: int) -> int: numbers = [int(i) for i in num_string] total = 1 - for i in range(0, len(numbers)): + for i in range(len(numbers)): total *= numbers[i] num_string = str(total) @@ -67,7 +67,7 @@ def additive_persistence(num: int) -> int: numbers = [int(i) for i in num_string] total = 0 - for i in range(0, len(numbers)): + for i in range(len(numbers)): total += numbers[i] num_string = str(total) diff --git a/maths/pi_generator.py b/maths/pi_generator.py new file mode 100644 index 000000000000..97f2c540c1ce --- /dev/null +++ b/maths/pi_generator.py @@ -0,0 +1,87 @@ +def calculate_pi(limit: int) -> str: + """ + https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80 + Leibniz Formula for Pi + + The Leibniz formula is the special case arctan(1) = pi / 4. + Leibniz's formula converges extremely slowly: it exhibits sublinear convergence. + + Convergence (https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80#Convergence) + + We cannot try to prove against an interrupted, uncompleted generation. + https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80#Unusual_behaviour + The errors can in fact be predicted, but those calculations also approach infinity + for accuracy. + + Our output will be a string so that we can definitely store all digits. + + >>> import math + >>> float(calculate_pi(15)) == math.pi + True + + Since we cannot predict errors or interrupt any infinite alternating series + generation since they approach infinity, or interrupt any alternating series, we'll + need math.isclose() + + >>> math.isclose(float(calculate_pi(50)), math.pi) + True + >>> math.isclose(float(calculate_pi(100)), math.pi) + True + + Since math.pi contains only 16 digits, here are some tests with known values: + + >>> calculate_pi(50) + '3.14159265358979323846264338327950288419716939937510' + >>> calculate_pi(80) + '3.14159265358979323846264338327950288419716939937510582097494459230781640628620899' + """ + # Variables used for the iteration process + q = 1 + r = 0 + t = 1 + k = 1 + n = 3 + m = 3 + + decimal = limit + counter = 0 + + result = "" + + # We can't compare against anything if we make a generator, + # so we'll stick with plain return logic + while counter != decimal + 1: + if 4 * q + r - t < n * t: + result += str(n) + if counter == 0: + result += "." + + if decimal == counter: + break + + counter += 1 + nr = 10 * (r - n * t) + n = ((10 * (3 * q + r)) // t) - 10 * n + q *= 10 + r = nr + else: + nr = (2 * q + r) * m + nn = (q * (7 * k) + 2 + (r * m)) // (t * m) + q *= k + t *= m + m += 2 + k += 1 + n = nn + r = nr + return result + + +def main() -> None: + print(f"{calculate_pi(50) = }") + import doctest + + doctest.testmod() + + +if __name__ == "__main__": + main() diff --git a/maths/points_are_collinear_3d.py b/maths/points_are_collinear_3d.py index 3bc0b3b9ebe5..c7adddda9494 100644 --- a/maths/points_are_collinear_3d.py +++ b/maths/points_are_collinear_3d.py @@ -76,9 +76,9 @@ def get_3d_vectors_cross(ab: Vector3d, ac: Vector3d) -> Vector3d: def is_zero_vector(vector: Vector3d, accuracy: int) -> bool: """ - Check if vector is equal to (0, 0, 0) of not. + Check if vector is equal to (0, 0, 0) or not. - Sine the algorithm is very accurate, we will never get a zero vector, + Since the algorithm is very accurate, we will never get a zero vector, so we need to round the vector axis, because we want a result that is either True or False. In other applications, we can return a float that represents the collinearity ratio. @@ -97,9 +97,9 @@ def are_collinear(a: Point3d, b: Point3d, c: Point3d, accuracy: int = 10) -> boo """ Check if three points are collinear or not. - 1- Create tow vectors AB and AC. - 2- Get the cross vector of the tow vectors. - 3- Calcolate the length of the cross vector. + 1- Create two vectors AB and AC. + 2- Get the cross vector of the two vectors. + 3- Calculate the length of the cross vector. 4- If the length is zero then the points are collinear, else they are not. The use of the accuracy parameter is explained in is_zero_vector docstring. diff --git a/maths/pollard_rho.py b/maths/pollard_rho.py index 5082f54f71a8..e8bc89cef6c5 100644 --- a/maths/pollard_rho.py +++ b/maths/pollard_rho.py @@ -94,14 +94,13 @@ def rand_fn(value: int, step: int, modulus: int) -> int: if divisor == 1: # No common divisor yet, just keep searching. continue + # We found a common divisor! + elif divisor == num: + # Unfortunately, the divisor is ``num`` itself and is useless. + break else: - # We found a common divisor! - if divisor == num: - # Unfortunately, the divisor is ``num`` itself and is useless. - break - else: - # The divisor is a nontrivial factor of ``num``! - return divisor + # The divisor is a nontrivial factor of ``num``! + return divisor # If we made it here, then this attempt failed. # We need to pick a new starting seed for the tortoise and hare diff --git a/maths/polynomials/single_indeterminate_operations.py b/maths/polynomials/single_indeterminate_operations.py index 8bafdb591793..e31e6caa3988 100644 --- a/maths/polynomials/single_indeterminate_operations.py +++ b/maths/polynomials/single_indeterminate_operations.py @@ -87,7 +87,7 @@ def __mul__(self, polynomial_2: Polynomial) -> Polynomial: return Polynomial(self.degree + polynomial_2.degree, coefficients) - def evaluate(self, substitution: int | float) -> int | float: + def evaluate(self, substitution: float) -> float: """ Evaluates the polynomial at x. >>> p = Polynomial(2, [1, 2, 3]) @@ -144,7 +144,7 @@ def derivative(self) -> Polynomial: coefficients[i] = self.coefficients[i + 1] * (i + 1) return Polynomial(self.degree - 1, coefficients) - def integral(self, constant: int | float = 0) -> Polynomial: + def integral(self, constant: float = 0) -> Polynomial: """ Returns the integral of the polynomial. >>> p = Polynomial(2, [1, 2, 3]) diff --git a/maths/power_using_recursion.py b/maths/power_using_recursion.py index f82097f6d8ec..eb775b161ae8 100644 --- a/maths/power_using_recursion.py +++ b/maths/power_using_recursion.py @@ -15,18 +15,45 @@ def power(base: int, exponent: int) -> float: """ - power(3, 4) + Calculate the power of a base raised to an exponent. + + >>> power(3, 4) 81 >>> power(2, 0) 1 >>> all(power(base, exponent) == pow(base, exponent) ... for base in range(-10, 10) for exponent in range(10)) True + >>> power('a', 1) + 'a' + >>> power('a', 2) + Traceback (most recent call last): + ... + TypeError: can't multiply sequence by non-int of type 'str' + >>> power('a', 'b') + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for -: 'str' and 'int' + >>> power(2, -1) + Traceback (most recent call last): + ... + RecursionError: maximum recursion depth exceeded + >>> power(0, 0) + 1 + >>> power(0, 1) + 0 + >>> power(5,6) + 15625 + >>> power(23, 12) + 21914624432020321 """ return base * power(base, (exponent - 1)) if exponent else 1 if __name__ == "__main__": + from doctest import testmod + + testmod() print("Raise base to the power of exponent using recursion...") base = int(input("Enter the base: ").strip()) exponent = int(input("Enter the exponent: ").strip()) diff --git a/maths/prime_check.py b/maths/prime_check.py index 80ab8bc5d2cd..a757c4108f24 100644 --- a/maths/prime_check.py +++ b/maths/prime_check.py @@ -3,6 +3,8 @@ import math import unittest +import pytest + def is_prime(number: int) -> bool: """Checks to see if a number is a prime in O(sqrt(n)). @@ -27,12 +29,19 @@ def is_prime(number: int) -> bool: True >>> is_prime(67483) False + >>> is_prime(16.1) + Traceback (most recent call last): + ... + ValueError: is_prime() only accepts positive integers + >>> is_prime(-4) + Traceback (most recent call last): + ... + ValueError: is_prime() only accepts positive integers """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and positive" + if not isinstance(number, int) or not number >= 0: + raise ValueError("is_prime() only accepts positive integers") if 1 < number < 4: # 2 and 3 are primes @@ -50,33 +59,31 @@ def is_prime(number: int) -> bool: class Test(unittest.TestCase): def test_primes(self): - self.assertTrue(is_prime(2)) - self.assertTrue(is_prime(3)) - self.assertTrue(is_prime(5)) - self.assertTrue(is_prime(7)) - self.assertTrue(is_prime(11)) - self.assertTrue(is_prime(13)) - self.assertTrue(is_prime(17)) - self.assertTrue(is_prime(19)) - self.assertTrue(is_prime(23)) - self.assertTrue(is_prime(29)) + assert is_prime(2) + assert is_prime(3) + assert is_prime(5) + assert is_prime(7) + assert is_prime(11) + assert is_prime(13) + assert is_prime(17) + assert is_prime(19) + assert is_prime(23) + assert is_prime(29) def test_not_primes(self): - with self.assertRaises(AssertionError): + with pytest.raises(ValueError): is_prime(-19) - self.assertFalse( - is_prime(0), - "Zero doesn't have any positive factors, primes must have exactly two.", + assert not is_prime(0), ( + "Zero doesn't have any positive factors, primes must have exactly two." ) - self.assertFalse( - is_prime(1), - "One only has 1 positive factor, primes must have exactly two.", + assert not is_prime(1), ( + "One only has 1 positive factor, primes must have exactly two." ) - self.assertFalse(is_prime(2 * 2)) - self.assertFalse(is_prime(2 * 3)) - self.assertFalse(is_prime(3 * 3)) - self.assertFalse(is_prime(3 * 5)) - self.assertFalse(is_prime(3 * 5 * 7)) + assert not is_prime(2 * 2) + assert not is_prime(2 * 3) + assert not is_prime(3 * 3) + assert not is_prime(3 * 5) + assert not is_prime(3 * 5 * 7) if __name__ == "__main__": diff --git a/maths/prime_factors.py b/maths/prime_factors.py index e520ae3a6d04..47abcf10e618 100644 --- a/maths/prime_factors.py +++ b/maths/prime_factors.py @@ -1,6 +1,7 @@ """ python/black : True """ + from __future__ import annotations diff --git a/maths/prime_numbers.py b/maths/prime_numbers.py index c5297ed9264c..5ad12baf3dc3 100644 --- a/maths/prime_numbers.py +++ b/maths/prime_numbers.py @@ -2,7 +2,7 @@ from collections.abc import Generator -def slow_primes(max_n: int) -> Generator[int, None, None]: +def slow_primes(max_n: int) -> Generator[int]: """ Return a list of all primes numbers up to max. >>> list(slow_primes(0)) @@ -17,8 +17,8 @@ def slow_primes(max_n: int) -> Generator[int, None, None]: [2, 3, 5, 7, 11] >>> list(slow_primes(33)) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] - >>> list(slow_primes(10000))[-1] - 9973 + >>> list(slow_primes(1000))[-1] + 997 """ numbers: Generator = (i for i in range(1, (max_n + 1))) for i in (n for n in numbers if n > 1): @@ -29,7 +29,7 @@ def slow_primes(max_n: int) -> Generator[int, None, None]: yield i -def primes(max_n: int) -> Generator[int, None, None]: +def primes(max_n: int) -> Generator[int]: """ Return a list of all primes numbers up to max. >>> list(primes(0)) @@ -44,8 +44,8 @@ def primes(max_n: int) -> Generator[int, None, None]: [2, 3, 5, 7, 11] >>> list(primes(33)) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] - >>> list(primes(10000))[-1] - 9973 + >>> list(primes(1000))[-1] + 997 """ numbers: Generator = (i for i in range(1, (max_n + 1))) for i in (n for n in numbers if n > 1): @@ -58,7 +58,7 @@ def primes(max_n: int) -> Generator[int, None, None]: yield i -def fast_primes(max_n: int) -> Generator[int, None, None]: +def fast_primes(max_n: int) -> Generator[int]: """ Return a list of all primes numbers up to max. >>> list(fast_primes(0)) @@ -73,8 +73,8 @@ def fast_primes(max_n: int) -> Generator[int, None, None]: [2, 3, 5, 7, 11] >>> list(fast_primes(33)) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] - >>> list(fast_primes(10000))[-1] - 9973 + >>> list(fast_primes(1000))[-1] + 997 """ numbers: Generator = (i for i in range(1, (max_n + 1), 2)) # It's useless to test even numbers as they will not be prime diff --git a/maths/primelib.py b/maths/primelib.py index 81d5737063f0..3a966e5cd936 100644 --- a/maths/primelib.py +++ b/maths/primelib.py @@ -21,7 +21,6 @@ is_even(number) is_odd(number) -gcd(number1, number2) // greatest common divisor kg_v(number1, number2) // least common multiple get_divisors(number) // all divisors of 'number' inclusive 1, number is_perfect_number(number) @@ -40,17 +39,36 @@ from math import sqrt +from maths.greatest_common_divisor import gcd_by_iterative + def is_prime(number: int) -> bool: """ input: positive integer 'number' returns true if 'number' is prime otherwise false. - """ - # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and positive" + >>> is_prime(3) + True + >>> is_prime(10) + False + >>> is_prime(97) + True + >>> is_prime(9991) + False + >>> is_prime(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and positive + >>> is_prime("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and positive + """ + + # precondition + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and positive" + ) status = True @@ -82,6 +100,16 @@ def sieve_er(n): This function implements the algorithm called sieve of erathostenes. + >>> sieve_er(8) + [2, 3, 5, 7] + >>> sieve_er(-1) + Traceback (most recent call last): + ... + AssertionError: 'N' must been an int and > 2 + >>> sieve_er("test") + Traceback (most recent call last): + ... + AssertionError: 'N' must been an int and > 2 """ # precondition @@ -115,6 +143,17 @@ def get_prime_numbers(n): input: positive integer 'N' > 2 returns a list of prime numbers from 2 up to N (inclusive) This function is more efficient as function 'sieveEr(...)' + + >>> get_prime_numbers(8) + [2, 3, 5, 7] + >>> get_prime_numbers(-1) + Traceback (most recent call last): + ... + AssertionError: 'N' must been an int and > 2 + >>> get_prime_numbers("test") + Traceback (most recent call last): + ... + AssertionError: 'N' must been an int and > 2 """ # precondition @@ -141,6 +180,21 @@ def prime_factorization(number): """ input: positive integer 'number' returns a list of the prime number factors of 'number' + + >>> prime_factorization(0) + [0] + >>> prime_factorization(8) + [2, 2, 2] + >>> prime_factorization(287) + [7, 41] + >>> prime_factorization(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 + >>> prime_factorization("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 """ # precondition @@ -154,7 +208,7 @@ def prime_factorization(number): quotient = number - if number == 0 or number == 1: + if number in {0, 1}: ans.append(number) # if 'number' not prime then builds the prime factorization of 'number' @@ -182,12 +236,27 @@ def greatest_prime_factor(number): """ input: positive integer 'number' >= 0 returns the greatest prime number factor of 'number' + + >>> greatest_prime_factor(0) + 0 + >>> greatest_prime_factor(8) + 2 + >>> greatest_prime_factor(287) + 41 + >>> greatest_prime_factor(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 + >>> greatest_prime_factor("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' bust been an int and >= 0" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and >= 0" + ) ans = 0 @@ -209,12 +278,27 @@ def smallest_prime_factor(number): """ input: integer 'number' >= 0 returns the smallest prime number factor of 'number' + + >>> smallest_prime_factor(0) + 0 + >>> smallest_prime_factor(8) + 2 + >>> smallest_prime_factor(287) + 7 + >>> smallest_prime_factor(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 + >>> smallest_prime_factor("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 0 """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' bust been an int and >= 0" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and >= 0" + ) ans = 0 @@ -236,11 +320,24 @@ def is_even(number): """ input: integer 'number' returns true if 'number' is even, otherwise false. + + >>> is_even(0) + True + >>> is_even(8) + True + >>> is_even(287) + False + >>> is_even(-1) + False + >>> is_even("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int """ # precondition assert isinstance(number, int), "'number' must been an int" - assert isinstance(number % 2 == 0, bool), "compare bust been from type bool" + assert isinstance(number % 2 == 0, bool), "compare must been from type bool" return number % 2 == 0 @@ -252,11 +349,24 @@ def is_odd(number): """ input: integer 'number' returns true if 'number' is odd, otherwise false. + + >>> is_odd(0) + False + >>> is_odd(8) + False + >>> is_odd(287) + True + >>> is_odd(-1) + True + >>> is_odd("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int """ # precondition assert isinstance(number, int), "'number' must been an int" - assert isinstance(number % 2 != 0, bool), "compare bust been from type bool" + assert isinstance(number % 2 != 0, bool), "compare must been from type bool" return number % 2 != 0 @@ -269,12 +379,29 @@ def goldbach(number): Goldbach's assumption input: a even positive integer 'number' > 2 returns a list of two prime numbers whose sum is equal to 'number' - """ - # precondition - assert ( - isinstance(number, int) and (number > 2) and is_even(number) - ), "'number' must been an int, even and > 2" + >>> goldbach(8) + [3, 5] + >>> goldbach(824) + [3, 821] + >>> goldbach(0) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int, even and > 2 + >>> goldbach(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int, even and > 2 + >>> goldbach("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int, even and > 2 + """ + + # precondition + assert isinstance(number, int) and (number > 2) and is_even(number), ( + "'number' must been an int, even and > 2" + ) ans = [] # this list will returned @@ -317,44 +444,30 @@ def goldbach(number): # ---------------------------------------------- -def gcd(number1, number2): - """ - Greatest common divisor - input: two positive integer 'number1' and 'number2' - returns the greatest common divisor of 'number1' and 'number2' - """ - - # precondition - assert ( - isinstance(number1, int) - and isinstance(number2, int) - and (number1 >= 0) - and (number2 >= 0) - ), "'number1' and 'number2' must been positive integer." - - rest = 0 - - while number2 != 0: - rest = number1 % number2 - number1 = number2 - number2 = rest - - # precondition - assert isinstance(number1, int) and ( - number1 >= 0 - ), "'number' must been from type int and positive" - - return number1 - - -# ---------------------------------------------------- - - def kg_v(number1, number2): """ Least common multiple input: two positive integer 'number1' and 'number2' returns the least common multiple of 'number1' and 'number2' + + >>> kg_v(8,10) + 40 + >>> kg_v(824,67) + 55208 + >>> kg_v(1, 10) + 10 + >>> kg_v(0) + Traceback (most recent call last): + ... + TypeError: kg_v() missing 1 required positional argument: 'number2' + >>> kg_v(10,-1) + Traceback (most recent call last): + ... + AssertionError: 'number1' and 'number2' must been positive integer. + >>> kg_v("test","test2") + Traceback (most recent call last): + ... + AssertionError: 'number1' and 'number2' must been positive integer. """ # precondition @@ -412,9 +525,9 @@ def kg_v(number1, number2): done.append(n) # precondition - assert isinstance(ans, int) and ( - ans >= 0 - ), "'ans' must been from type int and positive" + assert isinstance(ans, int) and (ans >= 0), ( + "'ans' must been from type int and positive" + ) return ans @@ -427,6 +540,21 @@ def get_prime(n): Gets the n-th prime number. input: positive integer 'n' >= 0 returns the n-th prime number, beginning at index 0 + + >>> get_prime(0) + 2 + >>> get_prime(8) + 23 + >>> get_prime(824) + 6337 + >>> get_prime(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been a positive int + >>> get_prime("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been a positive int """ # precondition @@ -446,9 +574,9 @@ def get_prime(n): ans += 1 # precondition - assert isinstance(ans, int) and is_prime( - ans - ), "'ans' must been a prime number and from type int" + assert isinstance(ans, int) and is_prime(ans), ( + "'ans' must been a prime number and from type int" + ) return ans @@ -462,6 +590,25 @@ def get_primes_between(p_number_1, p_number_2): pNumber1 < pNumber2 returns a list of all prime numbers between 'pNumber1' (exclusive) and 'pNumber2' (exclusive) + + >>> get_primes_between(3, 67) + [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61] + >>> get_primes_between(0) + Traceback (most recent call last): + ... + TypeError: get_primes_between() missing 1 required positional argument: 'p_number_2' + >>> get_primes_between(0, 1) + Traceback (most recent call last): + ... + AssertionError: The arguments must been prime numbers and 'pNumber1' < 'pNumber2' + >>> get_primes_between(-1, 3) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and positive + >>> get_primes_between("test","test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and positive """ # precondition @@ -505,6 +652,19 @@ def get_divisors(n): """ input: positive integer 'n' >= 1 returns all divisors of n (inclusive 1 and 'n') + + >>> get_divisors(8) + [1, 2, 4, 8] + >>> get_divisors(824) + [1, 2, 4, 8, 103, 206, 412, 824] + >>> get_divisors(-1) + Traceback (most recent call last): + ... + AssertionError: 'n' must been int and >= 1 + >>> get_divisors("test") + Traceback (most recent call last): + ... + AssertionError: 'n' must been int and >= 1 """ # precondition @@ -529,12 +689,25 @@ def is_perfect_number(number): """ input: positive integer 'number' > 1 returns true if 'number' is a perfect number otherwise false. + + >>> is_perfect_number(28) + True + >>> is_perfect_number(824) + False + >>> is_perfect_number(-1) + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 1 + >>> is_perfect_number("test") + Traceback (most recent call last): + ... + AssertionError: 'number' must been an int and >= 1 """ # precondition - assert isinstance(number, int) and ( - number > 1 - ), "'number' must been an int and >= 1" + assert isinstance(number, int) and (number > 1), ( + "'number' must been an int and >= 1" + ) divisors = get_divisors(number) @@ -557,6 +730,15 @@ def simplify_fraction(numerator, denominator): input: two integer 'numerator' and 'denominator' assumes: 'denominator' != 0 returns: a tuple with simplify numerator and denominator. + + >>> simplify_fraction(10, 20) + (1, 2) + >>> simplify_fraction(10, -1) + (10, -1) + >>> simplify_fraction("test","test") + Traceback (most recent call last): + ... + AssertionError: The arguments must been from type int and 'denominator' != 0 """ # precondition @@ -567,14 +749,14 @@ def simplify_fraction(numerator, denominator): ), "The arguments must been from type int and 'denominator' != 0" # build the greatest common divisor of numerator and denominator. - gcd_of_fraction = gcd(abs(numerator), abs(denominator)) + gcd_of_fraction = gcd_by_iterative(abs(numerator), abs(denominator)) # precondition assert ( isinstance(gcd_of_fraction, int) and (numerator % gcd_of_fraction == 0) and (denominator % gcd_of_fraction == 0) - ), "Error in function gcd(...,...)" + ), "Error in function gcd_by_iterative(...,...)" return (numerator // gcd_of_fraction, denominator // gcd_of_fraction) @@ -586,6 +768,19 @@ def factorial(n): """ input: positive integer 'n' returns the factorial of 'n' (n!) + + >>> factorial(0) + 1 + >>> factorial(20) + 2432902008176640000 + >>> factorial(-1) + Traceback (most recent call last): + ... + AssertionError: 'n' must been a int and >= 0 + >>> factorial("test") + Traceback (most recent call last): + ... + AssertionError: 'n' must been a int and >= 0 """ # precondition @@ -602,10 +797,27 @@ def factorial(n): # ------------------------------------------------------------------- -def fib(n): +def fib(n: int) -> int: """ input: positive integer 'n' returns the n-th fibonacci term , indexing by 0 + + >>> fib(0) + 1 + >>> fib(5) + 8 + >>> fib(20) + 10946 + >>> fib(99) + 354224848179261915075 + >>> fib(-1) + Traceback (most recent call last): + ... + AssertionError: 'n' must been an int and >= 0 + >>> fib("test") + Traceback (most recent call last): + ... + AssertionError: 'n' must been an int and >= 0 """ # precondition @@ -621,3 +833,9 @@ def fib(n): fib1 = tmp return ans + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/pythagoras.py b/maths/pythagoras.py index 69a17731a0fd..7770e981d44d 100644 --- a/maths/pythagoras.py +++ b/maths/pythagoras.py @@ -14,17 +14,13 @@ def __repr__(self) -> str: def distance(a: Point, b: Point) -> float: - return math.sqrt(abs((b.x - a.x) ** 2 + (b.y - a.y) ** 2 + (b.z - a.z) ** 2)) - - -def test_distance() -> None: """ >>> point1 = Point(2, -1, 7) >>> point2 = Point(1, -3, 5) >>> print(f"Distance from {point1} to {point2} is {distance(point1, point2)}") Distance from Point(2, -1, 7) to Point(1, -3, 5) is 3.0 """ - pass + return math.sqrt(abs((b.x - a.x) ** 2 + (b.y - a.y) ** 2 + (b.z - a.z) ** 2)) if __name__ == "__main__": diff --git a/maths/qr_decomposition.py b/maths/qr_decomposition.py index a8414fbece87..670b49206aa7 100644 --- a/maths/qr_decomposition.py +++ b/maths/qr_decomposition.py @@ -1,7 +1,7 @@ import numpy as np -def qr_householder(a): +def qr_householder(a: np.ndarray): """Return a QR-decomposition of the matrix A using Householder reflection. The QR-decomposition decomposes the matrix A of shape (m, n) into an diff --git a/maths/radians.py b/maths/radians.py index 465467a3ba08..b8ac61cb135c 100644 --- a/maths/radians.py +++ b/maths/radians.py @@ -3,7 +3,7 @@ def radians(degree: float) -> float: """ - Coverts the given angle from degrees to radians + Converts the given angle from degrees to radians https://en.wikipedia.org/wiki/Radian >>> radians(180) @@ -16,7 +16,7 @@ def radians(degree: float) -> float: 1.9167205845401725 >>> from math import radians as math_radians - >>> all(abs(radians(i)-math_radians(i)) <= 0.00000001 for i in range(-2, 361)) + >>> all(abs(radians(i) - math_radians(i)) <= 1e-8 for i in range(-2, 361)) True """ diff --git a/maths/radix2_fft.py b/maths/radix2_fft.py index 1def58e1f226..d41dc82d5588 100644 --- a/maths/radix2_fft.py +++ b/maths/radix2_fft.py @@ -80,14 +80,10 @@ def __init__(self, poly_a=None, poly_b=None): # Discrete fourier transform of A and B def __dft(self, which): - if which == "A": - dft = [[x] for x in self.polyA] - else: - dft = [[x] for x in self.polyB] + dft = [[x] for x in self.polyA] if which == "A" else [[x] for x in self.polyB] # Corner case if len(dft) <= 1: return dft[0] - # next_ncol = self.c_max_length // 2 while next_ncol > 0: new_dft = [[] for i in range(next_ncol)] @@ -170,7 +166,7 @@ def __str__(self): f"{coef}*x^{i}" for coef, i in enumerate(self.product) ) - return "\n".join((a, b, c)) + return f"{a}\n{b}\n{c}" # Unit tests diff --git a/maths/remove_digit.py b/maths/remove_digit.py new file mode 100644 index 000000000000..db14ac902a6f --- /dev/null +++ b/maths/remove_digit.py @@ -0,0 +1,37 @@ +def remove_digit(num: int) -> int: + """ + + returns the biggest possible result + that can be achieved by removing + one digit from the given number + + >>> remove_digit(152) + 52 + >>> remove_digit(6385) + 685 + >>> remove_digit(-11) + 1 + >>> remove_digit(2222222) + 222222 + >>> remove_digit("2222222") + Traceback (most recent call last): + TypeError: only integers accepted as input + >>> remove_digit("string input") + Traceback (most recent call last): + TypeError: only integers accepted as input + """ + + if not isinstance(num, int): + raise TypeError("only integers accepted as input") + else: + num_str = str(abs(num)) + num_transpositions = [list(num_str) for char in range(len(num_str))] + for index in range(len(num_str)): + num_transpositions[index].pop(index) + return max( + int("".join(list(transposition))) for transposition in num_transpositions + ) + + +if __name__ == "__main__": + __import__("doctest").testmod() diff --git a/maths/segmented_sieve.py b/maths/segmented_sieve.py index e950a83b752a..125390edc588 100644 --- a/maths/segmented_sieve.py +++ b/maths/segmented_sieve.py @@ -4,7 +4,36 @@ def sieve(n: int) -> list[int]: - """Segmented Sieve.""" + """ + Segmented Sieve. + + Examples: + >>> sieve(8) + [2, 3, 5, 7] + + >>> sieve(27) + [2, 3, 5, 7, 11, 13, 17, 19, 23] + + >>> sieve(0) + Traceback (most recent call last): + ... + ValueError: Number 0 must instead be a positive integer + + >>> sieve(-1) + Traceback (most recent call last): + ... + ValueError: Number -1 must instead be a positive integer + + >>> sieve(22.2) + Traceback (most recent call last): + ... + ValueError: Number 22.2 must instead be a positive integer + """ + + if n <= 0 or isinstance(n, float): + msg = f"Number {n} must instead be a positive integer" + raise ValueError(msg) + in_prime = [] start = 2 end = int(math.sqrt(n)) # Size of every segment @@ -42,4 +71,9 @@ def sieve(n: int) -> list[int]: return prime -print(sieve(10**6)) +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(f"{sieve(10**6) = }") diff --git a/maths/series/geometric_series.py b/maths/series/geometric_series.py index 90c9fe77b733..55c42fd90e99 100644 --- a/maths/series/geometric_series.py +++ b/maths/series/geometric_series.py @@ -9,15 +9,14 @@ python3 geometric_series.py """ - from __future__ import annotations def geometric_series( - nth_term: float | int, - start_term_a: float | int, - common_ratio_r: float | int, -) -> list[float | int]: + nth_term: float, + start_term_a: float, + common_ratio_r: float, +) -> list[float]: """ Pure Python implementation of Geometric Series algorithm @@ -48,7 +47,7 @@ def geometric_series( """ if not all((nth_term, start_term_a, common_ratio_r)): return [] - series: list[float | int] = [] + series: list[float] = [] power = 1 multiple = common_ratio_r for _ in range(int(nth_term)): diff --git a/maths/series/harmonic.py b/maths/series/harmonic.py index 50f29c93dd5f..35792d38af9b 100644 --- a/maths/series/harmonic.py +++ b/maths/series/harmonic.py @@ -45,7 +45,7 @@ def is_harmonic_series(series: list) -> bool: return True rec_series = [] series_len = len(series) - for i in range(0, series_len): + for i in range(series_len): if series[i] == 0: raise ValueError("Input series cannot have 0 as an element") rec_series.append(1 / series[i]) diff --git a/maths/series/p_series.py b/maths/series/p_series.py index 34fa3f2399af..93812f443857 100644 --- a/maths/series/p_series.py +++ b/maths/series/p_series.py @@ -9,11 +9,10 @@ python3 p_series.py """ - from __future__ import annotations -def p_series(nth_term: int | float | str, power: int | float | str) -> list[str]: +def p_series(nth_term: float | str, power: float | str) -> list[str]: """ Pure Python implementation of P-Series algorithm :return: The P-Series starting from 1 to last (nth) term diff --git a/maths/sieve_of_eratosthenes.py b/maths/sieve_of_eratosthenes.py index 3cd6ce0b4d9d..3923dc3e1612 100644 --- a/maths/sieve_of_eratosthenes.py +++ b/maths/sieve_of_eratosthenes.py @@ -10,6 +10,7 @@ doctest provider: Bruno Simas Hadlich (https://github.com/brunohadlich) Also thanks to Dmitry (https://github.com/LizardWizzard) for finding the problem """ + from __future__ import annotations import math @@ -34,7 +35,8 @@ def prime_sieve(num: int) -> list[int]: """ if num <= 0: - raise ValueError(f"{num}: Invalid input, please enter a positive integer.") + msg = f"{num}: Invalid input, please enter a positive integer." + raise ValueError(msg) sieve = [True] * (num + 1) prime = [] diff --git a/maths/sigmoid.py b/maths/sigmoid.py index 147588e8871f..cb45bde2702c 100644 --- a/maths/sigmoid.py +++ b/maths/sigmoid.py @@ -11,7 +11,7 @@ import numpy as np -def sigmoid(vector: np.array) -> np.array: +def sigmoid(vector: np.ndarray) -> np.ndarray: """ Implements the sigmoid function diff --git a/maths/signum.py b/maths/signum.py index 148f931767c1..c89753e76637 100644 --- a/maths/signum.py +++ b/maths/signum.py @@ -7,12 +7,29 @@ def signum(num: float) -> int: """ Applies signum function on the number + Custom test cases: >>> signum(-10) -1 >>> signum(10) 1 >>> signum(0) 0 + >>> signum(-20.5) + -1 + >>> signum(20.5) + 1 + >>> signum(-1e-6) + -1 + >>> signum(1e-6) + 1 + >>> signum("Hello") + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + >>> signum([]) + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'list' and 'int' """ if num < 0: return -1 @@ -22,10 +39,17 @@ def signum(num: float) -> int: def test_signum() -> None: """ Tests the signum function + >>> test_signum() """ assert signum(5) == 1 assert signum(-5) == -1 assert signum(0) == 0 + assert signum(10.5) == 1 + assert signum(-10.5) == -1 + assert signum(1e-6) == 1 + assert signum(-1e-6) == -1 + assert signum(123456789) == 1 + assert signum(-123456789) == -1 if __name__ == "__main__": diff --git a/maths/simpson_rule.py b/maths/simpson_rule.py deleted file mode 100644 index d66dc39a7171..000000000000 --- a/maths/simpson_rule.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Numerical integration or quadrature for a smooth function f with known values at x_i - -This method is the classical approach of suming 'Equally Spaced Abscissas' - -method 2: -"Simpson Rule" - -""" - - -def method_2(boundary, steps): - # "Simpson Rule" - # int(f) = delta_x/2 * (b-a)/3*(f1 + 4f2 + 2f_3 + ... + fn) - h = (boundary[1] - boundary[0]) / steps - a = boundary[0] - b = boundary[1] - x_i = make_points(a, b, h) - y = 0.0 - y += (h / 3.0) * f(a) - cnt = 2 - for i in x_i: - y += (h / 3) * (4 - 2 * (cnt % 2)) * f(i) - cnt += 1 - y += (h / 3.0) * f(b) - return y - - -def make_points(a, b, h): - x = a + h - while x < (b - h): - yield x - x = x + h - - -def f(x): # enter your function here - y = (x - 0) * (x - 0) - return y - - -def main(): - a = 0.0 # Lower bound of integration - b = 1.0 # Upper bound of integration - steps = 10.0 # define number of steps or resolution - boundary = [a, b] # define boundary of integration - y = method_2(boundary, steps) - print(f"y = {y}") - - -if __name__ == "__main__": - main() diff --git a/maths/simultaneous_linear_equation_solver.py b/maths/simultaneous_linear_equation_solver.py new file mode 100644 index 000000000000..9685a33e82fe --- /dev/null +++ b/maths/simultaneous_linear_equation_solver.py @@ -0,0 +1,142 @@ +""" +https://en.wikipedia.org/wiki/Augmented_matrix + +This algorithm solves simultaneous linear equations of the form +λa + λb + λc + λd + ... = y as [λ, λ, λ, λ, ..., y] +Where λ & y are individual coefficients, the no. of equations = no. of coefficients - 1 + +Note in order to work there must exist 1 equation where all instances of λ and y != 0 +""" + + +def simplify(current_set: list[list]) -> list[list]: + """ + >>> simplify([[1, 2, 3], [4, 5, 6]]) + [[1.0, 2.0, 3.0], [0.0, 0.75, 1.5]] + >>> simplify([[5, 2, 5], [5, 1, 10]]) + [[1.0, 0.4, 1.0], [0.0, 0.2, -1.0]] + """ + # Divide each row by magnitude of first term --> creates 'unit' matrix + duplicate_set = current_set.copy() + for row_index, row in enumerate(duplicate_set): + magnitude = row[0] + for column_index, column in enumerate(row): + if magnitude == 0: + current_set[row_index][column_index] = column + continue + current_set[row_index][column_index] = column / magnitude + # Subtract to cancel term + first_row = current_set[0] + final_set = [first_row] + current_set = current_set[1::] + for row in current_set: + temp_row = [] + # If first term is 0, it is already in form we want, so we preserve it + if row[0] == 0: + final_set.append(row) + continue + for column_index in range(len(row)): + temp_row.append(first_row[column_index] - row[column_index]) + final_set.append(temp_row) + # Create next recursion iteration set + if len(final_set[0]) != 3: + current_first_row = final_set[0] + current_first_column = [] + next_iteration = [] + for row in final_set[1::]: + current_first_column.append(row[0]) + next_iteration.append(row[1::]) + resultant = simplify(next_iteration) + for i in range(len(resultant)): + resultant[i].insert(0, current_first_column[i]) + resultant.insert(0, current_first_row) + final_set = resultant + return final_set + + +def solve_simultaneous(equations: list[list]) -> list: + """ + >>> solve_simultaneous([[1, 2, 3],[4, 5, 6]]) + [-1.0, 2.0] + >>> solve_simultaneous([[0, -3, 1, 7],[3, 2, -1, 11],[5, 1, -2, 12]]) + [6.4, 1.2, 10.6] + >>> solve_simultaneous([]) + Traceback (most recent call last): + ... + IndexError: solve_simultaneous() requires n lists of length n+1 + >>> solve_simultaneous([[1, 2, 3],[1, 2]]) + Traceback (most recent call last): + ... + IndexError: solve_simultaneous() requires n lists of length n+1 + >>> solve_simultaneous([[1, 2, 3],["a", 7, 8]]) + Traceback (most recent call last): + ... + ValueError: solve_simultaneous() requires lists of integers + >>> solve_simultaneous([[0, 2, 3],[4, 0, 6]]) + Traceback (most recent call last): + ... + ValueError: solve_simultaneous() requires at least 1 full equation + """ + if len(equations) == 0: + raise IndexError("solve_simultaneous() requires n lists of length n+1") + _length = len(equations) + 1 + if any(len(item) != _length for item in equations): + raise IndexError("solve_simultaneous() requires n lists of length n+1") + for row in equations: + if any(not isinstance(column, (int, float)) for column in row): + raise ValueError("solve_simultaneous() requires lists of integers") + if len(equations) == 1: + return [equations[0][-1] / equations[0][0]] + data_set = equations.copy() + if any(0 in row for row in data_set): + temp_data = data_set.copy() + full_row = [] + for row_index, row in enumerate(temp_data): + if 0 not in row: + full_row = data_set.pop(row_index) + break + if not full_row: + raise ValueError("solve_simultaneous() requires at least 1 full equation") + data_set.insert(0, full_row) + useable_form = data_set.copy() + simplified = simplify(useable_form) + simplified = simplified[::-1] + solutions: list = [] + for row in simplified: + current_solution = row[-1] + if not solutions: + if row[-2] == 0: + solutions.append(0) + continue + solutions.append(current_solution / row[-2]) + continue + temp_row = row.copy()[: len(row) - 1 :] + while temp_row[0] == 0: + temp_row.pop(0) + if len(temp_row) == 0: + solutions.append(0) + continue + temp_row = temp_row[1::] + temp_row = temp_row[::-1] + for column_index, column in enumerate(temp_row): + current_solution -= column * solutions[column_index] + solutions.append(current_solution) + final = [] + for item in solutions: + final.append(float(round(item, 5))) + return final[::-1] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + eq = [ + [2, 1, 1, 1, 1, 4], + [1, 2, 1, 1, 1, 5], + [1, 1, 2, 1, 1, 6], + [1, 1, 1, 2, 1, 7], + [1, 1, 1, 1, 2, 8], + ] + print(solve_simultaneous(eq)) + print(solve_simultaneous([[4, 2]])) diff --git a/maths/softmax.py b/maths/softmax.py index 04cf77525420..95c95e66f59e 100644 --- a/maths/softmax.py +++ b/maths/softmax.py @@ -28,7 +28,7 @@ def softmax(vector): The softmax vector adds up to one. We need to ceil to mitigate for precision - >>> np.ceil(np.sum(softmax([1,2,3,4]))) + >>> float(np.ceil(np.sum(softmax([1,2,3,4])))) 1.0 >>> vec = np.array([5,5]) diff --git a/maths/solovay_strassen_primality_test.py b/maths/solovay_strassen_primality_test.py new file mode 100644 index 000000000000..b2d905b07bed --- /dev/null +++ b/maths/solovay_strassen_primality_test.py @@ -0,0 +1,106 @@ +""" +This script implements the Solovay-Strassen Primality test. + +This probabilistic primality test is based on Euler's criterion. It is similar +to the Fermat test but uses quadratic residues. It can quickly identify +composite numbers but may occasionally classify composite numbers as prime. + +More details and concepts about this can be found on: +https://en.wikipedia.org/wiki/Solovay%E2%80%93Strassen_primality_test +""" + +import random + + +def jacobi_symbol(random_a: int, number: int) -> int: + """ + Calculate the Jacobi symbol. The Jacobi symbol is a generalization + of the Legendre symbol, which can be used to simplify computations involving + quadratic residues. The Jacobi symbol is used in primality tests, like the + Solovay-Strassen test, because it helps determine if an integer is a + quadratic residue modulo a given modulus, providing valuable information + about the number's potential primality or compositeness. + + Parameters: + random_a: A randomly chosen integer from 2 to n-2 (inclusive) + number: The number that is tested for primality + + Returns: + jacobi_symbol: The Jacobi symbol is a mathematical function + used to determine whether an integer is a quadratic residue modulo + another integer (usually prime) or not. + + >>> jacobi_symbol(2, 13) + -1 + >>> jacobi_symbol(5, 19) + 1 + >>> jacobi_symbol(7, 14) + 0 + """ + + if random_a in (0, 1): + return random_a + + random_a %= number + t = 1 + + while random_a != 0: + while random_a % 2 == 0: + random_a //= 2 + r = number % 8 + if r in (3, 5): + t = -t + + random_a, number = number, random_a + + if random_a % 4 == number % 4 == 3: + t = -t + + random_a %= number + + return t if number == 1 else 0 + + +def solovay_strassen(number: int, iterations: int) -> bool: + """ + Check whether the input number is prime or not using + the Solovay-Strassen Primality test + + Parameters: + number: The number that is tested for primality + iterations: The number of times that the test is run + which effects the accuracy + + Returns: + result: True if number is probably prime and false + if not + + >>> random.seed(10) + >>> solovay_strassen(13, 5) + True + >>> solovay_strassen(9, 10) + False + >>> solovay_strassen(17, 15) + True + """ + + if number <= 1: + return False + if number <= 3: + return True + + for _ in range(iterations): + a = random.randint(2, number - 2) + x = jacobi_symbol(a, number) + y = pow(a, (number - 1) // 2, number) + + if x == 0 or y != x % number: + return False + + return True + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/spearman_rank_correlation_coefficient.py b/maths/spearman_rank_correlation_coefficient.py new file mode 100644 index 000000000000..32ff6b9e3d71 --- /dev/null +++ b/maths/spearman_rank_correlation_coefficient.py @@ -0,0 +1,82 @@ +from collections.abc import Sequence + + +def assign_ranks(data: Sequence[float]) -> list[int]: + """ + Assigns ranks to elements in the array. + + :param data: List of floats. + :return: List of ints representing the ranks. + + Example: + >>> assign_ranks([3.2, 1.5, 4.0, 2.7, 5.1]) + [3, 1, 4, 2, 5] + + >>> assign_ranks([10.5, 8.1, 12.4, 9.3, 11.0]) + [3, 1, 5, 2, 4] + """ + ranked_data = sorted((value, index) for index, value in enumerate(data)) + ranks = [0] * len(data) + + for position, (_, index) in enumerate(ranked_data): + ranks[index] = position + 1 + + return ranks + + +def calculate_spearman_rank_correlation( + variable_1: Sequence[float], variable_2: Sequence[float] +) -> float: + """ + Calculates Spearman's rank correlation coefficient. + + :param variable_1: List of floats representing the first variable. + :param variable_2: List of floats representing the second variable. + :return: Spearman's rank correlation coefficient. + + Example Usage: + + >>> x = [1, 2, 3, 4, 5] + >>> y = [5, 4, 3, 2, 1] + >>> calculate_spearman_rank_correlation(x, y) + -1.0 + + >>> x = [1, 2, 3, 4, 5] + >>> y = [2, 4, 6, 8, 10] + >>> calculate_spearman_rank_correlation(x, y) + 1.0 + + >>> x = [1, 2, 3, 4, 5] + >>> y = [5, 1, 2, 9, 5] + >>> calculate_spearman_rank_correlation(x, y) + 0.6 + """ + n = len(variable_1) + rank_var1 = assign_ranks(variable_1) + rank_var2 = assign_ranks(variable_2) + + # Calculate differences of ranks + d = [rx - ry for rx, ry in zip(rank_var1, rank_var2)] + + # Calculate the sum of squared differences + d_squared = sum(di**2 for di in d) + + # Calculate the Spearman's rank correlation coefficient + rho = 1 - (6 * d_squared) / (n * (n**2 - 1)) + + return rho + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example usage: + print( + f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]) = }" + ) + + print(f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [5, 4, 3, 2, 1]) = }") + + print(f"{calculate_spearman_rank_correlation([1, 2, 3, 4, 5], [5, 1, 2, 9, 5]) = }") diff --git a/maths/special_numbers/__init__.py b/maths/special_numbers/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/maths/armstrong_numbers.py b/maths/special_numbers/armstrong_numbers.py similarity index 87% rename from maths/armstrong_numbers.py rename to maths/special_numbers/armstrong_numbers.py index 26709b428b78..a3cb69b814de 100644 --- a/maths/armstrong_numbers.py +++ b/maths/special_numbers/armstrong_numbers.py @@ -1,100 +1,99 @@ -""" -An Armstrong number is equal to the sum of its own digits each raised to the -power of the number of digits. - -For example, 370 is an Armstrong number because 3*3*3 + 7*7*7 + 0*0*0 = 370. - -Armstrong numbers are also called Narcissistic numbers and Pluperfect numbers. - -On-Line Encyclopedia of Integer Sequences entry: https://oeis.org/A005188 -""" -PASSING = (1, 153, 370, 371, 1634, 24678051, 115132219018763992565095597973971522401) -FAILING: tuple = (-153, -1, 0, 1.2, 200, "A", [], {}, None) - - -def armstrong_number(n: int) -> bool: - """ - Return True if n is an Armstrong number or False if it is not. - - >>> all(armstrong_number(n) for n in PASSING) - True - >>> any(armstrong_number(n) for n in FAILING) - False - """ - if not isinstance(n, int) or n < 1: - return False - - # Initialization of sum and number of digits. - total = 0 - number_of_digits = 0 - temp = n - # Calculation of digits of the number - while temp > 0: - number_of_digits += 1 - temp //= 10 - # Dividing number into separate digits and find Armstrong number - temp = n - while temp > 0: - rem = temp % 10 - total += rem**number_of_digits - temp //= 10 - return n == total - - -def pluperfect_number(n: int) -> bool: - """Return True if n is a pluperfect number or False if it is not - - >>> all(armstrong_number(n) for n in PASSING) - True - >>> any(armstrong_number(n) for n in FAILING) - False - """ - if not isinstance(n, int) or n < 1: - return False - - # Init a "histogram" of the digits - digit_histogram = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - digit_total = 0 - total = 0 - temp = n - while temp > 0: - temp, rem = divmod(temp, 10) - digit_histogram[rem] += 1 - digit_total += 1 - - for cnt, i in zip(digit_histogram, range(len(digit_histogram))): - total += cnt * i**digit_total - - return n == total - - -def narcissistic_number(n: int) -> bool: - """Return True if n is a narcissistic number or False if it is not. - - >>> all(armstrong_number(n) for n in PASSING) - True - >>> any(armstrong_number(n) for n in FAILING) - False - """ - if not isinstance(n, int) or n < 1: - return False - expo = len(str(n)) # the power that all digits will be raised to - # check if sum of each digit multiplied expo times is equal to number - return n == sum(int(i) ** expo for i in str(n)) - - -def main(): - """ - Request that user input an integer and tell them if it is Armstrong number. - """ - num = int(input("Enter an integer to see if it is an Armstrong number: ").strip()) - print(f"{num} is {'' if armstrong_number(num) else 'not '}an Armstrong number.") - print(f"{num} is {'' if narcissistic_number(num) else 'not '}an Armstrong number.") - print(f"{num} is {'' if pluperfect_number(num) else 'not '}an Armstrong number.") - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - main() +""" +An Armstrong number is equal to the sum of its own digits each raised to the +power of the number of digits. + +For example, 370 is an Armstrong number because 3*3*3 + 7*7*7 + 0*0*0 = 370. + +Armstrong numbers are also called Narcissistic numbers and Pluperfect numbers. + +On-Line Encyclopedia of Integer Sequences entry: https://oeis.org/A005188 +""" + +PASSING = (1, 153, 370, 371, 1634, 24678051, 115132219018763992565095597973971522401) +FAILING: tuple = (-153, -1, 0, 1.2, 200, "A", [], {}, None) + + +def armstrong_number(n: int) -> bool: + """ + Return True if n is an Armstrong number or False if it is not. + + >>> all(armstrong_number(n) for n in PASSING) + True + >>> any(armstrong_number(n) for n in FAILING) + False + """ + if not isinstance(n, int) or n < 1: + return False + + # Initialization of sum and number of digits. + total = 0 + number_of_digits = 0 + temp = n + # Calculation of digits of the number + number_of_digits = len(str(n)) + # Dividing number into separate digits and find Armstrong number + temp = n + while temp > 0: + rem = temp % 10 + total += rem**number_of_digits + temp //= 10 + return n == total + + +def pluperfect_number(n: int) -> bool: + """Return True if n is a pluperfect number or False if it is not + + >>> all(pluperfect_number(n) for n in PASSING) + True + >>> any(pluperfect_number(n) for n in FAILING) + False + """ + if not isinstance(n, int) or n < 1: + return False + + # Init a "histogram" of the digits + digit_histogram = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + digit_total = 0 + total = 0 + temp = n + while temp > 0: + temp, rem = divmod(temp, 10) + digit_histogram[rem] += 1 + digit_total += 1 + + for cnt, i in zip(digit_histogram, range(len(digit_histogram))): + total += cnt * i**digit_total + + return n == total + + +def narcissistic_number(n: int) -> bool: + """Return True if n is a narcissistic number or False if it is not. + + >>> all(narcissistic_number(n) for n in PASSING) + True + >>> any(narcissistic_number(n) for n in FAILING) + False + """ + if not isinstance(n, int) or n < 1: + return False + expo = len(str(n)) # the power that all digits will be raised to + # check if sum of each digit multiplied expo times is equal to number + return n == sum(int(i) ** expo for i in str(n)) + + +def main(): + """ + Request that user input an integer and tell them if it is Armstrong number. + """ + num = int(input("Enter an integer to see if it is an Armstrong number: ").strip()) + print(f"{num} is {'' if armstrong_number(num) else 'not '}an Armstrong number.") + print(f"{num} is {'' if narcissistic_number(num) else 'not '}an Armstrong number.") + print(f"{num} is {'' if pluperfect_number(num) else 'not '}an Armstrong number.") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/maths/automorphic_number.py b/maths/special_numbers/automorphic_number.py similarity index 93% rename from maths/automorphic_number.py rename to maths/special_numbers/automorphic_number.py index 103fc7301831..8ed9375632a4 100644 --- a/maths/automorphic_number.py +++ b/maths/special_numbers/automorphic_number.py @@ -40,7 +40,8 @@ def is_automorphic_number(number: int) -> bool: TypeError: Input value of [number=5.0] must be an integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 0: return False number_square = number * number diff --git a/maths/special_numbers/bell_numbers.py b/maths/special_numbers/bell_numbers.py new file mode 100644 index 000000000000..d573e7a3962d --- /dev/null +++ b/maths/special_numbers/bell_numbers.py @@ -0,0 +1,81 @@ +""" +Bell numbers represent the number of ways to partition a set into non-empty +subsets. This module provides functions to calculate Bell numbers for sets of +integers. In other words, the first (n + 1) Bell numbers. + +For more information about Bell numbers, refer to: +https://en.wikipedia.org/wiki/Bell_number +""" + + +def bell_numbers(max_set_length: int) -> list[int]: + """ + Calculate Bell numbers for the sets of lengths from 0 to max_set_length. + In other words, calculate first (max_set_length + 1) Bell numbers. + + Args: + max_set_length (int): The maximum length of the sets for which + Bell numbers are calculated. + + Returns: + list: A list of Bell numbers for sets of lengths from 0 to max_set_length. + + Examples: + >>> bell_numbers(-2) + Traceback (most recent call last): + ... + ValueError: max_set_length must be non-negative + >>> bell_numbers(0) + [1] + >>> bell_numbers(1) + [1, 1] + >>> bell_numbers(5) + [1, 1, 2, 5, 15, 52] + """ + if max_set_length < 0: + raise ValueError("max_set_length must be non-negative") + + bell = [0] * (max_set_length + 1) + bell[0] = 1 + + for i in range(1, max_set_length + 1): + for j in range(i): + bell[i] += _binomial_coefficient(i - 1, j) * bell[j] + + return bell + + +def _binomial_coefficient(total_elements: int, elements_to_choose: int) -> int: + """ + Calculate the binomial coefficient C(total_elements, elements_to_choose) + + Args: + total_elements (int): The total number of elements. + elements_to_choose (int): The number of elements to choose. + + Returns: + int: The binomial coefficient C(total_elements, elements_to_choose). + + Examples: + >>> _binomial_coefficient(5, 2) + 10 + >>> _binomial_coefficient(6, 3) + 20 + """ + if elements_to_choose in {0, total_elements}: + return 1 + + elements_to_choose = min(elements_to_choose, total_elements - elements_to_choose) + + coefficient = 1 + for i in range(elements_to_choose): + coefficient *= total_elements - i + coefficient //= i + 1 + + return coefficient + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/special_numbers/carmichael_number.py b/maths/special_numbers/carmichael_number.py new file mode 100644 index 000000000000..c73908545702 --- /dev/null +++ b/maths/special_numbers/carmichael_number.py @@ -0,0 +1,86 @@ +""" +== Carmichael Numbers == +A number n is said to be a Carmichael number if it +satisfies the following modular arithmetic condition: + + power(b, n-1) MOD n = 1, + for all b ranging from 1 to n such that b and + n are relatively prime, i.e, gcd(b, n) = 1 + +Examples of Carmichael Numbers: 561, 1105, ... +https://en.wikipedia.org/wiki/Carmichael_number +""" + +from maths.greatest_common_divisor import greatest_common_divisor + + +def power(x: int, y: int, mod: int) -> int: + """ + Examples: + >>> power(2, 15, 3) + 2 + >>> power(5, 1, 30) + 5 + """ + + if y == 0: + return 1 + temp = power(x, y // 2, mod) % mod + temp = (temp * temp) % mod + if y % 2 == 1: + temp = (temp * x) % mod + return temp + + +def is_carmichael_number(n: int) -> bool: + """ + Examples: + >>> is_carmichael_number(4) + False + >>> is_carmichael_number(561) + True + >>> is_carmichael_number(562) + False + >>> is_carmichael_number(900) + False + >>> is_carmichael_number(1105) + True + >>> is_carmichael_number(8911) + True + >>> is_carmichael_number(5.1) + Traceback (most recent call last): + ... + ValueError: Number 5.1 must instead be a positive integer + + >>> is_carmichael_number(-7) + Traceback (most recent call last): + ... + ValueError: Number -7 must instead be a positive integer + + >>> is_carmichael_number(0) + Traceback (most recent call last): + ... + ValueError: Number 0 must instead be a positive integer + """ + + if n <= 0 or not isinstance(n, int): + msg = f"Number {n} must instead be a positive integer" + raise ValueError(msg) + + return all( + power(b, n - 1, n) == 1 + for b in range(2, n) + if greatest_common_divisor(b, n) == 1 + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + number = int(input("Enter number: ").strip()) + if is_carmichael_number(number): + print(f"{number} is a Carmichael Number.") + else: + print(f"{number} is not a Carmichael Number.") diff --git a/maths/catalan_number.py b/maths/special_numbers/catalan_number.py similarity index 83% rename from maths/catalan_number.py rename to maths/special_numbers/catalan_number.py index 85607dc1eca4..20c2cfb17c06 100644 --- a/maths/catalan_number.py +++ b/maths/special_numbers/catalan_number.py @@ -31,10 +31,12 @@ def catalan(number: int) -> int: """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 1: - raise ValueError(f"Input value of [number={number}] must be > 0") + msg = f"Input value of [number={number}] must be > 0" + raise ValueError(msg) current_number = 1 diff --git a/maths/hamming_numbers.py b/maths/special_numbers/hamming_numbers.py similarity index 89% rename from maths/hamming_numbers.py rename to maths/special_numbers/hamming_numbers.py index 4575119c8a95..a473cc93883b 100644 --- a/maths/hamming_numbers.py +++ b/maths/special_numbers/hamming_numbers.py @@ -13,6 +13,10 @@ def hamming(n_element: int) -> list: :param n_element: The number of elements on the list :return: The nth element of the list + >>> hamming(-5) + Traceback (most recent call last): + ... + ValueError: n_element should be a positive number >>> hamming(5) [1, 2, 3, 4, 5] >>> hamming(10) @@ -22,7 +26,7 @@ def hamming(n_element: int) -> list: """ n_element = int(n_element) if n_element < 1: - my_error = ValueError("a should be a positive number") + my_error = ValueError("n_element should be a positive number") raise my_error hamming_list = [1] diff --git a/maths/special_numbers/happy_number.py b/maths/special_numbers/happy_number.py new file mode 100644 index 000000000000..eac3167e304b --- /dev/null +++ b/maths/special_numbers/happy_number.py @@ -0,0 +1,48 @@ +def is_happy_number(number: int) -> bool: + """ + A happy number is a number which eventually reaches 1 when replaced by the sum of + the square of each digit. + + :param number: The number to check for happiness. + :return: True if the number is a happy number, False otherwise. + + >>> is_happy_number(19) + True + >>> is_happy_number(2) + False + >>> is_happy_number(23) + True + >>> is_happy_number(1) + True + >>> is_happy_number(0) + Traceback (most recent call last): + ... + ValueError: number=0 must be a positive integer + >>> is_happy_number(-19) + Traceback (most recent call last): + ... + ValueError: number=-19 must be a positive integer + >>> is_happy_number(19.1) + Traceback (most recent call last): + ... + ValueError: number=19.1 must be a positive integer + >>> is_happy_number("happy") + Traceback (most recent call last): + ... + ValueError: number='happy' must be a positive integer + """ + if not isinstance(number, int) or number <= 0: + msg = f"{number=} must be a positive integer" + raise ValueError(msg) + + seen = set() + while number != 1 and number not in seen: + seen.add(number) + number = sum(int(digit) ** 2 for digit in str(number)) + return number == 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/special_numbers/harshad_numbers.py b/maths/special_numbers/harshad_numbers.py new file mode 100644 index 000000000000..417120bd840e --- /dev/null +++ b/maths/special_numbers/harshad_numbers.py @@ -0,0 +1,166 @@ +""" +A harshad number (or more specifically an n-harshad number) is a number that's +divisible by the sum of its digits in some given base n. +Reference: https://en.wikipedia.org/wiki/Harshad_number +""" + + +def int_to_base(number: int, base: int) -> str: + """ + Convert a given positive decimal integer to base 'base'. + Where 'base' ranges from 2 to 36. + + Examples: + >>> int_to_base(0, 21) + '0' + >>> int_to_base(23, 2) + '10111' + >>> int_to_base(58, 5) + '213' + >>> int_to_base(167, 16) + 'A7' + >>> # bases below 2 and beyond 36 will error + >>> int_to_base(98, 1) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> int_to_base(98, 37) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> int_to_base(-99, 16) + Traceback (most recent call last): + ... + ValueError: number must be a positive integer + """ + + if base < 2 or base > 36: + raise ValueError("'base' must be between 2 and 36 inclusive") + + digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + result = "" + + if number < 0: + raise ValueError("number must be a positive integer") + + while number > 0: + number, remainder = divmod(number, base) + result = digits[remainder] + result + + if result == "": + result = "0" + + return result + + +def sum_of_digits(num: int, base: int) -> str: + """ + Calculate the sum of digit values in a positive integer + converted to the given 'base'. + Where 'base' ranges from 2 to 36. + + Examples: + >>> sum_of_digits(103, 12) + '13' + >>> sum_of_digits(1275, 4) + '30' + >>> sum_of_digits(6645, 2) + '1001' + >>> # bases below 2 and beyond 36 will error + >>> sum_of_digits(543, 1) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> sum_of_digits(543, 37) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + """ + + if base < 2 or base > 36: + raise ValueError("'base' must be between 2 and 36 inclusive") + + num_str = int_to_base(num, base) + res = sum(int(char, base) for char in num_str) + res_str = int_to_base(res, base) + return res_str + + +def harshad_numbers_in_base(limit: int, base: int) -> list[str]: + """ + Finds all Harshad numbers smaller than num in base 'base'. + Where 'base' ranges from 2 to 36. + + Examples: + >>> harshad_numbers_in_base(15, 2) + ['1', '10', '100', '110', '1000', '1010', '1100'] + >>> harshad_numbers_in_base(12, 34) + ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B'] + >>> harshad_numbers_in_base(12, 4) + ['1', '2', '3', '10', '12', '20', '21'] + >>> # bases below 2 and beyond 36 will error + >>> harshad_numbers_in_base(234, 37) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> harshad_numbers_in_base(234, 1) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> harshad_numbers_in_base(-12, 6) + [] + """ + + if base < 2 or base > 36: + raise ValueError("'base' must be between 2 and 36 inclusive") + + if limit < 0: + return [] + + numbers = [ + int_to_base(i, base) + for i in range(1, limit) + if i % int(sum_of_digits(i, base), base) == 0 + ] + + return numbers + + +def is_harshad_number_in_base(num: int, base: int) -> bool: + """ + Determines whether n in base 'base' is a harshad number. + Where 'base' ranges from 2 to 36. + + Examples: + >>> is_harshad_number_in_base(18, 10) + True + >>> is_harshad_number_in_base(21, 10) + True + >>> is_harshad_number_in_base(-21, 5) + False + >>> # bases below 2 and beyond 36 will error + >>> is_harshad_number_in_base(45, 37) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + >>> is_harshad_number_in_base(45, 1) + Traceback (most recent call last): + ... + ValueError: 'base' must be between 2 and 36 inclusive + """ + + if base < 2 or base > 36: + raise ValueError("'base' must be between 2 and 36 inclusive") + + if num < 0: + return False + + n = int_to_base(num, base) + d = sum_of_digits(num, base) + return int(n, base) % int(d, base) == 0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/hexagonal_number.py b/maths/special_numbers/hexagonal_number.py similarity index 92% rename from maths/hexagonal_number.py rename to maths/special_numbers/hexagonal_number.py index 28735c638f80..3677ab95ee00 100644 --- a/maths/hexagonal_number.py +++ b/maths/special_numbers/hexagonal_number.py @@ -36,7 +36,8 @@ def hexagonal(number: int) -> int: TypeError: Input value of [number=11.0] must be an integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 1: raise ValueError("Input must be a positive integer") return number * (2 * number - 1) diff --git a/maths/krishnamurthy_number.py b/maths/special_numbers/krishnamurthy_number.py similarity index 100% rename from maths/krishnamurthy_number.py rename to maths/special_numbers/krishnamurthy_number.py diff --git a/maths/special_numbers/perfect_number.py b/maths/special_numbers/perfect_number.py new file mode 100644 index 000000000000..a022dc677638 --- /dev/null +++ b/maths/special_numbers/perfect_number.py @@ -0,0 +1,79 @@ +""" +== Perfect Number == +In number theory, a perfect number is a positive integer that is equal to the sum of +its positive divisors, excluding the number itself. +For example: 6 ==> divisors[1, 2, 3, 6] + Excluding 6, the sum(divisors) is 1 + 2 + 3 = 6 + So, 6 is a Perfect Number + +Other examples of Perfect Numbers: 28, 486, ... + +https://en.wikipedia.org/wiki/Perfect_number +""" + + +def perfect(number: int) -> bool: + """ + Check if a number is a perfect number. + + A perfect number is a positive integer that is equal to the sum of its proper + divisors (excluding itself). + + Args: + number: The number to be checked. + + Returns: + True if the number is a perfect number, False otherwise. + + Start from 1 because dividing by 0 will raise ZeroDivisionError. + A number at most can be divisible by the half of the number except the number + itself. For example, 6 is at most can be divisible by 3 except by 6 itself. + + Examples: + >>> perfect(27) + False + >>> perfect(28) + True + >>> perfect(29) + False + >>> perfect(6) + True + >>> perfect(12) + False + >>> perfect(496) + True + >>> perfect(8128) + True + >>> perfect(0) + False + >>> perfect(-1) + False + >>> perfect(12.34) + Traceback (most recent call last): + ... + ValueError: number must be an integer + >>> perfect("Hello") + Traceback (most recent call last): + ... + ValueError: number must be an integer + """ + if not isinstance(number, int): + raise ValueError("number must be an integer") + if number <= 0: + return False + return sum(i for i in range(1, number // 2 + 1) if number % i == 0) == number + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print("Program to check whether a number is a Perfect number or not...") + try: + number = int(input("Enter a positive integer: ").strip()) + except ValueError: + msg = "number must be an integer" + print(msg) + raise ValueError(msg) + + print(f"{number} is {'' if perfect(number) else 'not '}a Perfect Number.") diff --git a/maths/special_numbers/polygonal_numbers.py b/maths/special_numbers/polygonal_numbers.py new file mode 100644 index 000000000000..7a7dc91acb26 --- /dev/null +++ b/maths/special_numbers/polygonal_numbers.py @@ -0,0 +1,32 @@ +def polygonal_num(num: int, sides: int) -> int: + """ + Returns the `num`th `sides`-gonal number. It is assumed that `num` >= 0 and + `sides` >= 3 (see for reference https://en.wikipedia.org/wiki/Polygonal_number). + + >>> polygonal_num(0, 3) + 0 + >>> polygonal_num(3, 3) + 6 + >>> polygonal_num(5, 4) + 25 + >>> polygonal_num(2, 5) + 5 + >>> polygonal_num(-1, 0) + Traceback (most recent call last): + ... + ValueError: Invalid input: num must be >= 0 and sides must be >= 3. + >>> polygonal_num(0, 2) + Traceback (most recent call last): + ... + ValueError: Invalid input: num must be >= 0 and sides must be >= 3. + """ + if num < 0 or sides < 3: + raise ValueError("Invalid input: num must be >= 0 and sides must be >= 3.") + + return ((sides - 2) * num**2 - (sides - 4) * num) // 2 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/pronic_number.py b/maths/special_numbers/pronic_number.py similarity index 92% rename from maths/pronic_number.py rename to maths/special_numbers/pronic_number.py index 8b554dbbd602..cf4d3d2eb24b 100644 --- a/maths/pronic_number.py +++ b/maths/special_numbers/pronic_number.py @@ -41,7 +41,8 @@ def is_pronic(number: int) -> bool: TypeError: Input value of [number=6.0] must be an integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 0 or number % 2 == 1: return False number_sqrt = int(number**0.5) diff --git a/maths/proth_number.py b/maths/special_numbers/proth_number.py similarity index 90% rename from maths/proth_number.py rename to maths/special_numbers/proth_number.py index ce911473a2d2..47747ed260f7 100644 --- a/maths/proth_number.py +++ b/maths/special_numbers/proth_number.py @@ -29,10 +29,12 @@ def proth(number: int) -> int: """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if number < 1: - raise ValueError(f"Input value of [number={number}] must be > 0") + msg = f"Input value of [number={number}] must be > 0" + raise ValueError(msg) elif number == 1: return 3 elif number == 2: diff --git a/maths/special_numbers/triangular_numbers.py b/maths/special_numbers/triangular_numbers.py new file mode 100644 index 000000000000..5be89e6108b2 --- /dev/null +++ b/maths/special_numbers/triangular_numbers.py @@ -0,0 +1,43 @@ +""" +A triangular number or triangle number counts objects arranged in an +equilateral triangle. This module provides a function to generate n'th +triangular number. + +For more information about triangular numbers, refer to: +https://en.wikipedia.org/wiki/Triangular_number +""" + + +def triangular_number(position: int) -> int: + """ + Generate the triangular number at the specified position. + + Args: + position (int): The position of the triangular number to generate. + + Returns: + int: The triangular number at the specified position. + + Raises: + ValueError: If `position` is negative. + + Examples: + >>> triangular_number(1) + 1 + >>> triangular_number(3) + 6 + >>> triangular_number(-1) + Traceback (most recent call last): + ... + ValueError: param `position` must be non-negative + """ + if position < 0: + raise ValueError("param `position` must be non-negative") + + return position * (position + 1) // 2 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/ugly_numbers.py b/maths/special_numbers/ugly_numbers.py similarity index 96% rename from maths/ugly_numbers.py rename to maths/special_numbers/ugly_numbers.py index 81bd928c6b3d..c6ceb784622a 100644 --- a/maths/ugly_numbers.py +++ b/maths/special_numbers/ugly_numbers.py @@ -1,54 +1,54 @@ -""" -Ugly numbers are numbers whose only prime factors are 2, 3 or 5. The sequence -1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, … shows the first 11 ugly numbers. By convention, -1 is included. -Given an integer n, we have to find the nth ugly number. - -For more details, refer this article -https://www.geeksforgeeks.org/ugly-numbers/ -""" - - -def ugly_numbers(n: int) -> int: - """ - Returns the nth ugly number. - >>> ugly_numbers(100) - 1536 - >>> ugly_numbers(0) - 1 - >>> ugly_numbers(20) - 36 - >>> ugly_numbers(-5) - 1 - >>> ugly_numbers(-5.5) - Traceback (most recent call last): - ... - TypeError: 'float' object cannot be interpreted as an integer - """ - ugly_nums = [1] - - i2, i3, i5 = 0, 0, 0 - next_2 = ugly_nums[i2] * 2 - next_3 = ugly_nums[i3] * 3 - next_5 = ugly_nums[i5] * 5 - - for _ in range(1, n): - next_num = min(next_2, next_3, next_5) - ugly_nums.append(next_num) - if next_num == next_2: - i2 += 1 - next_2 = ugly_nums[i2] * 2 - if next_num == next_3: - i3 += 1 - next_3 = ugly_nums[i3] * 3 - if next_num == next_5: - i5 += 1 - next_5 = ugly_nums[i5] * 5 - return ugly_nums[-1] - - -if __name__ == "__main__": - from doctest import testmod - - testmod(verbose=True) - print(f"{ugly_numbers(200) = }") +""" +Ugly numbers are numbers whose only prime factors are 2, 3 or 5. The sequence +1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, … shows the first 11 ugly numbers. By convention, +1 is included. +Given an integer n, we have to find the nth ugly number. + +For more details, refer this article +https://www.geeksforgeeks.org/ugly-numbers/ +""" + + +def ugly_numbers(n: int) -> int: + """ + Returns the nth ugly number. + >>> ugly_numbers(100) + 1536 + >>> ugly_numbers(0) + 1 + >>> ugly_numbers(20) + 36 + >>> ugly_numbers(-5) + 1 + >>> ugly_numbers(-5.5) + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + """ + ugly_nums = [1] + + i2, i3, i5 = 0, 0, 0 + next_2 = ugly_nums[i2] * 2 + next_3 = ugly_nums[i3] * 3 + next_5 = ugly_nums[i5] * 5 + + for _ in range(1, n): + next_num = min(next_2, next_3, next_5) + ugly_nums.append(next_num) + if next_num == next_2: + i2 += 1 + next_2 = ugly_nums[i2] * 2 + if next_num == next_3: + i3 += 1 + next_3 = ugly_nums[i3] * 3 + if next_num == next_5: + i5 += 1 + next_5 = ugly_nums[i5] * 5 + return ugly_nums[-1] + + +if __name__ == "__main__": + from doctest import testmod + + testmod(verbose=True) + print(f"{ugly_numbers(200) = }") diff --git a/maths/weird_number.py b/maths/special_numbers/weird_number.py similarity index 99% rename from maths/weird_number.py rename to maths/special_numbers/weird_number.py index 2834a9fee31e..5c9240d0ea4e 100644 --- a/maths/weird_number.py +++ b/maths/special_numbers/weird_number.py @@ -3,6 +3,7 @@ Fun fact: The set of weird numbers has positive asymptotic density. """ + from math import sqrt diff --git a/maths/sylvester_sequence.py b/maths/sylvester_sequence.py index 114c9dd58582..607424c6a90b 100644 --- a/maths/sylvester_sequence.py +++ b/maths/sylvester_sequence.py @@ -31,7 +31,8 @@ def sylvester(number: int) -> int: if number == 1: return 2 elif number < 1: - raise ValueError(f"The input value of [n={number}] has to be > 0") + msg = f"The input value of [n={number}] has to be > 0" + raise ValueError(msg) else: num = sylvester(number - 1) lower = num - 1 diff --git a/maths/tanh.py b/maths/tanh.py new file mode 100644 index 000000000000..011d6f17e22b --- /dev/null +++ b/maths/tanh.py @@ -0,0 +1,43 @@ +""" +This script demonstrates the implementation of the tangent hyperbolic +or tanh function. + +The function takes a vector of K real numbers as input and +then (e^x - e^(-x))/(e^x + e^(-x)). After through tanh, the +element of the vector mostly -1 between 1. + +Script inspired from its corresponding Wikipedia article +https://en.wikipedia.org/wiki/Activation_function +""" + +import numpy as np + + +def tangent_hyperbolic(vector: np.ndarray) -> np.ndarray: + """ + Implements the tanh function + + Parameters: + vector: np.ndarray + + Returns: + tanh (np.array): The input numpy array after applying tanh. + + mathematically (e^x - e^(-x))/(e^x + e^(-x)) can be written as (2/(1+e^(-2x))-1 + + Examples: + >>> tangent_hyperbolic(np.array([1,5,6,-0.67])) + array([ 0.76159416, 0.9999092 , 0.99998771, -0.58497988]) + + >>> tangent_hyperbolic(np.array([8,10,2,-0.98,13])) + array([ 0.99999977, 1. , 0.96402758, -0.7530659 , 1. ]) + + """ + + return (2 / (1 + np.exp(-2 * vector))) - 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/three_sum.py b/maths/three_sum.py new file mode 100644 index 000000000000..09956f8415a0 --- /dev/null +++ b/maths/three_sum.py @@ -0,0 +1,47 @@ +""" +https://en.wikipedia.org/wiki/3SUM +""" + + +def three_sum(nums: list[int]) -> list[list[int]]: + """ + Find all unique triplets in a sorted array of integers that sum up to zero. + + Args: + nums: A sorted list of integers. + + Returns: + A list of lists containing unique triplets that sum up to zero. + + >>> three_sum([-1, 0, 1, 2, -1, -4]) + [[-1, -1, 2], [-1, 0, 1]] + >>> three_sum([1, 2, 3, 4]) + [] + """ + nums.sort() + ans = [] + for i in range(len(nums) - 2): + if i == 0 or (nums[i] != nums[i - 1]): + low, high, c = i + 1, len(nums) - 1, 0 - nums[i] + while low < high: + if nums[low] + nums[high] == c: + ans.append([nums[i], nums[low], nums[high]]) + + while low < high and nums[low] == nums[low + 1]: + low += 1 + while low < high and nums[high] == nums[high - 1]: + high -= 1 + + low += 1 + high -= 1 + elif nums[low] + nums[high] < c: + low += 1 + else: + high -= 1 + return ans + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/trapezoidal_rule.py b/maths/trapezoidal_rule.py index 9a4ddc8af66b..21b10b239b5f 100644 --- a/maths/trapezoidal_rule.py +++ b/maths/trapezoidal_rule.py @@ -1,17 +1,26 @@ """ Numerical integration or quadrature for a smooth function f with known values at x_i +""" -This method is the classical approach of suming 'Equally Spaced Abscissas' - -method 1: -"extended trapezoidal rule" -""" +def trapezoidal_rule(boundary, steps): + """ + Implements the extended trapezoidal rule for numerical integration. + The function f(x) is provided below. + :param boundary: List containing the lower and upper bounds of integration [a, b] + :param steps: The number of steps (intervals) used in the approximation + :return: The numerical approximation of the integral -def method_1(boundary, steps): - # "extended trapezoidal rule" - # int(f) = dx/2 * (f1 + 2f2 + ... + fn) + >>> abs(trapezoidal_rule([0, 1], 10) - 0.33333) < 0.01 + True + >>> abs(trapezoidal_rule([0, 1], 100) - 0.33333) < 0.01 + True + >>> abs(trapezoidal_rule([0, 2], 1000) - 2.66667) < 0.01 + True + >>> abs(trapezoidal_rule([1, 2], 1000) - 2.33333) < 0.01 + True + """ h = (boundary[1] - boundary[0]) / steps a = boundary[0] b = boundary[1] @@ -19,32 +28,78 @@ def method_1(boundary, steps): y = 0.0 y += (h / 2.0) * f(a) for i in x_i: - # print(i) y += h * f(i) y += (h / 2.0) * f(b) return y def make_points(a, b, h): + """ + Generates points between a and b with step size h for trapezoidal integration. + + :param a: The lower bound of integration + :param b: The upper bound of integration + :param h: The step size + :yield: The next x-value in the range (a, b) + + >>> list(make_points(0, 1, 0.1)) # doctest: +NORMALIZE_WHITESPACE + [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, \ + 0.8999999999999999] + >>> list(make_points(0, 10, 2.5)) + [2.5, 5.0, 7.5] + >>> list(make_points(0, 10, 2)) + [2, 4, 6, 8] + >>> list(make_points(1, 21, 5)) + [6, 11, 16] + >>> list(make_points(1, 5, 2)) + [3] + >>> list(make_points(1, 4, 3)) + [] + """ x = a + h - while x < (b - h): + while x <= (b - h): yield x - x = x + h + x += h -def f(x): # enter your function here - y = (x - 0) * (x - 0) - return y +def f(x): + """ + This is the function to integrate, f(x) = (x - 0)^2 = x^2. + + :param x: The input value + :return: The value of f(x) + + >>> f(0) + 0 + >>> f(1) + 1 + >>> f(0.5) + 0.25 + """ + return x**2 def main(): - a = 0.0 # Lower bound of integration - b = 1.0 # Upper bound of integration - steps = 10.0 # define number of steps or resolution - boundary = [a, b] # define boundary of integration - y = method_1(boundary, steps) + """ + Main function to test the trapezoidal rule. + :a: Lower bound of integration + :b: Upper bound of integration + :steps: define number of steps or resolution + :boundary: define boundary of integration + + >>> main() + y = 0.3349999999999999 + """ + a = 0.0 + b = 1.0 + steps = 10.0 + boundary = [a, b] + y = trapezoidal_rule(boundary, steps) print(f"y = {y}") if __name__ == "__main__": + import doctest + + doctest.testmod() main() diff --git a/maths/triplet_sum.py b/maths/triplet_sum.py index af77ed145bce..e74f67daad47 100644 --- a/maths/triplet_sum.py +++ b/maths/triplet_sum.py @@ -3,6 +3,7 @@ we are required to find a triplet from the array such that it's sum is equal to the target. """ + from __future__ import annotations from itertools import permutations diff --git a/maths/twin_prime.py b/maths/twin_prime.py index e6ac0cc7805b..912b10b366c0 100644 --- a/maths/twin_prime.py +++ b/maths/twin_prime.py @@ -32,7 +32,8 @@ def twin_prime(number: int) -> int: TypeError: Input value of [number=6.0] must be an integer """ if not isinstance(number, int): - raise TypeError(f"Input value of [number={number}] must be an integer") + msg = f"Input value of [number={number}] must be an integer" + raise TypeError(msg) if is_prime(number) and is_prime(number + 2): return number + 2 else: diff --git a/maths/two_pointer.py b/maths/two_pointer.py index d0fb0fc9c2f1..8a6d8eb7aff0 100644 --- a/maths/two_pointer.py +++ b/maths/two_pointer.py @@ -17,6 +17,7 @@ [1]: https://github.com/TheAlgorithms/Python/blob/master/other/two_sum.py """ + from __future__ import annotations diff --git a/maths/two_sum.py b/maths/two_sum.py index 12ad332d6c4e..58c933a5078a 100644 --- a/maths/two_sum.py +++ b/maths/two_sum.py @@ -11,6 +11,7 @@ Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1]. """ + from __future__ import annotations diff --git a/maths/volume.py b/maths/volume.py index 1da4584c893e..08bdf72b013b 100644 --- a/maths/volume.py +++ b/maths/volume.py @@ -1,16 +1,19 @@ """ Find the volume of various shapes. + * https://en.wikipedia.org/wiki/Volume * https://en.wikipedia.org/wiki/Spherical_cap """ + from __future__ import annotations -from math import pi, pow +from math import pi, pow # noqa: A004 -def vol_cube(side_length: int | float) -> float: +def vol_cube(side_length: float) -> float: """ Calculate the Volume of a Cube. + >>> vol_cube(1) 1.0 >>> vol_cube(3) @@ -32,6 +35,7 @@ def vol_cube(side_length: int | float) -> float: def vol_spherical_cap(height: float, radius: float) -> float: """ Calculate the volume of the spherical cap. + >>> vol_spherical_cap(1, 2) 5.235987755982988 >>> vol_spherical_cap(1.6, 2.6) @@ -56,20 +60,29 @@ def vol_spherical_cap(height: float, radius: float) -> float: def vol_spheres_intersect( radius_1: float, radius_2: float, centers_distance: float ) -> float: - """ + r""" Calculate the volume of the intersection of two spheres. + The intersection is composed by two spherical caps and therefore its volume is the - sum of the volumes of the spherical caps. First, it calculates the heights (h1, h2) - of the spherical caps, then the two volumes and it returns the sum. + sum of the volumes of the spherical caps. + First, it calculates the heights :math:`(h_1, h_2)` of the spherical caps, + then the two volumes and it returns the sum. The height formulas are - h1 = (radius_1 - radius_2 + centers_distance) - * (radius_1 + radius_2 - centers_distance) - / (2 * centers_distance) - h2 = (radius_2 - radius_1 + centers_distance) - * (radius_2 + radius_1 - centers_distance) - / (2 * centers_distance) - if centers_distance is 0 then it returns the volume of the smallers sphere - :return vol_spherical_cap(h1, radius_2) + vol_spherical_cap(h2, radius_1) + + .. math:: + h_1 = \frac{(radius_1 - radius_2 + centers\_distance) + \cdot (radius_1 + radius_2 - centers\_distance)} + {2 \cdot centers\_distance} + + h_2 = \frac{(radius_2 - radius_1 + centers\_distance) + \cdot (radius_2 + radius_1 - centers\_distance)} + {2 \cdot centers\_distance} + + if `centers_distance` is 0 then it returns the volume of the smallers sphere + + :return: ``vol_spherical_cap`` (:math:`h_1`, :math:`radius_2`) + + ``vol_spherical_cap`` (:math:`h_2`, :math:`radius_1`) + >>> vol_spheres_intersect(2, 2, 1) 21.205750411731103 >>> vol_spheres_intersect(2.6, 2.6, 1.6) @@ -111,14 +124,18 @@ def vol_spheres_intersect( def vol_spheres_union( radius_1: float, radius_2: float, centers_distance: float ) -> float: - """ + r""" Calculate the volume of the union of two spheres that possibly intersect. - It is the sum of sphere A and sphere B minus their intersection. - First, it calculates the volumes (v1, v2) of the spheres, - then the volume of the intersection (i) and it returns the sum v1+v2-i. - If centers_distance is 0 then it returns the volume of the larger sphere - :return vol_sphere(radius_1) + vol_sphere(radius_2) - - vol_spheres_intersect(radius_1, radius_2, centers_distance) + + It is the sum of sphere :math:`A` and sphere :math:`B` minus their intersection. + First, it calculates the volumes :math:`(v_1, v_2)` of the spheres, + then the volume of the intersection :math:`i` and + it returns the sum :math:`v_1 + v_2 - i`. + If `centers_distance` is 0 then it returns the volume of the larger sphere + + :return: ``vol_sphere`` (:math:`radius_1`) + ``vol_sphere`` (:math:`radius_2`) + - ``vol_spheres_intersect`` + (:math:`radius_1`, :math:`radius_2`, :math:`centers\_distance`) >>> vol_spheres_union(2, 2, 1) 45.814892864851146 @@ -156,7 +173,9 @@ def vol_spheres_union( def vol_cuboid(width: float, height: float, length: float) -> float: """ Calculate the Volume of a Cuboid. - :return multiple of width, length and height + + :return: multiple of `width`, `length` and `height` + >>> vol_cuboid(1, 1, 1) 1.0 >>> vol_cuboid(1, 2, 3) @@ -184,10 +203,12 @@ def vol_cuboid(width: float, height: float, length: float) -> float: def vol_cone(area_of_base: float, height: float) -> float: - """ - Calculate the Volume of a Cone. - Wikipedia reference: https://en.wikipedia.org/wiki/Cone - :return (1/3) * area_of_base * height + r""" + | Calculate the Volume of a Cone. + | Wikipedia reference: https://en.wikipedia.org/wiki/Cone + + :return: :math:`\frac{1}{3} \cdot area\_of\_base \cdot height` + >>> vol_cone(10, 3) 10.0 >>> vol_cone(1, 1) @@ -211,10 +232,12 @@ def vol_cone(area_of_base: float, height: float) -> float: def vol_right_circ_cone(radius: float, height: float) -> float: - """ - Calculate the Volume of a Right Circular Cone. - Wikipedia reference: https://en.wikipedia.org/wiki/Cone - :return (1/3) * pi * radius^2 * height + r""" + | Calculate the Volume of a Right Circular Cone. + | Wikipedia reference: https://en.wikipedia.org/wiki/Cone + + :return: :math:`\frac{1}{3} \cdot \pi \cdot radius^2 \cdot height` + >>> vol_right_circ_cone(2, 3) 12.566370614359172 >>> vol_right_circ_cone(0, 0) @@ -236,10 +259,12 @@ def vol_right_circ_cone(radius: float, height: float) -> float: def vol_prism(area_of_base: float, height: float) -> float: - """ - Calculate the Volume of a Prism. - Wikipedia reference: https://en.wikipedia.org/wiki/Prism_(geometry) - :return V = Bh + r""" + | Calculate the Volume of a Prism. + | Wikipedia reference: https://en.wikipedia.org/wiki/Prism_(geometry) + + :return: :math:`V = B \cdot h` + >>> vol_prism(10, 2) 20.0 >>> vol_prism(11, 1) @@ -263,10 +288,12 @@ def vol_prism(area_of_base: float, height: float) -> float: def vol_pyramid(area_of_base: float, height: float) -> float: - """ - Calculate the Volume of a Pyramid. - Wikipedia reference: https://en.wikipedia.org/wiki/Pyramid_(geometry) - :return (1/3) * Bh + r""" + | Calculate the Volume of a Pyramid. + | Wikipedia reference: https://en.wikipedia.org/wiki/Pyramid_(geometry) + + :return: :math:`\frac{1}{3} \cdot B \cdot h` + >>> vol_pyramid(10, 3) 10.0 >>> vol_pyramid(1.5, 3) @@ -290,10 +317,12 @@ def vol_pyramid(area_of_base: float, height: float) -> float: def vol_sphere(radius: float) -> float: - """ - Calculate the Volume of a Sphere. - Wikipedia reference: https://en.wikipedia.org/wiki/Sphere - :return (4/3) * pi * r^3 + r""" + | Calculate the Volume of a Sphere. + | Wikipedia reference: https://en.wikipedia.org/wiki/Sphere + + :return: :math:`\frac{4}{3} \cdot \pi \cdot r^3` + >>> vol_sphere(5) 523.5987755982989 >>> vol_sphere(1) @@ -314,10 +343,13 @@ def vol_sphere(radius: float) -> float: def vol_hemisphere(radius: float) -> float: - """Calculate the volume of a hemisphere - Wikipedia reference: https://en.wikipedia.org/wiki/Hemisphere - Other references: https://www.cuemath.com/geometry/hemisphere - :return 2/3 * pi * radius^3 + r""" + | Calculate the volume of a hemisphere + | Wikipedia reference: https://en.wikipedia.org/wiki/Hemisphere + | Other references: https://www.cuemath.com/geometry/hemisphere + + :return: :math:`\frac{2}{3} \cdot \pi \cdot radius^3` + >>> vol_hemisphere(1) 2.0943951023931953 >>> vol_hemisphere(7) @@ -338,9 +370,12 @@ def vol_hemisphere(radius: float) -> float: def vol_circular_cylinder(radius: float, height: float) -> float: - """Calculate the Volume of a Circular Cylinder. - Wikipedia reference: https://en.wikipedia.org/wiki/Cylinder - :return pi * radius^2 * height + r""" + | Calculate the Volume of a Circular Cylinder. + | Wikipedia reference: https://en.wikipedia.org/wiki/Cylinder + + :return: :math:`\pi \cdot radius^2 \cdot height` + >>> vol_circular_cylinder(1, 1) 3.141592653589793 >>> vol_circular_cylinder(4, 3) @@ -367,7 +402,9 @@ def vol_circular_cylinder(radius: float, height: float) -> float: def vol_hollow_circular_cylinder( inner_radius: float, outer_radius: float, height: float ) -> float: - """Calculate the Volume of a Hollow Circular Cylinder. + """ + Calculate the Volume of a Hollow Circular Cylinder. + >>> vol_hollow_circular_cylinder(1, 2, 3) 28.274333882308138 >>> vol_hollow_circular_cylinder(1.6, 2.6, 3.6) @@ -404,8 +441,9 @@ def vol_hollow_circular_cylinder( def vol_conical_frustum(height: float, radius_1: float, radius_2: float) -> float: - """Calculate the Volume of a Conical Frustum. - Wikipedia reference: https://en.wikipedia.org/wiki/Frustum + """ + | Calculate the Volume of a Conical Frustum. + | Wikipedia reference: https://en.wikipedia.org/wiki/Frustum >>> vol_conical_frustum(45, 7, 28) 48490.482608158454 @@ -442,9 +480,12 @@ def vol_conical_frustum(height: float, radius_1: float, radius_2: float) -> floa def vol_torus(torus_radius: float, tube_radius: float) -> float: - """Calculate the Volume of a Torus. - Wikipedia reference: https://en.wikipedia.org/wiki/Torus - :return 2pi^2 * torus_radius * tube_radius^2 + r""" + | Calculate the Volume of a Torus. + | Wikipedia reference: https://en.wikipedia.org/wiki/Torus + + :return: :math:`2 \pi^2 \cdot torus\_radius \cdot tube\_radius^2` + >>> vol_torus(1, 1) 19.739208802178716 >>> vol_torus(4, 3) @@ -469,6 +510,36 @@ def vol_torus(torus_radius: float, tube_radius: float) -> float: return 2 * pow(pi, 2) * torus_radius * pow(tube_radius, 2) +def vol_icosahedron(tri_side: float) -> float: + """ + | Calculate the Volume of an Icosahedron. + | Wikipedia reference: https://en.wikipedia.org/wiki/Regular_icosahedron + + >>> from math import isclose + >>> isclose(vol_icosahedron(2.5), 34.088984228514256) + True + >>> isclose(vol_icosahedron(10), 2181.694990624912374) + True + >>> isclose(vol_icosahedron(5), 272.711873828114047) + True + >>> isclose(vol_icosahedron(3.49), 92.740688412033628) + True + >>> vol_icosahedron(0) + 0.0 + >>> vol_icosahedron(-1) + Traceback (most recent call last): + ... + ValueError: vol_icosahedron() only accepts non-negative values + >>> vol_icosahedron(-0.2) + Traceback (most recent call last): + ... + ValueError: vol_icosahedron() only accepts non-negative values + """ + if tri_side < 0: + raise ValueError("vol_icosahedron() only accepts non-negative values") + return tri_side**3 * (3 + 5**0.5) * 5 / 12 + + def main(): """Print the Results of Various Volume Calculations.""" print("Volumes:") @@ -489,6 +560,7 @@ def main(): print( f"Hollow Circular Cylinder: {vol_hollow_circular_cylinder(1, 2, 3) = }" ) # ~= 28.3 + print(f"Icosahedron: {vol_icosahedron(2.5) = }") # ~=34.09 if __name__ == "__main__": diff --git a/maths/zellers_congruence.py b/maths/zellers_congruence.py index 483fb000f86b..b958ed3b8659 100644 --- a/maths/zellers_congruence.py +++ b/maths/zellers_congruence.py @@ -4,13 +4,14 @@ def zeller(date_input: str) -> str: """ - Zellers Congruence Algorithm - Find the day of the week for nearly any Gregorian or Julian calendar date + | Zellers Congruence Algorithm + | Find the day of the week for nearly any Gregorian or Julian calendar date >>> zeller('01-31-2010') 'Your date 01-31-2010, is a Sunday!' - Validate out of range month + Validate out of range month: + >>> zeller('13-31-2010') Traceback (most recent call last): ... @@ -21,6 +22,7 @@ def zeller(date_input: str) -> str: ValueError: invalid literal for int() with base 10: '.2' Validate out of range date: + >>> zeller('01-33-2010') Traceback (most recent call last): ... @@ -31,30 +33,35 @@ def zeller(date_input: str) -> str: ValueError: invalid literal for int() with base 10: '.4' Validate second separator: + >>> zeller('01-31*2010') Traceback (most recent call last): ... ValueError: Date separator must be '-' or '/' Validate first separator: + >>> zeller('01^31-2010') Traceback (most recent call last): ... ValueError: Date separator must be '-' or '/' Validate out of range year: + >>> zeller('01-31-8999') Traceback (most recent call last): ... ValueError: Year out of range. There has to be some sort of limit...right? Test null input: + >>> zeller() Traceback (most recent call last): ... TypeError: zeller() missing 1 required positional argument: 'date_input' - Test length of date_input: + Test length of `date_input`: + >>> zeller('') Traceback (most recent call last): ... diff --git a/matrix/count_negative_numbers_in_sorted_matrix.py b/matrix/count_negative_numbers_in_sorted_matrix.py new file mode 100644 index 000000000000..2799ff3b45fe --- /dev/null +++ b/matrix/count_negative_numbers_in_sorted_matrix.py @@ -0,0 +1,151 @@ +""" +Given an matrix of numbers in which all rows and all columns are sorted in decreasing +order, return the number of negative numbers in grid. + +Reference: https://leetcode.com/problems/count-negative-numbers-in-a-sorted-matrix +""" + + +def generate_large_matrix() -> list[list[int]]: + """ + >>> generate_large_matrix() # doctest: +ELLIPSIS + [[1000, ..., -999], [999, ..., -1001], ..., [2, ..., -1998]] + """ + return [list(range(1000 - i, -1000 - i, -1)) for i in range(1000)] + + +grid = generate_large_matrix() +test_grids = ( + [[4, 3, 2, -1], [3, 2, 1, -1], [1, 1, -1, -2], [-1, -1, -2, -3]], + [[3, 2], [1, 0]], + [[7, 7, 6]], + [[7, 7, 6], [-1, -2, -3]], + grid, +) + + +def validate_grid(grid: list[list[int]]) -> None: + """ + Validate that the rows and columns of the grid is sorted in decreasing order. + >>> for grid in test_grids: + ... validate_grid(grid) + """ + assert all(row == sorted(row, reverse=True) for row in grid) + assert all(list(col) == sorted(col, reverse=True) for col in zip(*grid)) + + +def find_negative_index(array: list[int]) -> int: + """ + Find the smallest negative index + + >>> find_negative_index([0,0,0,0]) + 4 + >>> find_negative_index([4,3,2,-1]) + 3 + >>> find_negative_index([1,0,-1,-10]) + 2 + >>> find_negative_index([0,0,0,-1]) + 3 + >>> find_negative_index([11,8,7,-3,-5,-9]) + 3 + >>> find_negative_index([-1,-1,-2,-3]) + 0 + >>> find_negative_index([5,1,0]) + 3 + >>> find_negative_index([-5,-5,-5]) + 0 + >>> find_negative_index([0]) + 1 + >>> find_negative_index([]) + 0 + """ + left = 0 + right = len(array) - 1 + + # Edge cases such as no values or all numbers are negative. + if not array or array[0] < 0: + return 0 + + while right + 1 > left: + mid = (left + right) // 2 + num = array[mid] + + # Num must be negative and the index must be greater than or equal to 0. + if num < 0 and array[mid - 1] >= 0: + return mid + + if num >= 0: + left = mid + 1 + else: + right = mid - 1 + # No negative numbers so return the last index of the array + 1 which is the length. + return len(array) + + +def count_negatives_binary_search(grid: list[list[int]]) -> int: + """ + An O(m logn) solution that uses binary search in order to find the boundary between + positive and negative numbers + + >>> [count_negatives_binary_search(grid) for grid in test_grids] + [8, 0, 0, 3, 1498500] + """ + total = 0 + bound = len(grid[0]) + + for i in range(len(grid)): + bound = find_negative_index(grid[i][:bound]) + total += bound + return (len(grid) * len(grid[0])) - total + + +def count_negatives_brute_force(grid: list[list[int]]) -> int: + """ + This solution is O(n^2) because it iterates through every column and row. + + >>> [count_negatives_brute_force(grid) for grid in test_grids] + [8, 0, 0, 3, 1498500] + """ + return len([number for row in grid for number in row if number < 0]) + + +def count_negatives_brute_force_with_break(grid: list[list[int]]) -> int: + """ + Similar to the brute force solution above but uses break in order to reduce the + number of iterations. + + >>> [count_negatives_brute_force_with_break(grid) for grid in test_grids] + [8, 0, 0, 3, 1498500] + """ + total = 0 + for row in grid: + for i, number in enumerate(row): + if number < 0: + total += len(row) - i + break + return total + + +def benchmark() -> None: + """Benchmark our functions next to each other""" + from timeit import timeit + + print("Running benchmarks") + setup = ( + "from __main__ import count_negatives_binary_search, " + "count_negatives_brute_force, count_negatives_brute_force_with_break, grid" + ) + for func in ( + "count_negatives_binary_search", # took 0.7727 seconds + "count_negatives_brute_force_with_break", # took 4.6505 seconds + "count_negatives_brute_force", # took 12.8160 seconds + ): + time = timeit(f"{func}(grid=grid)", setup=setup, number=500) + print(f"{func}() took {time:0.4f} seconds") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + benchmark() diff --git a/matrix/cramers_rule_2x2.py b/matrix/cramers_rule_2x2.py index 4f52dbe646ad..081035bec002 100644 --- a/matrix/cramers_rule_2x2.py +++ b/matrix/cramers_rule_2x2.py @@ -73,12 +73,11 @@ def cramers_rule_2x2(equation1: list[int], equation2: list[int]) -> tuple[float, raise ValueError("Infinite solutions. (Consistent system)") else: raise ValueError("No solution. (Inconsistent system)") + elif determinant_x == determinant_y == 0: + # Trivial solution (Inconsistent system) + return (0.0, 0.0) else: - if determinant_x == determinant_y == 0: - # Trivial solution (Inconsistent system) - return (0.0, 0.0) - else: - x = determinant_x / determinant - y = determinant_y / determinant - # Non-Trivial Solution (Consistent system) - return (x, y) + x = determinant_x / determinant + y = determinant_y / determinant + # Non-Trivial Solution (Consistent system) + return (x, y) diff --git a/matrix/largest_square_area_in_matrix.py b/matrix/largest_square_area_in_matrix.py index a93369c56bbd..16263fb798f1 100644 --- a/matrix/largest_square_area_in_matrix.py +++ b/matrix/largest_square_area_in_matrix.py @@ -31,7 +31,7 @@ Approach: We initialize another matrix (dp) with the same dimensions -as the original one initialized with all 0’s. +as the original one initialized with all 0's. dp_array(i,j) represents the side length of the maximum square whose bottom right corner is the cell with index (i,j) in the original matrix. @@ -39,7 +39,7 @@ Starting from index (0,0), for every 1 found in the original matrix, we update the value of the current element as -dp_array(i,j)=dp_array(dp(i−1,j),dp_array(i−1,j−1),dp_array(i,j−1)) + 1. +dp_array(i,j)=dp_array(dp(i-1,j),dp_array(i-1,j-1),dp_array(i,j-1)) + 1. """ diff --git a/matrix/matrix_based_game.py b/matrix/matrix_based_game.py new file mode 100644 index 000000000000..6181086c6704 --- /dev/null +++ b/matrix/matrix_based_game.py @@ -0,0 +1,284 @@ +""" +Matrix-Based Game Script +========================= +This script implements a matrix-based game where players interact with a grid of +elements. The primary goals are to: +- Identify connected elements of the same type from a selected position. +- Remove those elements, adjust the matrix by simulating gravity, and reorganize empty + columns. +- Calculate and display the score based on the number of elements removed in each move. + +Functions: +----------- +1. `find_repeat`: Finds all connected elements of the same type. +2. `increment_score`: Calculates the score for a given move. +3. `move_x`: Simulates gravity in a column. +4. `move_y`: Reorganizes the matrix by shifting columns leftward when a column becomes + empty. +5. `play`: Executes a single move, updating the matrix and returning the score. + +Input Format: +-------------- +1. Matrix size (`lines`): Integer specifying the size of the matrix (N x N). +2. Matrix content (`matrix`): Rows of the matrix, each consisting of characters. +3. Number of moves (`movs`): Integer indicating the number of moves. +4. List of moves (`movements`): A comma-separated string of coordinates for each move. + +(0,0) position starts from first left column to last right, and below row to up row + + +Example Input: +--------------- +4 +RRBG +RBBG +YYGG +XYGG +2 +0 1,1 1 + +Example (0,0) = X + +Output: +-------- +The script outputs the total score after processing all moves. + +Usage: +------- +Run the script and provide the required inputs as prompted. + +""" + + +def validate_matrix_size(size: int) -> None: + """ + >>> validate_matrix_size(-1) + Traceback (most recent call last): + ... + ValueError: Matrix size must be a positive integer. + """ + if not isinstance(size, int) or size <= 0: + raise ValueError("Matrix size must be a positive integer.") + + +def validate_matrix_content(matrix: list[str], size: int) -> None: + """ + Validates that the number of elements in the matrix matches the given size. + + >>> validate_matrix_content(['aaaa', 'aaaa', 'aaaa', 'aaaa'], 3) + Traceback (most recent call last): + ... + ValueError: The matrix dont match with size. + >>> validate_matrix_content(['aa%', 'aaa', 'aaa'], 3) + Traceback (most recent call last): + ... + ValueError: Matrix rows can only contain letters and numbers. + >>> validate_matrix_content(['aaa', 'aaa', 'aaaa'], 3) + Traceback (most recent call last): + ... + ValueError: Each row in the matrix must have exactly 3 characters. + """ + print(matrix) + if len(matrix) != size: + raise ValueError("The matrix dont match with size.") + for row in matrix: + if len(row) != size: + msg = f"Each row in the matrix must have exactly {size} characters." + raise ValueError(msg) + if not all(char.isalnum() for char in row): + raise ValueError("Matrix rows can only contain letters and numbers.") + + +def validate_moves(moves: list[tuple[int, int]], size: int) -> None: + """ + >>> validate_moves([(1, 2), (-1, 0)], 3) + Traceback (most recent call last): + ... + ValueError: Move is out of bounds for a matrix. + """ + for move in moves: + x, y = move + if not (0 <= x < size and 0 <= y < size): + raise ValueError("Move is out of bounds for a matrix.") + + +def parse_moves(input_str: str) -> list[tuple[int, int]]: + """ + >>> parse_moves("0 1, 1 1") + [(0, 1), (1, 1)] + >>> parse_moves("0 1, 1 1, 2") + Traceback (most recent call last): + ... + ValueError: Each move must have exactly two numbers. + >>> parse_moves("0 1, 1 1, 2 4 5 6") + Traceback (most recent call last): + ... + ValueError: Each move must have exactly two numbers. + """ + moves = [] + for pair in input_str.split(","): + parts = pair.strip().split() + if len(parts) != 2: + raise ValueError("Each move must have exactly two numbers.") + x, y = map(int, parts) + moves.append((x, y)) + return moves + + +def find_repeat( + matrix_g: list[list[str]], row: int, column: int, size: int +) -> set[tuple[int, int]]: + """ + Finds all connected elements of the same type from a given position. + + >>> find_repeat([['A', 'B', 'A'], ['A', 'B', 'A'], ['A', 'A', 'A']], 0, 0, 3) + {(1, 2), (2, 1), (0, 0), (2, 0), (0, 2), (2, 2), (1, 0)} + >>> find_repeat([['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-']], 1, 1, 3) + set() + """ + + column = size - 1 - column + visited = set() + repeated = set() + + if (color := matrix_g[column][row]) != "-": + + def dfs(row_n: int, column_n: int) -> None: + if row_n < 0 or row_n >= size or column_n < 0 or column_n >= size: + return + if (row_n, column_n) in visited: + return + visited.add((row_n, column_n)) + if matrix_g[row_n][column_n] == color: + repeated.add((row_n, column_n)) + dfs(row_n - 1, column_n) + dfs(row_n + 1, column_n) + dfs(row_n, column_n - 1) + dfs(row_n, column_n + 1) + + dfs(column, row) + + return repeated + + +def increment_score(count: int) -> int: + """ + Calculates the score for a move based on the number of elements removed. + + >>> increment_score(3) + 6 + >>> increment_score(0) + 0 + """ + return int(count * (count + 1) / 2) + + +def move_x(matrix_g: list[list[str]], column: int, size: int) -> list[list[str]]: + """ + Simulates gravity in a specific column. + + >>> move_x([['-', 'A'], ['-', '-'], ['-', 'C']], 1, 2) + [['-', '-'], ['-', 'A'], ['-', 'C']] + """ + + new_list = [] + + for row in range(size): + if matrix_g[row][column] != "-": + new_list.append(matrix_g[row][column]) + else: + new_list.insert(0, matrix_g[row][column]) + for row in range(size): + matrix_g[row][column] = new_list[row] + return matrix_g + + +def move_y(matrix_g: list[list[str]], size: int) -> list[list[str]]: + """ + Shifts all columns leftward when an entire column becomes empty. + + >>> move_y([['-', 'A'], ['-', '-'], ['-', 'C']], 2) + [['A', '-'], ['-', '-'], ['-', 'C']] + """ + + empty_columns = [] + + for column in range(size - 1, -1, -1): + if all(matrix_g[row][column] == "-" for row in range(size)): + empty_columns.append(column) + + for column in empty_columns: + for col in range(column + 1, size): + for row in range(size): + matrix_g[row][col - 1] = matrix_g[row][col] + for row in range(size): + matrix_g[row][-1] = "-" + + return matrix_g + + +def play( + matrix_g: list[list[str]], pos_x: int, pos_y: int, size: int +) -> tuple[list[list[str]], int]: + """ + Processes a single move, updating the matrix and calculating the score. + + >>> play([['R', 'G'], ['R', 'G']], 0, 0, 2) + ([['G', '-'], ['G', '-']], 3) + """ + + same_colors = find_repeat(matrix_g, pos_x, pos_y, size) + + if len(same_colors) != 0: + for pos in same_colors: + matrix_g[pos[0]][pos[1]] = "-" + for column in range(size): + matrix_g = move_x(matrix_g, column, size) + + matrix_g = move_y(matrix_g, size) + + return (matrix_g, increment_score(len(same_colors))) + + +def process_game(size: int, matrix: list[str], moves: list[tuple[int, int]]) -> int: + """Processes the game logic for the given matrix and moves. + + Args: + size (int): Size of the game board. + matrix (List[str]): Initial game matrix. + moves (List[Tuple[int, int]]): List of moves as (x, y) coordinates. + + Returns: + int: The total score obtained. + >>> process_game(3, ['aaa', 'bbb', 'ccc'], [(0, 0)]) + 6 + """ + + game_matrix = [list(row) for row in matrix] + total_score = 0 + + for move in moves: + pos_x, pos_y = move + game_matrix, score = play(game_matrix, pos_x, pos_y, size) + total_score += score + + return total_score + + +if __name__ == "__main__": + import doctest + + doctest.testmod(verbose=True) + try: + size = int(input("Enter the size of the matrix: ")) + validate_matrix_size(size) + print(f"Enter the {size} rows of the matrix:") + matrix = [input(f"Row {i + 1}: ") for i in range(size)] + validate_matrix_content(matrix, size) + moves_input = input("Enter the moves (e.g., '0 0, 1 1'): ") + moves = parse_moves(moves_input) + validate_moves(moves, size) + score = process_game(size, matrix, moves) + print(f"Total score: {score}") + except ValueError as e: + print(f"{e}") diff --git a/matrix/matrix_class.py b/matrix/matrix_class.py index a73e8b92a286..a5940a38e836 100644 --- a/matrix/matrix_class.py +++ b/matrix/matrix_class.py @@ -141,7 +141,7 @@ def num_columns(self) -> int: @property def order(self) -> tuple[int, int]: - return (self.num_rows, self.num_columns) + return self.num_rows, self.num_columns @property def is_square(self) -> bool: @@ -315,7 +315,7 @@ def __sub__(self, other: Matrix) -> Matrix: ] ) - def __mul__(self, other: Matrix | int | float) -> Matrix: + def __mul__(self, other: Matrix | float) -> Matrix: if isinstance(other, (int, float)): return Matrix( [[int(element * other) for element in row] for row in self.rows] diff --git a/matrix/matrix_equalization.py b/matrix/matrix_equalization.py new file mode 100644 index 000000000000..e7e76505cf63 --- /dev/null +++ b/matrix/matrix_equalization.py @@ -0,0 +1,55 @@ +from sys import maxsize + + +def array_equalization(vector: list[int], step_size: int) -> int: + """ + This algorithm equalizes all elements of the input vector + to a common value, by making the minimal number of + "updates" under the constraint of a step size (step_size). + + >>> array_equalization([1, 1, 6, 2, 4, 6, 5, 1, 7, 2, 2, 1, 7, 2, 2], 4) + 4 + >>> array_equalization([22, 81, 88, 71, 22, 81, 632, 81, 81, 22, 92], 2) + 5 + >>> array_equalization([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 5) + 0 + >>> array_equalization([22, 22, 22, 33, 33, 33], 2) + 2 + >>> array_equalization([1, 2, 3], 0) + Traceback (most recent call last): + ValueError: Step size must be positive and non-zero. + >>> array_equalization([1, 2, 3], -1) + Traceback (most recent call last): + ValueError: Step size must be positive and non-zero. + >>> array_equalization([1, 2, 3], 0.5) + Traceback (most recent call last): + ValueError: Step size must be an integer. + >>> array_equalization([1, 2, 3], maxsize) + 1 + """ + if step_size <= 0: + raise ValueError("Step size must be positive and non-zero.") + if not isinstance(step_size, int): + raise ValueError("Step size must be an integer.") + + unique_elements = set(vector) + min_updates = maxsize + + for element in unique_elements: + elem_index = 0 + updates = 0 + while elem_index < len(vector): + if vector[elem_index] != element: + updates += 1 + elem_index += step_size + else: + elem_index += 1 + min_updates = min(min_updates, updates) + + return min_updates + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/matrix/matrix_multiplication_recursion.py b/matrix/matrix_multiplication_recursion.py new file mode 100644 index 000000000000..57c4d80de017 --- /dev/null +++ b/matrix/matrix_multiplication_recursion.py @@ -0,0 +1,181 @@ +# @Author : ojas-wani +# @File : matrix_multiplication_recursion.py +# @Date : 10/06/2023 + + +""" +Perform matrix multiplication using a recursive algorithm. +https://en.wikipedia.org/wiki/Matrix_multiplication +""" + +# type Matrix = list[list[int]] # psf/black currenttly fails on this line +Matrix = list[list[int]] + +matrix_1_to_4 = [ + [1, 2], + [3, 4], +] + +matrix_5_to_8 = [ + [5, 6], + [7, 8], +] + +matrix_5_to_9_high = [ + [5, 6], + [7, 8], + [9], +] + +matrix_5_to_9_wide = [ + [5, 6], + [7, 8, 9], +] + +matrix_count_up = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], +] + +matrix_unordered = [ + [5, 8, 1, 2], + [6, 7, 3, 0], + [4, 5, 9, 1], + [2, 6, 10, 14], +] +matrices = ( + matrix_1_to_4, + matrix_5_to_8, + matrix_5_to_9_high, + matrix_5_to_9_wide, + matrix_count_up, + matrix_unordered, +) + + +def is_square(matrix: Matrix) -> bool: + """ + >>> is_square([]) + True + >>> is_square(matrix_1_to_4) + True + >>> is_square(matrix_5_to_9_high) + False + """ + len_matrix = len(matrix) + return all(len(row) == len_matrix for row in matrix) + + +def matrix_multiply(matrix_a: Matrix, matrix_b: Matrix) -> Matrix: + """ + >>> matrix_multiply(matrix_1_to_4, matrix_5_to_8) + [[19, 22], [43, 50]] + """ + return [ + [sum(a * b for a, b in zip(row, col)) for col in zip(*matrix_b)] + for row in matrix_a + ] + + +def matrix_multiply_recursive(matrix_a: Matrix, matrix_b: Matrix) -> Matrix: + """ + :param matrix_a: A square Matrix. + :param matrix_b: Another square Matrix with the same dimensions as matrix_a. + :return: Result of matrix_a * matrix_b. + :raises ValueError: If the matrices cannot be multiplied. + + >>> matrix_multiply_recursive([], []) + [] + >>> matrix_multiply_recursive(matrix_1_to_4, matrix_5_to_8) + [[19, 22], [43, 50]] + >>> matrix_multiply_recursive(matrix_count_up, matrix_unordered) + [[37, 61, 74, 61], [105, 165, 166, 129], [173, 269, 258, 197], [241, 373, 350, 265]] + >>> matrix_multiply_recursive(matrix_1_to_4, matrix_5_to_9_wide) + Traceback (most recent call last): + ... + ValueError: Invalid matrix dimensions + >>> matrix_multiply_recursive(matrix_1_to_4, matrix_5_to_9_high) + Traceback (most recent call last): + ... + ValueError: Invalid matrix dimensions + >>> matrix_multiply_recursive(matrix_1_to_4, matrix_count_up) + Traceback (most recent call last): + ... + ValueError: Invalid matrix dimensions + """ + if not matrix_a or not matrix_b: + return [] + if not all( + (len(matrix_a) == len(matrix_b), is_square(matrix_a), is_square(matrix_b)) + ): + raise ValueError("Invalid matrix dimensions") + + # Initialize the result matrix with zeros + result = [[0] * len(matrix_b[0]) for _ in range(len(matrix_a))] + + # Recursive multiplication of matrices + def multiply( + i_loop: int, + j_loop: int, + k_loop: int, + matrix_a: Matrix, + matrix_b: Matrix, + result: Matrix, + ) -> None: + """ + :param matrix_a: A square Matrix. + :param matrix_b: Another square Matrix with the same dimensions as matrix_a. + :param result: Result matrix + :param i: Index used for iteration during multiplication. + :param j: Index used for iteration during multiplication. + :param k: Index used for iteration during multiplication. + >>> 0 > 1 # Doctests in inner functions are never run + True + """ + if i_loop >= len(matrix_a): + return + if j_loop >= len(matrix_b[0]): + return multiply(i_loop + 1, 0, 0, matrix_a, matrix_b, result) + if k_loop >= len(matrix_b): + return multiply(i_loop, j_loop + 1, 0, matrix_a, matrix_b, result) + result[i_loop][j_loop] += matrix_a[i_loop][k_loop] * matrix_b[k_loop][j_loop] + return multiply(i_loop, j_loop, k_loop + 1, matrix_a, matrix_b, result) + + # Perform the recursive matrix multiplication + multiply(0, 0, 0, matrix_a, matrix_b, result) + return result + + +if __name__ == "__main__": + from doctest import testmod + + failure_count, test_count = testmod() + if not failure_count: + matrix_a = matrices[0] + for matrix_b in matrices[1:]: + print("Multiplying:") + for row in matrix_a: + print(row) + print("By:") + for row in matrix_b: + print(row) + print("Result:") + try: + result = matrix_multiply_recursive(matrix_a, matrix_b) + for row in result: + print(row) + assert result == matrix_multiply(matrix_a, matrix_b) + except ValueError as e: + print(f"{e!r}") + print() + matrix_a = matrix_b + + print("Benchmark:") + from functools import partial + from timeit import timeit + + mytimeit = partial(timeit, globals=globals(), number=100_000) + for func in ("matrix_multiply", "matrix_multiply_recursive"): + print(f"{func:>25}(): {mytimeit(f'{func}(matrix_count_up, matrix_unordered)')}") diff --git a/matrix/matrix_operation.py b/matrix/matrix_operation.py index 576094902af4..d63e758f1838 100644 --- a/matrix/matrix_operation.py +++ b/matrix/matrix_operation.py @@ -47,7 +47,7 @@ def subtract(matrix_a: list[list[int]], matrix_b: list[list[int]]) -> list[list[ raise TypeError("Expected a matrix, got int/list instead") -def scalar_multiply(matrix: list[list[int]], n: int | float) -> list[list[float]]: +def scalar_multiply(matrix: list[list[int]], n: float) -> list[list[float]]: """ >>> scalar_multiply([[1,2],[3,4]],5) [[5, 10], [15, 20]] @@ -70,10 +70,11 @@ def multiply(matrix_a: list[list[int]], matrix_b: list[list[int]]) -> list[list[ rows, cols = _verify_matrix_sizes(matrix_a, matrix_b) if cols[0] != rows[1]: - raise ValueError( - f"Cannot multiply matrix of dimensions ({rows[0]},{cols[0]}) " - f"and ({rows[1]},{cols[1]})" + msg = ( + "Cannot multiply matrix of dimensions " + f"({rows[0]},{cols[0]}) and ({rows[1]},{cols[1]})" ) + raise ValueError(msg) return [ [sum(m * n for m, n in zip(i, j)) for j in zip(*matrix_b)] for i in matrix_a ] @@ -174,10 +175,11 @@ def _verify_matrix_sizes( ) -> tuple[tuple[int, int], tuple[int, int]]: shape = _shape(matrix_a) + _shape(matrix_b) if shape[0] != shape[3] or shape[1] != shape[2]: - raise ValueError( - f"operands could not be broadcast together with shape " + msg = ( + "operands could not be broadcast together with shape " f"({shape[0], shape[1]}), ({shape[2], shape[3]})" ) + raise ValueError(msg) return (shape[0], shape[2]), (shape[1], shape[3]) @@ -187,9 +189,7 @@ def main() -> None: matrix_c = [[11, 12, 13, 14], [21, 22, 23, 24], [31, 32, 33, 34], [41, 42, 43, 44]] matrix_d = [[3, 0, 2], [2, 0, -2], [0, 1, 1]] print(f"Add Operation, {add(matrix_a, matrix_b) = } \n") - print( - f"Multiply Operation, {multiply(matrix_a, matrix_b) = } \n", - ) + print(f"Multiply Operation, {multiply(matrix_a, matrix_b) = } \n") print(f"Identity: {identity(5)}\n") print(f"Minor of {matrix_c} = {minor(matrix_c, 1, 2)} \n") print(f"Determinant of {matrix_b} = {determinant(matrix_b)} \n") diff --git a/matrix/median_matrix.py b/matrix/median_matrix.py new file mode 100644 index 000000000000..116e609a587c --- /dev/null +++ b/matrix/median_matrix.py @@ -0,0 +1,38 @@ +""" +https://en.wikipedia.org/wiki/Median +""" + + +def median(matrix: list[list[int]]) -> int: + """ + Calculate the median of a sorted matrix. + + Args: + matrix: A 2D matrix of integers. + + Returns: + The median value of the matrix. + + Examples: + >>> matrix = [[1, 3, 5], [2, 6, 9], [3, 6, 9]] + >>> median(matrix) + 5 + + >>> matrix = [[1, 2, 3], [4, 5, 6]] + >>> median(matrix) + 3 + """ + # Flatten the matrix into a sorted 1D list + linear = sorted(num for row in matrix for num in row) + + # Calculate the middle index + mid = (len(linear) - 1) // 2 + + # Return the median + return linear[mid] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/matrix/searching_in_sorted_matrix.py b/matrix/searching_in_sorted_matrix.py index ddca3b1ce781..f55cc71d6f3a 100644 --- a/matrix/searching_in_sorted_matrix.py +++ b/matrix/searching_in_sorted_matrix.py @@ -1,9 +1,7 @@ from __future__ import annotations -def search_in_a_sorted_matrix( - mat: list[list[int]], m: int, n: int, key: int | float -) -> None: +def search_in_a_sorted_matrix(mat: list[list[int]], m: int, n: int, key: float) -> None: """ >>> search_in_a_sorted_matrix( ... [[2, 5, 7], [4, 8, 13], [9, 11, 15], [12, 17, 20]], 3, 3, 5) diff --git a/matrix/sherman_morrison.py b/matrix/sherman_morrison.py index 39eddfed81f3..e2a09c1d0070 100644 --- a/matrix/sherman_morrison.py +++ b/matrix/sherman_morrison.py @@ -22,7 +22,7 @@ def __init__(self, row: int, column: int, default_value: float = 0) -> None: """ self.row, self.column = row, column - self.array = [[default_value for c in range(column)] for r in range(row)] + self.array = [[default_value for _ in range(column)] for _ in range(row)] def __str__(self) -> str: """ @@ -54,18 +54,18 @@ def single_line(row_vector: list[float]) -> str: def __repr__(self) -> str: return str(self) - def validate_indicies(self, loc: tuple[int, int]) -> bool: + def validate_indices(self, loc: tuple[int, int]) -> bool: """ Check if given indices are valid to pick element from matrix. Example: >>> a = Matrix(2, 6, 0) - >>> a.validate_indicies((2, 7)) + >>> a.validate_indices((2, 7)) False - >>> a.validate_indicies((0, 0)) + >>> a.validate_indices((0, 0)) True """ - if not (isinstance(loc, (list, tuple)) and len(loc) == 2): + if not (isinstance(loc, (list, tuple)) and len(loc) == 2): # noqa: SIM114 return False elif not (0 <= loc[0] < self.row and 0 <= loc[1] < self.column): return False @@ -81,7 +81,7 @@ def __getitem__(self, loc: tuple[int, int]) -> Any: >>> a[1, 0] 7 """ - assert self.validate_indicies(loc) + assert self.validate_indices(loc) return self.array[loc[0]][loc[1]] def __setitem__(self, loc: tuple[int, int], value: float) -> None: @@ -96,7 +96,7 @@ def __setitem__(self, loc: tuple[int, int], value: float) -> None: [ 1, 1, 1] [ 1, 1, 51] """ - assert self.validate_indicies(loc) + assert self.validate_indices(loc) self.array[loc[0]][loc[1]] = value def __add__(self, another: Matrix) -> Matrix: @@ -114,7 +114,8 @@ def __add__(self, another: Matrix) -> Matrix: # Validation assert isinstance(another, Matrix) - assert self.row == another.row and self.column == another.column + assert self.row == another.row + assert self.column == another.column # Add result = Matrix(self.row, self.column) @@ -145,7 +146,7 @@ def __neg__(self) -> Matrix: def __sub__(self, another: Matrix) -> Matrix: return self + (-another) - def __mul__(self, another: int | float | Matrix) -> Matrix: + def __mul__(self, another: float | Matrix) -> Matrix: """ Return self * another. @@ -173,7 +174,8 @@ def __mul__(self, another: int | float | Matrix) -> Matrix: result[r, c] += self[r, i] * another[i, c] return result else: - raise TypeError(f"Unsupported type given for another ({type(another)})") + msg = f"Unsupported type given for another ({type(another)})" + raise TypeError(msg) def transpose(self) -> Matrix: """ @@ -224,7 +226,8 @@ def sherman_morrison(self, u: Matrix, v: Matrix) -> Any: """ # Size validation - assert isinstance(u, Matrix) and isinstance(v, Matrix) + assert isinstance(u, Matrix) + assert isinstance(v, Matrix) assert self.row == self.column == u.row == v.row # u, v should be column vector assert u.column == v.column == 1 # u, v should be column vector @@ -232,7 +235,7 @@ def sherman_morrison(self, u: Matrix, v: Matrix) -> Any: v_t = v.transpose() numerator_factor = (v_t * self * u)[0, 0] + 1 if numerator_factor == 0: - return None # It's not invertable + return None # It's not invertible return self - ((self * u) * (v_t * self) * (1.0 / numerator_factor)) diff --git a/matrix/spiral_print.py b/matrix/spiral_print.py index 0d0be1527aec..88bde1db594d 100644 --- a/matrix/spiral_print.py +++ b/matrix/spiral_print.py @@ -54,7 +54,7 @@ def spiral_print_clockwise(a: list[list[int]]) -> None: return # horizotal printing increasing - for i in range(0, mat_col): + for i in range(mat_col): print(a[0][i]) # vertical printing down for i in range(1, mat_row): @@ -89,7 +89,7 @@ def spiral_traversal(matrix: list[list]) -> list[int]: Algorithm: Step 1. first pop the 0 index list. (which is [1,2,3,4] and concatenate the output of [step 2]) - Step 2. Now perform matrix’s Transpose operation (Change rows to column + Step 2. Now perform matrix's Transpose operation (Change rows to column and vice versa) and reverse the resultant matrix. Step 3. Pass the output of [2nd step], to same recursive function till base case hits. @@ -116,7 +116,9 @@ def spiral_traversal(matrix: list[list]) -> list[int]: [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7] + spiral_traversal([]) """ if matrix: - return list(matrix.pop(0)) + spiral_traversal(list(zip(*matrix))[::-1]) + return list(matrix.pop(0)) + spiral_traversal( + [list(row) for row in zip(*matrix)][::-1] + ) else: return [] diff --git a/matrix/tests/test_matrix_operation.py b/matrix/tests/test_matrix_operation.py index 65b35fd7e78b..21ed7e371fd8 100644 --- a/matrix/tests/test_matrix_operation.py +++ b/matrix/tests/test_matrix_operation.py @@ -12,7 +12,7 @@ import sys import numpy as np -import pytest # type: ignore +import pytest # Custom/local libraries from matrix import matrix_operation as matop @@ -37,8 +37,8 @@ ) def test_addition(mat1, mat2): if (np.array(mat1)).shape < (2, 2) or (np.array(mat2)).shape < (2, 2): + logger.info(f"\n\t{test_addition.__name__} returned integer") with pytest.raises(TypeError): - logger.info(f"\n\t{test_addition.__name__} returned integer") matop.add(mat1, mat2) elif (np.array(mat1)).shape == (np.array(mat2)).shape: logger.info(f"\n\t{test_addition.__name__} with same matrix dims") @@ -46,8 +46,8 @@ def test_addition(mat1, mat2): theo = matop.add(mat1, mat2) assert theo == act else: + logger.info(f"\n\t{test_addition.__name__} with different matrix dims") with pytest.raises(ValueError): - logger.info(f"\n\t{test_addition.__name__} with different matrix dims") matop.add(mat1, mat2) @@ -57,8 +57,8 @@ def test_addition(mat1, mat2): ) def test_subtraction(mat1, mat2): if (np.array(mat1)).shape < (2, 2) or (np.array(mat2)).shape < (2, 2): + logger.info(f"\n\t{test_subtraction.__name__} returned integer") with pytest.raises(TypeError): - logger.info(f"\n\t{test_subtraction.__name__} returned integer") matop.subtract(mat1, mat2) elif (np.array(mat1)).shape == (np.array(mat2)).shape: logger.info(f"\n\t{test_subtraction.__name__} with same matrix dims") @@ -66,8 +66,8 @@ def test_subtraction(mat1, mat2): theo = matop.subtract(mat1, mat2) assert theo == act else: + logger.info(f"\n\t{test_subtraction.__name__} with different matrix dims") with pytest.raises(ValueError): - logger.info(f"\n\t{test_subtraction.__name__} with different matrix dims") assert matop.subtract(mat1, mat2) @@ -86,10 +86,10 @@ def test_multiplication(mat1, mat2): theo = matop.multiply(mat1, mat2) assert theo == act else: + logger.info( + f"\n\t{test_multiplication.__name__} does not meet dim requirements" + ) with pytest.raises(ValueError): - logger.info( - f"\n\t{test_multiplication.__name__} does not meet dim requirements" - ) assert matop.subtract(mat1, mat2) @@ -111,8 +111,8 @@ def test_identity(): @pytest.mark.parametrize("mat", [mat_a, mat_b, mat_c, mat_d, mat_e, mat_f]) def test_transpose(mat): if (np.array(mat)).shape < (2, 2): + logger.info(f"\n\t{test_transpose.__name__} returned integer") with pytest.raises(TypeError): - logger.info(f"\n\t{test_transpose.__name__} returned integer") matop.transpose(mat) else: act = (np.transpose(mat)).tolist() diff --git a/matrix/validate_sudoku_board.py b/matrix/validate_sudoku_board.py new file mode 100644 index 000000000000..a7e08d169059 --- /dev/null +++ b/matrix/validate_sudoku_board.py @@ -0,0 +1,167 @@ +""" +LeetCode 36. Valid Sudoku +https://leetcode.com/problems/valid-sudoku/ +https://en.wikipedia.org/wiki/Sudoku + +Determine if a 9 x 9 Sudoku board is valid. Only the filled cells need to be +validated according to the following rules: + +- Each row must contain the digits 1-9 without repetition. +- Each column must contain the digits 1-9 without repetition. +- Each of the nine 3 x 3 sub-boxes of the grid must contain the digits 1-9 + without repetition. + +Note: + +A Sudoku board (partially filled) could be valid but is not necessarily +solvable. + +Only the filled cells need to be validated according to the mentioned rules. +""" + +from collections import defaultdict + +NUM_SQUARES = 9 +EMPTY_CELL = "." + + +def is_valid_sudoku_board(sudoku_board: list[list[str]]) -> bool: + """ + This function validates (but does not solve) a sudoku board. + The board may be valid but unsolvable. + + >>> is_valid_sudoku_board([ + ... ["5","3",".",".","7",".",".",".","."] + ... ,["6",".",".","1","9","5",".",".","."] + ... ,[".","9","8",".",".",".",".","6","."] + ... ,["8",".",".",".","6",".",".",".","3"] + ... ,["4",".",".","8",".","3",".",".","1"] + ... ,["7",".",".",".","2",".",".",".","6"] + ... ,[".","6",".",".",".",".","2","8","."] + ... ,[".",".",".","4","1","9",".",".","5"] + ... ,[".",".",".",".","8",".",".","7","9"] + ... ]) + True + >>> is_valid_sudoku_board([ + ... ["8","3",".",".","7",".",".",".","."] + ... ,["6",".",".","1","9","5",".",".","."] + ... ,[".","9","8",".",".",".",".","6","."] + ... ,["8",".",".",".","6",".",".",".","3"] + ... ,["4",".",".","8",".","3",".",".","1"] + ... ,["7",".",".",".","2",".",".",".","6"] + ... ,[".","6",".",".",".",".","2","8","."] + ... ,[".",".",".","4","1","9",".",".","5"] + ... ,[".",".",".",".","8",".",".","7","9"] + ... ]) + False + >>> is_valid_sudoku_board([ + ... ["1","2","3","4","5","6","7","8","9"] + ... ,["4","5","6","7","8","9","1","2","3"] + ... ,["7","8","9","1","2","3","4","5","6"] + ... ,[".",".",".",".",".",".",".",".","."] + ... ,[".",".",".",".",".",".",".",".","."] + ... ,[".",".",".",".",".",".",".",".","."] + ... ,[".",".",".",".",".",".",".",".","."] + ... ,[".",".",".",".",".",".",".",".","."] + ... ,[".",".",".",".",".",".",".",".","."] + ... ]) + True + >>> is_valid_sudoku_board([ + ... ["1","2","3",".",".",".",".",".","."] + ... ,["4","5","6",".",".",".",".",".","."] + ... ,["7","8","9",".",".",".",".",".","."] + ... ,[".",".",".","4","5","6",".",".","."] + ... ,[".",".",".","7","8","9",".",".","."] + ... ,[".",".",".","1","2","3",".",".","."] + ... ,[".",".",".",".",".",".","7","8","9"] + ... ,[".",".",".",".",".",".","1","2","3"] + ... ,[".",".",".",".",".",".","4","5","6"] + ... ]) + True + >>> is_valid_sudoku_board([ + ... ["1","2","3",".",".",".","5","6","4"] + ... ,["4","5","6",".",".",".","8","9","7"] + ... ,["7","8","9",".",".",".","2","3","1"] + ... ,[".",".",".","4","5","6",".",".","."] + ... ,[".",".",".","7","8","9",".",".","."] + ... ,[".",".",".","1","2","3",".",".","."] + ... ,["3","1","2",".",".",".","7","8","9"] + ... ,["6","4","5",".",".",".","1","2","3"] + ... ,["9","7","8",".",".",".","4","5","6"] + ... ]) + True + >>> is_valid_sudoku_board([ + ... ["1","2","3","4","5","6","7","8","9"] + ... ,["2",".",".",".",".",".",".",".","8"] + ... ,["3",".",".",".",".",".",".",".","7"] + ... ,["4",".",".",".",".",".",".",".","6"] + ... ,["5",".",".",".",".",".",".",".","5"] + ... ,["6",".",".",".",".",".",".",".","4"] + ... ,["7",".",".",".",".",".",".",".","3"] + ... ,["8",".",".",".",".",".",".",".","2"] + ... ,["9","8","7","6","5","4","3","2","1"] + ... ]) + False + >>> is_valid_sudoku_board([ + ... ["1","2","3","8","9","7","5","6","4"] + ... ,["4","5","6","2","3","1","8","9","7"] + ... ,["7","8","9","5","6","4","2","3","1"] + ... ,["2","3","1","4","5","6","9","7","8"] + ... ,["5","6","4","7","8","9","3","1","2"] + ... ,["8","9","7","1","2","3","6","4","5"] + ... ,["3","1","2","6","4","5","7","8","9"] + ... ,["6","4","5","9","7","8","1","2","3"] + ... ,["9","7","8","3","1","2","4","5","6"] + ... ]) + True + >>> is_valid_sudoku_board([["1", "2", "3", "4", "5", "6", "7", "8", "9"]]) + Traceback (most recent call last): + ... + ValueError: Sudoku boards must be 9x9 squares. + >>> is_valid_sudoku_board( + ... [["1"], ["2"], ["3"], ["4"], ["5"], ["6"], ["7"], ["8"], ["9"]] + ... ) + Traceback (most recent call last): + ... + ValueError: Sudoku boards must be 9x9 squares. + """ + if len(sudoku_board) != NUM_SQUARES or ( + any(len(row) != NUM_SQUARES for row in sudoku_board) + ): + error_message = f"Sudoku boards must be {NUM_SQUARES}x{NUM_SQUARES} squares." + raise ValueError(error_message) + + row_values: defaultdict[int, set[str]] = defaultdict(set) + col_values: defaultdict[int, set[str]] = defaultdict(set) + box_values: defaultdict[tuple[int, int], set[str]] = defaultdict(set) + + for row in range(NUM_SQUARES): + for col in range(NUM_SQUARES): + value = sudoku_board[row][col] + + if value == EMPTY_CELL: + continue + + box = (row // 3, col // 3) + + if ( + value in row_values[row] + or value in col_values[col] + or value in box_values[box] + ): + return False + + row_values[row].add(value) + col_values[col].add(value) + box_values[box].add(value) + + return True + + +if __name__ == "__main__": + from doctest import testmod + from timeit import timeit + + testmod() + print(timeit("is_valid_sudoku_board(valid_board)", globals=globals())) + print(timeit("is_valid_sudoku_board(invalid_board)", globals=globals())) diff --git a/networking_flow/ford_fulkerson.py b/networking_flow/ford_fulkerson.py index 716ed508e679..b47d3b68f3d1 100644 --- a/networking_flow/ford_fulkerson.py +++ b/networking_flow/ford_fulkerson.py @@ -1,39 +1,96 @@ -# Ford-Fulkerson Algorithm for Maximum Flow Problem """ +Ford-Fulkerson Algorithm for Maximum Flow Problem +* https://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm + Description: - (1) Start with initial flow as 0; - (2) Choose augmenting path from source to sink and add path to flow; + (1) Start with initial flow as 0 + (2) Choose the augmenting path from source to sink and add the path to flow """ +graph = [ + [0, 16, 13, 0, 0, 0], + [0, 0, 10, 12, 0, 0], + [0, 4, 0, 0, 14, 0], + [0, 0, 9, 0, 0, 20], + [0, 0, 0, 7, 0, 4], + [0, 0, 0, 0, 0, 0], +] + + +def breadth_first_search(graph: list, source: int, sink: int, parents: list) -> bool: + """ + This function returns True if there is a node that has not iterated. + + Args: + graph: Adjacency matrix of graph + source: Source + sink: Sink + parents: Parent list + + Returns: + True if there is a node that has not iterated. + + >>> breadth_first_search(graph, 0, 5, [-1, -1, -1, -1, -1, -1]) + True + >>> breadth_first_search(graph, 0, 6, [-1, -1, -1, -1, -1, -1]) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + visited = [False] * len(graph) # Mark all nodes as not visited + queue = [] # breadth-first search queue -def bfs(graph, s, t, parent): - # Return True if there is node that has not iterated. - visited = [False] * len(graph) - queue = [] - queue.append(s) - visited[s] = True + # Source node + queue.append(source) + visited[source] = True while queue: - u = queue.pop(0) - for ind in range(len(graph[u])): - if visited[ind] is False and graph[u][ind] > 0: + u = queue.pop(0) # Pop the front node + # Traverse all adjacent nodes of u + for ind, node in enumerate(graph[u]): + if visited[ind] is False and node > 0: queue.append(ind) visited[ind] = True - parent[ind] = u + parents[ind] = u + return visited[sink] - return visited[t] +def ford_fulkerson(graph: list, source: int, sink: int) -> int: + """ + This function returns the maximum flow from source to sink in the given graph. -def ford_fulkerson(graph, source, sink): - # This array is filled by BFS and to store path + CAUTION: This function changes the given graph. + + Args: + graph: Adjacency matrix of graph + source: Source + sink: Sink + + Returns: + Maximum flow + + >>> test_graph = [ + ... [0, 16, 13, 0, 0, 0], + ... [0, 0, 10, 12, 0, 0], + ... [0, 4, 0, 0, 14, 0], + ... [0, 0, 9, 0, 0, 20], + ... [0, 0, 0, 7, 0, 4], + ... [0, 0, 0, 0, 0, 0], + ... ] + >>> ford_fulkerson(test_graph, 0, 5) + 23 + """ + # This array is filled by breadth-first search and to store path parent = [-1] * (len(graph)) max_flow = 0 - while bfs(graph, source, sink, parent): - path_flow = float("Inf") + + # While there is a path from source to sink + while breadth_first_search(graph, source, sink, parent): + path_flow = int(1e9) # Infinite value s = sink while s != source: - # Find the minimum value in select path + # Find the minimum value in the selected path path_flow = min(path_flow, graph[parent[s]][s]) s = parent[s] @@ -45,17 +102,12 @@ def ford_fulkerson(graph, source, sink): graph[u][v] -= path_flow graph[v][u] += path_flow v = parent[v] + return max_flow -graph = [ - [0, 16, 13, 0, 0, 0], - [0, 0, 10, 12, 0, 0], - [0, 4, 0, 0, 14, 0], - [0, 0, 9, 0, 0, 20], - [0, 0, 0, 7, 0, 4], - [0, 0, 0, 0, 0, 0], -] +if __name__ == "__main__": + from doctest import testmod -source, sink = 0, 5 -print(ford_fulkerson(graph, source, sink)) + testmod() + print(f"{ford_fulkerson(graph, source=0, sink=5) = }") diff --git a/neural_network/activation_functions/__init__.py b/neural_network/activation_functions/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/neural_network/activation_functions/binary_step.py b/neural_network/activation_functions/binary_step.py new file mode 100644 index 000000000000..d3d774602182 --- /dev/null +++ b/neural_network/activation_functions/binary_step.py @@ -0,0 +1,35 @@ +""" +This script demonstrates the implementation of the Binary Step function. + +It's an activation function in which the neuron is activated if the input is positive +or 0, else it is deactivated + +It's a simple activation function which is mentioned in this wikipedia article: +https://en.wikipedia.org/wiki/Activation_function +""" + +import numpy as np + + +def binary_step(vector: np.ndarray) -> np.ndarray: + """ + Implements the binary step function + + Parameters: + vector (ndarray): A vector that consists of numeric values + + Returns: + vector (ndarray): Input vector after applying binary step function + + >>> vector = np.array([-1.2, 0, 2, 1.45, -3.7, 0.3]) + >>> binary_step(vector) + array([0, 1, 1, 1, 0, 1]) + """ + + return np.where(vector >= 0, 1, 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/neural_network/activation_functions/exponential_linear_unit.py b/neural_network/activation_functions/exponential_linear_unit.py new file mode 100644 index 000000000000..7a3cf1d84e71 --- /dev/null +++ b/neural_network/activation_functions/exponential_linear_unit.py @@ -0,0 +1,40 @@ +""" +Implements the Exponential Linear Unit or ELU function. + +The function takes a vector of K real numbers and a real number alpha as +input and then applies the ELU function to each element of the vector. + +Script inspired from its corresponding Wikipedia article +https://en.wikipedia.org/wiki/Rectifier_(neural_networks) +""" + +import numpy as np + + +def exponential_linear_unit(vector: np.ndarray, alpha: float) -> np.ndarray: + """ + Implements the ELU activation function. + Parameters: + vector: the array containing input of elu activation + alpha: hyper-parameter + return: + elu (np.array): The input numpy array after applying elu. + + Mathematically, f(x) = x, x>0 else (alpha * (e^x -1)), x<=0, alpha >=0 + + Examples: + >>> exponential_linear_unit(vector=np.array([2.3,0.6,-2,-3.8]), alpha=0.3) + array([ 2.3 , 0.6 , -0.25939942, -0.29328877]) + + >>> exponential_linear_unit(vector=np.array([-9.2,-0.3,0.45,-4.56]), alpha=0.067) + array([-0.06699323, -0.01736518, 0.45 , -0.06629904]) + + + """ + return np.where(vector > 0, vector, (alpha * (np.exp(vector) - 1))) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/gaussian_error_linear_unit.py b/neural_network/activation_functions/gaussian_error_linear_unit.py similarity index 81% rename from maths/gaussian_error_linear_unit.py rename to neural_network/activation_functions/gaussian_error_linear_unit.py index 7b5f875143b9..b3cbd7810716 100644 --- a/maths/gaussian_error_linear_unit.py +++ b/neural_network/activation_functions/gaussian_error_linear_unit.py @@ -13,7 +13,7 @@ import numpy as np -def sigmoid(vector: np.array) -> np.array: +def sigmoid(vector: np.ndarray) -> np.ndarray: """ Mathematical function sigmoid takes a vector x of K real numbers as input and returns 1/ (1 + e^-x). @@ -25,17 +25,15 @@ def sigmoid(vector: np.array) -> np.array: return 1 / (1 + np.exp(-vector)) -def gaussian_error_linear_unit(vector: np.array) -> np.array: +def gaussian_error_linear_unit(vector: np.ndarray) -> np.ndarray: """ Implements the Gaussian Error Linear Unit (GELU) function Parameters: - vector (np.array): A numpy array of shape (1,n) - consisting of real values + vector (np.ndarray): A numpy array of shape (1, n) consisting of real values Returns: - gelu_vec (np.array): The input numpy array, after applying - gelu. + gelu_vec (np.ndarray): The input numpy array, after applying gelu Examples: >>> gaussian_error_linear_unit(np.array([-1.0, 1.0, 2.0])) diff --git a/neural_network/activation_functions/leaky_rectified_linear_unit.py b/neural_network/activation_functions/leaky_rectified_linear_unit.py new file mode 100644 index 000000000000..019086fd9821 --- /dev/null +++ b/neural_network/activation_functions/leaky_rectified_linear_unit.py @@ -0,0 +1,39 @@ +""" +Leaky Rectified Linear Unit (Leaky ReLU) + +Use Case: Leaky ReLU addresses the problem of the vanishing gradient. +For more detailed information, you can refer to the following link: +https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Leaky_ReLU +""" + +import numpy as np + + +def leaky_rectified_linear_unit(vector: np.ndarray, alpha: float) -> np.ndarray: + """ + Implements the LeakyReLU activation function. + + Parameters: + vector (np.ndarray): The input array for LeakyReLU activation. + alpha (float): The slope for negative values. + + Returns: + np.ndarray: The input array after applying the LeakyReLU activation. + + Formula: f(x) = x if x > 0 else f(x) = alpha * x + + Examples: + >>> leaky_rectified_linear_unit(vector=np.array([2.3,0.6,-2,-3.8]), alpha=0.3) + array([ 2.3 , 0.6 , -0.6 , -1.14]) + + >>> leaky_rectified_linear_unit(np.array([-9.2, -0.3, 0.45, -4.56]), alpha=0.067) + array([-0.6164 , -0.0201 , 0.45 , -0.30552]) + + """ + return np.where(vector > 0, vector, alpha * vector) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/neural_network/activation_functions/mish.py b/neural_network/activation_functions/mish.py new file mode 100644 index 000000000000..57a91413fe50 --- /dev/null +++ b/neural_network/activation_functions/mish.py @@ -0,0 +1,41 @@ +""" +Mish Activation Function + +Use Case: Improved version of the ReLU activation function used in Computer Vision. +For more detailed information, you can refer to the following link: +https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Mish +""" + +import numpy as np + +from .softplus import softplus + + +def mish(vector: np.ndarray) -> np.ndarray: + """ + Implements the Mish activation function. + + Parameters: + vector (np.ndarray): The input array for Mish activation. + + Returns: + np.ndarray: The input array after applying the Mish activation. + + Formula: + f(x) = x * tanh(softplus(x)) = x * tanh(ln(1 + e^x)) + + Examples: + >>> mish(vector=np.array([2.3,0.6,-2,-3.8])) + array([ 2.26211893, 0.46613649, -0.25250148, -0.08405831]) + + >>> mish(np.array([-9.2, -0.3, 0.45, -4.56])) + array([-0.00092952, -0.15113318, 0.33152014, -0.04745745]) + + """ + return vector * np.tanh(softplus(vector)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/relu.py b/neural_network/activation_functions/rectified_linear_unit.py similarity index 99% rename from maths/relu.py rename to neural_network/activation_functions/rectified_linear_unit.py index 458c6bd5c391..2d5cf96fd387 100644 --- a/maths/relu.py +++ b/neural_network/activation_functions/rectified_linear_unit.py @@ -9,6 +9,7 @@ Script inspired from its corresponding Wikipedia article https://en.wikipedia.org/wiki/Rectifier_(neural_networks) """ + from __future__ import annotations import numpy as np diff --git a/neural_network/activation_functions/scaled_exponential_linear_unit.py b/neural_network/activation_functions/scaled_exponential_linear_unit.py new file mode 100644 index 000000000000..f91dc6852136 --- /dev/null +++ b/neural_network/activation_functions/scaled_exponential_linear_unit.py @@ -0,0 +1,44 @@ +""" +Implements the Scaled Exponential Linear Unit or SELU function. +The function takes a vector of K real numbers and two real numbers +alpha(default = 1.6732) & lambda (default = 1.0507) as input and +then applies the SELU function to each element of the vector. +SELU is a self-normalizing activation function. It is a variant +of the ELU. The main advantage of SELU is that we can be sure +that the output will always be standardized due to its +self-normalizing behavior. That means there is no need to +include Batch-Normalization layers. +References : +https://iq.opengenus.org/scaled-exponential-linear-unit/ +""" + +import numpy as np + + +def scaled_exponential_linear_unit( + vector: np.ndarray, alpha: float = 1.6732, lambda_: float = 1.0507 +) -> np.ndarray: + """ + Applies the Scaled Exponential Linear Unit function to each element of the vector. + Parameters : + vector : np.ndarray + alpha : float (default = 1.6732) + lambda_ : float (default = 1.0507) + + Returns : np.ndarray + Formula : f(x) = lambda_ * x if x > 0 + lambda_ * alpha * (e**x - 1) if x <= 0 + Examples : + >>> scaled_exponential_linear_unit(vector=np.array([1.3, 3.7, 2.4])) + array([1.36591, 3.88759, 2.52168]) + + >>> scaled_exponential_linear_unit(vector=np.array([1.3, 4.7, 8.2])) + array([1.36591, 4.93829, 8.61574]) + """ + return lambda_ * np.where(vector > 0, vector, alpha * (np.exp(vector) - 1)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py b/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py new file mode 100644 index 000000000000..a053e690ba44 --- /dev/null +++ b/neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py @@ -0,0 +1,48 @@ +""" +This script implements the Soboleva Modified Hyperbolic Tangent function. + +The function applies the Soboleva Modified Hyperbolic Tangent function +to each element of the vector. + +More details about the activation function can be found on: +https://en.wikipedia.org/wiki/Soboleva_modified_hyperbolic_tangent +""" + +import numpy as np + + +def soboleva_modified_hyperbolic_tangent( + vector: np.ndarray, a_value: float, b_value: float, c_value: float, d_value: float +) -> np.ndarray: + """ + Implements the Soboleva Modified Hyperbolic Tangent function + + Parameters: + vector (ndarray): A vector that consists of numeric values + a_value (float): parameter a of the equation + b_value (float): parameter b of the equation + c_value (float): parameter c of the equation + d_value (float): parameter d of the equation + + Returns: + vector (ndarray): Input array after applying SMHT function + + >>> vector = np.array([5.4, -2.4, 6.3, -5.23, 3.27, 0.56]) + >>> soboleva_modified_hyperbolic_tangent(vector, 0.2, 0.4, 0.6, 0.8) + array([ 0.11075085, -0.28236685, 0.07861169, -0.1180085 , 0.22999056, + 0.1566043 ]) + """ + + # Separate the numerator and denominator for simplicity + # Calculate the numerator and denominator element-wise + numerator = np.exp(a_value * vector) - np.exp(-b_value * vector) + denominator = np.exp(c_value * vector) + np.exp(-d_value * vector) + + # Calculate and return the final result element-wise + return numerator / denominator + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/neural_network/activation_functions/softplus.py b/neural_network/activation_functions/softplus.py new file mode 100644 index 000000000000..35fdf41afc96 --- /dev/null +++ b/neural_network/activation_functions/softplus.py @@ -0,0 +1,37 @@ +""" +Softplus Activation Function + +Use Case: The Softplus function is a smooth approximation of the ReLU function. +For more detailed information, you can refer to the following link: +https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Softplus +""" + +import numpy as np + + +def softplus(vector: np.ndarray) -> np.ndarray: + """ + Implements the Softplus activation function. + + Parameters: + vector (np.ndarray): The input array for the Softplus activation. + + Returns: + np.ndarray: The input array after applying the Softplus activation. + + Formula: f(x) = ln(1 + e^x) + + Examples: + >>> softplus(np.array([2.3, 0.6, -2, -3.8])) + array([2.39554546, 1.03748795, 0.12692801, 0.02212422]) + + >>> softplus(np.array([-9.2, -0.3, 0.45, -4.56])) + array([1.01034298e-04, 5.54355244e-01, 9.43248946e-01, 1.04077103e-02]) + """ + return np.log(1 + np.exp(vector)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/neural_network/activation_functions/squareplus.py b/neural_network/activation_functions/squareplus.py new file mode 100644 index 000000000000..40fa800d6b4a --- /dev/null +++ b/neural_network/activation_functions/squareplus.py @@ -0,0 +1,38 @@ +""" +Squareplus Activation Function + +Use Case: Squareplus designed to enhance positive values and suppress negative values. +For more detailed information, you can refer to the following link: +https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Squareplus +""" + +import numpy as np + + +def squareplus(vector: np.ndarray, beta: float) -> np.ndarray: + """ + Implements the SquarePlus activation function. + + Parameters: + vector (np.ndarray): The input array for the SquarePlus activation. + beta (float): size of the curved region + + Returns: + np.ndarray: The input array after applying the SquarePlus activation. + + Formula: f(x) = ( x + sqrt(x^2 + b) ) / 2 + + Examples: + >>> squareplus(np.array([2.3, 0.6, -2, -3.8]), beta=2) + array([2.5 , 1.06811457, 0.22474487, 0.12731349]) + + >>> squareplus(np.array([-9.2, -0.3, 0.45, -4.56]), beta=3) + array([0.0808119 , 0.72891979, 1.11977651, 0.15893419]) + """ + return (vector + np.sqrt(vector**2 + beta)) / 2 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/sigmoid_linear_unit.py b/neural_network/activation_functions/swish.py similarity index 61% rename from maths/sigmoid_linear_unit.py rename to neural_network/activation_functions/swish.py index a8ada10dd8ec..ab3d8fa1203b 100644 --- a/maths/sigmoid_linear_unit.py +++ b/neural_network/activation_functions/swish.py @@ -12,12 +12,13 @@ This script is inspired by a corresponding research paper. * https://arxiv.org/abs/1710.05941 +* https://blog.paperspace.com/swish-activation-function/ """ import numpy as np -def sigmoid(vector: np.array) -> np.array: +def sigmoid(vector: np.ndarray) -> np.ndarray: """ Mathematical function sigmoid takes a vector x of K real numbers as input and returns 1/ (1 + e^-x). @@ -29,17 +30,15 @@ def sigmoid(vector: np.array) -> np.array: return 1 / (1 + np.exp(-vector)) -def sigmoid_linear_unit(vector: np.array) -> np.array: +def sigmoid_linear_unit(vector: np.ndarray) -> np.ndarray: """ Implements the Sigmoid Linear Unit (SiLU) or swish function Parameters: - vector (np.array): A numpy array consisting of real - values. + vector (np.ndarray): A numpy array consisting of real values Returns: - swish_vec (np.array): The input numpy array, after applying - swish. + swish_vec (np.ndarray): The input numpy array, after applying swish Examples: >>> sigmoid_linear_unit(np.array([-1.0, 1.0, 2.0])) @@ -51,6 +50,25 @@ def sigmoid_linear_unit(vector: np.array) -> np.array: return vector * sigmoid(vector) +def swish(vector: np.ndarray, trainable_parameter: int) -> np.ndarray: + """ + Parameters: + vector (np.ndarray): A numpy array consisting of real values + trainable_parameter: Use to implement various Swish Activation Functions + + Returns: + swish_vec (np.ndarray): The input numpy array, after applying swish + + Examples: + >>> swish(np.array([-1.0, 1.0, 2.0]), 2) + array([-0.11920292, 0.88079708, 1.96402758]) + + >>> swish(np.array([-2]), 1) + array([-0.23840584]) + """ + return vector * sigmoid(trainable_parameter * vector) + + if __name__ == "__main__": import doctest diff --git a/neural_network/back_propagation_neural_network.py b/neural_network/back_propagation_neural_network.py index cb47b829010c..182f759c5fc7 100644 --- a/neural_network/back_propagation_neural_network.py +++ b/neural_network/back_propagation_neural_network.py @@ -2,10 +2,10 @@ """ -A Framework of Back Propagation Neural Network(BP) model +A Framework of Back Propagation Neural Network (BP) model Easy to use: - * add many layers as you want ï¼ï¼ï¼ + * add many layers as you want ! ! ! * clearly see how the loss decreasing Easy to expand: * more activation functions @@ -17,12 +17,13 @@ Date: 2017.11.23 """ + import numpy as np from matplotlib import pyplot as plt -def sigmoid(x): - return 1 / (1 + np.exp(-1 * x)) +def sigmoid(x: np.ndarray) -> np.ndarray: + return 1 / (1 + np.exp(-x)) class DenseLayer: @@ -50,8 +51,9 @@ def __init__( self.is_input_layer = is_input_layer def initializer(self, back_units): - self.weight = np.asmatrix(np.random.normal(0, 0.5, (self.units, back_units))) - self.bias = np.asmatrix(np.random.normal(0, 0.5, self.units)).T + rng = np.random.default_rng() + self.weight = np.asmatrix(rng.normal(0, 0.5, (self.units, back_units))) + self.bias = np.asmatrix(rng.normal(0, 0.5, self.units)).T if self.activation is None: self.activation = sigmoid @@ -153,6 +155,7 @@ def train(self, xdata, ydata, train_round, accuracy): if mse < self.accuracy: print("----达到精度----") return mse + return None def cal_loss(self, ydata, ydata_): self.loss = np.sum(np.power((ydata - ydata_), 2)) @@ -172,7 +175,8 @@ def plot_loss(self): def example(): - x = np.random.randn(10, 10) + rng = np.random.default_rng() + x = rng.normal(size=(10, 10)) y = np.asarray( [ [0.8, 0.4], diff --git a/neural_network/convolution_neural_network.py b/neural_network/convolution_neural_network.py index bd0550212157..d4ac360a98de 100644 --- a/neural_network/convolution_neural_network.py +++ b/neural_network/convolution_neural_network.py @@ -1,18 +1,19 @@ """ - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - Name - - CNN - Convolution Neural Network For Photo Recognizing - Goal - - Recognize Handing Writing Word Photo - Detail:Total 5 layers neural network - * Convolution layer - * Pooling layer - * Input layer layer of BP - * Hidden layer of BP - * Output layer of BP - Author: Stephen Lee - Github: 245885195@qq.com - Date: 2017.9.20 - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - + - - - - - -- - - - - - - - - - - - - - - - - - - - - - - +Name - - CNN - Convolution Neural Network For Photo Recognizing +Goal - - Recognize Handwriting Word Photo +Detail: Total 5 layers neural network + * Convolution layer + * Pooling layer + * Input layer layer of BP + * Hidden layer of BP + * Output layer of BP +Author: Stephen Lee +Github: 245885195@qq.com +Date: 2017.9.20 +- - - - - -- - - - - - - - - - - - - - - - - - - - - - - """ + import pickle import numpy as np @@ -24,7 +25,7 @@ def __init__( self, conv1_get, size_p1, bp_num1, bp_num2, bp_num3, rate_w=0.2, rate_t=0.2 ): """ - :param conv1_get: [a,c,d],size, number, step of convolution kernel + :param conv1_get: [a,c,d], size, number, step of convolution kernel :param size_p1: pooling size :param bp_num1: units number of flatten layer :param bp_num2: units number of hidden layer @@ -40,15 +41,16 @@ def __init__( self.size_pooling1 = size_p1 self.rate_weight = rate_w self.rate_thre = rate_t + rng = np.random.default_rng() self.w_conv1 = [ - np.mat(-1 * np.random.rand(self.conv1[0], self.conv1[0]) + 0.5) + np.asmatrix(-1 * rng.random((self.conv1[0], self.conv1[0])) + 0.5) for i in range(self.conv1[1]) ] - self.wkj = np.mat(-1 * np.random.rand(self.num_bp3, self.num_bp2) + 0.5) - self.vji = np.mat(-1 * np.random.rand(self.num_bp2, self.num_bp1) + 0.5) - self.thre_conv1 = -2 * np.random.rand(self.conv1[1]) + 1 - self.thre_bp2 = -2 * np.random.rand(self.num_bp2) + 1 - self.thre_bp3 = -2 * np.random.rand(self.num_bp3) + 1 + self.wkj = np.asmatrix(-1 * rng.random((self.num_bp3, self.num_bp2)) + 0.5) + self.vji = np.asmatrix(-1 * rng.random((self.num_bp2, self.num_bp1)) + 0.5) + self.thre_conv1 = -2 * rng.random(self.conv1[1]) + 1 + self.thre_bp2 = -2 * rng.random(self.num_bp2) + 1 + self.thre_bp3 = -2 * rng.random(self.num_bp3) + 1 def save_model(self, save_path): # save model dict with pickle @@ -71,13 +73,13 @@ def save_model(self, save_path): with open(save_path, "wb") as f: pickle.dump(model_dic, f) - print(f"Model saved: {save_path}") + print(f"Model saved: {save_path}") @classmethod def read_model(cls, model_path): # read saved model with open(model_path, "rb") as f: - model_dic = pickle.load(f) + model_dic = pickle.load(f) # noqa: S301 conv_get = model_dic.get("conv1") conv_get.append(model_dic.get("step_conv1")) @@ -133,7 +135,7 @@ def convolute(self, data, convs, w_convs, thre_convs, conv_step): ) data_featuremap.append(featuremap) - # expanding the data slice to One dimenssion + # expanding the data slice to one dimension focus1_list = [] for each_focus in data_focus: focus1_list.extend(self.Expand_Mat(each_focus)) @@ -210,7 +212,7 @@ def _calculate_gradient_from_pool( def train( self, patterns, datas_train, datas_teach, n_repeat, error_accuracy, draw_e=bool ): - # model traning + # model training print("----------------------Start Training-------------------------") print((" - - Shape: Train_Data ", np.shape(datas_train))) print((" - - Shape: Teach_Data ", np.shape(datas_teach))) @@ -302,7 +304,7 @@ def draw_error(): plt.grid(True, alpha=0.5) plt.show() - print("------------------Training Complished---------------------") + print("------------------Training Complete---------------------") print((" - - Training epoch: ", rp, f" - - Mse: {mse:.6f}")) if draw_e: draw_error() @@ -351,5 +353,5 @@ def convolution(self, data): if __name__ == "__main__": """ - I will put the example on other file + I will put the example in another file """ diff --git a/neural_network/gan.py_tf b/neural_network/gan.py_tf index deb062c48dc7..9c6e1c05b8b4 100644 --- a/neural_network/gan.py_tf +++ b/neural_network/gan.py_tf @@ -158,7 +158,7 @@ if __name__ == "__main__": # G_b2 = np.random.normal(size=(784),scale=(1. / np.sqrt(784 / 2.))) *0.002 G_b7 = np.zeros(784) - # 3. For Adam Optimzier + # 3. For Adam Optimizer v1, m1 = 0, 0 v2, m2 = 0, 0 v3, m3 = 0, 0 diff --git a/neural_network/input_data.py_tf b/neural_network/input_data.py similarity index 69% rename from neural_network/input_data.py_tf rename to neural_network/input_data.py index 0e22ac0bcda5..3a8628f939f8 100644 --- a/neural_network/input_data.py_tf +++ b/neural_network/input_data.py @@ -17,57 +17,57 @@ This module and all its submodules are deprecated. """ - -import collections import gzip import os +import typing +import urllib -import numpy -from six.moves import urllib -from six.moves import xrange # pylint: disable=redefined-builtin - -from tensorflow.python.framework import dtypes -from tensorflow.python.framework import random_seed +import numpy as np +from tensorflow.python.framework import dtypes, random_seed from tensorflow.python.platform import gfile from tensorflow.python.util.deprecation import deprecated -_Datasets = collections.namedtuple("_Datasets", ["train", "validation", "test"]) + +class _Datasets(typing.NamedTuple): + train: "_DataSet" + validation: "_DataSet" + test: "_DataSet" + # CVDF mirror of http://yann.lecun.com/exdb/mnist/ DEFAULT_SOURCE_URL = "https://storage.googleapis.com/cvdf-datasets/mnist/" def _read32(bytestream): - dt = numpy.dtype(numpy.uint32).newbyteorder(">") - return numpy.frombuffer(bytestream.read(4), dtype=dt)[0] + dt = np.dtype(np.uint32).newbyteorder(">") + return np.frombuffer(bytestream.read(4), dtype=dt)[0] @deprecated(None, "Please use tf.data to implement this functionality.") def _extract_images(f): """Extract the images into a 4D uint8 numpy array [index, y, x, depth]. - Args: - f: A file object that can be passed into a gzip reader. + Args: + f: A file object that can be passed into a gzip reader. - Returns: - data: A 4D uint8 numpy array [index, y, x, depth]. + Returns: + data: A 4D uint8 numpy array [index, y, x, depth]. - Raises: - ValueError: If the bytestream does not start with 2051. + Raises: + ValueError: If the bytestream does not start with 2051. - """ + """ print("Extracting", f.name) with gzip.GzipFile(fileobj=f) as bytestream: magic = _read32(bytestream) if magic != 2051: - raise ValueError( - "Invalid magic number %d in MNIST image file: %s" % (magic, f.name) - ) + msg = f"Invalid magic number {magic} in MNIST image file: {f.name}" + raise ValueError(msg) num_images = _read32(bytestream) rows = _read32(bytestream) cols = _read32(bytestream) buf = bytestream.read(rows * cols * num_images) - data = numpy.frombuffer(buf, dtype=numpy.uint8) + data = np.frombuffer(buf, dtype=np.uint8) data = data.reshape(num_images, rows, cols, 1) return data @@ -76,8 +76,8 @@ def _extract_images(f): def _dense_to_one_hot(labels_dense, num_classes): """Convert class labels from scalars to one-hot vectors.""" num_labels = labels_dense.shape[0] - index_offset = numpy.arange(num_labels) * num_classes - labels_one_hot = numpy.zeros((num_labels, num_classes)) + index_offset = np.arange(num_labels) * num_classes + labels_one_hot = np.zeros((num_labels, num_classes)) labels_one_hot.flat[index_offset + labels_dense.ravel()] = 1 return labels_one_hot @@ -86,27 +86,26 @@ def _dense_to_one_hot(labels_dense, num_classes): def _extract_labels(f, one_hot=False, num_classes=10): """Extract the labels into a 1D uint8 numpy array [index]. - Args: - f: A file object that can be passed into a gzip reader. - one_hot: Does one hot encoding for the result. - num_classes: Number of classes for the one hot encoding. + Args: + f: A file object that can be passed into a gzip reader. + one_hot: Does one hot encoding for the result. + num_classes: Number of classes for the one hot encoding. - Returns: - labels: a 1D uint8 numpy array. + Returns: + labels: a 1D uint8 numpy array. - Raises: - ValueError: If the bystream doesn't start with 2049. - """ + Raises: + ValueError: If the bystream doesn't start with 2049. + """ print("Extracting", f.name) with gzip.GzipFile(fileobj=f) as bytestream: magic = _read32(bytestream) if magic != 2049: - raise ValueError( - "Invalid magic number %d in MNIST label file: %s" % (magic, f.name) - ) + msg = f"Invalid magic number {magic} in MNIST label file: {f.name}" + raise ValueError(msg) num_items = _read32(bytestream) buf = bytestream.read(num_items) - labels = numpy.frombuffer(buf, dtype=numpy.uint8) + labels = np.frombuffer(buf, dtype=np.uint8) if one_hot: return _dense_to_one_hot(labels, num_classes) return labels @@ -115,8 +114,8 @@ def _extract_labels(f, one_hot=False, num_classes=10): class _DataSet: """Container class for a _DataSet (deprecated). - THIS CLASS IS DEPRECATED. - """ + THIS CLASS IS DEPRECATED. + """ @deprecated( None, @@ -135,34 +134,35 @@ def __init__( ): """Construct a _DataSet. - one_hot arg is used only if fake_data is true. `dtype` can be either - `uint8` to leave the input as `[0, 255]`, or `float32` to rescale into - `[0, 1]`. Seed arg provides for convenient deterministic testing. - - Args: - images: The images - labels: The labels - fake_data: Ignore inages and labels, use fake data. - one_hot: Bool, return the labels as one hot vectors (if True) or ints (if - False). - dtype: Output image dtype. One of [uint8, float32]. `uint8` output has - range [0,255]. float32 output has range [0,1]. - reshape: Bool. If True returned images are returned flattened to vectors. - seed: The random seed to use. - """ + one_hot arg is used only if fake_data is true. `dtype` can be either + `uint8` to leave the input as `[0, 255]`, or `float32` to rescale into + `[0, 1]`. Seed arg provides for convenient deterministic testing. + + Args: + images: The images + labels: The labels + fake_data: Ignore inages and labels, use fake data. + one_hot: Bool, return the labels as one hot vectors (if True) or ints (if + False). + dtype: Output image dtype. One of [uint8, float32]. `uint8` output has + range [0,255]. float32 output has range [0,1]. + reshape: Bool. If True returned images are returned flattened to vectors. + seed: The random seed to use. + """ seed1, seed2 = random_seed.get_seed(seed) # If op level seed is not set, use whatever graph level seed is returned - numpy.random.seed(seed1 if seed is None else seed2) + self._rng = np.random.default_rng(seed1 if seed is None else seed2) dtype = dtypes.as_dtype(dtype).base_dtype if dtype not in (dtypes.uint8, dtypes.float32): - raise TypeError("Invalid image dtype %r, expected uint8 or float32" % dtype) + msg = f"Invalid image dtype {dtype!r}, expected uint8 or float32" + raise TypeError(msg) if fake_data: self._num_examples = 10000 self.one_hot = one_hot else: - assert ( - images.shape[0] == labels.shape[0] - ), f"images.shape: {images.shape} labels.shape: {labels.shape}" + assert images.shape[0] == labels.shape[0], ( + f"images.shape: {images.shape} labels.shape: {labels.shape}" + ) self._num_examples = images.shape[0] # Convert shape from [num examples, rows, columns, depth] @@ -174,8 +174,8 @@ def __init__( ) if dtype == dtypes.float32: # Convert from [0, 255] -> [0.0, 1.0]. - images = images.astype(numpy.float32) - images = numpy.multiply(images, 1.0 / 255.0) + images = images.astype(np.float32) + images = np.multiply(images, 1.0 / 255.0) self._images = images self._labels = labels self._epochs_completed = 0 @@ -201,19 +201,16 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True): """Return the next `batch_size` examples from this data set.""" if fake_data: fake_image = [1] * 784 - if self.one_hot: - fake_label = [1] + [0] * 9 - else: - fake_label = 0 + fake_label = [1] + [0] * 9 if self.one_hot else 0 return ( - [fake_image for _ in xrange(batch_size)], - [fake_label for _ in xrange(batch_size)], + [fake_image for _ in range(batch_size)], + [fake_label for _ in range(batch_size)], ) start = self._index_in_epoch # Shuffle for the first epoch if self._epochs_completed == 0 and start == 0 and shuffle: - perm0 = numpy.arange(self._num_examples) - numpy.random.shuffle(perm0) + perm0 = np.arange(self._num_examples) + self._rng.shuffle(perm0) self._images = self.images[perm0] self._labels = self.labels[perm0] # Go to the next epoch @@ -226,8 +223,8 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True): labels_rest_part = self._labels[start : self._num_examples] # Shuffle the data if shuffle: - perm = numpy.arange(self._num_examples) - numpy.random.shuffle(perm) + perm = np.arange(self._num_examples) + self._rng.shuffle(perm) self._images = self.images[perm] self._labels = self.labels[perm] # Start next epoch @@ -237,8 +234,8 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True): images_new_part = self._images[start:end] labels_new_part = self._labels[start:end] return ( - numpy.concatenate((images_rest_part, images_new_part), axis=0), - numpy.concatenate((labels_rest_part, labels_new_part), axis=0), + np.concatenate((images_rest_part, images_new_part), axis=0), + np.concatenate((labels_rest_part, labels_new_part), axis=0), ) else: self._index_in_epoch += batch_size @@ -250,28 +247,26 @@ def next_batch(self, batch_size, fake_data=False, shuffle=True): def _maybe_download(filename, work_directory, source_url): """Download the data from source url, unless it's already here. - Args: - filename: string, name of the file in the directory. - work_directory: string, path to working directory. - source_url: url to download from if file doesn't exist. + Args: + filename: string, name of the file in the directory. + work_directory: string, path to working directory. + source_url: url to download from if file doesn't exist. - Returns: - Path to resulting file. - """ + Returns: + Path to resulting file. + """ if not gfile.Exists(work_directory): gfile.MakeDirs(work_directory) filepath = os.path.join(work_directory, filename) if not gfile.Exists(filepath): - urllib.request.urlretrieve(source_url, filepath) + urllib.request.urlretrieve(source_url, filepath) # noqa: S310 with gfile.GFile(filepath) as f: size = f.size() print("Successfully downloaded", filename, size, "bytes.") return filepath -@deprecated( - None, "Please use alternatives such as:" " tensorflow_datasets.load('mnist')" -) +@deprecated(None, "Please use alternatives such as: tensorflow_datasets.load('mnist')") def read_data_sets( train_dir, fake_data=False, @@ -327,16 +322,18 @@ def fake(): test_labels = _extract_labels(f, one_hot=one_hot) if not 0 <= validation_size <= len(train_images): - raise ValueError( - f"Validation size should be between 0 and {len(train_images)}. Received: {validation_size}." + msg = ( + "Validation size should be between 0 and " + f"{len(train_images)}. Received: {validation_size}." ) + raise ValueError(msg) validation_images = train_images[:validation_size] validation_labels = train_labels[:validation_size] train_images = train_images[validation_size:] train_labels = train_labels[validation_size:] - options = dict(dtype=dtype, reshape=reshape, seed=seed) + options = {"dtype": dtype, "reshape": reshape, "seed": seed} train = _DataSet(train_images, train_labels, **options) validation = _DataSet(validation_images, validation_labels, **options) diff --git a/neural_network/perceptron.py b/neural_network/perceptron.py.DISABLED similarity index 100% rename from neural_network/perceptron.py rename to neural_network/perceptron.py.DISABLED diff --git a/neural_network/simple_neural_network.py b/neural_network/simple_neural_network.py index f2a3234873b5..8751a38908cf 100644 --- a/neural_network/simple_neural_network.py +++ b/neural_network/simple_neural_network.py @@ -28,7 +28,7 @@ def sigmoid_function(value: float, deriv: bool = False) -> float: def forward_propagation(expected: int, number_propagations: int) -> float: """Return the value found after the forward propagation training. - >>> res = forward_propagation(32, 10000000) + >>> res = forward_propagation(32, 450_000) # Was 10_000_000 >>> res > 31 and res < 33 True diff --git a/neural_network/2_hidden_layers_neural_network.py b/neural_network/two_hidden_layers_neural_network.py similarity index 78% rename from neural_network/2_hidden_layers_neural_network.py rename to neural_network/two_hidden_layers_neural_network.py index 9c5772326165..1b7c0beed3ba 100644 --- a/neural_network/2_hidden_layers_neural_network.py +++ b/neural_network/two_hidden_layers_neural_network.py @@ -5,11 +5,11 @@ - https://en.wikipedia.org/wiki/Feedforward_neural_network (Feedforward) """ -import numpy +import numpy as np class TwoHiddenLayerNeuralNetwork: - def __init__(self, input_array: numpy.ndarray, output_array: numpy.ndarray) -> None: + def __init__(self, input_array: np.ndarray, output_array: np.ndarray) -> None: """ This function initializes the TwoHiddenLayerNeuralNetwork class with random weights for every layer and initializes predicted output with zeroes. @@ -28,30 +28,29 @@ def __init__(self, input_array: numpy.ndarray, output_array: numpy.ndarray) -> N # Random initial weights are assigned. # self.input_array.shape[1] is used to represent number of nodes in input layer. # First hidden layer consists of 4 nodes. - self.input_layer_and_first_hidden_layer_weights = numpy.random.rand( - self.input_array.shape[1], 4 + rng = np.random.default_rng() + self.input_layer_and_first_hidden_layer_weights = rng.random( + (self.input_array.shape[1], 4) ) # Random initial values for the first hidden layer. # First hidden layer has 4 nodes. # Second hidden layer has 3 nodes. - self.first_hidden_layer_and_second_hidden_layer_weights = numpy.random.rand( - 4, 3 - ) + self.first_hidden_layer_and_second_hidden_layer_weights = rng.random((4, 3)) # Random initial values for the second hidden layer. # Second hidden layer has 3 nodes. # Output layer has 1 node. - self.second_hidden_layer_and_output_layer_weights = numpy.random.rand(3, 1) + self.second_hidden_layer_and_output_layer_weights = rng.random((3, 1)) # Real output values provided. self.output_array = output_array # Predicted output values by the neural network. # Predicted_output array initially consists of zeroes. - self.predicted_output = numpy.zeros(output_array.shape) + self.predicted_output = np.zeros(output_array.shape) - def feedforward(self) -> numpy.ndarray: + def feedforward(self) -> np.ndarray: """ The information moves in only one direction i.e. forward from the input nodes, through the two hidden nodes and to the output nodes. @@ -60,24 +59,24 @@ def feedforward(self) -> numpy.ndarray: Return layer_between_second_hidden_layer_and_output (i.e the last layer of the neural network). - >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) - >>> output_val = numpy.array(([0], [0], [0]), dtype=float) + >>> input_val = np.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) + >>> output_val = np.array(([0], [0], [0]), dtype=float) >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) >>> res = nn.feedforward() - >>> array_sum = numpy.sum(res) - >>> numpy.isnan(array_sum) + >>> array_sum = np.sum(res) + >>> bool(np.isnan(array_sum)) False """ # Layer_between_input_and_first_hidden_layer is the layer connecting the # input nodes with the first hidden layer nodes. self.layer_between_input_and_first_hidden_layer = sigmoid( - numpy.dot(self.input_array, self.input_layer_and_first_hidden_layer_weights) + np.dot(self.input_array, self.input_layer_and_first_hidden_layer_weights) ) # layer_between_first_hidden_layer_and_second_hidden_layer is the layer # connecting the first hidden set of nodes with the second hidden set of nodes. self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid( - numpy.dot( + np.dot( self.layer_between_input_and_first_hidden_layer, self.first_hidden_layer_and_second_hidden_layer_weights, ) @@ -86,7 +85,7 @@ def feedforward(self) -> numpy.ndarray: # layer_between_second_hidden_layer_and_output is the layer connecting # second hidden layer with the output node. self.layer_between_second_hidden_layer_and_output = sigmoid( - numpy.dot( + np.dot( self.layer_between_first_hidden_layer_and_second_hidden_layer, self.second_hidden_layer_and_output_layer_weights, ) @@ -100,25 +99,25 @@ def back_propagation(self) -> None: error rate obtained in the previous epoch (i.e., iteration). Updation is done using derivative of sogmoid activation function. - >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) - >>> output_val = numpy.array(([0], [0], [0]), dtype=float) + >>> input_val = np.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) + >>> output_val = np.array(([0], [0], [0]), dtype=float) >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) >>> res = nn.feedforward() >>> nn.back_propagation() >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights - >>> (res == updated_weights).all() + >>> bool((res == updated_weights).all()) False """ - updated_second_hidden_layer_and_output_layer_weights = numpy.dot( + updated_second_hidden_layer_and_output_layer_weights = np.dot( self.layer_between_first_hidden_layer_and_second_hidden_layer.T, 2 * (self.output_array - self.predicted_output) * sigmoid_derivative(self.predicted_output), ) - updated_first_hidden_layer_and_second_hidden_layer_weights = numpy.dot( + updated_first_hidden_layer_and_second_hidden_layer_weights = np.dot( self.layer_between_input_and_first_hidden_layer.T, - numpy.dot( + np.dot( 2 * (self.output_array - self.predicted_output) * sigmoid_derivative(self.predicted_output), @@ -128,10 +127,10 @@ def back_propagation(self) -> None: self.layer_between_first_hidden_layer_and_second_hidden_layer ), ) - updated_input_layer_and_first_hidden_layer_weights = numpy.dot( + updated_input_layer_and_first_hidden_layer_weights = np.dot( self.input_array.T, - numpy.dot( - numpy.dot( + np.dot( + np.dot( 2 * (self.output_array - self.predicted_output) * sigmoid_derivative(self.predicted_output), @@ -155,7 +154,7 @@ def back_propagation(self) -> None: updated_second_hidden_layer_and_output_layer_weights ) - def train(self, output: numpy.ndarray, iterations: int, give_loss: bool) -> None: + def train(self, output: np.ndarray, iterations: int, give_loss: bool) -> None: """ Performs the feedforwarding and back propagation process for the given number of iterations. @@ -166,23 +165,23 @@ def train(self, output: numpy.ndarray, iterations: int, give_loss: bool) -> None give_loss : boolean value, If True then prints loss for each iteration, If False then nothing is printed - >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) - >>> output_val = numpy.array(([0], [1], [1]), dtype=float) + >>> input_val = np.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) + >>> output_val = np.array(([0], [1], [1]), dtype=float) >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) >>> first_iteration_weights = nn.feedforward() >>> nn.back_propagation() >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights - >>> (first_iteration_weights == updated_weights).all() + >>> bool((first_iteration_weights == updated_weights).all()) False """ for iteration in range(1, iterations + 1): self.output = self.feedforward() self.back_propagation() if give_loss: - loss = numpy.mean(numpy.square(output - self.feedforward())) + loss = np.mean(np.square(output - self.feedforward())) print(f"Iteration {iteration} Loss: {loss}") - def predict(self, input_arr: numpy.ndarray) -> int: + def predict(self, input_arr: np.ndarray) -> int: """ Predict's the output for the given input values using the trained neural network. @@ -192,11 +191,11 @@ def predict(self, input_arr: numpy.ndarray) -> int: than the threshold value else returns 0, as the real output values are in binary. - >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) - >>> output_val = numpy.array(([0], [1], [1]), dtype=float) + >>> input_val = np.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) + >>> output_val = np.array(([0], [1], [1]), dtype=float) >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) >>> nn.train(output_val, 1000, False) - >>> nn.predict([0,1,0]) in (0, 1) + >>> nn.predict([0, 1, 0]) in (0, 1) True """ @@ -204,46 +203,46 @@ def predict(self, input_arr: numpy.ndarray) -> int: self.array = input_arr self.layer_between_input_and_first_hidden_layer = sigmoid( - numpy.dot(self.array, self.input_layer_and_first_hidden_layer_weights) + np.dot(self.array, self.input_layer_and_first_hidden_layer_weights) ) self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid( - numpy.dot( + np.dot( self.layer_between_input_and_first_hidden_layer, self.first_hidden_layer_and_second_hidden_layer_weights, ) ) self.layer_between_second_hidden_layer_and_output = sigmoid( - numpy.dot( + np.dot( self.layer_between_first_hidden_layer_and_second_hidden_layer, self.second_hidden_layer_and_output_layer_weights, ) ) - return int(self.layer_between_second_hidden_layer_and_output > 0.6) + return int((self.layer_between_second_hidden_layer_and_output > 0.6)[0]) -def sigmoid(value: numpy.ndarray) -> numpy.ndarray: +def sigmoid(value: np.ndarray) -> np.ndarray: """ Applies sigmoid activation function. return normalized values - >>> sigmoid(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64)) + >>> sigmoid(np.array(([1, 0, 2], [1, 0, 0]), dtype=np.float64)) array([[0.73105858, 0.5 , 0.88079708], [0.73105858, 0.5 , 0.5 ]]) """ - return 1 / (1 + numpy.exp(-value)) + return 1 / (1 + np.exp(-value)) -def sigmoid_derivative(value: numpy.ndarray) -> numpy.ndarray: +def sigmoid_derivative(value: np.ndarray) -> np.ndarray: """ Provides the derivative value of the sigmoid function. returns derivative of the sigmoid value - >>> sigmoid_derivative(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64)) + >>> sigmoid_derivative(np.array(([1, 0, 2], [1, 0, 0]), dtype=np.float64)) array([[ 0., 0., -2.], [ 0., 0., 0.]]) """ @@ -264,7 +263,7 @@ def example() -> int: True """ # Input values. - test_input = numpy.array( + test_input = np.array( ( [0, 0, 0], [0, 0, 1], @@ -275,11 +274,11 @@ def example() -> int: [1, 1, 0], [1, 1, 1], ), - dtype=numpy.float64, + dtype=np.float64, ) # True output values for the given input values. - output = numpy.array(([0], [1], [1], [0], [1], [0], [0], [1]), dtype=numpy.float64) + output = np.array(([0], [1], [1], [0], [1], [0], [0], [1]), dtype=np.float64) # Calling neural network class. neural_network = TwoHiddenLayerNeuralNetwork( @@ -290,7 +289,7 @@ def example() -> int: # Set give_loss to True if you want to see loss in every iteration. neural_network.train(output=output, iterations=10, give_loss=False) - return neural_network.predict(numpy.array(([1, 1, 1]), dtype=numpy.float64)) + return neural_network.predict(np.array(([1, 1, 1]), dtype=np.float64)) if __name__ == "__main__": diff --git a/other/dijkstra_bankers_algorithm.py b/other/bankers_algorithm.py similarity index 92% rename from other/dijkstra_bankers_algorithm.py rename to other/bankers_algorithm.py index be7bceba125d..b1da851fc0f3 100644 --- a/other/dijkstra_bankers_algorithm.py +++ b/other/bankers_algorithm.py @@ -10,15 +10,14 @@ predetermined maximum possible amounts of all resources, and then makes a "s-state" check to test for possible deadlock conditions for all other pending activities, before deciding whether allocation should be allowed to continue. -[Source] Wikipedia -[Credit] Rosetta Code C implementation helped very much. - (https://rosettacode.org/wiki/Banker%27s_algorithm) + +| [Source] Wikipedia +| [Credit] Rosetta Code C implementation helped very much. +| (https://rosettacode.org/wiki/Banker%27s_algorithm) """ from __future__ import annotations -import time - import numpy as np test_claim_vector = [8, 5, 9, 7] @@ -77,7 +76,7 @@ def __available_resources(self) -> list[int]: def __need(self) -> list[list[int]]: """ Implement safety checker that calculates the needs by ensuring that - max_claim[i][j] - alloc_table[i][j] <= avail[j] + ``max_claim[i][j] - alloc_table[i][j] <= avail[j]`` """ return [ list(np.array(self.__maximum_claim_table[i]) - np.array(allocated_resource)) @@ -88,10 +87,14 @@ def __need_index_manager(self) -> dict[int, list[int]]: """ This function builds an index control dictionary to track original ids/indices of processes when altered during execution of method "main" - Return: {0: [a: int, b: int], 1: [c: int, d: int]} - >>> (BankersAlgorithm(test_claim_vector, test_allocated_res_table, - ... test_maximum_claim_table)._BankersAlgorithm__need_index_manager() - ... ) # doctest: +NORMALIZE_WHITESPACE + + :Return: {0: [a: int, b: int], 1: [c: int, d: int]} + + >>> index_control = BankersAlgorithm( + ... test_claim_vector, test_allocated_res_table, test_maximum_claim_table + ... )._BankersAlgorithm__need_index_manager() + >>> {key: [int(x) for x in value] for key, value + ... in index_control.items()} # doctest: +NORMALIZE_WHITESPACE {0: [1, 2, 0, 3], 1: [0, 1, 3, 1], 2: [1, 1, 0, 2], 3: [1, 3, 2, 0], 4: [2, 0, 0, 3]} """ @@ -100,7 +103,8 @@ def __need_index_manager(self) -> dict[int, list[int]]: def main(self, **kwargs) -> None: """ Utilize various methods in this class to simulate the Banker's algorithm - Return: None + :Return: None + >>> BankersAlgorithm(test_claim_vector, test_allocated_res_table, ... test_maximum_claim_table).main(describe=True) Allocated Resource Table @@ -216,7 +220,6 @@ def __pretty_data(self): "Initial Available Resources: " + " ".join(str(x) for x in self.__available_resources()) ) - time.sleep(1) if __name__ == "__main__": diff --git a/other/davisb_putnamb_logemannb_loveland.py b/other/davis_putnam_logemann_loveland.py similarity index 76% rename from other/davisb_putnamb_logemannb_loveland.py rename to other/davis_putnam_logemann_loveland.py index a1bea5b3992e..e95bf371a817 100644 --- a/other/davisb_putnamb_logemannb_loveland.py +++ b/other/davis_putnam_logemann_loveland.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 """ -Davis–Putnam–Logemann–Loveland (DPLL) algorithm is a complete, backtracking-based +Davis-Putnam-Logemann-Loveland (DPLL) algorithm is a complete, backtracking-based search algorithm for deciding the satisfiability of propositional logic formulae in conjunctive normal form, i.e, for solving the Conjunctive Normal Form SATisfiability (CNF-SAT) problem. For more information about the algorithm: https://en.wikipedia.org/wiki/DPLL_algorithm """ + from __future__ import annotations import random @@ -16,13 +17,15 @@ class Clause: """ - A clause represented in Conjunctive Normal Form. - A clause is a set of literals, either complemented or otherwise. + | A clause represented in Conjunctive Normal Form. + | A clause is a set of literals, either complemented or otherwise. + For example: - {A1, A2, A3'} is the clause (A1 v A2 v A3') - {A5', A2', A1} is the clause (A5' v A2' v A1) + * {A1, A2, A3'} is the clause (A1 v A2 v A3') + * {A5', A2', A1} is the clause (A5' v A2' v A1) Create model + >>> clause = Clause(["A1", "A2'", "A3"]) >>> clause.evaluate({"A1": True}) True @@ -38,6 +41,7 @@ def __init__(self, literals: list[str]) -> None: def __str__(self) -> str: """ To print a clause as in Conjunctive Normal Form. + >>> str(Clause(["A1", "A2'", "A3"])) "{A1 , A2' , A3}" """ @@ -46,6 +50,7 @@ def __str__(self) -> str: def __len__(self) -> int: """ To print a clause as in Conjunctive Normal Form. + >>> len(Clause([])) 0 >>> len(Clause(["A1", "A2'", "A3"])) @@ -63,20 +68,21 @@ def assign(self, model: dict[str, bool | None]) -> None: value = model[symbol] else: continue - if value is not None: - # Complement assignment if literal is in complemented form - if literal.endswith("'"): - value = not value + # Complement assignment if literal is in complemented form + if value is not None and literal.endswith("'"): + value = not value self.literals[literal] = value def evaluate(self, model: dict[str, bool | None]) -> bool | None: """ Evaluates the clause with the assignments in model. + This has the following steps: - 1. Return True if both a literal and its complement exist in the clause. - 2. Return True if a single literal has the assignment True. - 3. Return None(unable to complete evaluation) if a literal has no assignment. - 4. Compute disjunction of all values assigned in clause. + 1. Return ``True`` if both a literal and its complement exist in the clause. + 2. Return ``True`` if a single literal has the assignment ``True``. + 3. Return ``None`` (unable to complete evaluation) + if a literal has no assignment. + 4. Compute disjunction of all values assigned in clause. """ for literal in self.literals: symbol = literal.rstrip("'") if literal.endswith("'") else literal + "'" @@ -92,10 +98,10 @@ def evaluate(self, model: dict[str, bool | None]) -> bool | None: class Formula: """ - A formula represented in Conjunctive Normal Form. - A formula is a set of clauses. - For example, - {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1)) + | A formula represented in Conjunctive Normal Form. + | A formula is a set of clauses. + | For example, + | {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1)) """ def __init__(self, clauses: Iterable[Clause]) -> None: @@ -107,7 +113,8 @@ def __init__(self, clauses: Iterable[Clause]) -> None: def __str__(self) -> str: """ To print a formula as in Conjunctive Normal Form. - str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])) + + >>> str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])) "{{A1 , A2' , A3} , {A5' , A2' , A1}}" """ return "{" + " , ".join(str(clause) for clause in self.clauses) + "}" @@ -115,8 +122,8 @@ def __str__(self) -> str: def generate_clause() -> Clause: """ - Randomly generate a clause. - All literals have the name Ax, where x is an integer from 1 to 5. + | Randomly generate a clause. + | All literals have the name Ax, where x is an integer from ``1`` to ``5``. """ literals = [] no_of_literals = random.randint(1, 5) @@ -149,11 +156,12 @@ def generate_formula() -> Formula: def generate_parameters(formula: Formula) -> tuple[list[Clause], list[str]]: """ - Return the clauses and symbols from a formula. - A symbol is the uncomplemented form of a literal. + | Return the clauses and symbols from a formula. + | A symbol is the uncomplemented form of a literal. + For example, - Symbol of A3 is A3. - Symbol of A5' is A5. + * Symbol of A3 is A3. + * Symbol of A5' is A5. >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]) >>> clauses, symbols = generate_parameters(formula) @@ -177,21 +185,20 @@ def find_pure_symbols( clauses: list[Clause], symbols: list[str], model: dict[str, bool | None] ) -> tuple[list[str], dict[str, bool | None]]: """ - Return pure symbols and their values to satisfy clause. - Pure symbols are symbols in a formula that exist only - in one form, either complemented or otherwise. - For example, - { { A4 , A3 , A5' , A1 , A3' } , { A4 } , { A3 } } has - pure symbols A4, A5' and A1. + | Return pure symbols and their values to satisfy clause. + | Pure symbols are symbols in a formula that exist only in one form, + | either complemented or otherwise. + | For example, + | {{A4 , A3 , A5' , A1 , A3'} , {A4} , {A3}} has pure symbols A4, A5' and A1. + This has the following steps: - 1. Ignore clauses that have already evaluated to be True. - 2. Find symbols that occur only in one form in the rest of the clauses. - 3. Assign value True or False depending on whether the symbols occurs - in normal or complemented form respectively. + 1. Ignore clauses that have already evaluated to be ``True``. + 2. Find symbols that occur only in one form in the rest of the clauses. + 3. Assign value ``True`` or ``False`` depending on whether the symbols occurs + in normal or complemented form respectively. >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]) >>> clauses, symbols = generate_parameters(formula) - >>> pure_symbols, values = find_pure_symbols(clauses, symbols, {}) >>> pure_symbols ['A1', 'A2', 'A3', 'A5'] @@ -226,24 +233,26 @@ def find_pure_symbols( def find_unit_clauses( - clauses: list[Clause], model: dict[str, bool | None] + clauses: list[Clause], + model: dict[str, bool | None], # noqa: ARG001 ) -> tuple[list[str], dict[str, bool | None]]: """ Returns the unit symbols and their values to satisfy clause. + Unit symbols are symbols in a formula that are: - - Either the only symbol in a clause - - Or all other literals in that clause have been assigned False + - Either the only symbol in a clause + - Or all other literals in that clause have been assigned ``False`` + This has the following steps: - 1. Find symbols that are the only occurrences in a clause. - 2. Find symbols in a clause where all other literals are assigned False. - 3. Assign True or False depending on whether the symbols occurs in - normal or complemented form respectively. + 1. Find symbols that are the only occurrences in a clause. + 2. Find symbols in a clause where all other literals are assigned ``False``. + 3. Assign ``True`` or ``False`` depending on whether the symbols occurs in + normal or complemented form respectively. >>> clause1 = Clause(["A4", "A3", "A5'", "A1", "A3'"]) >>> clause2 = Clause(["A4"]) >>> clause3 = Clause(["A3"]) >>> clauses, symbols = generate_parameters(Formula([clause1, clause2, clause3])) - >>> unit_clauses, values = find_unit_clauses(clauses, {}) >>> unit_clauses ['A4', 'A3'] @@ -253,7 +262,7 @@ def find_unit_clauses( unit_symbols = [] for clause in clauses: if len(clause) == 1: - unit_symbols.append(list(clause.literals.keys())[0]) + unit_symbols.append(next(iter(clause.literals.keys()))) else: f_count, n_count = 0, 0 for literal, value in clause.literals.items(): @@ -277,16 +286,16 @@ def dpll_algorithm( clauses: list[Clause], symbols: list[str], model: dict[str, bool | None] ) -> tuple[bool | None, dict[str, bool | None] | None]: """ - Returns the model if the formula is satisfiable, else None + Returns the model if the formula is satisfiable, else ``None`` + This has the following steps: - 1. If every clause in clauses is True, return True. - 2. If some clause in clauses is False, return False. - 3. Find pure symbols. - 4. Find unit symbols. + 1. If every clause in clauses is ``True``, return ``True``. + 2. If some clause in clauses is ``False``, return ``False``. + 3. Find pure symbols. + 4. Find unit symbols. >>> formula = Formula([Clause(["A4", "A3", "A5'", "A1", "A3'"]), Clause(["A4"])]) >>> clauses, symbols = generate_parameters(formula) - >>> soln, model = dpll_algorithm(clauses, symbols, {}) >>> soln True diff --git a/other/doomsday.py b/other/doomsday.py index d8fe261156a1..be3b18eeecaa 100644 --- a/other/doomsday.py +++ b/other/doomsday.py @@ -46,7 +46,7 @@ def get_week_day(year: int, month: int, day: int) -> str: ) % 7 day_anchor = ( DOOMSDAY_NOT_LEAP[month - 1] - if (year % 4 != 0) or (centurian == 0 and (year % 400) == 0) + if year % 4 != 0 or (centurian == 0 and year % 400 != 0) else DOOMSDAY_LEAP[month - 1] ) week_day = (dooms_day + day - day_anchor) % 7 diff --git a/other/fischer_yates_shuffle.py b/other/fischer_yates_shuffle.py index fa2f4dce9db0..5e90b10edd89 100644 --- a/other/fischer_yates_shuffle.py +++ b/other/fischer_yates_shuffle.py @@ -1,10 +1,11 @@ #!/usr/bin/python """ -The Fisher–Yates shuffle is an algorithm for generating a random permutation of a +The Fisher-Yates shuffle is an algorithm for generating a random permutation of a finite sequence. For more details visit wikipedia/Fischer-Yates-Shuffle. """ + import random from typing import Any diff --git a/other/gauss_easter.py b/other/gauss_easter.py index 4447d4ab86af..8c8c37c92796 100644 --- a/other/gauss_easter.py +++ b/other/gauss_easter.py @@ -1,8 +1,9 @@ """ https://en.wikipedia.org/wiki/Computus#Gauss'_Easter_algorithm """ + import math -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta def gauss_easter(year: int) -> datetime: @@ -10,16 +11,16 @@ def gauss_easter(year: int) -> datetime: Calculation Gregorian easter date for given year >>> gauss_easter(2007) - datetime.datetime(2007, 4, 8, 0, 0) + datetime.datetime(2007, 4, 8, 0, 0, tzinfo=datetime.timezone.utc) >>> gauss_easter(2008) - datetime.datetime(2008, 3, 23, 0, 0) + datetime.datetime(2008, 3, 23, 0, 0, tzinfo=datetime.timezone.utc) >>> gauss_easter(2020) - datetime.datetime(2020, 4, 12, 0, 0) + datetime.datetime(2020, 4, 12, 0, 0, tzinfo=datetime.timezone.utc) >>> gauss_easter(2021) - datetime.datetime(2021, 4, 4, 0, 0) + datetime.datetime(2021, 4, 4, 0, 0, tzinfo=datetime.timezone.utc) """ metonic_cycle = year % 19 julian_leap_year = year % 4 @@ -44,16 +45,16 @@ def gauss_easter(year: int) -> datetime: ) % 7 if days_to_add == 29 and days_from_phm_to_sunday == 6: - return datetime(year, 4, 19) + return datetime(year, 4, 19, tzinfo=UTC) elif days_to_add == 28 and days_from_phm_to_sunday == 6: - return datetime(year, 4, 18) + return datetime(year, 4, 18, tzinfo=UTC) else: - return datetime(year, 3, 22) + timedelta( + return datetime(year, 3, 22, tzinfo=UTC) + timedelta( days=int(days_to_add + days_from_phm_to_sunday) ) if __name__ == "__main__": - for year in (1994, 2000, 2010, 2021, 2023): - tense = "will be" if year > datetime.now().year else "was" + for year in (1994, 2000, 2010, 2021, 2023, 2032, 2100): + tense = "will be" if year > datetime.now(tz=UTC).year else "was" print(f"Easter in {year} {tense} {gauss_easter(year)}") diff --git a/other/graham_scan.py b/other/graham_scan.py index 8e83bfcf4c49..3f11d40f141c 100644 --- a/other/graham_scan.py +++ b/other/graham_scan.py @@ -1,5 +1,5 @@ """ -This is a pure Python implementation of the merge-insertion sort algorithm +This is a pure Python implementation of the Graham scan algorithm Source: https://en.wikipedia.org/wiki/Graham_scan For doctests run following command: @@ -125,10 +125,9 @@ def graham_scan(points: list[tuple[int, int]]) -> list[tuple[int, int]]: miny = y minx = x minidx = i - if y == miny: - if x < minx: - minx = x - minidx = i + if y == miny and x < minx: + minx = x + minidx = i # remove the lowest and the most left point from points for preparing for sort points.pop(minidx) @@ -143,8 +142,8 @@ def graham_scan(points: list[tuple[int, int]]) -> list[tuple[int, int]]: stack.append(sorted_points[0]) stack.append(sorted_points[1]) stack.append(sorted_points[2]) - # In any ways, the first 3 points line are towards left. - # Because we sort them the angle from minx, miny. + # The first 3 points lines are towards the left because we sort them by their angle + # from minx, miny. current_direction = Direction.left for i in range(3, len(sorted_points)): @@ -165,7 +164,7 @@ def graham_scan(points: list[tuple[int, int]]) -> list[tuple[int, int]]: break elif current_direction == Direction.right: # If the straight line is towards right, - # every previous points on those straigh line is not convex hull. + # every previous points on that straight line is not convex hull. stack.pop() if next_direction == Direction.right: stack.pop() diff --git a/other/guess_the_number_search.py b/other/guess_the_number_search.py new file mode 100644 index 000000000000..01e8898bbb8a --- /dev/null +++ b/other/guess_the_number_search.py @@ -0,0 +1,165 @@ +""" +guess the number using lower,higher and the value to find or guess + +solution works by dividing lower and higher of number guessed + +suppose lower is 0, higher is 1000 and the number to guess is 355 + +>>> guess_the_number(10, 1000, 17) +started... +guess the number : 17 +details : [505, 257, 133, 71, 40, 25, 17] + +""" + + +def temp_input_value( + min_val: int = 10, max_val: int = 1000, option: bool = True +) -> int: + """ + Temporary input values for tests + + >>> temp_input_value(option=True) + 10 + + >>> temp_input_value(option=False) + 1000 + + >>> temp_input_value(min_val=100, option=True) + 100 + + >>> temp_input_value(min_val=100, max_val=50) + Traceback (most recent call last): + ... + ValueError: Invalid value for min_val or max_val (min_value < max_value) + + >>> temp_input_value("ten","fifty",1) + Traceback (most recent call last): + ... + AssertionError: Invalid type of value(s) specified to function! + + >>> temp_input_value(min_val=-100, max_val=500) + -100 + + >>> temp_input_value(min_val=-5100, max_val=-100) + -5100 + """ + assert ( + isinstance(min_val, int) + and isinstance(max_val, int) + and isinstance(option, bool) + ), "Invalid type of value(s) specified to function!" + + if min_val > max_val: + raise ValueError("Invalid value for min_val or max_val (min_value < max_value)") + return min_val if option else max_val + + +def get_avg(number_1: int, number_2: int) -> int: + """ + Return the mid-number(whole) of two integers a and b + + >>> get_avg(10, 15) + 12 + + >>> get_avg(20, 300) + 160 + + >>> get_avg("abcd", 300) + Traceback (most recent call last): + ... + TypeError: can only concatenate str (not "int") to str + + >>> get_avg(10.5,50.25) + 30 + """ + return int((number_1 + number_2) / 2) + + +def guess_the_number(lower: int, higher: int, to_guess: int) -> None: + """ + The `guess_the_number` function that guess the number by some operations + and using inner functions + + >>> guess_the_number(10, 1000, 17) + started... + guess the number : 17 + details : [505, 257, 133, 71, 40, 25, 17] + + >>> guess_the_number(-10000, 10000, 7) + started... + guess the number : 7 + details : [0, 5000, 2500, 1250, 625, 312, 156, 78, 39, 19, 9, 4, 6, 7] + + >>> guess_the_number(10, 1000, "a") + Traceback (most recent call last): + ... + AssertionError: argument values must be type of "int" + + >>> guess_the_number(10, 1000, 5) + Traceback (most recent call last): + ... + ValueError: guess value must be within the range of lower and higher value + + >>> guess_the_number(10000, 100, 5) + Traceback (most recent call last): + ... + ValueError: argument value for lower and higher must be(lower > higher) + """ + assert ( + isinstance(lower, int) and isinstance(higher, int) and isinstance(to_guess, int) + ), 'argument values must be type of "int"' + + if lower > higher: + raise ValueError("argument value for lower and higher must be(lower > higher)") + + if not lower < to_guess < higher: + raise ValueError( + "guess value must be within the range of lower and higher value" + ) + + def answer(number: int) -> str: + """ + Returns value by comparing with entered `to_guess` number + """ + if number > to_guess: + return "high" + elif number < to_guess: + return "low" + else: + return "same" + + print("started...") + + last_lowest = lower + last_highest = higher + + last_numbers = [] + + while True: + number = get_avg(last_lowest, last_highest) + last_numbers.append(number) + + if answer(number) == "low": + last_lowest = number + elif answer(number) == "high": + last_highest = number + else: + break + + print(f"guess the number : {last_numbers[-1]}") + print(f"details : {last_numbers!s}") + + +def main() -> None: + """ + starting point or function of script + """ + lower = int(input("Enter lower value : ").strip()) + higher = int(input("Enter high value : ").strip()) + guess = int(input("Enter value to guess : ").strip()) + guess_the_number(lower, higher, guess) + + +if __name__ == "__main__": + main() diff --git a/other/h_index.py b/other/h_index.py new file mode 100644 index 000000000000..e91389675b16 --- /dev/null +++ b/other/h_index.py @@ -0,0 +1,71 @@ +""" +Task: +Given an array of integers citations where citations[i] is the number of +citations a researcher received for their ith paper, return compute the +researcher's h-index. + +According to the definition of h-index on Wikipedia: A scientist has an +index h if h of their n papers have at least h citations each, and the other +n - h papers have no more than h citations each. + +If there are several possible values for h, the maximum one is taken as the +h-index. + +H-Index link: https://en.wikipedia.org/wiki/H-index + +Implementation notes: +Use sorting of array + +Leetcode link: https://leetcode.com/problems/h-index/description/ + +n = len(citations) +Runtime Complexity: O(n * log(n)) +Space Complexity: O(1) + +""" + + +def h_index(citations: list[int]) -> int: + """ + Return H-index of citations + + >>> h_index([3, 0, 6, 1, 5]) + 3 + >>> h_index([1, 3, 1]) + 1 + >>> h_index([1, 2, 3]) + 2 + >>> h_index('test') + Traceback (most recent call last): + ... + ValueError: The citations should be a list of non negative integers. + >>> h_index([1,2,'3']) + Traceback (most recent call last): + ... + ValueError: The citations should be a list of non negative integers. + >>> h_index([1,2,-3]) + Traceback (most recent call last): + ... + ValueError: The citations should be a list of non negative integers. + """ + + # validate: + if not isinstance(citations, list) or not all( + isinstance(item, int) and item >= 0 for item in citations + ): + raise ValueError("The citations should be a list of non negative integers.") + + citations.sort() + len_citations = len(citations) + + for i in range(len_citations): + if citations[len_citations - 1 - i] <= i: + return i + + return len_citations + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/lfu_cache.py b/other/lfu_cache.py index b68ba3a4605c..788fdf19bb60 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -24,8 +24,9 @@ def __init__(self, key: T | None, val: U | None): self.prev: DoubleLinkedListNode[T, U] | None = None def __repr__(self) -> str: - return "Node: key: {}, val: {}, freq: {}, has next: {}, has prev: {}".format( - self.key, self.val, self.freq, self.next is not None, self.prev is not None + return ( + f"Node: key: {self.key}, val: {self.val}, freq: {self.freq}, " + f"has next: {self.next is not None}, has prev: {self.prev is not None}" ) diff --git a/other/linear_congruential_generator.py b/other/linear_congruential_generator.py index 777ee6355b9b..c7de15b94bbd 100644 --- a/other/linear_congruential_generator.py +++ b/other/linear_congruential_generator.py @@ -8,11 +8,11 @@ class LinearCongruentialGenerator: A pseudorandom number generator. """ - # The default value for **seed** is the result of a function call which is not - # normally recommended and causes flake8-bugbear to raise a B008 error. However, - # in this case, it is accptable because `LinearCongruentialGenerator.__init__()` - # will only be called once per instance and it ensures that each instance will - # generate a unique sequence of numbers. + # The default value for **seed** is the result of a function call, which is not + # normally recommended and causes ruff to raise a B008 error. However, in this case, + # it is acceptable because `LinearCongruentialGenerator.__init__()` will only be + # called once per instance and it ensures that each instance will generate a unique + # sequence of numbers. def __init__(self, multiplier, increment, modulo, seed=int(time())): # noqa: B008 """ diff --git a/other/magicdiamondpattern.py b/other/magicdiamondpattern.py index 0fc41d7a25d8..58889280ab17 100644 --- a/other/magicdiamondpattern.py +++ b/other/magicdiamondpattern.py @@ -4,52 +4,76 @@ # Function to print upper half of diamond (pyramid) def floyd(n): """ - Parameters: - n : size of pattern + Print the upper half of a diamond pattern with '*' characters. + + Args: + n (int): Size of the pattern. + + Examples: + >>> floyd(3) + ' * \\n * * \\n* * * \\n' + + >>> floyd(5) + ' * \\n * * \\n * * * \\n * * * * \\n* * * * * \\n' """ - for i in range(0, n): - for _ in range(0, n - i - 1): # printing spaces - print(" ", end="") - for _ in range(0, i + 1): # printing stars - print("* ", end="") - print() + result = "" + for i in range(n): + for _ in range(n - i - 1): # printing spaces + result += " " + for _ in range(i + 1): # printing stars + result += "* " + result += "\n" + return result # Function to print lower half of diamond (pyramid) def reverse_floyd(n): """ - Parameters: - n : size of pattern + Print the lower half of a diamond pattern with '*' characters. + + Args: + n (int): Size of the pattern. + + Examples: + >>> reverse_floyd(3) + '* * * \\n * * \\n * \\n ' + + >>> reverse_floyd(5) + '* * * * * \\n * * * * \\n * * * \\n * * \\n * \\n ' """ + result = "" for i in range(n, 0, -1): for _ in range(i, 0, -1): # printing stars - print("* ", end="") - print() + result += "* " + result += "\n" for _ in range(n - i + 1, 0, -1): # printing spaces - print(" ", end="") + result += " " + return result # Function to print complete diamond pattern of "*" def pretty_print(n): """ - Parameters: - n : size of pattern + Print a complete diamond pattern with '*' characters. + + Args: + n (int): Size of the pattern. + + Examples: + >>> pretty_print(0) + ' ... .... nothing printing :(' + + >>> pretty_print(3) + ' * \\n * * \\n* * * \\n* * * \\n * * \\n * \\n ' """ if n <= 0: - print(" ... .... nothing printing :(") - return - floyd(n) # upper half - reverse_floyd(n) # lower half + return " ... .... nothing printing :(" + upper_half = floyd(n) # upper half + lower_half = reverse_floyd(n) # lower half + return upper_half + lower_half if __name__ == "__main__": - print(r"| /\ | |- | |- |--| |\ /| |-") - print(r"|/ \| |- |_ |_ |__| | \/ | |_") - K = 1 - while K: - user_number = int(input("enter the number and , and see the magic : ")) - print() - pretty_print(user_number) - K = int(input("press 0 to exit... and 1 to continue...")) - - print("Good Bye...") + import doctest + + doctest.testmod() diff --git a/other/majority_vote_algorithm.py b/other/majority_vote_algorithm.py new file mode 100644 index 000000000000..8d3b56707d06 --- /dev/null +++ b/other/majority_vote_algorithm.py @@ -0,0 +1,38 @@ +""" +This is Booyer-Moore Majority Vote Algorithm. The problem statement goes like this: +Given an integer array of size n, find all elements that appear more than ⌊ n/k ⌋ times. +We have to solve in O(n) time and O(1) Space. +URL : https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_majority_vote_algorithm +""" + +from collections import Counter + + +def majority_vote(votes: list[int], votes_needed_to_win: int) -> list[int]: + """ + >>> majority_vote([1, 2, 2, 3, 1, 3, 2], 3) + [2] + >>> majority_vote([1, 2, 2, 3, 1, 3, 2], 2) + [] + >>> majority_vote([1, 2, 2, 3, 1, 3, 2], 4) + [1, 2, 3] + """ + majority_candidate_counter: Counter[int] = Counter() + for vote in votes: + majority_candidate_counter[vote] += 1 + if len(majority_candidate_counter) == votes_needed_to_win: + majority_candidate_counter -= Counter(set(majority_candidate_counter)) + majority_candidate_counter = Counter( + vote for vote in votes if vote in majority_candidate_counter + ) + return [ + vote + for vote in majority_candidate_counter + if majority_candidate_counter[vote] > len(votes) / votes_needed_to_win + ] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/maximum_subarray.py b/other/maximum_subarray.py deleted file mode 100644 index 1c8c8cabcd2d..000000000000 --- a/other/maximum_subarray.py +++ /dev/null @@ -1,32 +0,0 @@ -from collections.abc import Sequence - - -def max_subarray_sum(nums: Sequence[int]) -> int: - """Return the maximum possible sum amongst all non - empty subarrays. - - Raises: - ValueError: when nums is empty. - - >>> max_subarray_sum([1,2,3,4,-2]) - 10 - >>> max_subarray_sum([-2,1,-3,4,-1,2,1,-5,4]) - 6 - """ - if not nums: - raise ValueError("Input sequence should not be empty") - - curr_max = ans = nums[0] - nums_len = len(nums) - - for i in range(1, nums_len): - num = nums[i] - curr_max = max(curr_max + num, num) - ans = max(curr_max, ans) - - return ans - - -if __name__ == "__main__": - n = int(input("Enter number of elements : ").strip()) - array = list(map(int, input("\nEnter the numbers : ").strip().split()))[:n] - print(max_subarray_sum(array)) diff --git a/other/maximum_subsequence.py b/other/maximum_subsequence.py new file mode 100644 index 000000000000..f81717596532 --- /dev/null +++ b/other/maximum_subsequence.py @@ -0,0 +1,42 @@ +from collections.abc import Sequence + + +def max_subsequence_sum(nums: Sequence[int] | None = None) -> int: + """Return the maximum possible sum amongst all non - empty subsequences. + + Raises: + ValueError: when nums is empty. + + >>> max_subsequence_sum([1,2,3,4,-2]) + 10 + >>> max_subsequence_sum([-2, -3, -1, -4, -6]) + -1 + >>> max_subsequence_sum([]) + Traceback (most recent call last): + . . . + ValueError: Input sequence should not be empty + >>> max_subsequence_sum() + Traceback (most recent call last): + . . . + ValueError: Input sequence should not be empty + """ + if nums is None or not nums: + raise ValueError("Input sequence should not be empty") + + ans = nums[0] + for i in range(1, len(nums)): + num = nums[i] + ans = max(ans, ans + num, num) + + return ans + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Try on a sample input from the user + n = int(input("Enter number of elements : ").strip()) + array = list(map(int, input("\nEnter the numbers : ").strip().split()))[:n] + print(max_subsequence_sum(array)) diff --git a/other/nested_brackets.py b/other/nested_brackets.py index 3f61a4e7006c..5760fa29b2fd 100644 --- a/other/nested_brackets.py +++ b/other/nested_brackets.py @@ -3,9 +3,9 @@ brackets are properly nested. A sequence of brackets s is considered properly nested if any of the following conditions are true: - - s is empty - - s has the form (U) or [U] or {U} where U is a properly nested string - - s has the form VW where V and W are properly nested strings + - s is empty + - s has the form (U) or [U] or {U} where U is a properly nested string + - s has the form VW where V and W are properly nested strings For example, the string "()()[()]" is properly nested but "[(()]" is not. @@ -14,32 +14,60 @@ """ -def is_balanced(s): +def is_balanced(s: str) -> bool: + """ + >>> is_balanced("") + True + >>> is_balanced("()") + True + >>> is_balanced("[]") + True + >>> is_balanced("{}") + True + >>> is_balanced("()[]{}") + True + >>> is_balanced("(())") + True + >>> is_balanced("[[") + False + >>> is_balanced("([{}])") + True + >>> is_balanced("(()[)]") + False + >>> is_balanced("([)]") + False + >>> is_balanced("[[()]]") + True + >>> is_balanced("(()(()))") + True + >>> is_balanced("]") + False + >>> is_balanced("Life is a bowl of cherries.") + True + >>> is_balanced("Life is a bowl of che{}ies.") + True + >>> is_balanced("Life is a bowl of che}{ies.") + False + """ + open_to_closed = {"{": "}", "[": "]", "(": ")"} stack = [] - open_brackets = set({"(", "[", "{"}) - closed_brackets = set({")", "]", "}"}) - open_to_closed = dict({"{": "}", "[": "]", "(": ")"}) - - for i in range(len(s)): - if s[i] in open_brackets: - stack.append(s[i]) - - elif s[i] in closed_brackets: - if len(stack) == 0 or ( - len(stack) > 0 and open_to_closed[stack.pop()] != s[i] - ): - return False - - return len(stack) == 0 + for symbol in s: + if symbol in open_to_closed: + stack.append(symbol) + elif symbol in open_to_closed.values() and ( + not stack or open_to_closed[stack.pop()] != symbol + ): + return False + return not stack # stack should be empty def main(): s = input("Enter sequence of brackets: ") - if is_balanced(s): - print(s, "is balanced") - else: - print(s, "is not balanced") + print(f"'{s}' is {'' if is_balanced(s) else 'not '}balanced.") if __name__ == "__main__": + from doctest import testmod + + testmod() main() diff --git a/other/number_container_system.py b/other/number_container_system.py new file mode 100644 index 000000000000..6c95dd0a3544 --- /dev/null +++ b/other/number_container_system.py @@ -0,0 +1,180 @@ +""" +A number container system that uses binary search to delete and insert values into +arrays with O(log n) write times and O(1) read times. + +This container system holds integers at indexes. + +Further explained in this leetcode problem +> https://leetcode.com/problems/minimum-cost-tree-from-leaf-values +""" + + +class NumberContainer: + def __init__(self) -> None: + # numbermap keys are the number and its values are lists of indexes sorted + # in ascending order + self.numbermap: dict[int, list[int]] = {} + # indexmap keys are an index and it's values are the number at that index + self.indexmap: dict[int, int] = {} + + def binary_search_delete(self, array: list | str | range, item: int) -> list[int]: + """ + Removes the item from the sorted array and returns + the new array. + + >>> NumberContainer().binary_search_delete([1,2,3], 2) + [1, 3] + >>> NumberContainer().binary_search_delete([0, 0, 0], 0) + [0, 0] + >>> NumberContainer().binary_search_delete([-1, -1, -1], -1) + [-1, -1] + >>> NumberContainer().binary_search_delete([-1, 0], 0) + [-1] + >>> NumberContainer().binary_search_delete([-1, 0], -1) + [0] + >>> NumberContainer().binary_search_delete(range(7), 3) + [0, 1, 2, 4, 5, 6] + >>> NumberContainer().binary_search_delete([1.1, 2.2, 3.3], 2.2) + [1.1, 3.3] + >>> NumberContainer().binary_search_delete("abcde", "c") + ['a', 'b', 'd', 'e'] + >>> NumberContainer().binary_search_delete([0, -1, 2, 4], 0) + Traceback (most recent call last): + ... + ValueError: Either the item is not in the array or the array was unsorted + >>> NumberContainer().binary_search_delete([2, 0, 4, -1, 11], -1) + Traceback (most recent call last): + ... + ValueError: Either the item is not in the array or the array was unsorted + >>> NumberContainer().binary_search_delete(125, 1) + Traceback (most recent call last): + ... + TypeError: binary_search_delete() only accepts either a list, range or str + """ + if isinstance(array, (range, str)): + array = list(array) + elif not isinstance(array, list): + raise TypeError( + "binary_search_delete() only accepts either a list, range or str" + ) + + low = 0 + high = len(array) - 1 + + while low <= high: + mid = (low + high) // 2 + if array[mid] == item: + array.pop(mid) + return array + elif array[mid] < item: + low = mid + 1 + else: + high = mid - 1 + raise ValueError( + "Either the item is not in the array or the array was unsorted" + ) + + def binary_search_insert(self, array: list | str | range, index: int) -> list[int]: + """ + Inserts the index into the sorted array + at the correct position. + + >>> NumberContainer().binary_search_insert([1,2,3], 2) + [1, 2, 2, 3] + >>> NumberContainer().binary_search_insert([0,1,3], 2) + [0, 1, 2, 3] + >>> NumberContainer().binary_search_insert([-5, -3, 0, 0, 11, 103], 51) + [-5, -3, 0, 0, 11, 51, 103] + >>> NumberContainer().binary_search_insert([-5, -3, 0, 0, 11, 100, 103], 101) + [-5, -3, 0, 0, 11, 100, 101, 103] + >>> NumberContainer().binary_search_insert(range(10), 4) + [0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9] + >>> NumberContainer().binary_search_insert("abd", "c") + ['a', 'b', 'c', 'd'] + >>> NumberContainer().binary_search_insert(131, 23) + Traceback (most recent call last): + ... + TypeError: binary_search_insert() only accepts either a list, range or str + """ + if isinstance(array, (range, str)): + array = list(array) + elif not isinstance(array, list): + raise TypeError( + "binary_search_insert() only accepts either a list, range or str" + ) + + low = 0 + high = len(array) - 1 + + while low <= high: + mid = (low + high) // 2 + if array[mid] == index: + # If the item already exists in the array, + # insert it after the existing item + array.insert(mid + 1, index) + return array + elif array[mid] < index: + low = mid + 1 + else: + high = mid - 1 + + # If the item doesn't exist in the array, insert it at the appropriate position + array.insert(low, index) + return array + + def change(self, index: int, number: int) -> None: + """ + Changes (sets) the index as number + + >>> cont = NumberContainer() + >>> cont.change(0, 10) + >>> cont.change(0, 20) + >>> cont.change(-13, 20) + >>> cont.change(-100030, 20032903290) + """ + # Remove previous index + if index in self.indexmap: + n = self.indexmap[index] + if len(self.numbermap[n]) == 1: + del self.numbermap[n] + else: + self.numbermap[n] = self.binary_search_delete(self.numbermap[n], index) + + # Set new index + self.indexmap[index] = number + + # Number not seen before or empty so insert number value + if number not in self.numbermap: + self.numbermap[number] = [index] + + # Here we need to perform a binary search insertion in order to insert + # The item in the correct place + else: + self.numbermap[number] = self.binary_search_insert( + self.numbermap[number], index + ) + + def find(self, number: int) -> int: + """ + Returns the smallest index where the number is. + + >>> cont = NumberContainer() + >>> cont.find(10) + -1 + >>> cont.change(0, 10) + >>> cont.find(10) + 0 + >>> cont.change(0, 20) + >>> cont.find(10) + -1 + >>> cont.find(20) + 0 + """ + # Simply return the 0th index (smallest) of the indexes found (or -1) + return self.numbermap.get(number, [-1])[0] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/password.py b/other/password.py index 9a6161af87d7..dff1316c049c 100644 --- a/other/password.py +++ b/other/password.py @@ -51,23 +51,12 @@ def random(chars_incl: str, i: int) -> str: return "".join(secrets.choice(chars_incl) for _ in range(i)) -def random_number(chars_incl, i): - pass # Put your code here... - - -def random_letters(chars_incl, i): - pass # Put your code here... - - -def random_characters(chars_incl, i): - pass # Put your code here... - - -# This Will Check Whether A Given Password Is Strong Or Not -# It Follows The Rule that Length Of Password Should Be At Least 8 Characters -# And At Least 1 Lower, 1 Upper, 1 Number And 1 Special Character def is_strong_password(password: str, min_length: int = 8) -> bool: """ + This will check whether a given password is strong or not. The password must be at + least as long as the provided minimum length, and it must contain at least 1 + lowercase letter, 1 uppercase letter, 1 number and 1 special character. + >>> is_strong_password('Hwea7$2!') True >>> is_strong_password('Sh0r1') @@ -81,7 +70,6 @@ def is_strong_password(password: str, min_length: int = 8) -> bool: """ if len(password) < min_length: - # Your Password must be at least 8 characters long return False upper = any(char in ascii_uppercase for char in password) @@ -90,8 +78,6 @@ def is_strong_password(password: str, min_length: int = 8) -> bool: spec_char = any(char in punctuation for char in password) return upper and lower and num and spec_char - # Passwords should contain UPPERCASE, lowerase - # numbers, and special characters def main(): @@ -104,7 +90,7 @@ def main(): "Alternative Password generated:", alternative_password_generator(chars_incl, length), ) - print("[If you are thinking of using this passsword, You better save it.]") + print("[If you are thinking of using this password, You better save it.]") if __name__ == "__main__": diff --git a/other/quine.py b/other/quine.py index 01e03bbb02cb..08e885bc1ce7 100644 --- a/other/quine.py +++ b/other/quine.py @@ -1,4 +1,5 @@ #!/bin/python3 +# ruff: noqa """ Quine: @@ -7,4 +8,5 @@ More info on: https://en.wikipedia.org/wiki/Quine_(computing) """ + print((lambda quine: quine % quine)("print((lambda quine: quine %% quine)(%r))")) diff --git a/other/scoring_algorithm.py b/other/scoring_algorithm.py index 00d87cfc0b73..0185d7a2e0c0 100644 --- a/other/scoring_algorithm.py +++ b/other/scoring_algorithm.py @@ -1,51 +1,52 @@ """ -developed by: markmelnic -original repo: https://github.com/markmelnic/Scoring-Algorithm +| developed by: markmelnic +| original repo: https://github.com/markmelnic/Scoring-Algorithm Analyse data using a range based percentual proximity algorithm and calculate the linear maximum likelihood estimation. The basic principle is that all values supplied will be broken -down to a range from 0 to 1 and each column's score will be added +down to a range from ``0`` to ``1`` and each column's score will be added up to get the total score. -========== Example for data of vehicles -price|mileage|registration_year -20k |60k |2012 -22k |50k |2011 -23k |90k |2015 -16k |210k |2010 +:: + + price|mileage|registration_year + 20k |60k |2012 + 22k |50k |2011 + 23k |90k |2015 + 16k |210k |2010 We want the vehicle with the lowest price, lowest mileage but newest registration year. Thus the weights for each column are as follows: -[0, 0, 1] +``[0, 0, 1]`` """ -def procentual_proximity( - source_data: list[list[float]], weights: list[int] -) -> list[list[float]]: +def get_data(source_data: list[list[float]]) -> list[list[float]]: """ - weights - int list - possible values - 0 / 1 - 0 if lower values have higher weight in the data set - 1 if higher values have higher weight in the data set - - >>> procentual_proximity([[20, 60, 2012],[23, 90, 2015],[22, 50, 2011]], [0, 0, 1]) - [[20, 60, 2012, 2.0], [23, 90, 2015, 1.0], [22, 50, 2011, 1.3333333333333335]] + >>> get_data([[20, 60, 2012],[23, 90, 2015],[22, 50, 2011]]) + [[20.0, 23.0, 22.0], [60.0, 90.0, 50.0], [2012.0, 2015.0, 2011.0]] """ - - # getting data data_lists: list[list[float]] = [] for data in source_data: for i, el in enumerate(data): if len(data_lists) < i + 1: data_lists.append([]) data_lists[i].append(float(el)) + return data_lists + +def calculate_each_score( + data_lists: list[list[float]], weights: list[int] +) -> list[list[float]]: + """ + >>> calculate_each_score([[20, 23, 22], [60, 90, 50], [2012, 2015, 2011]], + ... [0, 0, 1]) + [[1.0, 0.0, 0.33333333333333337], [0.75, 0.0, 1.0], [0.25, 1.0, 0.0]] + """ score_lists: list[list[float]] = [] - # calculating each score for dlist, weight in zip(data_lists, weights): mind = min(dlist) maxd = max(dlist) @@ -68,18 +69,49 @@ def procentual_proximity( # weight not 0 or 1 else: - raise ValueError(f"Invalid weight of {weight:f} provided") + msg = f"Invalid weight of {weight:f} provided" + raise ValueError(msg) score_lists.append(score) + return score_lists + + +def generate_final_scores(score_lists: list[list[float]]) -> list[float]: + """ + >>> generate_final_scores([[1.0, 0.0, 0.33333333333333337], + ... [0.75, 0.0, 1.0], + ... [0.25, 1.0, 0.0]]) + [2.0, 1.0, 1.3333333333333335] + """ # initialize final scores final_scores: list[float] = [0 for i in range(len(score_lists[0]))] - # generate final scores for slist in score_lists: for j, ele in enumerate(slist): final_scores[j] = final_scores[j] + ele + return final_scores + + +def procentual_proximity( + source_data: list[list[float]], weights: list[int] +) -> list[list[float]]: + """ + | `weights` - ``int`` list + | possible values - ``0`` / ``1`` + + * ``0`` if lower values have higher weight in the data set + * ``1`` if higher values have higher weight in the data set + + >>> procentual_proximity([[20, 60, 2012],[23, 90, 2015],[22, 50, 2011]], [0, 0, 1]) + [[20, 60, 2012, 2.0], [23, 90, 2015, 1.0], [22, 50, 2011, 1.3333333333333335]] + """ + + data_lists = get_data(source_data) + score_lists = calculate_each_score(data_lists, weights) + final_scores = generate_final_scores(score_lists) + # append scores to source data for i, ele in enumerate(final_scores): source_data[i].append(ele) diff --git a/other/sdes.py b/other/sdes.py index 31105984b9bb..42186f453a3d 100644 --- a/other/sdes.py +++ b/other/sdes.py @@ -44,11 +44,11 @@ def function(expansion, s0, s1, key, message): right = message[4:] temp = apply_table(right, expansion) temp = xor(temp, key) - l = apply_sbox(s0, temp[:4]) # noqa: E741 - r = apply_sbox(s1, temp[4:]) - l = "0" * (2 - len(l)) + l # noqa: E741 - r = "0" * (2 - len(r)) + r - temp = apply_table(l + r, p4_table) + left_bin_str = apply_sbox(s0, temp[:4]) + right_bin_str = apply_sbox(s1, temp[4:]) + left_bin_str = "0" * (2 - len(left_bin_str)) + left_bin_str + right_bin_str = "0" * (2 - len(right_bin_str)) + right_bin_str + temp = apply_table(left_bin_str + right_bin_str, p4_table) temp = xor(left, temp) return temp + right diff --git a/other/word_search.py b/other/word_search.py new file mode 100644 index 000000000000..9e8acadbd9a4 --- /dev/null +++ b/other/word_search.py @@ -0,0 +1,395 @@ +""" +Creates a random wordsearch with eight different directions +that are best described as compass locations. + +@ https://en.wikipedia.org/wiki/Word_search +""" + +from random import choice, randint, shuffle + +# The words to display on the word search - +# can be made dynamic by randonly selecting a certain number of +# words from a predefined word file, while ensuring the character +# count fits within the matrix size (n x m) +WORDS = ["cat", "dog", "snake", "fish"] + +WIDTH = 10 +HEIGHT = 10 + + +class WordSearch: + """ + >>> ws = WordSearch(WORDS, WIDTH, HEIGHT) + >>> ws.board # doctest: +ELLIPSIS + [[None, ..., None], ..., [None, ..., None]] + >>> ws.generate_board() + """ + + def __init__(self, words: list[str], width: int, height: int) -> None: + self.words = words + self.width = width + self.height = height + + # Board matrix holding each letter + self.board: list[list[str | None]] = [[None] * width for _ in range(height)] + + def insert_north(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_north("cat", [2], [2]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, 't'], + [None, None, 'a'], + [None, None, 'c']] + >>> ws.insert_north("at", [0, 1, 2], [2, 1]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, 't', 't'], + [None, 'a', 'a'], + [None, None, 'c']] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space above the row to fit in the word + if word_length > row + 1: + continue + + # Attempt to insert the word into each column + for col in cols: + # Only check to be made here is if there are existing letters + # above the column that will be overwritten + letters_above = [self.board[row - i][col] for i in range(word_length)] + if all(letter is None for letter in letters_above): + # Successful, insert the word north + for i in range(word_length): + self.board[row - i][col] = word[i] + return + + def insert_northeast(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_northeast("cat", [2], [0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, 't'], + [None, 'a', None], + ['c', None, None]] + >>> ws.insert_northeast("at", [0, 1], [2, 1, 0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, 't', 't'], + ['a', 'a', None], + ['c', None, None]] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space for the word above the row + if word_length > row + 1: + continue + + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the right of the word as well as above + if word_length + col > self.width: + continue + + # Check if there are existing letters + # to the right of the column that will be overwritten + letters_diagonal_left = [ + self.board[row - i][col + i] for i in range(word_length) + ] + if all(letter is None for letter in letters_diagonal_left): + # Successful, insert the word northeast + for i in range(word_length): + self.board[row - i][col + i] = word[i] + return + + def insert_east(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_east("cat", [1], [0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, None], + ['c', 'a', 't'], + [None, None, None]] + >>> ws.insert_east("at", [1, 0], [2, 1, 0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, 'a', 't'], + ['c', 'a', 't'], + [None, None, None]] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the right of the word + if word_length + col > self.width: + continue + + # Check if there are existing letters + # to the right of the column that will be overwritten + letters_left = [self.board[row][col + i] for i in range(word_length)] + if all(letter is None for letter in letters_left): + # Successful, insert the word east + for i in range(word_length): + self.board[row][col + i] = word[i] + return + + def insert_southeast(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_southeast("cat", [0], [0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['c', None, None], + [None, 'a', None], + [None, None, 't']] + >>> ws.insert_southeast("at", [1, 0], [2, 1, 0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['c', None, None], + ['a', 'a', None], + [None, 't', 't']] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space for the word below the row + if word_length + row > self.height: + continue + + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the right of the word as well as below + if word_length + col > self.width: + continue + + # Check if there are existing letters + # to the right of the column that will be overwritten + letters_diagonal_left = [ + self.board[row + i][col + i] for i in range(word_length) + ] + if all(letter is None for letter in letters_diagonal_left): + # Successful, insert the word southeast + for i in range(word_length): + self.board[row + i][col + i] = word[i] + return + + def insert_south(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_south("cat", [0], [0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['c', None, None], + ['a', None, None], + ['t', None, None]] + >>> ws.insert_south("at", [2, 1, 0], [0, 1, 2]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['c', None, None], + ['a', 'a', None], + ['t', 't', None]] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space below the row to fit in the word + if word_length + row > self.height: + continue + + # Attempt to insert the word into each column + for col in cols: + # Only check to be made here is if there are existing letters + # below the column that will be overwritten + letters_below = [self.board[row + i][col] for i in range(word_length)] + if all(letter is None for letter in letters_below): + # Successful, insert the word south + for i in range(word_length): + self.board[row + i][col] = word[i] + return + + def insert_southwest(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_southwest("cat", [0], [2]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, 'c'], + [None, 'a', None], + ['t', None, None]] + >>> ws.insert_southwest("at", [1, 2], [2, 1, 0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, 'c'], + [None, 'a', 'a'], + ['t', 't', None]] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space for the word below the row + if word_length + row > self.height: + continue + + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the left of the word as well as below + if word_length > col + 1: + continue + + # Check if there are existing letters + # to the right of the column that will be overwritten + letters_diagonal_left = [ + self.board[row + i][col - i] for i in range(word_length) + ] + if all(letter is None for letter in letters_diagonal_left): + # Successful, insert the word southwest + for i in range(word_length): + self.board[row + i][col - i] = word[i] + return + + def insert_west(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_west("cat", [1], [2]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [[None, None, None], + ['t', 'a', 'c'], + [None, None, None]] + >>> ws.insert_west("at", [1, 0], [1, 2, 0]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['t', 'a', None], + ['t', 'a', 'c'], + [None, None, None]] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the left of the word + if word_length > col + 1: + continue + + # Check if there are existing letters + # to the left of the column that will be overwritten + letters_left = [self.board[row][col - i] for i in range(word_length)] + if all(letter is None for letter in letters_left): + # Successful, insert the word west + for i in range(word_length): + self.board[row][col - i] = word[i] + return + + def insert_northwest(self, word: str, rows: list[int], cols: list[int]) -> None: + """ + >>> ws = WordSearch(WORDS, 3, 3) + >>> ws.insert_northwest("cat", [2], [2]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['t', None, None], + [None, 'a', None], + [None, None, 'c']] + >>> ws.insert_northwest("at", [1, 2], [0, 1]) + >>> ws.board # doctest: +NORMALIZE_WHITESPACE + [['t', None, None], + ['t', 'a', None], + [None, 'a', 'c']] + """ + word_length = len(word) + # Attempt to insert the word into each row and when successful, exit + for row in rows: + # Check if there is space for the word above the row + if word_length > row + 1: + continue + + # Attempt to insert the word into each column + for col in cols: + # Check if there is space to the left of the word as well as above + if word_length > col + 1: + continue + + # Check if there are existing letters + # to the right of the column that will be overwritten + letters_diagonal_left = [ + self.board[row - i][col - i] for i in range(word_length) + ] + if all(letter is None for letter in letters_diagonal_left): + # Successful, insert the word northwest + for i in range(word_length): + self.board[row - i][col - i] = word[i] + return + + def generate_board(self) -> None: + """ + Generates a board with a random direction for each word. + + >>> wt = WordSearch(WORDS, WIDTH, HEIGHT) + >>> wt.generate_board() + >>> len(list(filter(lambda word: word is not None, sum(wt.board, start=[]))) + ... ) == sum(map(lambda word: len(word), WORDS)) + True + """ + directions = ( + self.insert_north, + self.insert_northeast, + self.insert_east, + self.insert_southeast, + self.insert_south, + self.insert_southwest, + self.insert_west, + self.insert_northwest, + ) + for word in self.words: + # Shuffle the row order and column order that is used when brute forcing + # the insertion of the word + rows, cols = list(range(self.height)), list(range(self.width)) + shuffle(rows) + shuffle(cols) + + # Insert the word via the direction + choice(directions)(word, rows, cols) + + +def visualise_word_search( + board: list[list[str | None]] | None = None, *, add_fake_chars: bool = True +) -> None: + """ + Graphically displays the word search in the terminal. + + >>> ws = WordSearch(WORDS, 5, 5) + >>> ws.insert_north("cat", [4], [4]) + >>> visualise_word_search( + ... ws.board, add_fake_chars=False) # doctest: +NORMALIZE_WHITESPACE + # # # # # + # # # # # + # # # # t + # # # # a + # # # # c + >>> ws.insert_northeast("snake", [4], [4, 3, 2, 1, 0]) + >>> visualise_word_search( + ... ws.board, add_fake_chars=False) # doctest: +NORMALIZE_WHITESPACE + # # # # e + # # # k # + # # a # t + # n # # a + s # # # c + """ + if board is None: + word_search = WordSearch(WORDS, WIDTH, HEIGHT) + word_search.generate_board() + board = word_search.board + + result = "" + for row in range(len(board)): + for col in range(len(board[0])): + character = "#" + if (letter := board[row][col]) is not None: + character = letter + # Empty char, so add a fake char + elif add_fake_chars: + character = chr(randint(97, 122)) + result += f"{character} " + result += "\n" + print(result, end="") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + visualise_word_search() diff --git a/physics/altitude_pressure.py b/physics/altitude_pressure.py new file mode 100644 index 000000000000..65307d223fa7 --- /dev/null +++ b/physics/altitude_pressure.py @@ -0,0 +1,52 @@ +""" +Title : Calculate altitude using Pressure + +Description : + The below algorithm approximates the altitude using Barometric formula + + +""" + + +def get_altitude_at_pressure(pressure: float) -> float: + """ + This method calculates the altitude from Pressure wrt to + Sea level pressure as reference .Pressure is in Pascals + https://en.wikipedia.org/wiki/Pressure_altitude + https://community.bosch-sensortec.com/t5/Question-and-answers/How-to-calculate-the-altitude-from-the-pressure-sensor-data/qaq-p/5702 + + H = 44330 * [1 - (P/p0)^(1/5.255) ] + + Where : + H = altitude (m) + P = measured pressure + p0 = reference pressure at sea level 101325 Pa + + Examples: + >>> get_altitude_at_pressure(pressure=100_000) + 105.47836610778828 + >>> get_altitude_at_pressure(pressure=101_325) + 0.0 + >>> get_altitude_at_pressure(pressure=80_000) + 1855.873388064995 + >>> get_altitude_at_pressure(pressure=201_325) + Traceback (most recent call last): + ... + ValueError: Value Higher than Pressure at Sea Level ! + >>> get_altitude_at_pressure(pressure=-80_000) + Traceback (most recent call last): + ... + ValueError: Atmospheric Pressure can not be negative ! + """ + + if pressure > 101325: + raise ValueError("Value Higher than Pressure at Sea Level !") + if pressure < 0: + raise ValueError("Atmospheric Pressure can not be negative !") + return 44_330 * (1 - (pressure / 101_325) ** (1 / 5.5255)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/archimedes_principle.py b/physics/archimedes_principle.py deleted file mode 100644 index 6ecfc65e7461..000000000000 --- a/physics/archimedes_principle.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Calculates buoyant force on object submerged within static fluid. -Discovered by greek mathematician, Archimedes. The principle is named after him. - -Equation for calculating buoyant force: -Fb = Ï * V * g - -Source: -- https://en.wikipedia.org/wiki/Archimedes%27_principle -""" - - -# Acceleration Constant on Earth (unit m/s^2) -g = 9.80665 - - -def archimedes_principle( - fluid_density: float, volume: float, gravity: float = g -) -> float: - """ - Args: - fluid_density: density of fluid (kg/m^3) - volume: volume of object / liquid being displaced by object - gravity: Acceleration from gravity. Gravitational force on system, - Default is Earth Gravity - returns: - buoyant force on object in Newtons - - >>> archimedes_principle(fluid_density=997, volume=0.5, gravity=9.8) - 4885.3 - >>> archimedes_principle(fluid_density=997, volume=0.7) - 6844.061035 - """ - - if fluid_density <= 0: - raise ValueError("Impossible fluid density") - if volume < 0: - raise ValueError("Impossible Object volume") - if gravity <= 0: - raise ValueError("Impossible Gravity") - - return fluid_density * gravity * volume - - -if __name__ == "__main__": - import doctest - - # run doctest - doctest.testmod() diff --git a/physics/archimedes_principle_of_buoyant_force.py b/physics/archimedes_principle_of_buoyant_force.py new file mode 100644 index 000000000000..38f1a0a83832 --- /dev/null +++ b/physics/archimedes_principle_of_buoyant_force.py @@ -0,0 +1,62 @@ +""" +Calculate the buoyant force of any body completely or partially submerged in a static +fluid. This principle was discovered by the Greek mathematician Archimedes. + +Equation for calculating buoyant force: +Fb = p * V * g + +https://en.wikipedia.org/wiki/Archimedes%27_principle +""" + +# Acceleration Constant on Earth (unit m/s^2) +g = 9.80665 # Also available in scipy.constants.g + + +def archimedes_principle( + fluid_density: float, volume: float, gravity: float = g +) -> float: + """ + Args: + fluid_density: density of fluid (kg/m^3) + volume: volume of object/liquid being displaced by the object (m^3) + gravity: Acceleration from gravity. Gravitational force on the system, + The default is Earth Gravity + returns: + the buoyant force on an object in Newtons + + >>> archimedes_principle(fluid_density=500, volume=4, gravity=9.8) + 19600.0 + >>> archimedes_principle(fluid_density=997, volume=0.5, gravity=9.8) + 4885.3 + >>> archimedes_principle(fluid_density=997, volume=0.7) + 6844.061035 + >>> archimedes_principle(fluid_density=997, volume=-0.7) + Traceback (most recent call last): + ... + ValueError: Impossible object volume + >>> archimedes_principle(fluid_density=0, volume=0.7) + Traceback (most recent call last): + ... + ValueError: Impossible fluid density + >>> archimedes_principle(fluid_density=997, volume=0.7, gravity=0) + 0.0 + >>> archimedes_principle(fluid_density=997, volume=0.7, gravity=-9.8) + Traceback (most recent call last): + ... + ValueError: Impossible gravity + """ + + if fluid_density <= 0: + raise ValueError("Impossible fluid density") + if volume <= 0: + raise ValueError("Impossible object volume") + if gravity < 0: + raise ValueError("Impossible gravity") + + return fluid_density * gravity * volume + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/basic_orbital_capture.py b/physics/basic_orbital_capture.py new file mode 100644 index 000000000000..eb1fdd9d6420 --- /dev/null +++ b/physics/basic_orbital_capture.py @@ -0,0 +1,177 @@ +""" +These two functions will return the radii of impact for a target object +of mass M and radius R as well as it's effective cross sectional area sigma. +That is to say any projectile with velocity v passing within sigma, will impact the +target object with mass M. The derivation of which is given at the bottom +of this file. + +The derivation shows that a projectile does not need to aim directly at the target +body in order to hit it, as R_capture>R_target. Astronomers refer to the effective +cross section for capture as sigma=Ï€*R_capture**2. + +This algorithm does not account for an N-body problem. +""" + +from math import pow, sqrt # noqa: A004 + +from scipy.constants import G, c, pi + + +def capture_radii( + target_body_radius: float, target_body_mass: float, projectile_velocity: float +) -> float: + """ + Input Params: + ------------- + target_body_radius: Radius of the central body SI units: meters | m + target_body_mass: Mass of the central body SI units: kilograms | kg + projectile_velocity: Velocity of object moving toward central body + SI units: meters/second | m/s + Returns: + -------- + >>> capture_radii(6.957e8, 1.99e30, 25000.0) + 17209590691.0 + >>> capture_radii(-6.957e8, 1.99e30, 25000.0) + Traceback (most recent call last): + ... + ValueError: Radius cannot be less than 0 + >>> capture_radii(6.957e8, -1.99e30, 25000.0) + Traceback (most recent call last): + ... + ValueError: Mass cannot be less than 0 + >>> capture_radii(6.957e8, 1.99e30, c+1) + Traceback (most recent call last): + ... + ValueError: Cannot go beyond speed of light + + Returned SI units: + ------------------ + meters | m + """ + + if target_body_mass < 0: + raise ValueError("Mass cannot be less than 0") + if target_body_radius < 0: + raise ValueError("Radius cannot be less than 0") + if projectile_velocity > c: + raise ValueError("Cannot go beyond speed of light") + + escape_velocity_squared = (2 * G * target_body_mass) / target_body_radius + capture_radius = target_body_radius * sqrt( + 1 + escape_velocity_squared / pow(projectile_velocity, 2) + ) + return round(capture_radius, 0) + + +def capture_area(capture_radius: float) -> float: + """ + Input Param: + ------------ + capture_radius: The radius of orbital capture and impact for a central body of + mass M and a projectile moving towards it with velocity v + SI units: meters | m + Returns: + -------- + >>> capture_area(17209590691) + 9.304455331329126e+20 + >>> capture_area(-1) + Traceback (most recent call last): + ... + ValueError: Cannot have a capture radius less than 0 + + Returned SI units: + ------------------ + meters*meters | m**2 + """ + + if capture_radius < 0: + raise ValueError("Cannot have a capture radius less than 0") + sigma = pi * pow(capture_radius, 2) + return round(sigma, 0) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + +""" +Derivation: + +Let: Mt=target mass, Rt=target radius, v=projectile_velocity, + r_0=radius of projectile at instant 0 to CM of target + v_p=v at closest approach, + r_p=radius from projectile to target CM at closest approach, + R_capture= radius of impact for projectile with velocity v + +(1)At time=0 the projectile's energy falling from infinity| E=K+U=0.5*m*(v**2)+0 + + E_initial=0.5*m*(v**2) + +(2)at time=0 the angular momentum of the projectile relative to CM target| + L_initial=m*r_0*v*sin(Θ)->m*r_0*v*(R_capture/r_0)->m*v*R_capture + + L_i=m*v*R_capture + +(3)The energy of the projectile at closest approach will be its kinetic energy + at closest approach plus gravitational potential energy(-(GMm)/R)| + E_p=K_p+U_p->E_p=0.5*m*(v_p**2)-(G*Mt*m)/r_p + + E_p=0.0.5*m*(v_p**2)-(G*Mt*m)/r_p + +(4)The angular momentum of the projectile relative to the target at closest + approach will be L_p=m*r_p*v_p*sin(Θ), however relative to the target Θ=90° + sin(90°)=1| + + L_p=m*r_p*v_p +(5)Using conservation of angular momentum and energy, we can write a quadratic + equation that solves for r_p| + + (a) + Ei=Ep-> 0.5*m*(v**2)=0.5*m*(v_p**2)-(G*Mt*m)/r_p-> v**2=v_p**2-(2*G*Mt)/r_p + + (b) + Li=Lp-> m*v*R_capture=m*r_p*v_p-> v*R_capture=r_p*v_p-> v_p=(v*R_capture)/r_p + + (c) b plugs int a| + v**2=((v*R_capture)/r_p)**2-(2*G*Mt)/r_p-> + + v**2-(v**2)*(R_c**2)/(r_p**2)+(2*G*Mt)/r_p=0-> + + (v**2)*(r_p**2)+2*G*Mt*r_p-(v**2)*(R_c**2)=0 + + (d) Using the quadratic formula, we'll solve for r_p then rearrange to solve to + R_capture + + r_p=(-2*G*Mt ± sqrt(4*G^2*Mt^2+ 4(v^4*R_c^2)))/(2*v^2)-> + + r_p=(-G*Mt ± sqrt(G^2*Mt+v^4*R_c^2))/v^2-> + + r_p<0 is something we can ignore, as it has no physical meaning for our purposes.-> + + r_p=(-G*Mt)/v^2 + sqrt(G^2*Mt^2/v^4 + R_c^2) + + (e)We are trying to solve for R_c. We are looking for impact, so we want r_p=Rt + + Rt + G*Mt/v^2 = sqrt(G^2*Mt^2/v^4 + R_c^2)-> + + (Rt + G*Mt/v^2)^2 = G^2*Mt^2/v^4 + R_c^2-> + + Rt^2 + 2*G*Mt*Rt/v^2 + G^2*Mt^2/v^4 = G^2*Mt^2/v^4 + R_c^2-> + + Rt**2 + 2*G*Mt*Rt/v**2 = R_c**2-> + + Rt**2 * (1 + 2*G*Mt/Rt *1/v**2) = R_c**2-> + + escape velocity = sqrt(2GM/R)= v_escape**2=2GM/R-> + + Rt**2 * (1 + v_esc**2/v**2) = R_c**2-> + +(6) + R_capture = Rt * sqrt(1 + v_esc**2/v**2) + +Source: Problem Set 3 #8 c.Fall_2017|Honors Astronomy|Professor Rachel Bezanson + +Source #2: http://www.nssc.ac.cn/wxzygx/weixin/201607/P020160718380095698873.pdf + 8.8 Planetary Rendezvous: Pg.368 +""" diff --git a/physics/center_of_mass.py b/physics/center_of_mass.py new file mode 100644 index 000000000000..7a20e71be801 --- /dev/null +++ b/physics/center_of_mass.py @@ -0,0 +1,110 @@ +""" +Calculating the center of mass for a discrete system of particles, given their +positions and masses. + +Description: + +In physics, the center of mass of a distribution of mass in space (sometimes referred +to as the barycenter or balance point) is the unique point at any given time where the +weighted relative position of the distributed mass sums to zero. This is the point to +which a force may be applied to cause a linear acceleration without an angular +acceleration. + +Calculations in mechanics are often simplified when formulated with respect to the +center of mass. It is a hypothetical point where the entire mass of an object may be +assumed to be concentrated to visualize its motion. In other words, the center of mass +is the particle equivalent of a given object for the application of Newton's laws of +motion. + +In the case of a system of particles P_i, i = 1, ..., n , each with mass m_i that are +located in space with coordinates r_i, i = 1, ..., n , the coordinates R of the center +of mass corresponds to: + +R = (Σ(mi * ri) / Σ(mi)) + +Reference: https://en.wikipedia.org/wiki/Center_of_mass +""" + +from collections import namedtuple + +Particle = namedtuple("Particle", "x y z mass") # noqa: PYI024 +Coord3D = namedtuple("Coord3D", "x y z") # noqa: PYI024 + + +def center_of_mass(particles: list[Particle]) -> Coord3D: + """ + Input Parameters + ---------------- + particles: list(Particle): + A list of particles where each particle is a tuple with it's (x, y, z) position and + it's mass. + + Returns + ------- + Coord3D: + A tuple with the coordinates of the center of mass (Xcm, Ycm, Zcm) rounded to two + decimal places. + + Examples + -------- + >>> center_of_mass([ + ... Particle(1.5, 4, 3.4, 4), + ... Particle(5, 6.8, 7, 8.1), + ... Particle(9.4, 10.1, 11.6, 12) + ... ]) + Coord3D(x=6.61, y=7.98, z=8.69) + + >>> center_of_mass([ + ... Particle(1, 2, 3, 4), + ... Particle(5, 6, 7, 8), + ... Particle(9, 10, 11, 12) + ... ]) + Coord3D(x=6.33, y=7.33, z=8.33) + + >>> center_of_mass([ + ... Particle(1, 2, 3, -4), + ... Particle(5, 6, 7, 8), + ... Particle(9, 10, 11, 12) + ... ]) + Traceback (most recent call last): + ... + ValueError: Mass of all particles must be greater than 0 + + >>> center_of_mass([ + ... Particle(1, 2, 3, 0), + ... Particle(5, 6, 7, 8), + ... Particle(9, 10, 11, 12) + ... ]) + Traceback (most recent call last): + ... + ValueError: Mass of all particles must be greater than 0 + + >>> center_of_mass([]) + Traceback (most recent call last): + ... + ValueError: No particles provided + """ + if not particles: + raise ValueError("No particles provided") + + if any(particle.mass <= 0 for particle in particles): + raise ValueError("Mass of all particles must be greater than 0") + + total_mass = sum(particle.mass for particle in particles) + + center_of_mass_x = round( + sum(particle.x * particle.mass for particle in particles) / total_mass, 2 + ) + center_of_mass_y = round( + sum(particle.y * particle.mass for particle in particles) / total_mass, 2 + ) + center_of_mass_z = round( + sum(particle.z * particle.mass for particle in particles) / total_mass, 2 + ) + return Coord3D(center_of_mass_x, center_of_mass_y, center_of_mass_z) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/centripetal_force.py b/physics/centripetal_force.py index 04069d256468..a4c624582475 100644 --- a/physics/centripetal_force.py +++ b/physics/centripetal_force.py @@ -6,7 +6,7 @@ The unit of centripetal force is newton. The centripetal force is always directed perpendicular to the -direction of the object’s displacement. Using Newton’s second +direction of the object's displacement. Using Newton's second law of motion, it is found that the centripetal force of an object moving in a circular path always acts towards the centre of the circle. The Centripetal Force Formula is given as the product of mass (in kg) diff --git a/physics/coulombs_law.py b/physics/coulombs_law.py new file mode 100644 index 000000000000..fe2d358f653e --- /dev/null +++ b/physics/coulombs_law.py @@ -0,0 +1,42 @@ +""" +Coulomb's law states that the magnitude of the electrostatic force of attraction +or repulsion between two point charges is directly proportional to the product +of the magnitudes of charges and inversely proportional to the square of the +distance between them. + +F = k * q1 * q2 / r^2 + +k is Coulomb's constant and equals 1/(4Ï€*ε0) +q1 is charge of first body (C) +q2 is charge of second body (C) +r is distance between two charged bodies (m) + +Reference: https://en.wikipedia.org/wiki/Coulomb%27s_law +""" + + +def coulombs_law(q1: float, q2: float, radius: float) -> float: + """ + Calculate the electrostatic force of attraction or repulsion + between two point charges + + >>> coulombs_law(15.5, 20, 15) + 12382849136.06 + >>> coulombs_law(1, 15, 5) + 5392531075.38 + >>> coulombs_law(20, -50, 15) + -39944674632.44 + >>> coulombs_law(-5, -8, 10) + 3595020716.92 + >>> coulombs_law(50, 100, 50) + 17975103584.6 + """ + if radius <= 0: + raise ValueError("The radius is always a positive number") + return round(((8.9875517923 * 10**9) * q1 * q2) / (radius**2), 2) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/doppler_frequency.py b/physics/doppler_frequency.py new file mode 100644 index 000000000000..2a761c72d9b8 --- /dev/null +++ b/physics/doppler_frequency.py @@ -0,0 +1,104 @@ +""" +Doppler's effect + +The Doppler effect (also Doppler shift) is the change in the frequency of a wave in +relation to an observer who is moving relative to the source of the wave. The Doppler +effect is named after the physicist Christian Doppler. A common example of Doppler +shift is the change of pitch heard when a vehicle sounding a horn approaches and +recedes from an observer. + +The reason for the Doppler effect is that when the source of the waves is moving +towards the observer, each successive wave crest is emitted from a position closer to +the observer than the crest of the previous wave. Therefore, each wave takes slightly +less time to reach the observer than the previous wave. Hence, the time between the +arrivals of successive wave crests at the observer is reduced, causing an increase in +the frequency. Similarly, if the source of waves is moving away from the observer, +each wave is emitted from a position farther from the observer than the previous wave, +so the arrival time between successive waves is increased, reducing the frequency. + +If the source of waves is stationary but the observer is moving with respect to the +source, the transmission velocity of the waves changes (ie the rate at which the +observer receives waves) even if the wavelength and frequency emitted from the source +remain constant. + +These results are all summarized by the Doppler formula: + + f = (f0 * (v + v0)) / (v - vs) + +where: + f: frequency of the wave + f0: frequency of the wave when the source is stationary + v: velocity of the wave in the medium + v0: velocity of the observer, positive if the observer is moving towards the source + vs: velocity of the source, positive if the source is moving towards the observer + +Doppler's effect has many applications in physics and engineering, such as radar, +astronomy, medical imaging, and seismology. + +References: +https://en.wikipedia.org/wiki/Doppler_effect + +Now, we will implement a function that calculates the frequency of a wave as a function +of the frequency of the wave when the source is stationary, the velocity of the wave +in the medium, the velocity of the observer and the velocity of the source. +""" + + +def doppler_effect( + org_freq: float, wave_vel: float, obs_vel: float, src_vel: float +) -> float: + """ + Input Parameters: + ----------------- + org_freq: frequency of the wave when the source is stationary + wave_vel: velocity of the wave in the medium + obs_vel: velocity of the observer, +ve if the observer is moving towards the source + src_vel: velocity of the source, +ve if the source is moving towards the observer + + Returns: + -------- + f: frequency of the wave as perceived by the observer + + Docstring Tests: + >>> doppler_effect(100, 330, 10, 0) # observer moving towards the source + 103.03030303030303 + >>> doppler_effect(100, 330, -10, 0) # observer moving away from the source + 96.96969696969697 + >>> doppler_effect(100, 330, 0, 10) # source moving towards the observer + 103.125 + >>> doppler_effect(100, 330, 0, -10) # source moving away from the observer + 97.05882352941177 + >>> doppler_effect(100, 330, 10, 10) # source & observer moving towards each other + 106.25 + >>> doppler_effect(100, 330, -10, -10) # source and observer moving away + 94.11764705882354 + >>> doppler_effect(100, 330, 10, 330) # source moving at same speed as the wave + Traceback (most recent call last): + ... + ZeroDivisionError: Division by zero implies vs=v and observer in front of the source + >>> doppler_effect(100, 330, 10, 340) # source moving faster than the wave + Traceback (most recent call last): + ... + ValueError: Non-positive frequency implies vs>v or v0>v (in the opposite direction) + >>> doppler_effect(100, 330, -340, 10) # observer moving faster than the wave + Traceback (most recent call last): + ... + ValueError: Non-positive frequency implies vs>v or v0>v (in the opposite direction) + """ + + if wave_vel == src_vel: + raise ZeroDivisionError( + "Division by zero implies vs=v and observer in front of the source" + ) + doppler_freq = (org_freq * (wave_vel + obs_vel)) / (wave_vel - src_vel) + if doppler_freq <= 0: + raise ValueError( + "Non-positive frequency implies vs>v or v0>v (in the opposite direction)" + ) + return doppler_freq + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/grahams_law.py b/physics/grahams_law.py new file mode 100644 index 000000000000..c56359280ea4 --- /dev/null +++ b/physics/grahams_law.py @@ -0,0 +1,208 @@ +""" +Title: Graham's Law of Effusion + +Description: Graham's law of effusion states that the rate of effusion of a gas is +inversely proportional to the square root of the molar mass of its particles: + +r1/r2 = sqrt(m2/m1) + +r1 = Rate of effusion for the first gas. +r2 = Rate of effusion for the second gas. +m1 = Molar mass of the first gas. +m2 = Molar mass of the second gas. + +(Description adapted from https://en.wikipedia.org/wiki/Graham%27s_law) +""" + +from math import pow, sqrt # noqa: A004 + + +def validate(*values: float) -> bool: + """ + Input Parameters: + ----------------- + effusion_rate_1: Effustion rate of first gas (m^2/s, mm^2/s, etc.) + effusion_rate_2: Effustion rate of second gas (m^2/s, mm^2/s, etc.) + molar_mass_1: Molar mass of the first gas (g/mol, kg/kmol, etc.) + molar_mass_2: Molar mass of the second gas (g/mol, kg/kmol, etc.) + + Returns: + -------- + >>> validate(2.016, 4.002) + True + >>> validate(-2.016, 4.002) + False + >>> validate() + False + """ + result = len(values) > 0 and all(value > 0.0 for value in values) + return result + + +def effusion_ratio(molar_mass_1: float, molar_mass_2: float) -> float | ValueError: + """ + Input Parameters: + ----------------- + molar_mass_1: Molar mass of the first gas (g/mol, kg/kmol, etc.) + molar_mass_2: Molar mass of the second gas (g/mol, kg/kmol, etc.) + + Returns: + -------- + >>> effusion_ratio(2.016, 4.002) + 1.408943 + >>> effusion_ratio(-2.016, 4.002) + ValueError('Input Error: Molar mass values must greater than 0.') + >>> effusion_ratio(2.016) + Traceback (most recent call last): + ... + TypeError: effusion_ratio() missing 1 required positional argument: 'molar_mass_2' + """ + return ( + round(sqrt(molar_mass_2 / molar_mass_1), 6) + if validate(molar_mass_1, molar_mass_2) + else ValueError("Input Error: Molar mass values must greater than 0.") + ) + + +def first_effusion_rate( + effusion_rate: float, molar_mass_1: float, molar_mass_2: float +) -> float | ValueError: + """ + Input Parameters: + ----------------- + effusion_rate: Effustion rate of second gas (m^2/s, mm^2/s, etc.) + molar_mass_1: Molar mass of the first gas (g/mol, kg/kmol, etc.) + molar_mass_2: Molar mass of the second gas (g/mol, kg/kmol, etc.) + + Returns: + -------- + >>> first_effusion_rate(1, 2.016, 4.002) + 1.408943 + >>> first_effusion_rate(-1, 2.016, 4.002) + ValueError('Input Error: Molar mass and effusion rate values must greater than 0.') + >>> first_effusion_rate(1) + Traceback (most recent call last): + ... + TypeError: first_effusion_rate() missing 2 required positional arguments: \ +'molar_mass_1' and 'molar_mass_2' + >>> first_effusion_rate(1, 2.016) + Traceback (most recent call last): + ... + TypeError: first_effusion_rate() missing 1 required positional argument: \ +'molar_mass_2' + """ + return ( + round(effusion_rate * sqrt(molar_mass_2 / molar_mass_1), 6) + if validate(effusion_rate, molar_mass_1, molar_mass_2) + else ValueError( + "Input Error: Molar mass and effusion rate values must greater than 0." + ) + ) + + +def second_effusion_rate( + effusion_rate: float, molar_mass_1: float, molar_mass_2: float +) -> float | ValueError: + """ + Input Parameters: + ----------------- + effusion_rate: Effustion rate of second gas (m^2/s, mm^2/s, etc.) + molar_mass_1: Molar mass of the first gas (g/mol, kg/kmol, etc.) + molar_mass_2: Molar mass of the second gas (g/mol, kg/kmol, etc.) + + Returns: + -------- + >>> second_effusion_rate(1, 2.016, 4.002) + 0.709752 + >>> second_effusion_rate(-1, 2.016, 4.002) + ValueError('Input Error: Molar mass and effusion rate values must greater than 0.') + >>> second_effusion_rate(1) + Traceback (most recent call last): + ... + TypeError: second_effusion_rate() missing 2 required positional arguments: \ +'molar_mass_1' and 'molar_mass_2' + >>> second_effusion_rate(1, 2.016) + Traceback (most recent call last): + ... + TypeError: second_effusion_rate() missing 1 required positional argument: \ +'molar_mass_2' + """ + return ( + round(effusion_rate / sqrt(molar_mass_2 / molar_mass_1), 6) + if validate(effusion_rate, molar_mass_1, molar_mass_2) + else ValueError( + "Input Error: Molar mass and effusion rate values must greater than 0." + ) + ) + + +def first_molar_mass( + molar_mass: float, effusion_rate_1: float, effusion_rate_2: float +) -> float | ValueError: + """ + Input Parameters: + ----------------- + molar_mass: Molar mass of the first gas (g/mol, kg/kmol, etc.) + effusion_rate_1: Effustion rate of first gas (m^2/s, mm^2/s, etc.) + effusion_rate_2: Effustion rate of second gas (m^2/s, mm^2/s, etc.) + + Returns: + -------- + >>> first_molar_mass(2, 1.408943, 0.709752) + 0.507524 + >>> first_molar_mass(-1, 2.016, 4.002) + ValueError('Input Error: Molar mass and effusion rate values must greater than 0.') + >>> first_molar_mass(1) + Traceback (most recent call last): + ... + TypeError: first_molar_mass() missing 2 required positional arguments: \ +'effusion_rate_1' and 'effusion_rate_2' + >>> first_molar_mass(1, 2.016) + Traceback (most recent call last): + ... + TypeError: first_molar_mass() missing 1 required positional argument: \ +'effusion_rate_2' + """ + return ( + round(molar_mass / pow(effusion_rate_1 / effusion_rate_2, 2), 6) + if validate(molar_mass, effusion_rate_1, effusion_rate_2) + else ValueError( + "Input Error: Molar mass and effusion rate values must greater than 0." + ) + ) + + +def second_molar_mass( + molar_mass: float, effusion_rate_1: float, effusion_rate_2: float +) -> float | ValueError: + """ + Input Parameters: + ----------------- + molar_mass: Molar mass of the first gas (g/mol, kg/kmol, etc.) + effusion_rate_1: Effustion rate of first gas (m^2/s, mm^2/s, etc.) + effusion_rate_2: Effustion rate of second gas (m^2/s, mm^2/s, etc.) + + Returns: + -------- + >>> second_molar_mass(2, 1.408943, 0.709752) + 1.970351 + >>> second_molar_mass(-2, 1.408943, 0.709752) + ValueError('Input Error: Molar mass and effusion rate values must greater than 0.') + >>> second_molar_mass(1) + Traceback (most recent call last): + ... + TypeError: second_molar_mass() missing 2 required positional arguments: \ +'effusion_rate_1' and 'effusion_rate_2' + >>> second_molar_mass(1, 2.016) + Traceback (most recent call last): + ... + TypeError: second_molar_mass() missing 1 required positional argument: \ +'effusion_rate_2' + """ + return ( + round(pow(effusion_rate_1 / effusion_rate_2, 2) / molar_mass, 6) + if validate(molar_mass, effusion_rate_1, effusion_rate_2) + else ValueError( + "Input Error: Molar mass and effusion rate values must greater than 0." + ) + ) diff --git a/physics/horizontal_projectile_motion.py b/physics/horizontal_projectile_motion.py index dbde3660f62f..60f21c2b39c4 100644 --- a/physics/horizontal_projectile_motion.py +++ b/physics/horizontal_projectile_motion.py @@ -1,15 +1,18 @@ """ Horizontal Projectile Motion problem in physics. + This algorithm solves a specific problem in which -the motion starts from the ground as can be seen below: - (v = 0) - * * - * * - * * - * * - * * - * * -GROUND GROUND +the motion starts from the ground as can be seen below:: + + (v = 0) + * * + * * + * * + * * + * * + * * + GROUND GROUND + For more info: https://en.wikipedia.org/wiki/Projectile_motion """ @@ -43,14 +46,17 @@ def check_args(init_velocity: float, angle: float) -> None: def horizontal_distance(init_velocity: float, angle: float) -> float: - """ + r""" Returns the horizontal distance that the object cover + Formula: - v_0^2 * sin(2 * alpha) - --------------------- - g - v_0 - initial velocity - alpha - angle + .. math:: + \frac{v_0^2 \cdot \sin(2 \alpha)}{g} + + v_0 - \text{initial velocity} + + \alpha - \text{angle} + >>> horizontal_distance(30, 45) 91.77 >>> horizontal_distance(100, 78) @@ -70,14 +76,17 @@ def horizontal_distance(init_velocity: float, angle: float) -> float: def max_height(init_velocity: float, angle: float) -> float: - """ + r""" Returns the maximum height that the object reach + Formula: - v_0^2 * sin^2(alpha) - -------------------- - 2g - v_0 - initial velocity - alpha - angle + .. math:: + \frac{v_0^2 \cdot \sin^2 (\alpha)}{2 g} + + v_0 - \text{initial velocity} + + \alpha - \text{angle} + >>> max_height(30, 45) 22.94 >>> max_height(100, 78) @@ -97,14 +106,17 @@ def max_height(init_velocity: float, angle: float) -> float: def total_time(init_velocity: float, angle: float) -> float: - """ + r""" Returns total time of the motion + Formula: - 2 * v_0 * sin(alpha) - -------------------- - g - v_0 - initial velocity - alpha - angle + .. math:: + \frac{2 v_0 \cdot \sin (\alpha)}{g} + + v_0 - \text{initial velocity} + + \alpha - \text{angle} + >>> total_time(30, 45) 4.33 >>> total_time(100, 78) @@ -125,6 +137,8 @@ def total_time(init_velocity: float, angle: float) -> float: def test_motion() -> None: """ + Test motion + >>> test_motion() """ v0, angle = 25, 20 @@ -147,6 +161,6 @@ def test_motion() -> None: # Print results print() print("Results: ") - print(f"Horizontal Distance: {str(horizontal_distance(init_vel, angle))} [m]") - print(f"Maximum Height: {str(max_height(init_vel, angle))} [m]") - print(f"Total Time: {str(total_time(init_vel, angle))} [s]") + print(f"Horizontal Distance: {horizontal_distance(init_vel, angle)!s} [m]") + print(f"Maximum Height: {max_height(init_vel, angle)!s} [m]") + print(f"Total Time: {total_time(init_vel, angle)!s} [s]") diff --git a/physics/hubble_parameter.py b/physics/hubble_parameter.py index 6bc62e7131c5..f7b2d28a6716 100644 --- a/physics/hubble_parameter.py +++ b/physics/hubble_parameter.py @@ -70,10 +70,10 @@ def hubble_parameter( 68.3 """ parameters = [redshift, radiation_density, matter_density, dark_energy] - if any(0 > p for p in parameters): + if any(p < 0 for p in parameters): raise ValueError("All input parameters must be positive") - if any(1 < p for p in parameters[1:4]): + if any(p > 1 for p in parameters[1:4]): raise ValueError("Relative densities cannot be greater than one") else: curvature = 1 - (matter_density + radiation_density + dark_energy) diff --git a/physics/ideal_gas_law.py b/physics/ideal_gas_law.py index 805da47b0079..09b4fb3a9c14 100644 --- a/physics/ideal_gas_law.py +++ b/physics/ideal_gas_law.py @@ -53,6 +53,40 @@ def volume_of_gas_system(moles: float, kelvin: float, pressure: float) -> float: return moles * kelvin * UNIVERSAL_GAS_CONSTANT / pressure +def temperature_of_gas_system(moles: float, volume: float, pressure: float) -> float: + """ + >>> temperature_of_gas_system(2, 100, 5) + 30.068090996146232 + >>> temperature_of_gas_system(11, 5009, 1000) + 54767.66101807144 + >>> temperature_of_gas_system(3, -0.46, 23.5) + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter positive value. + """ + if moles < 0 or volume < 0 or pressure < 0: + raise ValueError("Invalid inputs. Enter positive value.") + + return pressure * volume / (moles * UNIVERSAL_GAS_CONSTANT) + + +def moles_of_gas_system(kelvin: float, volume: float, pressure: float) -> float: + """ + >>> moles_of_gas_system(100, 5, 10) + 0.06013618199229246 + >>> moles_of_gas_system(110, 5009, 1000) + 5476.766101807144 + >>> moles_of_gas_system(3, -0.46, 23.5) + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter positive value. + """ + if kelvin < 0 or volume < 0 or pressure < 0: + raise ValueError("Invalid inputs. Enter positive value.") + + return pressure * volume / (kelvin * UNIVERSAL_GAS_CONSTANT) + + if __name__ == "__main__": from doctest import testmod diff --git a/arithmetic_analysis/image_data/2D_problems.jpg b/physics/image_data/2D_problems.jpg similarity index 100% rename from arithmetic_analysis/image_data/2D_problems.jpg rename to physics/image_data/2D_problems.jpg diff --git a/arithmetic_analysis/image_data/2D_problems_1.jpg b/physics/image_data/2D_problems_1.jpg similarity index 100% rename from arithmetic_analysis/image_data/2D_problems_1.jpg rename to physics/image_data/2D_problems_1.jpg diff --git a/physics/image_data/__init__.py b/physics/image_data/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/arithmetic_analysis/in_static_equilibrium.py b/physics/in_static_equilibrium.py similarity index 95% rename from arithmetic_analysis/in_static_equilibrium.py rename to physics/in_static_equilibrium.py index 7aaecf174a5e..fb5a9b5fff66 100644 --- a/arithmetic_analysis/in_static_equilibrium.py +++ b/physics/in_static_equilibrium.py @@ -1,94 +1,95 @@ -""" -Checks if a system of forces is in static equilibrium. -""" -from __future__ import annotations - -from numpy import array, cos, cross, float64, radians, sin -from numpy.typing import NDArray - - -def polar_force( - magnitude: float, angle: float, radian_mode: bool = False -) -> list[float]: - """ - Resolves force along rectangular components. - (force, angle) => (force_x, force_y) - >>> import math - >>> force = polar_force(10, 45) - >>> math.isclose(force[0], 7.071067811865477) - True - >>> math.isclose(force[1], 7.0710678118654755) - True - >>> force = polar_force(10, 3.14, radian_mode=True) - >>> math.isclose(force[0], -9.999987317275396) - True - >>> math.isclose(force[1], 0.01592652916486828) - True - """ - if radian_mode: - return [magnitude * cos(angle), magnitude * sin(angle)] - return [magnitude * cos(radians(angle)), magnitude * sin(radians(angle))] - - -def in_static_equilibrium( - forces: NDArray[float64], location: NDArray[float64], eps: float = 10**-1 -) -> bool: - """ - Check if a system is in equilibrium. - It takes two numpy.array objects. - forces ==> [ - [force1_x, force1_y], - [force2_x, force2_y], - ....] - location ==> [ - [x1, y1], - [x2, y2], - ....] - >>> force = array([[1, 1], [-1, 2]]) - >>> location = array([[1, 0], [10, 0]]) - >>> in_static_equilibrium(force, location) - False - """ - # summation of moments is zero - moments: NDArray[float64] = cross(location, forces) - sum_moments: float = sum(moments) - return abs(sum_moments) < eps - - -if __name__ == "__main__": - # Test to check if it works - forces = array( - [ - polar_force(718.4, 180 - 30), - polar_force(879.54, 45), - polar_force(100, -90), - ] - ) - - location: NDArray[float64] = array([[0, 0], [0, 0], [0, 0]]) - - assert in_static_equilibrium(forces, location) - - # Problem 1 in image_data/2D_problems.jpg - forces = array( - [ - polar_force(30 * 9.81, 15), - polar_force(215, 180 - 45), - polar_force(264, 90 - 30), - ] - ) - - location = array([[0, 0], [0, 0], [0, 0]]) - - assert in_static_equilibrium(forces, location) - - # Problem in image_data/2D_problems_1.jpg - forces = array([[0, -2000], [0, -1200], [0, 15600], [0, -12400]]) - - location = array([[0, 0], [6, 0], [10, 0], [12, 0]]) - - assert in_static_equilibrium(forces, location) - - import doctest - - doctest.testmod() +""" +Checks if a system of forces is in static equilibrium. +""" + +from __future__ import annotations + +from numpy import array, cos, cross, float64, radians, sin +from numpy.typing import NDArray + + +def polar_force( + magnitude: float, angle: float, radian_mode: bool = False +) -> list[float]: + """ + Resolves force along rectangular components. + (force, angle) => (force_x, force_y) + >>> import math + >>> force = polar_force(10, 45) + >>> math.isclose(force[0], 7.071067811865477) + True + >>> math.isclose(force[1], 7.0710678118654755) + True + >>> force = polar_force(10, 3.14, radian_mode=True) + >>> math.isclose(force[0], -9.999987317275396) + True + >>> math.isclose(force[1], 0.01592652916486828) + True + """ + if radian_mode: + return [magnitude * cos(angle), magnitude * sin(angle)] + return [magnitude * cos(radians(angle)), magnitude * sin(radians(angle))] + + +def in_static_equilibrium( + forces: NDArray[float64], location: NDArray[float64], eps: float = 10**-1 +) -> bool: + """ + Check if a system is in equilibrium. + It takes two numpy.array objects. + forces ==> [ + [force1_x, force1_y], + [force2_x, force2_y], + ....] + location ==> [ + [x1, y1], + [x2, y2], + ....] + >>> force = array([[1, 1], [-1, 2]]) + >>> location = array([[1, 0], [10, 0]]) + >>> in_static_equilibrium(force, location) + False + """ + # summation of moments is zero + moments: NDArray[float64] = cross(location, forces) + sum_moments: float = sum(moments) + return bool(abs(sum_moments) < eps) + + +if __name__ == "__main__": + # Test to check if it works + forces = array( + [ + polar_force(718.4, 180 - 30), + polar_force(879.54, 45), + polar_force(100, -90), + ] + ) + + location: NDArray[float64] = array([[0, 0], [0, 0], [0, 0]]) + + assert in_static_equilibrium(forces, location) + + # Problem 1 in image_data/2D_problems.jpg + forces = array( + [ + polar_force(30 * 9.81, 15), + polar_force(215, 180 - 45), + polar_force(264, 90 - 30), + ] + ) + + location = array([[0, 0], [0, 0], [0, 0]]) + + assert in_static_equilibrium(forces, location) + + # Problem in image_data/2D_problems_1.jpg + forces = array([[0, -2000], [0, -1200], [0, 15600], [0, -12400]]) + + location = array([[0, 0], [6, 0], [10, 0], [12, 0]]) + + assert in_static_equilibrium(forces, location) + + import doctest + + doctest.testmod() diff --git a/physics/lens_formulae.py b/physics/lens_formulae.py new file mode 100644 index 000000000000..162f3a8f3b29 --- /dev/null +++ b/physics/lens_formulae.py @@ -0,0 +1,131 @@ +""" +This module has functions which calculate focal length of lens, distance of +image from the lens and distance of object from the lens. +The above is calculated using the lens formula. + +In optics, the relationship between the distance of the image (v), +the distance of the object (u), and +the focal length (f) of the lens is given by the formula known as the Lens formula. +The Lens formula is applicable for convex as well as concave lenses. The formula +is given as follows: + +------------------- +| 1/f = 1/v + 1/u | +------------------- + +Where + f = focal length of the lens in meters. + v = distance of the image from the lens in meters. + u = distance of the object from the lens in meters. + +To make our calculations easy few assumptions are made while deriving the formula +which are important to keep in mind before solving this equation. +The assumptions are as follows: + 1. The object O is a point object lying somewhere on the principle axis. + 2. The lens is thin. + 3. The aperture of the lens taken must be small. + 4. The angles of incidence and angle of refraction should be small. + +Sign convention is a set of rules to set signs for image distance, object distance, +focal length, etc +for mathematical analysis of image formation. According to it: + 1. Object is always placed to the left of lens. + 2. All distances are measured from the optical centre of the mirror. + 3. Distances measured in the direction of the incident ray are positive and + the distances measured in the direction opposite + to that of the incident rays are negative. + 4. Distances measured along y-axis above the principal axis are positive and + that measured along y-axis below the principal + axis are negative. + +Note: Sign convention can be reversed and will still give the correct results. + +Reference for Sign convention: +https://www.toppr.com/ask/content/concept/sign-convention-for-lenses-210246/ + +Reference for assumptions: +https://testbook.com/physics/derivation-of-lens-maker-formula +""" + + +def focal_length_of_lens( + object_distance_from_lens: float, image_distance_from_lens: float +) -> float: + """ + Doctests: + >>> from math import isclose + >>> isclose(focal_length_of_lens(10,4), 6.666666666666667) + True + >>> from math import isclose + >>> isclose(focal_length_of_lens(2.7,5.8), -5.0516129032258075) + True + >>> focal_length_of_lens(0, 20) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + + if object_distance_from_lens == 0 or image_distance_from_lens == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + focal_length = 1 / ( + (1 / image_distance_from_lens) - (1 / object_distance_from_lens) + ) + return focal_length + + +def object_distance( + focal_length_of_lens: float, image_distance_from_lens: float +) -> float: + """ + Doctests: + >>> from math import isclose + >>> isclose(object_distance(10,40), -13.333333333333332) + True + + >>> from math import isclose + >>> isclose(object_distance(6.2,1.5), 1.9787234042553192) + True + + >>> object_distance(0, 20) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + + if image_distance_from_lens == 0 or focal_length_of_lens == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + + object_distance = 1 / ((1 / image_distance_from_lens) - (1 / focal_length_of_lens)) + return object_distance + + +def image_distance( + focal_length_of_lens: float, object_distance_from_lens: float +) -> float: + """ + Doctests: + >>> from math import isclose + >>> isclose(image_distance(50,40), 22.22222222222222) + True + >>> from math import isclose + >>> isclose(image_distance(5.3,7.9), 3.1719696969696973) + True + + >>> object_distance(0, 20) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + if object_distance_from_lens == 0 or focal_length_of_lens == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + image_distance = 1 / ((1 / object_distance_from_lens) + (1 / focal_length_of_lens)) + return image_distance diff --git a/physics/lorentz_transformation_four_vector.py b/physics/lorentz_transformation_four_vector.py index 64be97245f29..3b0fd83d45df 100644 --- a/physics/lorentz_transformation_four_vector.py +++ b/physics/lorentz_transformation_four_vector.py @@ -2,7 +2,7 @@ Lorentz transformations describe the transition between two inertial reference frames F and F', each of which is moving in some direction with respect to the other. This code only calculates Lorentz transformations for movement in the x -direction with no spacial rotation (i.e., a Lorentz boost in the x direction). +direction with no spatial rotation (i.e., a Lorentz boost in the x direction). The Lorentz transformations are calculated here as linear transformations of four-vectors [ct, x, y, z] described by Minkowski space. Note that t (time) is multiplied by c (the speed of light) in the first entry of each four-vector. @@ -12,13 +12,13 @@ with respect to X, then the Lorentz transformation from X to X' is X' = BX, where - | γ -γβ 0 0| -B = |-γβ γ 0 0| + | y -γβ 0 0| +B = |-γβ y 0 0| | 0 0 1 0| | 0 0 0 1| is the matrix describing the Lorentz boost between X and X', -γ = 1 / √(1 - v²/c²) is the Lorentz factor, and β = v/c is the velocity as +y = 1 / √(1 - v²/c²) is the Lorentz factor, and β = v/c is the velocity as a fraction of c. Reference: https://en.wikipedia.org/wiki/Lorentz_transformation @@ -63,7 +63,7 @@ def beta(velocity: float) -> float: def gamma(velocity: float) -> float: """ - Calculate the Lorentz factor γ = 1 / √(1 - v²/c²) for a given velocity + Calculate the Lorentz factor y = 1 / √(1 - v²/c²) for a given velocity >>> gamma(4) 1.0000000000000002 >>> gamma(1e5) @@ -90,12 +90,12 @@ def transformation_matrix(velocity: float) -> np.ndarray: """ Calculate the Lorentz transformation matrix for movement in the x direction: - | γ -γβ 0 0| - |-γβ γ 0 0| + | y -γβ 0 0| + |-γβ y 0 0| | 0 0 1 0| | 0 0 0 1| - where γ is the Lorentz factor and β is the velocity as a fraction of c + where y is the Lorentz factor and β is the velocity as a fraction of c >>> transformation_matrix(29979245) array([[ 1.00503781, -0.10050378, 0. , 0. ], [-0.10050378, 1.00503781, 0. , 0. ], diff --git a/physics/malus_law.py b/physics/malus_law.py index ae77d45cf614..374b3423f8ff 100644 --- a/physics/malus_law.py +++ b/physics/malus_law.py @@ -31,7 +31,7 @@ Real polarizers are also not perfect blockers of the polarization orthogonal to their polarization axis; the ratio of the transmission of the unwanted component to the wanted component is called the extinction ratio, and varies from around -1:500 for Polaroid to about 1:106 for Glan–Taylor prism polarizers. +1:500 for Polaroid to about 1:106 for Glan-Taylor prism polarizers. Reference : "https://en.wikipedia.org/wiki/Polarizer#Malus's_law_and_other_properties" """ diff --git a/physics/mass_energy_equivalence.py b/physics/mass_energy_equivalence.py new file mode 100644 index 000000000000..4a4c7890f4e0 --- /dev/null +++ b/physics/mass_energy_equivalence.py @@ -0,0 +1,77 @@ +""" +Title: +Finding the energy equivalence of mass and mass equivalence of energy +by Einstein's equation. + +Description: +Einstein's mass-energy equivalence is a pivotal concept in theoretical physics. +It asserts that energy (E) and mass (m) are directly related by the speed of +light in vacuum (c) squared, as described in the equation E = mc². This means that +mass and energy are interchangeable; a mass increase corresponds to an energy increase, +and vice versa. This principle has profound implications in nuclear reactions, +explaining the release of immense energy from minuscule changes in atomic nuclei. + +Equations: +E = mc² and m = E/c², where m is mass, E is Energy, c is speed of light in vacuum. + +Reference: +https://en.wikipedia.org/wiki/Mass%E2%80%93energy_equivalence +""" + +from scipy.constants import c # speed of light in vacuum (299792458 m/s) + + +def energy_from_mass(mass: float) -> float: + """ + Calculates the Energy equivalence of the Mass using E = mc² + in SI units J from Mass in kg. + + mass (float): Mass of body. + + Usage example: + >>> energy_from_mass(124.56) + 1.11948945063458e+19 + >>> energy_from_mass(320) + 2.8760165719578165e+19 + >>> energy_from_mass(0) + 0.0 + >>> energy_from_mass(-967.9) + Traceback (most recent call last): + ... + ValueError: Mass can't be negative. + + """ + if mass < 0: + raise ValueError("Mass can't be negative.") + return mass * c**2 + + +def mass_from_energy(energy: float) -> float: + """ + Calculates the Mass equivalence of the Energy using m = E/c² + in SI units kg from Energy in J. + + energy (float): Mass of body. + + Usage example: + >>> mass_from_energy(124.56) + 1.3859169098203872e-15 + >>> mass_from_energy(320) + 3.560480179371579e-15 + >>> mass_from_energy(0) + 0.0 + >>> mass_from_energy(-967.9) + Traceback (most recent call last): + ... + ValueError: Energy can't be negative. + + """ + if energy < 0: + raise ValueError("Energy can't be negative.") + return energy / c**2 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/mirror_formulae.py b/physics/mirror_formulae.py new file mode 100644 index 000000000000..7efc52438140 --- /dev/null +++ b/physics/mirror_formulae.py @@ -0,0 +1,127 @@ +""" +This module contains the functions to calculate the focal length, object distance +and image distance of a mirror. + +The mirror formula is an equation that relates the object distance (u), +image distance (v), and focal length (f) of a spherical mirror. +It is commonly used in optics to determine the position and characteristics +of an image formed by a mirror. It is expressed using the formulae : + +------------------- +| 1/f = 1/v + 1/u | +------------------- + +Where, +f = Focal length of the spherical mirror (metre) +v = Image distance from the mirror (metre) +u = Object distance from the mirror (metre) + + +The signs of the distances are taken with respect to the sign convention. +The sign convention is as follows: + 1) Object is always placed to the left of mirror + 2) Distances measured in the direction of the incident ray are positive + and the distances measured in the direction opposite to that of the incident + rays are negative. + 3) All distances are measured from the pole of the mirror. + + +There are a few assumptions that are made while using the mirror formulae. +They are as follows: + 1) Thin Mirror: The mirror is assumed to be thin, meaning its thickness is + negligible compared to its radius of curvature. This assumption allows + us to treat the mirror as a two-dimensional surface. + 2) Spherical Mirror: The mirror is assumed to have a spherical shape. While this + assumption may not hold exactly for all mirrors, it is a reasonable approximation + for most practical purposes. + 3) Small Angles: The angles involved in the derivation are assumed to be small. + This assumption allows us to use the small-angle approximation, where the tangent + of a small angle is approximately equal to the angle itself. It simplifies the + calculations and makes the derivation more manageable. + 4) Paraxial Rays: The mirror formula is derived using paraxial rays, which are + rays that are close to the principal axis and make small angles with it. This + assumption ensures that the rays are close enough to the principal axis, making the + calculations more accurate. + 5) Reflection and Refraction Laws: The derivation assumes that the laws of + reflection and refraction hold. + These laws state that the angle of incidence is equal to the angle of reflection + for reflection, and the incident and refracted rays lie in the same plane and + obey Snell's law for refraction. + +(Description and Assumptions adapted from +https://www.collegesearch.in/articles/mirror-formula-derivation) + +(Sign Convention adapted from +https://www.toppr.com/ask/content/concept/sign-convention-for-mirrors-210189/) + + +""" + + +def focal_length(distance_of_object: float, distance_of_image: float) -> float: + """ + >>> from math import isclose + >>> isclose(focal_length(10, 20), 6.66666666666666) + True + >>> from math import isclose + >>> isclose(focal_length(9.5, 6.7), 3.929012346) + True + >>> focal_length(0, 20) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + + if distance_of_object == 0 or distance_of_image == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + focal_length = 1 / ((1 / distance_of_object) + (1 / distance_of_image)) + return focal_length + + +def object_distance(focal_length: float, distance_of_image: float) -> float: + """ + >>> from math import isclose + >>> isclose(object_distance(30, 20), -60.0) + True + >>> from math import isclose + >>> isclose(object_distance(10.5, 11.7), 102.375) + True + >>> object_distance(90, 0) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + + if distance_of_image == 0 or focal_length == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + object_distance = 1 / ((1 / focal_length) - (1 / distance_of_image)) + return object_distance + + +def image_distance(focal_length: float, distance_of_object: float) -> float: + """ + >>> from math import isclose + >>> isclose(image_distance(10, 40), 13.33333333) + True + >>> from math import isclose + >>> isclose(image_distance(1.5, 6.7), 1.932692308) + True + >>> image_distance(0, 0) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter non zero values with respect + to the sign convention. + """ + + if distance_of_object == 0 or focal_length == 0: + raise ValueError( + "Invalid inputs. Enter non zero values with respect to the sign convention." + ) + image_distance = 1 / ((1 / focal_length) - (1 / distance_of_object)) + return image_distance diff --git a/physics/n_body_simulation.py b/physics/n_body_simulation.py index 2b701283f166..9bfb6b3c6864 100644 --- a/physics/n_body_simulation.py +++ b/physics/n_body_simulation.py @@ -11,7 +11,6 @@ (See also http://www.shodor.org/refdesk/Resources/Algorithms/EulersMethod/ ) """ - from __future__ import annotations import random @@ -165,9 +164,7 @@ def update_system(self, delta_time: float) -> None: # Calculation of the distance using Pythagoras's theorem # Extra factor due to the softening technique - distance = (dif_x**2 + dif_y**2 + self.softening_factor) ** ( - 1 / 2 - ) + distance = (dif_x**2 + dif_y**2 + self.softening_factor) ** (1 / 2) # Newton's law of universal gravitation. force_x += ( @@ -226,7 +223,7 @@ def plot( No doctest provided since this function does not have a return value. """ fig = plt.figure() - fig.canvas.set_window_title(title) + fig.canvas.manager.set_window_title(title) ax = plt.axes( xlim=(x_start, x_end), ylim=(y_start, y_end) ) # Set section to be plotted @@ -242,7 +239,7 @@ def plot( ax.add_patch(patch) # Function called at each step of the animation - def update(frame: int) -> list[plt.Circle]: + def update(frame: int) -> list[plt.Circle]: # noqa: ARG001 update_step(body_system, DELTA_TIME, patches) return patches diff --git a/physics/newtons_law_of_gravitation.py b/physics/newtons_law_of_gravitation.py index 4bbeddd61d5b..ae9da2f1e949 100644 --- a/physics/newtons_law_of_gravitation.py +++ b/physics/newtons_law_of_gravitation.py @@ -3,7 +3,7 @@ provided that the other three parameters are given. Description : Newton's Law of Universal Gravitation explains the presence of force of -attraction between bodies having a definite mass situated at a distance. It is usually +attraction between bodies having a definite mass situated at a distance. It is usually stated as that, every particle attracts every other particle in the universe with a force that is directly proportional to the product of their masses and inversely proportional to the square of the distance between their centers. The publication of the diff --git a/physics/newtons_second_law_of_motion.py b/physics/newtons_second_law_of_motion.py index cb53f8f6571f..4149e2494f31 100644 --- a/physics/newtons_second_law_of_motion.py +++ b/physics/newtons_second_law_of_motion.py @@ -1,18 +1,22 @@ -""" -Description : -Newton's second law of motion pertains to the behavior of objects for which -all existing forces are not balanced. -The second law states that the acceleration of an object is dependent upon two variables -- the net force acting upon the object and the mass of the object. -The acceleration of an object depends directly -upon the net force acting upon the object, -and inversely upon the mass of the object. -As the force acting upon an object is increased, -the acceleration of the object is increased. -As the mass of an object is increased, the acceleration of the object is decreased. +r""" +Description: + Newton's second law of motion pertains to the behavior of objects for which + all existing forces are not balanced. + The second law states that the acceleration of an object is dependent upon + two variables - the net force acting upon the object and the mass of the object. + The acceleration of an object depends directly + upon the net force acting upon the object, + and inversely upon the mass of the object. + As the force acting upon an object is increased, + the acceleration of the object is increased. + As the mass of an object is increased, the acceleration of the object is decreased. + Source: https://www.physicsclassroom.com/class/newtlaws/Lesson-3/Newton-s-Second-Law -Formulation: Fnet = m • a -Diagrammatic Explanation: + +Formulation: F_net = m • a + +Diagrammatic Explanation:: + Forces are unbalanced | | @@ -26,41 +30,48 @@ / \ / \ / \ - __________________ ____ ________________ - |The acceleration | |The acceleration | - |depends directly | |depends inversely | - |on the net Force | |upon the object's | - |_________________| |mass_______________| -Units: -1 Newton = 1 kg X meters / (seconds^2) + __________________ ____________________ + | The acceleration | | The acceleration | + | depends directly | | depends inversely | + | on the net force | | upon the object's | + | | | mass | + |__________________| |____________________| + +Units: 1 Newton = 1 kg • meters/seconds^2 + How to use? -Inputs: - ___________________________________________________ - |Name | Units | Type | - |-------------|-------------------------|-----------| - |mass | (in kgs) | float | - |-------------|-------------------------|-----------| - |acceleration | (in meters/(seconds^2)) | float | - |_____________|_________________________|___________| - -Output: - ___________________________________________________ - |Name | Units | Type | - |-------------|-------------------------|-----------| - |force | (in Newtons) | float | - |_____________|_________________________|___________| + +Inputs:: + + ______________ _____________________ ___________ + | Name | Units | Type | + |--------------|---------------------|-----------| + | mass | in kgs | float | + |--------------|---------------------|-----------| + | acceleration | in meters/seconds^2 | float | + |______________|_____________________|___________| + +Output:: + + ______________ _______________________ ___________ + | Name | Units | Type | + |--------------|-----------------------|-----------| + | force | in Newtons | float | + |______________|_______________________|___________| """ def newtons_second_law_of_motion(mass: float, acceleration: float) -> float: """ + Calculates force from `mass` and `acceleration` + >>> newtons_second_law_of_motion(10, 10) 100 >>> newtons_second_law_of_motion(2.0, 1) 2.0 """ - force = float() + force = 0.0 try: force = mass * acceleration except Exception: diff --git a/physics/period_of_pendulum.py b/physics/period_of_pendulum.py new file mode 100644 index 000000000000..2e3c7bc3ef1e --- /dev/null +++ b/physics/period_of_pendulum.py @@ -0,0 +1,53 @@ +""" +Title : Computing the time period of a simple pendulum + +The simple pendulum is a mechanical system that sways or moves in an +oscillatory motion. The simple pendulum comprises of a small bob of +mass m suspended by a thin string of length L and secured to a platform +at its upper end. Its motion occurs in a vertical plane and is mainly +driven by gravitational force. The period of the pendulum depends on the +length of the string and the amplitude (the maximum angle) of oscillation. +However, the effect of the amplitude can be ignored if the amplitude is +small. It should be noted that the period does not depend on the mass of +the bob. + +For small amplitudes, the period of a simple pendulum is given by the +following approximation: +T ≈ 2Ï€ * √(L / g) + +where: +L = length of string from which the bob is hanging (in m) +g = acceleration due to gravity (approx 9.8 m/s²) + +Reference : https://byjus.com/jee/simple-pendulum/ +""" + +from math import pi + +from scipy.constants import g + + +def period_of_pendulum(length: float) -> float: + """ + >>> period_of_pendulum(1.23) + 2.2252155506257845 + >>> period_of_pendulum(2.37) + 3.0888278441908574 + >>> period_of_pendulum(5.63) + 4.76073193364765 + >>> period_of_pendulum(-12) + Traceback (most recent call last): + ... + ValueError: The length should be non-negative + >>> period_of_pendulum(0) + 0.0 + """ + if length < 0: + raise ValueError("The length should be non-negative") + return 2 * pi * (length / g) ** 0.5 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/photoelectric_effect.py b/physics/photoelectric_effect.py new file mode 100644 index 000000000000..3a0138ffe045 --- /dev/null +++ b/physics/photoelectric_effect.py @@ -0,0 +1,67 @@ +""" +The photoelectric effect is the emission of electrons when electromagnetic radiation , +such as light, hits a material. Electrons emitted in this manner are called +photoelectrons. + +In 1905, Einstein proposed a theory of the photoelectric effect using a concept that +light consists of tiny packets of energy known as photons or light quanta. Each packet +carries energy hv that is proportional to the frequency v of the corresponding +electromagnetic wave. The proportionality constant h has become known as the +Planck constant. In the range of kinetic energies of the electrons that are removed from +their varying atomic bindings by the absorption of a photon of energy hv, the highest +kinetic energy K_max is : + +K_max = hv-W + +Here, W is the minimum energy required to remove an electron from the surface of the +material. It is called the work function of the surface + +Reference: https://en.wikipedia.org/wiki/Photoelectric_effect + +""" + +PLANCK_CONSTANT_JS = 6.6261 * pow(10, -34) # in SI (Js) +PLANCK_CONSTANT_EVS = 4.1357 * pow(10, -15) # in eVs + + +def maximum_kinetic_energy( + frequency: float, work_function: float, in_ev: bool = False +) -> float: + """ + Calculates the maximum kinetic energy of emitted electron from the surface. + if the maximum kinetic energy is zero then no electron will be emitted + or given electromagnetic wave frequency is small. + + frequency (float): Frequency of electromagnetic wave. + work_function (float): Work function of the surface. + in_ev (optional)(bool): Pass True if values are in eV. + + Usage example: + >>> maximum_kinetic_energy(1000000,2) + 0 + >>> maximum_kinetic_energy(1000000,2,True) + 0 + >>> maximum_kinetic_energy(10000000000000000,2,True) + 39.357000000000006 + >>> maximum_kinetic_energy(-9,20) + Traceback (most recent call last): + ... + ValueError: Frequency can't be negative. + + >>> maximum_kinetic_energy(1000,"a") + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for -: 'float' and 'str' + + """ + if frequency < 0: + raise ValueError("Frequency can't be negative.") + if in_ev: + return max(PLANCK_CONSTANT_EVS * frequency - work_function, 0) + return max(PLANCK_CONSTANT_JS * frequency - work_function, 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/rainfall_intensity.py b/physics/rainfall_intensity.py new file mode 100644 index 000000000000..cee8d50ddc2f --- /dev/null +++ b/physics/rainfall_intensity.py @@ -0,0 +1,143 @@ +""" +Rainfall Intensity +================== +This module contains functions to calculate the intensity of +a rainfall event for a given duration and return period. + +This function uses the Sherman intensity-duration-frequency curve. + +References +---------- +- Aparicio, F. (1997): Fundamentos de Hidrología de Superficie. + Balderas, México, Limusa. 303 p. +- https://en.wikipedia.org/wiki/Intensity-duration-frequency_curve +""" + + +def rainfall_intensity( + coefficient_k: float, + coefficient_a: float, + coefficient_b: float, + coefficient_c: float, + return_period: float, + duration: float, +) -> float: + """ + Calculate the intensity of a rainfall event for a given duration and return period. + It's based on the Sherman intensity-duration-frequency curve: + + I = k * T^a / (D + b)^c + + where: + I = Intensity of the rainfall event [mm/h] + k, a, b, c = Coefficients obtained through statistical distribution adjust + T = Return period in years + D = Rainfall event duration in minutes + + Parameters + ---------- + coefficient_k : float + Coefficient obtained through statistical distribution adjust. + coefficient_a : float + Coefficient obtained through statistical distribution adjust. + coefficient_b : float + Coefficient obtained through statistical distribution adjust. + coefficient_c : float + Coefficient obtained through statistical distribution adjust. + return_period : float + Return period in years. + duration : float + Rainfall event duration in minutes. + + Returns + ------- + intensity : float + Intensity of the rainfall event in mm/h. + + Raises + ------ + ValueError + If any of the parameters are not positive. + + Examples + -------- + + >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 60) + 49.83339231138578 + + >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 30) + 77.36319588106228 + + >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 5, 60) + 43.382487747633625 + + >>> rainfall_intensity(0, 0.2, 11.6, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, -0.2, 11.6, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, -11.6, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, 11.6, -0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0, 11.6, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, 0, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, 11.6, 0, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(0, 0.2, 11.6, 0.81, 10, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 0, 60) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + >>> rainfall_intensity(1000, 0.2, 11.6, 0.81, 10, 0) + Traceback (most recent call last): + ... + ValueError: All parameters must be positive. + + """ + if ( + coefficient_k <= 0 + or coefficient_a <= 0 + or coefficient_b <= 0 + or coefficient_c <= 0 + or return_period <= 0 + or duration <= 0 + ): + raise ValueError("All parameters must be positive.") + intensity = (coefficient_k * (return_period**coefficient_a)) / ( + (duration + coefficient_b) ** coefficient_c + ) + return intensity + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/reynolds_number.py b/physics/reynolds_number.py new file mode 100644 index 000000000000..c24a9e002855 --- /dev/null +++ b/physics/reynolds_number.py @@ -0,0 +1,63 @@ +""" +Title : computing the Reynolds number to find + out the type of flow (laminar or turbulent) + +Reynolds number is a dimensionless quantity that is used to determine +the type of flow pattern as laminar or turbulent while flowing through a +pipe. Reynolds number is defined by the ratio of inertial forces to that of +viscous forces. + +R = Inertial Forces / Viscous Forces +R = (p * V * D)/μ + +where : +p = Density of fluid (in Kg/m^3) +D = Diameter of pipe through which fluid flows (in m) +V = Velocity of flow of the fluid (in m/s) +μ = Viscosity of the fluid (in Ns/m^2) + +If the Reynolds number calculated is high (greater than 2000), then the +flow through the pipe is said to be turbulent. If Reynolds number is low +(less than 2000), the flow is said to be laminar. Numerically, these are +acceptable values, although in general the laminar and turbulent flows +are classified according to a range. Laminar flow falls below Reynolds +number of 1100 and turbulent falls in a range greater than 2200. +Laminar flow is the type of flow in which the fluid travels smoothly in +regular paths. Conversely, turbulent flow isn't smooth and follows an +irregular path with lots of mixing. + +Reference : https://byjus.com/physics/reynolds-number/ +""" + + +def reynolds_number( + density: float, velocity: float, diameter: float, viscosity: float +) -> float: + """ + >>> reynolds_number(900, 2.5, 0.05, 0.4) + 281.25 + >>> reynolds_number(450, 3.86, 0.078, 0.23) + 589.0695652173912 + >>> reynolds_number(234, -4.5, 0.3, 0.44) + 717.9545454545454 + >>> reynolds_number(-90, 2, 0.045, 1) + Traceback (most recent call last): + ... + ValueError: please ensure that density, diameter and viscosity are positive + >>> reynolds_number(0, 2, -0.4, -2) + Traceback (most recent call last): + ... + ValueError: please ensure that density, diameter and viscosity are positive + """ + + if density <= 0 or diameter <= 0 or viscosity <= 0: + raise ValueError( + "please ensure that density, diameter and viscosity are positive" + ) + return (density * abs(velocity) * diameter) / viscosity + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/rms_speed_of_molecule.py b/physics/rms_speed_of_molecule.py index 478cee01c7fd..fb23eb8a21cf 100644 --- a/physics/rms_speed_of_molecule.py +++ b/physics/rms_speed_of_molecule.py @@ -20,7 +20,6 @@ alternative method. """ - UNIVERSAL_GAS_CONSTANT = 8.3144598 diff --git a/physics/speed_of_sound.py b/physics/speed_of_sound.py new file mode 100644 index 000000000000..3fa952cdb411 --- /dev/null +++ b/physics/speed_of_sound.py @@ -0,0 +1,48 @@ +""" +Title : Calculating the speed of sound + +Description : + The speed of sound (c) is the speed that a sound wave travels per unit time (m/s). + During propagation, the sound wave propagates through an elastic medium. + + Sound propagates as longitudinal waves in liquids and gases and as transverse waves + in solids. This file calculates the speed of sound in a fluid based on its bulk + module and density. + + Equation for the speed of sound in a fluid: + c_fluid = sqrt(K_s / p) + + c_fluid: speed of sound in fluid + K_s: isentropic bulk modulus + p: density of fluid + +Source : https://en.wikipedia.org/wiki/Speed_of_sound +""" + + +def speed_of_sound_in_a_fluid(density: float, bulk_modulus: float) -> float: + """ + Calculates the speed of sound in a fluid from its density and bulk modulus + + Examples: + Example 1 --> Water 20°C: bulk_modulus= 2.15MPa, density=998kg/m³ + Example 2 --> Mercury 20°C: bulk_modulus= 28.5MPa, density=13600kg/m³ + + >>> speed_of_sound_in_a_fluid(bulk_modulus=2.15e9, density=998) + 1467.7563207952705 + >>> speed_of_sound_in_a_fluid(bulk_modulus=28.5e9, density=13600) + 1447.614670861731 + """ + + if density <= 0: + raise ValueError("Impossible fluid density") + if bulk_modulus <= 0: + raise ValueError("Impossible bulk modulus") + + return (bulk_modulus / density) ** 0.5 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/speeds_of_gas_molecules.py b/physics/speeds_of_gas_molecules.py new file mode 100644 index 000000000000..42f90a9fd6f3 --- /dev/null +++ b/physics/speeds_of_gas_molecules.py @@ -0,0 +1,113 @@ +""" +The root-mean-square, average and most probable speeds of gas molecules are +derived from the Maxwell-Boltzmann distribution. The Maxwell-Boltzmann +distribution is a probability distribution that describes the distribution of +speeds of particles in an ideal gas. + +The distribution is given by the following equation:: + + ------------------------------------------------- + | f(v) = (M/2Ï€RT)^(3/2) * 4Ï€v^2 * e^(-Mv^2/2RT) | + ------------------------------------------------- + +where: + * ``f(v)`` is the fraction of molecules with a speed ``v`` + * ``M`` is the molar mass of the gas in kg/mol + * ``R`` is the gas constant + * ``T`` is the absolute temperature + +More information about the Maxwell-Boltzmann distribution can be found here: +https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution + +The average speed can be calculated by integrating the Maxwell-Boltzmann distribution +from 0 to infinity and dividing by the total number of molecules. The result is:: + + ---------------------- + | v_avg = √(8RT/Ï€M) | + ---------------------- + +The most probable speed is the speed at which the Maxwell-Boltzmann distribution +is at its maximum. This can be found by differentiating the Maxwell-Boltzmann +distribution with respect to ``v`` and setting the result equal to zero. The result is:: + + ---------------------- + | v_mp = √(2RT/M) | + ---------------------- + +The root-mean-square speed is another measure of the average speed +of the molecules in a gas. It is calculated by taking the square root +of the average of the squares of the speeds of the molecules. The result is:: + + ---------------------- + | v_rms = √(3RT/M) | + ---------------------- + +Here we have defined functions to calculate the average and +most probable speeds of molecules in a gas given the +temperature and molar mass of the gas. +""" + +# import the constants R and pi from the scipy.constants library +from scipy.constants import R, pi + + +def avg_speed_of_molecule(temperature: float, molar_mass: float) -> float: + """ + Takes the temperature (in K) and molar mass (in kg/mol) of a gas + and returns the average speed of a molecule in the gas (in m/s). + + Examples: + + >>> avg_speed_of_molecule(273, 0.028) # nitrogen at 273 K + 454.3488755020387 + >>> avg_speed_of_molecule(300, 0.032) # oxygen at 300 K + 445.52572733919885 + >>> avg_speed_of_molecule(-273, 0.028) # invalid temperature + Traceback (most recent call last): + ... + Exception: Absolute temperature cannot be less than 0 K + >>> avg_speed_of_molecule(273, 0) # invalid molar mass + Traceback (most recent call last): + ... + Exception: Molar mass should be greater than 0 kg/mol + """ + + if temperature < 0: + raise Exception("Absolute temperature cannot be less than 0 K") + if molar_mass <= 0: + raise Exception("Molar mass should be greater than 0 kg/mol") + return (8 * R * temperature / (pi * molar_mass)) ** 0.5 + + +def mps_speed_of_molecule(temperature: float, molar_mass: float) -> float: + """ + Takes the temperature (in K) and molar mass (in kg/mol) of a gas + and returns the most probable speed of a molecule in the gas (in m/s). + + Examples: + + >>> mps_speed_of_molecule(273, 0.028) # nitrogen at 273 K + 402.65620701908966 + >>> mps_speed_of_molecule(300, 0.032) # oxygen at 300 K + 394.836895549922 + >>> mps_speed_of_molecule(-273, 0.028) # invalid temperature + Traceback (most recent call last): + ... + Exception: Absolute temperature cannot be less than 0 K + >>> mps_speed_of_molecule(273, 0) # invalid molar mass + Traceback (most recent call last): + ... + Exception: Molar mass should be greater than 0 kg/mol + """ + + if temperature < 0: + raise Exception("Absolute temperature cannot be less than 0 K") + if molar_mass <= 0: + raise Exception("Molar mass should be greater than 0 kg/mol") + return (2 * R * temperature / molar_mass) ** 0.5 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/terminal_velocity.py b/physics/terminal_velocity.py new file mode 100644 index 000000000000..16714bd02671 --- /dev/null +++ b/physics/terminal_velocity.py @@ -0,0 +1,60 @@ +""" +Title : Computing the terminal velocity of an object falling + through a fluid. + +Terminal velocity is defined as the highest velocity attained by an +object falling through a fluid. It is observed when the sum of drag force +and buoyancy is equal to the downward gravity force acting on the +object. The acceleration of the object is zero as the net force acting on +the object is zero. + +Vt = ((2 * m * g)/(p * A * Cd))^0.5 + +where : +Vt = Terminal velocity (in m/s) +m = Mass of the falling object (in Kg) +g = Acceleration due to gravity (value taken : imported from scipy) +p = Density of the fluid through which the object is falling (in Kg/m^3) +A = Projected area of the object (in m^2) +Cd = Drag coefficient (dimensionless) + +Reference : https://byjus.com/physics/derivation-of-terminal-velocity/ +""" + +from scipy.constants import g + + +def terminal_velocity( + mass: float, density: float, area: float, drag_coefficient: float +) -> float: + """ + >>> terminal_velocity(1, 25, 0.6, 0.77) + 1.3031197996044768 + >>> terminal_velocity(2, 100, 0.45, 0.23) + 1.9467947148674276 + >>> terminal_velocity(5, 50, 0.2, 0.5) + 4.428690551393267 + >>> terminal_velocity(-5, 50, -0.2, -2) + Traceback (most recent call last): + ... + ValueError: mass, density, area and the drag coefficient all need to be positive + >>> terminal_velocity(3, -20, -1, 2) + Traceback (most recent call last): + ... + ValueError: mass, density, area and the drag coefficient all need to be positive + >>> terminal_velocity(-2, -1, -0.44, -1) + Traceback (most recent call last): + ... + ValueError: mass, density, area and the drag coefficient all need to be positive + """ + if mass <= 0 or density <= 0 or area <= 0 or drag_coefficient <= 0: + raise ValueError( + "mass, density, area and the drag coefficient all need to be positive" + ) + return ((2 * mass * g) / (density * area * drag_coefficient)) ** 0.5 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/project_euler/README.md b/project_euler/README.md index e3dc035eee5e..16865edf2a67 100644 --- a/project_euler/README.md +++ b/project_euler/README.md @@ -10,7 +10,7 @@ The solutions will be checked by our [automated testing on GitHub Actions](https ## Solution Guidelines -Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before reading the solution guidelines, make sure you read the whole [Contributing Guidelines](https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md) as it won't be repeated in here. If you have any doubt on the guidelines, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community in [Gitter](https://gitter.im/TheAlgorithms). You can use the [template](https://github.com/TheAlgorithms/Python/blob/master/project_euler/README.md#solution-template) we have provided below as your starting point but be sure to read the [Coding Style](https://github.com/TheAlgorithms/Python/blob/master/project_euler/README.md#coding-style) part first. +Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before reading the solution guidelines, make sure you read the whole [Contributing Guidelines](https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md) as it won't be repeated in here. If you have any doubt on the guidelines, please feel free to [state it clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community in [Gitter](https://gitter.im/TheAlgorithms/community). You can use the [template](https://github.com/TheAlgorithms/Python/blob/master/project_euler/README.md#solution-template) we have provided below as your starting point but be sure to read the [Coding Style](https://github.com/TheAlgorithms/Python/blob/master/project_euler/README.md#coding-style) part first. ### Coding Style diff --git a/project_euler/problem_002/sol4.py b/project_euler/problem_002/sol4.py index 70b7d6a80a1d..a13d34fd760e 100644 --- a/project_euler/problem_002/sol4.py +++ b/project_euler/problem_002/sol4.py @@ -14,6 +14,7 @@ References: - https://en.wikipedia.org/wiki/Fibonacci_number """ + import math from decimal import Decimal, getcontext @@ -60,7 +61,7 @@ def solution(n: int = 4000000) -> int: if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") getcontext().prec = 100 - phi = (Decimal(5) ** Decimal(0.5) + 1) / Decimal(2) + phi = (Decimal(5) ** Decimal("0.5") + 1) / Decimal(2) index = (math.floor(math.log(n * (phi + 2), phi) - 1) // 3) * 3 + 2 num = Decimal(round(phi ** Decimal(index + 1))) / (phi + 2) diff --git a/project_euler/problem_003/sol1.py b/project_euler/problem_003/sol1.py index a7d01bb041ba..d1c0e61cf1a6 100644 --- a/project_euler/problem_003/sol1.py +++ b/project_euler/problem_003/sol1.py @@ -10,6 +10,7 @@ References: - https://en.wikipedia.org/wiki/Prime_number#Unique_factorization """ + import math diff --git a/project_euler/problem_004/sol1.py b/project_euler/problem_004/sol1.py index f237afdd942d..f80a3253e741 100644 --- a/project_euler/problem_004/sol1.py +++ b/project_euler/problem_004/sol1.py @@ -4,7 +4,7 @@ Largest palindrome product A palindromic number reads the same both ways. The largest palindrome made -from the product of two 2-digit numbers is 9009 = 91 × 99. +from the product of two 2-digit numbers is 9009 = 91 x 99. Find the largest palindrome made from the product of two 3-digit numbers. diff --git a/project_euler/problem_004/sol2.py b/project_euler/problem_004/sol2.py index abc880966d58..1fa75e7d0c83 100644 --- a/project_euler/problem_004/sol2.py +++ b/project_euler/problem_004/sol2.py @@ -4,7 +4,7 @@ Largest palindrome product A palindromic number reads the same both ways. The largest palindrome made -from the product of two 2-digit numbers is 9009 = 91 × 99. +from the product of two 2-digit numbers is 9009 = 91 x 99. Find the largest palindrome made from the product of two 3-digit numbers. diff --git a/project_euler/problem_005/sol1.py b/project_euler/problem_005/sol1.py index f272c102d2bb..01cbd0e15ff7 100644 --- a/project_euler/problem_005/sol1.py +++ b/project_euler/problem_005/sol1.py @@ -63,6 +63,7 @@ def solution(n: int = 20) -> int: if i == 0: i = 1 return i + return None if __name__ == "__main__": diff --git a/project_euler/problem_005/sol2.py b/project_euler/problem_005/sol2.py index 1b3e5e130f03..4558e21fd0f0 100644 --- a/project_euler/problem_005/sol2.py +++ b/project_euler/problem_005/sol2.py @@ -1,3 +1,5 @@ +from maths.greatest_common_divisor import greatest_common_divisor + """ Project Euler Problem 5: https://projecteuler.net/problem=5 @@ -16,23 +18,6 @@ """ -def greatest_common_divisor(x: int, y: int) -> int: - """ - Euclidean Greatest Common Divisor algorithm - - >>> greatest_common_divisor(0, 0) - 0 - >>> greatest_common_divisor(23, 42) - 1 - >>> greatest_common_divisor(15, 33) - 3 - >>> greatest_common_divisor(12345, 67890) - 15 - """ - - return x if y == 0 else greatest_common_divisor(y, x % y) - - def lcm(x: int, y: int) -> int: """ Least Common Multiple. diff --git a/project_euler/problem_006/sol3.py b/project_euler/problem_006/sol3.py index 529f233c9f8e..16445258c2b7 100644 --- a/project_euler/problem_006/sol3.py +++ b/project_euler/problem_006/sol3.py @@ -15,6 +15,7 @@ Find the difference between the sum of the squares of the first one hundred natural numbers and the square of the sum. """ + import math diff --git a/project_euler/problem_007/sol2.py b/project_euler/problem_007/sol2.py index 75d351889ea8..fd99453c1100 100644 --- a/project_euler/problem_007/sol2.py +++ b/project_euler/problem_007/sol2.py @@ -11,6 +11,7 @@ References: - https://en.wikipedia.org/wiki/Prime_number """ + import math diff --git a/project_euler/problem_007/sol3.py b/project_euler/problem_007/sol3.py index 774260db99a0..39db51a93427 100644 --- a/project_euler/problem_007/sol3.py +++ b/project_euler/problem_007/sol3.py @@ -11,6 +11,7 @@ References: - https://en.wikipedia.org/wiki/Prime_number """ + import itertools import math diff --git a/project_euler/problem_008/sol1.py b/project_euler/problem_008/sol1.py index 69dd1b4736c1..a38b2045f996 100644 --- a/project_euler/problem_008/sol1.py +++ b/project_euler/problem_008/sol1.py @@ -4,7 +4,7 @@ Largest product in a series The four adjacent digits in the 1000-digit number that have the greatest -product are 9 × 9 × 8 × 9 = 5832. +product are 9 x 9 x 8 x 9 = 5832. 73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 @@ -75,8 +75,7 @@ def solution(n: str = N) -> int: product = 1 for j in range(13): product *= int(n[i + j]) - if product > largest_product: - largest_product = product + largest_product = max(largest_product, product) return largest_product diff --git a/project_euler/problem_008/sol2.py b/project_euler/problem_008/sol2.py index 889c3a3143c2..e48231e4023b 100644 --- a/project_euler/problem_008/sol2.py +++ b/project_euler/problem_008/sol2.py @@ -4,7 +4,7 @@ Largest product in a series The four adjacent digits in the 1000-digit number that have the greatest -product are 9 × 9 × 8 × 9 = 5832. +product are 9 x 9 x 8 x 9 = 5832. 73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 @@ -30,6 +30,7 @@ Find the thirteen adjacent digits in the 1000-digit number that have the greatest product. What is the value of this product? """ + from functools import reduce N = ( diff --git a/project_euler/problem_008/sol3.py b/project_euler/problem_008/sol3.py index c6081aa05e2c..0d319b9684dd 100644 --- a/project_euler/problem_008/sol3.py +++ b/project_euler/problem_008/sol3.py @@ -4,7 +4,7 @@ Largest product in a series The four adjacent digits in the 1000-digit number that have the greatest -product are 9 × 9 × 8 × 9 = 5832. +product are 9 x 9 x 8 x 9 = 5832. 73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 @@ -30,6 +30,7 @@ Find the thirteen adjacent digits in the 1000-digit number that have the greatest product. What is the value of this product? """ + import sys N = ( diff --git a/project_euler/problem_009/sol1.py b/project_euler/problem_009/sol1.py index 1d908402b6b1..e65c9b857990 100644 --- a/project_euler/problem_009/sol1.py +++ b/project_euler/problem_009/sol1.py @@ -32,9 +32,8 @@ def solution() -> int: for a in range(300): for b in range(a + 1, 400): for c in range(b + 1, 500): - if (a + b + c) == 1000: - if (a**2) + (b**2) == (c**2): - return a * b * c + if (a + b + c) == 1000 and (a**2) + (b**2) == (c**2): + return a * b * c return -1 diff --git a/project_euler/problem_009/sol2.py b/project_euler/problem_009/sol2.py index 722ad522ee45..443a529571cc 100644 --- a/project_euler/problem_009/sol2.py +++ b/project_euler/problem_009/sol2.py @@ -39,8 +39,7 @@ def solution(n: int = 1000) -> int: c = n - a - b if c * c == (a * a + b * b): candidate = a * b * c - if candidate >= product: - product = candidate + product = max(product, candidate) return product diff --git a/project_euler/problem_009/sol3.py b/project_euler/problem_009/sol3.py index d299f821d4f6..37340d3063bb 100644 --- a/project_euler/problem_009/sol3.py +++ b/project_euler/problem_009/sol3.py @@ -28,12 +28,16 @@ def solution() -> int: 31875000 """ - return [ - a * b * (1000 - a - b) - for a in range(1, 999) - for b in range(a, 999) - if (a * a + b * b == (1000 - a - b) ** 2) - ][0] + return next( + iter( + [ + a * b * (1000 - a - b) + for a in range(1, 999) + for b in range(a, 999) + if (a * a + b * b == (1000 - a - b) ** 2) + ] + ) + ) if __name__ == "__main__": diff --git a/project_euler/problem_010/sol2.py b/project_euler/problem_010/sol2.py index 245cca1d1720..1a1fc0f33cb3 100644 --- a/project_euler/problem_010/sol2.py +++ b/project_euler/problem_010/sol2.py @@ -10,6 +10,7 @@ References: - https://en.wikipedia.org/wiki/Prime_number """ + import math from collections.abc import Iterator from itertools import takewhile diff --git a/project_euler/problem_011/sol1.py b/project_euler/problem_011/sol1.py index ad45f0983a7c..3d3e864f927b 100644 --- a/project_euler/problem_011/sol1.py +++ b/project_euler/problem_011/sol1.py @@ -63,8 +63,7 @@ def largest_product(grid): max_product = max( vert_product, horz_product, lr_diag_product, rl_diag_product ) - if max_product > largest: - largest = max_product + largest = max(largest, max_product) return largest diff --git a/project_euler/problem_011/sol2.py b/project_euler/problem_011/sol2.py index 9ea0db991aaf..7637deafc3cb 100644 --- a/project_euler/problem_011/sol2.py +++ b/project_euler/problem_011/sol2.py @@ -35,39 +35,45 @@ def solution(): 70600674 """ with open(os.path.dirname(__file__) + "/grid.txt") as f: - l = [] # noqa: E741 + grid = [] for _ in range(20): - l.append([int(x) for x in f.readline().split()]) + grid.append([int(x) for x in f.readline().split()]) maximum = 0 # right for i in range(20): for j in range(17): - temp = l[i][j] * l[i][j + 1] * l[i][j + 2] * l[i][j + 3] - if temp > maximum: - maximum = temp + temp = grid[i][j] * grid[i][j + 1] * grid[i][j + 2] * grid[i][j + 3] + maximum = max(maximum, temp) # down for i in range(17): for j in range(20): - temp = l[i][j] * l[i + 1][j] * l[i + 2][j] * l[i + 3][j] - if temp > maximum: - maximum = temp + temp = grid[i][j] * grid[i + 1][j] * grid[i + 2][j] * grid[i + 3][j] + maximum = max(maximum, temp) # diagonal 1 for i in range(17): for j in range(17): - temp = l[i][j] * l[i + 1][j + 1] * l[i + 2][j + 2] * l[i + 3][j + 3] - if temp > maximum: - maximum = temp + temp = ( + grid[i][j] + * grid[i + 1][j + 1] + * grid[i + 2][j + 2] + * grid[i + 3][j + 3] + ) + maximum = max(maximum, temp) # diagonal 2 for i in range(17): for j in range(3, 20): - temp = l[i][j] * l[i + 1][j - 1] * l[i + 2][j - 2] * l[i + 3][j - 3] - if temp > maximum: - maximum = temp + temp = ( + grid[i][j] + * grid[i + 1][j - 1] + * grid[i + 2][j - 2] + * grid[i + 3][j - 3] + ) + maximum = max(maximum, temp) return maximum diff --git a/project_euler/problem_013/sol1.py b/project_euler/problem_013/sol1.py index 7a414a9379e0..87d0e0a60e9b 100644 --- a/project_euler/problem_013/sol1.py +++ b/project_euler/problem_013/sol1.py @@ -5,6 +5,7 @@ Work out the first ten digits of the sum of the following one-hundred 50-digit numbers. """ + import os diff --git a/project_euler/problem_014/sol2.py b/project_euler/problem_014/sol2.py index d2a1d9f0e468..797b0f9886fe 100644 --- a/project_euler/problem_014/sol2.py +++ b/project_euler/problem_014/sol2.py @@ -25,6 +25,7 @@ Which starting number, under one million, produces the longest chain? """ + from __future__ import annotations COLLATZ_SEQUENCE_LENGTHS = {1: 1} @@ -34,10 +35,7 @@ def collatz_sequence_length(n: int) -> int: """Returns the Collatz sequence length for n.""" if n in COLLATZ_SEQUENCE_LENGTHS: return COLLATZ_SEQUENCE_LENGTHS[n] - if n % 2 == 0: - next_n = n // 2 - else: - next_n = 3 * n + 1 + next_n = n // 2 if n % 2 == 0 else 3 * n + 1 sequence_length = collatz_sequence_length(next_n) + 1 COLLATZ_SEQUENCE_LENGTHS[n] = sequence_length return sequence_length diff --git a/project_euler/problem_015/sol1.py b/project_euler/problem_015/sol1.py index fb2020d6179f..3c9dae1aed77 100644 --- a/project_euler/problem_015/sol1.py +++ b/project_euler/problem_015/sol1.py @@ -1,10 +1,11 @@ """ Problem 15: https://projecteuler.net/problem=15 -Starting in the top left corner of a 2×2 grid, and only being able to move to +Starting in the top left corner of a 2x2 grid, and only being able to move to the right and down, there are exactly 6 routes to the bottom right corner. -How many such routes are there through a 20×20 grid? +How many such routes are there through a 20x20 grid? """ + from math import factorial diff --git a/project_euler/problem_018/solution.py b/project_euler/problem_018/solution.py index 82fc3ce3c9db..cbe8743be15f 100644 --- a/project_euler/problem_018/solution.py +++ b/project_euler/problem_018/solution.py @@ -27,6 +27,7 @@ 63 66 04 68 89 53 67 30 73 16 69 87 40 31 04 62 98 27 23 09 70 98 73 93 38 53 60 04 23 """ + import os @@ -48,14 +49,8 @@ def solution(): for i in range(1, len(a)): for j in range(len(a[i])): - if j != len(a[i - 1]): - number1 = a[i - 1][j] - else: - number1 = 0 - if j > 0: - number2 = a[i - 1][j - 1] - else: - number2 = 0 + number1 = a[i - 1][j] if j != len(a[i - 1]) else 0 + number2 = a[i - 1][j - 1] if j > 0 else 0 a[i][j] += max(number1, number2) return max(a[-1]) diff --git a/project_euler/problem_019/sol1.py b/project_euler/problem_019/sol1.py index ab59365843b2..656f104c390d 100644 --- a/project_euler/problem_019/sol1.py +++ b/project_euler/problem_019/sol1.py @@ -39,17 +39,16 @@ def solution(): while year < 2001: day += 7 - if (year % 4 == 0 and not year % 100 == 0) or (year % 400 == 0): + if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): if day > days_per_month[month - 1] and month != 2: month += 1 day = day - days_per_month[month - 2] elif day > 29 and month == 2: month += 1 day = day - 29 - else: - if day > days_per_month[month - 1]: - month += 1 - day = day - days_per_month[month - 2] + elif day > days_per_month[month - 1]: + month += 1 + day = day - days_per_month[month - 2] if month > 12: year += 1 diff --git a/project_euler/problem_020/sol1.py b/project_euler/problem_020/sol1.py index b472024e54c0..1439bdca38e6 100644 --- a/project_euler/problem_020/sol1.py +++ b/project_euler/problem_020/sol1.py @@ -1,9 +1,9 @@ """ Problem 20: https://projecteuler.net/problem=20 -n! means n × (n − 1) × ... × 3 × 2 × 1 +n! means n x (n - 1) x ... x 3 x 2 x 1 -For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800, and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. Find the sum of the digits in the number 100! diff --git a/project_euler/problem_020/sol2.py b/project_euler/problem_020/sol2.py index 676e96e7836a..61684cd5ef6d 100644 --- a/project_euler/problem_020/sol2.py +++ b/project_euler/problem_020/sol2.py @@ -1,13 +1,14 @@ """ Problem 20: https://projecteuler.net/problem=20 -n! means n × (n − 1) × ... × 3 × 2 × 1 +n! means n x (n - 1) x ... x 3 x 2 x 1 -For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800, and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. Find the sum of the digits in the number 100! """ + from math import factorial diff --git a/project_euler/problem_020/sol3.py b/project_euler/problem_020/sol3.py index 4f28ac5fcfde..8984def9c34e 100644 --- a/project_euler/problem_020/sol3.py +++ b/project_euler/problem_020/sol3.py @@ -1,13 +1,14 @@ """ Problem 20: https://projecteuler.net/problem=20 -n! means n × (n − 1) × ... × 3 × 2 × 1 +n! means n x (n - 1) x ... x 3 x 2 x 1 -For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800, and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. Find the sum of the digits in the number 100! """ + from math import factorial diff --git a/project_euler/problem_020/sol4.py b/project_euler/problem_020/sol4.py index b32ce309dfa6..511ac81e176b 100644 --- a/project_euler/problem_020/sol4.py +++ b/project_euler/problem_020/sol4.py @@ -1,9 +1,9 @@ """ Problem 20: https://projecteuler.net/problem=20 -n! means n × (n − 1) × ... × 3 × 2 × 1 +n! means n x (n - 1) x ... x 3 x 2 x 1 -For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +For example, 10! = 10 x 9 x ... x 3 x 2 x 1 = 3628800, and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. Find the sum of the digits in the number 100! diff --git a/project_euler/problem_021/sol1.py b/project_euler/problem_021/sol1.py index 353510ae8f94..f6dbfa8864db 100644 --- a/project_euler/problem_021/sol1.py +++ b/project_euler/problem_021/sol1.py @@ -13,6 +13,7 @@ Evaluate the sum of all the amicable numbers under 10000. """ + from math import sqrt diff --git a/project_euler/problem_022/sol1.py b/project_euler/problem_022/sol1.py index 982906245e87..c4af5dfa81df 100644 --- a/project_euler/problem_022/sol1.py +++ b/project_euler/problem_022/sol1.py @@ -10,10 +10,11 @@ For example, when the list is sorted into alphabetical order, COLIN, which is worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would -obtain a score of 938 × 53 = 49714. +obtain a score of 938 x 53 = 49714. What is the total of all the name scores in the file? """ + import os diff --git a/project_euler/problem_022/sol2.py b/project_euler/problem_022/sol2.py index 5ae41c84686e..9c22b6bba0cc 100644 --- a/project_euler/problem_022/sol2.py +++ b/project_euler/problem_022/sol2.py @@ -10,10 +10,11 @@ For example, when the list is sorted into alphabetical order, COLIN, which is worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would -obtain a score of 938 × 53 = 49714. +obtain a score of 938 x 53 = 49714. What is the total of all the name scores in the file? """ + import os diff --git a/project_euler/problem_024/sol1.py b/project_euler/problem_024/sol1.py index 1c6378b38260..3fb1bd4ec582 100644 --- a/project_euler/problem_024/sol1.py +++ b/project_euler/problem_024/sol1.py @@ -9,6 +9,7 @@ What is the millionth lexicographic permutation of the digits 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9? """ + from itertools import permutations diff --git a/project_euler/problem_025/sol1.py b/project_euler/problem_025/sol1.py index 803464b5d786..b3bbb56d20be 100644 --- a/project_euler/problem_025/sol1.py +++ b/project_euler/problem_025/sol1.py @@ -1,7 +1,7 @@ """ The Fibonacci sequence is defined by the recurrence relation: - Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. + Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1. Hence the first 12 terms will be: diff --git a/project_euler/problem_025/sol2.py b/project_euler/problem_025/sol2.py index 6f49e89fb465..4094b6251d50 100644 --- a/project_euler/problem_025/sol2.py +++ b/project_euler/problem_025/sol2.py @@ -1,7 +1,7 @@ """ The Fibonacci sequence is defined by the recurrence relation: - Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. + Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1. Hence the first 12 terms will be: @@ -23,10 +23,11 @@ What is the index of the first term in the Fibonacci sequence to contain 1000 digits? """ + from collections.abc import Generator -def fibonacci_generator() -> Generator[int, None, None]: +def fibonacci_generator() -> Generator[int]: """ A generator that produces numbers in the Fibonacci sequence diff --git a/project_euler/problem_025/sol3.py b/project_euler/problem_025/sol3.py index 0b9f3a0c84ef..e33b159ac65c 100644 --- a/project_euler/problem_025/sol3.py +++ b/project_euler/problem_025/sol3.py @@ -1,7 +1,7 @@ """ The Fibonacci sequence is defined by the recurrence relation: - Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. + Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1. Hence the first 12 terms will be: diff --git a/project_euler/problem_027/sol1.py b/project_euler/problem_027/sol1.py index c93e2b4fa251..48755ec19763 100644 --- a/project_euler/problem_027/sol1.py +++ b/project_euler/problem_027/sol1.py @@ -9,12 +9,12 @@ It turns out that the formula will produce 40 primes for the consecutive values n = 0 to 39. However, when n = 40, 402 + 40 + 41 = 40(40 + 1) + 41 is divisible by 41, and certainly when n = 41, 412 + 41 + 41 is clearly divisible by 41. -The incredible formula n2 − 79n + 1601 was discovered, which produces 80 primes -for the consecutive values n = 0 to 79. The product of the coefficients, −79 and -1601, is −126479. +The incredible formula n2 - 79n + 1601 was discovered, which produces 80 primes +for the consecutive values n = 0 to 79. The product of the coefficients, -79 and +1601, is -126479. Considering quadratics of the form: n² + an + b, where |a| < 1000 and |b| < 1000 -where |n| is the modulus/absolute value of ne.g. |11| = 11 and |−4| = 4 +where |n| is the modulus/absolute value of ne.g. |11| = 11 and |-4| = 4 Find the product of the coefficients, a and b, for the quadratic expression that produces the maximum number of primes for consecutive values of n, starting with n = 0. diff --git a/project_euler/problem_030/sol1.py b/project_euler/problem_030/sol1.py index 2c6b4e4e85d5..7d83e314523f 100644 --- a/project_euler/problem_030/sol1.py +++ b/project_euler/problem_030/sol1.py @@ -1,4 +1,4 @@ -""" Problem Statement (Digit Fifth Powers): https://projecteuler.net/problem=30 +"""Problem Statement (Digit Fifth Powers): https://projecteuler.net/problem=30 Surprisingly there are only three numbers that can be written as the sum of fourth powers of their digits: @@ -21,7 +21,6 @@ and hence a number between 1000 and 1000000 """ - DIGITS_FIFTH_POWER = {str(digit): digit**5 for digit in range(10)} diff --git a/project_euler/problem_031/sol1.py b/project_euler/problem_031/sol1.py index ba40cf383175..4c9c533eecb7 100644 --- a/project_euler/problem_031/sol1.py +++ b/project_euler/problem_031/sol1.py @@ -2,14 +2,14 @@ Coin sums Problem 31: https://projecteuler.net/problem=31 -In England the currency is made up of pound, £, and pence, p, and there are +In England the currency is made up of pound, f, and pence, p, and there are eight coins in general circulation: -1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p). -It is possible to make £2 in the following way: +1p, 2p, 5p, 10p, 20p, 50p, f1 (100p) and f2 (200p). +It is possible to make f2 in the following way: -1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p -How many different ways can £2 be made using any number of coins? +1xf1 + 1x50p + 2x20p + 1x5p + 1x2p + 3x1p +How many different ways can f2 be made using any number of coins? """ diff --git a/project_euler/problem_031/sol2.py b/project_euler/problem_031/sol2.py index f9e4dc384bff..574f8d4107a1 100644 --- a/project_euler/problem_031/sol2.py +++ b/project_euler/problem_031/sol2.py @@ -3,17 +3,17 @@ Coin sums -In England the currency is made up of pound, £, and pence, p, and there are +In England the currency is made up of pound, f, and pence, p, and there are eight coins in general circulation: -1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p). -It is possible to make £2 in the following way: +1p, 2p, 5p, 10p, 20p, 50p, f1 (100p) and f2 (200p). +It is possible to make f2 in the following way: -1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p -How many different ways can £2 be made using any number of coins? +1xf1 + 1x50p + 2x20p + 1x5p + 1x2p + 3x1p +How many different ways can f2 be made using any number of coins? Hint: - > There are 100 pence in a pound (£1 = 100p) + > There are 100 pence in a pound (f1 = 100p) > There are coins(in pence) are available: 1, 2, 5, 10, 20, 50, 100 and 200. > how many different ways you can combine these values to create 200 pence. diff --git a/project_euler/problem_032/sol32.py b/project_euler/problem_032/sol32.py index c4d11e86c877..c0ca2ce10791 100644 --- a/project_euler/problem_032/sol32.py +++ b/project_euler/problem_032/sol32.py @@ -3,7 +3,7 @@ digits 1 to n exactly once; for example, the 5-digit number, 15234, is 1 through 5 pandigital. -The product 7254 is unusual, as the identity, 39 × 186 = 7254, containing +The product 7254 is unusual, as the identity, 39 x 186 = 7254, containing multiplicand, multiplier, and product is 1 through 9 pandigital. Find the sum of all products whose multiplicand/multiplier/product identity can @@ -12,6 +12,7 @@ HINT: Some products can be obtained in more than one way so be sure to only include it once in your sum. """ + import itertools diff --git a/project_euler/problem_033/sol1.py b/project_euler/problem_033/sol1.py index e0c9a058af53..71790d34fbed 100644 --- a/project_euler/problem_033/sol1.py +++ b/project_euler/problem_033/sol1.py @@ -14,17 +14,16 @@ If the product of these four fractions is given in its lowest common terms, find the value of the denominator. """ + from __future__ import annotations from fractions import Fraction def is_digit_cancelling(num: int, den: int) -> bool: - if num != den: - if num % 10 == den // 10: - if (num // 10) / (den % 10) == num / den: - return True - return False + return ( + num != den and num % 10 == den // 10 and (num // 10) / (den % 10) == num / den + ) def fraction_list(digit_len: int) -> list[str]: @@ -45,9 +44,13 @@ def fraction_list(digit_len: int) -> list[str]: last_digit = int("1" + "0" * digit_len) for num in range(den, last_digit): while den <= 99: - if (num != den) and (num % 10 == den // 10) and (den % 10 != 0): - if is_digit_cancelling(num, den): - solutions.append(f"{num}/{den}") + if ( + (num != den) + and (num % 10 == den // 10) + and (den % 10 != 0) + and is_digit_cancelling(num, den) + ): + solutions.append(f"{num}/{den}") den += 1 num += 1 den = 10 diff --git a/project_euler/problem_034/__init__.py b/project_euler/problem_034/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_034/__init__.py +++ b/project_euler/problem_034/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_035/__init__.py b/project_euler/problem_035/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_035/__init__.py +++ b/project_euler/problem_035/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_035/sol1.py b/project_euler/problem_035/sol1.py index 17a4e9088ae2..cf9f6821d798 100644 --- a/project_euler/problem_035/sol1.py +++ b/project_euler/problem_035/sol1.py @@ -11,18 +11,19 @@ How many circular primes are there below one million? To solve this problem in an efficient manner, we will first mark all the primes -below 1 million using the Seive of Eratosthenes. Then, out of all these primes, -we will rule out the numbers which contain an even digit. After this we will +below 1 million using the Sieve of Eratosthenes. Then, out of all these primes, +we will rule out the numbers which contain an even digit. After this we will generate each circular combination of the number and check if all are prime. """ + from __future__ import annotations -seive = [True] * 1000001 +sieve = [True] * 1000001 i = 2 while i * i <= 1000000: - if seive[i]: + if sieve[i]: for j in range(i * i, 1000001, i): - seive[j] = False + sieve[j] = False i += 1 @@ -36,7 +37,7 @@ def is_prime(n: int) -> bool: >>> is_prime(25363) False """ - return seive[n] + return sieve[n] def contains_an_even_digit(n: int) -> bool: diff --git a/project_euler/problem_036/sol1.py b/project_euler/problem_036/sol1.py index 1d27356ec51e..3865b2a39ea9 100644 --- a/project_euler/problem_036/sol1.py +++ b/project_euler/problem_036/sol1.py @@ -14,6 +14,7 @@ (Please note that the palindromic number, in either base, may not include leading zeros.) """ + from __future__ import annotations diff --git a/project_euler/problem_037/__init__.py b/project_euler/problem_037/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_037/__init__.py +++ b/project_euler/problem_037/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_037/sol1.py b/project_euler/problem_037/sol1.py index ef7686cbcb96..c66eb9fb1735 100644 --- a/project_euler/problem_037/sol1.py +++ b/project_euler/problem_037/sol1.py @@ -85,10 +85,10 @@ def validate(n: int) -> bool: >>> validate(3797) True """ - if len(str(n)) > 3: - if not is_prime(int(str(n)[-3:])) or not is_prime(int(str(n)[:3])): - return False - return True + return not ( + len(str(n)) > 3 + and (not is_prime(int(str(n)[-3:])) or not is_prime(int(str(n)[:3]))) + ) def compute_truncated_primes(count: int = 11) -> list[int]: diff --git a/project_euler/problem_038/sol1.py b/project_euler/problem_038/sol1.py index e4a6d09f8f7d..382892723b7d 100644 --- a/project_euler/problem_038/sol1.py +++ b/project_euler/problem_038/sol1.py @@ -3,9 +3,9 @@ Take the number 192 and multiply it by each of 1, 2, and 3: -192 × 1 = 192 -192 × 2 = 384 -192 × 3 = 576 +192 x 1 = 192 +192 x 2 = 384 +192 x 3 = 576 By concatenating each product we get the 1 to 9 pandigital, 192384576. We will call 192384576 the concatenated product of 192 and (1,2,3) @@ -37,6 +37,7 @@ => 100 <= a < 334, candidate = a * 10^6 + 2a * 10^3 + 3a = 1002003 * a """ + from __future__ import annotations diff --git a/project_euler/problem_039/__init__.py b/project_euler/problem_039/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_039/__init__.py +++ b/project_euler/problem_039/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_040/sol1.py b/project_euler/problem_040/sol1.py index 69be377723a5..721bd063c28a 100644 --- a/project_euler/problem_040/sol1.py +++ b/project_euler/problem_040/sol1.py @@ -11,7 +11,7 @@ If dn represents the nth digit of the fractional part, find the value of the following expression. -d1 × d10 × d100 × d1000 × d10000 × d100000 × d1000000 +d1 x d10 x d100 x d1000 x d10000 x d100000 x d1000000 """ diff --git a/project_euler/problem_041/__init__.py b/project_euler/problem_041/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_041/__init__.py +++ b/project_euler/problem_041/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_041/sol1.py b/project_euler/problem_041/sol1.py index 2ef0120684c3..0c37f5469a6c 100644 --- a/project_euler/problem_041/sol1.py +++ b/project_euler/problem_041/sol1.py @@ -10,6 +10,7 @@ So we will check only 7 digit pandigital numbers to obtain the largest possible pandigital prime. """ + from __future__ import annotations import math diff --git a/project_euler/problem_042/solution42.py b/project_euler/problem_042/solution42.py index f8a54e40eaab..f678bcdef710 100644 --- a/project_euler/problem_042/solution42.py +++ b/project_euler/problem_042/solution42.py @@ -13,6 +13,7 @@ containing nearly two-thousand common English words, how many are triangle words? """ + import os # Precomputes a list of the 100 first triangular numbers diff --git a/project_euler/problem_043/__init__.py b/project_euler/problem_043/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_043/__init__.py +++ b/project_euler/problem_043/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_043/sol1.py b/project_euler/problem_043/sol1.py index c533f40da9c9..f3a2c71edc4e 100644 --- a/project_euler/problem_043/sol1.py +++ b/project_euler/problem_043/sol1.py @@ -18,7 +18,6 @@ Find the sum of all 0 to 9 pandigital numbers with this property. """ - from itertools import permutations diff --git a/project_euler/problem_044/__init__.py b/project_euler/problem_044/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_044/__init__.py +++ b/project_euler/problem_044/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_044/sol1.py b/project_euler/problem_044/sol1.py index 3b75b6a56a8e..2613563a4bf1 100644 --- a/project_euler/problem_044/sol1.py +++ b/project_euler/problem_044/sol1.py @@ -1,14 +1,14 @@ """ Problem 44: https://projecteuler.net/problem=44 -Pentagonal numbers are generated by the formula, Pn=n(3n−1)/2. The first ten +Pentagonal numbers are generated by the formula, Pn=n(3n-1)/2. The first ten pentagonal numbers are: 1, 5, 12, 22, 35, 51, 70, 92, 117, 145, ... It can be seen that P4 + P7 = 22 + 70 = 92 = P8. However, their difference, -70 − 22 = 48, is not pentagonal. +70 - 22 = 48, is not pentagonal. Find the pair of pentagonal numbers, Pj and Pk, for which their sum and difference -are pentagonal and D = |Pk − Pj| is minimised; what is the value of D? +are pentagonal and D = |Pk - Pj| is minimised; what is the value of D? """ diff --git a/project_euler/problem_045/__init__.py b/project_euler/problem_045/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_045/__init__.py +++ b/project_euler/problem_045/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_045/sol1.py b/project_euler/problem_045/sol1.py index d921b2802c2d..8d016de6e542 100644 --- a/project_euler/problem_045/sol1.py +++ b/project_euler/problem_045/sol1.py @@ -3,8 +3,8 @@ Triangle, pentagonal, and hexagonal numbers are generated by the following formulae: Triangle T(n) = (n * (n + 1)) / 2 1, 3, 6, 10, 15, ... -Pentagonal P(n) = (n * (3 * n − 1)) / 2 1, 5, 12, 22, 35, ... -Hexagonal H(n) = n * (2 * n − 1) 1, 6, 15, 28, 45, ... +Pentagonal P(n) = (n * (3 * n - 1)) / 2 1, 5, 12, 22, 35, ... +Hexagonal H(n) = n * (2 * n - 1) 1, 6, 15, 28, 45, ... It can be verified that T(285) = P(165) = H(143) = 40755. Find the next triangle number that is also pentagonal and hexagonal. diff --git a/project_euler/problem_046/__init__.py b/project_euler/problem_046/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_046/__init__.py +++ b/project_euler/problem_046/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_046/sol1.py b/project_euler/problem_046/sol1.py index 07dd9bbf84c8..f27f658e63e5 100644 --- a/project_euler/problem_046/sol1.py +++ b/project_euler/problem_046/sol1.py @@ -4,12 +4,12 @@ It was proposed by Christian Goldbach that every odd composite number can be written as the sum of a prime and twice a square. -9 = 7 + 2 × 12 -15 = 7 + 2 × 22 -21 = 3 + 2 × 32 -25 = 7 + 2 × 32 -27 = 19 + 2 × 22 -33 = 31 + 2 × 12 +9 = 7 + 2 x 12 +15 = 7 + 2 x 22 +21 = 3 + 2 x 32 +25 = 7 + 2 x 32 +27 = 19 + 2 x 22 +33 = 31 + 2 x 12 It turns out that the conjecture was false. diff --git a/project_euler/problem_047/sol1.py b/project_euler/problem_047/sol1.py index 1287e0d9e107..d174de27dcd0 100644 --- a/project_euler/problem_047/sol1.py +++ b/project_euler/problem_047/sol1.py @@ -5,14 +5,14 @@ The first two consecutive numbers to have two distinct prime factors are: -14 = 2 × 7 -15 = 3 × 5 +14 = 2 x 7 +15 = 3 x 5 The first three consecutive numbers to have three distinct prime factors are: -644 = 2² × 7 × 23 -645 = 3 × 5 × 43 -646 = 2 × 17 × 19. +644 = 2² x 7 x 23 +645 = 3 x 5 x 43 +646 = 2 x 17 x 19. Find the first four consecutive integers to have four distinct prime factors each. What is the first of these numbers? @@ -24,7 +24,7 @@ def unique_prime_factors(n: int) -> set: """ Find unique prime factors of an integer. - Tests include sorting because only the set really matters, + Tests include sorting because only the set matters, not the order in which it is produced. >>> sorted(set(unique_prime_factors(14))) [2, 7] @@ -58,7 +58,7 @@ def upf_len(num: int) -> int: def equality(iterable: list) -> bool: """ - Check equality of ALL elements in an interable. + Check the equality of ALL elements in an iterable >>> equality([1, 2, 3, 4]) False >>> equality([2, 2, 2, 2]) @@ -69,7 +69,7 @@ def equality(iterable: list) -> bool: return len(set(iterable)) in (0, 1) -def run(n: int) -> list: +def run(n: int) -> list[int]: """ Runs core process to find problem solution. >>> run(3) @@ -77,7 +77,7 @@ def run(n: int) -> list: """ # Incrementor variable for our group list comprehension. - # This serves as the first number in each list of values + # This is the first number in each list of values # to test. base = 2 @@ -85,7 +85,7 @@ def run(n: int) -> list: # Increment each value of a generated range group = [base + i for i in range(n)] - # Run elements through out unique_prime_factors function + # Run elements through the unique_prime_factors function # Append our target number to the end. checker = [upf_len(x) for x in group] checker.append(n) @@ -98,7 +98,7 @@ def run(n: int) -> list: base += 1 -def solution(n: int = 4) -> int: +def solution(n: int = 4) -> int | None: """Return the first value of the first four consecutive integers to have four distinct prime factors each. >>> solution() diff --git a/project_euler/problem_050/sol1.py b/project_euler/problem_050/sol1.py index fc6e6f2b9a5d..0a5f861f0ef0 100644 --- a/project_euler/problem_050/sol1.py +++ b/project_euler/problem_050/sol1.py @@ -15,6 +15,7 @@ Which prime, below one-million, can be written as the sum of the most consecutive primes? """ + from __future__ import annotations diff --git a/project_euler/problem_051/sol1.py b/project_euler/problem_051/sol1.py index 921704bc4455..dc740c8b947d 100644 --- a/project_euler/problem_051/sol1.py +++ b/project_euler/problem_051/sol1.py @@ -15,6 +15,7 @@ Find the smallest prime which, by replacing part of the number (not necessarily adjacent digits) with the same digit, is part of an eight prime value family. """ + from __future__ import annotations from collections import Counter diff --git a/project_euler/problem_053/sol1.py b/project_euler/problem_053/sol1.py index 0692bbe0ebb8..192cbf25e50c 100644 --- a/project_euler/problem_053/sol1.py +++ b/project_euler/problem_053/sol1.py @@ -10,12 +10,13 @@ In general, -nCr = n!/(r!(n−r)!),where r ≤ n, n! = n×(n−1)×...×3×2×1, and 0! = 1. +nCr = n!/(r!(n-r)!),where r ≤ n, n! = nx(n-1)x...x3x2x1, and 0! = 1. It is not until n = 23, that a value exceeds one-million: 23C10 = 1144066. How many, not necessarily distinct, values of nCr, for 1 ≤ n ≤ 100, are greater than one-million? """ + from math import factorial diff --git a/project_euler/problem_054/sol1.py b/project_euler/problem_054/sol1.py index 9af7aef5a716..66aa3a0826f5 100644 --- a/project_euler/problem_054/sol1.py +++ b/project_euler/problem_054/sol1.py @@ -40,6 +40,7 @@ https://www.codewars.com/kata/ranking-poker-hands https://www.codewars.com/kata/sortable-poker-hands """ + from __future__ import annotations import os @@ -47,18 +48,18 @@ class PokerHand: """Create an object representing a Poker Hand based on an input of a - string which represents the best 5 card combination from the player's hand + string which represents the best 5-card combination from the player's hand and board cards. Attributes: (read-only) - hand: string representing the hand consisting of five cards + hand: a string representing the hand consisting of five cards Methods: compare_with(opponent): takes in player's hand (self) and opponent's hand (opponent) and compares both hands according to the rules of Texas Hold'em. Returns one of 3 strings (Win, Loss, Tie) based on whether - player's hand is better than opponent's hand. + player's hand is better than the opponent's hand. hand_name(): Returns a string made up of two parts: hand name and high card. @@ -66,11 +67,11 @@ class PokerHand: Supported operators: Rich comparison operators: <, >, <=, >=, ==, != - Supported builtin methods and functions: + Supported built-in methods and functions: list.sort(), sorted() """ - _HAND_NAME = [ + _HAND_NAME = ( "High card", "One pair", "Two pairs", @@ -81,10 +82,10 @@ class PokerHand: "Four of a kind", "Straight flush", "Royal flush", - ] + ) - _CARD_NAME = [ - "", # placeholder as lists are zero indexed + _CARD_NAME = ( + "", # placeholder as tuples are zero-indexed "One", "Two", "Three", @@ -99,7 +100,7 @@ class PokerHand: "Queen", "King", "Ace", - ] + ) def __init__(self, hand: str) -> None: """ @@ -119,10 +120,12 @@ def __init__(self, hand: str) -> None: For example: "6S 4C KC AS TH" """ if not isinstance(hand, str): - raise TypeError(f"Hand should be of type 'str': {hand!r}") + msg = f"Hand should be of type 'str': {hand!r}" + raise TypeError(msg) # split removes duplicate whitespaces so no need of strip if len(hand.split(" ")) != 5: - raise ValueError(f"Hand should contain only 5 cards: {hand!r}") + msg = f"Hand should contain only 5 cards: {hand!r}" + raise ValueError(msg) self._hand = hand self._first_pair = 0 self._second_pair = 0 diff --git a/project_euler/problem_054/test_poker_hand.py b/project_euler/problem_054/test_poker_hand.py index 5735bfc37947..ba5e0c8a2643 100644 --- a/project_euler/problem_054/test_poker_hand.py +++ b/project_euler/problem_054/test_poker_hand.py @@ -147,39 +147,39 @@ def generate_random_hands(number_of_hands: int = 100): return (generate_random_hand() for _ in range(number_of_hands)) -@pytest.mark.parametrize("hand, expected", TEST_FLUSH) +@pytest.mark.parametrize(("hand", "expected"), TEST_FLUSH) def test_hand_is_flush(hand, expected): assert PokerHand(hand)._is_flush() == expected -@pytest.mark.parametrize("hand, expected", TEST_STRAIGHT) +@pytest.mark.parametrize(("hand", "expected"), TEST_STRAIGHT) def test_hand_is_straight(hand, expected): assert PokerHand(hand)._is_straight() == expected -@pytest.mark.parametrize("hand, expected, card_values", TEST_FIVE_HIGH_STRAIGHT) +@pytest.mark.parametrize(("hand", "expected", "card_values"), TEST_FIVE_HIGH_STRAIGHT) def test_hand_is_five_high_straight(hand, expected, card_values): player = PokerHand(hand) assert player._is_five_high_straight() == expected assert player._card_values == card_values -@pytest.mark.parametrize("hand, expected", TEST_KIND) +@pytest.mark.parametrize(("hand", "expected"), TEST_KIND) def test_hand_is_same_kind(hand, expected): assert PokerHand(hand)._is_same_kind() == expected -@pytest.mark.parametrize("hand, expected", TEST_TYPES) +@pytest.mark.parametrize(("hand", "expected"), TEST_TYPES) def test_hand_values(hand, expected): assert PokerHand(hand)._hand_type == expected -@pytest.mark.parametrize("hand, other, expected", TEST_COMPARE) +@pytest.mark.parametrize(("hand", "other", "expected"), TEST_COMPARE) def test_compare_simple(hand, other, expected): assert PokerHand(hand).compare_with(PokerHand(other)) == expected -@pytest.mark.parametrize("hand, other, expected", generate_random_hands()) +@pytest.mark.parametrize(("hand", "other", "expected"), generate_random_hands()) def test_compare_random(hand, other, expected): assert PokerHand(hand).compare_with(PokerHand(other)) == expected diff --git a/project_euler/problem_055/__init__.py b/project_euler/problem_055/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_055/__init__.py +++ b/project_euler/problem_055/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_056/sol1.py b/project_euler/problem_056/sol1.py index c772bec58692..828dbd3a8ddf 100644 --- a/project_euler/problem_056/sol1.py +++ b/project_euler/problem_056/sol1.py @@ -30,9 +30,7 @@ def solution(a: int = 100, b: int = 100) -> int: # RETURN the MAXIMUM from the list of SUMs of the list of INT converted from STR of # BASE raised to the POWER return max( - sum(int(x) for x in str(base**power)) - for base in range(a) - for power in range(b) + sum(int(x) for x in str(base**power)) for base in range(a) for power in range(b) ) diff --git a/project_euler/problem_058/__init__.py b/project_euler/problem_058/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_058/__init__.py +++ b/project_euler/problem_058/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_058/sol1.py b/project_euler/problem_058/sol1.py index 6a991c58b6b8..1d2f406eafdb 100644 --- a/project_euler/problem_058/sol1.py +++ b/project_euler/problem_058/sol1.py @@ -33,6 +33,7 @@ count of current primes. """ + import math diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py index b795dd243b08..65bfd3f0b0fb 100644 --- a/project_euler/problem_059/sol1.py +++ b/project_euler/problem_059/sol1.py @@ -25,6 +25,7 @@ must contain common English words, decrypt the message and find the sum of the ASCII values in the original text. """ + from __future__ import annotations import string diff --git a/project_euler/problem_063/__init__.py b/project_euler/problem_063/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_063/__init__.py +++ b/project_euler/problem_063/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_064/sol1.py b/project_euler/problem_064/sol1.py index 81ebcc7b73c3..12769decc62f 100644 --- a/project_euler/problem_064/sol1.py +++ b/project_euler/problem_064/sol1.py @@ -67,9 +67,8 @@ def solution(n: int = 10000) -> int: count_odd_periods = 0 for i in range(2, n + 1): sr = sqrt(i) - if sr - floor(sr) != 0: - if continuous_fraction_period(i) % 2 == 1: - count_odd_periods += 1 + if sr - floor(sr) != 0 and continuous_fraction_period(i) % 2 == 1: + count_odd_periods += 1 return count_odd_periods diff --git a/project_euler/problem_067/sol1.py b/project_euler/problem_067/sol1.py index f20c206cca11..171ff8c268f6 100644 --- a/project_euler/problem_067/sol1.py +++ b/project_euler/problem_067/sol1.py @@ -11,6 +11,7 @@ 'Save Link/Target As...'), a 15K text file containing a triangle with one-hundred rows. """ + import os @@ -37,14 +38,8 @@ def solution(): for i in range(1, len(a)): for j in range(len(a[i])): - if j != len(a[i - 1]): - number1 = a[i - 1][j] - else: - number1 = 0 - if j > 0: - number2 = a[i - 1][j - 1] - else: - number2 = 0 + number1 = a[i - 1][j] if j != len(a[i - 1]) else 0 + number2 = a[i - 1][j - 1] if j > 0 else 0 a[i][j] += max(number1, number2) return max(a[-1]) diff --git a/project_euler/problem_067/sol2.py b/project_euler/problem_067/sol2.py index 2e88a57170a8..4fb093d49956 100644 --- a/project_euler/problem_067/sol2.py +++ b/project_euler/problem_067/sol2.py @@ -11,6 +11,7 @@ 'Save Link/Target As...'), a 15K text file containing a triangle with one-hundred rows. """ + import os diff --git a/project_euler/problem_068/sol1.py b/project_euler/problem_068/sol1.py index 772be359f630..cf814b001d57 100644 --- a/project_euler/problem_068/sol1.py +++ b/project_euler/problem_068/sol1.py @@ -73,7 +73,8 @@ def solution(gon_side: int = 5) -> int: if is_magic_gon(numbers): return int("".join(str(n) for n in numbers)) - raise ValueError(f"Magic {gon_side}-gon ring is impossible") + msg = f"Magic {gon_side}-gon ring is impossible" + raise ValueError(msg) def generate_gon_ring(gon_side: int, perm: list[int]) -> list[int]: diff --git a/project_euler/problem_070/sol1.py b/project_euler/problem_070/sol1.py index 273f37efc5fc..9874b7418559 100644 --- a/project_euler/problem_070/sol1.py +++ b/project_euler/problem_070/sol1.py @@ -28,8 +28,11 @@ Finding totients https://en.wikipedia.org/wiki/Euler's_totient_function#Euler's_product_formula """ + from __future__ import annotations +import numpy as np + def get_totients(max_one: int) -> list[int]: """ @@ -42,17 +45,14 @@ def get_totients(max_one: int) -> list[int]: >>> get_totients(10) [0, 1, 1, 2, 2, 4, 2, 6, 4, 6] """ - totients = [0] * max_one - - for i in range(0, max_one): - totients[i] = i + totients = np.arange(max_one) for i in range(2, max_one): if totients[i] == i: - for j in range(i, max_one, i): - totients[j] -= totients[j] // i + x = np.arange(i, max_one, i) # array of indexes to select + totients[x] -= totients[x] // i - return totients + return totients.tolist() def has_same_digits(num1: int, num2: int) -> bool: diff --git a/project_euler/problem_072/sol1.py b/project_euler/problem_072/sol1.py index a2a0eeeb31c5..f09db0673323 100644 --- a/project_euler/problem_072/sol1.py +++ b/project_euler/problem_072/sol1.py @@ -21,6 +21,8 @@ Time: 1 sec """ +import numpy as np + def solution(limit: int = 1_000_000) -> int: """ @@ -33,14 +35,15 @@ def solution(limit: int = 1_000_000) -> int: 304191 """ - phi = [i - 1 for i in range(limit + 1)] + # generating an array from -1 to limit + phi = np.arange(-1, limit) for i in range(2, limit + 1): if phi[i] == i - 1: - for j in range(2 * i, limit + 1, i): - phi[j] -= phi[j] // i + ind = np.arange(2 * i, limit + 1, i) # indexes for selection + phi[ind] -= phi[ind] // i - return sum(phi[2 : limit + 1]) + return int(np.sum(phi[2 : limit + 1])) if __name__ == "__main__": diff --git a/project_euler/problem_074/sol1.py b/project_euler/problem_074/sol1.py index a257d4d94fa8..91440b3fd02b 100644 --- a/project_euler/problem_074/sol1.py +++ b/project_euler/problem_074/sol1.py @@ -27,7 +27,6 @@ non-repeating terms? """ - DIGIT_FACTORIALS = { "0": 1, "1": 1, diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py index b54bc023e387..52a996bfa51d 100644 --- a/project_euler/problem_074/sol2.py +++ b/project_euler/problem_074/sol2.py @@ -33,6 +33,7 @@ is greater then the desired one. After generating each chain, the length is checked and the counter increases. """ + from math import factorial DIGIT_FACTORIAL: dict[str, int] = {str(digit): factorial(digit) for digit in range(10)} diff --git a/project_euler/problem_075/sol1.py b/project_euler/problem_075/sol1.py index b57604d76a86..0ccaf5dee7ec 100644 --- a/project_euler/problem_075/sol1.py +++ b/project_euler/problem_075/sol1.py @@ -29,7 +29,6 @@ from collections import defaultdict from math import gcd -from typing import DefaultDict def solution(limit: int = 1500000) -> int: @@ -43,7 +42,7 @@ def solution(limit: int = 1500000) -> int: >>> solution(50000) 5502 """ - frequencies: DefaultDict = defaultdict(int) + frequencies: defaultdict = defaultdict(int) euclid_m = 2 while 2 * euclid_m * (euclid_m + 1) <= limit: for euclid_n in range((euclid_m % 2) + 1, euclid_m, 2): diff --git a/project_euler/problem_077/sol1.py b/project_euler/problem_077/sol1.py index 6098ea9e50a6..e8f4e979a625 100644 --- a/project_euler/problem_077/sol1.py +++ b/project_euler/problem_077/sol1.py @@ -12,6 +12,7 @@ What is the first value which can be written as the sum of primes in over five thousand different ways? """ + from __future__ import annotations from functools import lru_cache diff --git a/project_euler/problem_079/__init__.py b/project_euler/problem_079/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_079/keylog.txt b/project_euler/problem_079/keylog.txt new file mode 100644 index 000000000000..41f15673248d --- /dev/null +++ b/project_euler/problem_079/keylog.txt @@ -0,0 +1,50 @@ +319 +680 +180 +690 +129 +620 +762 +689 +762 +318 +368 +710 +720 +710 +629 +168 +160 +689 +716 +731 +736 +729 +316 +729 +729 +710 +769 +290 +719 +680 +318 +389 +162 +289 +162 +718 +729 +319 +790 +680 +890 +362 +319 +760 +316 +729 +380 +319 +728 +716 diff --git a/project_euler/problem_079/keylog_test.txt b/project_euler/problem_079/keylog_test.txt new file mode 100644 index 000000000000..2c7024bde948 --- /dev/null +++ b/project_euler/problem_079/keylog_test.txt @@ -0,0 +1,16 @@ +319 +680 +180 +690 +129 +620 +698 +318 +328 +310 +320 +610 +629 +198 +190 +631 diff --git a/project_euler/problem_079/sol1.py b/project_euler/problem_079/sol1.py new file mode 100644 index 000000000000..74392e9bd094 --- /dev/null +++ b/project_euler/problem_079/sol1.py @@ -0,0 +1,70 @@ +""" +Project Euler Problem 79: https://projecteuler.net/problem=79 + +Passcode derivation + +A common security method used for online banking is to ask the user for three +random characters from a passcode. For example, if the passcode was 531278, +they may ask for the 2nd, 3rd, and 5th characters; the expected reply would +be: 317. + +The text file, keylog.txt, contains fifty successful login attempts. + +Given that the three characters are always asked for in order, analyse the file +so as to determine the shortest possible secret passcode of unknown length. +""" + +import itertools +from pathlib import Path + + +def find_secret_passcode(logins: list[str]) -> int: + """ + Returns the shortest possible secret passcode of unknown length. + + >>> find_secret_passcode(["135", "259", "235", "189", "690", "168", "120", + ... "136", "289", "589", "160", "165", "580", "369", "250", "280"]) + 12365890 + + >>> find_secret_passcode(["426", "281", "061", "819" "268", "406", "420", + ... "428", "209", "689", "019", "421", "469", "261", "681", "201"]) + 4206819 + """ + + # Split each login by character e.g. '319' -> ('3', '1', '9') + split_logins = [tuple(login) for login in logins] + + unique_chars = {char for login in split_logins for char in login} + + for permutation in itertools.permutations(unique_chars): + satisfied = True + for login in logins: + if not ( + permutation.index(login[0]) + < permutation.index(login[1]) + < permutation.index(login[2]) + ): + satisfied = False + break + + if satisfied: + return int("".join(permutation)) + + raise Exception("Unable to find the secret passcode") + + +def solution(input_file: str = "keylog.txt") -> int: + """ + Returns the shortest possible secret passcode of unknown length + for successful login attempts given by `input_file` text file. + + >>> solution("keylog_test.txt") + 6312980 + """ + logins = Path(__file__).parent.joinpath(input_file).read_text().splitlines() + + return find_secret_passcode(logins) + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_080/sol1.py b/project_euler/problem_080/sol1.py index 916998bdd8ad..8cfcbd41b588 100644 --- a/project_euler/problem_080/sol1.py +++ b/project_euler/problem_080/sol1.py @@ -6,6 +6,7 @@ square roots. Time: 5 October 2020, 18:30 """ + import decimal diff --git a/project_euler/problem_081/sol1.py b/project_euler/problem_081/sol1.py index aef6106b54df..293027bddd0e 100644 --- a/project_euler/problem_081/sol1.py +++ b/project_euler/problem_081/sol1.py @@ -13,6 +13,7 @@ and down in matrix.txt (https://projecteuler.net/project/resources/p081_matrix.txt), a 31K text file containing an 80 by 80 matrix. """ + import os diff --git a/project_euler/problem_082/__init__.py b/project_euler/problem_082/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_082/input.txt b/project_euler/problem_082/input.txt new file mode 100644 index 000000000000..f65322a7e541 --- /dev/null +++ b/project_euler/problem_082/input.txt @@ -0,0 +1,80 @@ +4445,2697,5115,718,2209,2212,654,4348,3079,6821,7668,3276,8874,4190,3785,2752,9473,7817,9137,496,7338,3434,7152,4355,4552,7917,7827,2460,2350,691,3514,5880,3145,7633,7199,3783,5066,7487,3285,1084,8985,760,872,8609,8051,1134,9536,5750,9716,9371,7619,5617,275,9721,2997,2698,1887,8825,6372,3014,2113,7122,7050,6775,5948,2758,1219,3539,348,7989,2735,9862,1263,8089,6401,9462,3168,2758,3748,5870 +1096,20,1318,7586,5167,2642,1443,5741,7621,7030,5526,4244,2348,4641,9827,2448,6918,5883,3737,300,7116,6531,567,5997,3971,6623,820,6148,3287,1874,7981,8424,7672,7575,6797,6717,1078,5008,4051,8795,5820,346,1851,6463,2117,6058,3407,8211,117,4822,1317,4377,4434,5925,8341,4800,1175,4173,690,8978,7470,1295,3799,8724,3509,9849,618,3320,7068,9633,2384,7175,544,6583,1908,9983,481,4187,9353,9377 +9607,7385,521,6084,1364,8983,7623,1585,6935,8551,2574,8267,4781,3834,2764,2084,2669,4656,9343,7709,2203,9328,8004,6192,5856,3555,2260,5118,6504,1839,9227,1259,9451,1388,7909,5733,6968,8519,9973,1663,5315,7571,3035,4325,4283,2304,6438,3815,9213,9806,9536,196,5542,6907,2475,1159,5820,9075,9470,2179,9248,1828,4592,9167,3713,4640,47,3637,309,7344,6955,346,378,9044,8635,7466,5036,9515,6385,9230 +7206,3114,7760,1094,6150,5182,7358,7387,4497,955,101,1478,7777,6966,7010,8417,6453,4955,3496,107,449,8271,131,2948,6185,784,5937,8001,6104,8282,4165,3642,710,2390,575,715,3089,6964,4217,192,5949,7006,715,3328,1152,66,8044,4319,1735,146,4818,5456,6451,4113,1063,4781,6799,602,1504,6245,6550,1417,1343,2363,3785,5448,4545,9371,5420,5068,4613,4882,4241,5043,7873,8042,8434,3939,9256,2187 +3620,8024,577,9997,7377,7682,1314,1158,6282,6310,1896,2509,5436,1732,9480,706,496,101,6232,7375,2207,2306,110,6772,3433,2878,8140,5933,8688,1399,2210,7332,6172,6403,7333,4044,2291,1790,2446,7390,8698,5723,3678,7104,1825,2040,140,3982,4905,4160,2200,5041,2512,1488,2268,1175,7588,8321,8078,7312,977,5257,8465,5068,3453,3096,1651,7906,253,9250,6021,8791,8109,6651,3412,345,4778,5152,4883,7505 +1074,5438,9008,2679,5397,5429,2652,3403,770,9188,4248,2493,4361,8327,9587,707,9525,5913,93,1899,328,2876,3604,673,8576,6908,7659,2544,3359,3883,5273,6587,3065,1749,3223,604,9925,6941,2823,8767,7039,3290,3214,1787,7904,3421,7137,9560,8451,2669,9219,6332,1576,5477,6755,8348,4164,4307,2984,4012,6629,1044,2874,6541,4942,903,1404,9125,5160,8836,4345,2581,460,8438,1538,5507,668,3352,2678,6942 +4295,1176,5596,1521,3061,9868,7037,7129,8933,6659,5947,5063,3653,9447,9245,2679,767,714,116,8558,163,3927,8779,158,5093,2447,5782,3967,1716,931,7772,8164,1117,9244,5783,7776,3846,8862,6014,2330,6947,1777,3112,6008,3491,1906,5952,314,4602,8994,5919,9214,3995,5026,7688,6809,5003,3128,2509,7477,110,8971,3982,8539,2980,4689,6343,5411,2992,5270,5247,9260,2269,7474,1042,7162,5206,1232,4556,4757 +510,3556,5377,1406,5721,4946,2635,7847,4251,8293,8281,6351,4912,287,2870,3380,3948,5322,3840,4738,9563,1906,6298,3234,8959,1562,6297,8835,7861,239,6618,1322,2553,2213,5053,5446,4402,6500,5182,8585,6900,5756,9661,903,5186,7687,5998,7997,8081,8955,4835,6069,2621,1581,732,9564,1082,1853,5442,1342,520,1737,3703,5321,4793,2776,1508,1647,9101,2499,6891,4336,7012,3329,3212,1442,9993,3988,4930,7706 +9444,3401,5891,9716,1228,7107,109,3563,2700,6161,5039,4992,2242,8541,7372,2067,1294,3058,1306,320,8881,5756,9326,411,8650,8824,5495,8282,8397,2000,1228,7817,2099,6473,3571,5994,4447,1299,5991,543,7874,2297,1651,101,2093,3463,9189,6872,6118,872,1008,1779,2805,9084,4048,2123,5877,55,3075,1737,9459,4535,6453,3644,108,5982,4437,5213,1340,6967,9943,5815,669,8074,1838,6979,9132,9315,715,5048 +3327,4030,7177,6336,9933,5296,2621,4785,2755,4832,2512,2118,2244,4407,2170,499,7532,9742,5051,7687,970,6924,3527,4694,5145,1306,2165,5940,2425,8910,3513,1909,6983,346,6377,4304,9330,7203,6605,3709,3346,970,369,9737,5811,4427,9939,3693,8436,5566,1977,3728,2399,3985,8303,2492,5366,9802,9193,7296,1033,5060,9144,2766,1151,7629,5169,5995,58,7619,7565,4208,1713,6279,3209,4908,9224,7409,1325,8540 +6882,1265,1775,3648,4690,959,5837,4520,5394,1378,9485,1360,4018,578,9174,2932,9890,3696,116,1723,1178,9355,7063,1594,1918,8574,7594,7942,1547,6166,7888,354,6932,4651,1010,7759,6905,661,7689,6092,9292,3845,9605,8443,443,8275,5163,7720,7265,6356,7779,1798,1754,5225,6661,1180,8024,5666,88,9153,1840,3508,1193,4445,2648,3538,6243,6375,8107,5902,5423,2520,1122,5015,6113,8859,9370,966,8673,2442 +7338,3423,4723,6533,848,8041,7921,8277,4094,5368,7252,8852,9166,2250,2801,6125,8093,5738,4038,9808,7359,9494,601,9116,4946,2702,5573,2921,9862,1462,1269,2410,4171,2709,7508,6241,7522,615,2407,8200,4189,5492,5649,7353,2590,5203,4274,710,7329,9063,956,8371,3722,4253,4785,1194,4828,4717,4548,940,983,2575,4511,2938,1827,2027,2700,1236,841,5760,1680,6260,2373,3851,1841,4968,1172,5179,7175,3509 +4420,1327,3560,2376,6260,2988,9537,4064,4829,8872,9598,3228,1792,7118,9962,9336,4368,9189,6857,1829,9863,6287,7303,7769,2707,8257,2391,2009,3975,4993,3068,9835,3427,341,8412,2134,4034,8511,6421,3041,9012,2983,7289,100,1355,7904,9186,6920,5856,2008,6545,8331,3655,5011,839,8041,9255,6524,3862,8788,62,7455,3513,5003,8413,3918,2076,7960,6108,3638,6999,3436,1441,4858,4181,1866,8731,7745,3744,1000 +356,8296,8325,1058,1277,4743,3850,2388,6079,6462,2815,5620,8495,5378,75,4324,3441,9870,1113,165,1544,1179,2834,562,6176,2313,6836,8839,2986,9454,5199,6888,1927,5866,8760,320,1792,8296,7898,6121,7241,5886,5814,2815,8336,1576,4314,3109,2572,6011,2086,9061,9403,3947,5487,9731,7281,3159,1819,1334,3181,5844,5114,9898,4634,2531,4412,6430,4262,8482,4546,4555,6804,2607,9421,686,8649,8860,7794,6672 +9870,152,1558,4963,8750,4754,6521,6256,8818,5208,5691,9659,8377,9725,5050,5343,2539,6101,1844,9700,7750,8114,5357,3001,8830,4438,199,9545,8496,43,2078,327,9397,106,6090,8181,8646,6414,7499,5450,4850,6273,5014,4131,7639,3913,6571,8534,9703,4391,7618,445,1320,5,1894,6771,7383,9191,4708,9706,6939,7937,8726,9382,5216,3685,2247,9029,8154,1738,9984,2626,9438,4167,6351,5060,29,1218,1239,4785 +192,5213,8297,8974,4032,6966,5717,1179,6523,4679,9513,1481,3041,5355,9303,9154,1389,8702,6589,7818,6336,3539,5538,3094,6646,6702,6266,2759,4608,4452,617,9406,8064,6379,444,5602,4950,1810,8391,1536,316,8714,1178,5182,5863,5110,5372,4954,1978,2971,5680,4863,2255,4630,5723,2168,538,1692,1319,7540,440,6430,6266,7712,7385,5702,620,641,3136,7350,1478,3155,2820,9109,6261,1122,4470,14,8493,2095 +1046,4301,6082,474,4974,7822,2102,5161,5172,6946,8074,9716,6586,9962,9749,5015,2217,995,5388,4402,7652,6399,6539,1349,8101,3677,1328,9612,7922,2879,231,5887,2655,508,4357,4964,3554,5930,6236,7384,4614,280,3093,9600,2110,7863,2631,6626,6620,68,1311,7198,7561,1768,5139,1431,221,230,2940,968,5283,6517,2146,1646,869,9402,7068,8645,7058,1765,9690,4152,2926,9504,2939,7504,6074,2944,6470,7859 +4659,736,4951,9344,1927,6271,8837,8711,3241,6579,7660,5499,5616,3743,5801,4682,9748,8796,779,1833,4549,8138,4026,775,4170,2432,4174,3741,7540,8017,2833,4027,396,811,2871,1150,9809,2719,9199,8504,1224,540,2051,3519,7982,7367,2761,308,3358,6505,2050,4836,5090,7864,805,2566,2409,6876,3361,8622,5572,5895,3280,441,7893,8105,1634,2929,274,3926,7786,6123,8233,9921,2674,5340,1445,203,4585,3837 +5759,338,7444,7968,7742,3755,1591,4839,1705,650,7061,2461,9230,9391,9373,2413,1213,431,7801,4994,2380,2703,6161,6878,8331,2538,6093,1275,5065,5062,2839,582,1014,8109,3525,1544,1569,8622,7944,2905,6120,1564,1839,5570,7579,1318,2677,5257,4418,5601,7935,7656,5192,1864,5886,6083,5580,6202,8869,1636,7907,4759,9082,5854,3185,7631,6854,5872,5632,5280,1431,2077,9717,7431,4256,8261,9680,4487,4752,4286 +1571,1428,8599,1230,7772,4221,8523,9049,4042,8726,7567,6736,9033,2104,4879,4967,6334,6716,3994,1269,8995,6539,3610,7667,6560,6065,874,848,4597,1711,7161,4811,6734,5723,6356,6026,9183,2586,5636,1092,7779,7923,8747,6887,7505,9909,1792,3233,4526,3176,1508,8043,720,5212,6046,4988,709,5277,8256,3642,1391,5803,1468,2145,3970,6301,7767,2359,8487,9771,8785,7520,856,1605,8972,2402,2386,991,1383,5963 +1822,4824,5957,6511,9868,4113,301,9353,6228,2881,2966,6956,9124,9574,9233,1601,7340,973,9396,540,4747,8590,9535,3650,7333,7583,4806,3593,2738,8157,5215,8472,2284,9473,3906,6982,5505,6053,7936,6074,7179,6688,1564,1103,6860,5839,2022,8490,910,7551,7805,881,7024,1855,9448,4790,1274,3672,2810,774,7623,4223,4850,6071,9975,4935,1915,9771,6690,3846,517,463,7624,4511,614,6394,3661,7409,1395,8127 +8738,3850,9555,3695,4383,2378,87,6256,6740,7682,9546,4255,6105,2000,1851,4073,8957,9022,6547,5189,2487,303,9602,7833,1628,4163,6678,3144,8589,7096,8913,5823,4890,7679,1212,9294,5884,2972,3012,3359,7794,7428,1579,4350,7246,4301,7779,7790,3294,9547,4367,3549,1958,8237,6758,3497,3250,3456,6318,1663,708,7714,6143,6890,3428,6853,9334,7992,591,6449,9786,1412,8500,722,5468,1371,108,3939,4199,2535 +7047,4323,1934,5163,4166,461,3544,2767,6554,203,6098,2265,9078,2075,4644,6641,8412,9183,487,101,7566,5622,1975,5726,2920,5374,7779,5631,3753,3725,2672,3621,4280,1162,5812,345,8173,9785,1525,955,5603,2215,2580,5261,2765,2990,5979,389,3907,2484,1232,5933,5871,3304,1138,1616,5114,9199,5072,7442,7245,6472,4760,6359,9053,7876,2564,9404,3043,9026,2261,3374,4460,7306,2326,966,828,3274,1712,3446 +3975,4565,8131,5800,4570,2306,8838,4392,9147,11,3911,7118,9645,4994,2028,6062,5431,2279,8752,2658,7836,994,7316,5336,7185,3289,1898,9689,2331,5737,3403,1124,2679,3241,7748,16,2724,5441,6640,9368,9081,5618,858,4969,17,2103,6035,8043,7475,2181,939,415,1617,8500,8253,2155,7843,7974,7859,1746,6336,3193,2617,8736,4079,6324,6645,8891,9396,5522,6103,1857,8979,3835,2475,1310,7422,610,8345,7615 +9248,5397,5686,2988,3446,4359,6634,9141,497,9176,6773,7448,1907,8454,916,1596,2241,1626,1384,2741,3649,5362,8791,7170,2903,2475,5325,6451,924,3328,522,90,4813,9737,9557,691,2388,1383,4021,1609,9206,4707,5200,7107,8104,4333,9860,5013,1224,6959,8527,1877,4545,7772,6268,621,4915,9349,5970,706,9583,3071,4127,780,8231,3017,9114,3836,7503,2383,1977,4870,8035,2379,9704,1037,3992,3642,1016,4303 +5093,138,4639,6609,1146,5565,95,7521,9077,2272,974,4388,2465,2650,722,4998,3567,3047,921,2736,7855,173,2065,4238,1048,5,6847,9548,8632,9194,5942,4777,7910,8971,6279,7253,2516,1555,1833,3184,9453,9053,6897,7808,8629,4877,1871,8055,4881,7639,1537,7701,2508,7564,5845,5023,2304,5396,3193,2955,1088,3801,6203,1748,3737,1276,13,4120,7715,8552,3047,2921,106,7508,304,1280,7140,2567,9135,5266 +6237,4607,7527,9047,522,7371,4883,2540,5867,6366,5301,1570,421,276,3361,527,6637,4861,2401,7522,5808,9371,5298,2045,5096,5447,7755,5115,7060,8529,4078,1943,1697,1764,5453,7085,960,2405,739,2100,5800,728,9737,5704,5693,1431,8979,6428,673,7540,6,7773,5857,6823,150,5869,8486,684,5816,9626,7451,5579,8260,3397,5322,6920,1879,2127,2884,5478,4977,9016,6165,6292,3062,5671,5968,78,4619,4763 +9905,7127,9390,5185,6923,3721,9164,9705,4341,1031,1046,5127,7376,6528,3248,4941,1178,7889,3364,4486,5358,9402,9158,8600,1025,874,1839,1783,309,9030,1843,845,8398,1433,7118,70,8071,2877,3904,8866,6722,4299,10,1929,5897,4188,600,1889,3325,2485,6473,4474,7444,6992,4846,6166,4441,2283,2629,4352,7775,1101,2214,9985,215,8270,9750,2740,8361,7103,5930,8664,9690,8302,9267,344,2077,1372,1880,9550 +5825,8517,7769,2405,8204,1060,3603,7025,478,8334,1997,3692,7433,9101,7294,7498,9415,5452,3850,3508,6857,9213,6807,4412,7310,854,5384,686,4978,892,8651,3241,2743,3801,3813,8588,6701,4416,6990,6490,3197,6838,6503,114,8343,5844,8646,8694,65,791,5979,2687,2621,2019,8097,1423,3644,9764,4921,3266,3662,5561,2476,8271,8138,6147,1168,3340,1998,9874,6572,9873,6659,5609,2711,3931,9567,4143,7833,8887 +6223,2099,2700,589,4716,8333,1362,5007,2753,2848,4441,8397,7192,8191,4916,9955,6076,3370,6396,6971,3156,248,3911,2488,4930,2458,7183,5455,170,6809,6417,3390,1956,7188,577,7526,2203,968,8164,479,8699,7915,507,6393,4632,1597,7534,3604,618,3280,6061,9793,9238,8347,568,9645,2070,5198,6482,5000,9212,6655,5961,7513,1323,3872,6170,3812,4146,2736,67,3151,5548,2781,9679,7564,5043,8587,1893,4531 +5826,3690,6724,2121,9308,6986,8106,6659,2142,1642,7170,2877,5757,6494,8026,6571,8387,9961,6043,9758,9607,6450,8631,8334,7359,5256,8523,2225,7487,1977,9555,8048,5763,2414,4948,4265,2427,8978,8088,8841,9208,9601,5810,9398,8866,9138,4176,5875,7212,3272,6759,5678,7649,4922,5422,1343,8197,3154,3600,687,1028,4579,2084,9467,4492,7262,7296,6538,7657,7134,2077,1505,7332,6890,8964,4879,7603,7400,5973,739 +1861,1613,4879,1884,7334,966,2000,7489,2123,4287,1472,3263,4726,9203,1040,4103,6075,6049,330,9253,4062,4268,1635,9960,577,1320,3195,9628,1030,4092,4979,6474,6393,2799,6967,8687,7724,7392,9927,2085,3200,6466,8702,265,7646,8665,7986,7266,4574,6587,612,2724,704,3191,8323,9523,3002,704,5064,3960,8209,2027,2758,8393,4875,4641,9584,6401,7883,7014,768,443,5490,7506,1852,2005,8850,5776,4487,4269 +4052,6687,4705,7260,6645,6715,3706,5504,8672,2853,1136,8187,8203,4016,871,1809,1366,4952,9294,5339,6872,2645,6083,7874,3056,5218,7485,8796,7401,3348,2103,426,8572,4163,9171,3176,948,7654,9344,3217,1650,5580,7971,2622,76,2874,880,2034,9929,1546,2659,5811,3754,7096,7436,9694,9960,7415,2164,953,2360,4194,2397,1047,2196,6827,575,784,2675,8821,6802,7972,5996,6699,2134,7577,2887,1412,4349,4380 +4629,2234,6240,8132,7592,3181,6389,1214,266,1910,2451,8784,2790,1127,6932,1447,8986,2492,5476,397,889,3027,7641,5083,5776,4022,185,3364,5701,2442,2840,4160,9525,4828,6602,2614,7447,3711,4505,7745,8034,6514,4907,2605,7753,6958,7270,6936,3006,8968,439,2326,4652,3085,3425,9863,5049,5361,8688,297,7580,8777,7916,6687,8683,7141,306,9569,2384,1500,3346,4601,7329,9040,6097,2727,6314,4501,4974,2829 +8316,4072,2025,6884,3027,1808,5714,7624,7880,8528,4205,8686,7587,3230,1139,7273,6163,6986,3914,9309,1464,9359,4474,7095,2212,7302,2583,9462,7532,6567,1606,4436,8981,5612,6796,4385,5076,2007,6072,3678,8331,1338,3299,8845,4783,8613,4071,1232,6028,2176,3990,2148,3748,103,9453,538,6745,9110,926,3125,473,5970,8728,7072,9062,1404,1317,5139,9862,6496,6062,3338,464,1600,2532,1088,8232,7739,8274,3873 +2341,523,7096,8397,8301,6541,9844,244,4993,2280,7689,4025,4196,5522,7904,6048,2623,9258,2149,9461,6448,8087,7245,1917,8340,7127,8466,5725,6996,3421,5313,512,9164,9837,9794,8369,4185,1488,7210,1524,1016,4620,9435,2478,7765,8035,697,6677,3724,6988,5853,7662,3895,9593,1185,4727,6025,5734,7665,3070,138,8469,6748,6459,561,7935,8646,2378,462,7755,3115,9690,8877,3946,2728,8793,244,6323,8666,4271 +6430,2406,8994,56,1267,3826,9443,7079,7579,5232,6691,3435,6718,5698,4144,7028,592,2627,217,734,6194,8156,9118,58,2640,8069,4127,3285,694,3197,3377,4143,4802,3324,8134,6953,7625,3598,3584,4289,7065,3434,2106,7132,5802,7920,9060,7531,3321,1725,1067,3751,444,5503,6785,7937,6365,4803,198,6266,8177,1470,6390,1606,2904,7555,9834,8667,2033,1723,5167,1666,8546,8152,473,4475,6451,7947,3062,3281 +2810,3042,7759,1741,2275,2609,7676,8640,4117,1958,7500,8048,1757,3954,9270,1971,4796,2912,660,5511,3553,1012,5757,4525,6084,7198,8352,5775,7726,8591,7710,9589,3122,4392,6856,5016,749,2285,3356,7482,9956,7348,2599,8944,495,3462,3578,551,4543,7207,7169,7796,1247,4278,6916,8176,3742,8385,2310,1345,8692,2667,4568,1770,8319,3585,4920,3890,4928,7343,5385,9772,7947,8786,2056,9266,3454,2807,877,2660 +6206,8252,5928,5837,4177,4333,207,7934,5581,9526,8906,1498,8411,2984,5198,5134,2464,8435,8514,8674,3876,599,5327,826,2152,4084,2433,9327,9697,4800,2728,3608,3849,3861,3498,9943,1407,3991,7191,9110,5666,8434,4704,6545,5944,2357,1163,4995,9619,6754,4200,9682,6654,4862,4744,5953,6632,1054,293,9439,8286,2255,696,8709,1533,1844,6441,430,1999,6063,9431,7018,8057,2920,6266,6799,356,3597,4024,6665 +3847,6356,8541,7225,2325,2946,5199,469,5450,7508,2197,9915,8284,7983,6341,3276,3321,16,1321,7608,5015,3362,8491,6968,6818,797,156,2575,706,9516,5344,5457,9210,5051,8099,1617,9951,7663,8253,9683,2670,1261,4710,1068,8753,4799,1228,2621,3275,6188,4699,1791,9518,8701,5932,4275,6011,9877,2933,4182,6059,2930,6687,6682,9771,654,9437,3169,8596,1827,5471,8909,2352,123,4394,3208,8756,5513,6917,2056 +5458,8173,3138,3290,4570,4892,3317,4251,9699,7973,1163,1935,5477,6648,9614,5655,9592,975,9118,2194,7322,8248,8413,3462,8560,1907,7810,6650,7355,2939,4973,6894,3933,3784,3200,2419,9234,4747,2208,2207,1945,2899,1407,6145,8023,3484,5688,7686,2737,3828,3704,9004,5190,9740,8643,8650,5358,4426,1522,1707,3613,9887,6956,2447,2762,833,1449,9489,2573,1080,4167,3456,6809,2466,227,7125,2759,6250,6472,8089 +3266,7025,9756,3914,1265,9116,7723,9788,6805,5493,2092,8688,6592,9173,4431,4028,6007,7131,4446,4815,3648,6701,759,3312,8355,4485,4187,5188,8746,7759,3528,2177,5243,8379,3838,7233,4607,9187,7216,2190,6967,2920,6082,7910,5354,3609,8958,6949,7731,494,8753,8707,1523,4426,3543,7085,647,6771,9847,646,5049,824,8417,5260,2730,5702,2513,9275,4279,2767,8684,1165,9903,4518,55,9682,8963,6005,2102,6523 +1998,8731,936,1479,5259,7064,4085,91,7745,7136,3773,3810,730,8255,2705,2653,9790,6807,2342,355,9344,2668,3690,2028,9679,8102,574,4318,6481,9175,5423,8062,2867,9657,7553,3442,3920,7430,3945,7639,3714,3392,2525,4995,4850,2867,7951,9667,486,9506,9888,781,8866,1702,3795,90,356,1483,4200,2131,6969,5931,486,6880,4404,1084,5169,4910,6567,8335,4686,5043,2614,3352,2667,4513,6472,7471,5720,1616 +8878,1613,1716,868,1906,2681,564,665,5995,2474,7496,3432,9491,9087,8850,8287,669,823,347,6194,2264,2592,7871,7616,8508,4827,760,2676,4660,4881,7572,3811,9032,939,4384,929,7525,8419,5556,9063,662,8887,7026,8534,3111,1454,2082,7598,5726,6687,9647,7608,73,3014,5063,670,5461,5631,3367,9796,8475,7908,5073,1565,5008,5295,4457,1274,4788,1728,338,600,8415,8535,9351,7750,6887,5845,1741,125 +3637,6489,9634,9464,9055,2413,7824,9517,7532,3577,7050,6186,6980,9365,9782,191,870,2497,8498,2218,2757,5420,6468,586,3320,9230,1034,1393,9886,5072,9391,1178,8464,8042,6869,2075,8275,3601,7715,9470,8786,6475,8373,2159,9237,2066,3264,5000,679,355,3069,4073,494,2308,5512,4334,9438,8786,8637,9774,1169,1949,6594,6072,4270,9158,7916,5752,6794,9391,6301,5842,3285,2141,3898,8027,4310,8821,7079,1307 +8497,6681,4732,7151,7060,5204,9030,7157,833,5014,8723,3207,9796,9286,4913,119,5118,7650,9335,809,3675,2597,5144,3945,5090,8384,187,4102,1260,2445,2792,4422,8389,9290,50,1765,1521,6921,8586,4368,1565,5727,7855,2003,4834,9897,5911,8630,5070,1330,7692,7557,7980,6028,5805,9090,8265,3019,3802,698,9149,5748,1965,9658,4417,5994,5584,8226,2937,272,5743,1278,5698,8736,2595,6475,5342,6596,1149,6920 +8188,8009,9546,6310,8772,2500,9846,6592,6872,3857,1307,8125,7042,1544,6159,2330,643,4604,7899,6848,371,8067,2062,3200,7295,1857,9505,6936,384,2193,2190,301,8535,5503,1462,7380,5114,4824,8833,1763,4974,8711,9262,6698,3999,2645,6937,7747,1128,2933,3556,7943,2885,3122,9105,5447,418,2899,5148,3699,9021,9501,597,4084,175,1621,1,1079,6067,5812,4326,9914,6633,5394,4233,6728,9084,1864,5863,1225 +9935,8793,9117,1825,9542,8246,8437,3331,9128,9675,6086,7075,319,1334,7932,3583,7167,4178,1726,7720,695,8277,7887,6359,5912,1719,2780,8529,1359,2013,4498,8072,1129,9998,1147,8804,9405,6255,1619,2165,7491,1,8882,7378,3337,503,5758,4109,3577,985,3200,7615,8058,5032,1080,6410,6873,5496,1466,2412,9885,5904,4406,3605,8770,4361,6205,9193,1537,9959,214,7260,9566,1685,100,4920,7138,9819,5637,976 +3466,9854,985,1078,7222,8888,5466,5379,3578,4540,6853,8690,3728,6351,7147,3134,6921,9692,857,3307,4998,2172,5783,3931,9417,2541,6299,13,787,2099,9131,9494,896,8600,1643,8419,7248,2660,2609,8579,91,6663,5506,7675,1947,6165,4286,1972,9645,3805,1663,1456,8853,5705,9889,7489,1107,383,4044,2969,3343,152,7805,4980,9929,5033,1737,9953,7197,9158,4071,1324,473,9676,3984,9680,3606,8160,7384,5432 +1005,4512,5186,3953,2164,3372,4097,3247,8697,3022,9896,4101,3871,6791,3219,2742,4630,6967,7829,5991,6134,1197,1414,8923,8787,1394,8852,5019,7768,5147,8004,8825,5062,9625,7988,1110,3992,7984,9966,6516,6251,8270,421,3723,1432,4830,6935,8095,9059,2214,6483,6846,3120,1587,6201,6691,9096,9627,6671,4002,3495,9939,7708,7465,5879,6959,6634,3241,3401,2355,9061,2611,7830,3941,2177,2146,5089,7079,519,6351 +7280,8586,4261,2831,7217,3141,9994,9940,5462,2189,4005,6942,9848,5350,8060,6665,7519,4324,7684,657,9453,9296,2944,6843,7499,7847,1728,9681,3906,6353,5529,2822,3355,3897,7724,4257,7489,8672,4356,3983,1948,6892,7415,4153,5893,4190,621,1736,4045,9532,7701,3671,1211,1622,3176,4524,9317,7800,5638,6644,6943,5463,3531,2821,1347,5958,3436,1438,2999,994,850,4131,2616,1549,3465,5946,690,9273,6954,7991 +9517,399,3249,2596,7736,2142,1322,968,7350,1614,468,3346,3265,7222,6086,1661,5317,2582,7959,4685,2807,2917,1037,5698,1529,3972,8716,2634,3301,3412,8621,743,8001,4734,888,7744,8092,3671,8941,1487,5658,7099,2781,99,1932,4443,4756,4652,9328,1581,7855,4312,5976,7255,6480,3996,2748,1973,9731,4530,2790,9417,7186,5303,3557,351,7182,9428,1342,9020,7599,1392,8304,2070,9138,7215,2008,9937,1106,7110 +7444,769,9688,632,1571,6820,8743,4338,337,3366,3073,1946,8219,104,4210,6986,249,5061,8693,7960,6546,1004,8857,5997,9352,4338,6105,5008,2556,6518,6694,4345,3727,7956,20,3954,8652,4424,9387,2035,8358,5962,5304,5194,8650,8282,1256,1103,2138,6679,1985,3653,2770,2433,4278,615,2863,1715,242,3790,2636,6998,3088,1671,2239,957,5411,4595,6282,2881,9974,2401,875,7574,2987,4587,3147,6766,9885,2965 +3287,3016,3619,6818,9073,6120,5423,557,2900,2015,8111,3873,1314,4189,1846,4399,7041,7583,2427,2864,3525,5002,2069,748,1948,6015,2684,438,770,8367,1663,7887,7759,1885,157,7770,4520,4878,3857,1137,3525,3050,6276,5569,7649,904,4533,7843,2199,5648,7628,9075,9441,3600,7231,2388,5640,9096,958,3058,584,5899,8150,1181,9616,1098,8162,6819,8171,1519,1140,7665,8801,2632,1299,9192,707,9955,2710,7314 +1772,2963,7578,3541,3095,1488,7026,2634,6015,4633,4370,2762,1650,2174,909,8158,2922,8467,4198,4280,9092,8856,8835,5457,2790,8574,9742,5054,9547,4156,7940,8126,9824,7340,8840,6574,3547,1477,3014,6798,7134,435,9484,9859,3031,4,1502,4133,1738,1807,4825,463,6343,9701,8506,9822,9555,8688,8168,3467,3234,6318,1787,5591,419,6593,7974,8486,9861,6381,6758,194,3061,4315,2863,4665,3789,2201,1492,4416 +126,8927,6608,5682,8986,6867,1715,6076,3159,788,3140,4744,830,9253,5812,5021,7616,8534,1546,9590,1101,9012,9821,8132,7857,4086,1069,7491,2988,1579,2442,4321,2149,7642,6108,250,6086,3167,24,9528,7663,2685,1220,9196,1397,5776,1577,1730,5481,977,6115,199,6326,2183,3767,5928,5586,7561,663,8649,9688,949,5913,9160,1870,5764,9887,4477,6703,1413,4995,5494,7131,2192,8969,7138,3997,8697,646,1028 +8074,1731,8245,624,4601,8706,155,8891,309,2552,8208,8452,2954,3124,3469,4246,3352,1105,4509,8677,9901,4416,8191,9283,5625,7120,2952,8881,7693,830,4580,8228,9459,8611,4499,1179,4988,1394,550,2336,6089,6872,269,7213,1848,917,6672,4890,656,1478,6536,3165,4743,4990,1176,6211,7207,5284,9730,4738,1549,4986,4942,8645,3698,9429,1439,2175,6549,3058,6513,1574,6988,8333,3406,5245,5431,7140,7085,6407 +7845,4694,2530,8249,290,5948,5509,1588,5940,4495,5866,5021,4626,3979,3296,7589,4854,1998,5627,3926,8346,6512,9608,1918,7070,4747,4182,2858,2766,4606,6269,4107,8982,8568,9053,4244,5604,102,2756,727,5887,2566,7922,44,5986,621,1202,374,6988,4130,3627,6744,9443,4568,1398,8679,397,3928,9159,367,2917,6127,5788,3304,8129,911,2669,1463,9749,264,4478,8940,1109,7309,2462,117,4692,7724,225,2312 +4164,3637,2000,941,8903,39,3443,7172,1031,3687,4901,8082,4945,4515,7204,9310,9349,9535,9940,218,1788,9245,2237,1541,5670,6538,6047,5553,9807,8101,1925,8714,445,8332,7309,6830,5786,5736,7306,2710,3034,1838,7969,6318,7912,2584,2080,7437,6705,2254,7428,820,782,9861,7596,3842,3631,8063,5240,6666,394,4565,7865,4895,9890,6028,6117,4724,9156,4473,4552,602,470,6191,4927,5387,884,3146,1978,3000 +4258,6880,1696,3582,5793,4923,2119,1155,9056,9698,6603,3768,5514,9927,9609,6166,6566,4536,4985,4934,8076,9062,6741,6163,7399,4562,2337,5600,2919,9012,8459,1308,6072,1225,9306,8818,5886,7243,7365,8792,6007,9256,6699,7171,4230,7002,8720,7839,4533,1671,478,7774,1607,2317,5437,4705,7886,4760,6760,7271,3081,2997,3088,7675,6208,3101,6821,6840,122,9633,4900,2067,8546,4549,2091,7188,5605,8599,6758,5229 +7854,5243,9155,3556,8812,7047,2202,1541,5993,4600,4760,713,434,7911,7426,7414,8729,322,803,7960,7563,4908,6285,6291,736,3389,9339,4132,8701,7534,5287,3646,592,3065,7582,2592,8755,6068,8597,1982,5782,1894,2900,6236,4039,6569,3037,5837,7698,700,7815,2491,7272,5878,3083,6778,6639,3589,5010,8313,2581,6617,5869,8402,6808,2951,2321,5195,497,2190,6187,1342,1316,4453,7740,4154,2959,1781,1482,8256 +7178,2046,4419,744,8312,5356,6855,8839,319,2962,5662,47,6307,8662,68,4813,567,2712,9931,1678,3101,8227,6533,4933,6656,92,5846,4780,6256,6361,4323,9985,1231,2175,7178,3034,9744,6155,9165,7787,5836,9318,7860,9644,8941,6480,9443,8188,5928,161,6979,2352,5628,6991,1198,8067,5867,6620,3778,8426,2994,3122,3124,6335,3918,8897,2655,9670,634,1088,1576,8935,7255,474,8166,7417,9547,2886,5560,3842 +6957,3111,26,7530,7143,1295,1744,6057,3009,1854,8098,5405,2234,4874,9447,2620,9303,27,7410,969,40,2966,5648,7596,8637,4238,3143,3679,7187,690,9980,7085,7714,9373,5632,7526,6707,3951,9734,4216,2146,3602,5371,6029,3039,4433,4855,4151,1449,3376,8009,7240,7027,4602,2947,9081,4045,8424,9352,8742,923,2705,4266,3232,2264,6761,363,2651,3383,7770,6730,7856,7340,9679,2158,610,4471,4608,910,6241 +4417,6756,1013,8797,658,8809,5032,8703,7541,846,3357,2920,9817,1745,9980,7593,4667,3087,779,3218,6233,5568,4296,2289,2654,7898,5021,9461,5593,8214,9173,4203,2271,7980,2983,5952,9992,8399,3468,1776,3188,9314,1720,6523,2933,621,8685,5483,8986,6163,3444,9539,4320,155,3992,2828,2150,6071,524,2895,5468,8063,1210,3348,9071,4862,483,9017,4097,6186,9815,3610,5048,1644,1003,9865,9332,2145,1944,2213 +9284,3803,4920,1927,6706,4344,7383,4786,9890,2010,5228,1224,3158,6967,8580,8990,8883,5213,76,8306,2031,4980,5639,9519,7184,5645,7769,3259,8077,9130,1317,3096,9624,3818,1770,695,2454,947,6029,3474,9938,3527,5696,4760,7724,7738,2848,6442,5767,6845,8323,4131,2859,7595,2500,4815,3660,9130,8580,7016,8231,4391,8369,3444,4069,4021,556,6154,627,2778,1496,4206,6356,8434,8491,3816,8231,3190,5575,1015 +3787,7572,1788,6803,5641,6844,1961,4811,8535,9914,9999,1450,8857,738,4662,8569,6679,2225,7839,8618,286,2648,5342,2294,3205,4546,176,8705,3741,6134,8324,8021,7004,5205,7032,6637,9442,5539,5584,4819,5874,5807,8589,6871,9016,983,1758,3786,1519,6241,185,8398,495,3370,9133,3051,4549,9674,7311,9738,3316,9383,2658,2776,9481,7558,619,3943,3324,6491,4933,153,9738,4623,912,3595,7771,7939,1219,4405 +2650,3883,4154,5809,315,7756,4430,1788,4451,1631,6461,7230,6017,5751,138,588,5282,2442,9110,9035,6349,2515,1570,6122,4192,4174,3530,1933,4186,4420,4609,5739,4135,2963,6308,1161,8809,8619,2796,3819,6971,8228,4188,1492,909,8048,2328,6772,8467,7671,9068,2226,7579,6422,7056,8042,3296,2272,3006,2196,7320,3238,3490,3102,37,1293,3212,4767,5041,8773,5794,4456,6174,7279,7054,2835,7053,9088,790,6640 +3101,1057,7057,3826,6077,1025,2955,1224,1114,6729,5902,4698,6239,7203,9423,1804,4417,6686,1426,6941,8071,1029,4985,9010,6122,6597,1622,1574,3513,1684,7086,5505,3244,411,9638,4150,907,9135,829,981,1707,5359,8781,9751,5,9131,3973,7159,1340,6955,7514,7993,6964,8198,1933,2797,877,3993,4453,8020,9349,8646,2779,8679,2961,3547,3374,3510,1129,3568,2241,2625,9138,5974,8206,7669,7678,1833,8700,4480 +4865,9912,8038,8238,782,3095,8199,1127,4501,7280,2112,2487,3626,2790,9432,1475,6312,8277,4827,2218,5806,7132,8752,1468,7471,6386,739,8762,8323,8120,5169,9078,9058,3370,9560,7987,8585,8531,5347,9312,1058,4271,1159,5286,5404,6925,8606,9204,7361,2415,560,586,4002,2644,1927,2824,768,4409,2942,3345,1002,808,4941,6267,7979,5140,8643,7553,9438,7320,4938,2666,4609,2778,8158,6730,3748,3867,1866,7181 +171,3771,7134,8927,4778,2913,3326,2004,3089,7853,1378,1729,4777,2706,9578,1360,5693,3036,1851,7248,2403,2273,8536,6501,9216,613,9671,7131,7719,6425,773,717,8803,160,1114,7554,7197,753,4513,4322,8499,4533,2609,4226,8710,6627,644,9666,6260,4870,5744,7385,6542,6203,7703,6130,8944,5589,2262,6803,6381,7414,6888,5123,7320,9392,9061,6780,322,8975,7050,5089,1061,2260,3199,1150,1865,5386,9699,6501 +3744,8454,6885,8277,919,1923,4001,6864,7854,5519,2491,6057,8794,9645,1776,5714,9786,9281,7538,6916,3215,395,2501,9618,4835,8846,9708,2813,3303,1794,8309,7176,2206,1602,1838,236,4593,2245,8993,4017,10,8215,6921,5206,4023,5932,6997,7801,262,7640,3107,8275,4938,7822,2425,3223,3886,2105,8700,9526,2088,8662,8034,7004,5710,2124,7164,3574,6630,9980,4242,2901,9471,1491,2117,4562,1130,9086,4117,6698 +2810,2280,2331,1170,4554,4071,8387,1215,2274,9848,6738,1604,7281,8805,439,1298,8318,7834,9426,8603,6092,7944,1309,8828,303,3157,4638,4439,9175,1921,4695,7716,1494,1015,1772,5913,1127,1952,1950,8905,4064,9890,385,9357,7945,5035,7082,5369,4093,6546,5187,5637,2041,8946,1758,7111,6566,1027,1049,5148,7224,7248,296,6169,375,1656,7993,2816,3717,4279,4675,1609,3317,42,6201,3100,3144,163,9530,4531 +7096,6070,1009,4988,3538,5801,7149,3063,2324,2912,7911,7002,4338,7880,2481,7368,3516,2016,7556,2193,1388,3865,8125,4637,4096,8114,750,3144,1938,7002,9343,4095,1392,4220,3455,6969,9647,1321,9048,1996,1640,6626,1788,314,9578,6630,2813,6626,4981,9908,7024,4355,3201,3521,3864,3303,464,1923,595,9801,3391,8366,8084,9374,1041,8807,9085,1892,9431,8317,9016,9221,8574,9981,9240,5395,2009,6310,2854,9255 +8830,3145,2960,9615,8220,6061,3452,2918,6481,9278,2297,3385,6565,7066,7316,5682,107,7646,4466,68,1952,9603,8615,54,7191,791,6833,2560,693,9733,4168,570,9127,9537,1925,8287,5508,4297,8452,8795,6213,7994,2420,4208,524,5915,8602,8330,2651,8547,6156,1812,6271,7991,9407,9804,1553,6866,1128,2119,4691,9711,8315,5879,9935,6900,482,682,4126,1041,428,6247,3720,5882,7526,2582,4327,7725,3503,2631 +2738,9323,721,7434,1453,6294,2957,3786,5722,6019,8685,4386,3066,9057,6860,499,5315,3045,5194,7111,3137,9104,941,586,3066,755,4177,8819,7040,5309,3583,3897,4428,7788,4721,7249,6559,7324,825,7311,3760,6064,6070,9672,4882,584,1365,9739,9331,5783,2624,7889,1604,1303,1555,7125,8312,425,8936,3233,7724,1480,403,7440,1784,1754,4721,1569,652,3893,4574,5692,9730,4813,9844,8291,9199,7101,3391,8914 +6044,2928,9332,3328,8588,447,3830,1176,3523,2705,8365,6136,5442,9049,5526,8575,8869,9031,7280,706,2794,8814,5767,4241,7696,78,6570,556,5083,1426,4502,3336,9518,2292,1885,3740,3153,9348,9331,8051,2759,5407,9028,7840,9255,831,515,2612,9747,7435,8964,4971,2048,4900,5967,8271,1719,9670,2810,6777,1594,6367,6259,8316,3815,1689,6840,9437,4361,822,9619,3065,83,6344,7486,8657,8228,9635,6932,4864 +8478,4777,6334,4678,7476,4963,6735,3096,5860,1405,5127,7269,7793,4738,227,9168,2996,8928,765,733,1276,7677,6258,1528,9558,3329,302,8901,1422,8277,6340,645,9125,8869,5952,141,8141,1816,9635,4025,4184,3093,83,2344,2747,9352,7966,1206,1126,1826,218,7939,2957,2729,810,8752,5247,4174,4038,8884,7899,9567,301,5265,5752,7524,4381,1669,3106,8270,6228,6373,754,2547,4240,2313,5514,3022,1040,9738 +2265,8192,1763,1369,8469,8789,4836,52,1212,6690,5257,8918,6723,6319,378,4039,2421,8555,8184,9577,1432,7139,8078,5452,9628,7579,4161,7490,5159,8559,1011,81,478,5840,1964,1334,6875,8670,9900,739,1514,8692,522,9316,6955,1345,8132,2277,3193,9773,3923,4177,2183,1236,6747,6575,4874,6003,6409,8187,745,8776,9440,7543,9825,2582,7381,8147,7236,5185,7564,6125,218,7991,6394,391,7659,7456,5128,5294 +2132,8992,8160,5782,4420,3371,3798,5054,552,5631,7546,4716,1332,6486,7892,7441,4370,6231,4579,2121,8615,1145,9391,1524,1385,2400,9437,2454,7896,7467,2928,8400,3299,4025,7458,4703,7206,6358,792,6200,725,4275,4136,7390,5984,4502,7929,5085,8176,4600,119,3568,76,9363,6943,2248,9077,9731,6213,5817,6729,4190,3092,6910,759,2682,8380,1254,9604,3011,9291,5329,9453,9746,2739,6522,3765,5634,1113,5789 +5304,5499,564,2801,679,2653,1783,3608,7359,7797,3284,796,3222,437,7185,6135,8571,2778,7488,5746,678,6140,861,7750,803,9859,9918,2425,3734,2698,9005,4864,9818,6743,2475,132,9486,3825,5472,919,292,4411,7213,7699,6435,9019,6769,1388,802,2124,1345,8493,9487,8558,7061,8777,8833,2427,2238,5409,4957,8503,3171,7622,5779,6145,2417,5873,5563,5693,9574,9491,1937,7384,4563,6842,5432,2751,3406,7981 diff --git a/project_euler/problem_082/sol1.py b/project_euler/problem_082/sol1.py new file mode 100644 index 000000000000..7b50dc887719 --- /dev/null +++ b/project_euler/problem_082/sol1.py @@ -0,0 +1,65 @@ +""" +Project Euler Problem 82: https://projecteuler.net/problem=82 + +The minimal path sum in the 5 by 5 matrix below, by starting in any cell +in the left column and finishing in any cell in the right column, +and only moving up, down, and right, is indicated in red and bold; +the sum is equal to 994. + + 131 673 [234] [103] [18] + [201] [96] [342] 965 150 + 630 803 746 422 111 + 537 699 497 121 956 + 805 732 524 37 331 + +Find the minimal path sum from the left column to the right column in matrix.txt +(https://projecteuler.net/project/resources/p082_matrix.txt) +(right click and "Save Link/Target As..."), +a 31K text file containing an 80 by 80 matrix. +""" + +import os + + +def solution(filename: str = "input.txt") -> int: + """ + Returns the minimal path sum in the matrix from the file, by starting in any cell + in the left column and finishing in any cell in the right column, + and only moving up, down, and right + + >>> solution("test_matrix.txt") + 994 + """ + + with open(os.path.join(os.path.dirname(__file__), filename)) as input_file: + matrix = [ + [int(element) for element in line.split(",")] + for line in input_file.readlines() + ] + + rows = len(matrix) + cols = len(matrix[0]) + + minimal_path_sums = [[-1 for _ in range(cols)] for _ in range(rows)] + for i in range(rows): + minimal_path_sums[i][0] = matrix[i][0] + + for j in range(1, cols): + for i in range(rows): + minimal_path_sums[i][j] = minimal_path_sums[i][j - 1] + matrix[i][j] + + for i in range(1, rows): + minimal_path_sums[i][j] = min( + minimal_path_sums[i][j], minimal_path_sums[i - 1][j] + matrix[i][j] + ) + + for i in range(rows - 2, -1, -1): + minimal_path_sums[i][j] = min( + minimal_path_sums[i][j], minimal_path_sums[i + 1][j] + matrix[i][j] + ) + + return min(minimal_path_sums_row[-1] for minimal_path_sums_row in minimal_path_sums) + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_082/test_matrix.txt b/project_euler/problem_082/test_matrix.txt new file mode 100644 index 000000000000..76167d9e7fc1 --- /dev/null +++ b/project_euler/problem_082/test_matrix.txt @@ -0,0 +1,5 @@ +131,673,234,103,18 +201,96,342,965,150 +630,803,746,422,111 +537,699,497,121,956 +805,732,524,37,331 diff --git a/project_euler/problem_085/sol1.py b/project_euler/problem_085/sol1.py index d0f29796498c..d0b361ee750d 100644 --- a/project_euler/problem_085/sol1.py +++ b/project_euler/problem_085/sol1.py @@ -44,6 +44,7 @@ Reference: https://en.wikipedia.org/wiki/Triangular_number https://en.wikipedia.org/wiki/Quadratic_formula """ + from __future__ import annotations from math import ceil, floor, sqrt diff --git a/project_euler/problem_086/sol1.py b/project_euler/problem_086/sol1.py index 064af215c049..cbd2b648e0ac 100644 --- a/project_euler/problem_086/sol1.py +++ b/project_euler/problem_086/sol1.py @@ -66,7 +66,6 @@ """ - from math import sqrt diff --git a/project_euler/problem_089/__init__.py b/project_euler/problem_089/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_089/__init__.py +++ b/project_euler/problem_089/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_091/sol1.py b/project_euler/problem_091/sol1.py index 6c9aa3fa6c70..7db98fca0049 100644 --- a/project_euler/problem_091/sol1.py +++ b/project_euler/problem_091/sol1.py @@ -11,7 +11,6 @@ Given that 0 ≤ x1, y1, x2, y2 ≤ 50, how many right triangles can be formed? """ - from itertools import combinations, product diff --git a/project_euler/problem_092/sol1.py b/project_euler/problem_092/sol1.py index 8d3f0c9ddd7b..3e45e82207a7 100644 --- a/project_euler/problem_092/sol1.py +++ b/project_euler/problem_092/sol1.py @@ -68,7 +68,7 @@ def chain(number: int) -> bool: """ if CHAINS[number - 1] is not None: - return CHAINS[number - 1] # type: ignore + return CHAINS[number - 1] # type: ignore[return-value] number_chain = chain(next_number(number)) CHAINS[number - 1] = number_chain diff --git a/project_euler/problem_094/__init__.py b/project_euler/problem_094/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_094/sol1.py b/project_euler/problem_094/sol1.py new file mode 100644 index 000000000000..a41292fe26fd --- /dev/null +++ b/project_euler/problem_094/sol1.py @@ -0,0 +1,44 @@ +""" +Project Euler Problem 94: https://projecteuler.net/problem=94 + +It is easily proved that no equilateral triangle exists with integral length sides and +integral area. However, the almost equilateral triangle 5-5-6 has an area of 12 square +units. + +We shall define an almost equilateral triangle to be a triangle for which two sides are +equal and the third differs by no more than one unit. + +Find the sum of the perimeters of all almost equilateral triangles with integral side +lengths and area and whose perimeters do not exceed one billion (1,000,000,000). +""" + + +def solution(max_perimeter: int = 10**9) -> int: + """ + Returns the sum of the perimeters of all almost equilateral triangles with integral + side lengths and area and whose perimeters do not exceed max_perimeter + + >>> solution(20) + 16 + """ + + prev_value = 1 + value = 2 + + perimeters_sum = 0 + i = 0 + perimeter = 0 + while perimeter <= max_perimeter: + perimeters_sum += perimeter + + prev_value += 2 * value + value += prev_value + + perimeter = 2 * value + 2 if i % 2 == 0 else 2 * value - 2 + i += 1 + + return perimeters_sum + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_097/__init__.py b/project_euler/problem_097/__init__.py index 792d6005489e..e69de29bb2d1 100644 --- a/project_euler/problem_097/__init__.py +++ b/project_euler/problem_097/__init__.py @@ -1 +0,0 @@ -# diff --git a/project_euler/problem_097/sol1.py b/project_euler/problem_097/sol1.py index 2807e893ded0..a349f3a1dbc9 100644 --- a/project_euler/problem_097/sol1.py +++ b/project_euler/problem_097/sol1.py @@ -1,7 +1,7 @@ """ The first known prime found to exceed one million digits was discovered in 1999, -and is a Mersenne prime of the form 2**6972593 − 1; it contains exactly 2,098,960 -digits. Subsequently other Mersenne primes, of the form 2**p − 1, have been found +and is a Mersenne prime of the form 2**6972593 - 1; it contains exactly 2,098,960 +digits. Subsequently other Mersenne primes, of the form 2**p - 1, have been found which contain more digits. However, in 2004 there was found a massive non-Mersenne prime which contains 2,357,207 digits: (28433 * (2 ** 7830457 + 1)). diff --git a/project_euler/problem_100/__init__.py b/project_euler/problem_100/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_100/sol1.py b/project_euler/problem_100/sol1.py new file mode 100644 index 000000000000..367378e7ab17 --- /dev/null +++ b/project_euler/problem_100/sol1.py @@ -0,0 +1,48 @@ +""" +Project Euler Problem 100: https://projecteuler.net/problem=100 + +If a box contains twenty-one coloured discs, composed of fifteen blue discs and +six red discs, and two discs were taken at random, it can be seen that +the probability of taking two blue discs, P(BB) = (15/21) x (14/20) = 1/2. + +The next such arrangement, for which there is exactly 50% chance of taking two blue +discs at random, is a box containing eighty-five blue discs and thirty-five red discs. + +By finding the first arrangement to contain over 10^12 = 1,000,000,000,000 discs +in total, determine the number of blue discs that the box would contain. +""" + + +def solution(min_total: int = 10**12) -> int: + """ + Returns the number of blue discs for the first arrangement to contain + over min_total discs in total + + >>> solution(2) + 3 + + >>> solution(4) + 15 + + >>> solution(21) + 85 + """ + + prev_numerator = 1 + prev_denominator = 0 + + numerator = 1 + denominator = 1 + + while numerator <= 2 * min_total - 1: + prev_numerator += 2 * numerator + numerator += 2 * prev_numerator + + prev_denominator += 2 * denominator + denominator += 2 * prev_denominator + + return (denominator + 1) // 2 + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_101/sol1.py b/project_euler/problem_101/sol1.py index d5c503af796a..2d209333cf31 100644 --- a/project_euler/problem_101/sol1.py +++ b/project_euler/problem_101/sol1.py @@ -41,6 +41,7 @@ Find the sum of FITs for the BOPs. """ + from __future__ import annotations from collections.abc import Callable diff --git a/project_euler/problem_102/sol1.py b/project_euler/problem_102/sol1.py index 4f6e6361e3e8..85fe5eac1e22 100644 --- a/project_euler/problem_102/sol1.py +++ b/project_euler/problem_102/sol1.py @@ -18,6 +18,7 @@ NOTE: The first two examples in the file represent the triangles in the example given above. """ + from __future__ import annotations from pathlib import Path diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 60fd6fe99adb..a0267faa6a38 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -3,7 +3,7 @@ The Fibonacci sequence is defined by the recurrence relation: -Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. +Fn = Fn-1 + Fn-2, where F1 = 1 and F2 = 1. It turns out that F541, which contains 113 digits, is the first Fibonacci number for which the last nine digits are 1-9 pandigital (contain all the digits 1 to 9, but not necessarily in order). And F2749, which contains 575 digits, is the first @@ -15,7 +15,7 @@ import sys -sys.set_int_max_str_digits(0) # type: ignore +sys.set_int_max_str_digits(0) def check(number: int) -> bool: diff --git a/project_euler/problem_107/sol1.py b/project_euler/problem_107/sol1.py index 4659eac24bd3..79cdd937042e 100644 --- a/project_euler/problem_107/sol1.py +++ b/project_euler/problem_107/sol1.py @@ -27,6 +27,7 @@ We use Prim's algorithm to find a Minimum Spanning Tree. Reference: https://en.wikipedia.org/wiki/Prim%27s_algorithm """ + from __future__ import annotations import os @@ -80,10 +81,11 @@ def prims_algorithm(self) -> Graph: while len(subgraph.vertices) < len(self.vertices): min_weight = max(self.edges.values()) + 1 for edge, weight in self.edges.items(): - if (edge[0] in subgraph.vertices) ^ (edge[1] in subgraph.vertices): - if weight < min_weight: - min_edge = edge - min_weight = weight + if (edge[0] in subgraph.vertices) ^ ( + edge[1] in subgraph.vertices + ) and weight < min_weight: + min_edge = edge + min_weight = weight subgraph.add_edge(min_edge, min_weight) diff --git a/project_euler/problem_109/sol1.py b/project_euler/problem_109/sol1.py index 852f001d38af..ef145dda590b 100644 --- a/project_euler/problem_109/sol1.py +++ b/project_euler/problem_109/sol1.py @@ -65,7 +65,7 @@ def solution(limit: int = 100) -> int: >>> solution(50) 12577 """ - singles: list[int] = list(range(1, 21)) + [25] + singles: list[int] = [*list(range(1, 21)), 25] doubles: list[int] = [2 * x for x in range(1, 21)] + [50] triples: list[int] = [3 * x for x in range(1, 21)] all_values: list[int] = singles + doubles + triples + [0] diff --git a/project_euler/problem_112/sol1.py b/project_euler/problem_112/sol1.py index b3ea6b35654a..31996d070771 100644 --- a/project_euler/problem_112/sol1.py +++ b/project_euler/problem_112/sol1.py @@ -49,7 +49,7 @@ def check_bouncy(n: int) -> bool: raise ValueError("check_bouncy() accepts only integer arguments") str_n = str(n) sorted_str_n = "".join(sorted(str_n)) - return sorted_str_n != str_n and sorted_str_n[::-1] != str_n + return str_n not in {sorted_str_n, sorted_str_n[::-1]} def solution(percent: float = 99) -> int: diff --git a/project_euler/problem_117/__init__.py b/project_euler/problem_117/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_117/sol1.py b/project_euler/problem_117/sol1.py new file mode 100644 index 000000000000..e8214454fac5 --- /dev/null +++ b/project_euler/problem_117/sol1.py @@ -0,0 +1,53 @@ +""" +Project Euler Problem 117: https://projecteuler.net/problem=117 + +Using a combination of grey square tiles and oblong tiles chosen from: +red tiles (measuring two units), green tiles (measuring three units), +and blue tiles (measuring four units), +it is possible to tile a row measuring five units in length +in exactly fifteen different ways. + + |grey|grey|grey|grey|grey| |red,red|grey|grey|grey| + + |grey|red,red|grey|grey| |grey|grey|red,red|grey| + + |grey|grey|grey|red,red| |red,red|red,red|grey| + + |red,red|grey|red,red| |grey|red,red|red,red| + + |green,green,green|grey|grey| |grey|green,green,green|grey| + + |grey|grey|green,green,green| |red,red|green,green,green| + + |green,green,green|red,red| |blue,blue,blue,blue|grey| + + |grey|blue,blue,blue,blue| + +How many ways can a row measuring fifty units in length be tiled? + +NOTE: This is related to Problem 116 (https://projecteuler.net/problem=116). +""" + + +def solution(length: int = 50) -> int: + """ + Returns the number of ways can a row of the given length be tiled + + >>> solution(5) + 15 + """ + + ways_number = [1] * (length + 1) + + for row_length in range(length + 1): + for tile_length in range(2, 5): + for tile_start in range(row_length - tile_length + 1): + ways_number[row_length] += ways_number[ + row_length - tile_start - tile_length + ] + + return ways_number[length] + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_120/sol1.py b/project_euler/problem_120/sol1.py index 0e6821214560..2f403972502f 100644 --- a/project_euler/problem_120/sol1.py +++ b/project_euler/problem_120/sol1.py @@ -3,7 +3,7 @@ Description: -Let r be the remainder when (a−1)^n + (a+1)^n is divided by a^2. +Let r be the remainder when (a-1)^n + (a+1)^n is divided by a^2. For example, if a = 7 and n = 3, then r = 42: 6^3 + 8^3 = 728 ≡ 42 mod 49. And as n varies, so too will r, but for a = 7 it turns out that r_max = 42. For 3 ≤ a ≤ 1000, find ∑ r_max. diff --git a/project_euler/problem_123/sol1.py b/project_euler/problem_123/sol1.py index f74cdd999401..265348d2d4c8 100644 --- a/project_euler/problem_123/sol1.py +++ b/project_euler/problem_123/sol1.py @@ -4,7 +4,7 @@ Name: Prime square remainders Let pn be the nth prime: 2, 3, 5, 7, 11, ..., and -let r be the remainder when (pn−1)^n + (pn+1)^n is divided by pn^2. +let r be the remainder when (pn-1)^n + (pn+1)^n is divided by pn^2. For example, when n = 3, p3 = 5, and 43 + 63 = 280 ≡ 5 mod 25. The least value of n for which the remainder first exceeds 10^9 is 7037. @@ -37,12 +37,13 @@ r = 2pn when n is odd r = 2 when n is even. """ + from __future__ import annotations from collections.abc import Generator -def sieve() -> Generator[int, None, None]: +def sieve() -> Generator[int]: """ Returns a prime number generator using sieve method. >>> type(sieve()) diff --git a/project_euler/problem_131/__init__.py b/project_euler/problem_131/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_131/sol1.py b/project_euler/problem_131/sol1.py new file mode 100644 index 000000000000..be3ea9c81ae4 --- /dev/null +++ b/project_euler/problem_131/sol1.py @@ -0,0 +1,53 @@ +""" +Project Euler Problem 131: https://projecteuler.net/problem=131 + +There are some prime values, p, for which there exists a positive integer, n, +such that the expression n^3 + n^2p is a perfect cube. + +For example, when p = 19, 8^3 + 8^2 x 19 = 12^3. + +What is perhaps most surprising is that for each prime with this property +the value of n is unique, and there are only four such primes below one-hundred. + +How many primes below one million have this remarkable property? +""" + +from math import isqrt + + +def is_prime(number: int) -> bool: + """ + Determines whether number is prime + + >>> is_prime(3) + True + + >>> is_prime(4) + False + """ + + return all(number % divisor != 0 for divisor in range(2, isqrt(number) + 1)) + + +def solution(max_prime: int = 10**6) -> int: + """ + Returns number of primes below max_prime with the property + + >>> solution(100) + 4 + """ + + primes_count = 0 + cube_index = 1 + prime_candidate = 7 + while prime_candidate < max_prime: + primes_count += is_prime(prime_candidate) + + cube_index += 1 + prime_candidate += 6 * cube_index + + return primes_count + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_135/sol1.py b/project_euler/problem_135/sol1.py index d71a0439c7e9..d57ace489191 100644 --- a/project_euler/problem_135/sol1.py +++ b/project_euler/problem_135/sol1.py @@ -1,28 +1,22 @@ """ Project Euler Problem 135: https://projecteuler.net/problem=135 -Given the positive integers, x, y, and z, -are consecutive terms of an arithmetic progression, -the least value of the positive integer, n, -for which the equation, -x2 − y2 − z2 = n, has exactly two solutions is n = 27: +Given the positive integers, x, y, and z, are consecutive terms of an arithmetic +progression, the least value of the positive integer, n, for which the equation, +x2 - y2 - z2 = n, has exactly two solutions is n = 27: -342 − 272 − 202 = 122 − 92 − 62 = 27 +342 - 272 - 202 = 122 - 92 - 62 = 27 -It turns out that n = 1155 is the least value -which has exactly ten solutions. +It turns out that n = 1155 is the least value which has exactly ten solutions. -How many values of n less than one million -have exactly ten distinct solutions? +How many values of n less than one million have exactly ten distinct solutions? -Taking x,y,z of the form a+d,a,a-d respectively, -the given equation reduces to a*(4d-a)=n. -Calculating no of solutions for every n till 1 million by fixing a -,and n must be multiple of a. -Total no of steps=n*(1/1+1/2+1/3+1/4..+1/n) -,so roughly O(nlogn) time complexity. - +Taking x, y, z of the form a + d, a, a - d respectively, the given equation reduces to +a * (4d - a) = n. +Calculating no of solutions for every n till 1 million by fixing a, and n must be a +multiple of a. Total no of steps = n * (1/1 + 1/2 + 1/3 + 1/4 + ... + 1/n), so roughly +O(nlogn) time complexity. """ @@ -42,15 +36,15 @@ def solution(limit: int = 1000000) -> int: for first_term in range(1, limit): for n in range(first_term, limit, first_term): common_difference = first_term + n / first_term - if common_difference % 4: # d must be divisble by 4 + if common_difference % 4: # d must be divisible by 4 continue else: common_difference /= 4 if ( first_term > common_difference and first_term < 4 * common_difference - ): # since x,y,z are positive integers - frequency[n] += 1 # so z>0 and a>d ,also 4d 0, a > d and 4d < a count = sum(1 for x in frequency[1:limit] if x == 10) diff --git a/project_euler/problem_144/sol1.py b/project_euler/problem_144/sol1.py index b5f103b64ff5..9070455de79f 100644 --- a/project_euler/problem_144/sol1.py +++ b/project_euler/problem_144/sol1.py @@ -6,7 +6,7 @@ The specific white cell we will be considering is an ellipse with the equation 4x^2 + y^2 = 100 -The section corresponding to −0.01 ≤ x ≤ +0.01 at the top is missing, allowing the +The section corresponding to -0.01 ≤ x ≤ +0.01 at the top is missing, allowing the light to enter and exit through the hole.  The light beam in this problem starts at the point (0.0,10.1) just outside the white @@ -20,7 +20,7 @@ the laser beam and the wall of the white cell; the blue line shows the line tangent to the ellipse at the point of incidence of the first bounce. -The slope m of the tangent line at any point (x,y) of the given ellipse is: m = −4x/y +The slope m of the tangent line at any point (x,y) of the given ellipse is: m = -4x/y The normal line is perpendicular to this tangent line at the point of incidence. @@ -29,7 +29,6 @@ How many times does the beam hit the internal surface of the white cell before exiting? """ - from math import isclose, sqrt diff --git a/project_euler/problem_145/sol1.py b/project_euler/problem_145/sol1.py index e9fc1a199161..583bb03a0a90 100644 --- a/project_euler/problem_145/sol1.py +++ b/project_euler/problem_145/sol1.py @@ -13,21 +13,22 @@ How many reversible numbers are there below one-billion (10^9)? """ + EVEN_DIGITS = [0, 2, 4, 6, 8] ODD_DIGITS = [1, 3, 5, 7, 9] -def reversible_numbers( +def slow_reversible_numbers( remaining_length: int, remainder: int, digits: list[int], length: int ) -> int: """ Count the number of reversible numbers of given length. Iterate over possible digits considering parity of current sum remainder. - >>> reversible_numbers(1, 0, [0], 1) + >>> slow_reversible_numbers(1, 0, [0], 1) 0 - >>> reversible_numbers(2, 0, [0] * 2, 2) + >>> slow_reversible_numbers(2, 0, [0] * 2, 2) 20 - >>> reversible_numbers(3, 0, [0] * 3, 3) + >>> slow_reversible_numbers(3, 0, [0] * 3, 3) 100 """ if remaining_length == 0: @@ -51,7 +52,7 @@ def reversible_numbers( result = 0 for digit in range(10): digits[length // 2] = digit - result += reversible_numbers( + result += slow_reversible_numbers( 0, (remainder + 2 * digit) // 10, digits, length ) return result @@ -67,7 +68,7 @@ def reversible_numbers( for digit2 in other_parity_digits: digits[(length - remaining_length) // 2] = digit2 - result += reversible_numbers( + result += slow_reversible_numbers( remaining_length - 2, (remainder + digit1 + digit2) // 10, digits, @@ -76,6 +77,42 @@ def reversible_numbers( return result +def slow_solution(max_power: int = 9) -> int: + """ + To evaluate the solution, use solution() + >>> slow_solution(3) + 120 + >>> slow_solution(6) + 18720 + >>> slow_solution(7) + 68720 + """ + result = 0 + for length in range(1, max_power + 1): + result += slow_reversible_numbers(length, 0, [0] * length, length) + return result + + +def reversible_numbers( + remaining_length: int, remainder: int, digits: list[int], length: int +) -> int: + """ + Count the number of reversible numbers of given length. + Iterate over possible digits considering parity of current sum remainder. + >>> reversible_numbers(1, 0, [0], 1) + 0 + >>> reversible_numbers(2, 0, [0] * 2, 2) + 20 + >>> reversible_numbers(3, 0, [0] * 3, 3) + 100 + """ + # There exist no reversible 1, 5, 9, 13 (ie. 4k+1) digit numbers + if (length - 1) % 4 == 0: + return 0 + + return slow_reversible_numbers(remaining_length, remainder, digits, length) + + def solution(max_power: int = 9) -> int: """ To evaluate the solution, use solution() @@ -92,5 +129,25 @@ def solution(max_power: int = 9) -> int: return result +def benchmark() -> None: + """ + Benchmarks + """ + # Running performance benchmarks... + # slow_solution : 292.9300301000003 + # solution : 54.90970860000016 + + from timeit import timeit + + print("Running performance benchmarks...") + + print(f"slow_solution : {timeit('slow_solution()', globals=globals(), number=10)}") + print(f"solution : {timeit('solution()', globals=globals(), number=10)}") + + if __name__ == "__main__": - print(f"{solution() = }") + print(f"Solution : {solution()}") + benchmark() + + # for i in range(1, 15): + # print(f"{i}. {reversible_numbers(i, 0, [0]*i, i)}") diff --git a/project_euler/problem_173/sol1.py b/project_euler/problem_173/sol1.py index 5416e25462cc..9235d00e1752 100644 --- a/project_euler/problem_173/sol1.py +++ b/project_euler/problem_173/sol1.py @@ -11,7 +11,6 @@ Using up to one million tiles how many different square laminae can be formed? """ - from math import ceil, sqrt diff --git a/project_euler/problem_174/sol1.py b/project_euler/problem_174/sol1.py index cbc0df5a9d65..9a75e8638880 100644 --- a/project_euler/problem_174/sol1.py +++ b/project_euler/problem_174/sol1.py @@ -14,7 +14,7 @@ Let N(n) be the number of t ≤ 1000000 such that t is type L(n); for example, N(15) = 832. -What is ∑ N(n) for 1 ≤ n ≤ 10? +What is sum N(n) for 1 ≤ n ≤ 10? """ from collections import defaultdict @@ -26,6 +26,8 @@ def solution(t_limit: int = 1000000, n_limit: int = 10) -> int: Return the sum of N(n) for 1 <= n <= n_limit. >>> solution(1000,5) + 222 + >>> solution(1000,10) 249 >>> solution(10000,10) 2383 @@ -45,7 +47,7 @@ def solution(t_limit: int = 1000000, n_limit: int = 10) -> int: for hole_width in range(hole_width_lower_bound, outer_width - 1, 2): count[outer_width * outer_width - hole_width * hole_width] += 1 - return sum(1 for n in count.values() if 1 <= n <= 10) + return sum(1 for n in count.values() if 1 <= n <= n_limit) if __name__ == "__main__": diff --git a/project_euler/problem_180/sol1.py b/project_euler/problem_180/sol1.py index 12e34dcaa76b..72baed42b99e 100644 --- a/project_euler/problem_180/sol1.py +++ b/project_euler/problem_180/sol1.py @@ -44,6 +44,7 @@ Reference: https://en.wikipedia.org/wiki/Fermat%27s_Last_Theorem """ + from __future__ import annotations from fractions import Fraction diff --git a/project_euler/problem_187/__init__.py b/project_euler/problem_187/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_187/sol1.py b/project_euler/problem_187/sol1.py new file mode 100644 index 000000000000..8944776fef50 --- /dev/null +++ b/project_euler/problem_187/sol1.py @@ -0,0 +1,162 @@ +""" +Project Euler Problem 187: https://projecteuler.net/problem=187 + +A composite is a number containing at least two prime factors. +For example, 15 = 3 x 5; 9 = 3 x 3; 12 = 2 x 2 x 3. + +There are ten composites below thirty containing precisely two, +not necessarily distinct, prime factors: 4, 6, 9, 10, 14, 15, 21, 22, 25, 26. + +How many composite integers, n < 10^8, have precisely two, +not necessarily distinct, prime factors? +""" + +from math import isqrt + + +def slow_calculate_prime_numbers(max_number: int) -> list[int]: + """ + Returns prime numbers below max_number. + See: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes + + >>> slow_calculate_prime_numbers(10) + [2, 3, 5, 7] + + >>> slow_calculate_prime_numbers(2) + [] + """ + + # List containing a bool value for every number below max_number/2 + is_prime = [True] * max_number + + for i in range(2, isqrt(max_number - 1) + 1): + if is_prime[i]: + # Mark all multiple of i as not prime + for j in range(i**2, max_number, i): + is_prime[j] = False + + return [i for i in range(2, max_number) if is_prime[i]] + + +def calculate_prime_numbers(max_number: int) -> list[int]: + """ + Returns prime numbers below max_number. + See: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes + + >>> calculate_prime_numbers(10) + [2, 3, 5, 7] + + >>> calculate_prime_numbers(2) + [] + """ + + if max_number <= 2: + return [] + + # List containing a bool value for every odd number below max_number/2 + is_prime = [True] * (max_number // 2) + + for i in range(3, isqrt(max_number - 1) + 1, 2): + if is_prime[i // 2]: + # Mark all multiple of i as not prime using list slicing + is_prime[i**2 // 2 :: i] = [False] * ( + # Same as: (max_number - (i**2)) // (2 * i) + 1 + # but faster than len(is_prime[i**2 // 2 :: i]) + len(range(i**2 // 2, max_number // 2, i)) + ) + + return [2] + [2 * i + 1 for i in range(1, max_number // 2) if is_prime[i]] + + +def slow_solution(max_number: int = 10**8) -> int: + """ + Returns the number of composite integers below max_number have precisely two, + not necessarily distinct, prime factors. + + >>> slow_solution(30) + 10 + """ + + prime_numbers = slow_calculate_prime_numbers(max_number // 2) + + semiprimes_count = 0 + left = 0 + right = len(prime_numbers) - 1 + while left <= right: + while prime_numbers[left] * prime_numbers[right] >= max_number: + right -= 1 + semiprimes_count += right - left + 1 + left += 1 + + return semiprimes_count + + +def while_solution(max_number: int = 10**8) -> int: + """ + Returns the number of composite integers below max_number have precisely two, + not necessarily distinct, prime factors. + + >>> while_solution(30) + 10 + """ + + prime_numbers = calculate_prime_numbers(max_number // 2) + + semiprimes_count = 0 + left = 0 + right = len(prime_numbers) - 1 + while left <= right: + while prime_numbers[left] * prime_numbers[right] >= max_number: + right -= 1 + semiprimes_count += right - left + 1 + left += 1 + + return semiprimes_count + + +def solution(max_number: int = 10**8) -> int: + """ + Returns the number of composite integers below max_number have precisely two, + not necessarily distinct, prime factors. + + >>> solution(30) + 10 + """ + + prime_numbers = calculate_prime_numbers(max_number // 2) + + semiprimes_count = 0 + right = len(prime_numbers) - 1 + for left in range(len(prime_numbers)): + if left > right: + break + for r in range(right, left - 2, -1): + if prime_numbers[left] * prime_numbers[r] < max_number: + break + right = r + semiprimes_count += right - left + 1 + + return semiprimes_count + + +def benchmark() -> None: + """ + Benchmarks + """ + # Running performance benchmarks... + # slow_solution : 108.50874730000032 + # while_sol : 28.09581200000048 + # solution : 25.063097400000515 + + from timeit import timeit + + print("Running performance benchmarks...") + + print(f"slow_solution : {timeit('slow_solution()', globals=globals(), number=10)}") + print(f"while_sol : {timeit('while_solution()', globals=globals(), number=10)}") + print(f"solution : {timeit('solution()', globals=globals(), number=10)}") + + +if __name__ == "__main__": + print(f"Solution: {solution()}") + benchmark() diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py index 6bff9d54eeca..efb2a5d086ad 100644 --- a/project_euler/problem_191/sol1.py +++ b/project_euler/problem_191/sol1.py @@ -25,7 +25,6 @@ https://projecteuler.net/problem=191 """ - cache: dict[tuple[int, int, int], int] = {} diff --git a/project_euler/problem_203/sol1.py b/project_euler/problem_203/sol1.py index 713b530b6af2..8ad089ec09aa 100644 --- a/project_euler/problem_203/sol1.py +++ b/project_euler/problem_203/sol1.py @@ -27,6 +27,7 @@ References: - https://en.wikipedia.org/wiki/Pascal%27s_triangle """ + from __future__ import annotations @@ -50,8 +51,8 @@ def get_pascal_triangle_unique_coefficients(depth: int) -> set[int]: coefficients = {1} previous_coefficients = [1] for _ in range(2, depth + 1): - coefficients_begins_one = previous_coefficients + [0] - coefficients_ends_one = [0] + previous_coefficients + coefficients_begins_one = [*previous_coefficients, 0] + coefficients_ends_one = [0, *previous_coefficients] previous_coefficients = [] for x, y in zip(coefficients_begins_one, coefficients_ends_one): coefficients.add(x + y) diff --git a/project_euler/problem_207/sol1.py b/project_euler/problem_207/sol1.py index 2b3591f51cfa..c83dc1d4aaef 100644 --- a/project_euler/problem_207/sol1.py +++ b/project_euler/problem_207/sol1.py @@ -88,9 +88,11 @@ def solution(max_proportion: float = 1 / 12345) -> int: total_partitions += 1 if check_partition_perfect(partition_candidate): perfect_partitions += 1 - if perfect_partitions > 0: - if perfect_partitions / total_partitions < max_proportion: - return int(partition_candidate) + if ( + perfect_partitions > 0 + and perfect_partitions / total_partitions < max_proportion + ): + return int(partition_candidate) integer += 1 diff --git a/project_euler/problem_493/sol1.py b/project_euler/problem_493/sol1.py index c9879a528230..4d96c6c3207e 100644 --- a/project_euler/problem_493/sol1.py +++ b/project_euler/problem_493/sol1.py @@ -9,7 +9,7 @@ This combinatorial problem can be solved by decomposing the problem into the following steps: -1. Calculate the total number of possible picking cominations +1. Calculate the total number of possible picking combinations [combinations := binom_coeff(70, 20)] 2. Calculate the number of combinations with one colour missing [missing := binom_coeff(60, 20)] diff --git a/project_euler/problem_551/sol1.py b/project_euler/problem_551/sol1.py index 2cd75efbb68d..100e9d41dd31 100644 --- a/project_euler/problem_551/sol1.py +++ b/project_euler/problem_551/sol1.py @@ -12,7 +12,6 @@ Find a(10^15) """ - ks = range(2, 20 + 1) base = [10**k for k in range(ks[-1] + 1)] memo: dict[int, dict[int, list[list[int]]]] = {} diff --git a/project_euler/problem_800/__init__.py b/project_euler/problem_800/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_800/sol1.py b/project_euler/problem_800/sol1.py new file mode 100644 index 000000000000..f887787bcbc6 --- /dev/null +++ b/project_euler/problem_800/sol1.py @@ -0,0 +1,65 @@ +""" +Project Euler Problem 800: https://projecteuler.net/problem=800 + +An integer of the form p^q q^p with prime numbers p != q is called a hybrid-integer. +For example, 800 = 2^5 5^2 is a hybrid-integer. + +We define C(n) to be the number of hybrid-integers less than or equal to n. +You are given C(800) = 2 and C(800^800) = 10790 + +Find C(800800^800800) +""" + +from math import isqrt, log2 + + +def calculate_prime_numbers(max_number: int) -> list[int]: + """ + Returns prime numbers below max_number + + >>> calculate_prime_numbers(10) + [2, 3, 5, 7] + """ + + is_prime = [True] * max_number + for i in range(2, isqrt(max_number - 1) + 1): + if is_prime[i]: + for j in range(i**2, max_number, i): + is_prime[j] = False + + return [i for i in range(2, max_number) if is_prime[i]] + + +def solution(base: int = 800800, degree: int = 800800) -> int: + """ + Returns the number of hybrid-integers less than or equal to base^degree + + >>> solution(800, 1) + 2 + + >>> solution(800, 800) + 10790 + """ + + upper_bound = degree * log2(base) + max_prime = int(upper_bound) + prime_numbers = calculate_prime_numbers(max_prime) + + hybrid_integers_count = 0 + left = 0 + right = len(prime_numbers) - 1 + while left < right: + while ( + prime_numbers[right] * log2(prime_numbers[left]) + + prime_numbers[left] * log2(prime_numbers[right]) + > upper_bound + ): + right -= 1 + hybrid_integers_count += right - left + left += 1 + + return hybrid_integers_count + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/pyproject.toml b/pyproject.toml index 410e7655b2b5..7b7176705c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,264 @@ +[project] +name = "thealgorithms-python" +version = "0.0.1" +description = "TheAlgorithms in Python" +authors = [ { name = "TheAlgorithms Contributors" } ] +requires-python = ">=3.13" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "beautifulsoup4>=4.12.3", + "fake-useragent>=1.5.1", + "imageio>=2.36.1", + "keras>=3.7", + "lxml>=5.3", + "matplotlib>=3.9.3", + "numpy>=2.1.3", + "opencv-python>=4.10.0.84", + "pandas>=2.2.3", + "pillow>=11", + "requests>=2.32.3", + "rich>=13.9.4", + "scikit-learn>=1.5.2", + "sphinx-pyproject>=0.3", + "statsmodels>=0.14.4", + "sympy>=1.13.3", + "tweepy>=4.14", + "typing-extensions>=4.12.2", + "xgboost>=2.1.3", +] + +[dependency-groups] +test = [ + "pytest>=8.3.4", + "pytest-cov>=6", +] + +docs = [ + "myst-parser>=4", + "sphinx-autoapi>=3.4", + "sphinx-pyproject>=0.3", +] +euler-validate = [ + "numpy>=2.1.3", + "requests>=2.32.3", +] + +[tool.ruff] +target-version = "py313" + +output-format = "full" +lint.select = [ + # https://beta.ruff.rs/docs/rules + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "T10", # flake8-debugger + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 + # "ANN", # flake8-annotations -- FIX ME? + # "COM", # flake8-commas -- DO NOT FIX + # "D", # pydocstyle -- FIX ME? + # "ERA", # eradicate -- DO NOT FIX + # "FBT", # flake8-boolean-trap # FIX ME + # "PTH", # flake8-use-pathlib # FIX ME + # "Q", # flake8-quotes + # "RET", # flake8-return # FIX ME? + # "T20", # flake8-print + # "TCH", # flake8-type-checking + # "TRY", # tryceratops +] +lint.ignore = [ + # `ruff rule S101` for a description of that rule + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME + "B905", # `zip()` without an explicit `strict=` parameter -- FIX ME + "EM101", # Exception must not use a string literal, assign to variable first + "EXE001", # Shebang is present but file is not executable -- DO NOT FIX + "G004", # Logging statement uses f-string + "ISC001", # Conflicts with ruff format -- DO NOT FIX + "PLC1901", # `{}` can be simplified to `{}` as an empty string is falsey + "PLW060", # Using global for `{name}` but no assignment is done -- DO NOT FIX + "PLW2901", # PLW2901: Redefined loop variable -- FIX ME + "PT011", # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "S101", # Use of `assert` detected -- DO NOT FIX + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME + "SLF001", # Private member accessed: `_Iterator` -- FIX ME + "UP038", # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX +] + +lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [ + "BLE001", +] +lint.per-file-ignores."hashes/enigma_machine.py" = [ + "BLE001", +] +lint.per-file-ignores."machine_learning/sequential_minimum_optimization.py" = [ + "SIM115", +] +lint.per-file-ignores."matrix/sherman_morrison.py" = [ + "SIM103", +] +lint.per-file-ignores."other/l*u_cache.py" = [ + "RUF012", +] +lint.per-file-ignores."physics/newtons_second_law_of_motion.py" = [ + "BLE001", +] +lint.per-file-ignores."project_euler/problem_099/sol1.py" = [ + "SIM115", +] +lint.per-file-ignores."sorts/external_sort.py" = [ + "SIM115", +] +lint.mccabe.max-complexity = 17 # default: 10 +lint.pylint.allow-magic-value-types = [ + "float", + "int", + "str", +] +lint.pylint.max-args = 10 # default: 5 +lint.pylint.max-branches = 20 # default: 12 +lint.pylint.max-returns = 8 # default: 6 +lint.pylint.max-statements = 88 # default: 50 + +[tool.codespell] +ignore-words-list = "3rt,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar" +skip = "./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt" + [tool.pytest.ini_options] markers = [ - "mat_ops: mark a test as utilizing matrix operations.", + "mat_ops: mark a test as utilizing matrix operations.", ] addopts = [ - "--durations=10", - "--doctest-modules", - "--showlocals", + "--durations=10", + "--doctest-modules", + "--showlocals", ] - [tool.coverage.report] -omit = [".env/*"] +omit = [ + ".env/*", + "project_euler/*", +] sort = "Cover" -#[report] -#sort = Cover -#omit = -# .env/* -# backtracking/* +[tool.sphinx-pyproject] +copyright = "2014, TheAlgorithms" +autoapi_dirs = [ + "audio_filters", + "backtracking", + "bit_manipulation", + "blockchain", + "boolean_algebra", + "cellular_automata", + "ciphers", + "compression", + "computer_vision", + "conversions", + "data_structures", + "digital_image_processing", + "divide_and_conquer", + "dynamic_programming", + "electronics", + "file_transfer", + "financial", + "fractals", + "fuzzy_logic", + "genetic_algorithm", + "geodesy", + "geometry", + "graphics", + "graphs", + "greedy_methods", + "hashes", + "knapsack", + "linear_algebra", + "linear_programming", + "machine_learning", + "maths", + "matrix", + "networking_flow", + "neural_network", + "other", + "physics", + "project_euler", + "quantum", + "scheduling", + "searches", + "sorts", + "strings", + "web_programming", +] +autoapi_member_order = "groupwise" +# autoapi_python_use_implicit_namespaces = true +exclude_patterns = [ + ".*/*", + "docs/", +] +extensions = [ + "autoapi.extension", + "myst_parser", +] +html_static_path = [ "_static" ] +html_theme = "alabaster" +myst_enable_extensions = [ + "amsmath", + "attrs_inline", + "colon_fence", + "deflist", + "dollarmath", + "fieldlist", + "html_admonition", + "html_image", + # "linkify", + "replacements", + "smartquotes", + "strikethrough", + "substitution", + "tasklist", +] +myst_fence_as_directive = [ + "include", +] +templates_path = [ "_templates" ] +[tool.sphinx-pyproject.source_suffix] +".rst" = "restructuredtext" +# ".txt" = "markdown" +".md" = "markdown" diff --git a/quantum/bb84.py b/quantum/bb84.py.DISABLED.txt similarity index 99% rename from quantum/bb84.py rename to quantum/bb84.py.DISABLED.txt index 60d64371fe63..e90a11c2aef3 100644 --- a/quantum/bb84.py +++ b/quantum/bb84.py.DISABLED.txt @@ -64,10 +64,10 @@ def bb84(key_len: int = 8, seed: int | None = None) -> str: key: The key generated using BB84 protocol. >>> bb84(16, seed=0) - '1101101100010000' + '0111110111010010' >>> bb84(8, seed=0) - '01011011' + '10110001' """ # Set up the random number generator. rng = np.random.default_rng(seed=seed) diff --git a/quantum/deutsch_jozsa.py b/quantum/deutsch_jozsa.py.DISABLED.txt old mode 100755 new mode 100644 similarity index 99% rename from quantum/deutsch_jozsa.py rename to quantum/deutsch_jozsa.py.DISABLED.txt index 95c3e65b5edf..5c8a379debfc --- a/quantum/deutsch_jozsa.py +++ b/quantum/deutsch_jozsa.py.DISABLED.txt @@ -1,3 +1,4 @@ +# DISABLED!! #!/usr/bin/env python3 """ Deutsch-Jozsa Algorithm is one of the first examples of a quantum diff --git a/quantum/half_adder.py b/quantum/half_adder.py.DISABLED.txt old mode 100755 new mode 100644 similarity index 99% rename from quantum/half_adder.py rename to quantum/half_adder.py.DISABLED.txt index 21a57ddcf2dd..800d563ec76f --- a/quantum/half_adder.py +++ b/quantum/half_adder.py.DISABLED.txt @@ -1,3 +1,4 @@ +# DISABLED!! #!/usr/bin/env python3 """ Build a half-adder quantum circuit that takes two bits as input, diff --git a/quantum/not_gate.py b/quantum/not_gate.py.DISABLED.txt similarity index 100% rename from quantum/not_gate.py rename to quantum/not_gate.py.DISABLED.txt diff --git a/quantum/q_full_adder.py b/quantum/q_full_adder.py.DISABLED.txt similarity index 99% rename from quantum/q_full_adder.py rename to quantum/q_full_adder.py.DISABLED.txt index 66d93198519e..ec4efa4346a5 100644 --- a/quantum/q_full_adder.py +++ b/quantum/q_full_adder.py.DISABLED.txt @@ -88,7 +88,7 @@ def quantum_full_adder( quantum_circuit = qiskit.QuantumCircuit(qr, cr) - for i in range(0, 3): + for i in range(3): if entry[i] == 2: quantum_circuit.h(i) # for hadamard entries elif entry[i] == 1: diff --git a/quantum/quantum_entanglement.py b/quantum/quantum_entanglement.py.DISABLED.txt similarity index 100% rename from quantum/quantum_entanglement.py rename to quantum/quantum_entanglement.py.DISABLED.txt diff --git a/quantum/quantum_teleportation.py b/quantum/quantum_teleportation.py.DISABLED.txt similarity index 100% rename from quantum/quantum_teleportation.py rename to quantum/quantum_teleportation.py.DISABLED.txt diff --git a/quantum/ripple_adder_classic.py b/quantum/ripple_adder_classic.py.DISABLED.txt similarity index 91% rename from quantum/ripple_adder_classic.py rename to quantum/ripple_adder_classic.py.DISABLED.txt index c07757af7fff..2284141ccac2 100644 --- a/quantum/ripple_adder_classic.py +++ b/quantum/ripple_adder_classic.py.DISABLED.txt @@ -54,9 +54,9 @@ def full_adder( # The default value for **backend** is the result of a function call which is not -# normally recommended and causes flake8-bugbear to raise a B008 error. However, -# in this case, this is acceptable because `Aer.get_backend()` is called when the -# function is defined and that same backend is then reused for all function calls. +# normally recommended and causes ruff to raise a B008 error. However, in this case, +# this is acceptable because `Aer.get_backend()` is called when the function is defined +# and that same backend is then reused for all function calls. def ripple_adder( @@ -107,7 +107,7 @@ def ripple_adder( res = qiskit.execute(circuit, backend, shots=1).result() # The result is in binary. Convert it back to int - return int(list(res.get_counts())[0], 2) + return int(next(iter(res.get_counts())), 2) if __name__ == "__main__": diff --git a/quantum/single_qubit_measure.py b/quantum/single_qubit_measure.py.DISABLED.txt similarity index 100% rename from quantum/single_qubit_measure.py rename to quantum/single_qubit_measure.py.DISABLED.txt diff --git a/quantum/superdense_coding.py b/quantum/superdense_coding.py.DISABLED.txt similarity index 100% rename from quantum/superdense_coding.py rename to quantum/superdense_coding.py.DISABLED.txt diff --git a/requirements.txt b/requirements.txt index a1d607df07e1..b104505e01bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ beautifulsoup4 -fake_useragent +fake-useragent +imageio keras lxml matplotlib @@ -7,16 +8,12 @@ numpy opencv-python pandas pillow -projectq -qiskit requests rich -scikit-fuzzy scikit-learn +sphinx-pyproject statsmodels sympy -tensorflow; python_version < "3.11" -texttable tweepy +typing_extensions xgboost -yulewalker diff --git a/scheduling/highest_response_ratio_next.py b/scheduling/highest_response_ratio_next.py index 9c999ec65053..f858be2ee44a 100644 --- a/scheduling/highest_response_ratio_next.py +++ b/scheduling/highest_response_ratio_next.py @@ -4,6 +4,7 @@ to mitigate the problem of process starvation. https://en.wikipedia.org/wiki/Highest_response_ratio_next """ + from statistics import mean import numpy as np @@ -45,15 +46,14 @@ def calculate_turn_around_time( i = 0 while finished_process[i] == 1: i += 1 - if current_time < arrival_time[i]: - current_time = arrival_time[i] + current_time = max(current_time, arrival_time[i]) response_ratio = 0 # Index showing the location of the process being performed loc = 0 # Saves the current response ratio. temp = 0 - for i in range(0, no_of_process): + for i in range(no_of_process): if finished_process[i] == 0 and arrival_time[i] <= current_time: temp = (burst_time[i] + (current_time - arrival_time[i])) / burst_time[ i @@ -74,7 +74,10 @@ def calculate_turn_around_time( def calculate_waiting_time( - process_name: list, turn_around_time: list, burst_time: list, no_of_process: int + process_name: list, # noqa: ARG001 + turn_around_time: list, + burst_time: list, + no_of_process: int, ) -> list: """ Calculate the waiting time of each processes. @@ -87,7 +90,7 @@ def calculate_waiting_time( """ waiting_time = [0] * no_of_process - for i in range(0, no_of_process): + for i in range(no_of_process): waiting_time[i] = turn_around_time[i] - burst_time[i] return waiting_time @@ -106,7 +109,7 @@ def calculate_waiting_time( ) print("Process name \tArrival time \tBurst time \tTurn around time \tWaiting time") - for i in range(0, no_of_process): + for i in range(no_of_process): print( f"{process_name[i]}\t\t{arrival_time[i]}\t\t{burst_time[i]}\t\t" f"{turn_around_time[i]}\t\t\t{waiting_time[i]}" diff --git a/scheduling/job_sequence_with_deadline.py b/scheduling/job_sequence_with_deadline.py new file mode 100644 index 000000000000..ee1fdbd0e55c --- /dev/null +++ b/scheduling/job_sequence_with_deadline.py @@ -0,0 +1,63 @@ +""" +Given a list of tasks, each with a deadline and reward, calculate which tasks can be +completed to yield the maximum reward. Each task takes one unit of time to complete, +and we can only work on one task at a time. Once a task has passed its deadline, it +can no longer be scheduled. + +Example : +tasks_info = [(4, 20), (1, 10), (1, 40), (1, 30)] +max_tasks will return (2, [2, 0]) - +Scheduling these tasks would result in a reward of 40 + 20 + +This problem can be solved using the concept of "GREEDY ALGORITHM". +Time Complexity - O(n log n) +https://medium.com/@nihardudhat2000/job-sequencing-with-deadline-17ddbb5890b5 +""" + +from dataclasses import dataclass +from operator import attrgetter + + +@dataclass +class Task: + task_id: int + deadline: int + reward: int + + +def max_tasks(tasks_info: list[tuple[int, int]]) -> list[int]: + """ + Create a list of Task objects that are sorted so the highest rewards come first. + Return a list of those task ids that can be completed before i becomes too high. + >>> max_tasks([(4, 20), (1, 10), (1, 40), (1, 30)]) + [2, 0] + >>> max_tasks([(1, 10), (2, 20), (3, 30), (2, 40)]) + [3, 2] + >>> max_tasks([(9, 10)]) + [0] + >>> max_tasks([(-9, 10)]) + [] + >>> max_tasks([]) + [] + >>> max_tasks([(0, 10), (0, 20), (0, 30), (0, 40)]) + [] + >>> max_tasks([(-1, 10), (-2, 20), (-3, 30), (-4, 40)]) + [] + """ + tasks = sorted( + ( + Task(task_id, deadline, reward) + for task_id, (deadline, reward) in enumerate(tasks_info) + ), + key=attrgetter("reward"), + reverse=True, + ) + return [task.task_id for i, task in enumerate(tasks, start=1) if task.deadline >= i] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(f"{max_tasks([(4, 20), (1, 10), (1, 40), (1, 30)]) = }") + print(f"{max_tasks([(1, 10), (2, 20), (3, 30), (2, 40)]) = }") diff --git a/scheduling/job_sequencing_with_deadline.py b/scheduling/job_sequencing_with_deadline.py index 7b23c0b3575f..13946948492f 100644 --- a/scheduling/job_sequencing_with_deadline.py +++ b/scheduling/job_sequencing_with_deadline.py @@ -1,9 +1,8 @@ -def job_sequencing_with_deadlines(num_jobs: int, jobs: list) -> list: +def job_sequencing_with_deadlines(jobs: list) -> list: """ Function to find the maximum profit by doing jobs in a given time frame Args: - num_jobs [int]: Number of jobs jobs [list]: A list of tuples of (job_id, deadline, profit) Returns: @@ -11,10 +10,10 @@ def job_sequencing_with_deadlines(num_jobs: int, jobs: list) -> list: in a given time frame Examples: - >>> job_sequencing_with_deadlines(4, + >>> job_sequencing_with_deadlines( ... [(1, 4, 20), (2, 1, 10), (3, 1, 40), (4, 1, 30)]) [2, 60] - >>> job_sequencing_with_deadlines(5, + >>> job_sequencing_with_deadlines( ... [(1, 2, 100), (2, 1, 19), (3, 2, 27), (4, 1, 25), (5, 1, 15)]) [2, 127] """ diff --git a/scheduling/non_preemptive_shortest_job_first.py b/scheduling/non_preemptive_shortest_job_first.py index 69c974b0044d..cb7ffd3abd9c 100644 --- a/scheduling/non_preemptive_shortest_job_first.py +++ b/scheduling/non_preemptive_shortest_job_first.py @@ -5,7 +5,6 @@ https://en.wikipedia.org/wiki/Shortest_job_next """ - from __future__ import annotations from statistics import mean diff --git a/scheduling/round_robin.py b/scheduling/round_robin.py index e8d54dd9a553..5f6c7f341baa 100644 --- a/scheduling/round_robin.py +++ b/scheduling/round_robin.py @@ -3,6 +3,7 @@ In Round Robin each process is assigned a fixed time slot in a cyclic way. https://en.wikipedia.org/wiki/Round-robin_scheduling """ + from __future__ import annotations from statistics import mean diff --git a/scheduling/shortest_job_first.py b/scheduling/shortest_job_first.py index b3f81bfd10e7..91012ee3ac35 100644 --- a/scheduling/shortest_job_first.py +++ b/scheduling/shortest_job_first.py @@ -3,6 +3,7 @@ Please note arrival time and burst Please use spaces to separate times entered. """ + from __future__ import annotations import pandas as pd @@ -36,12 +37,14 @@ def calculate_waitingtime( # Process until all processes are completed while complete != no_of_processes: for j in range(no_of_processes): - if arrival_time[j] <= increment_time: - if remaining_time[j] > 0: - if remaining_time[j] < minm: - minm = remaining_time[j] - short = j - check = True + if ( + arrival_time[j] <= increment_time + and remaining_time[j] > 0 + and remaining_time[j] < minm + ): + minm = remaining_time[j] + short = j + check = True if not check: increment_time += 1 @@ -63,8 +66,7 @@ def calculate_waitingtime( finar = finish_time - arrival_time[short] waiting_time[short] = finar - burst_time[short] - if waiting_time[short] < 0: - waiting_time[short] = 0 + waiting_time[short] = max(waiting_time[short], 0) # Increment time increment_time += 1 diff --git a/scripts/build_directory_md.py b/scripts/build_directory_md.py index 7572ce342720..aa95b95db4b5 100755 --- a/scripts/build_directory_md.py +++ b/scripts/build_directory_md.py @@ -6,7 +6,11 @@ def good_file_paths(top_dir: str = ".") -> Iterator[str]: for dir_path, dir_names, filenames in os.walk(top_dir): - dir_names[:] = [d for d in dir_names if d != "scripts" and d[0] not in "._"] + dir_names[:] = [ + d + for d in dir_names + if d != "scripts" and d[0] not in "._" and "venv" not in d + ] for filename in filenames: if filename == "__init__.py": continue @@ -21,9 +25,8 @@ def md_prefix(i): def print_path(old_path: str, new_path: str) -> str: old_parts = old_path.split(os.sep) for i, new_part in enumerate(new_path.split(os.sep)): - if i + 1 > len(old_parts) or old_parts[i] != new_part: - if new_part: - print(f"{md_prefix(i)} {new_part.replace('_', ' ').title()}") + if (i + 1 > len(old_parts) or old_parts[i] != new_part) and new_part: + print(f"{md_prefix(i)} {new_part.replace('_', ' ').title()}") return new_path @@ -34,7 +37,7 @@ def print_directory_md(top_dir: str = ".") -> None: if filepath != old_path: old_path = print_path(old_path, filepath) indent = (filepath.count(os.sep) + 1) if filepath else 0 - url = "/".join((filepath, filename)).replace(" ", "%20") + url = f"{filepath}/{filename}".replace(" ", "%20") filename = os.path.splitext(filename.replace("_", " ").title())[0] print(f"{md_prefix(indent)} [{filename}]({url})") diff --git a/scripts/close_pull_requests_with_awaiting_changes.sh b/scripts/close_pull_requests_with_awaiting_changes.sh new file mode 100755 index 000000000000..55e19c980596 --- /dev/null +++ b/scripts/close_pull_requests_with_awaiting_changes.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# List all open pull requests +prs=$(gh pr list --state open --json number,title,labels --limit 500) + +# Loop through each pull request +echo "$prs" | jq -c '.[]' | while read -r pr; do + pr_number=$(echo "$pr" | jq -r '.number') + pr_title=$(echo "$pr" | jq -r '.title') + pr_labels=$(echo "$pr" | jq -r '.labels') + + # Check if the "awaiting changes" label is present + awaiting_changes=$(echo "$pr_labels" | jq -r '.[] | select(.name == "awaiting changes")') + echo "Checking PR #$pr_number $pr_title ($awaiting_changes) ($pr_labels)" + + # If awaiting_changes, close the pull request + if [[ -n "$awaiting_changes" ]]; then + echo "Closing PR #$pr_number $pr_title due to awaiting_changes label" + gh pr close "$pr_number" --comment "Closing awaiting_changes PRs to prepare for Hacktoberfest" + sleep 2 + fi +done diff --git a/scripts/close_pull_requests_with_failing_tests.sh b/scripts/close_pull_requests_with_failing_tests.sh new file mode 100755 index 000000000000..3ec5960aed27 --- /dev/null +++ b/scripts/close_pull_requests_with_failing_tests.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# List all open pull requests +prs=$(gh pr list --state open --json number,title,labels --limit 500) + +# Loop through each pull request +echo "$prs" | jq -c '.[]' | while read -r pr; do + pr_number=$(echo "$pr" | jq -r '.number') + pr_title=$(echo "$pr" | jq -r '.title') + pr_labels=$(echo "$pr" | jq -r '.labels') + + # Check if the "tests are failing" label is present + tests_are_failing=$(echo "$pr_labels" | jq -r '.[] | select(.name == "tests are failing")') + echo "Checking PR #$pr_number $pr_title ($tests_are_failing) ($pr_labels)" + + # If there are failing tests, close the pull request + if [[ -n "$tests_are_failing" ]]; then + echo "Closing PR #$pr_number $pr_title due to tests_are_failing label" + gh pr close "$pr_number" --comment "Closing tests_are_failing PRs to prepare for Hacktoberfest" + sleep 2 + fi +done diff --git a/scripts/close_pull_requests_with_require_descriptive_names.sh b/scripts/close_pull_requests_with_require_descriptive_names.sh new file mode 100755 index 000000000000..0fc3cec1d247 --- /dev/null +++ b/scripts/close_pull_requests_with_require_descriptive_names.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# List all open pull requests +prs=$(gh pr list --state open --json number,title,labels --limit 500) + +# Loop through each pull request +echo "$prs" | jq -c '.[]' | while read -r pr; do + pr_number=$(echo "$pr" | jq -r '.number') + pr_title=$(echo "$pr" | jq -r '.title') + pr_labels=$(echo "$pr" | jq -r '.labels') + + # Check if the "require descriptive names" label is present + require_descriptive_names=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require descriptive names")') + echo "Checking PR #$pr_number $pr_title ($require_descriptive_names) ($pr_labels)" + + # If there are require_descriptive_names, close the pull request + if [[ -n "$require_descriptive_names" ]]; then + echo "Closing PR #$pr_number $pr_title due to require_descriptive_names label" + gh pr close "$pr_number" --comment "Closing require_descriptive_names PRs to prepare for Hacktoberfest" + fi +done diff --git a/scripts/close_pull_requests_with_require_tests.sh b/scripts/close_pull_requests_with_require_tests.sh new file mode 100755 index 000000000000..89a54996b584 --- /dev/null +++ b/scripts/close_pull_requests_with_require_tests.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# List all open pull requests +prs=$(gh pr list --state open --json number,title,labels --limit 500) + +# Loop through each pull request +echo "$prs" | jq -c '.[]' | while read -r pr; do + pr_number=$(echo "$pr" | jq -r '.number') + pr_title=$(echo "$pr" | jq -r '.title') + pr_labels=$(echo "$pr" | jq -r '.labels') + + # Check if the "require_tests" label is present + require_tests=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require tests")') + echo "Checking PR #$pr_number $pr_title ($require_tests) ($pr_labels)" + + # If there require tests, close the pull request + if [[ -n "$require_tests" ]]; then + echo "Closing PR #$pr_number $pr_title due to require_tests label" + gh pr close "$pr_number" --comment "Closing require_tests PRs to prepare for Hacktoberfest" + # sleep 2 + fi +done diff --git a/scripts/close_pull_requests_with_require_type_hints.sh b/scripts/close_pull_requests_with_require_type_hints.sh new file mode 100755 index 000000000000..df5d88289cf0 --- /dev/null +++ b/scripts/close_pull_requests_with_require_type_hints.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# List all open pull requests +prs=$(gh pr list --state open --json number,title,labels --limit 500) + +# Loop through each pull request +echo "$prs" | jq -c '.[]' | while read -r pr; do + pr_number=$(echo "$pr" | jq -r '.number') + pr_title=$(echo "$pr" | jq -r '.title') + pr_labels=$(echo "$pr" | jq -r '.labels') + + # Check if the "require type hints" label is present + require_type_hints=$(echo "$pr_labels" | jq -r '.[] | select(.name == "require type hints")') + echo "Checking PR #$pr_number $pr_title ($require_type_hints) ($pr_labels)" + + # If require_type_hints, close the pull request + if [[ -n "$require_type_hints" ]]; then + echo "Closing PR #$pr_number $pr_title due to require_type_hints label" + gh pr close "$pr_number" --comment "Closing require_type_hints PRs to prepare for Hacktoberfest" + fi +done diff --git a/scripts/find_git_conflicts.sh b/scripts/find_git_conflicts.sh new file mode 100755 index 000000000000..8af33fa75279 --- /dev/null +++ b/scripts/find_git_conflicts.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Replace with your repository (format: owner/repo) +REPO="TheAlgorithms/Python" + +# Fetch open pull requests with conflicts into a variable +echo "Checking for pull requests with conflicts in $REPO..." + +prs=$(gh pr list --repo "$REPO" --state open --json number,title,mergeable --jq '.[] | select(.mergeable == "CONFLICTING") | {number, title}' --limit 500) + +# Process each conflicting PR +echo "$prs" | jq -c '.[]' | while read -r pr; do + PR_NUMBER=$(echo "$pr" | jq -r '.number') + PR_TITLE=$(echo "$pr" | jq -r '.title') + echo "PR #$PR_NUMBER - $PR_TITLE has conflicts." +done diff --git a/scripts/project_euler_answers.json b/scripts/project_euler_answers.json index 6d354363ee5f..f2b876934766 100644 --- a/scripts/project_euler_answers.json +++ b/scripts/project_euler_answers.json @@ -723,5 +723,112 @@ "722": "9687101dfe209fd65f57a10603baa38ba83c9152e43a8b802b96f1e07f568e0e", "723": "74832787e7d4e0cb7991256c8f6d02775dffec0684de234786f25f898003f2de", "724": "fa05e2b497e7eafa64574017a4c45aadef6b163d907b03d63ba3f4021096d329", - "725": "005c873563f51bbebfdb1f8dbc383259e9a98e506bc87ae8d8c9044b81fc6418" + "725": "005c873563f51bbebfdb1f8dbc383259e9a98e506bc87ae8d8c9044b81fc6418", + "726": "93e41c533136bf4b436e493090fd4e7b277234db2a69c62a871f775ff26681bf", + "727": "c366f7426ca9351dcdde2e3bea01181897cda4d9b44977678ea3828419b84851", + "728": "8de62a644511d27c7c23c7722f56112b3c1ab9b05a078a98a0891f09f92464c6", + "729": "0ae82177174eef99fc80a2ec921295f61a6ac4dfed86a1bf333a50c26d01955c", + "730": "78cd876a176c8fbf7c2155b80dccbdededdbc43c28ef17b5a6e554d649325d38", + "731": "54afb9f829be51d29f90eecbfe40e5ba91f3a3bf538de62f3e34674af15eb542", + "732": "c4dc4610dcafc806b30e5d3f5560b57f462218a04397809843a7110838f0ebac", + "733": "bdde7d98d057d6a6ae360fd2f872d8bccb7e7f2971df37a3c5f20712ea3c618f", + "734": "9a514875bd9af26fcc565337771f852d311cd77033186e4d957e7b6c7b8ce018", + "735": "8bbc5a27c0031d8c44f3f73c99622a202cd6ea9a080049d615a7ae80ce6024f9", + "736": "e0d4c78b9b3dae51940877aff28275d036eccfc641111c8e34227ff6015a0fab", + "737": "a600884bcaa01797310c83b198bad58c98530289305af29b0bf75f679af38d3a", + "738": "c85f15fdaafe7d5525acff960afef7e4b8ffded5a7ee0d1dc2b0e8d0c26b9b46", + "739": "8716e9302f0fb90153e2f522bd88a710361a897480e4ccc0542473c704793518", + "740": "6ff41ee34b263b742cda109aee3be9ad6c95eec2ce31d6a9fc5353bba1b41afd", + "741": "99ac0eb9589b895e5755895206bbad5febd6bc29b2912df1c7544c547e26bca3", + "742": "7d2761a240aa577348df4813ea248088d0d6d8d421142c712ed576cdc90d4df9", + "743": "d93c42a129c0961b4e36738efae3b7e8ffae3a4daeced20e85bb740d3d72522d", + "744": "211f76700a010461486dde6c723720be85e68c192cd8a8ed0a88860b8ae9b0f0", + "745": "2d32dc1fea2f1b8600c0ada927b057b566870ceb5362cce71ac3693dcb7136ae", + "746": "2df1c2a0181f0c25e8d13d2a1eadba55a6b06267a2b22075fcf6867fb2e10c02", + "747": "a8d8f93142e320c6f0dd386c7a3bfb011bbdc15b85291a9be8f0266b3608175e", + "748": "7de937e04c10386b240afb8bb2ff590009946df8b7850a0329ccdb59fca8955f", + "749": "1a55f5484ccf964aeb186faedefa01db05d87180891dc2280b6eb85b6efb4779", + "750": "fa4318c213179e6af1c949be7cf47210f4383e0a44d191e2bad44228d3192f14", + "751": "12fe650fcb3afc214b3d647c655070e8142cfd397441fc7636ad7e6ffcaefde2", + "752": "e416c0123bc6b82df8726b328494db31aa4781d938a0a6e2107b1e44c73c0434", + "753": "0ee3299bc89e1e4c2fc79285fb1cd84c887456358a825e56be92244b7115f5af", + "754": "1370574b16207c41d3dafb62aa898379ec101ac36843634b1633b7b509d4c35a", + "755": "78bb4b18b13f5254cfafe872c0e93791ab5206b2851960dc6aebea8f62b9580c", + "756": "6becaabbda2e9ea22373e62e989b6b70467efa24fbe2f0d124d7a99a53e93f74", + "757": "fbfee0a5c4fa57a1dd6cf0c9bb2423cf7e7bcb130e67114aa360e42234987314", + "758": "8e4dfc259cec9dfd89d4b4ac8c33c75af6e0f5f7926526ee22ad4d45f93d3c18", + "759": "40bac0ed2e4f7861a6d9a2d87191a9034e177c319aa40a43638cc1b69572e5f2", + "760": "7ab50386a211f0815593389ab05b57a1a5eb5cbf5b9a85fe4afc517dcab74e06", + "761": "1cdb0318ac16e11c8d2ae7b1d7ca7138f7b1a461e9d75bd69be0f9cdd3add0c5", + "762": "84c4662267d5809380a540dfc2881665b3019047d74d5ef0a01f86e45f4b5b59", + "763": "f0def5903139447fabe7d106db5fff660d94b45af7b8b48d789596cf65ab2514", + "764": "7b4131f4d1e13d091ca7dd4d32317a14a2a24e6e1abd214df1c14c215287b330", + "765": "7558b775727426bccd945f5aa6b3e131e6034a7b1ff8576332329ef65d6a1663", + "766": "23c309430fa9546adb617457dbfd30fb7432904595c8c000e9b67ea23f32a53b", + "767": "70aef22ac2db8a5bdfcc42ff8dafbd2901e85e268f5f3c45085aa40c590b1d42", + "768": "b69a808dfc654b037e2f47ace16f48fe3bb553b3c8eed3e2b6421942fbf521d0", + "769": "78537a30577e806c6d8d94725e54d2d52e56f7f39f89c133cd5d0a2aad7e46e4", + "770": "c9d80c19c4895d1498bf809fcc37c447fa961fb325e5667eb35d6aa992966b41", + "771": "9803ace30c0d90d422e703fdf25a10a9342d0178a277ebc20c7bd6feac4c7a15", + "772": "f5a1e391af815ea6453db58a1bd71790f433c44ed63e5e93d8f5c045dfd5a464", + "773": "e1b93fc323c4d9c383100603339548e1e56ce9c38bcdcc425024c12b862ea8cb", + "774": "3646cd098b213014fb7bbc9597871585e62ee0cf2770e141f1df771237cc09ab", + "775": "d9d7d515ce7350c9e5696d85f68bbb42daa74b9e171a601dd04c823b18bb7757", + "776": "83286074d3bc86a5b449facb5fe5eafc91eb4c8031e2fb5e716443402cd8ed0f", + "777": "e62616a387d05b619d47cee3d49d5d2db19393736bf54b6cdd20933c0531cb7e", + "778": "d4de958ba44d25353de5b380e04d06c7968794ad50dbf6231ad0049ff53e106b", + "779": "c08ce54a59afc4af62f28b80a9c9a5190822d124eed8d73fd6db3e19c81e2157", + "780": "fc7ba646c16482f0f4f5ce2b06d21183dba2bdeaf9469b36b55bc7bc2d87baf3", + "781": "8fa5733f06838fb61b55b3e9d59c5061d922147e59947fe52e566dd975b2199f", + "782": "9f757d92df401ee049bc066bb2625c6287e5e4bcd38c958396a77a578f036a24", + "783": "270ff37f60c267a673bd4b223e44941f01ae9cfbf6bbdf99ca57af89b1e9a66f", + "784": "388b17c4c7b829cef767f83b4686c903faeec1241edfe5f58ee91d2b0c7f8dfc", + "785": "77cf600204c5265e1d5d3d26bf28ba1e92e6f24def040c16977450bec8b1cb99", + "786": "fb14022b7edbc6c7bfde27f35b49f6acaa4f0fc383af27614cb9d4a1980e626b", + "787": "7516ba0ac1951665723dcc4adcc52764d9497e7b6ed30bdb9937ac9df82b7c4f", + "788": "adede1d30258bb0f353af11f559b67f8b823304c71e967f52db52d002760c24f", + "789": "0c82e744a1f9bc57fd8ae8b2f479998455bc45126de971c59b68541c254e303a", + "790": "319847122251afd20d4d650047c55981a509fa2be78abd7c9c3caa0555e60a05", + "791": "2e0bbdcd0a8460e1e33c55668d0dc9752379a78b9f3561d7a17b922a5541a3fb", + "792": "5f77834c5a509023dd95dd98411eae1dd4bafd125deca590632f409f92fd257b", + "793": "dbfd900a3b31eeec2f14b916f5151611541cb716d80b7b9a1229de12293a02ea", + "794": "d019fe415aba832c4c761140d60c466c9aaad52b504df3167c17f2d3f0b277a7", + "795": "617b259349da44c2af2664acde113673ab3bb03a85d31f1be8f01027d0ebd4d3", + "796": "cba6b30a818d073398e5802211987f0897523e4752987bb445b2bca079670e22", + "797": "61e42cac3d7858b8850111a8c64c56432a18dd058dfb6afd773f07d703703b1a", + "798": "ae8b155d6b77522af79f7e4017fefe92aaa5d45eff132c83dc4d4bcfc9686020", + "799": "a41cb14ddf8f1948a01f590fbe53d9ca4e2faf48375ce1c306f91acf7c94e005", + "800": "c6a47bc6f02cf06be16728fb308c83f2f2ae350325ef7016867f5bdaea849d71", + "801": "d14b358c76b55106613f9c0a2112393338dfd01513b0fd231b79fc8db20e41f0", + "802": "22ae33e67fb48accfaa3b36e70c5a19066b974194c3130680de0c7cdce2d0f2e", + "803": "d95b3f9bbb7054042c1fba4db02f7223a2dad94977a36f08c8aaf92f373f9e78", + "804": "b0b1cf7253593eb2334c75e66dbe22b4b4540347485f1ea24e80226b4b18171c", + "805": "41b1ff5db0e70984ad20c50d1a9ac2b5a53ccd5f42796c8e948ae8880005fbb9", + "806": "b9c813beb39671adb8e1530555cadca44c21ddc7127932274918df2091dbd9ca", + "807": "745fd9ba97970d85a29877942839e41fc192794420e86f3bde39fd26db7a8bff", + "808": "6c73b947eb603602a7e8afadc83eaaa381a46db8b82a6fb89c9c1d93cb023fce", + "809": "eebac7753da4c1230dfce0f15fc124ffff01b0e432f0b74623b60cff71bbc9a9", + "810": "42be7899672a1a0046823603ce60dbeda7250a56fcb8d0913093850c85394307", + "811": "8698cd28ae4d93db36631870c33e4a8a527d970050d994666115f54260b64138", + "812": "dc2495924f37353db8b846323b8085fae9db502e890c513ed2e64ed7281f567f", + "813": "92179dde05aa6557baca65699fda50ca024d33a77078d8e128caa3c5db84064b", + "814": "344ed8cb7684307c00b7f03d751729a7f9d2a5f4a4cb4574594113d69593c0c1", + "815": "f642cf15345af3feab60e26a02aee038f759914906a5b2b469b46fdeee50ff59", + "816": "058178444e85f2aedb2f75d824a469747381f0bd3235d8c72df4385fec86eb07", + "817": "582fdc2233298192b09ceaf1463d6be06a09894075532630aa9d9efcfcb31da4", + "818": "67f6964d6ff114a43371b8375c44db2f1362df4f110b4a7ce8d79cf1b76621a0", + "819": "c7a82513ad48dfc87f2c1e0f2915b71464b7f5a16501c71df4ae4a8741dceef3", + "820": "9b23ae0181f320aadda2637ac2179c8b41b00715630c3acb643c7aee3b81cf90", + "821": "0941e396ff15b98fd7827de8e33ef94996d48ba719a88ba8e2da7f2605df3e5c", + "822": "ed8ef7f568939b9df1b77ae58344940b91c7e154a4367fe2b179bc7b9484d4e6", + "823": "05139328571a86096032b57e3a6a02a61acad4fb0d8f8e1b5d0ffb0d063ba697", + "826": "7f40f14ca65e5c06dd9ec9bbb212adb4d97a503199cb3c30ed921a04373bbe1c", + "827": "80461f02c63654c642382a6ffb7a44d0a3554434dfcfcea00ba91537724c7106", + "828": "520c196175625a0230afb76579ea26033372de3ef4c78aceb146b84322bfa871", + "829": "ed0089e61cf5540dd4a8fef1c468b96cf57f1d2bb79968755ba856d547ddafdf", + "831": "8ec445084427419ca6da405e0ded9814a4b4e11a2be84d88a8dea421f8e49992", + "832": "cfcb9ebef9308823f64798b5e12a59bf77ff6f92b0eae3790a61c0a26f577010", + "833": "e6ff3a5b257eb53366a32bfc8ea410a00a78bafa63650c76ac2bceddfbb42ff5", + "834": "b0d2a7e7d629ef14db9e7352a9a06d6ca66f750429170bb169ca52c172b8cc96", + "835": "bdfa1b1eecbad79f5de48bc6daee4d2b07689d7fb172aa306dd6094172b396f0" } diff --git a/scripts/validate_filenames.py b/scripts/validate_filenames.py index ed23f3907114..e76b4dbfe288 100755 --- a/scripts/validate_filenames.py +++ b/scripts/validate_filenames.py @@ -1,10 +1,10 @@ -#!/usr/bin/env python3 +#!python import os try: from .build_directory_md import good_file_paths except ImportError: - from build_directory_md import good_file_paths # type: ignore + from build_directory_md import good_file_paths # type: ignore[no-redef] filepaths = list(good_file_paths()) assert filepaths, "good_file_paths() failed!" diff --git a/scripts/validate_solutions.py b/scripts/validate_solutions.py index ca4af5261a8f..df5d01086bbe 100755 --- a/scripts/validate_solutions.py +++ b/scripts/validate_solutions.py @@ -21,8 +21,8 @@ def convert_path_to_module(file_path: pathlib.Path) -> ModuleType: """Converts a file path to a Python module""" spec = importlib.util.spec_from_file_location(file_path.name, str(file_path)) - module = importlib.util.module_from_spec(spec) # type: ignore - spec.loader.exec_module(module) # type: ignore + module = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + spec.loader.exec_module(module) # type: ignore[union-attr] return module @@ -57,7 +57,7 @@ def added_solution_file_path() -> list[pathlib.Path]: "Accept": "application/vnd.github.v3+json", "Authorization": "token " + os.environ["GITHUB_TOKEN"], } - files = requests.get(get_files_url(), headers=headers).json() + files = requests.get(get_files_url(), headers=headers, timeout=10).json() for file in files: filepath = pathlib.Path.cwd().joinpath(file["filename"]) if ( @@ -71,10 +71,13 @@ def added_solution_file_path() -> list[pathlib.Path]: def collect_solution_file_paths() -> list[pathlib.Path]: - if os.environ.get("CI") and os.environ.get("GITHUB_EVENT_NAME") == "pull_request": - # Return only if there are any, otherwise default to all solutions - if filepaths := added_solution_file_path(): - return filepaths + # Return only if there are any, otherwise default to all solutions + if ( + os.environ.get("CI") + and os.environ.get("GITHUB_EVENT_NAME") == "pull_request" + and (filepaths := added_solution_file_path()) + ): + return filepaths return all_solution_file_paths() @@ -89,8 +92,8 @@ def test_project_euler(solution_path: pathlib.Path) -> None: problem_number: str = solution_path.parent.name[8:].zfill(3) expected: str = PROBLEM_ANSWERS[problem_number] solution_module = convert_path_to_module(solution_path) - answer = str(solution_module.solution()) # type: ignore + answer = str(solution_module.solution()) answer = hashlib.sha256(answer.encode()).hexdigest() - assert ( - answer == expected - ), f"Expected solution to {problem_number} to have hash {expected}, got {answer}" + assert answer == expected, ( + f"Expected solution to {problem_number} to have hash {expected}, got {answer}" + ) diff --git a/searches/binary_search.py b/searches/binary_search.py index 05dadd4fe965..2e66b672d5b4 100644 --- a/searches/binary_search.py +++ b/searches/binary_search.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 """ -This is pure Python implementation of binary search algorithms +Pure Python implementations of binary search algorithms -For doctests run following command: +For doctests run the following command: python3 -m doctest -v binary_search.py For manual testing run: python3 binary_search.py """ + from __future__ import annotations import bisect @@ -34,16 +35,12 @@ def bisect_left( Examples: >>> bisect_left([0, 5, 7, 10, 15], 0) 0 - >>> bisect_left([0, 5, 7, 10, 15], 6) 2 - >>> bisect_left([0, 5, 7, 10, 15], 20) 5 - >>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3) 3 - >>> bisect_left([0, 5, 7, 10, 15], 6, 2) 2 """ @@ -79,16 +76,12 @@ def bisect_right( Examples: >>> bisect_right([0, 5, 7, 10, 15], 0) 1 - >>> bisect_right([0, 5, 7, 10, 15], 15) 5 - >>> bisect_right([0, 5, 7, 10, 15], 6) 2 - >>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3) 3 - >>> bisect_right([0, 5, 7, 10, 15], 6, 2) 2 """ @@ -124,7 +117,6 @@ def insort_left( >>> insort_left(sorted_collection, 6) >>> sorted_collection [0, 5, 6, 7, 10, 15] - >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] >>> item = (5, 5) >>> insort_left(sorted_collection, item) @@ -134,12 +126,10 @@ def insort_left( True >>> item is sorted_collection[2] False - >>> sorted_collection = [0, 5, 7, 10, 15] >>> insort_left(sorted_collection, 20) >>> sorted_collection [0, 5, 7, 10, 15, 20] - >>> sorted_collection = [0, 5, 7, 10, 15] >>> insort_left(sorted_collection, 15, 1, 3) >>> sorted_collection @@ -167,7 +157,6 @@ def insort_right( >>> insort_right(sorted_collection, 6) >>> sorted_collection [0, 5, 6, 7, 10, 15] - >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] >>> item = (5, 5) >>> insort_right(sorted_collection, item) @@ -177,12 +166,10 @@ def insort_right( False >>> item is sorted_collection[2] True - >>> sorted_collection = [0, 5, 7, 10, 15] >>> insort_right(sorted_collection, 20) >>> sorted_collection [0, 5, 7, 10, 15, 20] - >>> sorted_collection = [0, 5, 7, 10, 15] >>> insort_right(sorted_collection, 15, 1, 3) >>> sorted_collection @@ -191,29 +178,28 @@ def insort_right( sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item) -def binary_search(sorted_collection: list[int], item: int) -> int | None: - """Pure implementation of binary search algorithm in Python +def binary_search(sorted_collection: list[int], item: int) -> int: + """Pure implementation of a binary search algorithm in Python - Be careful collection must be ascending sorted, otherwise result will be + Be careful collection must be ascending sorted otherwise, the result will be unpredictable :param sorted_collection: some ascending sorted collection with comparable items :param item: item value to search - :return: index of found item or None if item is not found + :return: index of the found item or -1 if the item is not found Examples: >>> binary_search([0, 5, 7, 10, 15], 0) 0 - >>> binary_search([0, 5, 7, 10, 15], 15) 4 - >>> binary_search([0, 5, 7, 10, 15], 5) 1 - >>> binary_search([0, 5, 7, 10, 15], 6) - + -1 """ + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") left = 0 right = len(sorted_collection) - 1 @@ -226,66 +212,66 @@ def binary_search(sorted_collection: list[int], item: int) -> int | None: right = midpoint - 1 else: left = midpoint + 1 - return None + return -1 -def binary_search_std_lib(sorted_collection: list[int], item: int) -> int | None: - """Pure implementation of binary search algorithm in Python using stdlib +def binary_search_std_lib(sorted_collection: list[int], item: int) -> int: + """Pure implementation of a binary search algorithm in Python using stdlib - Be careful collection must be ascending sorted, otherwise result will be + Be careful collection must be ascending sorted otherwise, the result will be unpredictable :param sorted_collection: some ascending sorted collection with comparable items :param item: item value to search - :return: index of found item or None if item is not found + :return: index of the found item or -1 if the item is not found Examples: >>> binary_search_std_lib([0, 5, 7, 10, 15], 0) 0 - >>> binary_search_std_lib([0, 5, 7, 10, 15], 15) 4 - >>> binary_search_std_lib([0, 5, 7, 10, 15], 5) 1 - >>> binary_search_std_lib([0, 5, 7, 10, 15], 6) - + -1 """ + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") index = bisect.bisect_left(sorted_collection, item) if index != len(sorted_collection) and sorted_collection[index] == item: return index - return None + return -1 def binary_search_by_recursion( - sorted_collection: list[int], item: int, left: int, right: int -) -> int | None: - """Pure implementation of binary search algorithm in Python by recursion + sorted_collection: list[int], item: int, left: int = 0, right: int = -1 +) -> int: + """Pure implementation of a binary search algorithm in Python by recursion - Be careful collection must be ascending sorted, otherwise result will be + Be careful collection must be ascending sorted otherwise, the result will be unpredictable First recursion should be started with left=0 and right=(len(sorted_collection)-1) :param sorted_collection: some ascending sorted collection with comparable items :param item: item value to search - :return: index of found item or None if item is not found + :return: index of the found item or -1 if the item is not found Examples: >>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4) 0 - >>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4) 4 - >>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4) 1 - >>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4) - + -1 """ + if right < 0: + right = len(sorted_collection) - 1 + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") if right < left: - return None + return -1 midpoint = left + (right - left) // 2 @@ -297,12 +283,78 @@ def binary_search_by_recursion( return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right) +def exponential_search(sorted_collection: list[int], item: int) -> int: + """Pure implementation of an exponential search algorithm in Python + Resources used: + https://en.wikipedia.org/wiki/Exponential_search + + Be careful collection must be ascending sorted otherwise, result will be + unpredictable + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item value to search + :return: index of the found item or -1 if the item is not found + + the order of this algorithm is O(lg I) where I is index position of item if exist + + Examples: + >>> exponential_search([0, 5, 7, 10, 15], 0) + 0 + >>> exponential_search([0, 5, 7, 10, 15], 15) + 4 + >>> exponential_search([0, 5, 7, 10, 15], 5) + 1 + >>> exponential_search([0, 5, 7, 10, 15], 6) + -1 + """ + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") + bound = 1 + while bound < len(sorted_collection) and sorted_collection[bound] < item: + bound *= 2 + left = bound // 2 + right = min(bound, len(sorted_collection) - 1) + last_result = binary_search_by_recursion( + sorted_collection=sorted_collection, item=item, left=left, right=right + ) + if last_result is None: + return -1 + return last_result + + +searches = ( # Fastest to slowest... + binary_search_std_lib, + binary_search, + exponential_search, + binary_search_by_recursion, +) + + if __name__ == "__main__": - user_input = input("Enter numbers separated by comma:\n").strip() + import doctest + import timeit + + doctest.testmod() + for search in searches: + name = f"{search.__name__:>26}" + print(f"{name}: {search([0, 5, 7, 10, 15], 10) = }") # type: ignore[operator] + + print("\nBenchmarks...") + setup = "collection = range(1000)" + for search in searches: + name = search.__name__ + print( + f"{name:>26}:", + timeit.timeit( + f"{name}(collection, 500)", setup=setup, number=5_000, globals=globals() + ), + ) + + user_input = input("\nEnter numbers separated by comma: ").strip() collection = sorted(int(item) for item in user_input.split(",")) - target = int(input("Enter a single number to be found in the list:\n")) - result = binary_search(collection, target) - if result is None: + target = int(input("Enter a single number to be found in the list: ")) + result = binary_search(sorted_collection=collection, item=target) + if result == -1: print(f"{target} was not found in {collection}.") else: - print(f"{target} was found at position {result} in {collection}.") + print(f"{target} was found at position {result} of {collection}.") diff --git a/searches/binary_tree_traversal.py b/searches/binary_tree_traversal.py index 66814b47883d..47af57f7f94d 100644 --- a/searches/binary_tree_traversal.py +++ b/searches/binary_tree_traversal.py @@ -1,6 +1,7 @@ """ This is pure Python implementation of tree traversal algorithms """ + from __future__ import annotations import queue @@ -13,11 +14,9 @@ def __init__(self, data): self.left = None -def build_tree(): +def build_tree() -> TreeNode: print("\n********Press N to stop entering at any point of time********\n") - check = input("Enter the value of the root node: ").strip().lower() or "n" - if check == "n": - return None + check = input("Enter the value of the root node: ").strip().lower() q: queue.Queue = queue.Queue() tree_node = TreeNode(int(check)) q.put(tree_node) @@ -37,6 +36,7 @@ def build_tree(): right_node = TreeNode(int(check)) node_found.right = right_node q.put(right_node) + raise ValueError("Something went wrong") def pre_order(node: TreeNode) -> None: @@ -164,8 +164,8 @@ def level_order_actual(node: TreeNode) -> None: if node_dequeued.right: list_.append(node_dequeued.right) print() - for node in list_: - q.put(node) + for inner_node in list_: + q.put(inner_node) # iteration version @@ -271,7 +271,7 @@ def prompt(s: str = "", width=50, char="*") -> str: doctest.testmod() print(prompt("Binary Tree Traversals")) - node = build_tree() + node: TreeNode = build_tree() print(prompt("Pre Order Traversal")) pre_order(node) print(prompt() + "\n") diff --git a/searches/exponential_search.py b/searches/exponential_search.py new file mode 100644 index 000000000000..ed09b14e101c --- /dev/null +++ b/searches/exponential_search.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +""" +Pure Python implementation of exponential search algorithm + +For more information, see the Wikipedia page: +https://en.wikipedia.org/wiki/Exponential_search + +For doctests run the following command: +python3 -m doctest -v exponential_search.py + +For manual testing run: +python3 exponential_search.py +""" + +from __future__ import annotations + + +def binary_search_by_recursion( + sorted_collection: list[int], item: int, left: int = 0, right: int = -1 +) -> int: + """Pure implementation of binary search algorithm in Python using recursion + + Be careful: the collection must be ascending sorted otherwise, the result will be + unpredictable. + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item value to search + :param left: starting index for the search + :param right: ending index for the search + :return: index of the found item or -1 if the item is not found + + Examples: + >>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4) + 0 + >>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4) + 4 + >>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4) + 1 + >>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4) + -1 + """ + if right < 0: + right = len(sorted_collection) - 1 + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") + if right < left: + return -1 + + midpoint = left + (right - left) // 2 + + if sorted_collection[midpoint] == item: + return midpoint + elif sorted_collection[midpoint] > item: + return binary_search_by_recursion(sorted_collection, item, left, midpoint - 1) + else: + return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right) + + +def exponential_search(sorted_collection: list[int], item: int) -> int: + """ + Pure implementation of an exponential search algorithm in Python. + For more information, refer to: + https://en.wikipedia.org/wiki/Exponential_search + + Be careful: the collection must be ascending sorted, otherwise the result will be + unpredictable. + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item value to search + :return: index of the found item or -1 if the item is not found + + The time complexity of this algorithm is O(log i) where i is the index of the item. + + Examples: + >>> exponential_search([0, 5, 7, 10, 15], 0) + 0 + >>> exponential_search([0, 5, 7, 10, 15], 15) + 4 + >>> exponential_search([0, 5, 7, 10, 15], 5) + 1 + >>> exponential_search([0, 5, 7, 10, 15], 6) + -1 + """ + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") + + if sorted_collection[0] == item: + return 0 + + bound = 1 + while bound < len(sorted_collection) and sorted_collection[bound] < item: + bound *= 2 + + left = bound // 2 + right = min(bound, len(sorted_collection) - 1) + return binary_search_by_recursion(sorted_collection, item, left, right) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Manual testing + user_input = input("Enter numbers separated by commas: ").strip() + collection = sorted(int(item) for item in user_input.split(",")) + target = int(input("Enter a number to search for: ")) + result = exponential_search(sorted_collection=collection, item=target) + if result == -1: + print(f"{target} was not found in {collection}.") + else: + print(f"{target} was found at index {result} in {collection}.") diff --git a/searches/fibonacci_search.py b/searches/fibonacci_search.py index 55fc05d39eeb..7b2252a68be2 100644 --- a/searches/fibonacci_search.py +++ b/searches/fibonacci_search.py @@ -10,6 +10,7 @@ For manual testing run: python3 fibonacci_search.py """ + from functools import lru_cache @@ -122,8 +123,7 @@ def fibonacci_search(arr: list, val: int) -> int: elif val > item_k_1: offset += fibonacci(fibb_k - 1) fibb_k -= 2 - else: - return -1 + return -1 if __name__ == "__main__": diff --git a/searches/hill_climbing.py b/searches/hill_climbing.py index 83a3b8b74e27..689b7e5cca8f 100644 --- a/searches/hill_climbing.py +++ b/searches/hill_climbing.py @@ -137,11 +137,10 @@ def hill_climbing( if change > max_change and change > 0: max_change = change next_state = neighbor - else: # finding min + elif change < min_change and change < 0: # finding min # to direction with greatest descent - if change < min_change and change < 0: - min_change = change - next_state = neighbor + min_change = change + next_state = neighbor if next_state is not None: # we found at least one neighbor which improved the current state current_state = next_state diff --git a/searches/interpolation_search.py b/searches/interpolation_search.py index 49194c2600a0..cb3e0011d0da 100644 --- a/searches/interpolation_search.py +++ b/searches/interpolation_search.py @@ -3,13 +3,41 @@ """ -def interpolation_search(sorted_collection, item): - """Pure implementation of interpolation search algorithm in Python - Be careful collection must be ascending sorted, otherwise result will be - unpredictable - :param sorted_collection: some ascending sorted collection with comparable items - :param item: item value to search - :return: index of found item or None if item is not found +def interpolation_search(sorted_collection: list[int], item: int) -> int | None: + """ + Searches for an item in a sorted collection by interpolation search algorithm. + + Args: + sorted_collection: sorted list of integers + item: item value to search + + Returns: + int: The index of the found item, or None if the item is not found. + Examples: + >>> interpolation_search([1, 2, 3, 4, 5], 2) + 1 + >>> interpolation_search([1, 2, 3, 4, 5], 4) + 3 + >>> interpolation_search([1, 2, 3, 4, 5], 6) is None + True + >>> interpolation_search([], 1) is None + True + >>> interpolation_search([100], 100) + 0 + >>> interpolation_search([1, 2, 3, 4, 5], 0) is None + True + >>> interpolation_search([1, 2, 3, 4, 5], 7) is None + True + >>> interpolation_search([1, 2, 3, 4, 5], 2) + 1 + >>> interpolation_search([1, 2, 3, 4, 5], 0) is None + True + >>> interpolation_search([1, 2, 3, 4, 5], 7) is None + True + >>> interpolation_search([1, 2, 3, 4, 5], 2) + 1 + >>> interpolation_search([5, 5, 5, 5, 5], 3) is None + True """ left = 0 right = len(sorted_collection) - 1 @@ -19,8 +47,7 @@ def interpolation_search(sorted_collection, item): if sorted_collection[left] == sorted_collection[right]: if sorted_collection[left] == item: return left - else: - return None + return None point = left + ((item - sorted_collection[left]) * (right - left)) // ( sorted_collection[right] - sorted_collection[left] @@ -33,37 +60,55 @@ def interpolation_search(sorted_collection, item): current_item = sorted_collection[point] if current_item == item: return point + if point < left: + right = left + left = point + elif point > right: + left = right + right = point + elif item < current_item: + right = point - 1 else: - if point < left: - right = left - left = point - elif point > right: - left = right - right = point - else: - if item < current_item: - right = point - 1 - else: - left = point + 1 + left = point + 1 return None -def interpolation_search_by_recursion(sorted_collection, item, left, right): +def interpolation_search_by_recursion( + sorted_collection: list[int], item: int, left: int = 0, right: int | None = None +) -> int | None: """Pure implementation of interpolation search algorithm in Python by recursion Be careful collection must be ascending sorted, otherwise result will be unpredictable First recursion should be started with left=0 and right=(len(sorted_collection)-1) - :param sorted_collection: some ascending sorted collection with comparable items - :param item: item value to search - :return: index of found item or None if item is not found - """ + Args: + sorted_collection: some sorted collection with comparable items + item: item value to search + left: left index in collection + right: right index in collection + + Returns: + index of item in collection or None if item is not present + + Examples: + >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 0) + 0 + >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 15) + 4 + >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 5) + 1 + >>> interpolation_search_by_recursion([0, 5, 7, 10, 15], 100) is None + True + >>> interpolation_search_by_recursion([5, 5, 5, 5, 5], 3) is None + True + """ + if right is None: + right = len(sorted_collection) - 1 # avoid divided by 0 during interpolation if sorted_collection[left] == sorted_collection[right]: if sorted_collection[left] == item: return left - else: - return None + return None point = left + ((item - sorted_collection[left]) * (right - left)) // ( sorted_collection[right] - sorted_collection[left] @@ -75,65 +120,18 @@ def interpolation_search_by_recursion(sorted_collection, item, left, right): if sorted_collection[point] == item: return point - elif point < left: + if point < left: return interpolation_search_by_recursion(sorted_collection, item, point, left) - elif point > right: + if point > right: return interpolation_search_by_recursion(sorted_collection, item, right, left) - else: - if sorted_collection[point] > item: - return interpolation_search_by_recursion( - sorted_collection, item, left, point - 1 - ) - else: - return interpolation_search_by_recursion( - sorted_collection, item, point + 1, right - ) - - -def __assert_sorted(collection): - """Check if collection is ascending sorted, if not - raises :py:class:`ValueError` - :param collection: collection - :return: True if collection is ascending sorted - :raise: :py:class:`ValueError` if collection is not ascending sorted - Examples: - >>> __assert_sorted([0, 1, 2, 4]) - True - >>> __assert_sorted([10, -1, 5]) - Traceback (most recent call last): - ... - ValueError: Collection must be ascending sorted - """ - if collection != sorted(collection): - raise ValueError("Collection must be ascending sorted") - return True + if sorted_collection[point] > item: + return interpolation_search_by_recursion( + sorted_collection, item, left, point - 1 + ) + return interpolation_search_by_recursion(sorted_collection, item, point + 1, right) if __name__ == "__main__": - import sys + import doctest - """ - user_input = input('Enter numbers separated by comma:\n').strip() - collection = [int(item) for item in user_input.split(',')] - try: - __assert_sorted(collection) - except ValueError: - sys.exit('Sequence must be ascending sorted to apply interpolation search') - - target_input = input('Enter a single number to be found in the list:\n') - target = int(target_input) - """ - - debug = 0 - if debug == 1: - collection = [10, 30, 40, 45, 50, 66, 77, 93] - try: - __assert_sorted(collection) - except ValueError: - sys.exit("Sequence must be ascending sorted to apply interpolation search") - target = 67 - - result = interpolation_search(collection, target) - if result is not None: - print(f"{target} found at positions: {result}") - else: - print("Not found") + doctest.testmod() diff --git a/searches/jump_search.py b/searches/jump_search.py index 31a9656c55fe..e72d85e8a868 100644 --- a/searches/jump_search.py +++ b/searches/jump_search.py @@ -4,14 +4,27 @@ until the element compared is bigger than the one searched. It will then perform a linear search until it matches the wanted number. If not found, it returns -1. + +https://en.wikipedia.org/wiki/Jump_search """ import math +from collections.abc import Sequence +from typing import Any, Protocol, TypeVar + + +class Comparable(Protocol): + def __lt__(self, other: Any, /) -> bool: ... + +T = TypeVar("T", bound=Comparable) -def jump_search(arr: list, x: int) -> int: + +def jump_search(arr: Sequence[T], item: T) -> int: """ - Pure Python implementation of the jump search algorithm. + Python implementation of the jump search algorithm. + Return the index if the `item` is found, otherwise return -1. + Examples: >>> jump_search([0, 1, 2, 3, 4, 5], 3) 3 @@ -21,31 +34,36 @@ def jump_search(arr: list, x: int) -> int: -1 >>> jump_search([0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610], 55) 10 + >>> jump_search(["aa", "bb", "cc", "dd", "ee", "ff"], "ee") + 4 """ - n = len(arr) - step = int(math.floor(math.sqrt(n))) + arr_size = len(arr) + block_size = int(math.sqrt(arr_size)) + prev = 0 - while arr[min(step, n) - 1] < x: + step = block_size + while arr[min(step, arr_size) - 1] < item: prev = step - step += int(math.floor(math.sqrt(n))) - if prev >= n: + step += block_size + if prev >= arr_size: return -1 - while arr[prev] < x: - prev = prev + 1 - if prev == min(step, n): + while arr[prev] < item: + prev += 1 + if prev == min(step, arr_size): return -1 - if arr[prev] == x: + if arr[prev] == item: return prev return -1 if __name__ == "__main__": user_input = input("Enter numbers separated by a comma:\n").strip() - arr = [int(item) for item in user_input.split(",")] + array = [int(item) for item in user_input.split(",")] x = int(input("Enter the number to be searched:\n")) - res = jump_search(arr, x) + + res = jump_search(array, x) if res == -1: print("Number not found!") else: diff --git a/searches/linear_search.py b/searches/linear_search.py index 777080d14e36..ba6e81d6bae4 100644 --- a/searches/linear_search.py +++ b/searches/linear_search.py @@ -15,7 +15,7 @@ def linear_search(sequence: list, target: int) -> int: :param sequence: a collection with comparable items (as sorted items not required in Linear Search) :param target: item value to search - :return: index of found item or None if item is not found + :return: index of found item or -1 if item is not found Examples: >>> linear_search([0, 5, 7, 10, 15], 0) diff --git a/searches/median_of_medians.py b/searches/median_of_medians.py new file mode 100644 index 000000000000..a8011a34af76 --- /dev/null +++ b/searches/median_of_medians.py @@ -0,0 +1,107 @@ +""" +A Python implementation of the Median of Medians algorithm +to select pivots for quick_select, which is efficient for +calculating the value that would appear in the index of a +list if it would be sorted, even if it is not already +sorted. Search in time complexity O(n) at any rank +deterministically +https://en.wikipedia.org/wiki/Median_of_medians +""" + + +def median_of_five(arr: list) -> int: + """ + Return the median of the input list + :param arr: Array to find median of + :return: median of arr + + >>> median_of_five([2, 4, 5, 7, 899]) + 5 + >>> median_of_five([5, 7, 899, 54, 32]) + 32 + >>> median_of_five([5, 4, 3, 2]) + 4 + >>> median_of_five([3, 5, 7, 10, 2]) + 5 + """ + arr = sorted(arr) + return arr[len(arr) // 2] + + +def median_of_medians(arr: list) -> int: + """ + Return a pivot to partition data on by calculating + Median of medians of input data + :param arr: The data to be checked (a list) + :return: median of medians of input array + + >>> median_of_medians([2, 4, 5, 7, 899, 54, 32]) + 54 + >>> median_of_medians([5, 7, 899, 54, 32]) + 32 + >>> median_of_medians([5, 4, 3, 2]) + 4 + >>> median_of_medians([3, 5, 7, 10, 2, 12]) + 12 + """ + + if len(arr) <= 5: + return median_of_five(arr) + medians = [] + i = 0 + while i < len(arr): + if (i + 4) <= len(arr): + medians.append(median_of_five(arr[i:].copy())) + else: + medians.append(median_of_five(arr[i : i + 5].copy())) + i += 5 + return median_of_medians(medians) + + +def quick_select(arr: list, target: int) -> int: + """ + Two way partition the data into smaller and greater lists, + in relationship to the pivot + :param arr: The data to be searched (a list) + :param target: The rank to be searched + :return: element at rank target + + >>> quick_select([2, 4, 5, 7, 899, 54, 32], 5) + 32 + >>> quick_select([2, 4, 5, 7, 899, 54, 32], 1) + 2 + >>> quick_select([5, 4, 3, 2], 2) + 3 + >>> quick_select([3, 5, 7, 10, 2, 12], 3) + 5 + """ + + # Invalid Input + if target > len(arr): + return -1 + + # x is the estimated pivot by median of medians algorithm + x = median_of_medians(arr) + left = [] + right = [] + check = False + for i in range(len(arr)): + if arr[i] < x: + left.append(arr[i]) + elif arr[i] > x: + right.append(arr[i]) + elif arr[i] == x and not check: + check = True + else: + right.append(arr[i]) + rank_x = len(left) + 1 + if rank_x == target: + answer = x + elif rank_x > target: + answer = quick_select(left, target) + elif rank_x < target: + answer = quick_select(right, target - rank_x) + return answer + + +print(median_of_five([5, 4, 3, 2])) diff --git a/searches/quick_select.py b/searches/quick_select.py index 5ede8c4dd07f..c8282e1fa5fc 100644 --- a/searches/quick_select.py +++ b/searches/quick_select.py @@ -4,6 +4,7 @@ sorted, even if it is not already sorted https://en.wikipedia.org/wiki/Quickselect """ + import random diff --git a/searches/simple_binary_search.py b/searches/simple_binary_search.py index ff043d7369af..00e83ff9e4a3 100644 --- a/searches/simple_binary_search.py +++ b/searches/simple_binary_search.py @@ -7,6 +7,7 @@ For manual testing run: python3 simple_binary_search.py """ + from __future__ import annotations diff --git a/searches/tabu_search.py b/searches/tabu_search.py index d998ddc55976..fd482a81224c 100644 --- a/searches/tabu_search.py +++ b/searches/tabu_search.py @@ -24,6 +24,7 @@ -s size_of_tabu_search e.g. python tabu_search.py -f tabudata2.txt -i 4 -s 3 """ + import argparse import copy diff --git a/searches/ternary_search.py b/searches/ternary_search.py index cb36e72faac6..73e4b1ddc68b 100644 --- a/searches/ternary_search.py +++ b/searches/ternary_search.py @@ -6,6 +6,7 @@ Time Complexity : O(log3 N) Space Complexity : O(1) """ + from __future__ import annotations # This is the precision for this function which can be altered. @@ -105,8 +106,7 @@ def ite_ternary_search(array: list[int], target: int) -> int: else: left = one_third + 1 right = two_third - 1 - else: - return -1 + return -1 def rec_ternary_search(left: int, right: int, array: list[int], target: int) -> int: diff --git a/sorts/README.md b/sorts/README.md index ceb0207c2be4..f24427d582e7 100644 --- a/sorts/README.md +++ b/sorts/README.md @@ -4,7 +4,7 @@ is specified by the sorting algorithm. The most typical orders are lexical or nu of sorting lies in the fact that, if data is stored in a sorted manner, data searching can be highly optimised. Another use for sorting is to represent data in a more readable manner. -This section contains a lot of important algorithms that helps us to use sorting algorithms in various scenarios. +This section contains a lot of important algorithms that help us to use sorting algorithms in various scenarios. ## References * * diff --git a/sorts/bead_sort.py b/sorts/bead_sort.py index e51173643d81..8ce0619fd573 100644 --- a/sorts/bead_sort.py +++ b/sorts/bead_sort.py @@ -31,7 +31,7 @@ def bead_sort(sequence: list) -> list: if any(not isinstance(x, int) or x < 0 for x in sequence): raise TypeError("Sequence must be list of non-negative integers") for _ in range(len(sequence)): - for i, (rod_upper, rod_lower) in enumerate(zip(sequence, sequence[1:])): + for i, (rod_upper, rod_lower) in enumerate(zip(sequence, sequence[1:])): # noqa: RUF007 if rod_upper > rod_lower: sequence[i] -= rod_upper - rod_lower sequence[i + 1] += rod_upper - rod_lower diff --git a/sorts/binary_insertion_sort.py b/sorts/binary_insertion_sort.py new file mode 100644 index 000000000000..50653a99e7ce --- /dev/null +++ b/sorts/binary_insertion_sort.py @@ -0,0 +1,66 @@ +""" +This is a pure Python implementation of the binary insertion sort algorithm + +For doctests run following command: +python -m doctest -v binary_insertion_sort.py +or +python3 -m doctest -v binary_insertion_sort.py + +For manual testing run: +python binary_insertion_sort.py +""" + + +def binary_insertion_sort(collection: list) -> list: + """ + Sorts a list using the binary insertion sort algorithm. + + :param collection: A mutable ordered collection with comparable items. + :return: The same collection ordered in ascending order. + + Examples: + >>> binary_insertion_sort([0, 4, 1234, 4, 1]) + [0, 1, 4, 4, 1234] + >>> binary_insertion_sort([]) == sorted([]) + True + >>> binary_insertion_sort([-1, -2, -3]) == sorted([-1, -2, -3]) + True + >>> lst = ['d', 'a', 'b', 'e', 'c'] + >>> binary_insertion_sort(lst) == sorted(lst) + True + >>> import random + >>> collection = random.sample(range(-50, 50), 100) + >>> binary_insertion_sort(collection) == sorted(collection) + True + >>> import string + >>> collection = random.choices(string.ascii_letters + string.digits, k=100) + >>> binary_insertion_sort(collection) == sorted(collection) + True + """ + + n = len(collection) + for i in range(1, n): + value_to_insert = collection[i] + low = 0 + high = i - 1 + + while low <= high: + mid = (low + high) // 2 + if value_to_insert < collection[mid]: + high = mid - 1 + else: + low = mid + 1 + for j in range(i, low, -1): + collection[j] = collection[j - 1] + collection[low] = value_to_insert + return collection + + +if __name__ == "__main": + user_input = input("Enter numbers separated by a comma:\n").strip() + try: + unsorted = [int(item) for item in user_input.split(",")] + except ValueError: + print("Invalid input. Please enter valid integers separated by commas.") + raise + print(f"{binary_insertion_sort(unsorted) = }") diff --git a/sorts/bitonic_sort.py b/sorts/bitonic_sort.py index b65f877a45e3..600f8139603a 100644 --- a/sorts/bitonic_sort.py +++ b/sorts/bitonic_sort.py @@ -3,6 +3,7 @@ Note that this program works only when size of input is a power of 2. """ + from __future__ import annotations diff --git a/sorts/bogo_sort.py b/sorts/bogo_sort.py index b72f2089f3d2..9c133f0d8a55 100644 --- a/sorts/bogo_sort.py +++ b/sorts/bogo_sort.py @@ -31,8 +31,6 @@ def bogo_sort(collection): """ def is_sorted(collection): - if len(collection) < 2: - return True for i in range(len(collection) - 1): if collection[i] > collection[i + 1]: return False diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index aef2da272bd0..9ec3d5384f38 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -1,4 +1,7 @@ -def bubble_sort(collection): +from typing import Any + + +def bubble_sort_iterative(collection: list[Any]) -> list[Any]: """Pure implementation of bubble sort algorithm in Python :param collection: some mutable ordered collection with heterogeneous @@ -6,31 +9,43 @@ def bubble_sort(collection): :return: the same collection ordered by ascending Examples: - >>> bubble_sort([0, 5, 2, 3, 2]) + >>> bubble_sort_iterative([0, 5, 2, 3, 2]) [0, 2, 2, 3, 5] - >>> bubble_sort([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2]) + >>> bubble_sort_iterative([]) + [] + >>> bubble_sort_iterative([-2, -45, -5]) + [-45, -5, -2] + >>> bubble_sort_iterative([-23, 0, 6, -4, 34]) + [-23, -4, 0, 6, 34] + >>> bubble_sort_iterative([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2]) True - >>> bubble_sort([]) == sorted([]) + >>> bubble_sort_iterative([]) == sorted([]) True - >>> bubble_sort([-2, -45, -5]) == sorted([-2, -45, -5]) + >>> bubble_sort_iterative([-2, -45, -5]) == sorted([-2, -45, -5]) True - >>> bubble_sort([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34]) + >>> bubble_sort_iterative([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34]) True - >>> bubble_sort(['d', 'a', 'b', 'e', 'c']) == sorted(['d', 'a', 'b', 'e', 'c']) + >>> bubble_sort_iterative(['d', 'a', 'b', 'e']) == sorted(['d', 'a', 'b', 'e']) True + >>> bubble_sort_iterative(['z', 'a', 'y', 'b', 'x', 'c']) + ['a', 'b', 'c', 'x', 'y', 'z'] + >>> bubble_sort_iterative([1.1, 3.3, 5.5, 7.7, 2.2, 4.4, 6.6]) + [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7] + >>> bubble_sort_iterative([1, 3.3, 5, 7.7, 2, 4.4, 6]) + [1, 2, 3.3, 4.4, 5, 6, 7.7] >>> import random - >>> collection = random.sample(range(-50, 50), 100) - >>> bubble_sort(collection) == sorted(collection) + >>> collection_arg = random.sample(range(-50, 50), 100) + >>> bubble_sort_iterative(collection_arg) == sorted(collection_arg) True >>> import string - >>> collection = random.choices(string.ascii_letters + string.digits, k=100) - >>> bubble_sort(collection) == sorted(collection) + >>> collection_arg = random.choices(string.ascii_letters + string.digits, k=100) + >>> bubble_sort_iterative(collection_arg) == sorted(collection_arg) True """ length = len(collection) - for i in range(length - 1): + for i in reversed(range(length)): swapped = False - for j in range(length - 1 - i): + for j in range(i): if collection[j] > collection[j + 1]: swapped = True collection[j], collection[j + 1] = collection[j + 1], collection[j] @@ -39,14 +54,79 @@ def bubble_sort(collection): return collection +def bubble_sort_recursive(collection: list[Any]) -> list[Any]: + """It is similar iterative bubble sort but recursive. + + :param collection: mutable ordered sequence of elements + :return: the same list in ascending order + + Examples: + >>> bubble_sort_recursive([0, 5, 2, 3, 2]) + [0, 2, 2, 3, 5] + >>> bubble_sort_iterative([]) + [] + >>> bubble_sort_recursive([-2, -45, -5]) + [-45, -5, -2] + >>> bubble_sort_recursive([-23, 0, 6, -4, 34]) + [-23, -4, 0, 6, 34] + >>> bubble_sort_recursive([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2]) + True + >>> bubble_sort_recursive([]) == sorted([]) + True + >>> bubble_sort_recursive([-2, -45, -5]) == sorted([-2, -45, -5]) + True + >>> bubble_sort_recursive([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34]) + True + >>> bubble_sort_recursive(['d', 'a', 'b', 'e']) == sorted(['d', 'a', 'b', 'e']) + True + >>> bubble_sort_recursive(['z', 'a', 'y', 'b', 'x', 'c']) + ['a', 'b', 'c', 'x', 'y', 'z'] + >>> bubble_sort_recursive([1.1, 3.3, 5.5, 7.7, 2.2, 4.4, 6.6]) + [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7] + >>> bubble_sort_recursive([1, 3.3, 5, 7.7, 2, 4.4, 6]) + [1, 2, 3.3, 4.4, 5, 6, 7.7] + >>> bubble_sort_recursive(['a', 'Z', 'B', 'C', 'A', 'c']) + ['A', 'B', 'C', 'Z', 'a', 'c'] + >>> import random + >>> collection_arg = random.sample(range(-50, 50), 100) + >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg) + True + >>> import string + >>> collection_arg = random.choices(string.ascii_letters + string.digits, k=100) + >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg) + True + """ + length = len(collection) + swapped = False + for i in range(length - 1): + if collection[i] > collection[i + 1]: + collection[i], collection[i + 1] = collection[i + 1], collection[i] + swapped = True + + return collection if not swapped else bubble_sort_recursive(collection) + + if __name__ == "__main__": import doctest - import time + from random import sample + from timeit import timeit doctest.testmod() - user_input = input("Enter numbers separated by a comma:").strip() - unsorted = [int(item) for item in user_input.split(",")] - start = time.process_time() - print(*bubble_sort(unsorted), sep=",") - print(f"Processing time: {(time.process_time() - start)%1e9 + 7}") + # Benchmark: Iterative seems slightly faster than recursive. + num_runs = 10_000 + unsorted = sample(range(-50, 50), 100) + timer_iterative = timeit( + "bubble_sort_iterative(unsorted[:])", globals=globals(), number=num_runs + ) + print("\nIterative bubble sort:") + print(*bubble_sort_iterative(unsorted), sep=",") + print(f"Processing time (iterative): {timer_iterative:.5f}s for {num_runs:,} runs") + + unsorted = sample(range(-50, 50), 100) + timer_recursive = timeit( + "bubble_sort_recursive(unsorted[:])", globals=globals(), number=num_runs + ) + print("\nRecursive bubble sort:") + print(*bubble_sort_recursive(unsorted), sep=",") + print(f"Processing time (recursive): {timer_recursive:.5f}s for {num_runs:,} runs") diff --git a/sorts/bucket_sort.py b/sorts/bucket_sort.py index 7bcbe61a4526..1c1320a58a7d 100644 --- a/sorts/bucket_sort.py +++ b/sorts/bucket_sort.py @@ -27,10 +27,11 @@ Source: https://en.wikipedia.org/wiki/Bucket_sort """ + from __future__ import annotations -def bucket_sort(my_list: list) -> list: +def bucket_sort(my_list: list, bucket_count: int = 10) -> list: """ >>> data = [-1, 2, -5, 0] >>> bucket_sort(data) == sorted(data) @@ -43,21 +44,27 @@ def bucket_sort(my_list: list) -> list: True >>> bucket_sort([]) == sorted([]) True + >>> data = [-1e10, 1e10] + >>> bucket_sort(data) == sorted(data) + True >>> import random >>> collection = random.sample(range(-50, 50), 50) >>> bucket_sort(collection) == sorted(collection) True """ - if len(my_list) == 0: + + if len(my_list) == 0 or bucket_count <= 0: return [] + min_value, max_value = min(my_list), max(my_list) - bucket_count = int(max_value - min_value) + 1 + bucket_size = (max_value - min_value) / bucket_count buckets: list[list] = [[] for _ in range(bucket_count)] - for i in my_list: - buckets[int(i - min_value)].append(i) + for val in my_list: + index = min(int((val - min_value) / bucket_size), bucket_count - 1) + buckets[index].append(val) - return [v for bucket in buckets for v in sorted(bucket)] + return [val for bucket in buckets for val in sorted(bucket)] if __name__ == "__main__": diff --git a/sorts/circle_sort.py b/sorts/circle_sort.py index da3c59059516..271fa1e8d58a 100644 --- a/sorts/circle_sort.py +++ b/sorts/circle_sort.py @@ -58,14 +58,13 @@ def circle_sort_util(collection: list, low: int, high: int) -> bool: left += 1 right -= 1 - if left == right: - if collection[left] > collection[right + 1]: - collection[left], collection[right + 1] = ( - collection[right + 1], - collection[left], - ) + if left == right and collection[left] > collection[right + 1]: + collection[left], collection[right + 1] = ( + collection[right + 1], + collection[left], + ) - swapped = True + swapped = True mid = low + int((high - low) / 2) left_swap = circle_sort_util(collection, low, mid) diff --git a/sorts/cocktail_shaker_sort.py b/sorts/cocktail_shaker_sort.py index b738ff31d768..de126426d986 100644 --- a/sorts/cocktail_shaker_sort.py +++ b/sorts/cocktail_shaker_sort.py @@ -1,40 +1,62 @@ -""" https://en.wikipedia.org/wiki/Cocktail_shaker_sort """ +""" +An implementation of the cocktail shaker sort algorithm in pure Python. +https://en.wikipedia.org/wiki/Cocktail_shaker_sort +""" -def cocktail_shaker_sort(unsorted: list) -> list: + +def cocktail_shaker_sort(arr: list[int]) -> list[int]: """ - Pure implementation of the cocktail shaker sort algorithm in Python. + Sorts a list using the Cocktail Shaker Sort algorithm. + + :param arr: List of elements to be sorted. + :return: Sorted list. + >>> cocktail_shaker_sort([4, 5, 2, 1, 2]) [1, 2, 2, 4, 5] - >>> cocktail_shaker_sort([-4, 5, 0, 1, 2, 11]) [-4, 0, 1, 2, 5, 11] - >>> cocktail_shaker_sort([0.1, -2.4, 4.4, 2.2]) [-2.4, 0.1, 2.2, 4.4] - >>> cocktail_shaker_sort([1, 2, 3, 4, 5]) [1, 2, 3, 4, 5] - >>> cocktail_shaker_sort([-4, -5, -24, -7, -11]) [-24, -11, -7, -5, -4] + >>> cocktail_shaker_sort(["elderberry", "banana", "date", "apple", "cherry"]) + ['apple', 'banana', 'cherry', 'date', 'elderberry'] + >>> cocktail_shaker_sort((-4, -5, -24, -7, -11)) + Traceback (most recent call last): + ... + TypeError: 'tuple' object does not support item assignment """ - for i in range(len(unsorted) - 1, 0, -1): + start, end = 0, len(arr) - 1 + + while start < end: swapped = False - for j in range(i, 0, -1): - if unsorted[j] < unsorted[j - 1]: - unsorted[j], unsorted[j - 1] = unsorted[j - 1], unsorted[j] + # Pass from left to right + for i in range(start, end): + if arr[i] > arr[i + 1]: + arr[i], arr[i + 1] = arr[i + 1], arr[i] swapped = True - for j in range(i): - if unsorted[j] > unsorted[j + 1]: - unsorted[j], unsorted[j + 1] = unsorted[j + 1], unsorted[j] + if not swapped: + break + + end -= 1 # Decrease the end pointer after each pass + + # Pass from right to left + for i in range(end, start, -1): + if arr[i] < arr[i - 1]: + arr[i], arr[i - 1] = arr[i - 1], arr[i] swapped = True if not swapped: break - return unsorted + + start += 1 # Increase the start pointer after each pass + + return arr if __name__ == "__main__": diff --git a/sorts/counting_sort.py b/sorts/counting_sort.py index 892ec5d5f344..256952df52d2 100644 --- a/sorts/counting_sort.py +++ b/sorts/counting_sort.py @@ -49,7 +49,7 @@ def counting_sort(collection): # place the elements in the output, respecting the original order (stable # sort) from end to begin, updating counting_arr - for i in reversed(range(0, coll_len)): + for i in reversed(range(coll_len)): ordered[counting_arr[collection[i] - coll_min] - 1] = collection[i] counting_arr[collection[i] - coll_min] -= 1 @@ -66,7 +66,7 @@ def counting_sort_string(string): if __name__ == "__main__": # Test string sort - assert "eghhiiinrsssttt" == counting_sort_string("thisisthestring") + assert counting_sort_string("thisisthestring") == "eghhiiinrsssttt" user_input = input("Enter numbers separated by a comma:\n").strip() unsorted = [int(item) for item in user_input.split(",")] diff --git a/sorts/cycle_sort.py b/sorts/cycle_sort.py index 806f40441d79..7177c8ea110d 100644 --- a/sorts/cycle_sort.py +++ b/sorts/cycle_sort.py @@ -19,7 +19,7 @@ def cycle_sort(array: list) -> list: [] """ array_len = len(array) - for cycle_start in range(0, array_len - 1): + for cycle_start in range(array_len - 1): item = array[cycle_start] pos = cycle_start diff --git a/sorts/double_sort.py b/sorts/double_sort.py index 5ca88a6745d5..bd5fdca1e63c 100644 --- a/sorts/double_sort.py +++ b/sorts/double_sort.py @@ -1,4 +1,7 @@ -def double_sort(lst): +from typing import Any + + +def double_sort(collection: list[Any]) -> list[Any]: """This sorting algorithm sorts an array using the principle of bubble sort, but does it both from left to right and right to left. Hence, it's called "Double sort" @@ -14,29 +17,28 @@ def double_sort(lst): >>> double_sort([-3, 10, 16, -42, 29]) == sorted([-3, 10, 16, -42, 29]) True """ - no_of_elements = len(lst) + no_of_elements = len(collection) for _ in range( - 0, int(((no_of_elements - 1) / 2) + 1) + int(((no_of_elements - 1) / 2) + 1) ): # we don't need to traverse to end of list as - for j in range(0, no_of_elements - 1): - if ( - lst[j + 1] < lst[j] - ): # applying bubble sort algorithm from left to right (or forwards) - temp = lst[j + 1] - lst[j + 1] = lst[j] - lst[j] = temp - if ( - lst[no_of_elements - 1 - j] < lst[no_of_elements - 2 - j] - ): # applying bubble sort algorithm from right to left (or backwards) - temp = lst[no_of_elements - 1 - j] - lst[no_of_elements - 1 - j] = lst[no_of_elements - 2 - j] - lst[no_of_elements - 2 - j] = temp - return lst + for j in range(no_of_elements - 1): + # apply the bubble sort algorithm from left to right (or forwards) + if collection[j + 1] < collection[j]: + collection[j], collection[j + 1] = collection[j + 1], collection[j] + # apply the bubble sort algorithm from right to left (or backwards) + if collection[no_of_elements - 1 - j] < collection[no_of_elements - 2 - j]: + ( + collection[no_of_elements - 1 - j], + collection[no_of_elements - 2 - j], + ) = ( + collection[no_of_elements - 2 - j], + collection[no_of_elements - 1 - j], + ) + return collection if __name__ == "__main__": - print("enter the list to be sorted") - lst = [int(x) for x in input().split()] # inputing elements of the list in one line - sorted_lst = double_sort(lst) + # allow the user to input the elements of the list on one line + unsorted = [int(x) for x in input("Enter the list to be sorted: ").split() if x] print("the sorted list is") - print(sorted_lst) + print(f"{double_sort(unsorted) = }") diff --git a/sorts/dutch_national_flag_sort.py b/sorts/dutch_national_flag_sort.py index 79afefa73afe..b4f1665cea00 100644 --- a/sorts/dutch_national_flag_sort.py +++ b/sorts/dutch_national_flag_sort.py @@ -23,7 +23,6 @@ python dnf_sort.py """ - # Python program to sort a sequence containing only 0, 1 and 2 in a single pass. red = 0 # The first color of the flag. white = 1 # The second color of the flag. @@ -84,9 +83,8 @@ def dutch_national_flag_sort(sequence: list) -> list: sequence[mid], sequence[high] = sequence[high], sequence[mid] high -= 1 else: - raise ValueError( - f"The elements inside the sequence must contains only {colors} values" - ) + msg = f"The elements inside the sequence must contains only {colors} values" + raise ValueError(msg) return sequence diff --git a/sorts/external_sort.py b/sorts/external_sort.py index 7af7dc0a609d..3fa7cacc0592 100644 --- a/sorts/external_sort.py +++ b/sorts/external_sort.py @@ -77,10 +77,7 @@ def refresh(self): self.empty.add(i) self.files[i].close() - if len(self.empty) == self.num_buffers: - return False - - return True + return len(self.empty) != self.num_buffers def unshift(self, index): value = self.buffers[index] @@ -104,7 +101,7 @@ def get_file_handles(self, filenames, buffer_size): files = {} for i in range(len(filenames)): - files[i] = open(filenames[i], "r", buffer_size) + files[i] = open(filenames[i], "r", buffer_size) # noqa: UP015 return files diff --git a/sorts/heap_sort.py b/sorts/heap_sort.py index 4dca879bd89c..44ee1d4b39f1 100644 --- a/sorts/heap_sort.py +++ b/sorts/heap_sort.py @@ -1,17 +1,22 @@ """ -This is a pure Python implementation of the heap sort algorithm. - -For doctests run following command: -python -m doctest -v heap_sort.py -or -python3 -m doctest -v heap_sort.py - -For manual testing run: -python heap_sort.py +A pure Python implementation of the heap sort algorithm. """ -def heapify(unsorted, index, heap_size): +def heapify(unsorted: list[int], index: int, heap_size: int) -> None: + """ + :param unsorted: unsorted list containing integers numbers + :param index: index + :param heap_size: size of the heap + :return: None + >>> unsorted = [1, 4, 3, 5, 2] + >>> heapify(unsorted, 0, len(unsorted)) + >>> unsorted + [4, 5, 3, 1, 2] + >>> heapify(unsorted, 0, len(unsorted)) + >>> unsorted + [5, 4, 3, 1, 2] + """ largest = index left_index = 2 * index + 1 right_index = 2 * index + 2 @@ -22,26 +27,26 @@ def heapify(unsorted, index, heap_size): largest = right_index if largest != index: - unsorted[largest], unsorted[index] = unsorted[index], unsorted[largest] + unsorted[largest], unsorted[index] = (unsorted[index], unsorted[largest]) heapify(unsorted, largest, heap_size) -def heap_sort(unsorted): +def heap_sort(unsorted: list[int]) -> list[int]: """ - Pure implementation of the heap sort algorithm in Python - :param collection: some mutable ordered collection with heterogeneous - comparable items inside + A pure Python implementation of the heap sort algorithm + + :param collection: a mutable ordered collection of heterogeneous comparable items :return: the same collection ordered by ascending Examples: >>> heap_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] - >>> heap_sort([]) [] - >>> heap_sort([-2, -5, -45]) [-45, -5, -2] + >>> heap_sort([3, 7, 9, 28, 123, -5, 8, -30, -200, 0, 4]) + [-200, -30, -5, 0, 3, 4, 7, 8, 9, 28, 123] """ n = len(unsorted) for i in range(n // 2 - 1, -1, -1): @@ -53,6 +58,10 @@ def heap_sort(unsorted): if __name__ == "__main__": + import doctest + + doctest.testmod() user_input = input("Enter numbers separated by a comma:\n").strip() - unsorted = [int(item) for item in user_input.split(",")] - print(heap_sort(unsorted)) + if user_input: + unsorted = [int(item) for item in user_input.split(",")] + print(f"{heap_sort(unsorted) = }") diff --git a/sorts/insertion_sort.py b/sorts/insertion_sort.py index 6d5bb2b46013..46b263d84a33 100644 --- a/sorts/insertion_sort.py +++ b/sorts/insertion_sort.py @@ -13,8 +13,18 @@ python3 insertion_sort.py """ +from collections.abc import MutableSequence +from typing import Any, Protocol, TypeVar -def insertion_sort(collection: list) -> list: + +class Comparable(Protocol): + def __lt__(self, other: Any, /) -> bool: ... + + +T = TypeVar("T", bound=Comparable) + + +def insertion_sort(collection: MutableSequence[T]) -> MutableSequence[T]: """A pure Python implementation of the insertion sort algorithm :param collection: some mutable ordered collection with heterogeneous @@ -40,13 +50,12 @@ def insertion_sort(collection: list) -> list: True """ - for insert_index, insert_value in enumerate(collection[1:]): - temp_index = insert_index - while insert_index >= 0 and insert_value < collection[insert_index]: - collection[insert_index + 1] = collection[insert_index] + for insert_index in range(1, len(collection)): + insert_value = collection[insert_index] + while insert_index > 0 and insert_value < collection[insert_index - 1]: + collection[insert_index] = collection[insert_index - 1] insert_index -= 1 - if insert_index != temp_index: - collection[insert_index + 1] = insert_value + collection[insert_index] = insert_value return collection diff --git a/sorts/intro_sort.py b/sorts/intro_sort.py index f0e3645adbb7..1184b381b05d 100644 --- a/sorts/intro_sort.py +++ b/sorts/intro_sort.py @@ -1,17 +1,29 @@ """ -Introspective Sort is hybrid sort (Quick Sort + Heap Sort + Insertion Sort) +Introspective Sort is a hybrid sort (Quick Sort + Heap Sort + Insertion Sort) if the size of the list is under 16, use insertion sort https://en.wikipedia.org/wiki/Introsort """ + import math def insertion_sort(array: list, start: int = 0, end: int = 0) -> list: """ >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - >>> insertion_sort(array, 0, len(array)) [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79] + >>> array = [21, 15, 11, 45, -2, -11, 46] + >>> insertion_sort(array, 0, len(array)) + [-11, -2, 11, 15, 21, 45, 46] + >>> array = [-2, 0, 89, 11, 48, 79, 12] + >>> insertion_sort(array, 0, len(array)) + [-2, 0, 11, 12, 48, 79, 89] + >>> array = ['a', 'z', 'd', 'p', 'v', 'l', 'o', 'o'] + >>> insertion_sort(array, 0, len(array)) + ['a', 'd', 'l', 'o', 'o', 'p', 'v', 'z'] + >>> array = [73.568, 73.56, -45.03, 1.7, 0, 89.45] + >>> insertion_sort(array, 0, len(array)) + [-45.03, 0, 1.7, 73.56, 73.568, 89.45] """ end = end or len(array) for i in range(start, end): @@ -27,8 +39,7 @@ def insertion_sort(array: list, start: int = 0, end: int = 0) -> list: def heapify(array: list, index: int, heap_size: int) -> None: # Max Heap """ >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - - >>> heapify(array, len(array) // 2 ,len(array)) + >>> heapify(array, len(array) // 2, len(array)) """ largest = index left_index = 2 * index + 1 # Left Node @@ -47,10 +58,14 @@ def heapify(array: list, index: int, heap_size: int) -> None: # Max Heap def heap_sort(array: list) -> list: """ - >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - - >>> heap_sort(array) + >>> heap_sort([4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]) [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79] + >>> heap_sort([-2, -11, 0, 0, 0, 87, 45, -69, 78, 12, 10, 103, 89, 52]) + [-69, -11, -2, 0, 0, 0, 10, 12, 45, 52, 78, 87, 89, 103] + >>> heap_sort(['b', 'd', 'e', 'f', 'g', 'p', 'x', 'z', 'b', 's', 'e', 'u', 'v']) + ['b', 'b', 'd', 'e', 'e', 'f', 'g', 'p', 's', 'u', 'v', 'x', 'z'] + >>> heap_sort([6.2, -45.54, 8465.20, 758.56, -457.0, 0, 1, 2.879, 1.7, 11.7]) + [-457.0, -45.54, 0, 1, 1.7, 2.879, 6.2, 11.7, 758.56, 8465.2] """ n = len(array) @@ -69,9 +84,14 @@ def median_of_3( ) -> int: """ >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - - >>> median_of_3(array, 0, 0 + ((len(array) - 0) // 2) + 1, len(array) - 1) + >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1) 12 + >>> array = [13, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] + >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1) + 13 + >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 15, 14, 27, 79, 23, 45, 14, 16] + >>> median_of_3(array, 0, ((len(array) - 0) // 2) + 1, len(array) - 1) + 14 """ if (array[first_index] > array[middle_index]) != ( array[first_index] > array[last_index] @@ -88,9 +108,17 @@ def median_of_3( def partition(array: list, low: int, high: int, pivot: int) -> int: """ >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - >>> partition(array, 0, len(array), 12) 8 + >>> array = [21, 15, 11, 45, -2, -11, 46] + >>> partition(array, 0, len(array), 15) + 3 + >>> array = ['a', 'z', 'd', 'p', 'v', 'l', 'o', 'o'] + >>> partition(array, 0, len(array), 'p') + 5 + >>> array = [6.2, -45.54, 8465.20, 758.56, -457.0, 0, 1, 2.879, 1.7, 11.7] + >>> partition(array, 0, len(array), 2.879) + 6 """ i = low j = high @@ -115,22 +143,16 @@ def sort(array: list) -> list: Examples: >>> sort([4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12]) [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79] - >>> sort([-1, -5, -3, -13, -44]) [-44, -13, -5, -3, -1] - >>> sort([]) [] - >>> sort([5]) [5] - >>> sort([-3, 0, -7, 6, 23, -34]) [-34, -7, -3, 0, 6, 23] - >>> sort([1.7, 1.0, 3.3, 2.1, 0.3 ]) [0.3, 1.0, 1.7, 2.1, 3.3] - >>> sort(['d', 'a', 'b', 'e', 'c']) ['a', 'b', 'c', 'd', 'e'] """ @@ -146,9 +168,7 @@ def intro_sort( ) -> list: """ >>> array = [4, 2, 6, 8, 1, 7, 8, 22, 14, 56, 27, 79, 23, 45, 14, 12] - >>> max_depth = 2 * math.ceil(math.log2(len(array))) - >>> intro_sort(array, 0, len(array), 16, max_depth) [1, 2, 4, 6, 7, 8, 8, 12, 14, 14, 22, 23, 27, 45, 56, 79] """ @@ -167,7 +187,6 @@ def intro_sort( import doctest doctest.testmod() - user_input = input("Enter numbers separated by a comma : ").strip() unsorted = [float(item) for item in user_input.split(",")] - print(sort(unsorted)) + print(f"{sort(unsorted) = }") diff --git a/sorts/merge_sort.py b/sorts/merge_sort.py index e80b1cb226ec..0628b848b794 100644 --- a/sorts/merge_sort.py +++ b/sorts/merge_sort.py @@ -12,9 +12,13 @@ def merge_sort(collection: list) -> list: """ - :param collection: some mutable ordered collection with heterogeneous - comparable items inside - :return: the same collection ordered by ascending + Sorts a list using the merge sort algorithm. + + :param collection: A mutable ordered collection with comparable items. + :return: The same collection ordered in ascending order. + + Time Complexity: O(n log n) + Examples: >>> merge_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] @@ -26,31 +30,34 @@ def merge_sort(collection: list) -> list: def merge(left: list, right: list) -> list: """ - Merge left and right. + Merge two sorted lists into a single sorted list. - :param left: left collection - :param right: right collection - :return: merge result + :param left: Left collection + :param right: Right collection + :return: Merged result """ - - def _merge(): - while left and right: - yield (left if left[0] <= right[0] else right).pop(0) - yield from left - yield from right - - return list(_merge()) + result = [] + while left and right: + result.append(left.pop(0) if left[0] <= right[0] else right.pop(0)) + result.extend(left) + result.extend(right) + return result if len(collection) <= 1: return collection - mid = len(collection) // 2 - return merge(merge_sort(collection[:mid]), merge_sort(collection[mid:])) + mid_index = len(collection) // 2 + return merge(merge_sort(collection[:mid_index]), merge_sort(collection[mid_index:])) if __name__ == "__main__": import doctest doctest.testmod() - user_input = input("Enter numbers separated by a comma:\n").strip() - unsorted = [int(item) for item in user_input.split(",")] - print(*merge_sort(unsorted), sep=",") + + try: + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + sorted_list = merge_sort(unsorted) + print(*sorted_list, sep=",") + except ValueError: + print("Invalid input. Please enter valid integers separated by commas.") diff --git a/sorts/msd_radix_sort.py b/sorts/msd_radix_sort.py index 74ce21762906..6aba4263663a 100644 --- a/sorts/msd_radix_sort.py +++ b/sorts/msd_radix_sort.py @@ -4,6 +4,7 @@ them. https://en.wikipedia.org/wiki/Radix_sort """ + from __future__ import annotations @@ -147,7 +148,7 @@ def _msd_radix_sort_inplace( list_of_ints[i], list_of_ints[j] = list_of_ints[j], list_of_ints[i] j -= 1 - if not j == i: + if j != i: i += 1 _msd_radix_sort_inplace(list_of_ints, bit_position, begin_index, i) diff --git a/sorts/odd_even_transposition_parallel.py b/sorts/odd_even_transposition_parallel.py index 87b0e4d1e20f..5d4e09b211c0 100644 --- a/sorts/odd_even_transposition_parallel.py +++ b/sorts/odd_even_transposition_parallel.py @@ -10,10 +10,12 @@ They are synchronized with locks and message passing but other forms of synchronization could be used. """ -from multiprocessing import Lock, Pipe, Process + +import multiprocessing as mp # lock used to ensure that two processes do not access a pipe at the same time -process_lock = Lock() +# NOTE This breaks testing on build runner. May work better locally +# process_lock = mp.Lock() """ The function run by the processes that sorts the list @@ -27,36 +29,41 @@ """ -def oe_process(position, value, l_send, r_send, lr_cv, rr_cv, result_pipe): - global process_lock +def oe_process( + position, + value, + l_send, + r_send, + lr_cv, + rr_cv, + result_pipe, + multiprocessing_context, +): + process_lock = multiprocessing_context.Lock() # we perform n swaps since after n swaps we know we are sorted # we *could* stop early if we are sorted already, but it takes as long to # find out we are sorted as it does to sort the list with this algorithm - for i in range(0, 10): + for i in range(10): if (i + position) % 2 == 0 and r_send is not None: # send your value to your right neighbor - process_lock.acquire() - r_send[1].send(value) - process_lock.release() + with process_lock: + r_send[1].send(value) # receive your right neighbor's value - process_lock.acquire() - temp = rr_cv[0].recv() - process_lock.release() + with process_lock: + temp = rr_cv[0].recv() # take the lower value since you are on the left value = min(value, temp) elif (i + position) % 2 != 0 and l_send is not None: # send your value to your left neighbor - process_lock.acquire() - l_send[1].send(value) - process_lock.release() + with process_lock: + l_send[1].send(value) # receive your left neighbor's value - process_lock.acquire() - temp = lr_cv[0].recv() - process_lock.release() + with process_lock: + temp = lr_cv[0].recv() # take the higher value since you are on the right value = max(value, temp) @@ -72,39 +79,80 @@ def oe_process(position, value, l_send, r_send, lr_cv, rr_cv, result_pipe): def odd_even_transposition(arr): + """ + >>> odd_even_transposition(list(range(10)[::-1])) == sorted(list(range(10)[::-1])) + True + >>> odd_even_transposition(["a", "x", "c"]) == sorted(["x", "a", "c"]) + True + >>> odd_even_transposition([1.9, 42.0, 2.8]) == sorted([1.9, 42.0, 2.8]) + True + >>> odd_even_transposition([False, True, False]) == sorted([False, False, True]) + True + >>> odd_even_transposition([1, 32.0, 9]) == sorted([False, False, True]) + False + >>> odd_even_transposition([1, 32.0, 9]) == sorted([1.0, 32, 9.0]) + True + >>> unsorted_list = [-442, -98, -554, 266, -491, 985, -53, -529, 82, -429] + >>> odd_even_transposition(unsorted_list) == sorted(unsorted_list) + True + >>> unsorted_list = [-442, -98, -554, 266, -491, 985, -53, -529, 82, -429] + >>> odd_even_transposition(unsorted_list) == sorted(unsorted_list + [1]) + False + """ + # spawn method is considered safer than fork + multiprocessing_context = mp.get_context("spawn") + process_array_ = [] result_pipe = [] # initialize the list of pipes where the values will be retrieved for _ in arr: - result_pipe.append(Pipe()) + result_pipe.append(multiprocessing_context.Pipe()) # creates the processes # the first and last process only have one neighbor so they are made outside # of the loop - temp_rs = Pipe() - temp_rr = Pipe() + temp_rs = multiprocessing_context.Pipe() + temp_rr = multiprocessing_context.Pipe() process_array_.append( - Process( + multiprocessing_context.Process( target=oe_process, - args=(0, arr[0], None, temp_rs, None, temp_rr, result_pipe[0]), + args=( + 0, + arr[0], + None, + temp_rs, + None, + temp_rr, + result_pipe[0], + multiprocessing_context, + ), ) ) temp_lr = temp_rs temp_ls = temp_rr for i in range(1, len(arr) - 1): - temp_rs = Pipe() - temp_rr = Pipe() + temp_rs = multiprocessing_context.Pipe() + temp_rr = multiprocessing_context.Pipe() process_array_.append( - Process( + multiprocessing_context.Process( target=oe_process, - args=(i, arr[i], temp_ls, temp_rs, temp_lr, temp_rr, result_pipe[i]), + args=( + i, + arr[i], + temp_ls, + temp_rs, + temp_lr, + temp_rr, + result_pipe[i], + multiprocessing_context, + ), ) ) temp_lr = temp_rs temp_ls = temp_rr process_array_.append( - Process( + multiprocessing_context.Process( target=oe_process, args=( len(arr) - 1, @@ -114,6 +162,7 @@ def odd_even_transposition(arr): temp_lr, None, result_pipe[len(arr) - 1], + multiprocessing_context, ), ) ) @@ -123,7 +172,7 @@ def odd_even_transposition(arr): p.start() # wait for the processes to end and write their values to the list - for p in range(0, len(result_pipe)): + for p in range(len(result_pipe)): arr[p] = result_pipe[p][0].recv() process_array_[p].join() return arr diff --git a/sorts/pigeon_sort.py b/sorts/pigeon_sort.py index 3e6d4c09c46f..fdfa692f4680 100644 --- a/sorts/pigeon_sort.py +++ b/sorts/pigeon_sort.py @@ -1,14 +1,15 @@ """ - This is an implementation of Pigeon Hole Sort. - For doctests run following command: +This is an implementation of Pigeon Hole Sort. +For doctests run following command: - python3 -m doctest -v pigeon_sort.py - or - python -m doctest -v pigeon_sort.py +python3 -m doctest -v pigeon_sort.py +or +python -m doctest -v pigeon_sort.py - For manual testing run: - python pigeon_sort.py +For manual testing run: +python pigeon_sort.py """ + from __future__ import annotations diff --git a/sorts/quick_sort.py b/sorts/quick_sort.py index 70cd19d7afe0..374d52e75c81 100644 --- a/sorts/quick_sort.py +++ b/sorts/quick_sort.py @@ -7,16 +7,17 @@ For manual testing run: python3 quick_sort.py """ + from __future__ import annotations from random import randrange def quick_sort(collection: list) -> list: - """A pure Python implementation of quick sort algorithm + """A pure Python implementation of quicksort algorithm. :param collection: a mutable collection of comparable items - :return: the same collection ordered by ascending + :return: the same collection ordered in ascending order Examples: >>> quick_sort([0, 5, 3, 2, 2]) @@ -26,23 +27,26 @@ def quick_sort(collection: list) -> list: >>> quick_sort([-2, 5, 0, -45]) [-45, -2, 0, 5] """ + # Base case: if the collection has 0 or 1 elements, it is already sorted if len(collection) < 2: return collection - pivot_index = randrange(len(collection)) # Use random element as pivot - pivot = collection[pivot_index] - greater: list[int] = [] # All elements greater than pivot - lesser: list[int] = [] # All elements less than or equal to pivot - for element in collection[:pivot_index]: - (greater if element > pivot else lesser).append(element) + # Randomly select a pivot index and remove the pivot element from the collection + pivot_index = randrange(len(collection)) + pivot = collection.pop(pivot_index) - for element in collection[pivot_index + 1 :]: - (greater if element > pivot else lesser).append(element) + # Partition the remaining elements into two groups: lesser or equal, and greater + lesser = [item for item in collection if item <= pivot] + greater = [item for item in collection if item > pivot] - return quick_sort(lesser) + [pivot] + quick_sort(greater) + # Recursively sort the lesser and greater groups, and combine with the pivot + return [*quick_sort(lesser), pivot, *quick_sort(greater)] if __name__ == "__main__": + # Get user input and convert it into a list of integers user_input = input("Enter numbers separated by a comma:\n").strip() unsorted = [int(item) for item in user_input.split(",")] + + # Print the result of sorting the user-provided list print(quick_sort(unsorted)) diff --git a/sorts/quick_sort_3_partition.py b/sorts/quick_sort_3_partition.py index 1a6db6a364f0..279b9a68f5a6 100644 --- a/sorts/quick_sort_3_partition.py +++ b/sorts/quick_sort_3_partition.py @@ -1,4 +1,27 @@ def quick_sort_3partition(sorting: list, left: int, right: int) -> None: + """ " + Python implementation of quick sort algorithm with 3-way partition. + The idea of 3-way quick sort is based on "Dutch National Flag algorithm". + + :param sorting: sort list + :param left: left endpoint of sorting + :param right: right endpoint of sorting + :return: None + + Examples: + >>> array1 = [5, -1, -1, 5, 5, 24, 0] + >>> quick_sort_3partition(array1, 0, 6) + >>> array1 + [-1, -1, 0, 5, 5, 5, 24] + >>> array2 = [9, 0, 2, 6] + >>> quick_sort_3partition(array2, 0, 3) + >>> array2 + [0, 2, 6, 9] + >>> array3 = [] + >>> quick_sort_3partition(array3, 0, 0) + >>> array3 + [] + """ if right <= left: return a = i = left diff --git a/sorts/radix_sort.py b/sorts/radix_sort.py index 832b6162f349..1dbf5fbd1365 100644 --- a/sorts/radix_sort.py +++ b/sorts/radix_sort.py @@ -3,6 +3,7 @@ Source: https://en.wikipedia.org/wiki/Radix_sort """ + from __future__ import annotations RADIX = 10 diff --git a/sorts/random_normal_distribution_quicksort.py b/sorts/random_normal_distribution_quicksort.py deleted file mode 100644 index f7f60903c546..000000000000 --- a/sorts/random_normal_distribution_quicksort.py +++ /dev/null @@ -1,62 +0,0 @@ -from random import randint -from tempfile import TemporaryFile - -import numpy as np - - -def _in_place_quick_sort(a, start, end): - count = 0 - if start < end: - pivot = randint(start, end) - temp = a[end] - a[end] = a[pivot] - a[pivot] = temp - - p, count = _in_place_partition(a, start, end) - count += _in_place_quick_sort(a, start, p - 1) - count += _in_place_quick_sort(a, p + 1, end) - return count - - -def _in_place_partition(a, start, end): - count = 0 - pivot = randint(start, end) - temp = a[end] - a[end] = a[pivot] - a[pivot] = temp - new_pivot_index = start - 1 - for index in range(start, end): - count += 1 - if a[index] < a[end]: # check if current val is less than pivot value - new_pivot_index = new_pivot_index + 1 - temp = a[new_pivot_index] - a[new_pivot_index] = a[index] - a[index] = temp - - temp = a[new_pivot_index + 1] - a[new_pivot_index + 1] = a[end] - a[end] = temp - return new_pivot_index + 1, count - - -outfile = TemporaryFile() -p = 100 # 1000 elements are to be sorted - - -mu, sigma = 0, 1 # mean and standard deviation -X = np.random.normal(mu, sigma, p) -np.save(outfile, X) -print("The array is") -print(X) - - -outfile.seek(0) # using the same array -M = np.load(outfile) -r = len(M) - 1 -z = _in_place_quick_sort(M, 0, r) - -print( - "No of Comparisons for 100 elements selected from a standard normal distribution" - "is :" -) -print(z) diff --git a/sorts/random_pivot_quick_sort.py b/sorts/random_pivot_quick_sort.py deleted file mode 100644 index 748b6741047e..000000000000 --- a/sorts/random_pivot_quick_sort.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Picks the random index as the pivot -""" -import random - - -def partition(a, left_index, right_index): - pivot = a[left_index] - i = left_index + 1 - for j in range(left_index + 1, right_index): - if a[j] < pivot: - a[j], a[i] = a[i], a[j] - i += 1 - a[left_index], a[i - 1] = a[i - 1], a[left_index] - return i - 1 - - -def quick_sort_random(a, left, right): - if left < right: - pivot = random.randint(left, right - 1) - a[pivot], a[left] = ( - a[left], - a[pivot], - ) # switches the pivot with the left most bound - pivot_index = partition(a, left, right) - quick_sort_random( - a, left, pivot_index - ) # recursive quicksort to the left of the pivot point - quick_sort_random( - a, pivot_index + 1, right - ) # recursive quicksort to the right of the pivot point - - -def main(): - user_input = input("Enter numbers separated by a comma:\n").strip() - arr = [int(item) for item in user_input.split(",")] - - quick_sort_random(arr, 0, len(arr)) - - print(arr) - - -if __name__ == "__main__": - main() diff --git a/sorts/recursive_bubble_sort.py b/sorts/recursive_bubble_sort.py deleted file mode 100644 index 82af89593e5b..000000000000 --- a/sorts/recursive_bubble_sort.py +++ /dev/null @@ -1,42 +0,0 @@ -def bubble_sort(list_data: list, length: int = 0) -> list: - """ - It is similar is bubble sort but recursive. - :param list_data: mutable ordered sequence of elements - :param length: length of list data - :return: the same list in ascending order - - >>> bubble_sort([0, 5, 2, 3, 2], 5) - [0, 2, 2, 3, 5] - - >>> bubble_sort([], 0) - [] - - >>> bubble_sort([-2, -45, -5], 3) - [-45, -5, -2] - - >>> bubble_sort([-23, 0, 6, -4, 34], 5) - [-23, -4, 0, 6, 34] - - >>> bubble_sort([-23, 0, 6, -4, 34], 5) == sorted([-23, 0, 6, -4, 34]) - True - - >>> bubble_sort(['z','a','y','b','x','c'], 6) - ['a', 'b', 'c', 'x', 'y', 'z'] - - >>> bubble_sort([1.1, 3.3, 5.5, 7.7, 2.2, 4.4, 6.6]) - [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7] - """ - length = length or len(list_data) - swapped = False - for i in range(length - 1): - if list_data[i] > list_data[i + 1]: - list_data[i], list_data[i + 1] = list_data[i + 1], list_data[i] - swapped = True - - return list_data if not swapped else bubble_sort(list_data, length - 1) - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/sorts/recursive_insertion_sort.py b/sorts/recursive_insertion_sort.py index 297dbe9457e6..93465350bee2 100644 --- a/sorts/recursive_insertion_sort.py +++ b/sorts/recursive_insertion_sort.py @@ -1,6 +1,7 @@ """ A recursive implementation of the insertion sort algorithm """ + from __future__ import annotations diff --git a/sorts/recursive_quick_sort.py b/sorts/recursive_quick_sort.py index c28a14e37ebd..c29009aca673 100644 --- a/sorts/recursive_quick_sort.py +++ b/sorts/recursive_quick_sort.py @@ -9,11 +9,11 @@ def quick_sort(data: list) -> list: if len(data) <= 1: return data else: - return ( - quick_sort([e for e in data[1:] if e <= data[0]]) - + [data[0]] - + quick_sort([e for e in data[1:] if e > data[0]]) - ) + return [ + *quick_sort([e for e in data[1:] if e <= data[0]]), + data[0], + *quick_sort([e for e in data[1:] if e > data[0]]), + ] if __name__ == "__main__": diff --git a/sorts/selection_sort.py b/sorts/selection_sort.py index f3beb31b7070..506836b53e44 100644 --- a/sorts/selection_sort.py +++ b/sorts/selection_sort.py @@ -1,22 +1,9 @@ -""" -This is a pure Python implementation of the selection sort algorithm - -For doctests run following command: -python -m doctest -v selection_sort.py -or -python3 -m doctest -v selection_sort.py - -For manual testing run: -python selection_sort.py -""" - - -def selection_sort(collection): - """Pure implementation of the selection sort algorithm in Python - :param collection: some mutable ordered collection with heterogeneous - comparable items inside - :return: the same collection ordered by ascending +def selection_sort(collection: list[int]) -> list[int]: + """ + Sorts a list in ascending order using the selection sort algorithm. + :param collection: A list of integers to be sorted. + :return: The sorted list. Examples: >>> selection_sort([0, 5, 3, 2, 2]) @@ -31,16 +18,17 @@ def selection_sort(collection): length = len(collection) for i in range(length - 1): - least = i + min_index = i for k in range(i + 1, length): - if collection[k] < collection[least]: - least = k - if least != i: - collection[least], collection[i] = (collection[i], collection[least]) + if collection[k] < collection[min_index]: + min_index = k + if min_index != i: + collection[i], collection[min_index] = collection[min_index], collection[i] return collection if __name__ == "__main__": user_input = input("Enter numbers separated by a comma:\n").strip() unsorted = [int(item) for item in user_input.split(",")] - print(selection_sort(unsorted)) + sorted_list = selection_sort(unsorted) + print("Sorted List:", sorted_list) diff --git a/sorts/shell_sort.py b/sorts/shell_sort.py index 10ae9ba407ec..b65609c974b7 100644 --- a/sorts/shell_sort.py +++ b/sorts/shell_sort.py @@ -3,7 +3,7 @@ """ -def shell_sort(collection): +def shell_sort(collection: list[int]) -> list[int]: """Pure implementation of shell sort algorithm in Python :param collection: Some mutable ordered collection with heterogeneous comparable items inside diff --git a/sorts/slowsort.py b/sorts/slowsort.py index a5f4e873ebb2..394e6eed50b1 100644 --- a/sorts/slowsort.py +++ b/sorts/slowsort.py @@ -8,6 +8,7 @@ Source: https://en.wikipedia.org/wiki/Slowsort """ + from __future__ import annotations diff --git a/sorts/stooge_sort.py b/sorts/stooge_sort.py index 9a5bedeae21b..767c6a05924f 100644 --- a/sorts/stooge_sort.py +++ b/sorts/stooge_sort.py @@ -1,4 +1,4 @@ -def stooge_sort(arr): +def stooge_sort(arr: list[int]) -> list[int]: """ Examples: >>> stooge_sort([18.1, 0, -7.1, -1, 2, 2]) @@ -11,7 +11,7 @@ def stooge_sort(arr): return arr -def stooge(arr, i, h): +def stooge(arr: list[int], i: int, h: int) -> None: if i >= h: return diff --git a/sorts/tim_sort.py b/sorts/tim_sort.py index c90c7e80390b..138f11c71bcc 100644 --- a/sorts/tim_sort.py +++ b/sorts/tim_sort.py @@ -32,9 +32,9 @@ def merge(left, right): return left if left[0] < right[0]: - return [left[0]] + merge(left[1:], right) + return [left[0], *merge(left[1:], right)] - return [right[0]] + merge(left, right[1:]) + return [right[0], *merge(left, right[1:])] def tim_sort(lst): diff --git a/sorts/topological_sort.py b/sorts/topological_sort.py index 59a0c8571b53..efce8165fcac 100644 --- a/sorts/topological_sort.py +++ b/sorts/topological_sort.py @@ -5,11 +5,17 @@ # b c # / \ # d e -edges = {"a": ["c", "b"], "b": ["d", "e"], "c": [], "d": [], "e": []} -vertices = ["a", "b", "c", "d", "e"] +edges: dict[str, list[str]] = { + "a": ["c", "b"], + "b": ["d", "e"], + "c": [], + "d": [], + "e": [], +} +vertices: list[str] = ["a", "b", "c", "d", "e"] -def topological_sort(start, visited, sort): +def topological_sort(start: str, visited: list[str], sort: list[str]) -> list[str]: """Perform topological sort on a directed acyclic graph.""" current = start # add current to visited diff --git a/sorts/tree_sort.py b/sorts/tree_sort.py index 78c3e893e0ce..056864957d4d 100644 --- a/sorts/tree_sort.py +++ b/sorts/tree_sort.py @@ -1,53 +1,72 @@ """ Tree_sort algorithm. -Build a BST and in order traverse. +Build a Binary Search Tree and then iterate thru it to get a sorted list. """ +from __future__ import annotations +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass class Node: - # BST data structure - def __init__(self, val): - self.val = val - self.left = None - self.right = None - - def insert(self, val): - if self.val: - if val < self.val: - if self.left is None: - self.left = Node(val) - else: - self.left.insert(val) - elif val > self.val: - if self.right is None: - self.right = Node(val) - else: - self.right.insert(val) - else: - self.val = val - - -def inorder(root, res): - # Recursive traversal - if root: - inorder(root.left, res) - res.append(root.val) - inorder(root.right, res) - - -def tree_sort(arr): - # Build BST + val: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.val + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + def insert(self, val: int) -> None: + if val < self.val: + if self.left is None: + self.left = Node(val) + else: + self.left.insert(val) + elif val > self.val: + if self.right is None: + self.right = Node(val) + else: + self.right.insert(val) + + +def tree_sort(arr: list[int]) -> tuple[int, ...]: + """ + >>> tree_sort([]) + () + >>> tree_sort((1,)) + (1,) + >>> tree_sort((1, 2)) + (1, 2) + >>> tree_sort([5, 2, 7]) + (2, 5, 7) + >>> tree_sort((5, -4, 9, 2, 7)) + (-4, 2, 5, 7, 9) + >>> tree_sort([5, 6, 1, -1, 4, 37, 2, 7]) + (-1, 1, 2, 4, 5, 6, 7, 37) + + # >>> tree_sort(range(10, -10, -1)) == tuple(sorted(range(10, -10, -1))) + # True + """ if len(arr) == 0: - return arr + return tuple(arr) root = Node(arr[0]) - for i in range(1, len(arr)): - root.insert(arr[i]) - # Traverse BST in order. - res = [] - inorder(root, res) - return res + for item in arr[1:]: + root.insert(item) + return tuple(root) if __name__ == "__main__": - print(tree_sort([10, 1, 3, 2, 9, 14, 13])) + import doctest + + doctest.testmod() + print(f"{tree_sort([5, 6, 1, -1, 4, 37, -3, 7]) = }") diff --git a/source/__init__.py b/source/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/strings/autocomplete_using_trie.py b/strings/autocomplete_using_trie.py index 758260292a30..77a3050ab15f 100644 --- a/strings/autocomplete_using_trie.py +++ b/strings/autocomplete_using_trie.py @@ -27,10 +27,7 @@ def find_word(self, prefix: str) -> tuple | list: def _elements(self, d: dict) -> tuple: result = [] for c, v in d.items(): - if c == END: - sub_result = [" "] - else: - sub_result = [c + s for s in self._elements(v)] + sub_result = [" "] if c == END else [(c + s) for s in self._elements(v)] result.extend(sub_result) return tuple(result) diff --git a/strings/barcode_validator.py b/strings/barcode_validator.py index e050cd337d74..b4f3864e2642 100644 --- a/strings/barcode_validator.py +++ b/strings/barcode_validator.py @@ -65,7 +65,8 @@ def get_barcode(barcode: str) -> int: ValueError: Barcode 'dwefgiweuf' has alphabetic characters. """ if str(barcode).isalpha(): - raise ValueError(f"Barcode '{barcode}' has alphabetic characters.") + msg = f"Barcode '{barcode}' has alphabetic characters." + raise ValueError(msg) elif int(barcode) < 0: raise ValueError("The entered barcode has a negative value. Try again.") else: diff --git a/strings/bitap_string_match.py b/strings/bitap_string_match.py new file mode 100644 index 000000000000..bd8a0f0d73ec --- /dev/null +++ b/strings/bitap_string_match.py @@ -0,0 +1,79 @@ +""" +Bitap exact string matching +https://en.wikipedia.org/wiki/Bitap_algorithm + +Searches for a pattern inside text, and returns the index of the first occurrence +of the pattern. Both text and pattern consist of lowercase alphabetical characters only. + +Complexity: O(m*n) + n = length of text + m = length of pattern + +Python doctests can be run using this command: +python3 -m doctest -v bitap_string_match.py +""" + + +def bitap_string_match(text: str, pattern: str) -> int: + """ + Retrieves the index of the first occurrence of pattern in text. + + Args: + text: A string consisting only of lowercase alphabetical characters. + pattern: A string consisting only of lowercase alphabetical characters. + + Returns: + int: The index where pattern first occurs. Return -1 if not found. + + >>> bitap_string_match('abdabababc', 'ababc') + 5 + >>> bitap_string_match('aaaaaaaaaaaaaaaaaa', 'a') + 0 + >>> bitap_string_match('zxywsijdfosdfnso', 'zxywsijdfosdfnso') + 0 + >>> bitap_string_match('abdabababc', '') + 0 + >>> bitap_string_match('abdabababc', 'c') + 9 + >>> bitap_string_match('abdabababc', 'fofosdfo') + -1 + >>> bitap_string_match('abdab', 'fofosdfo') + -1 + """ + if not pattern: + return 0 + m = len(pattern) + if m > len(text): + return -1 + + # Initial state of bit string 1110 + state = ~1 + # Bit = 0 if character appears at index, and 1 otherwise + pattern_mask: list[int] = [~0] * 27 # 1111 + + for i, char in enumerate(pattern): + # For the pattern mask for this character, set the bit to 0 for each i + # the character appears. + pattern_index: int = ord(char) - ord("a") + pattern_mask[pattern_index] &= ~(1 << i) + + for i, char in enumerate(text): + text_index = ord(char) - ord("a") + # If this character does not appear in pattern, it's pattern mask is 1111. + # Performing a bitwise OR between state and 1111 will reset the state to 1111 + # and start searching the start of pattern again. + state |= pattern_mask[text_index] + state <<= 1 + + # If the mth bit (counting right to left) of the state is 0, then we have + # found pattern in text + if (state & (1 << m)) == 0: + return i - m + 1 + + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/strings/boyer_moore_search.py b/strings/boyer_moore_search.py index 117305d32fd3..9615d2fd659b 100644 --- a/strings/boyer_moore_search.py +++ b/strings/boyer_moore_search.py @@ -17,6 +17,7 @@ n=length of main string m=length of pattern string """ + from __future__ import annotations diff --git a/strings/camel_case_to_snake_case.py b/strings/camel_case_to_snake_case.py new file mode 100644 index 000000000000..582907be2edb --- /dev/null +++ b/strings/camel_case_to_snake_case.py @@ -0,0 +1,60 @@ +def camel_to_snake_case(input_str: str) -> str: + """ + Transforms a camelCase (or PascalCase) string to snake_case + + >>> camel_to_snake_case("someRandomString") + 'some_random_string' + + >>> camel_to_snake_case("SomeRandomStr#ng") + 'some_random_str_ng' + + >>> camel_to_snake_case("123someRandom123String123") + '123_some_random_123_string_123' + + >>> camel_to_snake_case("123SomeRandom123String123") + '123_some_random_123_string_123' + + >>> camel_to_snake_case(123) + Traceback (most recent call last): + ... + ValueError: Expected string as input, found + + """ + + # check for invalid input type + if not isinstance(input_str, str): + msg = f"Expected string as input, found {type(input_str)}" + raise ValueError(msg) + + snake_str = "" + + for index, char in enumerate(input_str): + if char.isupper(): + snake_str += "_" + char.lower() + + # if char is lowercase but proceeded by a digit: + elif input_str[index - 1].isdigit() and char.islower(): + snake_str += "_" + char + + # if char is a digit proceeded by a letter: + elif input_str[index - 1].isalpha() and char.isnumeric(): + snake_str += "_" + char.lower() + + # if char is not alphanumeric: + elif not char.isalnum(): + snake_str += "_" + + else: + snake_str += char + + # remove leading underscore + if snake_str[0] == "_": + snake_str = snake_str[1:] + + return snake_str + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/can_string_be_rearranged_as_palindrome.py b/strings/can_string_be_rearranged_as_palindrome.py index 21d653db1405..95cda8b72180 100644 --- a/strings/can_string_be_rearranged_as_palindrome.py +++ b/strings/can_string_be_rearranged_as_palindrome.py @@ -72,9 +72,7 @@ def can_string_be_rearranged_as_palindrome(input_str: str = "") -> bool: for character_count in character_freq_dict.values(): if character_count % 2: odd_char += 1 - if odd_char > 1: - return False - return True + return not odd_char > 1 def benchmark(input_str: str = "") -> None: diff --git a/strings/capitalize.py b/strings/capitalize.py index 63603aa07e2d..c0b45e0d9614 100644 --- a/strings/capitalize.py +++ b/strings/capitalize.py @@ -3,7 +3,8 @@ def capitalize(sentence: str) -> str: """ - This function will capitalize the first letter of a sentence or a word + Capitalizes the first letter of a sentence or word. + >>> capitalize("hello world") 'Hello world' >>> capitalize("123 hello world") @@ -17,7 +18,11 @@ def capitalize(sentence: str) -> str: """ if not sentence: return "" - lower_to_upper = {lc: uc for lc, uc in zip(ascii_lowercase, ascii_uppercase)} + + # Create a dictionary that maps lowercase letters to uppercase letters + # Capitalize the first character if it's a lowercase letter + # Concatenate the capitalized character with the rest of the string + lower_to_upper = dict(zip(ascii_lowercase, ascii_uppercase)) return lower_to_upper.get(sentence[0], sentence[0]) + sentence[1:] diff --git a/strings/check_anagrams.py b/strings/check_anagrams.py index 0d2f8091a3f0..d747368b2373 100644 --- a/strings/check_anagrams.py +++ b/strings/check_anagrams.py @@ -1,8 +1,8 @@ """ wiki: https://en.wikipedia.org/wiki/Anagram """ + from collections import defaultdict -from typing import DefaultDict def check_anagrams(first_str: str, second_str: str) -> bool: @@ -30,7 +30,7 @@ def check_anagrams(first_str: str, second_str: str) -> bool: return False # Default values for count should be 0 - count: DefaultDict[str, int] = defaultdict(int) + count: defaultdict[str, int] = defaultdict(int) # For each character in input strings, # increment count in the corresponding @@ -38,10 +38,7 @@ def check_anagrams(first_str: str, second_str: str) -> bool: count[first_str[i]] += 1 count[second_str[i]] -= 1 - for _count in count.values(): - if _count != 0: - return False - return True + return all(_count == 0 for _count in count.values()) if __name__ == "__main__": diff --git a/strings/count_vowels.py b/strings/count_vowels.py new file mode 100644 index 000000000000..8a52b331c81b --- /dev/null +++ b/strings/count_vowels.py @@ -0,0 +1,34 @@ +def count_vowels(s: str) -> int: + """ + Count the number of vowels in a given string. + + :param s: Input string to count vowels in. + :return: Number of vowels in the input string. + + Examples: + >>> count_vowels("hello world") + 3 + >>> count_vowels("HELLO WORLD") + 3 + >>> count_vowels("123 hello world") + 3 + >>> count_vowels("") + 0 + >>> count_vowels("a quick brown fox") + 5 + >>> count_vowels("the quick BROWN fox") + 5 + >>> count_vowels("PYTHON") + 1 + """ + if not isinstance(s, str): + raise ValueError("Input must be a string") + + vowels = "aeiouAEIOU" + return sum(1 for char in s if char in vowels) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/credit_card_validator.py b/strings/credit_card_validator.py index 78bf45740a63..b8da1c745124 100644 --- a/strings/credit_card_validator.py +++ b/strings/credit_card_validator.py @@ -36,7 +36,7 @@ def luhn_validation(credit_card_number: str) -> bool: digit = int(cc_number[i]) digit *= 2 # If doubling of a number results in a two digit number - # i.e greater than 9(e.g., 6 × 2 = 12), + # i.e greater than 9(e.g., 6 x 2 = 12), # then add the digits of the product (e.g., 12: 1 + 2 = 3, 15: 1 + 5 = 6), # to get a single digit number. if digit > 9: diff --git a/strings/damerau_levenshtein_distance.py b/strings/damerau_levenshtein_distance.py new file mode 100644 index 000000000000..72de019499e2 --- /dev/null +++ b/strings/damerau_levenshtein_distance.py @@ -0,0 +1,71 @@ +""" +This script is a implementation of the Damerau-Levenshtein distance algorithm. + +It's an algorithm that measures the edit distance between two string sequences + +More information about this algorithm can be found in this wikipedia article: +https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance +""" + + +def damerau_levenshtein_distance(first_string: str, second_string: str) -> int: + """ + Implements the Damerau-Levenshtein distance algorithm that measures + the edit distance between two strings. + + Parameters: + first_string: The first string to compare + second_string: The second string to compare + + Returns: + distance: The edit distance between the first and second strings + + >>> damerau_levenshtein_distance("cat", "cut") + 1 + >>> damerau_levenshtein_distance("kitten", "sitting") + 3 + >>> damerau_levenshtein_distance("hello", "world") + 4 + >>> damerau_levenshtein_distance("book", "back") + 2 + >>> damerau_levenshtein_distance("container", "containment") + 3 + >>> damerau_levenshtein_distance("container", "containment") + 3 + """ + # Create a dynamic programming matrix to store the distances + dp_matrix = [[0] * (len(second_string) + 1) for _ in range(len(first_string) + 1)] + + # Initialize the matrix + for i in range(len(first_string) + 1): + dp_matrix[i][0] = i + for j in range(len(second_string) + 1): + dp_matrix[0][j] = j + + # Fill the matrix + for i, first_char in enumerate(first_string, start=1): + for j, second_char in enumerate(second_string, start=1): + cost = int(first_char != second_char) + + dp_matrix[i][j] = min( + dp_matrix[i - 1][j] + 1, # Deletion + dp_matrix[i][j - 1] + 1, # Insertion + dp_matrix[i - 1][j - 1] + cost, # Substitution + ) + + if ( + i > 1 + and j > 1 + and first_string[i - 1] == second_string[j - 2] + and first_string[i - 2] == second_string[j - 1] + ): + # Transposition + dp_matrix[i][j] = min(dp_matrix[i][j], dp_matrix[i - 2][j - 2] + cost) + + return dp_matrix[-1][-1] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/strings/detecting_english_programmatically.py b/strings/detecting_english_programmatically.py index b9000101beb4..e30e2ea8dd8b 100644 --- a/strings/detecting_english_programmatically.py +++ b/strings/detecting_english_programmatically.py @@ -25,6 +25,18 @@ def get_english_count(message: str) -> float: def remove_non_letters(message: str) -> str: + """ + >>> remove_non_letters("Hi! how are you?") + 'Hi how are you' + >>> remove_non_letters("P^y%t)h@o*n") + 'Python' + >>> remove_non_letters("1+1=2") + '' + >>> remove_non_letters("www.google.com/") + 'wwwgooglecom' + >>> remove_non_letters("") + '' + """ return "".join(symbol for symbol in message if symbol in LETTERS_AND_SPACE) diff --git a/strings/edit_distance.py b/strings/edit_distance.py new file mode 100644 index 000000000000..e842c8555c8e --- /dev/null +++ b/strings/edit_distance.py @@ -0,0 +1,32 @@ +def edit_distance(source: str, target: str) -> int: + """ + Edit distance algorithm is a string metric, i.e., it is a way of quantifying how + dissimilar two strings are to one another. It is measured by counting the minimum + number of operations required to transform one string into another. + + This implementation assumes that the cost of operations (insertion, deletion and + substitution) is always 1 + + Args: + source: the initial string with respect to which we are calculating the edit + distance for the target + target: the target string, formed after performing n operations on the source string + + >>> edit_distance("GATTIC", "GALTIC") + 1 + """ + if len(source) == 0: + return len(target) + elif len(target) == 0: + return len(source) + + delta = int(source[-1] != target[-1]) # Substitution + return min( + edit_distance(source[:-1], target[:-1]) + delta, + edit_distance(source, target[:-1]) + 1, + edit_distance(source[:-1], target) + 1, + ) + + +if __name__ == "__main__": + print(edit_distance("ATCGCTG", "TAGCTAA")) # Answer is 4 diff --git a/strings/frequency_finder.py b/strings/frequency_finder.py index 19f97afbbe37..e5afee891bd9 100644 --- a/strings/frequency_finder.py +++ b/strings/frequency_finder.py @@ -49,6 +49,15 @@ def get_item_at_index_zero(x: tuple) -> str: def get_frequency_order(message: str) -> str: + """ + Get the frequency order of the letters in the given string + >>> get_frequency_order('Hello World') + 'LOWDRHEZQXJKVBPYGFMUCSNIAT' + >>> get_frequency_order('Hello@') + 'LHOEZQXJKVBPYGFWMUCDRSNIAT' + >>> get_frequency_order('h') + 'HZQXJKVBPYGFWMUCLDRSNIOATE' + """ letter_to_freq = get_letter_count(message) freq_to_letter: dict[int, list[str]] = { freq: [] for letter, freq in letter_to_freq.items() @@ -58,7 +67,7 @@ def get_frequency_order(message: str) -> str: freq_to_letter_str: dict[int, str] = {} - for freq in freq_to_letter: + for freq in freq_to_letter: # noqa: PLC0206 freq_to_letter[freq].sort(key=ETAOIN.find, reverse=True) freq_to_letter_str[freq] = "".join(freq_to_letter[freq]) diff --git a/strings/is_palindrome.py b/strings/is_palindrome.py deleted file mode 100644 index 9bf2abd98486..000000000000 --- a/strings/is_palindrome.py +++ /dev/null @@ -1,44 +0,0 @@ -def is_palindrome(s: str) -> bool: - """ - Determine if the string s is a palindrome. - - >>> is_palindrome("A man, A plan, A canal -- Panama!") - True - >>> is_palindrome("Hello") - False - >>> is_palindrome("Able was I ere I saw Elba") - True - >>> is_palindrome("racecar") - True - >>> is_palindrome("Mr. Owl ate my metal worm?") - True - """ - # Since punctuation, capitalization, and spaces are often ignored while checking - # palindromes, we first remove them from our string. - s = "".join(character for character in s.lower() if character.isalnum()) - # return s == s[::-1] the slicing method - # uses extra spaces we can - # better with iteration method. - - end = len(s) // 2 - n = len(s) - - # We need to traverse till half of the length of string - # as we can get access of the i'th last element from - # i'th index. - # eg: [0,1,2,3,4,5] => 4th index can be accessed - # with the help of 1st index (i==n-i-1) - # where n is length of string - - for i in range(end): - if s[i] != s[n - i - 1]: - return False - return True - - -if __name__ == "__main__": - s = input("Please enter a string to see if it is a palindrome: ") - if is_palindrome(s): - print(f"'{s}' is a palindrome.") - else: - print(f"'{s}' is not a palindrome.") diff --git a/strings/is_polish_national_id.py b/strings/is_polish_national_id.py new file mode 100644 index 000000000000..8b463a24532a --- /dev/null +++ b/strings/is_polish_national_id.py @@ -0,0 +1,92 @@ +def is_polish_national_id(input_str: str) -> bool: + """ + Verification of the correctness of the PESEL number. + www-gov-pl.translate.goog/web/gov/czym-jest-numer-pesel?_x_tr_sl=auto&_x_tr_tl=en + + PESEL can start with 0, that's why we take str as input, + but convert it to int for some calculations. + + + >>> is_polish_national_id(123) + Traceback (most recent call last): + ... + ValueError: Expected str as input, found + + >>> is_polish_national_id("abc") + Traceback (most recent call last): + ... + ValueError: Expected number as input + + >>> is_polish_national_id("02070803628") # correct PESEL + True + + >>> is_polish_national_id("02150803629") # wrong month + False + + >>> is_polish_national_id("02075503622") # wrong day + False + + >>> is_polish_national_id("-99012212349") # wrong range + False + + >>> is_polish_national_id("990122123499999") # wrong range + False + + >>> is_polish_national_id("02070803621") # wrong checksum + False + """ + + # check for invalid input type + if not isinstance(input_str, str): + msg = f"Expected str as input, found {type(input_str)}" + raise ValueError(msg) + + # check if input can be converted to int + try: + input_int = int(input_str) + except ValueError: + msg = "Expected number as input" + raise ValueError(msg) + + # check number range + if not 10100000 <= input_int <= 99923199999: + return False + + # check month correctness + month = int(input_str[2:4]) + + if ( + month not in range(1, 13) # year 1900-1999 + and month not in range(21, 33) # 2000-2099 + and month not in range(41, 53) # 2100-2199 + and month not in range(61, 73) # 2200-2299 + and month not in range(81, 93) # 1800-1899 + ): + return False + + # check day correctness + day = int(input_str[4:6]) + + if day not in range(1, 32): + return False + + # check the checksum + multipliers = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3] + subtotal = 0 + + digits_to_check = str(input_str)[:-1] # cut off the checksum + + for index, digit in enumerate(digits_to_check): + # Multiply corresponding digits and multipliers. + # In case of a double-digit result, add only the last digit. + subtotal += (int(digit) * multipliers[index]) % 10 + + checksum = 10 - subtotal % 10 + + return checksum == input_int % 10 + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/is_spain_national_id.py b/strings/is_spain_national_id.py index 67f49755f412..60d06e123aae 100644 --- a/strings/is_spain_national_id.py +++ b/strings/is_spain_national_id.py @@ -48,7 +48,8 @@ def is_spain_national_id(spanish_id: str) -> bool: """ if not isinstance(spanish_id, str): - raise TypeError(f"Expected string as input, found {type(spanish_id).__name__}") + msg = f"Expected string as input, found {type(spanish_id).__name__}" + raise TypeError(msg) spanish_id_clean = spanish_id.replace("-", "").upper() if len(spanish_id_clean) != 9: diff --git a/strings/is_srilankan_phone_number.py b/strings/is_srilankan_phone_number.py index 7bded93f7f1d..6456f85e1a3d 100644 --- a/strings/is_srilankan_phone_number.py +++ b/strings/is_srilankan_phone_number.py @@ -22,9 +22,7 @@ def is_sri_lankan_phone_number(phone: str) -> bool: False """ - pattern = re.compile( - r"^(?:0|94|\+94|0{2}94)" r"7(0|1|2|4|5|6|7|8)" r"(-| |)" r"\d{7}$" - ) + pattern = re.compile(r"^(?:0|94|\+94|0{2}94)7(0|1|2|4|5|6|7|8)(-| |)\d{7}$") return bool(re.search(pattern, phone)) diff --git a/strings/is_valid_email_address.py b/strings/is_valid_email_address.py new file mode 100644 index 000000000000..c3bf7df7349d --- /dev/null +++ b/strings/is_valid_email_address.py @@ -0,0 +1,115 @@ +""" +Implements an is valid email address algorithm + +@ https://en.wikipedia.org/wiki/Email_address +""" + +import string + +email_tests: tuple[tuple[str, bool], ...] = ( + ("simple@example.com", True), + ("very.common@example.com", True), + ("disposable.style.email.with+symbol@example.com", True), + ("other-email-with-hyphen@and.subdomains.example.com", True), + ("fully-qualified-domain@example.com", True), + ("user.name+tag+sorting@example.com", True), + ("x@example.com", True), + ("example-indeed@strange-example.com", True), + ("test/test@test.com", True), + ( + "123456789012345678901234567890123456789012345678901234567890123@example.com", + True, + ), + ("admin@mailserver1", True), + ("example@s.example", True), + ("Abc.example.com", False), + ("A@b@c@example.com", False), + ("abc@example..com", False), + ("a(c)d,e:f;gi[j\\k]l@example.com", False), + ( + "12345678901234567890123456789012345678901234567890123456789012345@example.com", + False, + ), + ("i.like.underscores@but_its_not_allowed_in_this_part", False), + ("", False), +) + +# The maximum octets (one character as a standard unicode character is one byte) +# that the local part and the domain part can have +MAX_LOCAL_PART_OCTETS = 64 +MAX_DOMAIN_OCTETS = 255 + + +def is_valid_email_address(email: str) -> bool: + """ + Returns True if the passed email address is valid. + + The local part of the email precedes the singular @ symbol and + is associated with a display-name. For example, "john.smith" + The domain is stricter than the local part and follows the @ symbol. + + Global email checks: + 1. There can only be one @ symbol in the email address. Technically if the + @ symbol is quoted in the local-part, then it is valid, however this + implementation ignores "" for now. + (See https://en.wikipedia.org/wiki/Email_address#:~:text=If%20quoted,) + 2. The local-part and the domain are limited to a certain number of octets. With + unicode storing a single character in one byte, each octet is equivalent to + a character. Hence, we can just check the length of the string. + Checks for the local-part: + 3. The local-part may contain: upper and lowercase latin letters, digits 0 to 9, + and printable characters (!#$%&'*+-/=?^_`{|}~) + 4. The local-part may also contain a "." in any place that is not the first or + last character, and may not have more than one "." consecutively. + + Checks for the domain: + 5. The domain may contain: upper and lowercase latin letters and digits 0 to 9 + 6. Hyphen "-", provided that it is not the first or last character + 7. The domain may also contain a "." in any place that is not the first or + last character, and may not have more than one "." consecutively. + + >>> for email, valid in email_tests: + ... assert is_valid_email_address(email) == valid + """ + + # (1.) Make sure that there is only one @ symbol in the email address + if email.count("@") != 1: + return False + + local_part, domain = email.split("@") + # (2.) Check octet length of the local part and domain + if len(local_part) > MAX_LOCAL_PART_OCTETS or len(domain) > MAX_DOMAIN_OCTETS: + return False + + # (3.) Validate the characters in the local-part + if any( + char not in string.ascii_letters + string.digits + ".(!#$%&'*+-/=?^_`{|}~)" + for char in local_part + ): + return False + + # (4.) Validate the placement of "." characters in the local-part + if local_part.startswith(".") or local_part.endswith(".") or ".." in local_part: + return False + + # (5.) Validate the characters in the domain + if any(char not in string.ascii_letters + string.digits + ".-" for char in domain): + return False + + # (6.) Validate the placement of "-" characters + if domain.startswith("-") or domain.endswith("."): + return False + + # (7.) Validate the placement of "." characters + return not (domain.startswith(".") or domain.endswith(".") or ".." in domain) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + for email, valid in email_tests: + is_valid = is_valid_email_address(email) + assert is_valid == valid, f"{email} is {is_valid}" + print(f"Email address {email} is {'not ' if not is_valid else ''}valid") diff --git a/strings/jaro_winkler.py b/strings/jaro_winkler.py index f4a8fbad3ac8..0ce5d83b3c41 100644 --- a/strings/jaro_winkler.py +++ b/strings/jaro_winkler.py @@ -3,7 +3,7 @@ def jaro_winkler(str1: str, str2: str) -> float: """ - Jaro–Winkler distance is a string metric measuring an edit distance between two + Jaro-Winkler distance is a string metric measuring an edit distance between two sequences. Output value is between 0.0 and 1.0. @@ -28,12 +28,14 @@ def jaro_winkler(str1: str, str2: str) -> float: def get_matched_characters(_str1: str, _str2: str) -> str: matched = [] limit = min(len(_str1), len(_str2)) // 2 - for i, l in enumerate(_str1): + for i, char in enumerate(_str1): left = int(max(0, i - limit)) right = int(min(i + limit + 1, len(_str2))) - if l in _str2[left:right]: - matched.append(l) - _str2 = f"{_str2[0:_str2.index(l)]} {_str2[_str2.index(l) + 1:]}" + if char in _str2[left:right]: + matched.append(char) + _str2 = ( + f"{_str2[0 : _str2.index(char)]} {_str2[_str2.index(char) + 1 :]}" + ) return "".join(matched) diff --git a/strings/join.py b/strings/join.py index 739856c1aa93..cdcc3a1377f4 100644 --- a/strings/join.py +++ b/strings/join.py @@ -1,10 +1,21 @@ """ -Program to join a list of strings with a given separator +Program to join a list of strings with a separator """ def join(separator: str, separated: list[str]) -> str: """ + Joins a list of strings using a separator + and returns the result. + + :param separator: Separator to be used + for joining the strings. + :param separated: List of strings to be joined. + + :return: Joined string with the specified separator. + + Examples: + >>> join("", ["a", "b", "c", "d"]) 'abcd' >>> join("#", ["a", "b", "c", "d"]) @@ -13,17 +24,48 @@ def join(separator: str, separated: list[str]) -> str: 'a' >>> join(" ", ["You", "are", "amazing!"]) 'You are amazing!' + >>> join(",", ["", "", ""]) + ',,' + + This example should raise an + exception for non-string elements: >>> join("#", ["a", "b", "c", 1]) Traceback (most recent call last): ... - Exception: join() accepts only strings to be joined + Exception: join() accepts only strings + + Additional test case with a different separator: + >>> join("-", ["apple", "banana", "cherry"]) + 'apple-banana-cherry' """ - joined = "" + + # Check that all elements are strings for word_or_phrase in separated: + # If the element is not a string, raise an exception if not isinstance(word_or_phrase, str): - raise Exception("join() accepts only strings to be joined") + raise Exception("join() accepts only strings") + + joined: str = "" + """ + The last element of the list is not followed by the separator. + So, we need to iterate through the list and join each element + with the separator except the last element. + """ + last_index: int = len(separated) - 1 + """ + Iterate through the list and join each element with the separator. + Except the last element, all other elements are followed by the separator. + """ + for word_or_phrase in separated[:last_index]: + # join the element with the separator. joined += word_or_phrase + separator - return joined.strip(separator) + + # If the list is not empty, join the last element. + if separated != []: + joined += separated[last_index] + + # Return the joined string. + return joined if __name__ == "__main__": diff --git a/strings/knuth_morris_pratt.py b/strings/knuth_morris_pratt.py index a488c171a93b..5120779c571e 100644 --- a/strings/knuth_morris_pratt.py +++ b/strings/knuth_morris_pratt.py @@ -1,7 +1,7 @@ from __future__ import annotations -def kmp(pattern: str, text: str) -> bool: +def knuth_morris_pratt(text: str, pattern: str) -> int: """ The Knuth-Morris-Pratt Algorithm for finding a pattern within a piece of text with complexity O(n + m) @@ -14,6 +14,12 @@ def kmp(pattern: str, text: str) -> bool: 2) Step through the text one character at a time and compare it to a character in the pattern updating our location within the pattern if necessary + >>> kmp = "knuth_morris_pratt" + >>> all( + ... knuth_morris_pratt(kmp, s) == kmp.find(s) + ... for s in ("kn", "h_m", "rr", "tt", "not there") + ... ) + True """ # 1) Construct the failure array @@ -24,7 +30,7 @@ def kmp(pattern: str, text: str) -> bool: while i < len(text): if pattern[j] == text[i]: if j == (len(pattern) - 1): - return True + return i - j j += 1 # if this is a prefix in our pattern @@ -33,7 +39,7 @@ def kmp(pattern: str, text: str) -> bool: j = failure[j - 1] continue i += 1 - return False + return -1 def get_failure_array(pattern: str) -> list[int]: @@ -57,27 +63,39 @@ def get_failure_array(pattern: str) -> list[int]: if __name__ == "__main__": + import doctest + + doctest.testmod() + # Test 1) pattern = "abc1abc12" text1 = "alskfjaldsabc1abc1abc12k23adsfabcabc" text2 = "alskfjaldsk23adsfabcabc" - assert kmp(pattern, text1) and not kmp(pattern, text2) + assert knuth_morris_pratt(text1, pattern) + assert knuth_morris_pratt(text2, pattern) # Test 2) pattern = "ABABX" text = "ABABZABABYABABX" - assert kmp(pattern, text) + assert knuth_morris_pratt(text, pattern) # Test 3) pattern = "AAAB" text = "ABAAAAAB" - assert kmp(pattern, text) + assert knuth_morris_pratt(text, pattern) # Test 4) pattern = "abcdabcy" text = "abcxabcdabxabcdabcdabcy" - assert kmp(pattern, text) + assert knuth_morris_pratt(text, pattern) + + # Test 5) -> Doctests + kmp = "knuth_morris_pratt" + assert all( + knuth_morris_pratt(kmp, s) == kmp.find(s) + for s in ("kn", "h_m", "rr", "tt", "not there") + ) - # Test 5) + # Test 6) pattern = "aabaabaaa" assert get_failure_array(pattern) == [0, 1, 0, 1, 2, 3, 4, 5, 2] diff --git a/strings/levenshtein_distance.py b/strings/levenshtein_distance.py index 7be4074dc39b..3af6608723a5 100644 --- a/strings/levenshtein_distance.py +++ b/strings/levenshtein_distance.py @@ -1,20 +1,9 @@ -""" -This is a Python implementation of the levenshtein distance. -Levenshtein distance is a string metric for measuring the -difference between two sequences. - -For doctests run following command: -python -m doctest -v levenshtein-distance.py -or -python3 -m doctest -v levenshtein-distance.py - -For manual testing run: -python levenshtein-distance.py -""" +from collections.abc import Callable def levenshtein_distance(first_word: str, second_word: str) -> int: - """Implementation of the levenshtein distance in Python. + """ + Implementation of the Levenshtein distance in Python. :param first_word: the first word to measure the difference. :param second_word: the second word to measure the difference. :return: the levenshtein distance between the two words. @@ -47,7 +36,7 @@ def levenshtein_distance(first_word: str, second_word: str) -> int: current_row = [i + 1] for j, c2 in enumerate(second_word): - # Calculate insertions, deletions and substitutions + # Calculate insertions, deletions, and substitutions insertions = previous_row[j + 1] + 1 deletions = current_row[j] + 1 substitutions = previous_row[j] + (c1 != c2) @@ -62,9 +51,75 @@ def levenshtein_distance(first_word: str, second_word: str) -> int: return previous_row[-1] +def levenshtein_distance_optimized(first_word: str, second_word: str) -> int: + """ + Compute the Levenshtein distance between two words (strings). + The function is optimized for efficiency by modifying rows in place. + :param first_word: the first word to measure the difference. + :param second_word: the second word to measure the difference. + :return: the Levenshtein distance between the two words. + Examples: + >>> levenshtein_distance_optimized("planet", "planetary") + 3 + >>> levenshtein_distance_optimized("", "test") + 4 + >>> levenshtein_distance_optimized("book", "back") + 2 + >>> levenshtein_distance_optimized("book", "book") + 0 + >>> levenshtein_distance_optimized("test", "") + 4 + >>> levenshtein_distance_optimized("", "") + 0 + >>> levenshtein_distance_optimized("orchestration", "container") + 10 + """ + if len(first_word) < len(second_word): + return levenshtein_distance_optimized(second_word, first_word) + + if len(second_word) == 0: + return len(first_word) + + previous_row = list(range(len(second_word) + 1)) + + for i, c1 in enumerate(first_word): + current_row = [i + 1] + [0] * len(second_word) + + for j, c2 in enumerate(second_word): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row[j + 1] = min(insertions, deletions, substitutions) + + previous_row = current_row + + return previous_row[-1] + + +def benchmark_levenshtein_distance(func: Callable) -> None: + """ + Benchmark the Levenshtein distance function. + :param str: The name of the function being benchmarked. + :param func: The function to be benchmarked. + """ + from timeit import timeit + + stmt = f"{func.__name__}('sitting', 'kitten')" + setup = f"from __main__ import {func.__name__}" + number = 25_000 + result = timeit(stmt=stmt, setup=setup, number=number) + print(f"{func.__name__:<30} finished {number:,} runs in {result:.5f} seconds") + + if __name__ == "__main__": - first_word = input("Enter the first word:\n").strip() - second_word = input("Enter the second word:\n").strip() + # Get user input for words + first_word = input("Enter the first word for Levenshtein distance:\n").strip() + second_word = input("Enter the second word for Levenshtein distance:\n").strip() + + # Calculate and print Levenshtein distances + print(f"{levenshtein_distance(first_word, second_word) = }") + print(f"{levenshtein_distance_optimized(first_word, second_word) = }") - result = levenshtein_distance(first_word, second_word) - print(f"Levenshtein distance between {first_word} and {second_word} is {result}") + # Benchmark the Levenshtein distance functions + benchmark_levenshtein_distance(levenshtein_distance) + benchmark_levenshtein_distance(levenshtein_distance_optimized) diff --git a/strings/lower.py b/strings/lower.py index 9ae419123ceb..49256b0169ef 100644 --- a/strings/lower.py +++ b/strings/lower.py @@ -14,9 +14,9 @@ def lower(word: str) -> str: 'what' """ - # converting to ascii value int value and checking to see if char is a capital - # letter if it is a capital letter it is getting shift by 32 which makes it a lower - # case letter + # Converting to ASCII value, obtaining the integer representation + # and checking to see if the character is a capital letter. + # If it is a capital letter, it is shifted by 32, making it a lowercase letter. return "".join(chr(ord(char) + 32) if "A" <= char <= "Z" else char for char in word) diff --git a/strings/manacher.py b/strings/manacher.py index c58c7c19ec44..af1b10cf81fb 100644 --- a/strings/manacher.py +++ b/strings/manacher.py @@ -5,13 +5,13 @@ def palindromic_string(input_string: str) -> str: >>> palindromic_string('ababa') 'ababa' - Manacher’s algorithm which finds Longest palindromic Substring in linear time. + Manacher's algorithm which finds Longest palindromic Substring in linear time. 1. first this convert input_string("xyx") into new_string("x|y|x") where odd positions are actual input characters. - 2. for each character in new_string it find corresponding length and store the - length and l,r to store previously calculated info.(please look the explanation - for details) + 2. for each character in new_string it find corresponding length and + store the length and left,right to store previously calculated info. + (please look the explanation for details) 3. return corresponding output_string by removing all "|" """ @@ -29,7 +29,7 @@ def palindromic_string(input_string: str) -> str: # we will store the starting and ending of previous furthest ending palindromic # substring - l, r = 0, 0 + left, right = 0, 0 # length[i] shows the length of palindromic substring with center i length = [1 for i in range(len(new_input_string))] @@ -37,7 +37,7 @@ def palindromic_string(input_string: str) -> str: # for each character in new_string find corresponding palindromic string start = 0 for j in range(len(new_input_string)): - k = 1 if j > r else min(length[l + r - j] // 2, r - j + 1) + k = 1 if j > right else min(length[left + right - j] // 2, right - j + 1) while ( j - k >= 0 and j + k < len(new_input_string) @@ -47,11 +47,11 @@ def palindromic_string(input_string: str) -> str: length[j] = 2 * k - 1 - # does this string is ending after the previously explored end (that is r) ? - # if yes the update the new r to the last index of this - if j + k - 1 > r: - l = j - k + 1 # noqa: E741 - r = j + k - 1 + # does this string is ending after the previously explored end (that is right) ? + # if yes the update the new right to the last index of this + if j + k - 1 > right: + left = j - k + 1 + right = j + k - 1 # update max_length and start position if max_length < length[j]: @@ -78,8 +78,9 @@ def palindromic_string(input_string: str) -> str: consider the string for which we are calculating the longest palindromic substring is shown above where ... are some characters in between and right now we are calculating the length of palindromic substring with center at a5 with following conditions : -i) we have stored the length of palindromic substring which has center at a3 (starts at - l ends at r) and it is the furthest ending till now, and it has ending after a6 +i) we have stored the length of palindromic substring which has center at a3 + (starts at left ends at right) and it is the furthest ending till now, + and it has ending after a6 ii) a2 and a4 are equally distant from a3 so char(a2) == char(a4) iii) a0 and a6 are equally distant from a3 so char(a0) == char(a6) iv) a1 is corresponding equal character of a5 in palindrome with center a3 (remember @@ -98,11 +99,11 @@ def palindromic_string(input_string: str) -> str: a1 but this only holds if a0 and a6 are inside the limits of palindrome centered at a3 so finally .. -len_of_palindrome__at(a5) = min(len_of_palindrome_at(a1), r-a5) -where a3 lies from l to r and we have to keep updating that +len_of_palindrome__at(a5) = min(len_of_palindrome_at(a1), right-a5) +where a3 lies from left to right and we have to keep updating that -and if the a5 lies outside of l,r boundary we calculate length of palindrome with -bruteforce and update l,r. +and if the a5 lies outside of left,right boundary we calculate length of palindrome with +bruteforce and update left,right. it gives the linear time complexity just like z-function """ diff --git a/strings/min_cost_string_conversion.py b/strings/min_cost_string_conversion.py index 089c2532f900..87eb5189e16a 100644 --- a/strings/min_cost_string_conversion.py +++ b/strings/min_cost_string_conversion.py @@ -17,11 +17,27 @@ def compute_transform_tables( delete_cost: int, insert_cost: int, ) -> tuple[list[list[int]], list[list[str]]]: + """ + Finds the most cost efficient sequence + for converting one string into another. + + >>> costs, operations = compute_transform_tables("cat", "cut", 1, 2, 3, 3) + >>> costs[0][:4] + [0, 3, 6, 9] + >>> costs[2][:4] + [6, 4, 3, 6] + >>> operations[0][:4] + ['0', 'Ic', 'Iu', 'It'] + >>> operations[3][:4] + ['Dt', 'Dt', 'Rtu', 'Ct'] + + >>> compute_transform_tables("", "", 1, 2, 3, 3) + ([[0]], [['0']]) + """ source_seq = list(source_string) destination_seq = list(destination_string) len_source_seq = len(source_seq) len_destination_seq = len(destination_seq) - costs = [ [0 for _ in range(len_destination_seq + 1)] for _ in range(len_source_seq + 1) ] @@ -31,48 +47,73 @@ def compute_transform_tables( for i in range(1, len_source_seq + 1): costs[i][0] = i * delete_cost - ops[i][0] = f"D{source_seq[i - 1]:c}" + ops[i][0] = f"D{source_seq[i - 1]}" for i in range(1, len_destination_seq + 1): costs[0][i] = i * insert_cost - ops[0][i] = f"I{destination_seq[i - 1]:c}" + ops[0][i] = f"I{destination_seq[i - 1]}" for i in range(1, len_source_seq + 1): for j in range(1, len_destination_seq + 1): if source_seq[i - 1] == destination_seq[j - 1]: costs[i][j] = costs[i - 1][j - 1] + copy_cost - ops[i][j] = f"C{source_seq[i - 1]:c}" + ops[i][j] = f"C{source_seq[i - 1]}" else: costs[i][j] = costs[i - 1][j - 1] + replace_cost - ops[i][j] = f"R{source_seq[i - 1]:c}" + str(destination_seq[j - 1]) + ops[i][j] = f"R{source_seq[i - 1]}" + str(destination_seq[j - 1]) if costs[i - 1][j] + delete_cost < costs[i][j]: costs[i][j] = costs[i - 1][j] + delete_cost - ops[i][j] = f"D{source_seq[i - 1]:c}" + ops[i][j] = f"D{source_seq[i - 1]}" if costs[i][j - 1] + insert_cost < costs[i][j]: costs[i][j] = costs[i][j - 1] + insert_cost - ops[i][j] = f"I{destination_seq[j - 1]:c}" + ops[i][j] = f"I{destination_seq[j - 1]}" return costs, ops def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: + """ + Assembles the transformations based on the ops table. + + >>> ops = [['0', 'Ic', 'Iu', 'It'], + ... ['Dc', 'Cc', 'Iu', 'It'], + ... ['Da', 'Da', 'Rau', 'Rat'], + ... ['Dt', 'Dt', 'Rtu', 'Ct']] + >>> x = len(ops) - 1 + >>> y = len(ops[0]) - 1 + >>> assemble_transformation(ops, x, y) + ['Cc', 'Rau', 'Ct'] + + >>> ops1 = [['0']] + >>> x1 = len(ops1) - 1 + >>> y1 = len(ops1[0]) - 1 + >>> assemble_transformation(ops1, x1, y1) + [] + + >>> ops2 = [['0', 'I1', 'I2', 'I3'], + ... ['D1', 'C1', 'I2', 'I3'], + ... ['D2', 'D2', 'R23', 'R23']] + >>> x2 = len(ops2) - 1 + >>> y2 = len(ops2[0]) - 1 + >>> assemble_transformation(ops2, x2, y2) + ['C1', 'I2', 'R23'] + """ if i == 0 and j == 0: return [] + elif ops[i][j][0] in {"C", "R"}: + seq = assemble_transformation(ops, i - 1, j - 1) + seq.append(ops[i][j]) + return seq + elif ops[i][j][0] == "D": + seq = assemble_transformation(ops, i - 1, j) + seq.append(ops[i][j]) + return seq else: - if ops[i][j][0] == "C" or ops[i][j][0] == "R": - seq = assemble_transformation(ops, i - 1, j - 1) - seq.append(ops[i][j]) - return seq - elif ops[i][j][0] == "D": - seq = assemble_transformation(ops, i - 1, j) - seq.append(ops[i][j]) - return seq - else: - seq = assemble_transformation(ops, i, j - 1) - seq.append(ops[i][j]) - return seq + seq = assemble_transformation(ops, i, j - 1) + seq.append(ops[i][j]) + return seq if __name__ == "__main__": @@ -91,7 +132,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: print("".join(string)) if op[0] == "C": - file.write("%-16s" % "Copy %c" % op[1]) + file.write("%-16s" % "Copy %c" % op[1]) # noqa: UP031 file.write("\t\t\t" + "".join(string)) file.write("\r\n") @@ -99,7 +140,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: elif op[0] == "R": string[i] = op[2] - file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2]))) + file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2]))) # noqa: UP031 file.write("\t\t" + "".join(string)) file.write("\r\n") @@ -107,7 +148,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: elif op[0] == "D": string.pop(i) - file.write("%-16s" % "Delete %c" % op[1]) + file.write("%-16s" % "Delete %c" % op[1]) # noqa: UP031 file.write("\t\t\t" + "".join(string)) file.write("\r\n") @@ -115,7 +156,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: else: string.insert(i, op[1]) - file.write("%-16s" % "Insert %c" % op[1]) + file.write("%-16s" % "Insert %c" % op[1]) # noqa: UP031 file.write("\t\t\t" + "".join(string)) file.write("\r\n") diff --git a/strings/palindrome.py b/strings/palindrome.py index dd1fe316f479..bfdb3ddcf396 100644 --- a/strings/palindrome.py +++ b/strings/palindrome.py @@ -1,5 +1,7 @@ # Algorithms to determine if a string is palindrome +from timeit import timeit + test_data = { "MALAYALAM": True, "String": False, @@ -33,6 +35,25 @@ def is_palindrome(s: str) -> bool: return True +def is_palindrome_traversal(s: str) -> bool: + """ + Return True if s is a palindrome otherwise return False. + + >>> all(is_palindrome_traversal(key) is value for key, value in test_data.items()) + True + """ + end = len(s) // 2 + n = len(s) + + # We need to traverse till half of the length of string + # as we can get access of the i'th last element from + # i'th index. + # eg: [0,1,2,3,4,5] => 4th index can be accessed + # with the help of 1st index (i==n-i-1) + # where n is length of string + return all(s[i] == s[n - i - 1] for i in range(end)) + + def is_palindrome_recursive(s: str) -> bool: """ Return True if s is a palindrome otherwise return False. @@ -40,7 +61,7 @@ def is_palindrome_recursive(s: str) -> bool: >>> all(is_palindrome_recursive(key) is value for key, value in test_data.items()) True """ - if len(s) <= 1: + if len(s) <= 2: return True if s[0] == s[len(s) - 1]: return is_palindrome_recursive(s[1:-1]) @@ -58,9 +79,26 @@ def is_palindrome_slice(s: str) -> bool: return s == s[::-1] +def benchmark_function(name: str) -> None: + stmt = f"all({name}(key) is value for key, value in test_data.items())" + setup = f"from __main__ import test_data, {name}" + number = 500000 + result = timeit(stmt=stmt, setup=setup, number=number) + print(f"{name:<35} finished {number:,} runs in {result:.5f} seconds") + + if __name__ == "__main__": for key, value in test_data.items(): assert is_palindrome(key) is is_palindrome_recursive(key) assert is_palindrome(key) is is_palindrome_slice(key) print(f"{key:21} {value}") print("a man a plan a canal panama") + + # finished 500,000 runs in 0.46793 seconds + benchmark_function("is_palindrome_slice") + # finished 500,000 runs in 0.85234 seconds + benchmark_function("is_palindrome") + # finished 500,000 runs in 1.32028 seconds + benchmark_function("is_palindrome_recursive") + # finished 500,000 runs in 2.08679 seconds + benchmark_function("is_palindrome_traversal") diff --git a/strings/pig_latin.py b/strings/pig_latin.py new file mode 100644 index 000000000000..457dbb5a6cf6 --- /dev/null +++ b/strings/pig_latin.py @@ -0,0 +1,44 @@ +def pig_latin(word: str) -> str: + """Compute the piglatin of a given string. + + https://en.wikipedia.org/wiki/Pig_Latin + + Usage examples: + >>> pig_latin("pig") + 'igpay' + >>> pig_latin("latin") + 'atinlay' + >>> pig_latin("banana") + 'ananabay' + >>> pig_latin("friends") + 'iendsfray' + >>> pig_latin("smile") + 'ilesmay' + >>> pig_latin("string") + 'ingstray' + >>> pig_latin("eat") + 'eatway' + >>> pig_latin("omelet") + 'omeletway' + >>> pig_latin("are") + 'areway' + >>> pig_latin(" ") + '' + >>> pig_latin(None) + '' + """ + if not (word or "").strip(): + return "" + word = word.lower() + if word[0] in "aeiou": + return f"{word}way" + for i, char in enumerate(word): # noqa: B007 + if char in "aeiou": + break + return f"{word[i:]}{word[:i]}ay" + + +if __name__ == "__main__": + print(f"{pig_latin('friends') = }") + word = input("Enter a word: ") + print(f"{pig_latin(word) = }") diff --git a/strings/prefix_function.py b/strings/prefix_function.py index 65bbe9100735..04987deef469 100644 --- a/strings/prefix_function.py +++ b/strings/prefix_function.py @@ -1,7 +1,7 @@ """ https://cp-algorithms.com/string/prefix-function.html -Prefix function Knuth–Morris–Pratt algorithm +Prefix function Knuth-Morris-Pratt algorithm Different algorithm than Knuth-Morris-Pratt pattern finding diff --git a/strings/rabin_karp.py b/strings/rabin_karp.py index 81ca611a76b3..9c0d0fe5c739 100644 --- a/strings/rabin_karp.py +++ b/strings/rabin_karp.py @@ -38,7 +38,7 @@ def rabin_karp(pattern: str, text: str) -> bool: continue modulus_power = (modulus_power * alphabet_size) % modulus - for i in range(0, t_len - p_len + 1): + for i in range(t_len - p_len + 1): if text_hash == p_hash and text[i : i + p_len] == pattern: return True if i == t_len - p_len: @@ -60,7 +60,8 @@ def test_rabin_karp() -> None: pattern = "abc1abc12" text1 = "alskfjaldsabc1abc1abc12k23adsfabcabc" text2 = "alskfjaldsk23adsfabcabc" - assert rabin_karp(pattern, text1) and not rabin_karp(pattern, text2) + assert rabin_karp(pattern, text1) + assert not rabin_karp(pattern, text2) # Test 2) pattern = "ABABX" diff --git a/strings/reverse_letters.py b/strings/reverse_letters.py index 10b8a6d72a0f..4f73f816b382 100644 --- a/strings/reverse_letters.py +++ b/strings/reverse_letters.py @@ -1,19 +1,24 @@ -def reverse_letters(input_str: str) -> str: +def reverse_letters(sentence: str, length: int = 0) -> str: """ - Reverses letters in a given string without adjusting the position of the words - >>> reverse_letters('The cat in the hat') - 'ehT tac ni eht tah' - >>> reverse_letters('The quick brown fox jumped over the lazy dog.') - 'ehT kciuq nworb xof depmuj revo eht yzal .god' - >>> reverse_letters('Is this true?') - 'sI siht ?eurt' - >>> reverse_letters("I love Python") - 'I evol nohtyP' + Reverse all words that are longer than the given length of characters in a sentence. + If unspecified, length is taken as 0 + + >>> reverse_letters("Hey wollef sroirraw", 3) + 'Hey fellow warriors' + >>> reverse_letters("nohtyP is nohtyP", 2) + 'Python is Python' + >>> reverse_letters("1 12 123 1234 54321 654321", 0) + '1 21 321 4321 12345 123456' + >>> reverse_letters("racecar") + 'racecar' """ - return " ".join([word[::-1] for word in input_str.split()]) + return " ".join( + "".join(word[::-1]) if len(word) > length else word for word in sentence.split() + ) if __name__ == "__main__": import doctest doctest.testmod() + print(reverse_letters("Hey wollef sroirraw")) diff --git a/strings/reverse_long_words.py b/strings/reverse_long_words.py deleted file mode 100644 index 39ef11513f40..000000000000 --- a/strings/reverse_long_words.py +++ /dev/null @@ -1,21 +0,0 @@ -def reverse_long_words(sentence: str) -> str: - """ - Reverse all words that are longer than 4 characters in a sentence. - - >>> reverse_long_words("Hey wollef sroirraw") - 'Hey fellow warriors' - >>> reverse_long_words("nohtyP is nohtyP") - 'Python is Python' - >>> reverse_long_words("1 12 123 1234 54321 654321") - '1 12 123 1234 12345 123456' - """ - return " ".join( - "".join(word[::-1]) if len(word) > 4 else word for word in sentence.split() - ) - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - print(reverse_long_words("Hey wollef sroirraw")) diff --git a/strings/snake_case_to_camel_pascal_case.py b/strings/snake_case_to_camel_pascal_case.py index eaabdcb87a0f..8219337a63b0 100644 --- a/strings/snake_case_to_camel_pascal_case.py +++ b/strings/snake_case_to_camel_pascal_case.py @@ -27,11 +27,11 @@ def snake_to_camel_case(input_str: str, use_pascal: bool = False) -> str: """ if not isinstance(input_str, str): - raise ValueError(f"Expected string as input, found {type(input_str)}") + msg = f"Expected string as input, found {type(input_str)}" + raise ValueError(msg) if not isinstance(use_pascal, bool): - raise ValueError( - f"Expected boolean as use_pascal parameter, found {type(use_pascal)}" - ) + msg = f"Expected boolean as use_pascal parameter, found {type(use_pascal)}" + raise ValueError(msg) words = input_str.split("_") @@ -43,7 +43,7 @@ def snake_to_camel_case(input_str: str, use_pascal: bool = False) -> str: initial_word = "" if use_pascal else words[0] - return "".join([initial_word] + capitalized_words) + return "".join([initial_word, *capitalized_words]) if __name__ == "__main__": diff --git a/strings/split.py b/strings/split.py index b62b86d2401f..ed194ec69c2f 100644 --- a/strings/split.py +++ b/strings/split.py @@ -14,6 +14,9 @@ def split(string: str, separator: str = " ") -> list: >>> split("12:43:39",separator = ":") ['12', '43', '39'] + + >>> split(";abbb;;c;", separator=';') + ['', 'abbb', '', 'c', ''] """ split_words = [] @@ -23,7 +26,7 @@ def split(string: str, separator: str = " ") -> list: if char == separator: split_words.append(string[last_index:index]) last_index = index + 1 - elif index + 1 == len(string): + if index + 1 == len(string): split_words.append(string[last_index : index + 1]) return split_words diff --git a/strings/string_switch_case.py b/strings/string_switch_case.py new file mode 100644 index 000000000000..c16d9fa552f9 --- /dev/null +++ b/strings/string_switch_case.py @@ -0,0 +1,122 @@ +import re + +""" +general info: +https://en.wikipedia.org/wiki/Naming_convention_(programming)#Python_and_Ruby + +pascal case [ an upper Camel Case ]: https://en.wikipedia.org/wiki/Camel_case + +camel case: https://en.wikipedia.org/wiki/Camel_case + +kebab case [ can be found in general info ]: +https://en.wikipedia.org/wiki/Naming_convention_(programming)#Python_and_Ruby + +snake case: https://en.wikipedia.org/wiki/Snake_case +""" + + +# assistant functions +def split_input(str_: str) -> list: + """ + >>> split_input("one two 31235three4four") + [['one', 'two', '31235three4four']] + """ + return [char.split() for char in re.split(r"[^ a-z A-Z 0-9 \s]", str_)] + + +def to_simple_case(str_: str) -> str: + """ + >>> to_simple_case("one two 31235three4four") + 'OneTwo31235three4four' + >>> to_simple_case("This should be combined") + 'ThisShouldBeCombined' + >>> to_simple_case("The first letters are capitalized, then string is merged") + 'TheFirstLettersAreCapitalizedThenStringIsMerged' + >>> to_simple_case("special characters :, ', %, ^, $, are ignored") + 'SpecialCharactersAreIgnored' + """ + string_split = split_input(str_) + return "".join( + ["".join([char.capitalize() for char in sub_str]) for sub_str in string_split] + ) + + +def to_complex_case(text: str, upper: bool, separator: str) -> str: + """ + Returns the string concatenated with the delimiter we provide. + + Parameters: + @text: The string on which we want to perform operation + @upper: Boolean value to determine whether we want capitalized result or not + @separator: The delimiter with which we want to concatenate words + + Examples: + >>> to_complex_case("one two 31235three4four", True, "_") + 'ONE_TWO_31235THREE4FOUR' + >>> to_complex_case("one two 31235three4four", False, "-") + 'one-two-31235three4four' + """ + try: + string_split = split_input(text) + if upper: + res_str = "".join( + [ + separator.join([char.upper() for char in sub_str]) + for sub_str in string_split + ] + ) + else: + res_str = "".join( + [ + separator.join([char.lower() for char in sub_str]) + for sub_str in string_split + ] + ) + return res_str + except IndexError: + return "not valid string" + + +# main content +def to_pascal_case(text: str) -> str: + """ + >>> to_pascal_case("one two 31235three4four") + 'OneTwo31235three4four' + """ + return to_simple_case(text) + + +def to_camel_case(text: str) -> str: + """ + >>> to_camel_case("one two 31235three4four") + 'oneTwo31235three4four' + """ + try: + res_str = to_simple_case(text) + return res_str[0].lower() + res_str[1:] + except IndexError: + return "not valid string" + + +def to_snake_case(text: str, upper: bool) -> str: + """ + >>> to_snake_case("one two 31235three4four", True) + 'ONE_TWO_31235THREE4FOUR' + >>> to_snake_case("one two 31235three4four", False) + 'one_two_31235three4four' + """ + return to_complex_case(text, upper, "_") + + +def to_kebab_case(text: str, upper: bool) -> str: + """ + >>> to_kebab_case("one two 31235three4four", True) + 'ONE-TWO-31235THREE4FOUR' + >>> to_kebab_case("one two 31235three4four", False) + 'one-two-31235three4four' + """ + return to_complex_case(text, upper, "-") + + +if __name__ == "__main__": + __import__("doctest").testmod() diff --git a/strings/strip.py b/strings/strip.py new file mode 100644 index 000000000000..d4f901f0c7ea --- /dev/null +++ b/strings/strip.py @@ -0,0 +1,33 @@ +def strip(user_string: str, characters: str = " \t\n\r") -> str: + """ + Remove leading and trailing characters (whitespace by default) from a string. + + Args: + user_string (str): The input string to be stripped. + characters (str, optional): Optional characters to be removed + (default is whitespace). + + Returns: + str: The stripped string. + + Examples: + >>> strip(" hello ") + 'hello' + >>> strip("...world...", ".") + 'world' + >>> strip("123hello123", "123") + 'hello' + >>> strip("") + '' + """ + + start = 0 + end = len(user_string) + + while start < end and user_string[start] in characters: + start += 1 + + while end > start and user_string[end - 1] in characters: + end -= 1 + + return user_string[start:end] diff --git a/strings/text_justification.py b/strings/text_justification.py index b0ef12231224..e025edcfe13f 100644 --- a/strings/text_justification.py +++ b/strings/text_justification.py @@ -67,19 +67,19 @@ def justify(line: list, width: int, max_width: int) -> str: answer = [] line: list[str] = [] width = 0 - for word in words: - if width + len(word) + len(line) <= max_width: + for inner_word in words: + if width + len(inner_word) + len(line) <= max_width: # keep adding words until we can fill out max_width # width = sum of length of all words (without overall_spaces_count) - # len(word) = length of current word + # len(inner_word) = length of current inner_word # len(line) = number of overall_spaces_count to insert between words - line.append(word) - width += len(word) + line.append(inner_word) + width += len(inner_word) else: # justify the line and add it to result answer.append(justify(line, width, max_width)) # reset new line and new width - line, width = [word], len(word) + line, width = [inner_word], len(inner_word) remaining_spaces = max_width - width - len(line) answer.append(" ".join(line) + (remaining_spaces + 1) * " ") return answer diff --git a/strings/title.py b/strings/title.py new file mode 100644 index 000000000000..1ec2df548e2d --- /dev/null +++ b/strings/title.py @@ -0,0 +1,57 @@ +def to_title_case(word: str) -> str: + """ + Converts a string to capitalized case, preserving the input as is + + >>> to_title_case("Aakash") + 'Aakash' + + >>> to_title_case("aakash") + 'Aakash' + + >>> to_title_case("AAKASH") + 'Aakash' + + >>> to_title_case("aAkAsH") + 'Aakash' + """ + + """ + Convert the first character to uppercase if it's lowercase + """ + if "a" <= word[0] <= "z": + word = chr(ord(word[0]) - 32) + word[1:] + + """ + Convert the remaining characters to lowercase if they are uppercase + """ + for i in range(1, len(word)): + if "A" <= word[i] <= "Z": + word = word[:i] + chr(ord(word[i]) + 32) + word[i + 1 :] + + return word + + +def sentence_to_title_case(input_str: str) -> str: + """ + Converts a string to title case, preserving the input as is + + >>> sentence_to_title_case("Aakash Giri") + 'Aakash Giri' + + >>> sentence_to_title_case("aakash giri") + 'Aakash Giri' + + >>> sentence_to_title_case("AAKASH GIRI") + 'Aakash Giri' + + >>> sentence_to_title_case("aAkAsH gIrI") + 'Aakash Giri' + """ + + return " ".join(to_title_case(word) for word in input_str.split()) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/top_k_frequent_words.py b/strings/top_k_frequent_words.py new file mode 100644 index 000000000000..40fa7fc85cd1 --- /dev/null +++ b/strings/top_k_frequent_words.py @@ -0,0 +1,100 @@ +""" +Finds the top K most frequent words from the provided word list. + +This implementation aims to show how to solve the problem using the Heap class +already present in this repository. +Computing order statistics is, in fact, a typical usage of heaps. + +This is mostly shown for educational purposes, since the problem can be solved +in a few lines using collections.Counter from the Python standard library: + +from collections import Counter +def top_k_frequent_words(words, k_value): + return [x[0] for x in Counter(words).most_common(k_value)] +""" + +from collections import Counter +from functools import total_ordering + +from data_structures.heap.heap import Heap + + +@total_ordering +class WordCount: + def __init__(self, word: str, count: int) -> None: + self.word = word + self.count = count + + def __eq__(self, other: object) -> bool: + """ + >>> WordCount('a', 1).__eq__(WordCount('b', 1)) + True + >>> WordCount('a', 1).__eq__(WordCount('a', 1)) + True + >>> WordCount('a', 1).__eq__(WordCount('a', 2)) + False + >>> WordCount('a', 1).__eq__(WordCount('b', 2)) + False + >>> WordCount('a', 1).__eq__(1) + NotImplemented + """ + if not isinstance(other, WordCount): + return NotImplemented + return self.count == other.count + + def __lt__(self, other: object) -> bool: + """ + >>> WordCount('a', 1).__lt__(WordCount('b', 1)) + False + >>> WordCount('a', 1).__lt__(WordCount('a', 1)) + False + >>> WordCount('a', 1).__lt__(WordCount('a', 2)) + True + >>> WordCount('a', 1).__lt__(WordCount('b', 2)) + True + >>> WordCount('a', 2).__lt__(WordCount('a', 1)) + False + >>> WordCount('a', 2).__lt__(WordCount('b', 1)) + False + >>> WordCount('a', 1).__lt__(1) + NotImplemented + """ + if not isinstance(other, WordCount): + return NotImplemented + return self.count < other.count + + +def top_k_frequent_words(words: list[str], k_value: int) -> list[str]: + """ + Returns the `k_value` most frequently occurring words, + in non-increasing order of occurrence. + In this context, a word is defined as an element in the provided list. + + In case `k_value` is greater than the number of distinct words, a value of k equal + to the number of distinct words will be considered, instead. + + >>> top_k_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 3) + ['c', 'a', 'b'] + >>> top_k_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 2) + ['c', 'a'] + >>> top_k_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 1) + ['c'] + >>> top_k_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 0) + [] + >>> top_k_frequent_words([], 1) + [] + >>> top_k_frequent_words(['a', 'a'], 2) + ['a'] + """ + heap: Heap[WordCount] = Heap() + count_by_word = Counter(words) + heap.build_max_heap( + [WordCount(word, count) for word, count in count_by_word.items()] + ) + return [heap.extract_max().word for _ in range(min(k_value, len(count_by_word)))] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/strings/upper.py b/strings/upper.py index 5edd40b79808..0f68a27b99c6 100644 --- a/strings/upper.py +++ b/strings/upper.py @@ -1,6 +1,8 @@ def upper(word: str) -> str: """ - Will convert the entire string to uppercase letters + Convert an entire string to ASCII uppercase letters by looking for lowercase ASCII + letters and subtracting 32 from their integer representation to get the uppercase + letter. >>> upper("wow") 'WOW' @@ -11,10 +13,6 @@ def upper(word: str) -> str: >>> upper("wh[]32") 'WH[]32' """ - - # Converting to ascii value int value and checking to see if char is a lower letter - # if it is a lowercase letter it is getting shift by 32 which makes it an uppercase - # case letter return "".join(chr(ord(char) - 32) if "a" <= char <= "z" else char for char in word) diff --git a/strings/wave.py b/strings/wave_string.py similarity index 100% rename from strings/wave.py rename to strings/wave_string.py diff --git a/strings/word_occurrence.py b/strings/word_occurrence.py index 8260620c38a4..5a18ebf771e4 100644 --- a/strings/word_occurrence.py +++ b/strings/word_occurrence.py @@ -1,7 +1,6 @@ # Created by sarathkaul on 17/11/19 # Modified by Arkadip Bhattacharya(@darkmatter18) on 20/04/2020 from collections import defaultdict -from typing import DefaultDict def word_occurrence(sentence: str) -> dict: @@ -15,7 +14,7 @@ def word_occurrence(sentence: str) -> dict: >>> dict(word_occurrence("Two spaces")) {'Two': 1, 'spaces': 1} """ - occurrence: DefaultDict[str, int] = defaultdict(int) + occurrence: defaultdict[str, int] = defaultdict(int) # Creating a dictionary containing count of each word for word in sentence.split(): occurrence[word] += 1 diff --git a/strings/word_patterns.py b/strings/word_patterns.py index d12d267e7b35..ed603e9fefeb 100644 --- a/strings/word_patterns.py +++ b/strings/word_patterns.py @@ -1,11 +1,32 @@ def get_word_pattern(word: str) -> str: """ + Returns numerical pattern of character appearances in given word + >>> get_word_pattern("") + '' + >>> get_word_pattern(" ") + '0' >>> get_word_pattern("pattern") '0.1.2.2.3.4.5' >>> get_word_pattern("word pattern") '0.1.2.3.4.5.6.7.7.8.2.9' >>> get_word_pattern("get word pattern") '0.1.2.3.4.5.6.7.3.8.9.2.2.1.6.10' + >>> get_word_pattern() + Traceback (most recent call last): + ... + TypeError: get_word_pattern() missing 1 required positional argument: 'word' + >>> get_word_pattern(1) + Traceback (most recent call last): + ... + AttributeError: 'int' object has no attribute 'upper' + >>> get_word_pattern(1.1) + Traceback (most recent call last): + ... + AttributeError: 'float' object has no attribute 'upper' + >>> get_word_pattern([]) + Traceback (most recent call last): + ... + AttributeError: 'list' object has no attribute 'upper' """ word = word.upper() next_num = 0 diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000000..077288f041a1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1246 @@ +version = 1 +requires-python = ">=3.13" +resolution-markers = [ + "platform_system == 'Darwin'", + "platform_machine == 'aarch64' and platform_system == 'Linux'", + "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux')", +] + +[[package]] +name = "absl-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/8f/fc001b92ecc467cc32ab38398bd0bfb45df46e7523bf33c2ad22a505f06e/absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff", size = 118055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308", size = 133706 }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, +] + +[[package]] +name = "astroid" +version = "3.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/1e/326fb1d3d83a3bb77c9f9be29d31f2901e35acb94b0605c3f2e5085047f9/astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", size = 397229 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/30/624365383fa4a40329c0f0bbbc151abc4a64e30dfc110fc8f6e2afcd02bb/astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8", size = 274586 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "codespell" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/a9/98353dfc7afcdf18cffd2dd3e959a25eaaf2728cf450caa59af89648a8e4/codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f", size = 329791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/20/b6019add11e84f821184234cea0ad91442373489ef7ccfa3d73a71b908fa/codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1", size = 329167 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "contourpy" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 }, + { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 }, + { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 }, + { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 }, + { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 }, + { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 }, + { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 }, + { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 }, + { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 }, + { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 }, + { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 }, + { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 }, + { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 }, + { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 }, + { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 }, + { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 }, + { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 }, + { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 }, + { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 }, +] + +[[package]] +name = "coverage" +version = "7.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, + { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, + { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, + { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, + { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, + { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, + { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, + { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, + { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, + { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, + { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, + { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, + { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, + { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, + { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, + { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, + { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, + { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, + { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, + { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "dom-toml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "domdf-python-tools" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/34/f7690cf288eaa86b55c8f1b890d0834e6df44a026a88eca12274fcd624ab/dom_toml-2.0.0.tar.gz", hash = "sha256:3c07e8436538994974127b1ae037661d1a779ac915c44fd06b3ab5fe140ff589", size = 11133 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/99/b6fc87dff3138491d81676bdcbf1531080925ba41486ec1dafd86e33fdbc/dom_toml-2.0.0-py3-none-any.whl", hash = "sha256:0b6d02a72bcbc6be8175c61afc30623bbb6b74c4650f2a806fbc3fb7fe86935d", size = 13376 }, +] + +[[package]] +name = "domdf-python-tools" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "natsort" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/78/974e10c583ba9d2302e748c9585313a7f2c7ba00e4f600324f432e38fe68/domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18", size = 103792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e9/7447a88b217650a74927d3444a89507986479a69b83741900eddd34167fe/domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e", size = 127106 }, +] + +[[package]] +name = "fake-useragent" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/1f662631ab153975fa8dbf09296324ecbaf53370dce922054e8de6b57370/fake-useragent-1.5.1.tar.gz", hash = "sha256:6387269f5a2196b5ba7ed8935852f75486845a1c95c50e72460e6a8e762f5c49", size = 22631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/99/60d8cf1b26938c2e0a57e232f7f15641dfcd6f8deda454d73e4145910ff6/fake_useragent-1.5.1-py3-none-any.whl", hash = "sha256:57415096557c8a4e23b62a375c21c55af5fd4ba30549227f562d2c4f5b60e3b3", size = 17190 }, +] + +[[package]] +name = "fonttools" +version = "4.55.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4e/053fe1b5c0ce346c0a9d0557492c654362bafb14f026eae0d3ee98009152/fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71", size = 3490431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/87/a669ac26c6077e37ffb06abf29c5571789eefe518d06c52df392181ee694/fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9", size = 2752519 }, + { url = "https://files.pythonhosted.org/packages/0c/e9/4822ad238fe215133c7df20f1cdb1a58cfb634a31523e77ff0fb2033970a/fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c", size = 2286819 }, + { url = "https://files.pythonhosted.org/packages/3e/a4/d7941c3897129e60fe68d20e4819fda4d0c4858d77badae0e80ca6440b36/fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c", size = 4770382 }, + { url = "https://files.pythonhosted.org/packages/31/cf/c51ea1348f9fba9c627439afad9dee0090040809ab431f4422b5bfdda34c/fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd", size = 4858336 }, + { url = "https://files.pythonhosted.org/packages/73/be/36c1fe0e5c9a96b068ddd7e82001243bbe7fe12549c8d14e1bd025bf40c9/fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4", size = 4756072 }, + { url = "https://files.pythonhosted.org/packages/5c/18/6dd381c29f215a017f79aa9fea0722424a0046b47991c4390a78ff87ce0c/fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18", size = 5008668 }, + { url = "https://files.pythonhosted.org/packages/b8/95/316f20092b389b927dba1d1dccd3f541853f96e707e210f1b9f4e7bacdd5/fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b", size = 2155841 }, + { url = "https://files.pythonhosted.org/packages/35/ca/b4638aa3e446184892e2f9cc8ef44bb506f47fea04580df7fb84f5a4363d/fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998", size = 2200587 }, + { url = "https://files.pythonhosted.org/packages/b4/4a/786589606d4989cb34d8bc766cd687d955aaf3039c367fe7104bcf82dc98/fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f", size = 1100249 }, +] + +[[package]] +name = "h5py" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/0c/5c2b0a88158682aeafb10c1c2b735df5bc31f165bfe192f2ee9f2a23b5f1/h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf", size = 411457 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/1c/ecdd0efab52c24f2a9bf2324289828b860e8dd1e3c5ada3cf0889e14fdc1/h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc", size = 3346239 }, + { url = "https://files.pythonhosted.org/packages/93/cd/5b6f574bf3e318bbe305bc93ba45181676550eb44ba35e006d2e98004eaa/h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653", size = 2843416 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/b74332f313bfbe94ba03fff784219b9db385e6139708e55b11490149f90a/h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32", size = 5154390 }, + { url = "https://files.pythonhosted.org/packages/1a/57/93ea9e10a6457ea8d3b867207deb29a527e966a08a84c57ffd954e32152a/h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f", size = 5378244 }, + { url = "https://files.pythonhosted.org/packages/50/51/0bbf3663062b2eeee78aa51da71e065f8a0a6e3cb950cc7020b4444999e6/h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8", size = 2979760 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imageio" +version = "2.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/aa/2e7a49259339e691ff2b477ae0696b1784a09313c5872700bbbdd00a3030/imageio-2.36.1.tar.gz", hash = "sha256:e4e1d231f47f9a9e16100b0f7ce1a86e8856fb4d1c0fa2c4365a316f1746be62", size = 389522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/f9/f78e7f5ac8077c481bf6b43b8bc736605363034b3d5eb3ce8eb79f53f5f1/imageio-2.36.1-py3-none-any.whl", hash = "sha256:20abd2cae58e55ca1af8a8dcf43293336a59adf0391f1917bf8518633cfc2cdf", size = 315435 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "joblib" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, +] + +[[package]] +name = "keras" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "h5py" }, + { name = "ml-dtypes" }, + { name = "namex" }, + { name = "numpy" }, + { name = "optree" }, + { name = "packaging" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/c3/56fc6800c5eab94bd0f5e930751bd4c0fa1ee0aee272fad4a72723ffae87/keras-3.7.0.tar.gz", hash = "sha256:a4451a5591e75dfb414d0b84a3fd2fb9c0240cc87ebe7e397f547ce10b0e67b7", size = 924719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/bf/9e3f10e55df30b0fb4bf6c2ee7d50bda2e070599b86f62ea3f9954af172b/keras-3.7.0-py3-none-any.whl", hash = "sha256:546a64f302e4779c129c06d9826fa586de752cdfd43d7dc4010c31b282587969", size = 1228365 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, +] + +[[package]] +name = "lxml" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/6a/42141e4d373903bfea6f8e94b2f554d05506dfda522ada5343c651410dc8/lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", size = 8156284 }, + { url = "https://files.pythonhosted.org/packages/91/5e/fa097f0f7d8b3d113fb7312c6308af702f2667f22644441715be961f2c7e/lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", size = 4432407 }, + { url = "https://files.pythonhosted.org/packages/2d/a1/b901988aa6d4ff937f2e5cfc114e4ec561901ff00660c3e56713642728da/lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", size = 5048331 }, + { url = "https://files.pythonhosted.org/packages/30/0f/b2a54f48e52de578b71bbe2a2f8160672a8a5e103df3a78da53907e8c7ed/lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", size = 4744835 }, + { url = "https://files.pythonhosted.org/packages/82/9d/b000c15538b60934589e83826ecbc437a1586488d7c13f8ee5ff1f79a9b8/lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", size = 5316649 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/ffbb9eaff5e541922611d2c56b175c45893d1c0b8b11e5a497708a6a3b3b/lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", size = 4812046 }, + { url = "https://files.pythonhosted.org/packages/15/ff/7ff89d567485c7b943cdac316087f16b2399a8b997007ed352a1248397e5/lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", size = 4918597 }, + { url = "https://files.pythonhosted.org/packages/c6/a3/535b6ed8c048412ff51268bdf4bf1cf052a37aa7e31d2e6518038a883b29/lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", size = 4738071 }, + { url = "https://files.pythonhosted.org/packages/7a/8f/cbbfa59cb4d4fd677fe183725a76d8c956495d7a3c7f111ab8f5e13d2e83/lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", size = 5342213 }, + { url = "https://files.pythonhosted.org/packages/5c/fb/db4c10dd9958d4b52e34d1d1f7c1f434422aeaf6ae2bbaaff2264351d944/lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", size = 4893749 }, + { url = "https://files.pythonhosted.org/packages/f2/38/bb4581c143957c47740de18a3281a0cab7722390a77cc6e610e8ebf2d736/lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", size = 4945901 }, + { url = "https://files.pythonhosted.org/packages/fc/d5/18b7de4960c731e98037bd48fa9f8e6e8f2558e6fbca4303d9b14d21ef3b/lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", size = 4815447 }, + { url = "https://files.pythonhosted.org/packages/97/a8/cd51ceaad6eb849246559a8ef60ae55065a3df550fc5fcd27014361c1bab/lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", size = 5411186 }, + { url = "https://files.pythonhosted.org/packages/89/c3/1e3dabab519481ed7b1fdcba21dcfb8832f57000733ef0e71cf6d09a5e03/lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", size = 5324481 }, + { url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053 }, + { url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634 }, + { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "matplotlib" +version = "3.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/9f/562ed484b11ac9f4bb4f9d2d7546954ec106a8c0f06cc755d6f63e519274/matplotlib-3.9.3.tar.gz", hash = "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5", size = 36113438 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/04/949640040982822416c471d9ebe4e9e6c69ca9f9bb6ba82ed30808863c02/matplotlib-3.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83", size = 7883417 }, + { url = "https://files.pythonhosted.org/packages/9f/90/ebd37143cd3150b6c650ee1580024df3dd649d176e68d346f826b8d24e37/matplotlib-3.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad", size = 7768720 }, + { url = "https://files.pythonhosted.org/packages/dc/84/6591e6b55d755d16dacdc113205067031867c1f5e3c08b32c01aad831420/matplotlib-3.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a", size = 8192723 }, + { url = "https://files.pythonhosted.org/packages/29/09/146a17d37e32313507f11ac984e65311f2d5805d731eb981d4f70eb928dc/matplotlib-3.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0", size = 8305801 }, + { url = "https://files.pythonhosted.org/packages/85/cb/d2690572c08f19ca7c0f44b1fb4d11c121d63467a57b508cc3656ff80b43/matplotlib-3.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9", size = 9086564 }, + { url = "https://files.pythonhosted.org/packages/28/dd/0a5176027c1cb94fe75f69f76cb274180c8abf740df6fc0e6a1e4cbaec3f/matplotlib-3.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70", size = 7833257 }, + { url = "https://files.pythonhosted.org/packages/42/d4/e477d50a8e4b437c2afbb5c665cb8e5d79b06abe6fe3c6915d6f7f0c2ef2/matplotlib-3.9.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639", size = 7911906 }, + { url = "https://files.pythonhosted.org/packages/ae/a1/ba5ab89666c42ace8e31b4ff5a2c76a17e4d6f91aefce476b064c56ff61d/matplotlib-3.9.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6", size = 7801336 }, + { url = "https://files.pythonhosted.org/packages/77/59/4dcdb3a6695af6c698a95aec13016a550ef2f85144d22f61f81d1e064148/matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0", size = 8218178 }, + { url = "https://files.pythonhosted.org/packages/4f/27/7c72db0d0ee35d9237572565ffa3c0eb25fc46a3f47e0f16412a587bc9d8/matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006", size = 8327768 }, + { url = "https://files.pythonhosted.org/packages/de/ad/213eee624feadba7b77e881c9d2c04c1e036efe69d19031e3fa927fdb5dc/matplotlib-3.9.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4", size = 9094075 }, + { url = "https://files.pythonhosted.org/packages/19/1b/cb8e99a5fe2e2b14e3b8234cb1649a675be63f74a5224a648ae4ab61f60c/matplotlib-3.9.3-cp313-cp313t-win_amd64.whl", hash = "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a", size = 7888937 }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/79/717c5e22ad25d63ce3acdfe8ff8d64bdedec18914256c59b838218708b16/ml_dtypes-0.5.0.tar.gz", hash = "sha256:3e7d3a380fe73a63c884f06136f8baa7a5249cc8e9fdec677997dd78549f8128", size = 699367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/4a/18f670a2703e771a6775fbc354208e597ff062a88efb0cecc220a282210b/ml_dtypes-0.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d3b3db9990c3840986a0e70524e122cfa32b91139c3653df76121ba7776e015f", size = 753345 }, + { url = "https://files.pythonhosted.org/packages/ed/c6/358d85e274e22d53def0c85f3cbe0933475fa3cf6922e9dca66eb25cb22f/ml_dtypes-0.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e04fde367b2fe901b1d47234426fe8819909bd1dd862a5adb630f27789c20599", size = 4424962 }, + { url = "https://files.pythonhosted.org/packages/4c/b4/d766586e24e7a073333c8eb8bd9275f3c6fe0569b509ae7b1699d4f00c74/ml_dtypes-0.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54415257f00eb44fbcc807454efac3356f75644f1cbfc2d4e5522a72ae1dacab", size = 4475201 }, + { url = "https://files.pythonhosted.org/packages/14/87/30323ad2e52f56262019a4493fe5f5e71067c5561ce7e2f9c75de520f5e8/ml_dtypes-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:cb5cc7b25acabd384f75bbd78892d0c724943f3e2e1986254665a1aa10982e07", size = 213195 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "myst-parser" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, +] + +[[package]] +name = "namex" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/48/d275cdb6216c6bb4f9351675795a0b48974e138f16b1ffe0252c1f8faa28/namex-0.0.8.tar.gz", hash = "sha256:32a50f6c565c0bb10aa76298c959507abdc0e850efe085dc38f3440fcb3aa90b", size = 6623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/59/7854fbfb59f8ae35483ce93493708be5942ebb6328cd85b3a609df629736/namex-0.0.8-py3-none-any.whl", hash = "sha256:7ddb6c2bb0e753a311b7590f84f6da659dd0c05e65cb89d519d54c0a250c0487", size = 5806 }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268 }, +] + +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.23.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/3a/0112397396dec37ffc8edd7836d48261b4d14ca60ec8ed7bc857cce1d916/nvidia_nccl_cu12-2.23.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa946c8327e22ced28e7cef508a334673abc42064ec85f02d005ba1785ea4cec", size = 198953892 }, + { url = "https://files.pythonhosted.org/packages/ed/1f/6482380ec8dcec4894e7503490fc536d846b0d59694acad9cf99f27d0e7d/nvidia_nccl_cu12-2.23.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b097258d9aab2fa9f686e33c6fe40ae57b27df60cedbd15d139701bb5509e0c1", size = 198954603 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "opencv-python" +version = "4.10.0.84" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/b70a2d9ab205110d715906fc8ec83fbb00404aeb3a37a0654fdb68eb0c8c/opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526", size = 95103981 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/82/564168a349148298aca281e342551404ef5521f33fba17b388ead0a84dc5/opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251", size = 54835524 }, + { url = "https://files.pythonhosted.org/packages/64/4a/016cda9ad7cf18c58ba074628a4eaae8aa55f3fd06a266398cef8831a5b9/opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98", size = 56475426 }, + { url = "https://files.pythonhosted.org/packages/81/e4/7a987ebecfe5ceaf32db413b67ff18eb3092c598408862fff4d7cc3fd19b/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6", size = 41746971 }, + { url = "https://files.pythonhosted.org/packages/3f/a4/d2537f47fd7fcfba966bd806e3ec18e7ee1681056d4b0a9c8d983983e4d5/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f", size = 62548253 }, + { url = "https://files.pythonhosted.org/packages/1e/39/bbf57e7b9dab623e8773f6ff36385456b7ae7fa9357a5e53db732c347eac/opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236", size = 28737688 }, + { url = "https://files.pythonhosted.org/packages/ec/6c/fab8113424af5049f85717e8e527ca3773299a3c6b02506e66436e19874f/opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe", size = 38842521 }, +] + +[[package]] +name = "optree" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/f2/56afdaeaae36b076659be7db8e72be0924dd64ebd1c131675c77f7e704a6/optree-0.13.1.tar.gz", hash = "sha256:af67856aa8073d237fe67313d84f8aeafac32c1cef7239c628a2768d02679c43", size = 155738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/53/f3727cad24f16a06666f328f1212476988cadac9b9e7919ddfb2c22eb662/optree-0.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f788b2ad120deb73b4908a74473cd6de79cfb9f33bbe9dcb59cea2e2477d4e28", size = 608270 }, + { url = "https://files.pythonhosted.org/packages/64/f2/68beb9da2dd52baa50e7a589ed2bd8434fdd70cdba06754aa5910263da06/optree-0.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2909cb42add6bb1a5a2b0243bdd8c4b861bf072f3741e26239481907ac8ad4e6", size = 325703 }, + { url = "https://files.pythonhosted.org/packages/45/db/08921e56f3425bf649eb593eb28775263c935d029985d35572dc5690cc1a/optree-0.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc5fa2ff5090389f3a906567446f01d692bd6fe5cfcc5ae2d5861f24e8e0e4d", size = 355813 }, + { url = "https://files.pythonhosted.org/packages/e5/e3/587e0d28dc2cee064902adfebca97db124e12b275dbe9c2b05a70a22345f/optree-0.13.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4711f5cac5a2a49c3d6c9f0eca7b77c22b452170bb33ea01c3214ebb17931db9", size = 402566 }, + { url = "https://files.pythonhosted.org/packages/8a/1d/0d5bbab8c99580b732b89ef2c5fcdd6ef410478295949fdf2984fa1bfc28/optree-0.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c4ab1d391b89cb88eb3c63383d5eb0930bc21141de9d5acd277feed9e38eb65", size = 397005 }, + { url = "https://files.pythonhosted.org/packages/16/fa/fc2a8183e14f0d195d25824bf65095ff32b34bd469614a6c30d0a596a30f/optree-0.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5e5f09c85ae558a6bdaea57e63168082e728e777391393e9e2792f0d15b7b59", size = 369400 }, + { url = "https://files.pythonhosted.org/packages/9f/42/8c08ce4ebb3d9a6e4415f1a97830c84879e2d1a43710a7c8a18b2c3e169d/optree-0.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8ee1e988c634a451146b87d9ebdbf650a75dc1f52a9cffcd89fabb7289321c", size = 390179 }, + { url = "https://files.pythonhosted.org/packages/06/02/3a701d6307fdfefe4fcecbac644803e2a4314ab2406ff465e03129cc85f6/optree-0.13.1-cp313-cp313-win32.whl", hash = "sha256:5b6531cd4eb23fadbbf77faf834e1119da06d7af3154f55786b59953cd87bb8a", size = 264264 }, + { url = "https://files.pythonhosted.org/packages/ef/f9/8a1421181c5eb0c0f81d1423a900baeb3faba68a48747bbdffb7581239ac/optree-0.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:27d81dc43b522ba47ba7d2e7d91dbb486940348b1bf85caeb0afc2815c0aa492", size = 293682 }, + { url = "https://files.pythonhosted.org/packages/80/34/d1b1849a6240385c4a3af5da9425b11912204d0b1cf142d802815319b73a/optree-0.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:f39c7174a3f3cdc3f5fe6fb4b832f608c40ac174d7567ed6734b2ee952094631", size = 293670 }, + { url = "https://files.pythonhosted.org/packages/0d/d6/f81e6748bcc3f35a2f570a814014e3418b0ed425d7cbc2b42d88d12863d5/optree-0.13.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3010ae24e994f6e00071098d34e98e78eb995b7454a2ef629a0bf7df17441b24", size = 702861 }, + { url = "https://files.pythonhosted.org/packages/08/7f/70a2d02110ccb245bc57bd9ad57668acfea0ff364c27d7dfe1735ede79ed/optree-0.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b5626c38d4a18a144063db5c1dbb558431d83ca10682324f74665a12214801f", size = 370740 }, + { url = "https://files.pythonhosted.org/packages/63/37/4ddf05267467809236203e2007e9443519c4d55e0744ce7eea1aa74dffee/optree-0.13.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1935639dd498a42367633e3877797e1330e39d44d48bbca1a136bb4dbe4c1bc9", size = 374695 }, + { url = "https://files.pythonhosted.org/packages/19/f2/51a63a799f6dce31813d7e02a7547394aebcb39f407e62038ecbd999d490/optree-0.13.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01819c3df950696f32c91faf8d376ae6b695ffdba18f330f1cab6b8e314e4612", size = 418671 }, + { url = "https://files.pythonhosted.org/packages/f0/7c/a08191e0c9202f2be9c415057eea3cf3a5af18e9a6d81f4c7b0e6faf0a1f/optree-0.13.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48c29d9c6c64c8dc48c8ee97f7c1d5cdb83e37320f0be0857c06ce4b97994aea", size = 414966 }, + { url = "https://files.pythonhosted.org/packages/8f/37/7bf815f4da7234e387863228b17246b42b8c02553882581a4013a64a88d0/optree-0.13.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:025d23400b8b579462a251420f0a9ae77d3d3593f84276f3465985731d79d722", size = 389219 }, + { url = "https://files.pythonhosted.org/packages/3d/84/bb521a66d3a84fe2f1500ef67d245c2cc1a26277fcaaf4bc70b22c06e99b/optree-0.13.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55e82426bef151149cfa41d68ac957730fcd420996c0db8324fca81aa6a810ba", size = 405377 }, + { url = "https://files.pythonhosted.org/packages/06/99/3eb53829c4c0b6dc20115d957d2d8e945630ddf40c656dc4e39c5a6e51f2/optree-0.13.1-cp313-cp313t-win32.whl", hash = "sha256:e40f018f522fcfd244688d1b3a360518e636ba7f636385aae0566eae3e7d29bc", size = 292734 }, + { url = "https://files.pythonhosted.org/packages/2f/59/d7601959ad0b90d309794c0975a256304488b4c5671f24e3e12101ade7ef/optree-0.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d580f1bf23bb352c4db6b3544f282f1ac08dcb0d9ab537d25e56220353438cf7", size = 331457 }, + { url = "https://files.pythonhosted.org/packages/8b/36/c01a5bc34660d46c6a3b1fe090bbdc8c76af7b5c1a6613cc671aa6df8349/optree-0.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c4d13f55dbd509d27be3af54d53b4ca0751bc518244ced6d0567e518e51452a2", size = 331470 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.13'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "patsy" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/81/74f6a65b848ffd16c18f920620ce999fe45fe27f01ab3911260ce4ed85e4/patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4", size = 396010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2b/b50d3d08ea0fc419c183a84210571eba005328efa62b6b98bc28e9ead32a/patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c", size = 232923 }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/52/531ef197b426646f26b53815a7d2a67cb7a331ef098bb276db26a68ac49f/requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", size = 52027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/bb/5deac77a9af870143c684ab46a7934038a53eb4aa975bc0687ed6ca2c610/requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", size = 23892 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "ruff" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 }, + { url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 }, + { url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 }, + { url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 }, + { url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 }, + { url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 }, + { url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 }, + { url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 }, + { url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 }, + { url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 }, + { url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 }, + { url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 }, + { url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 }, + { url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 }, + { url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 }, + { url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 }, +] + +[[package]] +name = "scikit-learn" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584 }, + { url = "https://files.pythonhosted.org/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302 }, + { url = "https://files.pythonhosted.org/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811 }, + { url = "https://files.pythonhosted.org/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354 }, +] + +[[package]] +name = "scipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, +] + +[[package]] +name = "sphinx-autoapi" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid", marker = "python_full_version >= '3.13'" }, + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/eb/cc243583bb1d518ca3b10998c203d919a8ed90affd4831f2b61ad09043d2/sphinx_autoapi-3.4.0.tar.gz", hash = "sha256:e6d5371f9411bbb9fca358c00a9e57aef3ac94cbfc5df4bab285946462f69e0c", size = 29292 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d6/f2acdc2567337fd5f5dc091a4e58d8a0fb14927b9779fc1e5ecee96d9824/sphinx_autoapi-3.4.0-py3-none-any.whl", hash = "sha256:4027fef2875a22c5f2a57107c71641d82f6166bf55beb407a47aaf3ef14e7b92", size = 34095 }, +] + +[[package]] +name = "sphinx-pyproject" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dom-toml" }, + { name = "domdf-python-tools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/97/aa8cec3da3e78f2c396b63332e2fe92fe43f7ff2ad19b3998735f28b0a7f/sphinx_pyproject-0.3.0.tar.gz", hash = "sha256:efc4ee9d96f579c4e4ed1ac273868c64565e88c8e37fe6ec2dc59fbcd57684ab", size = 7695 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/d5/89cb47c6399fd57ca451af15361499813c5d53e588cb6e00d89411ce724f/sphinx_pyproject-0.3.0-py3-none-any.whl", hash = "sha256:3aca968919f5ecd390f96874c3f64a43c9c7fcfdc2fd4191a781ad9228501b52", size = 23076 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "statsmodels" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/3b/963a015dd8ea17e10c7b0e2f14d7c4daec903baf60a017e756b57953a4bf/statsmodels-0.14.4.tar.gz", hash = "sha256:5d69e0f39060dc72c067f9bb6e8033b6dccdb0bae101d76a7ef0bcc94e898b67", size = 20354802 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/f8/2662e6a101315ad336f75168fa9bac71f913ebcb92a6be84031d84a0f21f/statsmodels-0.14.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5a24f5d2c22852d807d2b42daf3a61740820b28d8381daaf59dcb7055bf1a79", size = 10186886 }, + { url = "https://files.pythonhosted.org/packages/fa/c0/ee6e8ed35fc1ca9c7538c592f4974547bf72274bc98db1ae4a6e87481a83/statsmodels-0.14.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df4f7864606fa843d7e7c0e6af288f034a2160dba14e6ccc09020a3cf67cb092", size = 9880066 }, + { url = "https://files.pythonhosted.org/packages/d1/97/3380ca6d8fd66cfb3d12941e472642f26e781a311c355a4e97aab2ed0216/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91341cbde9e8bea5fb419a76e09114e221567d03f34ca26e6d67ae2c27d8fe3c", size = 10283521 }, + { url = "https://files.pythonhosted.org/packages/fe/2a/55c5b5c5e5124a202ea3fe0bcdbdeceaf91b4ec6164b8434acb9dd97409c/statsmodels-0.14.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1322286a7bfdde2790bf72d29698a1b76c20b8423a55bdcd0d457969d0041f72", size = 10723228 }, + { url = "https://files.pythonhosted.org/packages/4f/76/67747e49dc758daae06f33aad8247b718cd7d224f091d2cd552681215bb2/statsmodels-0.14.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e31b95ac603415887c9f0d344cb523889cf779bc52d68e27e2d23c358958fec7", size = 10859503 }, + { url = "https://files.pythonhosted.org/packages/1d/eb/cb8b01f5edf8f135eb3d0553d159db113a35b2948d0e51eeb735e7ae09ea/statsmodels-0.14.4-cp313-cp313-win_amd64.whl", hash = "sha256:81030108d27aecc7995cac05aa280cf8c6025f6a6119894eef648997936c2dd0", size = 9817574 }, +] + +[[package]] +name = "sympy" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 }, +] + +[[package]] +name = "thealgorithms-python" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "fake-useragent" }, + { name = "imageio" }, + { name = "keras" }, + { name = "lxml" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "requests" }, + { name = "rich" }, + { name = "scikit-learn" }, + { name = "sphinx-pyproject" }, + { name = "statsmodels" }, + { name = "sympy" }, + { name = "tweepy" }, + { name = "typing-extensions" }, + { name = "xgboost" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] +docs = [ + { name = "myst-parser" }, + { name = "sphinx-autoapi" }, + { name = "sphinx-pyproject" }, +] +euler-validate = [ + { name = "numpy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "requests" }, +] +lint = [ + { name = "codespell" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.12.3" }, + { name = "fake-useragent", specifier = ">=1.5.1" }, + { name = "imageio", specifier = ">=2.36.1" }, + { name = "keras", specifier = ">=3.7" }, + { name = "lxml", specifier = ">=5.3" }, + { name = "matplotlib", specifier = ">=3.9.3" }, + { name = "numpy", specifier = ">=2.1.3" }, + { name = "opencv-python", specifier = ">=4.10.0.84" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "pillow", specifier = ">=11" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "scikit-learn", specifier = ">=1.5.2" }, + { name = "sphinx-pyproject", specifier = ">=0.3" }, + { name = "statsmodels", specifier = ">=0.14.4" }, + { name = "sympy", specifier = ">=1.13.3" }, + { name = "tweepy", specifier = ">=4.14" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, + { name = "xgboost", specifier = ">=2.1.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6" }, +] +docs = [ + { name = "myst-parser", specifier = ">=4.0.0" }, + { name = "sphinx-autoapi", specifier = ">=3.4.0" }, + { name = "sphinx-pyproject", specifier = ">=0.3.0" }, +] +euler-validate = [ + { name = "numpy", specifier = ">=2.1.3" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "requests", specifier = ">=2.32.3" }, +] +lint = [ + { name = "codespell", specifier = ">=2.3" }, + { name = "ruff", specifier = ">=0.8.1" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tweepy" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/1c/0db8c3cf9d31bf63853ff612d201060ae78e6db03468a70e063bef0eda62/tweepy-4.14.0.tar.gz", hash = "sha256:1f9f1707d6972de6cff6c5fd90dfe6a449cd2e0d70bd40043ffab01e07a06c8c", size = 88623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/78/ba0065d5636bbf4a35b78c4f81b74e7858b609cdf69e629d6da5c91b9d92/tweepy-4.14.0-py3-none-any.whl", hash = "sha256:db6d3844ccc0c6d27f339f12ba8acc89912a961da513c1ae50fa2be502a56afb", size = 98520 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "xgboost" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine != 'aarch64' and platform_system == 'Linux'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/b0/131ffc4a15fd3acee9be3a7baa6b2fa6faa479799c51b880de9fc3ddf550/xgboost-2.1.3.tar.gz", hash = "sha256:7699ec4226156887d3afc665c63ab87469db9d46e361c702ba9fccd22535730c", size = 1090326 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/c6/773ebd84414879bd0566788868ae46a6574f6efaf81e694f01ea1fed3277/xgboost-2.1.3-py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.macosx_12_0_x86_64.whl", hash = "sha256:c9b0c92f13e3650e1e1cf92ff9ecef3efc6f5dc3d10ce17858df2081a89976ef", size = 2139909 }, + { url = "https://files.pythonhosted.org/packages/28/3c/ddf5d9eb742cdb7fbcd5c854bce07471bad01194ac37de91db64fbef0c58/xgboost-2.1.3-py3-none-macosx_12_0_arm64.whl", hash = "sha256:fcbf1912a852bd07a7007be350c8dc3a484c5e775b612f2b3cd082fc76240eb3", size = 1938631 }, + { url = "https://files.pythonhosted.org/packages/4a/3a/8cd69a216993fd9d54ceb079d1b357b7ef50678b3c2695d8a71962b8d0aa/xgboost-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:27af88df1162cee016c67f267a0a16c3db1c48f256e12f64c45c8f8edf9571cd", size = 4441261 }, + { url = "https://files.pythonhosted.org/packages/48/bc/05d7db90d421c5e3d681a12fd1eb087e37bf2e9bbe2b105422d6319ecc92/xgboost-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32a43526208fe676527c698cb852e0e9515e6d7294143780e476d335290a131b", size = 4532380 }, + { url = "https://files.pythonhosted.org/packages/0f/c8/f679a816c06a4a6d23da3f4b448d5f0615b51de2886ad3e3e695d17121b3/xgboost-2.1.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5d33090880f3d474f8cf5dda557c7bf8dbceefb62f2fd655c77efcabb9cac222", size = 4207000 }, + { url = "https://files.pythonhosted.org/packages/32/93/66826e2f50cefecbb0a44bd1e667316bf0a3c8e78cd1f0cdf52f5b2c5c6f/xgboost-2.1.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8d85d38553855a1f8c40b8fbccca86af19202f91b244e2c7f77afbb2a6d9d785", size = 153894508 }, + { url = "https://files.pythonhosted.org/packages/70/58/2f94976df39470fb00eec2cb4f914dde44cd0df8d96483208bf7db4bc97e/xgboost-2.1.3-py3-none-win_amd64.whl", hash = "sha256:25c0ffcbd62aac5bc22c79e08b5b2edad1d5e37f16610ebefa5f06f3e2ea3d96", size = 124909665 }, +] diff --git a/web_programming/co2_emission.py b/web_programming/co2_emission.py index 97927e7ef541..19af70489d1d 100644 --- a/web_programming/co2_emission.py +++ b/web_programming/co2_emission.py @@ -1,6 +1,7 @@ """ Get CO2 emission data from the UK CarbonIntensity API """ + from datetime import date import requests @@ -10,13 +11,13 @@ # Emission in the last half hour def fetch_last_half_hour() -> str: - last_half_hour = requests.get(BASE_URL).json()["data"][0] + last_half_hour = requests.get(BASE_URL, timeout=10).json()["data"][0] return last_half_hour["intensity"]["actual"] # Emissions in a specific date range def fetch_from_to(start, end) -> list: - return requests.get(f"{BASE_URL}/{start}/{end}").json()["data"] + return requests.get(f"{BASE_URL}/{start}/{end}", timeout=10).json()["data"] if __name__ == "__main__": diff --git a/web_programming/convert_number_to_words.py b/web_programming/convert_number_to_words.py deleted file mode 100644 index 50612dec20dd..000000000000 --- a/web_programming/convert_number_to_words.py +++ /dev/null @@ -1,111 +0,0 @@ -import math - - -def convert(number: int) -> str: - """ - Given a number return the number in words. - - >>> convert(123) - 'OneHundred,TwentyThree' - """ - if number == 0: - words = "Zero" - return words - else: - digits = math.log10(number) - digits = digits + 1 - singles = {} - singles[0] = "" - singles[1] = "One" - singles[2] = "Two" - singles[3] = "Three" - singles[4] = "Four" - singles[5] = "Five" - singles[6] = "Six" - singles[7] = "Seven" - singles[8] = "Eight" - singles[9] = "Nine" - - doubles = {} - doubles[0] = "" - doubles[2] = "Twenty" - doubles[3] = "Thirty" - doubles[4] = "Forty" - doubles[5] = "Fifty" - doubles[6] = "Sixty" - doubles[7] = "Seventy" - doubles[8] = "Eighty" - doubles[9] = "Ninety" - - teens = {} - teens[0] = "Ten" - teens[1] = "Eleven" - teens[2] = "Twelve" - teens[3] = "Thirteen" - teens[4] = "Fourteen" - teens[5] = "Fifteen" - teens[6] = "Sixteen" - teens[7] = "Seventeen" - teens[8] = "Eighteen" - teens[9] = "Nineteen" - - placevalue = {} - placevalue[2] = "Hundred," - placevalue[3] = "Thousand," - placevalue[5] = "Lakh," - placevalue[7] = "Crore," - - temp_num = number - words = "" - counter = 0 - digits = int(digits) - while counter < digits: - current = temp_num % 10 - if counter % 2 == 0: - addition = "" - if counter in placevalue.keys() and current != 0: - addition = placevalue[counter] - if counter == 2: - words = singles[current] + addition + words - elif counter == 0: - if ((temp_num % 100) // 10) == 1: - words = teens[current] + addition + words - temp_num = temp_num // 10 - counter += 1 - else: - words = singles[current] + addition + words - - else: - words = doubles[current] + addition + words - - else: - if counter == 1: - if current == 1: - words = teens[number % 10] + words - else: - addition = "" - if counter in placevalue.keys(): - addition = placevalue[counter] - words = doubles[current] + addition + words - else: - addition = "" - if counter in placevalue.keys(): - if current == 0 and ((temp_num % 100) // 10) == 0: - addition = "" - else: - addition = placevalue[counter] - if ((temp_num % 100) // 10) == 1: - words = teens[current] + addition + words - temp_num = temp_num // 10 - counter += 1 - else: - words = singles[current] + addition + words - counter += 1 - temp_num = temp_num // 10 - return words - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/web_programming/covid_stats_via_xpath.py b/web_programming/covid_stats_via_xpath.py index 85ea5d940d85..c27a5d12bb3f 100644 --- a/web_programming/covid_stats_via_xpath.py +++ b/web_programming/covid_stats_via_xpath.py @@ -4,17 +4,23 @@ more convenient to use in Python web projects (e.g. Django or Flask-based) """ -from collections import namedtuple +from typing import NamedTuple import requests -from lxml import html # type: ignore +from lxml import html -covid_data = namedtuple("covid_data", "cases deaths recovered") +class CovidData(NamedTuple): + cases: int + deaths: int + recovered: int -def covid_stats(url: str = "https://www.worldometers.info/coronavirus/") -> covid_data: + +def covid_stats(url: str = "https://www.worldometers.info/coronavirus/") -> CovidData: xpath_str = '//div[@class = "maincounter-number"]/span/text()' - return covid_data(*html.fromstring(requests.get(url).content).xpath(xpath_str)) + return CovidData( + *html.fromstring(requests.get(url, timeout=10).content).xpath(xpath_str) + ) fmt = """Total COVID-19 cases in the world: {} diff --git a/web_programming/crawl_google_results.py b/web_programming/crawl_google_results.py index 1f5e6d31992b..cb75d450ff82 100644 --- a/web_programming/crawl_google_results.py +++ b/web_programming/crawl_google_results.py @@ -8,7 +8,7 @@ if __name__ == "__main__": print("Googling.....") url = "https://www.google.com/search?q=" + " ".join(sys.argv[1:]) - res = requests.get(url, headers={"UserAgent": UserAgent().random}) + res = requests.get(url, headers={"UserAgent": UserAgent().random}, timeout=10) # res.raise_for_status() with open("project1a.html", "wb") as out_file: # only for knowing the class for data in res.iter_content(10000): diff --git a/web_programming/crawl_google_scholar_citation.py b/web_programming/crawl_google_scholar_citation.py index f92a3d139520..5f2ccad5f414 100644 --- a/web_programming/crawl_google_scholar_citation.py +++ b/web_programming/crawl_google_scholar_citation.py @@ -11,7 +11,9 @@ def get_citation(base_url: str, params: dict) -> str: """ Return the citation number. """ - soup = BeautifulSoup(requests.get(base_url, params=params).content, "html.parser") + soup = BeautifulSoup( + requests.get(base_url, params=params, timeout=10).content, "html.parser" + ) div = soup.find("div", attrs={"class": "gs_ri"}) anchors = div.find("div", attrs={"class": "gs_fl"}).find_all("a") return anchors[2].get_text() diff --git a/web_programming/currency_converter.py b/web_programming/currency_converter.py index 6fcc60e8feeb..9623504b89ea 100644 --- a/web_programming/currency_converter.py +++ b/web_programming/currency_converter.py @@ -8,13 +8,7 @@ import requests URL_BASE = "https://www.amdoren.com/api/currency.php" -TESTING = os.getenv("CI", False) -API_KEY = os.getenv("AMDOREN_API_KEY", "") -if not API_KEY and not TESTING: - raise KeyError( - "API key must be provided in the 'AMDOREN_API_KEY' environment variable." - ) # Currency and their description list_of_currencies = """ @@ -175,20 +169,31 @@ def convert_currency( - from_: str = "USD", to: str = "INR", amount: float = 1.0, api_key: str = API_KEY + from_: str = "USD", to: str = "INR", amount: float = 1.0, api_key: str = "" ) -> str: """https://www.amdoren.com/currency-api/""" + # Instead of manually generating parameters params = locals() + # from is a reserved keyword params["from"] = params.pop("from_") - res = requests.get(URL_BASE, params=params).json() + res = requests.get(URL_BASE, params=params, timeout=10).json() return str(res["amount"]) if res["error"] == 0 else res["error_message"] if __name__ == "__main__": + TESTING = os.getenv("CI", "") + API_KEY = os.getenv("AMDOREN_API_KEY", "") + + if not API_KEY and not TESTING: + raise KeyError( + "API key must be provided in the 'AMDOREN_API_KEY' environment variable." + ) + print( convert_currency( input("Enter from currency: ").strip(), input("Enter to currency: ").strip(), float(input("Enter the amount: ").strip()), + API_KEY, ) ) diff --git a/web_programming/current_stock_price.py b/web_programming/current_stock_price.py index df44da4ef351..573e1f575c8e 100644 --- a/web_programming/current_stock_price.py +++ b/web_programming/current_stock_price.py @@ -1,14 +1,40 @@ import requests from bs4 import BeautifulSoup +""" +Get the HTML code of finance yahoo and select the current qsp-price +Current AAPL stock price is 228.43 +Current AMZN stock price is 201.85 +Current IBM stock price is 210.30 +Current GOOG stock price is 177.86 +Current MSFT stock price is 414.82 +Current ORCL stock price is 188.87 +""" + def stock_price(symbol: str = "AAPL") -> str: - url = f"https://in.finance.yahoo.com/quote/{symbol}?s={symbol}" - soup = BeautifulSoup(requests.get(url).text, "html.parser") - class_ = "My(6px) Pos(r) smartphone_Mt(6px)" - return soup.find("div", class_=class_).find("span").text + """ + >>> stock_price("EEEE") + '- ' + >>> isinstance(float(stock_price("GOOG")),float) + True + """ + url = f"https://finance.yahoo.com/quote/{symbol}?p={symbol}" + yahoo_finance_source = requests.get( + url, headers={"USER-AGENT": "Mozilla/5.0"}, timeout=10 + ).text + soup = BeautifulSoup(yahoo_finance_source, "html.parser") + + if specific_fin_streamer_tag := soup.find("span", {"data-testid": "qsp-price"}): + return specific_fin_streamer_tag.get_text() + return "No tag with the specified data-testid attribute found." +# Search for the symbol at https://finance.yahoo.com/lookup if __name__ == "__main__": + from doctest import testmod + + testmod() + for symbol in "AAPL AMZN IBM GOOG MSFT ORCL".split(): print(f"Current {symbol:<4} stock price is {stock_price(symbol):>8}") diff --git a/web_programming/current_weather.py b/web_programming/current_weather.py index 3ed4c8a95a0c..4a8fa5e3c845 100644 --- a/web_programming/current_weather.py +++ b/web_programming/current_weather.py @@ -1,30 +1,50 @@ import requests -APPID = "" # <-- Put your OpenWeatherMap appid here! -URL_BASE = "https://api.openweathermap.org/data/2.5/" - - -def current_weather(q: str = "Chicago", appid: str = APPID) -> dict: - """https://openweathermap.org/api""" - return requests.get(URL_BASE + "weather", params=locals()).json() - - -def weather_forecast(q: str = "Kolkata, India", appid: str = APPID) -> dict: - """https://openweathermap.org/forecast5""" - return requests.get(URL_BASE + "forecast", params=locals()).json() - - -def weather_onecall(lat: float = 55.68, lon: float = 12.57, appid: str = APPID) -> dict: - """https://openweathermap.org/api/one-call-api""" - return requests.get(URL_BASE + "onecall", params=locals()).json() +# Put your API key(s) here +OPENWEATHERMAP_API_KEY = "" +WEATHERSTACK_API_KEY = "" + +# Define the URL for the APIs with placeholders +OPENWEATHERMAP_URL_BASE = "https://api.openweathermap.org/data/2.5/weather" +WEATHERSTACK_URL_BASE = "http://api.weatherstack.com/current" + + +def current_weather(location: str) -> list[dict]: + """ + >>> current_weather("location") + Traceback (most recent call last): + ... + ValueError: No API keys provided or no valid data returned. + """ + weather_data = [] + if OPENWEATHERMAP_API_KEY: + params_openweathermap = {"q": location, "appid": OPENWEATHERMAP_API_KEY} + response_openweathermap = requests.get( + OPENWEATHERMAP_URL_BASE, params=params_openweathermap, timeout=10 + ) + weather_data.append({"OpenWeatherMap": response_openweathermap.json()}) + if WEATHERSTACK_API_KEY: + params_weatherstack = {"query": location, "access_key": WEATHERSTACK_API_KEY} + response_weatherstack = requests.get( + WEATHERSTACK_URL_BASE, params=params_weatherstack, timeout=10 + ) + weather_data.append({"Weatherstack": response_weatherstack.json()}) + if not weather_data: + raise ValueError("No API keys provided or no valid data returned.") + return weather_data if __name__ == "__main__": from pprint import pprint - while True: - location = input("Enter a location:").strip() + location = "to be determined..." + while location: + location = input("Enter a location (city name or latitude,longitude): ").strip() if location: - pprint(current_weather(location)) - else: - break + try: + weather_data = current_weather(location) + for forecast in weather_data: + pprint(forecast) + except ValueError as e: + print(repr(e)) + location = "" diff --git a/web_programming/daily_horoscope.py b/web_programming/daily_horoscope.py index b0dd1cd65924..75e637d8e52c 100644 --- a/web_programming/daily_horoscope.py +++ b/web_programming/daily_horoscope.py @@ -7,7 +7,7 @@ def horoscope(zodiac_sign: int, day: str) -> str: "https://www.horoscope.com/us/horoscopes/general/" f"horoscope-general-daily-{day}.aspx?sign={zodiac_sign}" ) - soup = BeautifulSoup(requests.get(url).content, "html.parser") + soup = BeautifulSoup(requests.get(url, timeout=10).content, "html.parser") return soup.find("div", class_="main-horoscope").p.text diff --git a/web_programming/download_images_from_google_query.py b/web_programming/download_images_from_google_query.py index 9c0c21dc804e..235cd35763ef 100644 --- a/web_programming/download_images_from_google_query.py +++ b/web_programming/download_images_from_google_query.py @@ -39,7 +39,9 @@ def download_images_from_google_query(query: str = "dhaka", max_images: int = 5) "ijn": "0", } - html = requests.get("https://www.google.com/search", params=params, headers=headers) + html = requests.get( + "https://www.google.com/search", params=params, headers=headers, timeout=10 + ) soup = BeautifulSoup(html.text, "html.parser") matched_images_data = "".join( re.findall(r"AF_initDataCallback\(([^<]+)\);", str(soup.select("script"))) @@ -86,7 +88,7 @@ def download_images_from_google_query(query: str = "dhaka", max_images: int = 5) path_name = f"query_{query.replace(' ', '_')}" if not os.path.exists(path_name): os.makedirs(path_name) - urllib.request.urlretrieve( + urllib.request.urlretrieve( # noqa: S310 original_size_img, f"{path_name}/original_size_img_{index}.jpg" ) return index diff --git a/web_programming/emails_from_url.py b/web_programming/emails_from_url.py index 074ef878c0d7..d41dc4893608 100644 --- a/web_programming/emails_from_url.py +++ b/web_programming/emails_from_url.py @@ -1,4 +1,5 @@ """Get the site emails from URL.""" + from __future__ import annotations __author__ = "Muhammad Umer Farooq" @@ -29,12 +30,10 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None if tag == "a": # Check the list of defined attributes. for name, value in attrs: - # If href is defined, and not empty nor # print it. - if name == "href" and value != "#" and value != "": - # If not already in urls. - if value not in self.urls: - url = parse.urljoin(self.domain, value) - self.urls.append(url) + # If href is defined, not empty nor # print it and not already in urls. + if name == "href" and value not in (*self.urls, "", "#"): + url = parse.urljoin(self.domain, value) + self.urls.append(url) # Get main domain name (example.com) @@ -73,7 +72,7 @@ def emails_from_url(url: str = "https://github.com") -> list[str]: try: # Open URL - r = requests.get(url) + r = requests.get(url, timeout=10) # pass the raw HTML to the parser to get links parser.feed(r.text) @@ -84,7 +83,7 @@ def emails_from_url(url: str = "https://github.com") -> list[str]: # open URL. # read = requests.get(link) try: - read = requests.get(link) + read = requests.get(link, timeout=10) # Get the valid email. emails = re.findall("[a-zA-Z0-9]+@" + domain, read.text) # If not in list then append it. diff --git a/web_programming/fetch_anime_and_play.py b/web_programming/fetch_anime_and_play.py index 3bd4f704dd8d..e56b7124eeb5 100644 --- a/web_programming/fetch_anime_and_play.py +++ b/web_programming/fetch_anime_and_play.py @@ -1,7 +1,5 @@ -from xml.dom import NotFoundErr - import requests -from bs4 import BeautifulSoup, NavigableString +from bs4 import BeautifulSoup, NavigableString, Tag from fake_useragent import UserAgent BASE_URL = "https://ww1.gogoanime2.org" @@ -30,7 +28,7 @@ def search_scraper(anime_name: str) -> list: search_url = f"{BASE_URL}/search/{anime_name}" response = requests.get( - search_url, headers={"UserAgent": UserAgent().chrome} + search_url, headers={"UserAgent": UserAgent().chrome}, timeout=10 ) # request the url. # Is the response ok? @@ -41,25 +39,23 @@ def search_scraper(anime_name: str) -> list: # get list of anime anime_ul = soup.find("ul", {"class": "items"}) + if anime_ul is None or isinstance(anime_ul, NavigableString): + msg = f"Could not find and anime with name {anime_name}" + raise ValueError(msg) anime_li = anime_ul.children # for each anime, insert to list. the name and url. anime_list = [] for anime in anime_li: - if not isinstance(anime, NavigableString): - try: - anime_url, anime_title = ( - anime.find("a")["href"], - anime.find("a")["title"], - ) - anime_list.append( - { - "title": anime_title, - "url": anime_url, - } - ) - except (NotFoundErr, KeyError): - pass + if isinstance(anime, Tag): + anime_url = anime.find("a") + if anime_url is None or isinstance(anime_url, NavigableString): + continue + anime_title = anime.find("a") + if anime_title is None or isinstance(anime_title, NavigableString): + continue + + anime_list.append({"title": anime_title["title"], "url": anime_url["href"]}) return anime_list @@ -86,29 +82,33 @@ def search_anime_episode_list(episode_endpoint: str) -> list: request_url = f"{BASE_URL}{episode_endpoint}" - response = requests.get(url=request_url, headers={"UserAgent": UserAgent().chrome}) + response = requests.get( + url=request_url, headers={"UserAgent": UserAgent().chrome}, timeout=10 + ) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") # With this id. get the episode list. episode_page_ul = soup.find("ul", {"id": "episode_related"}) + if episode_page_ul is None or isinstance(episode_page_ul, NavigableString): + msg = f"Could not find any anime eposiodes with name {anime_name}" + raise ValueError(msg) episode_page_li = episode_page_ul.children episode_list = [] for episode in episode_page_li: - try: - if not isinstance(episode, NavigableString): - episode_list.append( - { - "title": episode.find("div", {"class": "name"}).text.replace( - " ", "" - ), - "url": episode.find("a")["href"], - } - ) - except (KeyError, NotFoundErr): - pass + if isinstance(episode, Tag): + url = episode.find("a") + if url is None or isinstance(url, NavigableString): + continue + title = episode.find("div", {"class": "name"}) + if title is None or isinstance(title, NavigableString): + continue + + episode_list.append( + {"title": title.text.replace(" ", ""), "url": url["href"]} + ) return episode_list @@ -134,17 +134,22 @@ def get_anime_episode(episode_endpoint: str) -> list: episode_page_url = f"{BASE_URL}{episode_endpoint}" response = requests.get( - url=episode_page_url, headers={"User-Agent": UserAgent().chrome} + url=episode_page_url, headers={"User-Agent": UserAgent().chrome}, timeout=10 ) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") - try: - episode_url = soup.find("iframe", {"id": "playerframe"})["src"] - download_url = episode_url.replace("/embed/", "/playlist/") + ".m3u8" - except (KeyError, NotFoundErr) as e: - raise e + url = soup.find("iframe", {"id": "playerframe"}) + if url is None or isinstance(url, NavigableString): + msg = f"Could not find url and download url from {episode_endpoint}" + raise RuntimeError(msg) + + episode_url = url["src"] + if not isinstance(episode_url, str): + msg = f"Could not find url and download url from {episode_endpoint}" + raise RuntimeError(msg) + download_url = episode_url.replace("/embed/", "/playlist/") + ".m3u8" return [f"{BASE_URL}{episode_url}", f"{BASE_URL}{download_url}"] @@ -160,7 +165,7 @@ def get_anime_episode(episode_endpoint: str) -> list: print(f"Found {len(anime_list)} results: ") for i, anime in enumerate(anime_list): anime_title = anime["title"] - print(f"{i+1}. {anime_title}") + print(f"{i + 1}. {anime_title}") anime_choice = int(input("\nPlease choose from the following list: ").strip()) chosen_anime = anime_list[anime_choice - 1] @@ -172,7 +177,7 @@ def get_anime_episode(episode_endpoint: str) -> list: else: print(f"Found {len(episode_list)} results: ") for i, episode in enumerate(episode_list): - print(f"{i+1}. {episode['title']}") + print(f"{i + 1}. {episode['title']}") episode_choice = int(input("\nChoose an episode by serial no: ").strip()) chosen_episode = episode_list[episode_choice - 1] diff --git a/web_programming/fetch_bbc_news.py b/web_programming/fetch_bbc_news.py index 7f8bc57b69f5..e5cd864a9d83 100644 --- a/web_programming/fetch_bbc_news.py +++ b/web_programming/fetch_bbc_news.py @@ -7,7 +7,7 @@ def fetch_bbc_news(bbc_news_api_key: str) -> None: # fetching a list of articles in json format - bbc_news_page = requests.get(_NEWS_API + bbc_news_api_key).json() + bbc_news_page = requests.get(_NEWS_API + bbc_news_api_key, timeout=10).json() # each article in the list is a dict for i, article in enumerate(bbc_news_page["articles"], 1): print(f"{i}.) {article['title']}") diff --git a/web_programming/fetch_github_info.py b/web_programming/fetch_github_info.py index aa4e1d7b1963..25d44245bb58 100644 --- a/web_programming/fetch_github_info.py +++ b/web_programming/fetch_github_info.py @@ -17,6 +17,7 @@ #!/usr/bin/env bash export USER_TOKEN="" """ + from __future__ import annotations import os @@ -41,7 +42,7 @@ def fetch_github_info(auth_token: str) -> dict[Any, Any]: "Authorization": f"token {auth_token}", "Accept": "application/vnd.github.v3+json", } - return requests.get(AUTHENTICATED_USER_ENDPOINT, headers=headers).json() + return requests.get(AUTHENTICATED_USER_ENDPOINT, headers=headers, timeout=10).json() if __name__ == "__main__": # pragma: no cover diff --git a/web_programming/fetch_jobs.py b/web_programming/fetch_jobs.py index 5af90a0bb239..3753d25bbe5f 100644 --- a/web_programming/fetch_jobs.py +++ b/web_programming/fetch_jobs.py @@ -1,6 +1,7 @@ """ Scraping jobs given job title and location from indeed website """ + from __future__ import annotations from collections.abc import Generator @@ -11,8 +12,10 @@ url = "https://www.indeed.co.in/jobs?q=mobile+app+development&l=" -def fetch_jobs(location: str = "mumbai") -> Generator[tuple[str, str], None, None]: - soup = BeautifulSoup(requests.get(url + location).content, "html.parser") +def fetch_jobs(location: str = "mumbai") -> Generator[tuple[str, str]]: + soup = BeautifulSoup( + requests.get(url + location, timeout=10).content, "html.parser" + ) # This attribute finds out all the specifics listed in a job for job in soup.find_all("div", attrs={"data-tn-component": "organicJob"}): job_title = job.find("a", attrs={"data-tn-element": "jobTitle"}).text.strip() diff --git a/web_programming/fetch_quotes.py b/web_programming/fetch_quotes.py index d557e2d95e74..cf0add43f002 100644 --- a/web_programming/fetch_quotes.py +++ b/web_programming/fetch_quotes.py @@ -14,11 +14,11 @@ def quote_of_the_day() -> list: - return requests.get(API_ENDPOINT_URL + "/today").json() + return requests.get(API_ENDPOINT_URL + "/today", timeout=10).json() def random_quotes() -> list: - return requests.get(API_ENDPOINT_URL + "/random").json() + return requests.get(API_ENDPOINT_URL + "/random", timeout=10).json() if __name__ == "__main__": diff --git a/web_programming/fetch_well_rx_price.py b/web_programming/fetch_well_rx_price.py index ee51b9a5051b..93be2a9235d9 100644 --- a/web_programming/fetch_well_rx_price.py +++ b/web_programming/fetch_well_rx_price.py @@ -42,7 +42,7 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None: return None request_url = BASE_URL.format(drug_name, zip_code) - response = get(request_url) + response = get(request_url, timeout=10) # Is the response ok? response.raise_for_status() diff --git a/web_programming/get_amazon_product_data.py b/web_programming/get_amazon_product_data.py index c796793f2205..b98ff2c030af 100644 --- a/web_programming/get_amazon_product_data.py +++ b/web_programming/get_amazon_product_data.py @@ -4,7 +4,6 @@ information will include title, URL, price, ratings, and the discount available. """ - from itertools import zip_longest import requests @@ -19,11 +18,15 @@ def get_amazon_product_data(product: str = "laptop") -> DataFrame: """ url = f"https://www.amazon.in/laptop/s?k={product}" header = { - "User-Agent": """Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 - (KHTML, like Gecko)Chrome/44.0.2403.157 Safari/537.36""", + "User-Agent": ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" + "(KHTML, like Gecko)Chrome/44.0.2403.157 Safari/537.36" + ), "Accept-Language": "en-US, en;q=0.5", } - soup = BeautifulSoup(requests.get(url, headers=header).text) + soup = BeautifulSoup( + requests.get(url, headers=header, timeout=10).text, features="lxml" + ) # Initialize a Pandas dataframe with the column titles data_frame = DataFrame( columns=[ @@ -74,8 +77,8 @@ def get_amazon_product_data(product: str = "laptop") -> DataFrame: except ValueError: discount = float("nan") except AttributeError: - pass - data_frame.loc[len(data_frame.index)] = [ + continue + data_frame.loc[str(len(data_frame.index))] = [ product_title, product_link, product_price, diff --git a/web_programming/get_imdb_top_250_movies_csv.py b/web_programming/get_imdb_top_250_movies_csv.py index e54b076ebd94..c914b29cb3b3 100644 --- a/web_programming/get_imdb_top_250_movies_csv.py +++ b/web_programming/get_imdb_top_250_movies_csv.py @@ -8,7 +8,7 @@ def get_imdb_top_250_movies(url: str = "") -> dict[str, float]: url = url or "https://www.imdb.com/chart/top/?ref_=nv_mv_250" - soup = BeautifulSoup(requests.get(url).text, "html.parser") + soup = BeautifulSoup(requests.get(url, timeout=10).text, "html.parser") titles = soup.find_all("td", attrs="titleColumn") ratings = soup.find_all("td", class_="ratingColumn imdbRating") return { diff --git a/web_programming/get_imdbtop.py b/web_programming/get_imdbtop.py.DISABLED similarity index 100% rename from web_programming/get_imdbtop.py rename to web_programming/get_imdbtop.py.DISABLED diff --git a/web_programming/get_ip_geolocation.py b/web_programming/get_ip_geolocation.py new file mode 100644 index 000000000000..574d287f0db1 --- /dev/null +++ b/web_programming/get_ip_geolocation.py @@ -0,0 +1,40 @@ +import requests + + +# Function to get geolocation data for an IP address +def get_ip_geolocation(ip_address: str) -> str: + try: + # Construct the URL for the IP geolocation API + url = f"https://ipinfo.io/{ip_address}/json" + + # Send a GET request to the API + response = requests.get(url, timeout=10) + + # Check if the HTTP request was successful + response.raise_for_status() + + # Parse the response as JSON + data = response.json() + + # Check if city, region, and country information is available + if "city" in data and "region" in data and "country" in data: + location = f"Location: {data['city']}, {data['region']}, {data['country']}" + else: + location = "Location data not found." + + return location + except requests.exceptions.RequestException as e: + # Handle network-related exceptions + return f"Request error: {e}" + except ValueError as e: + # Handle JSON parsing errors + return f"JSON parsing error: {e}" + + +if __name__ == "__main__": + # Prompt the user to enter an IP address + ip_address = input("Enter an IP address: ") + + # Get the geolocation data and print it + location = get_ip_geolocation(ip_address) + print(location) diff --git a/web_programming/get_top_billionaires.py b/web_programming/get_top_billionaires.py index 6a8054e26270..99f6e0be948a 100644 --- a/web_programming/get_top_billionaires.py +++ b/web_programming/get_top_billionaires.py @@ -3,7 +3,7 @@ This works for some of us but fails for others. """ -from datetime import datetime +from datetime import UTC, date, datetime import requests from rich import box @@ -11,8 +11,7 @@ from rich import table as rich_table LIMIT = 10 -TODAY = datetime.now() - +TODAY = datetime.now(tz=UTC) API_URL = ( "https://www.forbes.com/forbesapi/person/rtb/0/position/true.json" "?fields=personName,gender,source,countryOfCitizenship,birthDate,finalWorth" @@ -20,32 +19,45 @@ ) -def calculate_age(unix_date: int) -> str: - """Calculates age from given unix time format. +def years_old(birth_timestamp: int, today: date | None = None) -> int: + """ + Calculate the age in years based on the given birth date. Only the year, month, + and day are used in the calculation. The time of day is ignored. - Returns: - Age as string + Args: + birth_timestamp: The date of birth. + today: (useful for writing tests) or if None then datetime.date.today(). - >>> calculate_age(-657244800000) - '73' - >>> calculate_age(46915200000) - '51' + Returns: + int: The age in years. + + Examples: + >>> today = date(2024, 1, 12) + >>> years_old(birth_timestamp=datetime(1959, 11, 20).timestamp(), today=today) + 64 + >>> years_old(birth_timestamp=datetime(1970, 2, 13).timestamp(), today=today) + 53 + >>> all( + ... years_old(datetime(today.year - i, 1, 12).timestamp(), today=today) == i + ... for i in range(1, 111) + ... ) + True """ - birthdate = datetime.fromtimestamp(unix_date / 1000).date() - return str( - TODAY.year - - birthdate.year - - ((TODAY.month, TODAY.day) < (birthdate.month, birthdate.day)) + today = today or TODAY.date() + birth_date = datetime.fromtimestamp(birth_timestamp, tz=UTC).date() + return (today.year - birth_date.year) - ( + (today.month, today.day) < (birth_date.month, birth_date.day) ) -def get_forbes_real_time_billionaires() -> list[dict[str, str]]: - """Get top 10 realtime billionaires using forbes API. +def get_forbes_real_time_billionaires() -> list[dict[str, int | str]]: + """ + Get the top 10 real-time billionaires using Forbes API. Returns: List of top 10 realtime billionaires data. """ - response_json = requests.get(API_URL).json() + response_json = requests.get(API_URL, timeout=10).json() return [ { "Name": person["personName"], @@ -53,21 +65,22 @@ def get_forbes_real_time_billionaires() -> list[dict[str, str]]: "Country": person["countryOfCitizenship"], "Gender": person["gender"], "Worth ($)": f"{person['finalWorth'] / 1000:.1f} Billion", - "Age": calculate_age(person["birthDate"]), + "Age": str(years_old(person["birthDate"] / 1000)), } for person in response_json["personList"]["personsLists"] ] -def display_billionaires(forbes_billionaires: list[dict[str, str]]) -> None: - """Display Forbes real time billionaires in a rich table. +def display_billionaires(forbes_billionaires: list[dict[str, int | str]]) -> None: + """ + Display Forbes real-time billionaires in a rich table. Args: - forbes_billionaires (list): Forbes top 10 real time billionaires + forbes_billionaires (list): Forbes top 10 real-time billionaires """ table = rich_table.Table( - title=f"Forbes Top {LIMIT} Real Time Billionaires at {TODAY:%Y-%m-%d %H:%M}", + title=f"Forbes Top {LIMIT} Real-Time Billionaires at {TODAY:%Y-%m-%d %H:%M}", style="green", highlight=True, box=box.SQUARE, @@ -82,4 +95,7 @@ def display_billionaires(forbes_billionaires: list[dict[str, str]]) -> None: if __name__ == "__main__": + from doctest import testmod + + testmod() display_billionaires(get_forbes_real_time_billionaires()) diff --git a/web_programming/get_top_hn_posts.py b/web_programming/get_top_hn_posts.py index fbb7c051a88e..f5d4f874c6c6 100644 --- a/web_programming/get_top_hn_posts.py +++ b/web_programming/get_top_hn_posts.py @@ -5,7 +5,7 @@ def get_hackernews_story(story_id: str) -> dict: url = f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json?print=pretty" - return requests.get(url).json() + return requests.get(url, timeout=10).json() def hackernews_top_stories(max_stories: int = 10) -> list[dict]: @@ -13,7 +13,7 @@ def hackernews_top_stories(max_stories: int = 10) -> list[dict]: Get the top max_stories posts from HackerNews - https://news.ycombinator.com/ """ url = "https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty" - story_ids = requests.get(url).json()[:max_stories] + story_ids = requests.get(url, timeout=10).json()[:max_stories] return [get_hackernews_story(story_id) for story_id in story_ids] diff --git a/web_programming/get_user_tweets.py b/web_programming/get_user_tweets.py.DISABLED similarity index 100% rename from web_programming/get_user_tweets.py rename to web_programming/get_user_tweets.py.DISABLED diff --git a/web_programming/giphy.py b/web_programming/giphy.py index a5c3f8f7493e..2bf3e3ea9c0b 100644 --- a/web_programming/giphy.py +++ b/web_programming/giphy.py @@ -11,7 +11,7 @@ def get_gifs(query: str, api_key: str = giphy_api_key) -> list: """ formatted_query = "+".join(query.split()) url = f"https://api.giphy.com/v1/gifs/search?q={formatted_query}&api_key={api_key}" - gifs = requests.get(url).json()["data"] + gifs = requests.get(url, timeout=10).json()["data"] return [gif["url"] for gif in gifs] diff --git a/web_programming/instagram_crawler.py b/web_programming/instagram_crawler.py index 4536257a984e..df62735fb328 100644 --- a/web_programming/instagram_crawler.py +++ b/web_programming/instagram_crawler.py @@ -39,7 +39,7 @@ def get_json(self) -> dict: """ Return a dict of user information """ - html = requests.get(self.url, headers=headers).text + html = requests.get(self.url, headers=headers, timeout=10).text scripts = BeautifulSoup(html, "html.parser").find_all("script") try: return extract_user_profile(scripts[4]) @@ -105,7 +105,7 @@ def test_instagram_user(username: str = "github") -> None: import os if os.environ.get("CI"): - return None # test failing on GitHub Actions + return # test failing on GitHub Actions instagram_user = InstagramUser(username) assert instagram_user.user_data assert isinstance(instagram_user.user_data, dict) diff --git a/web_programming/instagram_pic.py b/web_programming/instagram_pic.py index 8521da674d7d..292cacc16c04 100644 --- a/web_programming/instagram_pic.py +++ b/web_programming/instagram_pic.py @@ -1,16 +1,47 @@ -from datetime import datetime +from datetime import UTC, datetime import requests from bs4 import BeautifulSoup + +def download_image(url: str) -> str: + """ + Download an image from a given URL by scraping the 'og:image' meta tag. + + Parameters: + url: The URL to scrape. + + Returns: + A message indicating the result of the operation. + """ + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + except requests.exceptions.RequestException as e: + return f"An error occurred during the HTTP request to {url}: {e!r}" + + soup = BeautifulSoup(response.text, "html.parser") + image_meta_tag = soup.find("meta", {"property": "og:image"}) + if not image_meta_tag: + return "No meta tag with property 'og:image' was found." + + image_url = image_meta_tag.get("content") + if not image_url: + return f"Image URL not found in meta tag {image_meta_tag}." + + try: + image_data = requests.get(image_url, timeout=10).content + except requests.exceptions.RequestException as e: + return f"An error occurred during the HTTP request to {image_url}: {e!r}" + if not image_data: + return f"Failed to download the image from {image_url}." + + file_name = f"{datetime.now(tz=UTC).astimezone():%Y-%m-%d_%H:%M:%S}.jpg" + with open(file_name, "wb") as out_file: + out_file.write(image_data) + return f"Image downloaded and saved in the file {file_name}" + + if __name__ == "__main__": - url = input("Enter image url: ").strip() - print(f"Downloading image from {url} ...") - soup = BeautifulSoup(requests.get(url).content, "html.parser") - # The image URL is in the content field of the first meta tag with property og:image - image_url = soup.find("meta", {"property": "og:image"})["content"] - image_data = requests.get(image_url).content - file_name = f"{datetime.now():%Y-%m-%d_%H:%M:%S}.jpg" - with open(file_name, "wb") as fp: - fp.write(image_data) - print(f"Done. Image saved to disk as {file_name}.") + url = input("Enter image URL: ").strip() or "https://www.instagram.com" + print(f"download_image({url}): {download_image(url)}") diff --git a/web_programming/instagram_video.py b/web_programming/instagram_video.py index 243cece1a50e..a4cddce25138 100644 --- a/web_programming/instagram_video.py +++ b/web_programming/instagram_video.py @@ -1,17 +1,17 @@ -from datetime import datetime +from datetime import UTC, datetime import requests def download_video(url: str) -> bytes: base_url = "https://downloadgram.net/wp-json/wppress/video-downloader/video?url=" - video_url = requests.get(base_url + url).json()[0]["urls"][0]["src"] - return requests.get(video_url).content + video_url = requests.get(base_url + url, timeout=10).json()[0]["urls"][0]["src"] + return requests.get(video_url, timeout=10).content if __name__ == "__main__": url = input("Enter Video/IGTV url: ").strip() - file_name = f"{datetime.now():%Y-%m-%d_%H:%M:%S}.mp4" + file_name = f"{datetime.now(tz=UTC).astimezone():%Y-%m-%d_%H:%M:%S}.mp4" with open(file_name, "wb") as fp: fp.write(download_video(url)) print(f"Done. Video saved to disk as {file_name}.") diff --git a/web_programming/nasa_data.py b/web_programming/nasa_data.py index c0a2c4fdd1a7..33a6406c52a6 100644 --- a/web_programming/nasa_data.py +++ b/web_programming/nasa_data.py @@ -3,20 +3,20 @@ import requests -def get_apod_data(api_key: str, download: bool = False, path: str = ".") -> dict: +def get_apod_data(api_key: str) -> dict: """ Get the APOD(Astronomical Picture of the day) data Get your API Key from: https://api.nasa.gov/ """ url = "https://api.nasa.gov/planetary/apod" - return requests.get(url, params={"api_key": api_key}).json() + return requests.get(url, params={"api_key": api_key}, timeout=10).json() def save_apod(api_key: str, path: str = ".") -> dict: apod_data = get_apod_data(api_key) img_url = apod_data["url"] img_name = img_url.split("/")[-1] - response = requests.get(img_url, stream=True) + response = requests.get(img_url, stream=True, timeout=10) with open(f"{path}/{img_name}", "wb+") as img_file: shutil.copyfileobj(response.raw, img_file) @@ -29,7 +29,7 @@ def get_archive_data(query: str) -> dict: Get the data of a particular query from NASA archives """ url = "https://images-api.nasa.gov/search" - return requests.get(url, params={"q": query}).json() + return requests.get(url, params={"q": query}, timeout=10).json() if __name__ == "__main__": diff --git a/web_programming/open_google_results.py b/web_programming/open_google_results.py index 2685bf62114d..52dd37d7b91a 100644 --- a/web_programming/open_google_results.py +++ b/web_programming/open_google_results.py @@ -7,10 +7,7 @@ from fake_useragent import UserAgent if __name__ == "__main__": - if len(argv) > 1: - query = "%20".join(argv[1:]) - else: - query = quote(str(input("Search: "))) + query = "%20".join(argv[1:]) if len(argv) > 1 else quote(str(input("Search: "))) print("Googling.....") @@ -19,6 +16,7 @@ res = requests.get( url, headers={"User-Agent": str(UserAgent().random)}, + timeout=10, ) try: diff --git a/web_programming/random_anime_character.py b/web_programming/random_anime_character.py index f15a9c05d9e5..aed932866258 100644 --- a/web_programming/random_anime_character.py +++ b/web_programming/random_anime_character.py @@ -12,7 +12,7 @@ def save_image(image_url: str, image_title: str) -> None: """ Saves the image of anime character """ - image = requests.get(image_url, headers=headers) + image = requests.get(image_url, headers=headers, timeout=10) with open(image_title, "wb") as file: file.write(image.content) @@ -21,7 +21,9 @@ def random_anime_character() -> tuple[str, str, str]: """ Returns the Title, Description, and Image Title of a random anime character . """ - soup = BeautifulSoup(requests.get(URL, headers=headers).text, "html.parser") + soup = BeautifulSoup( + requests.get(URL, headers=headers, timeout=10).text, "html.parser" + ) title = soup.find("meta", attrs={"property": "og:title"}).attrs["content"] image_url = soup.find("meta", attrs={"property": "og:image"}).attrs["content"] description = soup.find("p", id="description").get_text() diff --git a/web_programming/recaptcha_verification.py b/web_programming/recaptcha_verification.py index 47c6c42f2ad0..168862204fa9 100644 --- a/web_programming/recaptcha_verification.py +++ b/web_programming/recaptcha_verification.py @@ -31,6 +31,7 @@ Below a Django function for the views.py file contains a login form for demonstrating recaptcha verification. """ + import requests try: @@ -42,7 +43,7 @@ def login_using_recaptcha(request): # Enter your recaptcha secret key here - secret_key = "secretKey" + secret_key = "secretKey" # noqa: S105 url = "https://www.google.com/recaptcha/api/siteverify" # when method is not POST, direct user to login page @@ -55,7 +56,9 @@ def login_using_recaptcha(request): client_key = request.POST.get("g-recaptcha-response") # post recaptcha response to Google's recaptcha api - response = requests.post(url, data={"secret": secret_key, "response": client_key}) + response = requests.post( + url, data={"secret": secret_key, "response": client_key}, timeout=10 + ) # if the recaptcha api verified our keys if response.json().get("success", False): # authenticate the user diff --git a/web_programming/reddit.py b/web_programming/reddit.py index 6a31c81c34bd..6cc1a6b62009 100644 --- a/web_programming/reddit.py +++ b/web_programming/reddit.py @@ -26,13 +26,15 @@ def get_subreddit_data( """ wanted_data = wanted_data or [] if invalid_search_terms := ", ".join(sorted(set(wanted_data) - valid_terms)): - raise ValueError(f"Invalid search term: {invalid_search_terms}") + msg = f"Invalid search term: {invalid_search_terms}" + raise ValueError(msg) response = requests.get( f"https://reddit.com/r/{subreddit}/{age}.json?limit={limit}", headers={"User-agent": "A random string"}, + timeout=10, ) if response.status_code == 429: - raise requests.HTTPError + raise requests.HTTPError(response=response) data = response.json() if not wanted_data: diff --git a/web_programming/search_books_by_isbn.py b/web_programming/search_books_by_isbn.py index abac3c70b22e..6b69018e6639 100644 --- a/web_programming/search_books_by_isbn.py +++ b/web_programming/search_books_by_isbn.py @@ -3,6 +3,7 @@ ISBN: https://en.wikipedia.org/wiki/International_Standard_Book_Number """ + from json import JSONDecodeError # Workaround for requests.exceptions.JSONDecodeError import requests @@ -22,8 +23,9 @@ def get_openlibrary_data(olid: str = "isbn/0140328726") -> dict: """ new_olid = olid.strip().strip("/") # Remove leading/trailing whitespace & slashes if new_olid.count("/") != 1: - raise ValueError(f"{olid} is not a valid Open Library olid") - return requests.get(f"https://openlibrary.org/{new_olid}.json").json() + msg = f"{olid} is not a valid Open Library olid" + raise ValueError(msg) + return requests.get(f"https://openlibrary.org/{new_olid}.json", timeout=10).json() def summarize_book(ol_book_data: dict) -> dict: diff --git a/web_programming/slack_message.py b/web_programming/slack_message.py index f35aa3ca587e..d4d5658898ac 100644 --- a/web_programming/slack_message.py +++ b/web_programming/slack_message.py @@ -5,12 +5,15 @@ def send_slack_message(message_body: str, slack_url: str) -> None: headers = {"Content-Type": "application/json"} - response = requests.post(slack_url, json={"text": message_body}, headers=headers) + response = requests.post( + slack_url, json={"text": message_body}, headers=headers, timeout=10 + ) if response.status_code != 200: - raise ValueError( - f"Request to slack returned an error {response.status_code}, " - f"the response is:\n{response.text}" + msg = ( + "Request to slack returned an error " + f"{response.status_code}, the response is:\n{response.text}" ) + raise ValueError(msg) if __name__ == "__main__": diff --git a/web_programming/world_covid19_stats.py b/web_programming/world_covid19_stats.py index 1dd1ff6d188e..4948d8cfd43c 100644 --- a/web_programming/world_covid19_stats.py +++ b/web_programming/world_covid19_stats.py @@ -13,7 +13,7 @@ def world_covid19_stats(url: str = "https://www.worldometers.info/coronavirus") """ Return a dict of current worldwide COVID-19 statistics """ - soup = BeautifulSoup(requests.get(url).text, "html.parser") + soup = BeautifulSoup(requests.get(url, timeout=10).text, "html.parser") keys = soup.findAll("h1") values = soup.findAll("div", {"class": "maincounter-number"}) keys += soup.findAll("span", {"class": "panel-title"}) @@ -22,6 +22,5 @@ def world_covid19_stats(url: str = "https://www.worldometers.info/coronavirus") if __name__ == "__main__": - print("\033[1m" + "COVID-19 Status of the World" + "\033[0m\n") - for key, value in world_covid19_stats().items(): - print(f"{key}\n{value}\n") + print("\033[1m COVID-19 Status of the World \033[0m\n") + print("\n".join(f"{key}\n{value}" for key, value in world_covid19_stats().items()))