pax_global_header00006660000000000000000000000064151502313230014505gustar00rootroot0000000000000052 comment=3e6de1df29d111c5fa5beb4c4294ef681978fd23 kalign-3.5.1/000077500000000000000000000000001515023132300127605ustar00rootroot00000000000000kalign-3.5.1/.claude/000077500000000000000000000000001515023132300142735ustar00rootroot00000000000000kalign-3.5.1/.claude/commands/000077500000000000000000000000001515023132300160745ustar00rootroot00000000000000kalign-3.5.1/.claude/commands/release.md000066400000000000000000000105541515023132300200430ustar00rootroot00000000000000Pre-release check and preparation for version $ARGUMENTS. Run ALL of the following checks in order. For each check, report PASS or FAIL. Fix issues automatically where possible, and ask before making changes that need judgment. ## 1. Validate version argument The user must provide a version in `X.Y.Z` format (e.g. `3.4.9`). If `$ARGUMENTS` is empty or not in the right format, stop and ask for the version number. ## 2. Check git state - Run `git status` — warn about uncommitted changes - Run `git tag -l "v$ARGUMENTS"` — verify the tag `v$ARGUMENTS` does not already exist - If the tag exists, warn and ask whether to continue ## 3. Check & fix version numbers All four files must contain the version `$ARGUMENTS`. Check each one and fix any that don't match: - **pyproject.toml**: `version = "$ARGUMENTS"` - **CMakeLists.txt**: `KALIGN_LIBRARY_VERSION_MAJOR`, `KALIGN_LIBRARY_VERSION_MINOR`, `KALIGN_LIBRARY_VERSION_PATCH` must match the three parts of `$ARGUMENTS` - **build.zig**: `kalignPackageVersion` string and the `KALIGN_PACKAGE_VERSION` in the cflags array must both be `"$ARGUMENTS"` - **CITATION.cff**: `version:` field must be `$ARGUMENTS` For any mismatches, fix them directly using the Edit tool and report what was changed. ## 4. Run linting (mirrors CI) Run these commands in sequence. If `black` or `isort` report formatting issues, auto-fix by running without `--check`: ``` uv run black --check python-kalign/ uv run isort --check-only python-kalign/ uv run flake8 python-kalign --select=E9,F63,F7,F82 ``` If black or isort fail the check, run the fix commands: ``` uv run black python-kalign/ uv run isort python-kalign/ ``` Then re-run the checks to confirm they pass. ## 5. Run Python tests ``` uv run pytest tests/python/ -v --no-header -q ``` Report the results. Note: there are some known pre-existing test failures in test_parameters.py and test_ecosystem_integration.py — flag any NEW failures beyond those. ## 6. Audit documentation — package name Search all files for bare `pip install kalign` that should be `pip install kalign-python`. Use grep to find: - Any `pip install kalign` NOT followed by `-python` - Specifically check: README files, python-docs/, python-examples/, python-kalign/ If any incorrect references are found, fix them. ## 7. Audit emails Extract the author email from `pyproject.toml` (the `authors` field). Then search all project files for email addresses and verify they are consistent. Check at minimum: - `python-kalign/__init__.py` (`__email__`) - `CITATION.cff` - `ChangeLog` (most recent entry) Report any mismatches. Do not hardcode any email — always extract the canonical email from pyproject.toml dynamically. ## 8. Check ChangeLog Read the `ChangeLog` file and check whether it contains an entry for `version $ARGUMENTS`. If an entry exists, report PASS. If NO entry exists: 1. Run `git log $(git describe --tags --abbrev=0)..HEAD --oneline` to get commits since the last tag 2. Read the existing ChangeLog to understand the format (date, author, email, version line, bullet points with changes) 3. Draft a new ChangeLog entry matching the existing format, using today's date and the author name and email from pyproject.toml 4. Present the draft to the user for approval or edits 5. Only after user approval, insert the entry at the top of the ChangeLog file ## 9. Build & verify Reinstall the package and verify the version: ``` uv pip install -e . uv run python -c "import kalign; print(kalign.__version__)" ``` Confirm the printed version matches `$ARGUMENTS`. If not, investigate and fix. ## 10. Summary Print a results table like this: ``` ## Release $ARGUMENTS — Pre-release Check Results | Check | Status | |------------------------|--------| | Git state | ... | | Version consistency | ... | | Linting (black) | ... | | Linting (isort) | ... | | Linting (flake8) | ... | | Python tests | ... | | Package name audit | ... | | Email consistency | ... | | ChangeLog | ... | | Build & version verify | ... | ``` Then, if all checks pass (or only known failures remain), ask the user: > All checks passed. Would you like me to: > 1. Commit all changes with message "Release vX.Y.Z" > 2. Create tag `vX.Y.Z` > 3. Push to remote > > Or pick individual steps? Wait for the user's response before taking any of those actions. kalign-3.5.1/.containerignore000066400000000000000000000001731515023132300161500ustar00rootroot00000000000000.git build benchmarks/data benchmarks/results __pycache__ *.pyc .venv *.egg-info .pytest_cache wheelhouse dist .mypy_cache kalign-3.5.1/.github/000077500000000000000000000000001515023132300143205ustar00rootroot00000000000000kalign-3.5.1/.github/workflows/000077500000000000000000000000001515023132300163555ustar00rootroot00000000000000kalign-3.5.1/.github/workflows/benchmark.yml000066400000000000000000000101721515023132300210330ustar00rootroot00000000000000name: Benchmark on: push: branches: [main] paths: - 'lib/**' - 'python-kalign/**' - 'benchmarks/**' - 'CMakeLists.txt' - '.github/workflows/benchmark.yml' pull_request: branches: [main] paths: - 'lib/**' - 'python-kalign/**' - 'benchmarks/**' - 'CMakeLists.txt' - '.github/workflows/benchmark.yml' workflow_dispatch: permissions: contents: write deployments: write jobs: benchmark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libomp-dev cmake - name: Build C binary run: | mkdir -p build cd build cmake .. make -j$(nproc) - name: Install Python package run: | python -m pip install --upgrade pip python -m pip install -e . - name: Cache BAliBASE dataset uses: actions/cache@v4 with: path: benchmarks/data/downloads key: benchmark-datasets-balibase-v1 - name: Run benchmarks run: | python -m benchmarks \ --dataset balibase \ --method python_api cli \ --binary build/src/kalign \ --output benchmarks/results/latest.json \ -v - name: Check if results were produced id: check_results run: | if [ -f benchmarks/results/latest.json ]; then echo "has_results=true" >> "$GITHUB_OUTPUT" else echo "::warning::No benchmark results produced (dataset download may have failed)" echo "has_results=false" >> "$GITHUB_OUTPUT" fi - name: Convert results for github-action-benchmark if: steps.check_results.outputs.has_results == 'true' run: | python -c " import json with open('benchmarks/results/latest.json') as f: data = json.load(f) entries = [] for method, stats in data.get('summary', {}).items(): entries.append({ 'name': f'SP Score Mean ({method})', 'unit': 'score', 'value': round(stats['sp_mean'], 2), 'range': f\"{stats['sp_min']:.1f}-{stats['sp_max']:.1f}\", }) entries.append({ 'name': f'Total Time ({method})', 'unit': 'seconds', 'value': round(stats['total_time'], 2), }) with open('benchmarks/results/benchmark_output.json', 'w') as f: json.dump(entries, f, indent=2) " - name: Store benchmark result if: github.ref == 'refs/heads/main' && steps.check_results.outputs.has_results == 'true' uses: benchmark-action/github-action-benchmark@v1 with: tool: 'customBiggerIsBetter' output-file-path: benchmarks/results/benchmark_output.json github-token: ${{ secrets.GITHUB_TOKEN }} gh-pages-branch: gh-pages benchmark-data-dir-path: dev/bench auto-push: true alert-threshold: '95%' comment-on-alert: true fail-on-alert: false - name: Compare with baseline (PRs only) if: github.event_name == 'pull_request' && steps.check_results.outputs.has_results == 'true' uses: benchmark-action/github-action-benchmark@v1 with: tool: 'customBiggerIsBetter' output-file-path: benchmarks/results/benchmark_output.json github-token: ${{ secrets.GITHUB_TOKEN }} gh-pages-branch: gh-pages benchmark-data-dir-path: dev/bench auto-push: false alert-threshold: '95%' comment-on-alert: true fail-on-alert: true - name: Upload results artifact if: steps.check_results.outputs.has_results == 'true' uses: actions/upload-artifact@v4 with: name: benchmark-results path: benchmarks/results/ kalign-3.5.1/.github/workflows/cmake.yml000066400000000000000000000050201515023132300201550ustar00rootroot00000000000000name: CMake on: push: branches: [ "main", "python" ] paths: - 'lib/**' - 'src/**' - 'tests/**' - 'CMakeLists.txt' - '.github/workflows/cmake.yml' pull_request: branches: [ "main" ] paths: - 'lib/**' - 'src/**' - 'tests/**' - 'CMakeLists.txt' - '.github/workflows/cmake.yml' jobs: build: name: Build C/C++ on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] build_type: [Release, Debug] steps: - uses: actions/checkout@v4 - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libomp-dev cmake - name: Install dependencies (macOS) if: runner.os == 'macOS' run: | brew install --formula libomp cmake - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{ matrix.build_type }} --parallel 2 - name: Test working-directory: ${{github.workspace}}/build run: ctest -C ${{ matrix.build_type }} --output-on-failure - name: Test kalign executable run: | # Create test input echo ">seq1" > test.fasta echo "ATCGATCGATCG" >> test.fasta echo ">seq2" >> test.fasta echo "ATCGTCGATCG" >> test.fasta echo ">seq3" >> test.fasta echo "ATCGATCATCG" >> test.fasta # Test kalign executable ./build/src/kalign -i test.fasta -o test_output.fasta # Verify output exists and is not empty if [ -f test_output.fasta ] && [ -s test_output.fasta ]; then echo "✓ kalign executable test passed" else echo "✗ kalign executable test failed" exit 1 fi address-sanitizer: name: Address Sanitizer runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y libomp-dev cmake - name: Configure CMake with ASAN run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=ASAN - name: Build run: cmake --build ${{github.workspace}}/build --config ASAN --parallel 2 - name: Test with ASAN working-directory: ${{github.workspace}}/build run: ctest -C ASAN --output-on-failurekalign-3.5.1/.github/workflows/codeql-analysis.yml000066400000000000000000000037001515023132300221700ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [main, python] pull_request: # The branches below must be a subset of the branches above branches: [main, python] schedule: - cron: '0 2 * * 4' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'] language: ['c-cpp'] steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # mkdir build && cd build # cmake .. # make - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" kalign-3.5.1/.github/workflows/python.yml000066400000000000000000000077211515023132300204300ustar00rootroot00000000000000name: Python on: push: branches: [ main, python ] paths: - 'python-kalign/**' - 'lib/**' - 'CMakeLists.txt' - 'pyproject.toml' - '.github/workflows/python.yml' pull_request: branches: [ main ] paths: - 'python-kalign/**' - 'lib/**' - 'CMakeLists.txt' - 'pyproject.toml' - '.github/workflows/python.yml' workflow_dispatch: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 black isort mypy - name: Lint with flake8 run: | flake8 python-kalign --count --select=E9,F63,F7,F82 --show-source --statistics flake8 python-kalign --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Check formatting with black run: | black --check --diff python-kalign/ - name: Check import sorting with isort run: | isort --check-only --diff python-kalign/ test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install system dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libomp-dev cmake - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | brew install libomp cmake - name: Install Python dependencies run: | python -m pip install --upgrade pip python -m pip install pytest pytest-cov pytest-benchmark numpy - name: Build and install package run: | python -m pip install -e . - name: Test with pytest run: | pytest tests/python/ -v --cov=kalign --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' uses: codecov/codecov-action@v4 with: file: coverage.xml flags: python name: codecov-umbrella build-test: name: Build and test package runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install system dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libomp-dev cmake - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | brew install libomp cmake - name: Install build dependencies run: | python -m pip install --upgrade pip python -m pip install build twine - name: Build package run: | python -m build - name: Check package run: | twine check dist/* - name: Test installation from wheel run: | python -m pip install dist/*.whl python -c "import kalign; print(f'kalign {kalign.__version__} installed successfully')" - name: Test basic functionality run: | python -c " import kalign sequences = ['ATCGATCG', 'ATCGTCG', 'ATCGATCG'] aligned = kalign.align(sequences, seq_type='dna') assert len(aligned) == 3 print('✓ Basic alignment test passed') "kalign-3.5.1/.github/workflows/wheels.yml000066400000000000000000000226031515023132300203720ustar00rootroot00000000000000name: Build Python Wheels on: push: branches: [ main, python ] tags: - 'v*' pull_request: branches: [ main ] paths: - 'python-kalign/**' - 'lib/**' - 'CMakeLists.txt' - 'pyproject.toml' - '.github/workflows/wheels.yml' workflow_dispatch: inputs: publish_target: description: "Publish target (manual runs only)" required: true default: "none" type: choice options: - none - testpypi jobs: build_wheels: name: Build wheels on ${{ matrix.os }} (${{ matrix.cibw_archs }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest cibw_archs: "x86_64" - os: macos-14 cibw_archs: "x86_64" - os: macos-14 cibw_archs: "arm64" steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install cibuildwheel build - name: Build wheels env: CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*" CIBW_ARCHS: ${{ matrix.cibw_archs }} # Set minimum macOS version to match OpenMP requirements CIBW_ENVIRONMENT_MACOS: > CMAKE_BUILD_PARALLEL_LEVEL=2 OMP_NUM_THREADS=1 MACOSX_DEPLOYMENT_TARGET=14.0 CMAKE_OSX_DEPLOYMENT_TARGET=14.0 # Linux specific settings - handle different package managers CIBW_BEFORE_ALL_LINUX: > (yum install -y cmake3) || (apt-get update && apt-get install -y cmake) || (apk add --no-cache cmake) CIBW_REPAIR_WHEEL_COMMAND_LINUX: auditwheel repair -w {dest_dir} {wheel} # macOS specific settings CIBW_BEFORE_ALL_MACOS: | brew install --formula cmake libomp || echo "Dependencies may already be installed" CIBW_REPAIR_WHEEL_COMMAND_MACOS: > delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} # Environment variables for builds CIBW_ENVIRONMENT: > CMAKE_BUILD_PARALLEL_LEVEL=2 OMP_NUM_THREADS=1 # Skip testing during wheel build to avoid cross-compilation issues CIBW_TEST_SKIP: "*" run: | python -m cibuildwheel --output-dir wheelhouse - name: Upload wheels uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl build_sdist: name: Build source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install build - name: Build sdist run: | python -m build --sdist --outdir dist/ - name: Upload sdist uses: actions/upload-artifact@v4 with: name: cibw-sdist path: dist/*.tar.gz test_install: name: Test wheel installation needs: [build_wheels] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-14] python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Download wheels uses: actions/download-artifact@v4 with: pattern: cibw-wheels-* merge-multiple: true path: wheelhouse/ - name: Test installation and import run: | # Let pip find and install the appropriate wheel echo "=== Available wheels ===" ls -la wheelhouse/*.whl # Determine the distribution name from the wheel filenames (PEP 427) # e.g. kalign_test-3.4.5-...whl -> kalign-test DIST_NAME="$(ls wheelhouse/*.whl | head -n 1 | xargs basename | cut -d- -f1 | tr '_' '-')" echo "=== Detected dist name: ${DIST_NAME} ===" echo "=== Installing wheel ===" pip install --find-links wheelhouse/ "${DIST_NAME}" --force-reinstall # Check what got installed echo "=== Checking installed packages ===" pip list | grep -E 'kalign' || true # Check the kalign module structure echo "=== Checking kalign module ===" python -c " import sys print('Python version:', sys.version) print('Attempting to import kalign...') try: import kalign print('✓ kalign imported successfully') print('kalign location:', kalign.__file__) import os kalign_dir = os.path.dirname(kalign.__file__) print('kalign dir contents:', sorted(os.listdir(kalign_dir))) # Check for _core specifically core_files = [f for f in os.listdir(kalign_dir) if '_core' in f] print('_core files found:', core_files) except ImportError as e: print('✗ Import failed:', str(e)) import os import site print('Site packages:', site.getsitepackages()) for site_dir in site.getsitepackages(): kalign_path = os.path.join(site_dir, 'kalign') if os.path.exists(kalign_path): print(f'Found kalign at {kalign_path}') print('Contents:', sorted(os.listdir(kalign_path))) break sys.exit(1) " # Test the installation python -c " import kalign print('kalign version:', kalign.__version__) # Test basic functionality sequences = ['ATCGATCG', 'ATCGTCG', 'ATCGATCG'] aligned = kalign.align(sequences, seq_type='dna') print('Number of aligned sequences:', len(aligned)) print('Alignment lengths:', [len(seq) for seq in aligned]) assert len(aligned) == 3 assert all(len(seq) == len(aligned[0]) for seq in aligned) print('✓ Basic alignment test passed') # Test with different sequence types protein_seqs = ['MKTAYIAKQRQ', 'MKTAYIAKQ', 'MKTAYIAK'] aligned_proteins = kalign.align(protein_seqs, seq_type='protein') print('✓ Protein alignment test passed') print('All tests passed successfully!') " test_ecosystem: name: Test ecosystem integrations needs: [build_wheels] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: actions/download-artifact@v4 with: pattern: cibw-wheels-* merge-multiple: true path: wheelhouse/ - name: Install kalign with all extras run: | DIST_NAME="$(ls wheelhouse/*.whl | head -n 1 | xargs basename | cut -d- -f1 | tr '_' '-')" pip install --find-links wheelhouse/ "${DIST_NAME}[all]" --force-reinstall pip install pytest - name: Verify ecosystem packages are installed run: | python -c "import Bio; print(f'Biopython {Bio.__version__}')" python -c "import skbio; print(f'scikit-bio {skbio.__version__}')" python -c "import pandas; print(f'pandas {pandas.__version__}')" python -c "import matplotlib; print(f'matplotlib {matplotlib.__version__}')" - name: Run ecosystem tests (fail if any skipped) run: | pytest tests/python/test_ecosystem_real.py -v --tb=short 2>&1 | tee test_output.txt # Fail if any tests were skipped — all ecosystem deps should be present if grep -q "skipped" test_output.txt; then echo "ERROR: Some ecosystem tests were skipped — all deps should be installed" exit 1 fi upload_pypi: name: Upload to PyPI needs: [build_wheels, build_sdist, test_install, test_ecosystem] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') permissions: id-token: write environment: name: pypi steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: pattern: cibw-* merge-multiple: true path: dist/ - name: Publish to PyPI (trusted publishing) uses: pypa/gh-action-pypi-publish@release/v1 with: packages_dir: dist/ upload_testpypi: name: Upload to TestPyPI needs: [build_wheels, build_sdist, test_install, test_ecosystem] runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' && inputs.publish_target == 'testpypi' steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: pattern: cibw-* merge-multiple: true path: dist/ - name: Publish to TestPyPI (API token) uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN }} packages_dir: dist/ repository-url: https://test.pypi.org/legacy/ verbose: true kalign-3.5.1/.gitignore000066400000000000000000000174051515023132300147570ustar00rootroot00000000000000# ============================================================================= # Mixed C/C++ and Python Project .gitignore # ============================================================================= # ----------------------------------------------------------------------------- # C/C++ Build Artifacts # ----------------------------------------------------------------------------- # Object files *.o *.obj *.elf # Shared libraries *.so *.so.* *.dylib *.dll # Static libraries *.a *.lib # Executables *.exe *.out *.app /kalign /kaligncpp kalign_*_test dssim # Debug symbols *.dSYM/ *.su *.idb *.pdb # Precompiled headers *.gch *.pch # Linker output *.ilk *.map *.exp # Build directories build/ build*/ build_*/ build_test_*/ .cache/ compile_commands.json # CMake CMakeCache.txt CMakeFiles/ CMakeScripts/ Testing/ Makefile cmake_install.cmake install_manifest.txt *.cmake !CMakeLists.txt CTestTestfile.cmake _deps/ # ----------------------------------------------------------------------------- # Zig Build System # ----------------------------------------------------------------------------- zig-out/ zig-cache/ .zig-cache/ # ----------------------------------------------------------------------------- # Python Development # ----------------------------------------------------------------------------- # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class *.pyc *.pyo *.pyd # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be added to the global gitignore or merged into this project gitignore. For a PyCharm # project, it is generally recommended to include this in version control. # https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this project gitignore. For a PyCharm # project, it is generally recommended to include this in version control. .idea/ # ----------------------------------------------------------------------------- # Python Package Building # ----------------------------------------------------------------------------- # scikit-build _skbuild/ # cibuildwheel wheelhouse/ # Build artifacts from setup.py build/ dist/ *.egg-info/ # ----------------------------------------------------------------------------- # Development Tools # ----------------------------------------------------------------------------- # Visual Studio Code .vscode/ *.code-workspace # Vim *.swp *.swo *~ # Emacs *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Sublime Text *.sublime-workspace *.sublime-project # Xcode *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint # ----------------------------------------------------------------------------- # Operating System Files # ----------------------------------------------------------------------------- # macOS .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Windows Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # Linux *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* # ----------------------------------------------------------------------------- # Documentation and Archives (Keep Project Specific) # ----------------------------------------------------------------------------- # Keep project documentation but ignore generated docs !docs/ docs/_build/ docs/build/ # Archive directories (project specific - keep these) # doc/ # Commented out - this might be needed # dev/ # Commented out - this might be needed # old_src/ # Commented out - this might be needed # But ignore common archive patterns *.tar.gz *.tgz *.zip *.rar *.7z # ----------------------------------------------------------------------------- # Temporary and Cache Files # ----------------------------------------------------------------------------- # General temporary files *.tmp *.temp *.log *.bak *.backup # ----------------------------------------------------------------------------- # Project Specific # ----------------------------------------------------------------------------- # Test data outputs test_output/ *.out.fasta *.out.clustal *.out.msf # Benchmark results benchmark_results/ benchmarks/data/ benchmarks/results/ benchmarks/figures/ performance_*.txt # Profiling data *.prof gmon.out callgrind.out.* # Large test files (keep small test data) *.large.fasta test_data/large/ kalign-3.5.1/.gitmodules000066400000000000000000000000001515023132300151230ustar00rootroot00000000000000kalign-3.5.1/AUTHORS000066400000000000000000000000151515023132300140240ustar00rootroot00000000000000Timo Lassmannkalign-3.5.1/CITATION.cff000066400000000000000000000023271515023132300146560ustar00rootroot00000000000000cff-version: 1.2.0 message: "If you use this software, please cite it as below." type: software authors: - given-names: "Timo" family-names: "Lassmann" orcid: "https://orcid.org/0000-0002-0138-2691" title: "Kalign 3: multiple sequence alignment of large datasets" version: 3.5.1 date-released: 2026-02-27 url: "https://github.com/TimoLassmann/kalign" repository-code: "https://github.com/TimoLassmann/kalign" license: Apache-2.0 preferred-citation: type: article authors: - given-names: "Timo" family-names: "Lassmann" orcid: "https://orcid.org/0000-0002-0138-2691" title: "Kalign 3: multiple sequence alignment of large datasets" journal: "Bioinformatics" volume: 36 issue: 6 start: 1928 end: 1929 year: 2020 month: 3 day: 20 doi: "10.1093/bioinformatics/btz795" url: "https://academic.oup.com/bioinformatics/article/36/6/1928/5607735" publisher: name: "Oxford University Press" abstract: "Kalign is a fast multiple sequence alignment program for biological sequences (protein, DNA, RNA) with optimized algorithms and multi-threading support." keywords: - "multiple sequence alignment" - "bioinformatics" - "computational biology" - "sequence analysis" - "phylogenetics"kalign-3.5.1/CLAUDE.md000066400000000000000000000120631515023132300142410ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Kalign is a fast multiple sequence alignment (MSA) program for biological sequences (protein, DNA, RNA) written in C. The project uses CMake as the primary build system with Zig as an alternative for cross-compilation. ## Build Commands ### CMake Build (Primary) ```bash mkdir build cd build cmake .. make make test make install ``` ### Debug Build ```bash mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Debug .. make ``` ### Address Sanitizer Build ```bash mkdir build cd build cmake -DCMAKE_BUILD_TYPE=ASAN .. make ``` ### Build Without OpenMP ```bash mkdir build cd build cmake -DUSE_OPENMP=OFF .. make ``` ### Zig Build (Alternative) ```bash zig build ``` ## Testing ### Run All Tests ```bash make test # or ctest ``` ### Run Specific Test ```bash ctest -R ``` ### Test Executables - `kalign_io_test` - I/O functionality tests - `kalign_lib_test` - Library API tests - `kalign_cmp_test` - Comparison tests - `kaligncpp` - C++ API tests - `dssim` - Distance similarity tests ## Code Architecture ### Directory Structure - `lib/` - Core library implementation - `src/` - Library source files (alignment algorithms, I/O, utilities) - `include/kalign/` - Public API headers - `src/` - Main executable (command-line interface) - `tests/` - Test suite with biological sequence data ### Key Components #### Alignment Engine (`lib/src/aln_*.c`) - `aln_seqseq.c` - Sequence-to-sequence alignment - `aln_seqprofile.c` - Sequence-to-profile alignment - `aln_profileprofile.c` - Profile-to-profile alignment - `aln_controller.c` - Alignment orchestration - `bpm.c` - Bit-parallel matching (Myers' algorithm) #### I/O System (`lib/src/msa_*.c`) - `msa_io.c` - Multi-format sequence reading/writing (FASTA, MSF, Clustal) - `msa_alloc.c` - Memory management for sequence data - `msa_op.c` - Sequence operations and manipulations #### Distance Calculations - `sequence_distance.c` - Sequence distance metrics - `euclidean_dist.c` - Euclidean distance calculations - `bisectingKmeans.c` - K-means clustering for guide tree construction #### Utilities - `tldevel.c` - Development utilities and debugging - `tlmisc.c` - Miscellaneous helper functions - `alphabet.c` - Sequence alphabet handling (DNA, RNA, protein) - `task.c` - Task scheduling and threading support ### Performance Features - **Multi-threading**: OpenMP parallelization - **SIMD**: SSE4.1, AVX, AVX2 optimizations (enabled by default) - **Bit-parallel algorithms**: Myers' algorithm for efficient alignment - **Memory optimization**: Custom allocation strategies ### API Usage The library provides a C API (`kalign.h`) with C++ compatibility. Key functions: - Reading sequences from files - Running alignments with different parameters - Writing results in various formats - Memory management helpers ### Build Configuration - C11 standard required - OpenMP support (can be disabled with `-DUSE_OPENMP=OFF`) - SIMD instruction support with runtime detection - Multiple build types: Release, Debug, ASAN (Address Sanitizer) - Cross-compilation support via Zig build system ### Testing Strategy - Unit tests for core components (BPM, distance calculations, I/O) - Integration tests using Balibase sequence datasets - C++ API compatibility tests - Performance benchmarks - Format conversion tests (FASTA, MSF, Clustal) ## Python Module The repository includes a Python package that provides Python bindings for the Kalign library. The Python package is configured at the root level using modern Python packaging standards. ### Python Development Commands #### Install in Development Mode (Recommended) ```bash uv pip install -e . ``` #### Build Python Package Locally ```bash uv run python -m build ``` #### Build with CMake (for debugging C extensions) ```bash mkdir build cd build cmake -DBUILD_PYTHON_MODULE=ON .. make ``` #### Test Python Package ```bash uv run python -c "import kalign; print(kalign.__version__); seqs=['ATCG','ATCGG']; print(kalign.align(seqs))" ``` #### Run Python Tests ```bash uv run pytest tests/python/ -v ``` #### Build Wheels for Distribution ```bash uv run python -m cibuildwheel --output-dir wheelhouse ``` ### Python Package Structure - `python-kalign/__init__.py` - High-level Python API - `python-kalign/_core.cpp` - pybind11 C++ bindings - `pyproject.toml` - Modern Python build configuration (at root level) - `README-python.md` - Python-specific documentation - `tests/python/` - Python test suite ### Python API Features - **Simple interface**: `kalign.align(sequences, seq_type="auto")` - **File support**: `kalign.align_from_file("file.fasta")` - **Parameter control**: Gap penalties, threading, sequence types - **Error handling**: Python exceptions for C library errors - **Memory safety**: Automatic memory management through pybind11 ### GitHub Actions Integration The `.github/workflows/wheels.yml` workflow: - Builds wheels for Linux, macOS, Windows - Tests multiple Python versions (3.9-3.13) - Uses cibuildwheel for cross-platform compatibility - Publishes to PyPI on tagged releaseskalign-3.5.1/CMakeLists.txt000066400000000000000000000253401515023132300155240ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.18) project(kalign LANGUAGES C CXX) set(NAMESPACE_NAME "kalign") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(BUILD_SHARED_LIBS "Build the shared library" ON) include(GNUInstallDirs) include(CMakePackageConfigHelpers) include(GenerateExportHeader) set(KALIGN_LIBRARY_VERSION_MAJOR 3) set(KALIGN_LIBRARY_VERSION_MINOR 5) set(KALIGN_LIBRARY_VERSION_PATCH 1) set(KALIGN_LIBRARY_VERSION_STRING ${KALIGN_LIBRARY_VERSION_MAJOR}.${KALIGN_LIBRARY_VERSION_MINOR}.${KALIGN_LIBRARY_VERSION_PATCH}) set (CMAKE_C_STANDARD 11) # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg") # SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -pg") # SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") # SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg") # to compile without open mp: # cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DUSE_OPENMP=OFF .. # Compiler-specific flags if(MSVC) add_compile_options("$<$:/W3;/O2>") add_compile_options("$<$:/W3;/Od;/Zi>") else() add_compile_options("$<$:-W;-Wall;-O3;-pedantic>") add_compile_options("$<$:-W;-Wall;-O0;-g;-pedantic>") add_compile_options("$<$:-W;-Wall;-Wextra;-O0;-g;-DDEBUG;-pedantic;-ffunction-sections;-fdata-sections;-fstack-protector-strong;-fsanitize=address>") endif() if(CMAKE_BUILD_TYPE MATCHES ASAN AND NOT MSVC) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address") endif() if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) if (MSVC) # warning level 3 for Windows builds (level 4 too strict for wheel builds) add_compile_options(/W3) else() # lots of warnings and all warnings as errors add_compile_options(-Wall -Wextra -pedantic ) endif() endif() if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() include(GNUInstallDirs) include(CTest) include(CheckCSourceRuns) option(USE_OPENMP "Use OpenMP for parallelization" ON) option(ENABLE_SSE "Enable compile-time SSE4.1 support." ON) option(ENABLE_AVX "Enable compile-time AVX support." ON) option(ENABLE_AVX2 "Enable compile-time AVX2 support." ON) # Performance tuning parameters set(KALIGN_ALN_SERIAL_THRESHOLD "250" CACHE STRING "Alignment positions threshold below which to use serial instead of parallel processing") set(KALIGN_KMEANS_UPGMA_THRESHOLD "50" CACHE STRING "Number of sequences threshold below which to use UPGMA instead of parallel k-means") # option(ENABLE_FMA "Enable compile-time FMA support." ON) # option(ENABLE_AVX512 "Enable compile-time AVX512 support." ON) if(USE_OPENMP) # Configure OpenMP for macOS with Homebrew (only for arm64 native builds) if(APPLE AND CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "arm64" AND NOT CMAKE_OSX_ARCHITECTURES MATCHES "x86_64") list(APPEND CMAKE_PREFIX_PATH /opt/homebrew) # Set OpenMP flags for Apple Clang + Homebrew libomp set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include") set(OpenMP_C_LIB_NAMES "omp") set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include") set(OpenMP_CXX_LIB_NAMES "omp") set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) endif() find_package(OpenMP) if(OPENMP_FOUND OR OpenMP_FOUND) message(STATUS "OpenMP is enabled.") add_definitions (-DHAVE_OPENMP) else(OPENMP_FOUND OR OpenMP_FOUND) message(STATUS "OpenMP not supported") endif(OPENMP_FOUND OR OpenMP_FOUND) endif(USE_OPENMP) if (ENABLE_SSE) # # Check compiler for SSE4_1 intrinsics # if (CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) set(CMAKE_REQUIRED_FLAGS "-msse4.1") check_c_source_runs(" #include #include int main() { __m128i a = _mm_setzero_si128(); __m128i b = _mm_minpos_epu16(a); return 0; }" HAVE_SSE) endif() if (HAVE_SSE) message(STATUS "SSE4.1 is enabled - target CPU must support it") endif() if (ENABLE_AVX) # # Check compiler for AVX intrinsics # if (CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) set(CMAKE_REQUIRED_FLAGS "-mavx") check_c_source_runs(" #include int main() { __m256 a, b, c; const float src[8] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }; float dst[8]; a = _mm256_loadu_ps( src ); b = _mm256_loadu_ps( src ); c = _mm256_add_ps( a, b ); _mm256_storeu_ps( dst, c ); int i = 0; for( i = 0; i < 8; i++ ){ if( ( src[i] + src[i] ) != dst[i] ){ return -1; } } return 0; }" HAVE_AVX) endif() if (HAVE_AVX) message(STATUS "AVX is enabled - target CPU must support it") endif() endif() if (ENABLE_AVX2) # # Check compiler for AVX intrinsics # if (CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) set(CMAKE_REQUIRED_FLAGS "-mavx2") check_c_source_runs(" #include int main() { __m256i a, b, c; const int src[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; int dst[8]; a = _mm256_loadu_si256( (__m256i*)src ); b = _mm256_loadu_si256( (__m256i*)src ); c = _mm256_add_epi32( a, b ); _mm256_storeu_si256( (__m256i*)dst, c ); int i = 0; for( i = 0; i < 8; i++ ){ if( ( src[i] + src[i] ) != dst[i] ){ return -1; } } return 0; }" HAVE_AVX2) endif() if (HAVE_AVX2) message(STATUS "AVX2 is enabled - target CPU must support it") endif() endif() endif() if (HAVE_AVX2) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx2 -DHAVE_AVX2") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mavx2 -DHAVE_AVX2") else(HAVE_AVX2) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOHAVE_AVX2") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNOHAVE_AVX2") endif(HAVE_AVX2) # Add performance tuning parameters as compile definitions add_definitions(-DKALIGN_ALN_SERIAL_THRESHOLD=${KALIGN_ALN_SERIAL_THRESHOLD}) add_definitions(-DKALIGN_KMEANS_UPGMA_THRESHOLD=${KALIGN_KMEANS_UPGMA_THRESHOLD}) add_subdirectory(lib) add_subdirectory(src) add_subdirectory(tests) # Python module build (optional) ########################################## option(BUILD_PYTHON_MODULE "Build Python extension module" OFF) if(BUILD_PYTHON_MODULE) # Find pybind11 - scikit-build-core will provide this find_package(pybind11 CONFIG) if(pybind11_FOUND) message(STATUS "Building Python extension module") # Create the Python extension module pybind11_add_module(_core python-kalign/_core.cpp tests/dssim.c ) # Link against the static kalign library target_link_libraries(_core PRIVATE kalign_static) # Link OpenMP if found if(OpenMP_CXX_FOUND) target_link_libraries(_core PRIVATE OpenMP::OpenMP_CXX) endif() # Add include directories for DSSim and lib headers target_include_directories(_core PRIVATE tests lib/src lib/include ) # Set properties for the Python module set_target_properties(_core PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF ) # Install only the Python module (no shared library needed with static linking) install(TARGETS _core LIBRARY DESTINATION kalign COMPONENT python) # Install Python source files install(DIRECTORY python-kalign/ DESTINATION kalign COMPONENT python FILES_MATCHING PATTERN "*.py" PATTERN "*.typed") message(STATUS "Python extension module configured") else() message(WARNING "pybind11 not found - Python module will not be built") endif() else() message(STATUS "Python module build disabled (use -DBUILD_PYTHON_MODULE=ON to enable)") endif() # Benchmark target ############################################################ # Runs BAliBASE benchmarks comparing the C binary and the Python API. # Requires: pip install -e . (to make the kalign Python package available) # # Usage: # make benchmark # full BAliBASE suite, both methods # make benchmark BENCH_MAX_CASES=5 # quick smoke test with 5 cases # find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_FOUND) set(BENCH_MAX_CASES "0" CACHE STRING "Max benchmark cases (0 = all)") add_custom_target(benchmark COMMAND ${CMAKE_COMMAND} -E echo "Running kalign benchmarks..." COMMAND ${Python3_EXECUTABLE} -m benchmarks --dataset balibase --method python_api cli --binary $ --max-cases ${BENCH_MAX_CASES} --output ${CMAKE_SOURCE_DIR}/benchmarks/results/latest.json -v DEPENDS kalign-bin WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Benchmarking kalign (C binary vs Python API)" VERBATIM ) endif() MESSAGE(STATUS "") MESSAGE(STATUS "Configuration:") MESSAGE(STATUS "--------------------------------------") MESSAGE(STATUS "Build type : " ${CMAKE_BUILD_TYPE}) MESSAGE(STATUS "Compiler flags : " ${CMAKE_C_COMPILE_FLAGS}) MESSAGE(STATUS "Compiler c debug flags : " ${CMAKE_C_FLAGS_DEBUG}) MESSAGE(STATUS "Compiler c release flags : " ${CMAKE_C_FLAGS_RELEASE}) MESSAGE(STATUS "Compiler c min size flags : " ${CMAKE_C_FLAGS_MINSIZEREL}) MESSAGE(STATUS "Compiler c flags : " ${CMAKE_C_FLAGS}) message(STATUS "OpenMP version : " ${OpenMP_C_VERSION}) message(STATUS "OpenMP flags : " ${OpenMP_C_FLAGS}) # Package Generator ####################################################### set(CPACK_PACKAGE_VENDOR "Timo Lassmann") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Kalign") set(CPACK_PACKAGE_VERSION_MAJOR ${KALIGN_LIBRARY_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${KALIGN_LIBRARY_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${KALIGN_LIBRARY_VERSION_PATCH}) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/COPYING") # set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.org") set(CPACK_SOURCE_GENERATOR "TGZ;ZIP") set(CPACK_SOURCE_IGNORE_FILES /.cache /.git /GPATH /GTAGS /GRTAGS /.*build.* /.dir-locals.el /\\\\.DS_Store ) include (CPack) kalign-3.5.1/CONTRIBUTING.md000066400000000000000000000162251515023132300152170ustar00rootroot00000000000000# Contributing to Kalign First off, thank you for considering contributing to Kalign! It's people like you that make Kalign such a powerful multiple sequence alignment tool for the bioinformatics community. ## Table of Contents - [Code of Conduct](#code-of-conduct) - [How Can I Contribute?](#how-can-i-contribute) - [Getting Started](#getting-started) - [Development Environment Setup](#development-environment-setup) - [Pull Request Process](#pull-request-process) - [Issue Guidelines](#issue-guidelines) - [Coding Standards](#coding-standards) - [Testing](#testing) - [Community](#community) ## Code of Conduct This project and everyone participating in it is governed by our commitment to providing a welcoming and inclusive environment. By participating, you are expected to uphold high standards of respectful communication and collaboration. **Our Standards:** - Use welcoming and inclusive language - Be respectful of differing viewpoints and experiences - Gracefully accept constructive criticism - Focus on what is best for the community - Show empathy towards other community members ## How Can I Contribute? ### Types of Contributions Welcome We welcome many different types of contributions: - **Bug reports and fixes** - Help us identify and resolve issues - **Performance improvements** - Optimizations for speed and memory usage - **New features** - Alignment algorithms, output formats, or usability enhancements - **Documentation** - API docs, tutorials, examples, or README improvements - **Testing** - Writing tests, testing on different platforms, benchmarking - **Python bindings** - Improvements to the Python package - **Build system** - CMake, Zig, CI/CD improvements ### What We're NOT Looking For Please don't use the issue tracker for: - Support questions (use discussions instead) - Feature requests that fundamentally change Kalign's core purpose - Issues related to third-party tools or dependencies ## Getting Started ### Your First Contribution Unsure where to begin? You can start by looking through these issues: - **Good first issue** - Issues that are good for newcomers - **Help wanted** - Issues that need attention from the community Never made an open source contribution before? Here are some helpful resources: - [First Timers Only](https://www.firsttimersonly.com/) - [How to Contribute to an Open Source Project on GitHub](https://opensource.guide/how-to-contribute/) ### Before You Start For large changes, please open an issue first to discuss what you would like to change. This helps ensure your contribution aligns with the project's goals and avoids duplicate work. ## Development Environment Setup ### Prerequisites - **C compiler** - GCC, Clang, or MSVC - **CMake** (3.18 or higher) - **Git** - **OpenMP** (optional, for parallelization) ### Optional Dependencies - **Zig** (for alternative build system) - **Python** (3.9+ for Python bindings) - **pybind11** (for Python module development) ### Building from Source ```bash # Clone your fork git clone https://github.com/yourusername/kalign.git cd kalign # Create build directory mkdir build && cd build # Configure and build cmake .. make # Run tests make test ``` ### Python Development ```bash cd python pip install -e . python -c "import kalign; print(kalign.__version__)" ``` ## Pull Request Process 1. **Fork** the repository and create your branch from `main` 2. **Make your changes** following our coding standards 3. **Add tests** for any new functionality 4. **Update documentation** if you change APIs or add features 5. **Ensure tests pass** locally before submitting 6. **Submit a pull request** with a clear description ### Pull Request Guidelines - **One feature per PR** - Keep changes focused and atomic - **Clear commit messages** - Use descriptive commit messages - **Link related issues** - Reference any related issue numbers - **Update CHANGELOG** - Add a brief description of your changes - **Cross-platform compatibility** - Ensure changes work on Linux, macOS, and Windows ## Issue Guidelines ### Bug Reports When reporting bugs, please include: - **Kalign version** - Output of `kalign --version` - **Operating system** - Including version - **Build information** - How you installed/built Kalign - **Input data** - Sample sequences that trigger the bug (if possible) - **Expected behavior** - What you expected to happen - **Actual behavior** - What actually happened - **Error messages** - Complete error output - **Steps to reproduce** - Minimal steps to trigger the issue ### Feature Requests When suggesting features: - **Use case** - Describe the problem you're trying to solve - **Proposed solution** - How you envision the feature working - **Alternatives** - Other approaches you've considered - **Impact** - Who would benefit from this feature ### Performance Issues For performance problems: - **Input size** - Number and length of sequences - **System specs** - CPU, RAM, OS - **Timing data** - How long operations take - **Comparison** - Performance with other tools (if applicable) ## Coding Standards ### C Code Style - **Indentation** - Use spaces, not tabs (consistent with existing code) - **Naming** - Use descriptive variable and function names - **Comments** - Document complex algorithms and non-obvious code - **Memory management** - Always check malloc/free pairs - **Error handling** - Use consistent error checking patterns ### Python Code Style - **PEP 8** - Follow Python style guidelines - **Type hints** - Use type annotations for function signatures - **Docstrings** - Document all public functions and classes ### Commit Message Format ``` type(scope): brief description Detailed explanation if needed Fixes #issue_number ``` Examples: - `fix(alignment): handle empty sequences correctly` - `feat(python): add support for custom gap penalties` - `docs(readme): update installation instructions` ## Testing ### Running Tests ```bash # C/C++ tests cd build make test # Python tests cd python python -m pytest ``` ### Test Requirements - **All tests must pass** before submitting PRs - **Add tests** for new features and bug fixes - **Cross-platform testing** - Test on different operating systems when possible - **Performance tests** - Include benchmarks for performance-critical changes ### Test Data Use the provided test sequences in `tests/data/` or create minimal test cases. For large datasets, provide instructions for users to download test data separately. ## Community ### Getting Help - **GitHub Discussions** - For questions and general discussion - **Issues** - For bug reports and feature requests - **Email** - Contact the maintainer for security issues ### Recognition Contributors are recognized in: - **AUTHORS file** - All contributors are listed - **Release notes** - Significant contributions are highlighted - **Paper acknowledgments** - For substantial algorithmic contributions ## License By contributing to Kalign, you agree that your contributions will be licensed under the Apache License 2.0. ## Questions? Don't hesitate to ask! The worst thing that can happen is that you'll be politely asked to change something. We appreciate any sort of contribution and don't want a wall of rules to get in the way of that. Thank you for contributing to Kalign! 🧬kalign-3.5.1/COPYING000066400000000000000000000260321515023132300140160ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to the Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by the Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding any notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. Please also get an OpenSSF Best Practices badge (https://www.bestpractices.dev/) to show that the project follows best practices. Copyright 2006-2026 Timo Lassmann Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. kalign-3.5.1/ChangeLog000066400000000000000000000147321515023132300145410ustar00rootroot000000000000002026-02-27 Timo Lassmann * version 3.5.1 - Bugfix release - Fix memory leak in build_tree_from_pairwise (realign/ensemble) - Move seaborn from core to optional dependency - Fix benchmark workflow to handle unavailable datasets gracefully - Black formatting fixes * version 3.5.0 - Three modes Kalign now has three modes: default (best general-purpose), fast (same as v3.4), and precise (ensemble, highest accuracy, ~10x slower). - Ensemble alignment (--precise or --ensemble N) - Per-column confidence scores from ensemble mode - Ensemble consensus save/load (--save-poar, --load-poar) - Alignment refinement (--refine) - PFASUM substitution matrices (--type pfasum, pfasum43, pfasum60) - stdin via -i - convention (samtools/bcftools style) - Python align() now supports all parameters (mode, ensemble, etc.) - License changed from GPL-3.0-or-later to Apache-2.0 - Fixed crash in Python align() on Linux (uninitialized MSA fields) - Fixed selenocysteine (U) handling in reduced alphabet (Debian #1127766) 2026-02-10 Timo Lassmann * version 3.4.9 - Updated documentation to use kalign-python package name - Added claude code release command 2024-04-24 Timo Lassmann * version 3.4.1 - Fixed an issue when kalign is given hundreds of identical sequences. - added build.zig 2023-12-10 Timo Lassmann * version 3.4.0 - Added a simple sequence simulator for testing - Fixed an issue where alignments would be slighly different depending on the number of threads used. 2022-11-05 Timo Lassmann * version 3.3.5 - Added a check to find and remove sequences of length 0. 2022-10-28 Timo Lassmann * version 3.3.4 - Cmake and more - switched to cmake Added: 1) a Kalign library to make it easier to use Kalign from another projects 2) a block version of Gene Myers bit parallel string matching code (described here: Myers, Gene. "A fast bit-vector algorithm for approximate string matching based on dynamic programming." Journal of the ACM (JACM) 46.3 (1999): 395-415). This means Kalign will now run equivalently on processors with and without AVX2 instructions (e.g. Apple M1 / M2 and ARM chips). 3) alignment types giving users more control over alignment parameters. 4) multi-threading 2022-03-21 Timo Lassmann * version 3.3.2 - Bug Fix There was a bug in building a guide tree from highly similar sequences. The fix was involved distributing identical sequences equally among branches. This only happened when there were thousands of identical sequences. In addition Kalign now compiles on Apple's M1 chip and possibly on other ARM architectures as well (although I did not test the latter). 2021-04-16 Timo Lassmann * version 3.3.1 - Bug Fix The previous version kalign checked the top 50 sequences in inputs to determine whether the sequences are aligned or not. If the first 50 sequences are not aligned, but following sequences contain gaps (or other characters!) kalign can crash. In this version (3.3.1) kalign checks all sequences, thereby avoiding this issue. To alert users to the situation described above and to warn users about the presence of odd characters, kalign now produces a warning message like this: [Date Time] : LOG : Start io tests. [Date Time] : LOG : reading: dev/data/a2m.good.1 [Date Time] : LOG : Detected protein sequences. [Date Time] : WARNING : -------------------------------------------- (rwalign.c line 505) [Date Time] : WARNING : The input sequences contain gap characters: (rwalign.c line 506) [Date Time] : WARNING : "-" : 36 found (rwalign.c line 510) [Date Time] : WARNING : BUT the sequences do not seem to be aligned! (rwalign.c line 514) [Date Time] : WARNING : (rwalign.c line 515) [Date Time] : WARNING : Kalign will remove the gap characters and (rwalign.c line 516) [Date Time] : WARNING : align the sequences. (rwalign.c line 517) [Date Time] : WARNING : -------------------------------------------- (rwalign.c line 518) 2020-11-06 Timo Lassmann * version 3.3 - Threading and more - Kalign now runs pairwise distance estimation, guide tree building and alignments in parallel. - Memory optimisations. - Optimised bi-sectional K-means algorithm. - added -clean option to check for sequences with identical names but different sequences. - fixed minor bug in alignment I/O module 2020-09-24 Timo Lassmann * version 3.2.7 - Development version - dynamic programming in now more modular. - fixed rare bug in alignment input / output - added gap parameters (--gpo, --gpe, --tgpe) - for protein alignment I now use the CorBLOSUM66_13plus matrix from: Hess M, Keul F, Goesele M, Hamacher K. Addressing inaccuracies in BLOSUM computation improves homology search performance. BMC bioinformatics. 2016 Dec 1;17(1):189. with the empirically derived gap penalties. 2020-04-22 Timo Lassmann * version 3.2.5 - Bug fix: when given long output named the first lines in msf output could be truncated. 2020-04-01 Timo Lassmann * version 3.2.4 - Fixed issue relating to stdin input on clusters. - Added more sanity checks 2020-03-16 Timo Lassmann * version 3.2.3 - replaced timing code with code from the easel lib. 2020-02-23 Timo Lassmann * version 3.2.2 - Fixed minor bug in rwaln test routine. It assumed that input alignments were correctly formatted (which was not true for one test case). The kalign executable was never affected by this. - Added a script to test a few alignments. 2020-02-22 Timo lassmann * version 3.2.1 minor bug fix removed "-lrt" required for old glibc versions and replaced with a search in configure.ac: AC_SEARCH_LIBS([clock_gettime],[rt]) 2020-02-15 Timo Lassmann * version 3.2.0 Added support for reading sequences from standard input: cat file.fasta | kalign -f fasta | .... Added support for combining multiple input files into one alignment: kalign sequencesA.fa sequencesB.fa > msa.fa Also works in combination: cat file.fasta | kalign sequencesA.fa sequencesB.fa > msa.fa Minor: - added m4 macros to enable / disable compiler flags - added m4 macro for valgrind. Now there is a make target called check-valgrind that run all tests through valgrind. kalign-3.5.1/Containerfile000066400000000000000000000050421515023132300154660ustar00rootroot00000000000000# Kalign Benchmark Container # # Includes kalign, Clustal Omega, MAFFT, and MUSCLE v5 for comparative # benchmarking on BAliBASE, BRAliBASE, and BaliFam100 datasets. # # Build: # podman build -t kalign-benchmark . # # Run the interactive dashboard: # podman run -it -p 8050:8050 \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # kalign-benchmark # # Run a CLI benchmark: # podman run -it \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # kalign-benchmark \ # python -m benchmarks \ # --dataset balibase --method python_api clustalo mafft muscle -v # # View results in the dashboard after a CLI run: # podman run -it -p 8050:8050 \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # -v ./benchmarks/results:/kalign/benchmarks/results \ # kalign-benchmark FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # System dependencies + alignment tools (clustalo, mafft from apt) RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake g++ git curl \ python3 python3-pip python3-venv python3-dev \ clustalo mafft \ pkg-config \ && rm -rf /var/lib/apt/lists/* # ---------- Build MUSCLE v5 from source ---------- # myutils.h checks __arm64__ (macOS) but not __aarch64__ (Linux); add it RUN cd /tmp && \ git clone --depth 1 https://github.com/rcedgar/muscle.git && \ cd muscle/src && \ sed -i 's/defined(__arm64__)/defined(__arm64__) || defined(__aarch64__)/' myutils.h && \ bash build_linux.bash && \ cp ../bin/muscle /usr/local/bin/ && \ rm -rf /tmp/muscle # ---------- Copy kalign source and build ---------- COPY . /kalign WORKDIR /kalign RUN mkdir -p build && cd build && \ cmake -DCMAKE_BUILD_TYPE=Release .. && \ make -j"$(nproc)" # ---------- Python environment ---------- RUN python3 -m venv /venv ENV PATH="/venv/bin:/kalign/build/src:$PATH" RUN pip install --no-cache-dir uv && \ uv pip install --no-cache -e ".[benchmark]" # ---------- Verify tools ---------- RUN which kalign && which clustalo && which mafft && which muscle # ---------- Data & results directories ---------- RUN mkdir -p /kalign/benchmarks/data/downloads /kalign/benchmarks/results # ---------- Hot-swap: cross-compiled kalign binary (last for fast rebuilds) ---------- COPY zig-out/kalign-linux-aarch64 /usr/local/bin/kalign RUN chmod +x /usr/local/bin/kalign # Rebuild Python module with latest source (uses cached venv layer) RUN uv pip install --no-cache -e ".[benchmark]" EXPOSE 8050 CMD ["python", "-m", "benchmarks.app", "--host", "0.0.0.0", "--port", "8050"] kalign-3.5.1/Containerfile.downstream000066400000000000000000000175451515023132300176630ustar00rootroot00000000000000# Kalign Downstream Benchmark Container # # Extends the base benchmark setup with tools for downstream application # benchmarks: positive selection (HyPhy), phylogenetics (IQ-TREE), # homology detection (HMMER), and confidence comparison (GUIDANCE2). # # Build: # podman build -f Containerfile.downstream -t kalign-downstream . # # Run all downstream benchmarks: # podman run -it \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # -v ./benchmarks/results:/kalign/benchmarks/results \ # kalign-downstream \ # python -m benchmarks.downstream --all -j 4 # # Quick smoke test (5 cases per pipeline): # podman run -it \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # -v ./benchmarks/results:/kalign/benchmarks/results \ # kalign-downstream \ # python -m benchmarks.downstream --all -j 4 --quick # # Generate figures from existing results: # podman run -it \ # -v ./benchmarks/results:/kalign/benchmarks/results \ # -v ./benchmarks/figures:/kalign/benchmarks/figures \ # kalign-downstream \ # python -m benchmarks.downstream --figures -o benchmarks/figures/ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # ── System dependencies ────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake g++ git curl wget ca-certificates \ python3 python3-pip python3-venv python3-dev \ pkg-config zlib1g-dev libcurl4-openssl-dev libssl-dev libeigen3-dev libboost-dev \ clustalo mafft hmmer \ perl libwww-perl libbio-perl-perl cpanminus \ && rm -rf /var/lib/apt/lists/* # Bio::Perl convenience module (removed from BioPerl core in 1.7.x) RUN cpanm --notest Bio::Perl # ── MUSCLE v5 from source ─────────────────────────────────────────── # myutils.h checks __arm64__ (macOS) but not __aarch64__ (Linux); add it RUN cd /tmp && \ git clone --depth 1 https://github.com/rcedgar/muscle.git && \ cd muscle/src && \ sed -i 's/defined(__arm64__)/defined(__arm64__) || defined(__aarch64__)/' myutils.h && \ bash build_linux.bash && \ cp ../bin/muscle /usr/local/bin/ && \ rm -rf /tmp/muscle # ── INDELible v1.03 from source ───────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 https://github.com/matsengrp/indelible.git && \ cd indelible/src && \ make && \ cp indelible /usr/local/bin/ && \ rm -rf /tmp/indelible # ── HyPhy from source ─────────────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 https://github.com/veg/hyphy.git && \ cd hyphy && \ cmake -DCMAKE_BUILD_TYPE=Release -DNOAVX=ON . && \ make -j"$(nproc)" hyphy && \ cp hyphy /usr/local/bin/hyphy && \ cp -r res /usr/local/lib/hyphy && \ rm -rf /tmp/hyphy ENV HYPHY_LIB=/usr/local/lib/hyphy ENV HYPHY_PATH=/usr/local/lib/hyphy # ── IQ-TREE 2 from source ─────────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 --recurse-submodules https://github.com/iqtree/iqtree2.git && \ cd iqtree2 && \ mkdir build && cd build && \ cmake -DCMAKE_BUILD_TYPE=Release .. && \ make -j"$(nproc)" && \ cp iqtree2 /usr/local/bin/ && \ rm -rf /tmp/iqtree2 # ── GUIDANCE2 from GitHub (original tar.gz URL is dead) ──────────── # guidance.pl uses FindBin-relative paths ($Bin, $Bin/../Selecton, etc.) # We install the full www/ tree under /opt/guidance-root/ so sibling # directories (Selecton, bioSequence_scripts_and_constants) resolve # correctly relative to the guidance.pl script location. RUN cd /tmp && \ git clone --depth 1 https://github.com/anzaika/guidance.git && \ cd guidance && make && \ mkdir -p /opt/guidance-root && \ cp -r www/Guidance /opt/guidance-root/Guidance && \ cp -r www/Selecton /opt/guidance-root/Selecton && \ cp -r www/bioSequence_scripts_and_constants /opt/guidance-root/bioSequence_scripts_and_constants && \ mkdir -p /opt/guidance-root/Guidance/exec && \ cp programs/msa_set_score/msa_set_score /opt/guidance-root/Guidance/exec/ && \ cp programs/removeTaxa/removeTaxa /opt/guidance-root/Guidance/exec/ && \ cp programs/isEqualTree/isEqualTree /opt/guidance-root/Guidance/exec/ && \ chmod +x /opt/guidance-root/Guidance/guidance.pl && \ mkdir -p /opt/programs/semphy /opt/programs/msa_set_score \ /opt/programs/removeTaxa /opt/programs/isEqualTree && \ cp programs/semphy/semphy /opt/programs/semphy/ && \ cp programs/msa_set_score/msa_set_score /opt/programs/msa_set_score/ && \ cp programs/removeTaxa/removeTaxa /opt/programs/removeTaxa/ && \ cp programs/isEqualTree/isEqualTree /opt/programs/isEqualTree/ && \ printf '#!/bin/sh\nexec perl /opt/guidance-root/Guidance/guidance.pl "$@"\n' \ > /usr/local/bin/guidance2 && \ chmod +x /usr/local/bin/guidance2 && \ rm -rf /tmp/guidance # ── Kalign C build ────────────────────────────────────────────────── COPY . /kalign WORKDIR /kalign RUN mkdir -p build && cd build && \ cmake -DCMAKE_BUILD_TYPE=Release .. && \ make -j"$(nproc)" # ── Record tool versions at build time ────────────────────────────── RUN echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /tool_versions.txt && \ echo "kalign=$(build/src/kalign --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "mafft=$(mafft --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "muscle=$(muscle --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "clustalo=$(clustalo --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "hmmer=$(hmmbuild -h 2>&1 | grep '^# HMMER' | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "iqtree=$(iqtree2 --version 2>&1 | grep 'IQ-TREE' | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "indelible=1.03" >> /tool_versions.txt && \ echo "hyphy=$(hyphy --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt # ── Python environment ────────────────────────────────────────────── RUN python3 -m venv /venv ENV PATH="/venv/bin:/kalign/build/src:$PATH" RUN pip install --no-cache-dir uv && \ uv pip install --no-cache -e ".[benchmark]" && \ uv pip install --no-cache \ dendropy biopython pandas matplotlib scipy seaborn numpy # ── Verify tools ──────────────────────────────────────────────────── RUN which kalign && which clustalo && which mafft && which muscle && \ which hmmbuild && which hmmsearch && which iqtree2 && \ which hyphy && which indelible && which guidance2 # ── Data & results directories ────────────────────────────────────── RUN mkdir -p benchmarks/data/downloads/pfam_seed \ benchmarks/data/downloads/swissprot \ benchmarks/data/downloads/selectome \ benchmarks/results/calibration \ benchmarks/results/positive_selection \ benchmarks/results/phylo_accuracy \ benchmarks/results/hmmer_detection \ benchmarks/figures EXPOSE 8050 CMD ["python", "-m", "benchmarks.downstream", "--help"] kalign-3.5.1/README-python.md000066400000000000000000000252361515023132300155660ustar00rootroot00000000000000# Kalign Python Package Python bindings for [Kalign](https://github.com/TimoLassmann/kalign), a fast multiple sequence alignment program for biological sequences (DNA, RNA, protein). ## Installation ```bash pip install kalign-python ``` Optional dependencies for ecosystem integration: ```bash pip install kalign-python[biopython] # Biopython integration (fmt="biopython", I/O helpers) pip install kalign-python[skbio] # scikit-bio integration (fmt="skbio") pip install kalign-python[io] # I/O helpers (requires Biopython) pip install kalign-python[analysis] # pandas + matplotlib for downstream analysis pip install kalign-python[all] # all of the above ``` ## Quick Start ```python import kalign sequences = [ "ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG" ] # Default mode — consistency anchors + VSM (best general-purpose) aligned = kalign.align(sequences) # Fast mode — no consistency, fastest aligned = kalign.align(sequences, mode="fast") # Precise mode — ensemble + realign, highest precision aligned = kalign.align(sequences, mode="precise") ``` ## Modes Kalign v3.5 provides three named modes that package the best configurations: | Mode | Python | CLI | Description | |------|--------|-----|-------------| | **default** | `mode="default"` or omit | `kalign` | Consistency anchors + VSM. Best general-purpose. | | **fast** | `mode="fast"` | `kalign --fast` | VSM only. Fastest, similar to kalign v3.4. | | **precise** | `mode="precise"` | `kalign --precise` | Ensemble(3) + VSM + realign. Highest precision. | Explicit parameters always override mode defaults: ```python # Fast base + 5 ensemble runs aligned = kalign.align(sequences, mode="fast", ensemble=5) # Precise base + custom gap penalty aligned = kalign.align(sequences, mode="precise", gap_open=8.0) ``` Mode constants are also available: `kalign.MODE_DEFAULT`, `kalign.MODE_FAST`, `kalign.MODE_PRECISE`. ## Core API ### `kalign.align()` ```python aligned = kalign.align( sequences, # list of str mode=None, # "default", "fast", "precise", or None (= default) seq_type="auto", # "auto", "dna", "rna", "protein", "divergent", "internal" gap_open=None, # positive float, or None for defaults gap_extend=None, # positive float, or None for defaults terminal_gap_extend=None, n_threads=None, # int, or None for global default refine="none", # refinement mode: "none", "all", "confident", "inline" ensemble=0, # number of ensemble runs (0 = off, try 3-5) min_support=0, # explicit consensus threshold (0 = auto) fmt="plain", # "plain", "biopython", "skbio" ids=None, # list of str (for biopython/skbio output) ) ``` Returns a list of aligned strings (default), a `Bio.Align.MultipleSeqAlignment` (`fmt="biopython"`), or a `skbio.TabularMSA` (`fmt="skbio"`). When `ensemble > 0` and `fmt="biopython"`, per-residue confidence is attached as HMMER-style PP `letter_annotations["posterior_probability"]`. ### `kalign.align_from_file()` Align sequences directly from a FASTA, MSF, or Clustal file: ```python result = kalign.align_from_file("sequences.fasta", seq_type="protein") for name, seq in zip(result.names, result.sequences): print(f"{name}: {seq}") ``` Returns an `AlignedSequences` object with `.names`, `.sequences`, and optional confidence fields (see below). Additional parameters for advanced use: ```python result = kalign.align_from_file( "input.fasta", mode="precise", # or "default", "fast" ensemble=5, # override: 5 runs instead of mode default (3) min_support=0, # consensus threshold (0 = auto) save_poar="consensus.poar", # save POAR table for re-thresholding # load_poar="consensus.poar", # OR load pre-computed POAR refine="none", # refinement mode ) ``` ### `kalign.compare()` Score a test alignment against a reference using the Sum-of-Pairs (SP) score: ```python score = kalign.compare("reference.msf", "test.fasta") print(f"SP score: {score:.1f}") # 0 (no match) to 100 (identical) ``` ### `kalign.compare_detailed()` Detailed alignment comparison returning POAR recall/precision/F1/TC: ```python scores = kalign.compare_detailed("reference.msf", "test.fasta") print(f"F1: {scores['f1']:.3f}, TC: {scores['tc']:.3f}") ``` ### `kalign.write_alignment()` Write aligned sequences to a file: ```python kalign.write_alignment(aligned, "output.fasta", format="fasta", ids=ids) ``` Supported formats: `fasta`, `clustal`, `stockholm`, `phylip` (non-FASTA formats require Biopython). ## Ensemble Alignment & Confidence Scores Ensemble mode runs multiple alignments with varied parameters and combines results via POAR (Pairs of Aligned Residues) consensus. The simplest way to use it is `mode="precise"` (ensemble=3 + realign). For more control, set `ensemble` directly. ```python import kalign # Precise mode: ensemble(3) + realign — highest precision result = kalign.align_from_file("proteins.fasta", mode="precise") # Or: explicit 5 ensemble runs result = kalign.align_from_file("proteins.fasta", ensemble=5) # Per-column confidence: average agreement across ensemble runs [0..1] print(result.column_confidence[:10]) # e.g. [0.93, 0.87, 1.0, ...] # Per-residue confidence: per-sequence, per-position agreement [0..1] print(result.residue_confidence[0][:10]) # e.g. [0.95, 0.90, 1.0, ...] ``` ### POAR Save/Load Save the POAR consensus table to avoid re-running the ensemble when experimenting with thresholds: ```python # First run: compute ensemble and save POAR kalign.align_file_to_file("input.fa", "output.fa", ensemble=5, save_poar="consensus.poar") # Later: instant re-threshold from saved POAR (no re-alignment) kalign.align_file_to_file("input.fa", "output2.fa", load_poar="consensus.poar", min_support=3) ``` ### Stockholm Output with Confidence Write confidence annotations in Stockholm format (`#=GR PP` per-residue, `#=GC PP_cons` per-column): ```python result = kalign.align_from_file("input.fasta", ensemble=5) kalign.write_alignment( result.sequences, "output.sto", format="stockholm", ids=result.names, column_confidence=result.column_confidence, residue_confidence=result.residue_confidence, ) ``` Confidence uses HMMER-style PP encoding: `*`=95%+, `9`=85-95%, ..., `0`=0-5%, `.`=gap. ### Biopython Per-Residue PP When using `fmt="biopython"` with ensemble, per-residue confidence is attached as `letter_annotations["posterior_probability"]`: ```python aln = kalign.align(seqs, ensemble=3, fmt="biopython", ids=ids) print(aln[0].letter_annotations["posterior_probability"]) # e.g. "998.76*9..." ``` ## Threading ```python import kalign kalign.set_num_threads(4) # set global default n = kalign.get_num_threads() # query current default # or override per call aligned = kalign.align(sequences, n_threads=8) ``` Thread settings are thread-local, so different threads can use different defaults. ## Utilities (`kalign.utils`) Requires only NumPy (installed automatically): ```python import kalign aligned = kalign.align(sequences) arr = kalign.utils.to_array(aligned) # numpy array stats = kalign.utils.alignment_stats(aligned) # dict with gap_fraction, conservation, identity consensus = kalign.utils.consensus_sequence(aligned, threshold=0.7) matrix = kalign.utils.pairwise_identity_matrix(aligned) # numpy array trimmed = kalign.utils.remove_gap_columns(aligned) region = kalign.utils.trim_alignment(aligned, start=2, end=10) ``` ## Biopython Integration Requires `pip install kalign-python[biopython]`. ```python import kalign # Return a Biopython MultipleSeqAlignment aln = kalign.align(sequences, fmt="biopython", ids=["s1", "s2", "s3"]) print(aln.get_alignment_length()) # Write in various formats via Biopython from Bio import AlignIO AlignIO.write(aln, "output.clustal", "clustal") ``` ### I/O helpers (`kalign.io`) ```python sequences = kalign.io.read_fasta("input.fasta") sequences, ids = kalign.io.read_sequences("input.fasta") aligned = kalign.align(sequences) kalign.io.write_fasta(aligned, "output.fasta", ids=ids) kalign.io.write_clustal(aligned, "output.aln", ids=ids) kalign.io.write_stockholm(aligned, "output.sto", ids=ids) kalign.io.write_phylip(aligned, "output.phy", ids=ids) ``` ## scikit-bio Integration Requires `pip install kalign-python[skbio]`. ```python import kalign # Returns a TabularMSA of DNA, RNA, or Protein depending on seq_type aln = kalign.align(sequences, seq_type="dna", fmt="skbio") print(type(aln)) # ``` ## Constants ### Sequence Types | String | Constant | Description | |--------|----------|-------------| | `"auto"` | `kalign.AUTO` | Auto-detect (default) | | `"dna"` | `kalign.DNA` | DNA sequences | | `"rna"` | `kalign.RNA` | RNA sequences | | `"protein"` | `kalign.PROTEIN` | Protein sequences | | `"divergent"` | `kalign.PROTEIN_DIVERGENT` | Divergent protein sequences | | `"internal"` | `kalign.DNA_INTERNAL` | DNA with internal gap preference | ### Refinement Modes | String | Constant | Description | |--------|----------|-------------| | `"none"` | `kalign.REFINE_NONE` | No refinement (default) | | `"all"` | `kalign.REFINE_ALL` | Refine all columns | | `"confident"` | `kalign.REFINE_CONFIDENT` | Refine only confident columns | | `"inline"` | `kalign.REFINE_INLINE` | Inline refinement (disables parallelism) | ## Command-line Interface ```bash # Modes kalign-py -i sequences.fasta -o aligned.fasta # default mode kalign-py --fast -i sequences.fasta -o aligned.fasta # fast mode kalign-py --precise -i sequences.fasta -o aligned.fasta # precise mode # I/O options kalign-py -i sequences.fasta -o - --format clustal # stdout cat input.fa | kalign-py -i - -o aligned.fasta # stdin kalign-py -i sequences.fasta -o aligned.fasta --type protein # explicit type # Ensemble with POAR save/load kalign-py -i input.fa -o output.fa --ensemble 5 --save-poar consensus.poar kalign-py -i input.fa -o output.fa --load-poar consensus.poar --min-support 3 kalign-py --version ``` ## Development ```bash git clone https://github.com/TimoLassmann/kalign.git cd kalign uv pip install -e . uv run pytest tests/python/ -v ``` Requirements: Python 3.9+, CMake 3.18+, C++11 compiler, NumPy. ## Citation If you use Kalign in your research, please cite: > Lassmann, T. (2020). Kalign 3: multiple sequence alignment of large data sets. > *Bioinformatics*, 36(6), 1928-1929. > [doi:10.1093/bioinformatics/btz795](https://doi.org/10.1093/bioinformatics/btz795) ## License Apache License, Version 2.0. See [COPYING](COPYING). kalign-3.5.1/README.md000066400000000000000000000064571515023132300142530ustar00rootroot00000000000000[![CMake](https://github.com/TimoLassmann/kalign/actions/workflows/cmake.yml/badge.svg)](https://github.com/TimoLassmann/kalign/actions/workflows/cmake.yml) [![Python](https://github.com/TimoLassmann/kalign/actions/workflows/python.yml/badge.svg)](https://github.com/TimoLassmann/kalign/actions/workflows/python.yml) [![Build Python Wheels](https://github.com/TimoLassmann/kalign/actions/workflows/wheels.yml/badge.svg)](https://github.com/TimoLassmann/kalign/actions/workflows/wheels.yml) ![CodeQL](https://github.com/TimoLassmann/kalign/workflows/CodeQL/badge.svg) # Kalign Kalign is a fast multiple sequence alignment program for biological sequences. It aligns protein, DNA, and RNA sequences using a progressive alignment approach with multi-threading support. ## Installation ### From source Prerequisites: C compiler (GCC or Clang), CMake 3.18+, optionally OpenMP. ```bash mkdir build && cd build cmake .. make make test make install ``` On macOS, `brew install libomp` for OpenMP support. ### Zig build (alternative) Requires zig version 0.12. ```bash zig build ``` ### Python ```bash pip install kalign-python ``` See [README-python.md](README-python.md) for the full Python documentation. ## Usage ``` kalign -i -o ``` Kalign v3.5 has three modes: | Mode | Flag | Description | |------|------|-------------| | default | (none) | Best general-purpose. | | fast | `--fast` | Fastest. Same as kalign v3.4. | | precise | `--precise` | Highest accuracy, ~10x slower. | ### Examples ```bash # Align sequences kalign -i sequences.fa -o aligned.fa # Fast mode kalign --fast -i sequences.fa -o aligned.fa # Precise mode (ensemble + realign) kalign --precise -i sequences.fa -o aligned.fa # Read from stdin cat input.fa | kalign -i - -o aligned.fa # Combine multiple input files kalign seqsA.fa seqsB.fa -o combined.fa # Save ensemble consensus for re-thresholding kalign --precise -i seqs.fa -o out.fa --save-poar consensus.poar kalign -i seqs.fa -o out2.fa --load-poar consensus.poar --min-support 3 ``` ### Options ``` --format Output format: fasta, msf, clu. [fasta] --type Sequence type: protein, dna, rna, divergent. [auto] --gpo Gap open penalty. [auto] --gpe Gap extension penalty. [auto] --tgpe Terminal gap extension penalty. [auto] --ensemble N Run N ensemble alignments. [off] --refine Refinement: none, all, confident. [none] -n Number of threads. [auto] ``` ### Output formats ```bash kalign -i input.fa -f msf -o output.msf kalign -i input.fa -f clu -o output.clu ``` ## C library Link Kalign into your C/C++ project: ```cmake find_package(kalign) target_link_libraries( kalign::kalign) ``` Or include directly: ```cmake add_subdirectory(/kalign EXCLUDE_FROM_ALL) target_link_libraries( kalign::kalign) ``` ## Benchmarks ### Balibase ![Balibase_scores](https://user-images.githubusercontent.com/8110320/198513840-0e08a634-bb41-4826-bd58-7fc66eae1054.jpeg) ### Bralibase ![Bralibase_scores](https://user-images.githubusercontent.com/8110320/198513850-00e5037f-355f-45ec-828f-ed8d47497272.jpeg) ## Citation Lassmann, Timo. "Kalign 3: multiple sequence alignment of large data sets." Bioinformatics (2019). [DOI](https://doi.org/10.1093/bioinformatics/btz795) ## License Apache License, Version 2.0. See [COPYING](COPYING). kalign-3.5.1/benchmarks/000077500000000000000000000000001515023132300150755ustar00rootroot00000000000000kalign-3.5.1/benchmarks/__init__.py000066400000000000000000000001071515023132300172040ustar00rootroot00000000000000"""Kalign benchmark suite for alignment quality regression testing.""" kalign-3.5.1/benchmarks/__main__.py000066400000000000000000000001201515023132300171600ustar00rootroot00000000000000"""Entry point for `python -m benchmarks`.""" from .runner import main main() kalign-3.5.1/benchmarks/analysis.py000066400000000000000000000533721515023132300173040ustar00rootroot00000000000000"""RV11 alignment structure analysis. Compares gap structure and alignment geometry between kalign, reference, and external tools (mafft, muscle, clustalo) to understand HOW alignments differ structurally — not just score differences. Usage: # Locally (kalign only, external tools skipped if not installed): uv run python -m benchmarks.analysis # Inside container (has mafft/muscle/clustalo): python -m benchmarks.analysis # Specific dataset: python -m benchmarks.analysis --dataset balibase_RV11 # Write CSV: python -m benchmarks.analysis --csv benchmarks/results/gap_analysis.csv """ import argparse import csv import json import re import statistics import sys import tempfile from dataclasses import dataclass, fields from pathlib import Path from typing import Dict, List, Optional, Tuple from .datasets import balibase_cases, balibase_is_available RESULTS_DIR = Path(__file__).parent / "results" # --------------------------------------------------------------------------- # MSF parser (reference alignments are in GCG MSF format) # --------------------------------------------------------------------------- def parse_msf(path: Path) -> Dict[str, str]: """Parse a GCG MSF file into {name: aligned_sequence}.""" text = path.read_text() # Split at "//" separator parts = text.split("//") if len(parts) < 2: raise ValueError(f"No // separator found in {path}") body = parts[1] seqs: Dict[str, List[str]] = {} for line in body.splitlines(): line = line.strip() if not line: continue tokens = line.split() if len(tokens) < 2: continue name = tokens[0] # Sequence characters (may contain dots for gaps) seq_parts = "".join(tokens[1:]) seqs.setdefault(name, []).append(seq_parts) # Join blocks and normalise: dots → dashes, remove whitespace result = {} for name, blocks in seqs.items(): seq = "".join(blocks).replace(".", "-").upper() result[name] = seq return result # --------------------------------------------------------------------------- # FASTA parser (kalign/tool outputs) # --------------------------------------------------------------------------- def parse_fasta(path: Path) -> Dict[str, str]: """Parse a FASTA file into {name: sequence}.""" seqs: Dict[str, str] = {} current = None parts: List[str] = [] for line in path.read_text().splitlines(): line = line.strip() if line.startswith(">"): if current is not None: seqs[current] = "".join(parts).upper() current = line[1:].split()[0] parts = [] elif current is not None: parts.append(line) if current is not None: seqs[current] = "".join(parts).upper() return seqs # --------------------------------------------------------------------------- # Gap structure metrics # --------------------------------------------------------------------------- @dataclass class GapStats: """Gap structure metrics for one alignment.""" n_seqs: int alignment_length: int mean_seq_length: float # unaligned (non-gap chars) expansion_factor: float # alignment_length / mean_seq_length total_gaps: int gap_fraction: float # total_gaps / (n_seqs * alignment_length) n_gap_blocks: int mean_gap_block_len: float mean_terminal_gap: float # average leading+trailing gap per sequence mean_internal_gap: float # average total internal gap chars per sequence n_gappy_columns: int # columns where >50% of sequences have a gap gappy_column_fraction: float def compute_gap_stats(seqs: Dict[str, str]) -> GapStats: """Compute gap structure metrics from aligned sequences.""" sequences = list(seqs.values()) n_seqs = len(sequences) if n_seqs == 0: return GapStats(0, 0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0) aln_len = len(sequences[0]) # Unaligned lengths (non-gap characters) ungapped_lens = [len(s.replace("-", "")) for s in sequences] mean_seq_len = statistics.mean(ungapped_lens) expansion = aln_len / mean_seq_len if mean_seq_len > 0 else 0.0 total_gaps = sum(s.count("-") for s in sequences) total_chars = n_seqs * aln_len gap_frac = total_gaps / total_chars if total_chars > 0 else 0.0 # Gap blocks and lengths all_block_lens: List[int] = [] terminal_gaps: List[int] = [] internal_gaps: List[int] = [] for seq in sequences: # Find all gap blocks blocks = [(m.start(), m.end()) for m in re.finditer(r"-+", seq)] all_block_lens.extend(m.end() - m.start() for m in re.finditer(r"-+", seq)) # Terminal: leading and trailing leading = len(seq) - len(seq.lstrip("-")) trailing = len(seq) - len(seq.rstrip("-")) terminal_gaps.append(leading + trailing) # Internal: everything that's not leading/trailing internal = sum(e - s for s, e in blocks) internal -= leading + trailing internal_gaps.append(max(0, internal)) n_gap_blocks = len(all_block_lens) mean_block_len = statistics.mean(all_block_lens) if all_block_lens else 0.0 mean_terminal = statistics.mean(terminal_gaps) mean_internal = statistics.mean(internal_gaps) # Gappy columns (>50% gaps) n_gappy = 0 for col in range(aln_len): gaps_in_col = sum(1 for s in sequences if s[col] == "-") if gaps_in_col > n_seqs / 2: n_gappy += 1 return GapStats( n_seqs=n_seqs, alignment_length=aln_len, mean_seq_length=mean_seq_len, expansion_factor=expansion, total_gaps=total_gaps, gap_fraction=gap_frac, n_gap_blocks=n_gap_blocks, mean_gap_block_len=mean_block_len, mean_terminal_gap=mean_terminal, mean_internal_gap=mean_internal, n_gappy_columns=n_gappy, gappy_column_fraction=n_gappy / aln_len if aln_len > 0 else 0.0, ) # --------------------------------------------------------------------------- # Alignment generation helpers # --------------------------------------------------------------------------- def _align_kalign(unaligned: Path, output: Path, seq_type: str) -> None: """Run kalign via Python API.""" import kalign kalign.align_file_to_file( str(unaligned), str(output), format="fasta", seq_type=seq_type, ) def _align_external(unaligned: Path, output: Path, tool: str) -> bool: """Run an external tool. Returns True if successful.""" import shutil import subprocess if shutil.which(tool) is None: return False try: if tool == "mafft": with open(output, "w") as f: subprocess.run( ["mafft", "--auto", str(unaligned)], stdout=f, stderr=subprocess.PIPE, check=True, ) elif tool == "clustalo": subprocess.run( ["clustalo", "-i", str(unaligned), "-o", str(output), "--outfmt=fasta", "--force"], capture_output=True, check=True, ) elif tool == "muscle": subprocess.run( ["muscle", "-align", str(unaligned), "-output", str(output)], capture_output=True, check=True, ) return True except (subprocess.CalledProcessError, FileNotFoundError): return False # --------------------------------------------------------------------------- # Per-case analysis row # --------------------------------------------------------------------------- @dataclass class CaseRow: family: str method: str # Scores from results JSON (NaN if not available) recall: float precision: float f1: float tc: float # Gap stats alignment_length: int expansion_factor: float gap_fraction: float n_gap_blocks: int mean_gap_block_len: float mean_terminal_gap: float mean_internal_gap: float n_gappy_columns: int gappy_column_fraction: float # --------------------------------------------------------------------------- # Load frozen scores from full_comparison.json # --------------------------------------------------------------------------- def load_scores(json_path: Path, dataset_filter: str = "balibase_RV11") -> Dict[Tuple[str, str], dict]: """Load {(family, method_key): scores} from results JSON. method_key is 'kalign' for python_api/refine=none, or the external tool name. """ with open(json_path) as f: data = json.load(f) scores: Dict[Tuple[str, str], dict] = {} for r in data["results"]: if dataset_filter and r["dataset"] != dataset_filter: continue if r["method"] == "python_api" and r["refine"] == "none": key = (r["family"], "kalign") elif r["method"] in ("clustalo", "mafft", "muscle"): key = (r["family"], r["method"]) else: continue scores[key] = { "recall": r.get("recall", float("nan")), "precision": r.get("precision", float("nan")), "f1": r.get("f1", float("nan")), "tc": r.get("tc", float("nan")), } return scores # --------------------------------------------------------------------------- # Main analysis # --------------------------------------------------------------------------- def analyse_rv11( dataset_filter: str = "balibase_RV11", external_tools: Optional[List[str]] = None, ) -> List[CaseRow]: """Run gap analysis on all RV11 cases. Returns list of CaseRow.""" if external_tools is None: external_tools = ["mafft", "muscle", "clustalo"] if not balibase_is_available(): print("BAliBASE not downloaded. Run: uv run python -m benchmarks --download-only") return [] cases = [c for c in balibase_cases() if c.dataset == dataset_filter] if not cases: print(f"No cases found for {dataset_filter}") return [] # Load frozen scores json_path = RESULTS_DIR / "full_comparison.json" scores = load_scores(json_path, dataset_filter) if json_path.exists() else {} rows: List[CaseRow] = [] for case in cases: print(f" {case.family} ...", end="", flush=True) # --- Reference alignment --- ref_seqs = parse_msf(case.reference) ref_stats = compute_gap_stats(ref_seqs) sc = scores.get((case.family, "reference"), {}) rows.append(CaseRow( family=case.family, method="reference", recall=1.0, precision=1.0, f1=1.0, tc=1.0, alignment_length=ref_stats.alignment_length, expansion_factor=ref_stats.expansion_factor, gap_fraction=ref_stats.gap_fraction, n_gap_blocks=ref_stats.n_gap_blocks, mean_gap_block_len=ref_stats.mean_gap_block_len, mean_terminal_gap=ref_stats.mean_terminal_gap, mean_internal_gap=ref_stats.mean_internal_gap, n_gappy_columns=ref_stats.n_gappy_columns, gappy_column_fraction=ref_stats.gappy_column_fraction, )) # --- Kalign --- with tempfile.TemporaryDirectory() as tmpdir: kalign_out = Path(tmpdir) / f"{case.family}_kalign.fa" _align_kalign(case.unaligned, kalign_out, case.seq_type) kalign_seqs = parse_fasta(kalign_out) kalign_stats = compute_gap_stats(kalign_seqs) sc = scores.get((case.family, "kalign"), {}) rows.append(CaseRow( family=case.family, method="kalign", recall=sc.get("recall", float("nan")), precision=sc.get("precision", float("nan")), f1=sc.get("f1", float("nan")), tc=sc.get("tc", float("nan")), alignment_length=kalign_stats.alignment_length, expansion_factor=kalign_stats.expansion_factor, gap_fraction=kalign_stats.gap_fraction, n_gap_blocks=kalign_stats.n_gap_blocks, mean_gap_block_len=kalign_stats.mean_gap_block_len, mean_terminal_gap=kalign_stats.mean_terminal_gap, mean_internal_gap=kalign_stats.mean_internal_gap, n_gappy_columns=kalign_stats.n_gappy_columns, gappy_column_fraction=kalign_stats.gappy_column_fraction, )) # --- External tools --- for tool in external_tools: with tempfile.TemporaryDirectory() as tmpdir: tool_out = Path(tmpdir) / f"{case.family}_{tool}.fa" ok = _align_external(case.unaligned, tool_out, tool) if not ok: continue tool_seqs = parse_fasta(tool_out) tool_stats = compute_gap_stats(tool_seqs) sc = scores.get((case.family, tool), {}) rows.append(CaseRow( family=case.family, method=tool, recall=sc.get("recall", float("nan")), precision=sc.get("precision", float("nan")), f1=sc.get("f1", float("nan")), tc=sc.get("tc", float("nan")), alignment_length=tool_stats.alignment_length, expansion_factor=tool_stats.expansion_factor, gap_fraction=tool_stats.gap_fraction, n_gap_blocks=tool_stats.n_gap_blocks, mean_gap_block_len=tool_stats.mean_gap_block_len, mean_terminal_gap=tool_stats.mean_terminal_gap, mean_internal_gap=tool_stats.mean_internal_gap, n_gappy_columns=tool_stats.n_gappy_columns, gappy_column_fraction=tool_stats.gappy_column_fraction, )) print(" done") return rows # --------------------------------------------------------------------------- # Output formatting # --------------------------------------------------------------------------- def print_table(rows: List[CaseRow]) -> None: """Print a summary table to stdout, grouped by family.""" if not rows: return families = sorted(set(r.family for r in rows)) methods = sorted(set(r.method for r in rows)) # Per-case comparison table hdr = f"{'Family':<10} {'Method':<10} {'Recall':>7} {'Prec':>7} {'F1':>7} {'AlnLen':>7} {'Expand':>7} {'GapFrac':>7} {'GapBlk':>7} {'MeanBL':>7} {'TermGap':>7} {'IntGap':>7} {'Gappy%':>7}" print("\n" + "=" * len(hdr)) print(hdr) print("-" * len(hdr)) for fam in families: fam_rows = sorted( [r for r in rows if r.family == fam], key=lambda r: (r.method != "reference", r.method != "kalign", r.method), ) for r in fam_rows: rec = f"{r.recall:.3f}" if r.recall == r.recall else " n/a" pre = f"{r.precision:.3f}" if r.precision == r.precision else " n/a" f1 = f"{r.f1:.3f}" if r.f1 == r.f1 else " n/a" print( f"{r.family:<10} {r.method:<10} {rec:>7} {pre:>7} {f1:>7} " f"{r.alignment_length:>7} {r.expansion_factor:>7.2f} " f"{r.gap_fraction:>7.3f} {r.n_gap_blocks:>7} " f"{r.mean_gap_block_len:>7.1f} {r.mean_terminal_gap:>7.1f} " f"{r.mean_internal_gap:>7.1f} {r.gappy_column_fraction:>7.3f}" ) print() # Aggregate summary by method print("=" * 80) print("AGGREGATE SUMMARY (means across all families)") print("-" * 80) fmt = "{:<10} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>8} {:>8}" print(fmt.format("Method", "Recall", "Prec", "F1", "Expand", "GapFrac", "MeanBL", "TermGap", "IntGap")) print("-" * 80) for method in ["reference", "kalign"] + [m for m in methods if m not in ("reference", "kalign")]: method_rows = [r for r in rows if r.method == method] if not method_rows: continue def safe_mean(vals): clean = [v for v in vals if v == v] # filter NaN return statistics.mean(clean) if clean else float("nan") rec = safe_mean([r.recall for r in method_rows]) pre = safe_mean([r.precision for r in method_rows]) f1 = safe_mean([r.f1 for r in method_rows]) exp = statistics.mean([r.expansion_factor for r in method_rows]) gf = statistics.mean([r.gap_fraction for r in method_rows]) mbl = statistics.mean([r.mean_gap_block_len for r in method_rows]) tg = statistics.mean([r.mean_terminal_gap for r in method_rows]) ig = statistics.mean([r.mean_internal_gap for r in method_rows]) rec_s = f"{rec:.3f}" if rec == rec else " n/a" pre_s = f"{pre:.3f}" if pre == pre else " n/a" f1_s = f"{f1:.3f}" if f1 == f1 else " n/a" print(fmt.format(method, rec_s, pre_s, f1_s, f"{exp:.2f}", f"{gf:.3f}", f"{mbl:.1f}", f"{tg:.1f}", f"{ig:.1f}")) # Correlation analysis: precision vs expansion factor for kalign kalign_rows = [r for r in rows if r.method == "kalign" and r.precision == r.precision] if len(kalign_rows) >= 5: print("\n" + "=" * 80) print("CORRELATION: kalign precision vs gap metrics") print("-" * 80) # Simple Pearson correlation def pearson(xs, ys): n = len(xs) if n < 3: return float("nan") mx, my = statistics.mean(xs), statistics.mean(ys) num = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) dx = sum((x - mx) ** 2 for x in xs) ** 0.5 dy = sum((y - my) ** 2 for y in ys) ** 0.5 return num / (dx * dy) if dx > 0 and dy > 0 else float("nan") prec = [r.precision for r in kalign_rows] metrics = [ ("expansion_factor", [r.expansion_factor for r in kalign_rows]), ("gap_fraction", [r.gap_fraction for r in kalign_rows]), ("mean_gap_block_len", [r.mean_gap_block_len for r in kalign_rows]), ("mean_terminal_gap", [r.mean_terminal_gap for r in kalign_rows]), ("mean_internal_gap", [r.mean_internal_gap for r in kalign_rows]), ("gappy_column_fraction", [r.gappy_column_fraction for r in kalign_rows]), ] for name, vals in metrics: r = pearson(prec, vals) print(f" precision vs {name:<25}: r = {r:+.3f}") # Also: kalign expansion vs reference expansion ref_rows = {r.family: r for r in rows if r.method == "reference"} kalign_dict = {r.family: r for r in kalign_rows} common = sorted(set(ref_rows) & set(kalign_dict)) if common: print(f"\n Expansion factor comparison (kalign vs reference, n={len(common)}):") over = [f for f in common if kalign_dict[f].expansion_factor > ref_rows[f].expansion_factor * 1.05] under = [f for f in common if kalign_dict[f].expansion_factor < ref_rows[f].expansion_factor * 0.95] same = [f for f in common if f not in over and f not in under] print(f" Over-expanded (>5%): {len(over)}") print(f" Under-expanded: {len(under)}") print(f" Similar (+/-5%): {len(same)}") # Relative expansion ratio correlated with precision ratios = [kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01) for f in common] precs = [kalign_dict[f].precision for f in common] r_ratio = pearson(ratios, precs) print(f" Correlation (expand_ratio vs precision): r = {r_ratio:+.3f}") if over: print(f"\n Worst over-expanded cases (kalign expand / ref expand):") ranked = sorted(over, key=lambda f: kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01), reverse=True) for f in ranked[:10]: ke = kalign_dict[f].expansion_factor re = ref_rows[f].expansion_factor kp = kalign_dict[f].precision print(f" {f}: kalign={ke:.2f} ref={re:.2f} ratio={ke/re:.2f} precision={kp:.3f}") if under: print(f"\n Worst under-expanded cases (kalign expand / ref expand):") ranked = sorted(under, key=lambda f: kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01)) for f in ranked[:10]: ke = kalign_dict[f].expansion_factor re = ref_rows[f].expansion_factor kp = kalign_dict[f].precision print(f" {f}: kalign={ke:.2f} ref={re:.2f} ratio={ke/re:.2f} precision={kp:.3f}") def write_csv(rows: List[CaseRow], path: str) -> None: """Write analysis results to CSV.""" fieldnames = [f.name for f in fields(CaseRow)] Path(path).parent.mkdir(parents=True, exist_ok=True) with open(path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for row in rows: d = {fn: getattr(row, fn) for fn in fieldnames} writer.writerow(d) print(f"\nCSV written to {path}") # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="RV11 alignment structure analysis", prog="python -m benchmarks.analysis", ) parser.add_argument( "--dataset", default="balibase_RV11", help="Dataset filter (default: balibase_RV11)", ) parser.add_argument( "--csv", default="", help="Write results to CSV file", ) parser.add_argument( "--no-external", action="store_true", help="Skip external tools (mafft, muscle, clustalo)", ) args = parser.parse_args() external = [] if args.no_external else ["mafft", "muscle", "clustalo"] print(f"Analysing {args.dataset} alignment structure...") rows = analyse_rv11(dataset_filter=args.dataset, external_tools=external) if not rows: sys.exit(1) print_table(rows) if args.csv: write_csv(rows, args.csv) if __name__ == "__main__": main() kalign-3.5.1/benchmarks/app.py000066400000000000000000000562061515023132300162400ustar00rootroot00000000000000"""Dash app for running kalign benchmarks and viewing results. Usage: python -m benchmarks.app [--port 8050] """ import argparse import json import sys import threading from pathlib import Path try: import dash from dash import dcc, html, dash_table, callback_context from dash.dependencies import Input, Output, State import plotly.express as px import pandas as pd except ImportError: print("Dash visualization requires extra dependencies:") print(" pip install dash plotly pandas") sys.exit(1) from .datasets import DATASETS, get_cases, download_dataset from .scoring import run_case RESULTS_DIR = Path(__file__).parent / "results" RESULTS_DIR.mkdir(parents=True, exist_ok=True) # --------------------------------------------------------------------------- # Configuration presets — the 4 varieties of kalign # --------------------------------------------------------------------------- CONFIG_PRESETS = { "Kalign (default)": {"refine": "none", "ensemble": 0}, "Kalign + Refinement": {"refine": "confident", "ensemble": 0}, "Ensemble (8 runs)": {"refine": "none", "ensemble": 8}, "Ensemble (12 runs)": {"refine": "none", "ensemble": 12}, "Clustal Omega": {"method": "clustalo", "refine": "none", "ensemble": 0}, "MAFFT": {"method": "mafft", "refine": "none", "ensemble": 0}, "MUSCLE": {"method": "muscle", "refine": "none", "ensemble": 0}, } # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _available_datasets(): """Return list of (name, available, n_cases) tuples.""" info = [] for name, ds in DATASETS.items(): avail = ds["is_available"]() n = len(ds["cases"]()) if avail else 0 info.append((name, avail, n)) return info def _load_json(path): with open(path) as f: return json.load(f) _EXTERNAL_LABELS = { "clustalo": "Clustal Omega", "mafft": "MAFFT", "muscle": "MUSCLE", } def _config_label(row): """Build a human-readable configuration label from result fields.""" method = row.get("method", "python_api") if method in _EXTERNAL_LABELS: return _EXTERNAL_LABELS[method] ensemble = row.get("ensemble", 0) refine = row.get("refine", "none") if ensemble and ensemble > 0: label = f"Ensemble ({ensemble})" elif refine and refine != "none": label = f"Kalign + {refine}" else: label = "Kalign" return label def _results_to_df(results): """Convert list of AlignmentResult dicts to DataFrame.""" df = pd.DataFrame(results) if "error" in df.columns: df = df[df["error"].isna() | (df["error"] == "None") | (df["error"].isnull())] # Ensure columns exist (backward compat with old JSON files) if "ensemble" not in df.columns: df["ensemble"] = 0 df["ensemble"] = df["ensemble"].fillna(0).astype(int) for col in ("recall", "precision", "f1", "tc"): if col not in df.columns: df[col] = 0.0 df[col] = df[col].fillna(0.0) # Add config label for plotting df["config"] = df.apply(_config_label, axis=1) return df def _build_figures(df): """Build plotly figures from a results DataFrame.""" figs = [] if df.empty: return figs # Filter out errors clean = df[df["sp_score"] > 0].copy() if "sp_score" in df.columns else df if clean.empty: return figs # Ensure new columns exist (backward compat with old JSON files) for col in ("recall", "precision", "f1", "tc"): if col not in clean.columns: clean[col] = 0.0 # Determine color column: use 'config' if multiple configs, else 'method' configs = clean["config"].nunique() color_col = "config" if configs > 1 else "method" # bali_score-compatible metrics has_recall = clean["recall"].sum() > 0 if has_recall: # SP score (bali_score compatible) = recall fig_sp = px.box( clean, x="dataset", y="recall", color=color_col, title="SP Score by Dataset (bali_score compatible)", labels={"recall": "SP Score", "dataset": "Dataset", "config": "Configuration"}, ) fig_sp.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("sp_box", fig_sp)) # TC score fig_tc = px.box( clean, x="dataset", y="tc", color=color_col, title="TC Score by Dataset", labels={"tc": "TC Score", "dataset": "Dataset", "config": "Configuration"}, ) fig_tc.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("tc_box", fig_tc)) # Precision fig_prec = px.box( clean, x="dataset", y="precision", color=color_col, title="Precision by Dataset", labels={"precision": "Precision", "dataset": "Dataset", "config": "Configuration"}, ) fig_prec.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("precision_box", fig_prec)) # F1 fig_f1 = px.box( clean, x="dataset", y="f1", color=color_col, title="F1 Score by Dataset", labels={"f1": "F1", "dataset": "Dataset", "config": "Configuration"}, ) fig_f1.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("f1_box", fig_f1)) else: # Fallback: legacy SP score (not bali_score compatible) fig_box = px.box( clean, x="dataset", y="sp_score", color=color_col, title="SP Score Distribution by Dataset (legacy)", labels={"sp_score": "SP Score", "dataset": "Dataset", "config": "Configuration"}, ) fig_box.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("sp_box", fig_box)) # Strip plot by family hover_cols = ["family", "wall_time", "refine", "ensemble"] if has_recall: hover_cols.extend(["recall", "precision", "f1", "tc"]) y_strip = "recall" if has_recall else "sp_score" fig_scatter = px.strip( clean, x="dataset", y=y_strip, color=color_col, hover_data=hover_cols, title="SP Score per Family", labels={"recall": "SP Score", "sp_score": "SP Score (legacy)", "config": "Configuration"}, ) fig_scatter.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("scatter", fig_scatter)) # Timing fig_time = px.box( clean, x="dataset", y="wall_time", color=color_col, title="Alignment Time by Dataset", labels={"wall_time": "Wall Time (s)", "dataset": "Dataset", "config": "Configuration"}, ) fig_time.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("timing", fig_time)) # Summary table by config x dataset if configs > 1: agg_dict = { "mean_sp": ("sp_score", "mean"), "median_sp": ("sp_score", "median"), "mean_time": ("wall_time", "mean"), "n_cases": ("sp_score", "count"), } if has_recall: agg_dict.update({ "mean_recall": ("recall", "mean"), "mean_precision": ("precision", "mean"), "mean_f1": ("f1", "mean"), "mean_tc": ("tc", "mean"), }) summary = clean.groupby(["config", "dataset"]).agg(**agg_dict).reset_index() summary = summary.round(3) y_col = "mean_recall" if has_recall else "mean_sp" y_label = "Mean SP Score" if has_recall else "Mean SP Score (legacy)" fig_summary = px.bar( summary, x="dataset", y=y_col, color="config", barmode="group", title=f"{y_label} by Dataset and Configuration", labels={y_col: y_label, "dataset": "Dataset", "config": "Configuration"}, ) fig_summary.update_layout(legend=dict(orientation="h", y=-0.2)) figs.append(("summary_bar", fig_summary)) return figs # --------------------------------------------------------------------------- # State shared between callbacks and the benchmark thread # --------------------------------------------------------------------------- _run_state = { "running": False, "progress": "", "results": [], "done": False, } def _run_in_thread(dataset, method, binary, max_cases, n_threads, configs): """Run benchmarks in a background thread so the UI stays responsive.""" global _run_state _run_state["running"] = True _run_state["done"] = False _run_state["results"] = [] _run_state["progress"] = f"Loading {dataset} cases..." try: cases = get_cases(dataset, max_cases=max_cases if max_cases > 0 else None) if not cases: _run_state["progress"] = f"No cases found for {dataset}. Try downloading first." _run_state["running"] = False _run_state["done"] = True return total = len(cases) * len(configs) done = 0 for cfg_name, cfg in configs: cfg_method = cfg.get("method", method) refine = cfg["refine"] ensemble = cfg["ensemble"] for case in cases: done += 1 _run_state["progress"] = ( f"[{done}/{total}] {cfg_name}: {case.family}..." ) result = run_case( case, method=cfg_method, binary=binary, n_threads=n_threads, refine=refine, ensemble=ensemble, ) _run_state["results"].append(result.to_dict()) # Auto-save import time as _time save_path = RESULTS_DIR / f"run_{_time.strftime('%Y%m%d_%H%M%S')}.json" data = { "timestamp": _time.strftime("%Y-%m-%dT%H:%M:%S"), "results": _run_state["results"], } with open(save_path, "w") as f: json.dump(data, f, indent=2) _run_state["progress"] = f"Done! {done} alignments scored. Saved to {save_path.name}" except Exception as e: _run_state["progress"] = f"Error: {e}" finally: _run_state["running"] = False _run_state["done"] = True # --------------------------------------------------------------------------- # App layout # --------------------------------------------------------------------------- def create_app(): app = dash.Dash(__name__) app.title = "Kalign Benchmarks" # Discover saved result files def _saved_files(): return sorted(RESULTS_DIR.glob("*.json"), reverse=True) ds_info = _available_datasets() app.layout = html.Div([ html.H1("Kalign Benchmark Dashboard"), # --- Run controls --- html.Div([ html.H3("Run Benchmark"), html.Div([ # Row 1: Dataset, method, binary html.Div([ html.Div([ html.Label("Dataset"), dcc.Dropdown( id="dataset-dropdown", options=[ {"label": f"{name} ({'available' if avail else 'needs download'}, {n} cases)", "value": name} for name, avail, n in ds_info ], value="balibase", ), ], style={"width": "30%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Label("Method"), dcc.RadioItems( id="method-radio", options=[ {"label": " Python API", "value": "python_api"}, {"label": " C binary", "value": "cli"}, ], value="python_api", ), ], style={"width": "12%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Label("C binary path"), dcc.Input(id="binary-input", type="text", value="build/src/kalign", style={"width": "160px"}), ], style={"width": "18%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Label("Max cases (0=all)"), dcc.Input(id="max-cases-input", type="number", value=0, min=0, style={"width": "80px"}), ], style={"width": "10%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Label("Threads"), dcc.Input(id="threads-input", type="number", value=1, min=1, style={"width": "60px"}), ], style={"width": "8%", "display": "inline-block", "verticalAlign": "top"}), ]), html.Hr(style={"margin": "10px 0"}), # Row 2: Configuration presets html.Div([ html.Div([ html.Label("Configurations to run"), dcc.Checklist( id="config-checklist", options=[{"label": f" {name}", "value": name} for name in CONFIG_PRESETS], value=["Kalign (default)"], style={"lineHeight": "2"}, ), ], style={"width": "35%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Label("Custom ensemble runs"), dcc.Input(id="custom-ensemble-input", type="number", value=8, min=2, max=32, style={"width": "80px"}), html.Br(), html.Label("Custom refine mode", style={"marginTop": "8px"}), dcc.Dropdown( id="custom-refine-dropdown", options=[ {"label": "none", "value": "none"}, {"label": "confident", "value": "confident"}, {"label": "all", "value": "all"}, ], value="none", style={"width": "140px"}, ), html.Button("+ Add custom config", id="add-custom-btn", n_clicks=0, style={"marginTop": "8px"}), ], style={"width": "25%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Br(), html.Button("Download Dataset", id="download-btn", n_clicks=0, style={"marginRight": "10px"}), html.Br(), html.Br(), html.Button("Run Benchmark", id="run-btn", n_clicks=0, style={"backgroundColor": "#4CAF50", "color": "white", "border": "none", "padding": "12px 24px", "cursor": "pointer", "fontSize": "14px"}), ], style={"width": "20%", "display": "inline-block", "verticalAlign": "top"}), ]), ]), html.Div(id="progress-text", style={"marginTop": "10px", "fontStyle": "italic"}), dcc.Interval(id="progress-interval", interval=1000, n_intervals=0, disabled=True), # Hidden store for custom configs dcc.Store(id="custom-configs-store", data=[]), ], style={"padding": "15px", "backgroundColor": "#f5f5f5", "borderRadius": "8px", "marginBottom": "20px"}), # --- Load saved results --- html.Div([ html.H3("View Results"), html.Div([ html.Div([ html.Label("Saved result files"), dcc.Dropdown(id="results-dropdown", multi=True), ], style={"width": "60%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), html.Div([ html.Br(), html.Button("Refresh file list", id="refresh-btn", n_clicks=0, style={"marginRight": "10px"}), html.Button("Load & Plot", id="load-btn", n_clicks=0), ], style={"width": "30%", "display": "inline-block", "verticalAlign": "top"}), ]), ], style={"padding": "15px", "backgroundColor": "#f0f8ff", "borderRadius": "8px", "marginBottom": "20px"}), # --- Charts --- html.Div(id="charts-container"), # --- Table --- html.Div(id="table-container"), ], style={"maxWidth": "1200px", "margin": "0 auto", "padding": "20px", "fontFamily": "sans-serif"}) # --- Callbacks --- @app.callback( Output("results-dropdown", "options"), Input("refresh-btn", "n_clicks"), Input("progress-interval", "n_intervals"), ) def refresh_file_list(_n1, _n2): files = _saved_files() return [{"label": f.name, "value": str(f)} for f in files] @app.callback( Output("custom-configs-store", "data"), Output("config-checklist", "options"), Output("config-checklist", "value"), Input("add-custom-btn", "n_clicks"), State("custom-ensemble-input", "value"), State("custom-refine-dropdown", "value"), State("custom-configs-store", "data"), State("config-checklist", "options"), State("config-checklist", "value"), prevent_initial_call=True, ) def add_custom_config(_n, ensemble_n, refine, custom_cfgs, options, selected): """Add a custom configuration to the checklist.""" ensemble_n = ensemble_n or 0 refine = refine or "none" if ensemble_n > 0: name = f"Ensemble ({ensemble_n}) + {refine}" else: name = f"Custom (refine={refine})" cfg = {"refine": refine, "ensemble": ensemble_n} # Avoid duplicates for existing in custom_cfgs: if existing["name"] == name: return custom_cfgs, options, selected custom_cfgs.append({"name": name, **cfg}) options.append({"label": f" {name}", "value": name}) selected.append(name) return custom_cfgs, options, selected @app.callback( Output("progress-text", "children"), Output("progress-interval", "disabled"), Input("run-btn", "n_clicks"), Input("download-btn", "n_clicks"), Input("progress-interval", "n_intervals"), State("dataset-dropdown", "value"), State("method-radio", "value"), State("binary-input", "value"), State("max-cases-input", "value"), State("threads-input", "value"), State("config-checklist", "value"), State("custom-configs-store", "data"), prevent_initial_call=True, ) def handle_run_or_download(run_clicks, dl_clicks, _n_intervals, dataset, method, binary, max_cases, n_threads, selected_configs, custom_cfgs): triggered = callback_context.triggered_id if triggered == "download-btn" and not _run_state["running"]: try: download_dataset(dataset) return f"Downloaded {dataset}.", True except Exception as e: return f"Download error: {e}", True if triggered == "run-btn" and not _run_state["running"]: if not selected_configs: return "Select at least one configuration.", True # Build config list from presets + custom configs = [] custom_by_name = {c["name"]: c for c in (custom_cfgs or [])} for name in selected_configs: if name in CONFIG_PRESETS: configs.append((name, CONFIG_PRESETS[name])) elif name in custom_by_name: c = custom_by_name[name] configs.append((name, {"refine": c["refine"], "ensemble": c["ensemble"]})) if not configs: return "No valid configurations selected.", True t = threading.Thread( target=_run_in_thread, args=(dataset, method, binary or "build/src/kalign", max_cases or 0, n_threads or 1, configs), daemon=True, ) t.start() return f"Starting {len(configs)} configuration(s)...", False # Polling progress if _run_state["running"]: return _run_state["progress"], False if _run_state["done"]: return _run_state["progress"], True return dash.no_update, dash.no_update @app.callback( Output("charts-container", "children"), Output("table-container", "children"), Input("load-btn", "n_clicks"), Input("progress-interval", "n_intervals"), State("results-dropdown", "value"), prevent_initial_call=True, ) def update_charts(load_clicks, _n_intervals, selected_files): triggered = callback_context.triggered_id # If benchmark just finished, show those results immediately if triggered == "progress-interval" and _run_state["done"] and _run_state["results"]: df = _results_to_df(_run_state["results"]) elif triggered == "load-btn" and selected_files: all_results = [] for path in selected_files: data = _load_json(path) for r in data["results"]: r["source"] = Path(path).stem all_results.extend(data["results"]) df = _results_to_df(all_results) else: return dash.no_update, dash.no_update if df.empty: return html.P("No results to display."), "" charts = [] for fig_id, fig in _build_figures(df): charts.append(dcc.Graph(id=fig_id, figure=fig)) # Table display_cols = [c for c in df.columns if c not in ("error",)] table = dash_table.DataTable( data=df.to_dict("records"), columns=[{"name": c, "id": c} for c in display_cols], filter_action="native", sort_action="native", page_size=50, style_table={"overflowX": "auto"}, style_cell={"textAlign": "left", "padding": "5px"}, style_header={"fontWeight": "bold"}, ) return charts, html.Div([html.H3("Results Table"), table]) return app def main(): parser = argparse.ArgumentParser( description="Kalign benchmark dashboard", prog="python -m benchmarks.app", ) parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)") parser.add_argument("--port", type=int, default=8050, help="Port (default: 8050)") args = parser.parse_args() app = create_app() print(f"Starting dashboard at http://{args.host}:{args.port}") app.run(debug=False, host=args.host, port=args.port) if __name__ == "__main__": main() kalign-3.5.1/benchmarks/combined_improvements.py000066400000000000000000000224031515023132300220400ustar00rootroot00000000000000"""Measure cumulative/additive effect of kalign improvements on BAliBASE. Tests individual features and their combinations to determine additivity: Individual features (from baseline): 1. baseline: vsm_amax=0, no extras 2. +vsm: vsm_amax=2.0 only 3. +ref: refine=confident only (vsm=0) 4. +re1: realign=1 only (vsm=0) Stacking (additive): 5. +vsm+ref: vsm + refine 6. +vsm+re1: vsm + realign=1 (no refine) 7. +vsm+ref+re1: vsm + refine + realign=1 Ensemble path: 8. +ens3: ensemble=3 (uses vsm, internal post-refine) 9. +ens5: ensemble=5 10. +ens5+adapt: ensemble=5 + adaptive_budget Usage: uv run python -m benchmarks.combined_improvements -j 4 uv run python -m benchmarks.combined_improvements -j 4 --categories RV11 RV12 """ import argparse import json import statistics import tempfile import time from collections import defaultdict from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path import kalign from .datasets import get_cases from .scoring import parse_balibase_xml CONFIGS = [ # --- Individual features (from baseline) --- { "name": "baseline", "desc": "old defaults (vsm=0, no extras)", "vsm_amax": 0.0, "dist_scale": 0.0, "refine": "none", "ensemble": 0, "adaptive_budget": False, "realign": 0, }, { "name": "+vsm", "desc": "vsm_amax=2.0 only", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "none", "ensemble": 0, "adaptive_budget": False, "realign": 0, }, { "name": "+ref", "desc": "refine=confident only (vsm=0)", "vsm_amax": 0.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 0, "adaptive_budget": False, "realign": 0, }, { "name": "+re1", "desc": "realign=1 only (vsm=0)", "vsm_amax": 0.0, "dist_scale": 0.0, "refine": "none", "ensemble": 0, "adaptive_budget": False, "realign": 1, }, # --- Stacking (additive combinations) --- { "name": "+vsm+ref", "desc": "vsm + refine", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 0, "adaptive_budget": False, "realign": 0, }, { "name": "+vsm+re1", "desc": "vsm + realign=1 (no refine)", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "none", "ensemble": 0, "adaptive_budget": False, "realign": 1, }, { "name": "+vsm+ref+re1", "desc": "vsm + refine + realign=1 (full stack)", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 0, "adaptive_budget": False, "realign": 1, }, # --- Ensemble path --- { "name": "+ens3", "desc": "ensemble=3 (uses vsm, internal post-refine)", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 3, "adaptive_budget": False, "realign": 0, }, { "name": "+ens5", "desc": "ensemble=5 (uses vsm, internal post-refine)", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 5, "adaptive_budget": False, "realign": 0, }, { "name": "+ens5+adapt", "desc": "ensemble=5 + adaptive_budget", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 5, "adaptive_budget": True, "realign": 0, }, # --- Ensemble + post-realign --- { "name": "+ens3+re1", "desc": "ensemble=3 + post-ensemble realign=1", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 3, "adaptive_budget": False, "realign": 1, }, { "name": "+ens5+re1", "desc": "ensemble=5 + post-ensemble realign=1", "vsm_amax": 2.0, "dist_scale": 0.0, "refine": "confident", "ensemble": 5, "adaptive_budget": False, "realign": 1, }, ] def _score_case(case, output_path): xml_path = case.reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) return kalign.compare_detailed(str(case.reference), str(output_path)) def _run_one(args): case, config = args with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: t0 = time.perf_counter() kalign.align_file_to_file( str(case.unaligned), tmp_path, format="fasta", seq_type=case.seq_type, vsm_amax=config["vsm_amax"], dist_scale=config["dist_scale"], refine=config["refine"], ensemble=config["ensemble"], adaptive_budget=config["adaptive_budget"], realign=config.get("realign", 0), ) wall_time = time.perf_counter() - t0 sp_score = kalign.compare(str(case.reference), tmp_path) scores = _score_case(case, tmp_path) return { "family": case.family, "dataset": case.dataset, "config": config["name"], "sp_score": sp_score, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], "wall_time": wall_time, } except Exception as e: return { "family": case.family, "dataset": case.dataset, "config": config["name"], "sp_score": 0, "recall": 0, "precision": 0, "f1": 0, "tc": 0, "wall_time": 0, "error": str(e), } finally: Path(tmp_path).unlink(missing_ok=True) def main(): parser = argparse.ArgumentParser(description="Combined improvements sweep on BAliBASE") parser.add_argument("-j", "--parallel", type=int, default=4) parser.add_argument("--max-cases", type=int, default=0) parser.add_argument("--categories", nargs="*", default=None, help="BAliBASE categories (e.g. RV11 RV12)") args = parser.parse_args() cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) if args.categories: cats = [c.upper() for c in args.categories] cases = [c for c in cases if any(cat in c.dataset.upper() for cat in cats)] print(f"{len(cases)} BAliBASE cases x {len(CONFIGS)} configs = {len(cases) * len(CONFIGS)} tasks") tasks = [(case, config) for case in cases for config in CONFIGS] t0 = time.perf_counter() results = [] done = 0 with ProcessPoolExecutor(max_workers=args.parallel) as pool: futures = {pool.submit(_run_one, t): t for t in tasks} for f in as_completed(futures): done += 1 r = f.result() results.append(r) if done % 100 == 0: print(f" {done}/{len(tasks)} ({time.perf_counter()-t0:.0f}s)") elapsed = time.perf_counter() - t0 print(f"All done in {elapsed:.0f}s\n") # Overall summary print("=" * 90) print("OVERALL") print("=" * 90) _print_config_table(results) # Per-category all_cats = sorted({r["dataset"] for r in results}) for cat in all_cats: cat_results = [r for r in results if r["dataset"] == cat] n_cases = len(cat_results) // len(CONFIGS) cat_label = cat.replace("balibase_", "") print(f"\n{'=' * 90}") print(f"{cat_label} ({n_cases} cases)") print(f"{'=' * 90}") _print_config_table(cat_results) # Save out = Path("benchmarks/results/combined_improvements.json") out.parent.mkdir(parents=True, exist_ok=True) with open(out, "w") as f: json.dump({"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "results": results}, f, indent=2) print(f"\nSaved to {out}") def _print_config_table(results): config_names = [c["name"] for c in CONFIGS] groups = defaultdict(list) for r in results: if "error" not in r: groups[r["config"]].append(r) print(f"{'Config':<16} {'SP':>8} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time(s)':>9} {'dSP':>6} {'dF1':>6}") print("-" * 92) baseline_sp = None baseline_f1 = None for name in config_names: entries = groups.get(name, []) if not entries: continue sp = statistics.mean(r["sp_score"] for r in entries) rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) total_time = sum(r["wall_time"] for r in entries) if baseline_f1 is None: baseline_sp = sp baseline_f1 = f1 dsp = "" df1 = "" else: dsp = f"{sp - baseline_sp:+.1f}" df1 = f"{f1 - baseline_f1:+.3f}" print(f"{name:<16} {sp:>8.1f} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f} {total_time:>9.1f} {dsp:>6} {df1:>6}") if __name__ == "__main__": main() kalign-3.5.1/benchmarks/datasets.py000066400000000000000000000233471515023132300172700ustar00rootroot00000000000000"""Dataset management for kalign benchmarks. Supports BAliBASE, BRAliBASE, and BaliFam100 benchmark datasets. """ import shutil import subprocess import tarfile import urllib.error import urllib.request from dataclasses import dataclass from pathlib import Path from typing import List, Optional BENCHMARKS_DIR = Path(__file__).parent DATA_DIR = BENCHMARKS_DIR / "data" DOWNLOADS_DIR = DATA_DIR / "downloads" @dataclass class BenchmarkCase: """A single benchmark test case (one MSA family).""" family: str dataset: str unaligned: Path reference: Path seq_type: str # "protein" | "rna" | "dna" # --------------------------------------------------------------------------- # BAliBASE # --------------------------------------------------------------------------- BALIBASE_URL = "http://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz" BALIBASE_DIR = DOWNLOADS_DIR / "bb3_release" def balibase_download() -> None: """Download and extract BAliBASE R1-5.""" DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True) tarball = DOWNLOADS_DIR / "BAliBASE_R1-5.tar.gz" if BALIBASE_DIR.exists(): return print(f"Downloading BAliBASE from {BALIBASE_URL} ...") urllib.request.urlretrieve(BALIBASE_URL, tarball) print("Extracting ...") with tarfile.open(tarball) as tf: tf.extractall(DOWNLOADS_DIR) tarball.unlink() def balibase_is_available() -> bool: return BALIBASE_DIR.exists() def balibase_cases() -> List[BenchmarkCase]: """Discover BAliBASE test cases (*.tfa paired with *.msf).""" cases = [] for tfa in sorted(BALIBASE_DIR.rglob("*.tfa")): # Skip BBS (truncated) cases — only use full-length BB alignments if tfa.stem.startswith("BBS"): continue msf = tfa.with_suffix(".msf") if not msf.exists(): continue # Determine RV category from path parts = tfa.parts rv = "" for p in parts: if p.startswith("RV"): rv = p break family = tfa.stem cases.append( BenchmarkCase( family=family, dataset=f"balibase_{rv}" if rv else "balibase", unaligned=tfa, reference=msf, seq_type="protein", ) ) return cases # --------------------------------------------------------------------------- # BRAliBASE # --------------------------------------------------------------------------- BRALIBASE_URLS = [ ("data-set1", "https://web.archive.org/web/20160408045442id_/http://projects.binf.ku.dk/pgardner/bralibase/data-set1.tar.gz"), ("data-set2", "https://web.archive.org/web/20160408045453id_/http://projects.binf.ku.dk/pgardner/bralibase/data-set2.tar.gz"), ] def bralibase_download() -> None: """Download and extract BRAliBASE set1 + set2.""" DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True) for name, url in BRALIBASE_URLS: target = DOWNLOADS_DIR / name if target.exists(): continue tarball = DOWNLOADS_DIR / f"{name}.tar.gz" print(f"Downloading BRAliBASE {name} from {url} ...") urllib.request.urlretrieve(url, tarball) print("Extracting ...") with tarfile.open(tarball) as tf: tf.extractall(DOWNLOADS_DIR) tarball.unlink() def bralibase_is_available() -> bool: return any((DOWNLOADS_DIR / name).exists() for name, _ in BRALIBASE_URLS) def bralibase_cases() -> List[BenchmarkCase]: """Discover BRAliBASE test cases. data-set1 structure: {family}/structural/*.fa paired with {family}/unaligned/*.fa families: g2intron, rRNA, SRP, tRNA, U5 data-set2 structure: structural/*.fasta paired with unaligned/*.fasta """ cases = [] for name, _ in BRALIBASE_URLS: base = DOWNLOADS_DIR / name if not base.exists(): continue # Glob for both .fa and .fasta reference files refs = sorted(list(base.rglob("structural/*.fa")) + list(base.rglob("structural/*.fasta"))) for ref in refs: # Corresponding unaligned file — try same extension first, then alternative unaligned_dir = ref.parent.parent / "unaligned" unaligned = unaligned_dir / ref.name if not unaligned.exists(): alt_ext = ".fasta" if ref.suffix == ".fa" else ".fa" unaligned = unaligned_dir / (ref.stem + alt_ext) if not unaligned.exists(): continue family = ref.stem # For data-set1, extract RNA family category from path # e.g. data-set1/tRNA/structural/aln1.fa -> category "tRNA" structural_parent = ref.parent.parent.name if structural_parent != name: # This is a subcategory (g2intron, rRNA, SRP, tRNA, U5) dataset = f"bralibase_{structural_parent}" else: dataset = f"bralibase_{name}" cases.append( BenchmarkCase( family=family, dataset=dataset, unaligned=unaligned, reference=ref, seq_type="rna", ) ) return cases # --------------------------------------------------------------------------- # BaliFam100 (CC0 - committed to repo) # --------------------------------------------------------------------------- BALIFAM_DIR = DATA_DIR / "balifam100" BALIFAM_REPO = "https://github.com/rcedgar/balifam.git" def balifam_download() -> None: """Clone BaliFam100 repo (CC0 license) into data/balifam100/.""" if BALIFAM_DIR.exists() and any(BALIFAM_DIR.iterdir()): return BALIFAM_DIR.mkdir(parents=True, exist_ok=True) print(f"Cloning BaliFam100 from {BALIFAM_REPO} ...") subprocess.run( ["git", "clone", "--depth", "1", BALIFAM_REPO, str(BALIFAM_DIR)], check=True, ) # Remove .git to keep it clean for committing git_dir = BALIFAM_DIR / ".git" if git_dir.exists(): shutil.rmtree(git_dir) def balifam_is_available() -> bool: return BALIFAM_DIR.exists() and ( any(BALIFAM_DIR.rglob("*.fa")) or any(BALIFAM_DIR.rglob("*.fasta")) ) def balifam_cases() -> List[BenchmarkCase]: """Discover BaliFam100 test cases.""" cases = [] # BaliFam100 has in/ (unaligned) and ref/ (reference) subdirectories in_dir = BALIFAM_DIR / "in" ref_dir = BALIFAM_DIR / "ref" if not in_dir.exists() or not ref_dir.exists(): # Try alternative structure for ref in sorted(BALIFAM_DIR.rglob("ref/*.fa")): in_file = ref.parent.parent / "in" / ref.name if not in_file.exists(): in_file = ref.parent.parent / "in" / ref.with_suffix(".fasta").name if not in_file.exists(): continue cases.append( BenchmarkCase( family=ref.stem, dataset="balifam100", unaligned=in_file, reference=ref, seq_type="protein", ) ) return cases for ref in sorted(ref_dir.glob("*")): if not ref.is_file(): continue in_file = in_dir / ref.name if not in_file.exists(): continue cases.append( BenchmarkCase( family=ref.stem, dataset="balifam100", unaligned=in_file, reference=ref, seq_type="protein", ) ) return cases # --------------------------------------------------------------------------- # Registry # --------------------------------------------------------------------------- DATASETS = { "balibase": { "download": balibase_download, "is_available": balibase_is_available, "cases": balibase_cases, }, "bralibase": { "download": bralibase_download, "is_available": bralibase_is_available, "cases": bralibase_cases, }, "balifam100": { "download": balifam_download, "is_available": balifam_is_available, "cases": balifam_cases, }, } def download_dataset(name: str) -> None: """Download a specific dataset by name, or 'all' for everything.""" if name == "all": for ds in DATASETS.values(): ds["download"]() elif name in DATASETS: DATASETS[name]["download"]() else: raise ValueError(f"Unknown dataset: {name}. Choose from: {list(DATASETS.keys())} or 'all'") def get_cases(name: str, max_cases: Optional[int] = None) -> List[BenchmarkCase]: """Get benchmark cases for a dataset. Downloads if not available.""" if name == "all": all_cases = [] for ds_name, ds in DATASETS.items(): if not ds["is_available"](): try: ds["download"]() except (urllib.error.URLError, OSError) as e: print(f"WARNING: Could not download {ds_name} dataset: {e}") continue if ds["is_available"](): all_cases.extend(ds["cases"]()) if max_cases: all_cases = all_cases[:max_cases] return all_cases if name not in DATASETS: raise ValueError(f"Unknown dataset: {name}. Choose from: {list(DATASETS.keys())}") ds = DATASETS[name] if not ds["is_available"](): try: ds["download"]() except (urllib.error.URLError, OSError) as e: print(f"WARNING: Could not download {name} dataset: {e}") print("Skipping benchmark — dataset unavailable.") return [] cases = ds["cases"]() if max_cases: cases = cases[:max_cases] return cases kalign-3.5.1/benchmarks/downstream/000077500000000000000000000000001515023132300172605ustar00rootroot00000000000000kalign-3.5.1/benchmarks/downstream/__init__.py000066400000000000000000000001101515023132300213610ustar00rootroot00000000000000"""Downstream application benchmarks for kalign ensemble confidence.""" kalign-3.5.1/benchmarks/downstream/__main__.py000066400000000000000000000142321515023132300213540ustar00rootroot00000000000000"""Unified CLI entry point for downstream benchmarks. Usage:: python -m benchmarks.downstream --all -j 4 python -m benchmarks.downstream --all -j 4 --quick python -m benchmarks.downstream calibration -j 4 python -m benchmarks.downstream positive_selection --quick python -m benchmarks.downstream --figures -o benchmarks/figures/ """ from __future__ import annotations import argparse import logging import os import sys import time from pathlib import Path logger = logging.getLogger("benchmarks.downstream") PIPELINES = ["alignment_accuracy", "calibration", "positive_selection", "phylo_accuracy", "hmmer_detection"] def _run_pipeline(name: str, params: dict) -> None: """Import and run a single pipeline by name.""" if name == "alignment_accuracy": from .alignment_accuracy import run_pipeline elif name == "calibration": from .calibration import run_pipeline elif name == "positive_selection": from .positive_selection import run_pipeline elif name == "phylo_accuracy": from .phylo_accuracy import run_pipeline elif name == "hmmer_detection": from .hmmer_detection import run_pipeline else: raise ValueError(f"Unknown pipeline: {name!r}") logger.info("Starting pipeline: %s", name) start = time.perf_counter() run_pipeline(params) elapsed = time.perf_counter() - start logger.info("Pipeline %s completed in %.1f s", name, elapsed) def _run_figures(results_dir: Path, output_dir: Path) -> None: """Generate all publication figures from saved results.""" from .figures import generate_all_figures generate_all_figures(str(results_dir), str(output_dir)) def main(argv: list[str] | None = None) -> int: """Parse arguments and dispatch to pipelines.""" parser = argparse.ArgumentParser( prog="python -m benchmarks.downstream", description="Kalign downstream application benchmarks", ) parser.add_argument( "pipelines", nargs="*", default=[], metavar="PIPELINE", help="Pipeline(s) to run: alignment_accuracy, calibration, " "positive_selection, phylo_accuracy, hmmer_detection, or 'all'.", ) parser.add_argument( "--all", action="store_true", help="Run all four pipelines.", ) parser.add_argument( "--figures", action="store_true", help="Generate publication figures from saved results.", ) _default_jobs = os.cpu_count() or 1 parser.add_argument( "-j", "--jobs", type=int, default=_default_jobs, help=f"Number of parallel jobs (default: {_default_jobs}, all cores). " "Use -j 1 for sequential execution with accurate timing.", ) parser.add_argument( "--quick", action="store_true", help="Quick smoke test (5 cases per pipeline).", ) parser.add_argument( "--data-dir", type=Path, default=Path("benchmarks/data"), help="Data directory (default: benchmarks/data).", ) parser.add_argument( "--results-dir", type=Path, default=Path("benchmarks/results"), help="Results directory (default: benchmarks/results).", ) parser.add_argument( "-o", "--output-dir", type=Path, default=Path("benchmarks/figures"), help="Figure output directory (default: benchmarks/figures).", ) parser.add_argument( "--methods", nargs="+", default=None, help="Override method list (default: pipeline-specific defaults).", ) parser.add_argument( "--no-cache", action="store_true", help="Ignore cached results and recompute everything.", ) parser.add_argument( "--full", action="store_true", help="Use full simulation grids and all methods (for publication).", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Enable debug logging.", ) parser.add_argument( "--depths", nargs="+", type=float, default=None, help="Filter simulations to specific tree depths (e.g., --depths 4.0).", ) args = parser.parse_args(argv) # Configure logging level = logging.DEBUG if args.verbose else logging.INFO logging.basicConfig( level=level, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S", ) # Validate pipeline names valid = set(PIPELINES + ["all"]) for p in args.pipelines: if p not in valid: parser.error(f"Unknown pipeline: {p!r}. Choose from: {', '.join(PIPELINES)}, all") # Determine which pipelines to run pipelines_to_run: list[str] = [] if args.all or "all" in args.pipelines: pipelines_to_run = list(PIPELINES) else: pipelines_to_run = list(args.pipelines) # Must specify at least one action if not pipelines_to_run and not args.figures: parser.print_help() return 1 # Build common params dict params = { "data_dir": str(args.data_dir), "results_dir": str(args.results_dir), "n_jobs": args.jobs, "quick": args.quick, "no_cache": args.no_cache, "full": args.full, } if args.methods: params["methods"] = args.methods if args.depths: params["depths"] = args.depths # Run pipelines failed: list[str] = [] for name in pipelines_to_run: try: _run_pipeline(name, params) except Exception: logger.exception("Pipeline %s failed", name) failed.append(name) # Generate figures if args.figures: try: _run_figures(args.results_dir, args.output_dir) except Exception: logger.exception("Figure generation failed") failed.append("figures") # Summary if failed: logger.error("Failed: %s", ", ".join(failed)) return 1 if pipelines_to_run: logger.info("All pipelines completed successfully.") if args.figures: logger.info("Figures saved to %s", args.output_dir) return 0 if __name__ == "__main__": sys.exit(main()) kalign-3.5.1/benchmarks/downstream/alignment_accuracy.py000066400000000000000000000303061515023132300234640ustar00rootroot00000000000000"""Pipeline: Alignment Accuracy (BAliBASE + BRAliBASE). Scores alignment quality for all 6 methods against reference alignments. BAliBASE cases use XML core-block masks; BRAliBASE uses all columns. Usage:: python -m benchmarks.downstream alignment_accuracy -j 8 python -m benchmarks.downstream alignment_accuracy --quick -j 4 """ from __future__ import annotations import json import logging from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict from pathlib import Path logger = logging.getLogger(__name__) # Default methods for alignment accuracy (all 6 + true reference) _DEFAULT_METHODS = [ "kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo", ] # --------------------------------------------------------------------------- # Single-case runner # --------------------------------------------------------------------------- def _run_case( case_dict: dict, method_name: str, work_dir: str, fingerprint: str | None = None, ) -> dict: """Align one case with one method and score against reference. Parameters ---------- case_dict : dict Serialised BenchmarkCase with keys: family, dataset, unaligned, reference, seq_type. method_name : str Key into ``utils.METHODS``. work_dir : str Scratch directory. fingerprint : str or None Tool versions fingerprint for caching (None disables cache). Returns ------- dict Per-case result with scoring metrics, timing, and metadata. """ import kalign as _kalign from ..scoring import parse_balibase_xml from .utils import ( cache_load, cache_save, clean_work_dir, parse_fasta, run_method, write_fasta, ) wd = Path(work_dir) family = case_dict["family"] dataset = case_dict["dataset"] seq_type = case_dict["seq_type"] unaligned = Path(case_dict["unaligned"]) reference = Path(case_dict["reference"]) # Check cache if fingerprint: cached = cache_load(wd, fingerprint) if cached is not None: return cached clean_work_dir(wd) wd.mkdir(parents=True, exist_ok=True) try: # Align aln_result = run_method( method_name, unaligned, wd, seq_type=seq_type, ) # Write alignment for scoring aln_path = wd / "aligned.fasta" write_fasta(aln_result.names, aln_result.sequences, aln_path) # Score: BAliBASE uses XML core-block mask, BRAliBASE uses all columns xml_path = reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) scores = _kalign.compare_detailed( str(reference), str(aln_path), column_mask=mask ) else: # Check for gapless reference (conserved RNA) — skip ref_names, ref_seqs = parse_fasta(reference) if ref_seqs and all("-" not in s for s in ref_seqs): return { "family": family, "dataset": dataset, "benchmark": "bralibase" if "brali" in dataset else "balibase", "method": method_name, "skipped": True, "reason": "gapless_reference", } scores = _kalign.compare_detailed( str(reference), str(aln_path), max_gap_frac=-1.0 ) result = { "family": family, "dataset": dataset, "benchmark": "bralibase" if "brali" in dataset else "balibase", "method": method_name, "recall": scores.get("recall", -1.0), "precision": scores.get("precision", -1.0), "f1": scores.get("f1", -1.0), "tc": scores.get("tc", -1.0), "wall_time": aln_result.wall_time, "peak_memory_mb": aln_result.peak_memory_mb, } if fingerprint: cache_save(wd, fingerprint, result) return result except Exception as exc: logger.warning("Case %s/%s failed: %s", family, method_name, exc) return { "family": family, "dataset": dataset, "benchmark": "bralibase" if "brali" in dataset else "balibase", "method": method_name, "error": str(exc), } # --------------------------------------------------------------------------- # Pipeline entry point # --------------------------------------------------------------------------- def run_pipeline(params: dict) -> dict: """Run the alignment accuracy pipeline. Parameters ---------- params : dict Configuration with keys: - ``data_dir`` : str or Path -- base data directory - ``results_dir`` : str or Path -- results output directory - ``methods`` : list[str] -- method names to evaluate - ``n_jobs`` : int -- parallel workers - ``quick`` : bool -- if True, limit to 5 cases per dataset - ``no_cache`` : bool -- disable caching Returns ------- dict Result dict with provenance, cases, and summary. """ from ..datasets import get_cases from .provenance import collect_provenance, result_path, update_latest_symlink from .utils import METHODS, tool_versions_fingerprint data_dir = Path(params.get("data_dir", "benchmarks/data")) results_dir = Path(params.get("results_dir", "benchmarks/results")) methods = params.get("methods", _DEFAULT_METHODS) n_jobs = params.get("n_jobs", 1) quick = params.get("quick", False) # Validate methods for m in methods: if m not in METHODS: raise ValueError(f"Unknown method {m!r}. Available: {sorted(METHODS.keys())}") # Load BAliBASE + BRAliBASE cases balibase_cases = get_cases("balibase") bralibase_cases = get_cases("bralibase") all_cases = balibase_cases + bralibase_cases if quick: # Take first 5 from each dataset balibase_subset = balibase_cases[:5] bralibase_subset = bralibase_cases[:5] all_cases = balibase_subset + bralibase_subset logger.info( "Alignment accuracy: %d cases x %d methods = %d total", len(all_cases), len(methods), len(all_cases) * len(methods), ) # Compute fingerprint if params.get("no_cache"): fingerprint = None logger.info("Caching disabled (--no-cache)") else: fingerprint = tool_versions_fingerprint() logger.info("Tool versions fingerprint: %s", fingerprint) # Build work items work_items = [] for case in all_cases: for method_name in methods: case_dict = { "family": case.family, "dataset": case.dataset, "unaligned": str(case.unaligned), "reference": str(case.reference), "seq_type": case.seq_type, } work_dir = str( data_dir / "alignment_accuracy_work" / case.dataset / case.family / method_name ) work_items.append((case_dict, method_name, work_dir, fingerprint)) # Execute from tqdm import tqdm results: list[dict] = [] pbar = tqdm(total=len(work_items), desc="Alignment accuracy", unit="case") if n_jobs <= 1: for case_dict, method_name, work_dir, fp in work_items: r = _run_case(case_dict, method_name, work_dir, fp) results.append(r) pbar.update(1) else: with ProcessPoolExecutor(max_workers=n_jobs) as pool: futures = { pool.submit(_run_case, cd, mn, wd, fp): (cd, mn) for cd, mn, wd, fp in work_items } for fut in as_completed(futures): try: results.append(fut.result()) except Exception as exc: cd, mn = futures[fut] logger.error("Worker failed: %s/%s: %s", cd["family"], mn, exc) results.append({ "family": cd["family"], "dataset": cd["dataset"], "method": mn, "error": str(exc), }) pbar.update(1) pbar.close() # Filter errors and skipped successful = [r for r in results if "error" not in r and not r.get("skipped")] skipped = [r for r in results if r.get("skipped")] errors = [r for r in results if "error" in r] logger.info( "%d successful, %d skipped, %d errors", len(successful), len(skipped), len(errors), ) # Aggregate summary summary = _aggregate_summary(successful, methods) # Provenance provenance = collect_provenance(params) output = { "provenance": asdict(provenance), "cases": results, "summary": summary, } # Save out_path = result_path(results_dir, "alignment_accuracy") with open(out_path, "w") as fh: json.dump(output, fh, indent=2, default=str) update_latest_symlink(out_path) logger.info("Results saved to %s", out_path) # Print summary _print_summary(summary, methods) return output # --------------------------------------------------------------------------- # Aggregation # --------------------------------------------------------------------------- def _aggregate_summary(cases: list[dict], methods: list[str]) -> dict: """Compute per-method and per-category aggregated metrics.""" from collections import defaultdict import numpy as np summary: dict = {} for method in methods: mc = [c for c in cases if c.get("method") == method] if not mc: summary[method] = {"n_cases": 0} continue recall_vals = [c["recall"] for c in mc if c.get("recall", -1) >= 0] prec_vals = [c["precision"] for c in mc if c.get("precision", -1) >= 0] f1_vals = [c["f1"] for c in mc if c.get("f1", -1) >= 0] tc_vals = [c["tc"] for c in mc if c.get("tc", -1) >= 0] time_vals = [c["wall_time"] for c in mc if "wall_time" in c] summary[method] = { "n_cases": len(mc), "mean_recall": float(np.mean(recall_vals)) if recall_vals else None, "mean_precision": float(np.mean(prec_vals)) if prec_vals else None, "mean_f1": float(np.mean(f1_vals)) if f1_vals else None, "mean_tc": float(np.mean(tc_vals)) if tc_vals else None, "mean_wall_time": float(np.mean(time_vals)) if time_vals else None, } # Per-category breakdown by_dataset: dict[str, list[dict]] = defaultdict(list) for c in mc: by_dataset[c["dataset"]].append(c) categories = {} for ds, ds_cases in sorted(by_dataset.items()): ds_recall = [c["recall"] for c in ds_cases if c.get("recall", -1) >= 0] ds_f1 = [c["f1"] for c in ds_cases if c.get("f1", -1) >= 0] ds_tc = [c["tc"] for c in ds_cases if c.get("tc", -1) >= 0] categories[ds] = { "n_cases": len(ds_cases), "mean_recall": float(np.mean(ds_recall)) if ds_recall else None, "mean_f1": float(np.mean(ds_f1)) if ds_f1 else None, "mean_tc": float(np.mean(ds_tc)) if ds_tc else None, } summary[method]["categories"] = categories return summary def _print_summary(summary: dict, methods: list[str]) -> None: """Print a summary table to the logger.""" logger.info("=" * 80) logger.info("Alignment Accuracy Summary") logger.info("=" * 80) logger.info( "%-18s %5s %8s %8s %8s %8s %8s", "Method", "N", "Recall", "Prec", "F1", "TC", "Time", ) logger.info("-" * 80) for m in methods: s = summary.get(m, {}) n = s.get("n_cases", 0) if n == 0: logger.info("%-18s %5d %8s", m, 0, "no data") continue logger.info( "%-18s %5d %8.3f %8.3f %8.3f %8.3f %7.2fs", m, n, s.get("mean_recall") or 0, s.get("mean_precision") or 0, s.get("mean_f1") or 0, s.get("mean_tc") or 0, s.get("mean_wall_time") or 0, ) logger.info("=" * 80) kalign-3.5.1/benchmarks/downstream/calibration.py000066400000000000000000000613341515023132300221300ustar00rootroot00000000000000"""Pipeline 0: Confidence Calibration. Validates that kalign ensemble confidence scores are meaningful by comparing predicted per-column confidence to actual column correctness against known true alignments from INDELible simulations. Usage:: python -m benchmarks.downstream.calibration -j 4 python -m benchmarks.downstream.calibration -j 4 --quick """ from __future__ import annotations import json import logging import tempfile from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict, dataclass, field from pathlib import Path from typing import List, Optional logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Per-case result # --------------------------------------------------------------------------- @dataclass class CaseResult: """Result from a single simulation x method calibration case.""" sim_id: str method: str predicted_confidence: list[float] # per-column confidence actual_correct: list[int] # 1 if column matches true alignment, 0 otherwise sp_score: float tc_score: float wall_time: float peak_memory_mb: float # --------------------------------------------------------------------------- # Aggregated calibration result # --------------------------------------------------------------------------- @dataclass class CalibrationResult: """Aggregated calibration result across all cases and methods.""" provenance: dict cases: list[dict] # serialised CaseResults summary: dict # aggregated metrics per method # --------------------------------------------------------------------------- # Column correctness # --------------------------------------------------------------------------- def _column_correctness( test_seqs: list[str], true_seqs: list[str], test_names: list[str], true_names: list[str], ) -> list[int]: """Check per-column correctness of *test* alignment vs *true* alignment. For each column in the test alignment, determine whether the set of residue pairs in that column matches the corresponding column in the true alignment. A column is correct (1) if every pair of non-gap characters in the test column appears at the same aligned positions in the true alignment; otherwise it is incorrect (0). Sequences are matched by name between test and true alignments. Parameters ---------- test_seqs, true_seqs : list[str] Aligned sequences (all same length within each list). test_names, true_names : list[str] Corresponding sequence names. Returns ------- list[int] One entry per column in the test alignment: 1 = correct, 0 = incorrect. """ if not test_seqs: return [] # Build name -> index mapping for the true alignment true_idx = {name: i for i, name in enumerate(true_names)} # Reorder true sequences to match test sequence order order = [] for name in test_names: if name not in true_idx: raise ValueError( f"Sequence {name!r} in test alignment not found in true alignment" ) order.append(true_idx[name]) reordered_true = [true_seqs[i] for i in order] # Build a mapping: for each sequence, residue position -> true column index # "residue position" = count of non-gap characters seen so far n_seqs = len(test_seqs) true_ncols = len(reordered_true[0]) if reordered_true else 0 # For each sequence, build: residue_index -> true_column true_res_to_col: list[dict[int, int]] = [] for s in range(n_seqs): mapping: dict[int, int] = {} res_idx = 0 for col in range(true_ncols): c = reordered_true[s][col] if c != "-" and c != ".": mapping[res_idx] = col res_idx += 1 true_res_to_col.append(mapping) # For the test alignment, do the same: track residue positions test_ncols = len(test_seqs[0]) test_res_counters = [0] * n_seqs result = [] for col in range(test_ncols): # Collect the true column index for each non-gap character in this test column true_cols_for_residues: list[int] = [] residue_indices_this_col: list[tuple[int, int]] = [] # (seq_idx, res_idx) for s in range(n_seqs): c = test_seqs[s][col] if c != "-" and c != ".": res_idx = test_res_counters[s] true_col = true_res_to_col[s].get(res_idx) if true_col is not None: true_cols_for_residues.append(true_col) residue_indices_this_col.append((s, res_idx)) test_res_counters[s] += 1 # A column is correct if all non-gap residues in this column # are aligned to the same column in the true alignment if len(true_cols_for_residues) <= 1: # 0 or 1 non-gap characters: trivially correct result.append(1) elif all(tc == true_cols_for_residues[0] for tc in true_cols_for_residues): result.append(1) else: result.append(0) return result # --------------------------------------------------------------------------- # Calibration metrics # --------------------------------------------------------------------------- def brier_score(predicted: list[float], actual: list[int]) -> float: """Brier score: mean squared error between predicted confidence and binary correctness. Parameters ---------- predicted : list[float] Predicted confidence values in [0, 1]. actual : list[int] Binary correctness values (0 or 1). Returns ------- float Brier score (lower is better). Returns 0.0 for empty inputs. """ n = len(predicted) if n == 0: return 0.0 total = 0.0 for p, o in zip(predicted, actual): total += (p - o) ** 2 return total / n def expected_calibration_error( predicted: list[float], actual: list[int], n_bins: int = 10, ) -> float: """Expected Calibration Error (ECE). Bins predictions into *n_bins* equal-width bins over [0, 1], computes |avg_confidence - fraction_correct| per bin, weighted by the fraction of samples in each bin. Parameters ---------- predicted : list[float] Predicted confidence values in [0, 1]. actual : list[int] Binary correctness values (0 or 1). n_bins : int Number of equal-width bins. Returns ------- float ECE value (lower is better). Returns 0.0 for empty inputs. """ n = len(predicted) if n == 0: return 0.0 bin_sums_conf = [0.0] * n_bins bin_sums_correct = [0.0] * n_bins bin_counts = [0] * n_bins for p, o in zip(predicted, actual): # Determine bin index b = int(p * n_bins) if b >= n_bins: b = n_bins - 1 bin_sums_conf[b] += p bin_sums_correct[b] += o bin_counts[b] += 1 ece = 0.0 for b in range(n_bins): if bin_counts[b] > 0: avg_conf = bin_sums_conf[b] / bin_counts[b] frac_correct = bin_sums_correct[b] / bin_counts[b] ece += (bin_counts[b] / n) * abs(avg_conf - frac_correct) return ece def calibration_curve( predicted: list[float], actual: list[int], n_bins: int = 10, ) -> tuple[list[float], list[float], list[int]]: """Compute calibration curve data for plotting. Parameters ---------- predicted : list[float] Predicted confidence values in [0, 1]. actual : list[int] Binary correctness values (0 or 1). n_bins : int Number of equal-width bins over [0, 1]. Returns ------- bin_centers : list[float] Midpoint of each bin. fraction_correct : list[float] Fraction of correct predictions in each bin (NaN for empty bins). bin_counts : list[int] Number of samples in each bin. """ bin_width = 1.0 / n_bins bin_centers = [(b + 0.5) * bin_width for b in range(n_bins)] bin_sums_correct = [0.0] * n_bins bin_counts = [0] * n_bins for p, o in zip(predicted, actual): b = int(p * n_bins) if b >= n_bins: b = n_bins - 1 bin_sums_correct[b] += o bin_counts[b] += 1 fraction_correct = [] for b in range(n_bins): if bin_counts[b] > 0: fraction_correct.append(bin_sums_correct[b] / bin_counts[b]) else: fraction_correct.append(float("nan")) return bin_centers, fraction_correct, bin_counts # --------------------------------------------------------------------------- # Single-case runner # --------------------------------------------------------------------------- def run_calibration_case( sim_dataset, method_name: str, work_dir: Path, seq_type: str = "protein", ) -> CaseResult: """Run one calibration case: align with *method_name*, evaluate column correctness. Parameters ---------- sim_dataset : SimulatedDataset A simulated dataset with known true alignment. method_name : str Key into ``utils.METHODS``. work_dir : Path Scratch directory for alignment output files. seq_type : str Sequence type hint for the aligner. Returns ------- CaseResult """ from .utils import alignment_accuracy, parse_fasta, run_method work_dir = Path(work_dir) work_dir.mkdir(parents=True, exist_ok=True) sim_id = sim_dataset.params.get("sim_id", "unknown") if hasattr(sim_dataset, "params") and isinstance(sim_dataset.params, dict): sim_id = sim_dataset.params.get("sim_id", sim_id) # "true" method: use the true alignment directly as a performance ceiling if method_name == "true": true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) aln_len = len(true_seqs[0]) if true_seqs else 0 return CaseResult( sim_id=sim_id, method="true", predicted_confidence=[1.0] * aln_len, actual_correct=[1] * aln_len, sp_score=1.0, tc_score=1.0, wall_time=0.0, peak_memory_mb=0.0, ) # Run the alignment method aln_result = run_method(method_name, sim_dataset.unaligned, work_dir, seq_type=seq_type) # Load the true alignment true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) # Compute alignment accuracy (SP, TC) acc = alignment_accuracy( aln_result.sequences, true_seqs, aln_result.names, true_names, ) # Compute per-column correctness correctness = _column_correctness( aln_result.sequences, true_seqs, aln_result.names, true_names, ) # Extract confidence: use column_confidence if available, else uniform 1.0 if aln_result.column_confidence is not None: confidence = list(aln_result.column_confidence) # After masking, the confidence list may be longer than the alignment; # truncate or pad to match the alignment length aln_len = len(aln_result.sequences[0]) if aln_result.sequences else 0 if len(confidence) > aln_len: confidence = confidence[:aln_len] elif len(confidence) < aln_len: confidence.extend([1.0] * (aln_len - len(confidence))) else: # Methods without confidence get uniform 1.0 aln_len = len(aln_result.sequences[0]) if aln_result.sequences else 0 confidence = [1.0] * aln_len return CaseResult( sim_id=sim_id, method=method_name, predicted_confidence=confidence, actual_correct=correctness, sp_score=acc["sp_score"], tc_score=acc["tc_score"], wall_time=aln_result.wall_time, peak_memory_mb=aln_result.peak_memory_mb, ) def _run_one_calibration(sim_dataset, method_name, work_dir, fingerprint=None): """Worker for parallel calibration cases. Returns a dict.""" from dataclasses import asdict as _asdict from .utils import cache_load, cache_save, clean_work_dir work_dir = Path(work_dir) if fingerprint: cached = cache_load(work_dir, fingerprint) if cached is not None: return cached # Remove stale output files from previous runs clean_work_dir(work_dir) case = run_calibration_case(sim_dataset, method_name, work_dir, seq_type="protein") result_dict = _asdict(case) if fingerprint: cache_save(work_dir, fingerprint, result_dict) return result_dict # --------------------------------------------------------------------------- # Pipeline entry point # --------------------------------------------------------------------------- def run_pipeline(params: dict) -> CalibrationResult: """Run the full confidence calibration pipeline. Parameters ---------- params : dict Configuration with keys: - ``data_dir`` : str or Path -- base directory for simulation data - ``results_dir`` : str or Path -- base directory for result JSON files - ``methods`` : list[str] -- method names to evaluate (keys in METHODS) - ``n_simulations`` : int -- override number of simulation cases (default: use PROTEIN_GRID) - ``n_jobs`` : int -- parallelism level (default 1) - ``quick`` : bool -- if True, only 5 cases for smoke testing Returns ------- CalibrationResult """ from dataclasses import asdict as _asdict from .provenance import collect_provenance, result_path, update_latest_symlink from .simulation import ( PROTEIN_GRID, PROTEIN_GRID_FULL, generate_indelible_dataset, iter_simulation_params, random_birth_death_tree, ) from .utils import METHODS, cache_load, cache_save, clean_work_dir, tool_versions_fingerprint data_dir = Path(params.get("data_dir", "benchmarks/data/downstream/calibration")) results_dir = Path(params.get("results_dir", "benchmarks/results")) # Calibration evaluates raw confidence scores → masked methods are excluded _default_methods = ["kalign", "kalign_cons", "kalign_ens3", "true"] methods = params.get("methods", _default_methods) quick = params.get("quick", False) n_jobs = params.get("n_jobs", 1) data_dir.mkdir(parents=True, exist_ok=True) # Validate method names for m in methods: if m not in METHODS: raise ValueError(f"Unknown method {m!r}. Available: {sorted(METHODS.keys())}") # Generate simulation parameter sets grid = PROTEIN_GRID_FULL if params.get("full") else PROTEIN_GRID sim_params_list = list(iter_simulation_params(grid, "WAG")) # Allow override of the number of simulations n_simulations = params.get("n_simulations") if n_simulations is not None: sim_params_list = sim_params_list[:n_simulations] if quick: sim_params_list = sim_params_list[:5] logger.info( "Running calibration pipeline: %d simulations x %d methods = %d cases", len(sim_params_list), len(methods), len(sim_params_list) * len(methods), ) # Step 1: Generate simulated datasets (sequential -- INDELible is fast) from tqdm import tqdm sim_datasets = [] for sp in tqdm(sim_params_list, desc="Generating simulations", unit="sim"): sim_id = sp["sim_id"] sim_dir = data_dir / sim_id tree = random_birth_death_tree( n_taxa=sp["n_taxa"], target_depth=sp["target_depth"], seed=sp.get("seed", 42), ) sim_dataset = generate_indelible_dataset( tree=tree, model=sp["model"], seq_length=sp["seq_length"], indel_rate=sp["indel_rate"], indel_length_mean=sp.get("indel_length_mean", 2.0), output_dir=sim_dir, seed=sp.get("seed", 42), ) sim_dataset.params["sim_id"] = sim_id sim_datasets.append(sim_dataset) # Step 2: Build work items for alignment + scoring work_items = [] for sim_dataset in sim_datasets: sim_id = sim_dataset.params["sim_id"] sim_dir = data_dir / sim_id for method_name in methods: work_dir = sim_dir / f"work_{method_name}" work_items.append((sim_dataset, method_name, work_dir)) # Compute fingerprint once for all workers (None disables caching) if params.get("no_cache"): fingerprint = None logger.info("Caching disabled (--no-cache)") else: fingerprint = tool_versions_fingerprint() logger.info("Tool versions fingerprint: %s", fingerprint) # Step 3: Run alignment cases (parallel when n_jobs > 1) all_cases: list[dict] = [] n_cached = 0 pbar = tqdm(total=len(work_items), desc="Calibration", unit="case") if n_jobs <= 1: for sim_dataset, method_name, work_dir in work_items: try: cached = cache_load(work_dir, fingerprint) if fingerprint else None if cached is not None: all_cases.append(cached) n_cached += 1 else: clean_work_dir(work_dir) case = run_calibration_case( sim_dataset, method_name, work_dir, seq_type="protein", ) result_dict = _asdict(case) if fingerprint: cache_save(work_dir, fingerprint, result_dict) all_cases.append(result_dict) except Exception as exc: logger.error("Failed: %s / %s: %s", sim_dataset.params["sim_id"], method_name, exc) all_cases.append({ "sim_id": sim_dataset.params["sim_id"], "method": method_name, "error": str(exc), }) pbar.update(1) pbar.set_postfix(cached=n_cached, computed=pbar.n - n_cached) else: with ProcessPoolExecutor(max_workers=n_jobs) as pool: futures = {} for sim_dataset, method_name, work_dir in work_items: fut = pool.submit( _run_one_calibration, sim_dataset, method_name, work_dir, fingerprint=fingerprint, ) futures[fut] = (sim_dataset.params["sim_id"], method_name) for fut in as_completed(futures): sim_id, method_name = futures[fut] try: result_dict = fut.result() all_cases.append(result_dict) except Exception as exc: logger.error("Failed: %s / %s: %s", sim_id, method_name, exc) all_cases.append({ "sim_id": sim_id, "method": method_name, "error": str(exc), }) pbar.update(1) pbar.close() # Aggregate results per method summary = _aggregate_summary(all_cases) # Collect provenance provenance = collect_provenance(params) result = CalibrationResult( provenance=_asdict(provenance), cases=all_cases, summary=summary, ) # Save results out_path = result_path(results_dir, "calibration") with open(out_path, "w") as fh: json.dump( { "provenance": result.provenance, "cases": result.cases, "summary": result.summary, }, fh, indent=2, default=str, ) update_latest_symlink(out_path) logger.info("Results saved to %s", out_path) return result def _aggregate_summary(cases: list[dict]) -> dict: """Compute per-method summary statistics from case result dicts. Returns a dict keyed by method name with aggregated metrics. """ from collections import defaultdict method_preds: dict[str, list[float]] = defaultdict(list) method_actuals: dict[str, list[int]] = defaultdict(list) method_sp: dict[str, list[float]] = defaultdict(list) method_tc: dict[str, list[float]] = defaultdict(list) method_time: dict[str, list[float]] = defaultdict(list) method_mem: dict[str, list[float]] = defaultdict(list) for case in cases: if "error" in case: continue m = case["method"] method_preds[m].extend(case.get("predicted_confidence", [])) method_actuals[m].extend(case.get("actual_correct", [])) if case.get("sp_score", -1.0) >= 0: method_sp[m].append(case["sp_score"]) if case.get("tc_score", -1.0) >= 0: method_tc[m].append(case["tc_score"]) method_time[m].append(case.get("wall_time", 0.0)) method_mem[m].append(case.get("peak_memory_mb", 0.0)) summary: dict[str, dict] = {} for m in sorted(method_preds.keys()): preds = method_preds[m] actuals = method_actuals[m] bs = brier_score(preds, actuals) ece = expected_calibration_error(preds, actuals) bin_centers, frac_correct, bin_counts = calibration_curve(preds, actuals) sp_vals = method_sp[m] tc_vals = method_tc[m] time_vals = method_time[m] mem_vals = method_mem[m] summary[m] = { "brier_score": bs, "ece": ece, "calibration_curve": { "bin_centers": bin_centers, "fraction_correct": frac_correct, "bin_counts": bin_counts, }, "n_columns_total": len(preds), "mean_sp": sum(sp_vals) / len(sp_vals) if sp_vals else None, "mean_tc": sum(tc_vals) / len(tc_vals) if tc_vals else None, "mean_time": sum(time_vals) / len(time_vals) if time_vals else None, "mean_peak_memory_mb": sum(mem_vals) / len(mem_vals) if mem_vals else None, "n_cases": len(time_vals), } return summary # --------------------------------------------------------------------------- # Result loading # --------------------------------------------------------------------------- def load_results(results_dir: Path | str) -> CalibrationResult: """Load calibration results from the latest.json symlink. Parameters ---------- results_dir : Path or str Base results directory (the ``calibration/`` subdirectory is appended automatically). Returns ------- CalibrationResult """ results_dir = Path(results_dir) cal_dir = results_dir / "calibration" latest = cal_dir / "latest.json" with open(latest) as fh: data = json.load(fh) return CalibrationResult( provenance=data.get("provenance", {}), cases=data.get("cases", []), summary=data.get("summary", {}), ) # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- if __name__ == "__main__": import argparse import sys logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) parser = argparse.ArgumentParser( description="Pipeline 0: Confidence calibration benchmarks" ) parser.add_argument( "-j", "--parallel", type=int, default=1, help="Number of parallel jobs" ) parser.add_argument( "--quick", action="store_true", help="Quick mode: 5 cases only" ) parser.add_argument( "--max-sims", type=int, default=None, help="Limit number of simulations" ) parser.add_argument( "--data-dir", type=str, default="benchmarks/data/downstream/calibration", help="Data directory for simulations", ) parser.add_argument( "--results-dir", type=str, default="benchmarks/results", help="Results output directory", ) parser.add_argument( "--methods", nargs="+", default=None, help="Methods to run (default: all)", ) parser.add_argument( "--no-cache", action="store_true", help="Ignore cached results and recompute everything.", ) args = parser.parse_args() params = { "data_dir": args.data_dir, "results_dir": args.results_dir, "n_jobs": args.parallel, "quick": args.quick, "no_cache": args.no_cache, } if args.max_sims is not None: params["n_simulations"] = args.max_sims if args.methods is not None: params["methods"] = args.methods result = run_pipeline(params) # Print summary print("\n=== Calibration Summary ===\n") for method, stats in result.summary.items(): print(f" {method:25s} Brier={stats['brier_score']:.4f} " f"ECE={stats['ece']:.4f} " f"SP={stats.get('mean_sp', 'N/A')!s:>6s} " f"TC={stats.get('mean_tc', 'N/A')!s:>6s} " f"n={stats['n_cases']}") print() kalign-3.5.1/benchmarks/downstream/figures.py000066400000000000000000001242301515023132300213000ustar00rootroot00000000000000"""Publication-quality figure generation for kalign downstream benchmarks. Each public function produces a PDF figure suitable for inclusion in a journal manuscript. All figures read from pipeline result JSON files via ``_load_latest_json()``. All figures use: - matplotlib with a clean, Nature-style aesthetic - METHOD_COLORS from utils for consistent per-method colouring - PDF output for vector graphics - 10 pt base font, 1.5 pt minimum line width - Single-column width = 3.5 in, double-column = 7 in """ from __future__ import annotations import json import logging from collections import defaultdict from pathlib import Path from typing import Any from .utils import METHOD_COLORS logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Method ordering (consistent across all figures) # --------------------------------------------------------------------------- METHOD_ORDER = ["kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo"] # --------------------------------------------------------------------------- # Style helpers # --------------------------------------------------------------------------- def _setup_figure_style() -> None: """Configure matplotlib rcParams for publication-quality output.""" import matplotlib as mpl mpl.use("Agg") import matplotlib.pyplot as plt plt.rcParams.update( { # Font "font.family": "sans-serif", "font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"], "font.size": 10, "axes.titlesize": 11, "axes.labelsize": 10, "xtick.labelsize": 9, "ytick.labelsize": 9, "legend.fontsize": 8, # Lines "lines.linewidth": 1.5, "lines.markersize": 5, # Axes "axes.linewidth": 1.0, "axes.spines.top": False, "axes.spines.right": False, "xtick.major.width": 0.8, "ytick.major.width": 0.8, "xtick.major.size": 4, "ytick.major.size": 4, # Grid "axes.grid": False, # Figure "figure.dpi": 150, "savefig.dpi": 300, "savefig.bbox": "tight", "savefig.pad_inches": 0.05, # Legend "legend.frameon": False, "legend.borderaxespad": 0.5, } ) def _add_panel_label(ax: Any, label: str) -> None: """Add a bold panel label (e.g. 'A', 'B') in the upper-left corner.""" ax.text( -0.12, 1.08, label, transform=ax.transAxes, fontsize=13, fontweight="bold", va="top", ha="left", ) def _method_color(method: str) -> str: """Return the colour for *method*, falling back to dark grey.""" return METHOD_COLORS.get(method, "#333333") def _method_label(method: str) -> str: """Return a human-friendly label for a method key.""" labels = { "kalign": "Kalign", "kalign_cons": "Kalign+cons", "kalign_ens3": "Kalign ens3", "mafft": "MAFFT", "muscle": "MUSCLE", "clustalo": "Clustal Omega", "true": "True alignment", } return labels.get(method, method) def _savefig(fig: Any, output_path: Path) -> None: """Save figure as PDF and close it.""" output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) fig.savefig(str(output_path), format="pdf") import matplotlib.pyplot as plt plt.close(fig) logger.info("Saved figure: %s", output_path) def _bootstrap_ci_vec( values: list[float], n_bootstrap: int = 5000, alpha: float = 0.05 ) -> tuple[float, float, float]: """Return (mean, ci_lo, ci_hi) via bootstrap resampling.""" import numpy as np arr = np.array(values, dtype=float) n = len(arr) if n < 2: mu = float(arr.mean()) return mu, mu, mu rng = np.random.default_rng(42) boot_means = np.array( [float(rng.choice(arr, size=n, replace=True).mean()) for _ in range(n_bootstrap)] ) lo = float(np.percentile(boot_means, 100 * alpha / 2)) hi = float(np.percentile(boot_means, 100 * (1 - alpha / 2))) return float(arr.mean()), lo, hi def _shared_legend(fig: Any, methods: list[str], ncol: int = 6) -> None: """Add a shared legend below all panels.""" from matplotlib.patches import Patch handles = [ Patch(facecolor=_method_color(m), alpha=0.7, label=_method_label(m)) for m in methods ] fig.legend( handles=handles, loc="lower center", ncol=ncol, fontsize=8, frameon=False, bbox_to_anchor=(0.5, -0.02), ) def _ordered_methods(available: set[str]) -> list[str]: """Return methods in canonical order, filtered to those in *available*.""" return [m for m in METHOD_ORDER if m in available] def _grouped_boxplot( ax: Any, data: dict[str, dict[str, list[float]]], categories: list[str], methods: list[str], ylabel: str, cat_labels: dict[str, str] | None = None, ) -> None: """Draw grouped box plots on *ax*. Parameters ---------- data : dict[method -> dict[category -> list[float]]] categories : list of category keys methods : list of method keys (in order) """ n_methods = len(methods) n_cats = len(categories) if n_methods == 0 or n_cats == 0: return width = 0.8 / n_methods for j, method in enumerate(methods): positions = [] box_data = [] color = _method_color(method) for i, cat in enumerate(categories): vals = data.get(method, {}).get(cat, []) box_data.append(vals if vals else [0]) positions.append(i + j * width - (n_methods - 1) * width / 2) bp = ax.boxplot( box_data, positions=positions, widths=width * 0.85, patch_artist=True, medianprops={"color": "black", "linewidth": 1.2}, whiskerprops={"linewidth": 0.8}, capprops={"linewidth": 0.8}, flierprops={"marker": ".", "markersize": 3, "alpha": 0.5}, manage_ticks=False, ) for patch in bp["boxes"]: patch.set_facecolor(color) patch.set_alpha(0.7) tick_labels = [] for cat in categories: label = (cat_labels or {}).get(cat, cat) # Count cases from first method with data n = 0 for m in methods: n = len(data.get(m, {}).get(cat, [])) if n > 0: break tick_labels.append(f"{label}\n(n={n})" if n > 0 else label) ax.set_xticks(range(n_cats)) ax.set_xticklabels(tick_labels, fontsize=8) ax.set_ylabel(ylabel) ax.set_ylim(-0.02, 1.05) ax.set_xlim(-0.5, n_cats - 0.5) # --------------------------------------------------------------------------- # Result loading helpers # --------------------------------------------------------------------------- def _load_latest_json(results_dir: Path) -> Any: """Load the ``latest.json`` symlink (or most recent run file).""" results_dir = Path(results_dir) if not results_dir.is_dir(): return None latest = results_dir / "latest.json" if latest.exists(): with open(latest) as fh: return json.load(fh) json_files = sorted(results_dir.glob("run_*.json")) if not json_files: return None with open(json_files[-1]) as fh: return json.load(fh) def _extract_cases(data: Any) -> list[dict]: """Extract the cases list from a pipeline result dict or list.""" if isinstance(data, list): return data if isinstance(data, dict): return data.get("cases", []) return [] # --------------------------------------------------------------------------- # Figure 1: BAliBASE SP/TC by category # --------------------------------------------------------------------------- _BALIBASE_CATEGORIES = ["RV11", "RV12", "RV20", "RV30", "RV40", "RV50"] def figure_balibase(cases: list[dict], output_path: Path) -> None: """BAliBASE alignment accuracy: SP, Precision, F1, and TC box plots. Panel A: SP (recall) per BAliBASE category, grouped by method. Panel B: Precision per BAliBASE category, grouped by method. Panel C: F1 per BAliBASE category, grouped by method. Panel D: TC per BAliBASE category, grouped by method. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt _setup_figure_style() fig, ((ax_sp, ax_prec), (ax_f1, ax_tc)) = plt.subplots(2, 2, figsize=(14, 10)) # Filter to balibase cases only bb_cases = [c for c in cases if c.get("benchmark") == "balibase" and "error" not in c and not c.get("skipped")] if not bb_cases: _savefig(fig, output_path) return available = {c["method"] for c in bb_cases} methods = _ordered_methods(available) # Build data[method][category] -> list of values def _build(metric: str) -> dict[str, dict[str, list[float]]]: out: dict[str, dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) for c in bb_cases: m = c["method"] ds = c.get("dataset", "") val = c.get(metric) if val is None or val < 0: continue for cat in _BALIBASE_CATEGORIES: if cat in ds: out[m][cat].append(val) break return out sp_data = _build("recall") prec_data = _build("precision") f1_data = _build("f1") tc_data = _build("tc") _grouped_boxplot(ax_sp, sp_data, _BALIBASE_CATEGORIES, methods, "SP (recall)") ax_sp.set_title("Sum-of-pairs score by category", fontsize=10) _add_panel_label(ax_sp, "A") _grouped_boxplot(ax_prec, prec_data, _BALIBASE_CATEGORIES, methods, "Precision") ax_prec.set_title("Precision by category", fontsize=10) _add_panel_label(ax_prec, "B") _grouped_boxplot(ax_f1, f1_data, _BALIBASE_CATEGORIES, methods, "F1") ax_f1.set_title("F1 score by category", fontsize=10) _add_panel_label(ax_f1, "C") _grouped_boxplot(ax_tc, tc_data, _BALIBASE_CATEGORIES, methods, "TC") ax_tc.set_title("Total column score by category", fontsize=10) _add_panel_label(ax_tc, "D") _shared_legend(fig, methods) fig.tight_layout(rect=[0, 0.04, 1, 1]) _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 2: BRAliBASE SP by RNA family # --------------------------------------------------------------------------- _RNA_FAMILIES = ["SRP", "tRNA", "rRNA", "g2intron", "U5"] _RNA_LABELS = { "SRP": "SRP", "tRNA": "tRNA", "rRNA": "rRNA", "g2intron": "Group II\nintron", "U5": "U5", } def figure_bralibase(cases: list[dict], output_path: Path) -> None: """BRAliBASE RNA alignment accuracy. Panel A: SP (recall) per RNA family, grouped by method. Panel B: F1 per RNA family, grouped by method. Panel C: Wall time box plots per method (log scale). """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() fig, (ax_sp, ax_f1, ax_time) = plt.subplots(1, 3, figsize=(18, 5)) br_cases = [c for c in cases if c.get("benchmark") == "bralibase" and "error" not in c and not c.get("skipped")] if not br_cases: _savefig(fig, output_path) return available = {c["method"] for c in br_cases} methods = _ordered_methods(available) # Build data per family def _build_family(metric: str) -> dict[str, dict[str, list[float]]]: out: dict[str, dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) for c in br_cases: m = c["method"] ds = c.get("dataset", "") val = c.get(metric) if val is None or val < 0: continue for fam in _RNA_FAMILIES: if fam.lower() in ds.lower() or fam in ds: out[m][fam].append(val) break return out sp_data = _build_family("recall") f1_data = _build_family("f1") _grouped_boxplot(ax_sp, sp_data, _RNA_FAMILIES, methods, "SP (recall)", _RNA_LABELS) ax_sp.set_title("Sum-of-pairs score by RNA family", fontsize=10) _add_panel_label(ax_sp, "A") _grouped_boxplot(ax_f1, f1_data, _RNA_FAMILIES, methods, "F1", _RNA_LABELS) ax_f1.set_title("F1 score by RNA family", fontsize=10) _add_panel_label(ax_f1, "B") # Panel C: Wall time time_data = [] time_colors = [] time_labels = [] for m in methods: times = [c["wall_time"] for c in br_cases if c.get("method") == m and c.get("wall_time", 0) > 0] if times: time_data.append(times) time_colors.append(_method_color(m)) time_labels.append(_method_label(m)) if time_data: bp = ax_time.boxplot( time_data, patch_artist=True, widths=0.6, medianprops={"color": "black", "linewidth": 1.2}, ) for patch, c in zip(bp["boxes"], time_colors): patch.set_facecolor(c) patch.set_alpha(0.7) ax_time.set_yscale("log") ax_time.set_xticks(range(1, len(time_labels) + 1)) ax_time.set_xticklabels(time_labels, fontsize=8, rotation=45, ha="right") ax_time.set_ylabel("Wall time (s)") ax_time.set_title("Alignment time", fontsize=10) else: ax_time.text(0.5, 0.5, "No timing data", ha="center", va="center", transform=ax_time.transAxes, fontsize=10, color="#999999") _add_panel_label(ax_time, "C") _shared_legend(fig, methods) fig.tight_layout(rect=[0, 0.05, 1, 1]) _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 3: Phylogenetic tree accuracy # --------------------------------------------------------------------------- def figure_phylo_accuracy(cases: list[dict], output_path: Path) -> None: """Phylogenetic tree accuracy. Panel A: nRF box plots per method. Panel B: nRF vs tree depth with CI bands. Panel C: Branch score distance box plots. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() fig, (ax_nrf, ax_depth, ax_bsd) = plt.subplots(1, 3, figsize=(14, 5)) valid = [c for c in cases if "error" not in c] available = {c["method"] for c in valid} methods = _ordered_methods(available | ({"true"} & available)) # Panel A: nRF box plots if methods: box_data = [] colors = [] labels = [] for m in methods: vals = [c["nrf"] for c in valid if c.get("method") == m] box_data.append(vals if vals else [0]) colors.append(_method_color(m)) labels.append(_method_label(m)) bp = ax_nrf.boxplot( box_data, patch_artist=True, widths=0.6, medianprops={"color": "black", "linewidth": 1.5}, ) for patch, c in zip(bp["boxes"], colors): patch.set_facecolor(c) patch.set_alpha(0.7) ax_nrf.set_xticks(range(1, len(methods) + 1)) ax_nrf.set_xticklabels(labels, rotation=45, ha="right", fontsize=7) ax_nrf.set_ylabel("nRF distance") _add_panel_label(ax_nrf, "A") # Panel B: nRF vs tree depth import re as _re for m in methods: depth_nrf: dict[float, list[float]] = defaultdict(list) for c in valid: if c.get("method") != m: continue d = c.get("tree_depth") if d is None: dm = _re.search(r"_d([\d.]+)_", c.get("sim_id", "")) if dm: d = float(dm.group(1)) nrf = c.get("nrf") if d is not None and nrf is not None: depth_nrf[d].append(nrf) if depth_nrf: depths = sorted(depth_nrf.keys()) means = [float(np.mean(depth_nrf[d])) for d in depths] stds = [float(np.std(depth_nrf[d])) for d in depths] lo = [max(0, mu - s) for mu, s in zip(means, stds)] hi = [mu + s for mu, s in zip(means, stds)] color = _method_color(m) ax_depth.plot(depths, means, marker="o", color=color, label=_method_label(m), markersize=4) ax_depth.fill_between(depths, lo, hi, color=color, alpha=0.07) ax_depth.set_xlabel("Tree depth (subs/site)") ax_depth.set_ylabel("Mean nRF distance") handles, labels = ax_depth.get_legend_handles_labels() if handles: ax_depth.legend(fontsize=6, ncol=2, loc="upper left") _add_panel_label(ax_depth, "B") # Panel C: Branch score distance if methods: bsd_data = [] colors = [] labels = [] for m in methods: vals = [c["branch_score_dist"] for c in valid if c.get("method") == m and "branch_score_dist" in c] bsd_data.append(vals if vals else [0]) colors.append(_method_color(m)) labels.append(_method_label(m)) bp = ax_bsd.boxplot( bsd_data, patch_artist=True, widths=0.6, medianprops={"color": "black", "linewidth": 1.5}, ) for patch, c in zip(bp["boxes"], colors): patch.set_facecolor(c) patch.set_alpha(0.7) ax_bsd.set_xticks(range(1, len(methods) + 1)) ax_bsd.set_xticklabels(labels, rotation=45, ha="right", fontsize=7) ax_bsd.set_ylabel("Branch score distance") _add_panel_label(ax_bsd, "C") fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 4: Positive selection detection # --------------------------------------------------------------------------- def figure_positive_selection(cases: list[dict], output_path: Path) -> None: """Positive selection detection. Panel A: Precision/Recall/F1 bars per method. Panel B: F1 vs tree depth. Panel C: F1 vs indel rate. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np import re as _re _setup_figure_style() fig, (ax_bar, ax_depth, ax_indel) = plt.subplots(1, 3, figsize=(14, 5)) valid = [c for c in cases if "error" not in c] by_method: dict[str, list[dict]] = defaultdict(list) for c in valid: by_method[c.get("method", "")].append(c) available = set(by_method.keys()) methods = _ordered_methods(available | ({"true"} & available)) # Panel A: bar chart if methods: n_methods = len(methods) x_pos = np.arange(n_methods) width = 0.25 prec_means = [np.mean([c.get("precision", 0) for c in by_method[m]]) for m in methods] rec_means = [np.mean([c.get("recall", c.get("tpr", 0)) for c in by_method[m]]) for m in methods] f1_means = [np.mean([c.get("f1", 0) for c in by_method[m]]) for m in methods] colors_prec = [_method_color(m) for m in methods] ax_bar.bar(x_pos - width, prec_means, width, label="Precision", color=colors_prec, alpha=0.8) ax_bar.bar(x_pos, rec_means, width, label="Recall", color=colors_prec, alpha=0.55) ax_bar.bar(x_pos + width, f1_means, width, label="F1", color=colors_prec, alpha=0.3) ax_bar.set_xticks(x_pos) ax_bar.set_xticklabels( [_method_label(m) for m in methods], rotation=45, ha="right", fontsize=7 ) ax_bar.set_ylabel("Score") all_vals = list(prec_means) + list(rec_means) + list(f1_means) if all_vals: ax_bar.set_ylim(0, max(all_vals) * 1.15) ax_bar.legend(fontsize=7) _add_panel_label(ax_bar, "A") # Panel B: F1 vs tree depth for m in methods: depth_f1: dict[float, list[float]] = defaultdict(list) for c in by_method[m]: d = c.get("tree_depth") if d is None: dm = _re.search(r"_d([\d.]+)_", c.get("sim_id", "")) if dm: d = float(dm.group(1)) f1 = c.get("f1") if d is not None and f1 is not None: depth_f1[d].append(f1) if depth_f1: depths_sorted = sorted(depth_f1.keys()) means = [np.mean(depth_f1[d]) for d in depths_sorted] ax_depth.plot(depths_sorted, means, marker="o", color=_method_color(m), label=_method_label(m)) ax_depth.set_xlabel("Tree depth (subs/site)") ax_depth.set_ylabel("F1 score") handles, _ = ax_depth.get_legend_handles_labels() if handles: ax_depth.legend(fontsize=6, ncol=2, loc="best") _add_panel_label(ax_depth, "B") # Panel C: F1 vs indel rate for m in methods: indel_f1: dict[float, list[float]] = defaultdict(list) for c in by_method[m]: ir = c.get("indel_rate") if ir is None: irm = _re.search(r"_ir([\d.]+)_", c.get("sim_id", "")) if irm: ir = float(irm.group(1)) f1 = c.get("f1") if ir is not None and f1 is not None: indel_f1[ir].append(f1) if indel_f1: rates_sorted = sorted(indel_f1.keys()) means = [np.mean(indel_f1[r]) for r in rates_sorted] ax_indel.plot(rates_sorted, means, marker="o", color=_method_color(m), label=_method_label(m)) ax_indel.set_xlabel("Indel rate") ax_indel.set_ylabel("F1 score") handles, _ = ax_indel.get_legend_handles_labels() if handles: ax_indel.legend(fontsize=6, ncol=2, loc="best") _add_panel_label(ax_indel, "C") fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 5: HMMER homology detection # --------------------------------------------------------------------------- def figure_hmmer_detection(cases: list[dict], output_path: Path) -> None: """HMMER homology detection — single bar chart with bootstrap CIs.""" import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() fig, ax = plt.subplots(1, 1, figsize=(5, 4)) valid = [c for c in cases if "error" not in c] by_method: dict[str, list[dict]] = defaultdict(list) for c in valid: by_method[c.get("method", "")].append(c) available = set(by_method.keys()) methods = _ordered_methods(available) def _sens(case: dict) -> float: if "sensitivity" in case: return case["sensitivity"] h = case.get("hits_at_1e3", {}) return h.get("sens", 0.0) if isinstance(h, dict) else 0.0 if methods: means = [] ci_lo = [] ci_hi = [] colors = [] for m in methods: sens_vals = [_sens(c) for c in by_method[m]] mu = float(np.mean(sens_vals)) if sens_vals else 0.0 means.append(mu) if len(sens_vals) >= 2: _, lo, hi = _bootstrap_ci_vec(sens_vals, n_bootstrap=2000) ci_lo.append(mu - lo) ci_hi.append(hi - mu) else: ci_lo.append(0) ci_hi.append(0) colors.append(_method_color(m)) x = np.arange(len(methods)) ax.bar( x, means, color=colors, alpha=0.8, yerr=[ci_lo, ci_hi], capsize=3, ecolor="#333333", ) ax.set_xticks(x) ax.set_xticklabels( [_method_label(m) for m in methods], rotation=45, ha="right", fontsize=8 ) ax.set_ylabel("Mean sensitivity (E < 1e-5)") if means and max(means) > 0: ymin_bar = max(0, min(means) - 0.05) ymax = min(1.0, max(mu + h for mu, h in zip(means, ci_hi)) * 1.02) ax.set_ylim(ymin_bar, ymax) ax.set_title("HMMER homology detection (50 Pfam families)", fontsize=10) fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 6: Speed comparison (aggregated across all pipelines) # --------------------------------------------------------------------------- def figure_speed_comparison(all_timed: list[dict], output_path: Path) -> None: """Speed and memory comparison aggregated across all pipelines. Panel A: Wall time box plots (log scale) per method. Panel B: Peak memory box plots per method. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() fig, (ax_time, ax_mem) = plt.subplots(1, 2, figsize=(10, 5)) by_method: dict[str, list[dict]] = defaultdict(list) for r in all_timed: m = r.get("method") if m: by_method[m].append(r) available = set(by_method.keys()) methods = _ordered_methods(available) # Panel A: Wall time if methods: time_data = [] colors = [] labels = [] for m in methods: # Try multiple time field names times = [] for c in by_method[m]: t = c.get("wall_time_align") or c.get("wall_time") if t and t > 0: times.append(t) time_data.append(times if times else [0.001]) colors.append(_method_color(m)) labels.append(_method_label(m)) bp = ax_time.boxplot( time_data, patch_artist=True, widths=0.6, medianprops={"color": "black", "linewidth": 1.5}, ) for patch, c in zip(bp["boxes"], colors): patch.set_facecolor(c) patch.set_alpha(0.7) ax_time.set_yscale("log") ax_time.set_xticks(range(1, len(methods) + 1)) ax_time.set_xticklabels(labels, rotation=45, ha="right", fontsize=7) ax_time.set_ylabel("Wall time (s)") _add_panel_label(ax_time, "A") # Panel B: Peak memory if methods: mem_data = [] colors = [] labels = [] for m in methods: mems = [c.get("peak_memory_mb", 0) for c in by_method[m] if c.get("peak_memory_mb", 0) > 0] mem_data.append(mems if mems else [0]) colors.append(_method_color(m)) labels.append(_method_label(m)) bp = ax_mem.boxplot( mem_data, patch_artist=True, widths=0.6, medianprops={"color": "black", "linewidth": 1.5}, ) for patch, c in zip(bp["boxes"], colors): patch.set_facecolor(c) patch.set_alpha(0.7) ax_mem.set_xticks(range(1, len(methods) + 1)) ax_mem.set_xticklabels(labels, rotation=45, ha="right", fontsize=7) ax_mem.set_ylabel("Peak memory (MB)") _add_panel_label(ax_mem, "B") fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Figure 7: Summary heatmap # --------------------------------------------------------------------------- def figure_summary_heatmap(all_results: dict, output_path: Path) -> None: """Summary heatmap across all pipelines. Rows = methods, columns = key metrics per pipeline. Colour-coded: green=best, red=worst. Annotated with raw values. Parameters ---------- all_results : dict Mapping with optional keys for each pipeline. Each value is a dict mapping method names to metric values (float). """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() # Columns: (label, data_dict, higher_is_better) columns_spec: list[tuple[str, dict[str, float], bool]] = [] # Alignment accuracy F1 aln = all_results.get("alignment_accuracy") if aln: columns_spec.append(("Aln F1", aln, True)) # Calibration ECE cal = all_results.get("calibration") if cal: columns_spec.append(("ECE", cal, False)) # Positive selection F1 psel = all_results.get("positive_selection") if psel: columns_spec.append(("Sel. F1", psel, True)) # Phylo nRF phylo = all_results.get("phylo_accuracy") if phylo: columns_spec.append(("nRF", phylo, False)) # HMMER sensitivity hmmer = all_results.get("hmmer_detection") if hmmer: columns_spec.append(("HMMER sens.", hmmer, True)) # Speed speed = all_results.get("speed") if speed: columns_spec.append(("Speed (s)", speed, False)) if not columns_spec: logger.warning("figure_summary_heatmap: no data, skipping.") return # Collect union of methods all_methods: set[str] = set() for _, mdict, _ in columns_spec: all_methods.update(mdict.keys()) methods = _ordered_methods(all_methods) if not methods: methods = sorted(all_methods) n_methods = len(methods) n_cols = len(columns_spec) raw = np.full((n_methods, n_cols), np.nan) for j, (_, mdict, _) in enumerate(columns_spec): for i, m in enumerate(methods): if m in mdict: raw[i, j] = mdict[m] # Normalise each column to [0, 1] (1 = best) normed = np.full_like(raw, np.nan) for j, (_, _, higher_better) in enumerate(columns_spec): col = raw[:, j] valid_mask = ~np.isnan(col) if valid_mask.sum() < 2: normed[valid_mask, j] = 0.5 continue vmin = np.nanmin(col) vmax = np.nanmax(col) if vmax - vmin < 1e-12: normed[valid_mask, j] = 0.5 else: scaled = (col[valid_mask] - vmin) / (vmax - vmin) if not higher_better: scaled = 1.0 - scaled normed[valid_mask, j] = scaled fig, ax = plt.subplots(figsize=(max(5, 1.7 * n_cols + 2), 0.65 * n_methods + 1.5)) cmap = plt.get_cmap("RdYlGn") display = np.where(np.isnan(normed), 0.5, normed) im = ax.imshow(display, cmap=cmap, aspect="auto", vmin=0, vmax=1) for i in range(n_methods): for j in range(n_cols): val = raw[i, j] if np.isnan(val): text = "-" elif abs(val) < 0.01 or abs(val) >= 1000: text = f"{val:.2e}" else: text = f"{val:.3f}" ax.text( j, i, text, ha="center", va="center", fontsize=7, color="black" if 0.3 < display[i, j] < 0.7 else "white", ) ax.set_xticks(range(n_cols)) ax.set_xticklabels([name for name, _, _ in columns_spec], fontsize=9) ax.set_yticks(range(n_methods)) ax.set_yticklabels([_method_label(m) for m in methods], fontsize=8) cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.04) cbar.set_label("Normalised score (1 = best)", fontsize=8) cbar.ax.tick_params(labelsize=7) fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Calibration figure (for calibration pipeline) # --------------------------------------------------------------------------- def figure_calibration(results: dict, output_path: Path) -> None: """Confidence calibration reliability diagram. Panel A: Calibration curve (predicted confidence vs fraction correct). Panel B: Histogram of confidence values. """ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np _setup_figure_style() fig, (ax_cal, ax_hist) = plt.subplots(1, 2, figsize=(10, 5)) ax_cal.plot([0, 1], [0, 1], ls="--", color="#aaaaaa", lw=1.0, label="Perfect") for method, data in results.items(): bins = data.get("bins") if not bins: continue midpoints = np.array([(b["bin_low"] + b["bin_high"]) / 2 for b in bins]) accuracies = np.array([b.get("accuracy", 0.0) for b in bins]) ece = data.get("ece") brier = data.get("brier_score") label_parts = [_method_label(method)] if ece is not None: label_parts.append(f"ECE={ece:.3f}") if brier is not None: label_parts.append(f"Brier={brier:.3f}") ax_cal.plot( midpoints, accuracies, marker="o", color=_method_color(method), label=" ".join(label_parts), ) ax_cal.set_xlabel("Predicted confidence") ax_cal.set_ylabel("Observed accuracy") ax_cal.set_xlim(0, 1) ax_cal.set_ylim(0, 1) ax_cal.set_aspect("equal") ax_cal.legend(loc="lower right", fontsize=7) _add_panel_label(ax_cal, "A") for method, data in results.items(): bins = data.get("bins") if not bins: continue midpoints = np.array([(b["bin_low"] + b["bin_high"]) / 2 for b in bins]) counts = np.array([b.get("n_pairs", 0) for b in bins], dtype=float) total = counts.sum() if total > 0: counts = counts / total widths = np.array([b["bin_high"] - b["bin_low"] for b in bins]) ax_hist.bar( midpoints, counts, width=widths * 0.85, alpha=0.5, color=_method_color(method), label=_method_label(method), ) ax_hist.set_xlabel("Confidence score") ax_hist.set_ylabel("Fraction of residue pairs") ax_hist.set_xlim(0, 1) ax_hist.legend(loc="upper left", fontsize=7) _add_panel_label(ax_hist, "B") fig.tight_layout() _savefig(fig, output_path) # --------------------------------------------------------------------------- # Aggregation helpers for heatmap # --------------------------------------------------------------------------- def _aggregate_for_heatmap( pipeline_results: list[dict], method_key: str, metric_key: str, ) -> dict[str, float]: """Compute mean of *metric_key* per method from a list of per-case dicts.""" import numpy as np by_method: dict[str, list[float]] = defaultdict(list) parts = metric_key.split(".") for case in pipeline_results: m = case.get(method_key) if m is None: continue val = case for p in parts: if isinstance(val, dict): val = val.get(p) else: val = None break if val is not None: try: by_method[m].append(float(val)) except (TypeError, ValueError): pass return {m: float(np.mean(vs)) for m, vs in by_method.items() if vs} def _adapt_calibration_summary(cal_methods: dict) -> dict: """Convert calibration summary format to the bins format expected by figure_calibration.""" import math adapted: dict = {} for method, data in cal_methods.items(): if not isinstance(data, dict): continue cc = data.get("calibration_curve") if not isinstance(cc, dict): adapted[method] = data continue centers = cc.get("bin_centers", []) counts = cc.get("bin_counts", []) fracs = cc.get("fraction_correct", []) if not centers: adapted[method] = data continue bins = [] for i, center in enumerate(centers): if len(centers) > 1: width = centers[1] - centers[0] if i == 0 else centers[i] - centers[i - 1] else: width = 0.1 acc = fracs[i] if i < len(fracs) else 0.0 if isinstance(acc, float) and math.isnan(acc): acc = 0.0 bins.append({ "bin_low": center - width / 2, "bin_high": center + width / 2, "n_pairs": counts[i] if i < len(counts) else 0, "accuracy": acc, }) adapted[method] = { "ece": data.get("ece"), "brier_score": data.get("brier_score"), "bins": bins, } return adapted # --------------------------------------------------------------------------- # Top-level entry point # --------------------------------------------------------------------------- def generate_all_figures( results_dir: str | Path, output_dir: str | Path, ) -> None: """Generate all publication figures from saved results. Reads ``latest.json`` from each pipeline's results sub-directory and calls the corresponding figure function. Skips pipelines whose results are not available. """ results_dir = Path(results_dir) output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) generated: list[str] = [] # --- Fig 1 & 2: Alignment accuracy (BAliBASE + BRAliBASE) ---------------- aln_data = _load_latest_json(results_dir / "alignment_accuracy") aln_cases = _extract_cases(aln_data) if aln_data else [] aln_valid = [c for c in aln_cases if "error" not in c and not c.get("skipped")] if aln_valid: path = output_dir / "fig1_balibase_accuracy.pdf" try: figure_balibase(aln_valid, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate BAliBASE figure: %s", exc) path = output_dir / "fig2_bralibase_rna.pdf" try: figure_bralibase(aln_valid, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate BRAliBASE figure: %s", exc) # --- Fig 3: Phylo accuracy ----------------------------------------------- phylo_data = _load_latest_json(results_dir / "phylo_accuracy") phylo_cases = _extract_cases(phylo_data) if phylo_cases: path = output_dir / "fig3_phylo_accuracy.pdf" try: figure_phylo_accuracy(phylo_cases, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate phylo accuracy figure: %s", exc) # --- Fig 4: Positive selection ------------------------------------------- psel_data = _load_latest_json(results_dir / "positive_selection") psel_cases = _extract_cases(psel_data) if psel_cases: path = output_dir / "fig4_positive_selection.pdf" try: figure_positive_selection(psel_cases, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate positive selection figure: %s", exc) # --- Fig 5: HMMER detection ---------------------------------------------- hmmer_data = _load_latest_json(results_dir / "hmmer_detection") hmmer_cases = _extract_cases(hmmer_data) if hmmer_cases: path = output_dir / "fig5_hmmer_detection.pdf" try: figure_hmmer_detection(hmmer_cases, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate HMMER detection figure: %s", exc) # --- Fig 6: Speed comparison (aggregated) -------------------------------- all_timed: list[dict] = [] # Collect timing data from alignment_accuracy + downstream pipelines for c in aln_valid: if "wall_time" in c: all_timed.append(c) for subdir in ("positive_selection", "phylo_accuracy"): sub_data = _load_latest_json(results_dir / subdir) sub_cases = _extract_cases(sub_data) all_timed.extend(c for c in sub_cases if "wall_time_align" in c or "wall_time" in c) if all_timed: path = output_dir / "fig6_speed_comparison.pdf" try: figure_speed_comparison(all_timed, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate speed comparison figure: %s", exc) # --- Fig 7: Summary heatmap ---------------------------------------------- heatmap_data: dict[str, dict[str, float]] = {} # Alignment accuracy F1 if aln_valid: agg = _aggregate_for_heatmap(aln_valid, "method", "f1") if agg: heatmap_data["alignment_accuracy"] = agg # Calibration ECE cal_data = _load_latest_json(results_dir / "calibration") if cal_data is not None: cal_src = None if isinstance(cal_data, dict): cal_src = cal_data.get("methods") or cal_data.get("summary") or cal_data if isinstance(cal_src, dict): ece_map = {} for m, v in cal_src.items(): if isinstance(v, dict) and "ece" in v: ece_map[m] = v["ece"] if ece_map: heatmap_data["calibration"] = ece_map # Calibration figure (standalone) if cal_data is not None: cal_methods = None if isinstance(cal_data, dict): cal_methods = cal_data.get("methods") or cal_data.get("summary") if cal_methods is None: cal_methods = cal_data if isinstance(cal_methods, dict): cal_methods = _adapt_calibration_summary(cal_methods) if cal_methods: path = output_dir / "fig_calibration.pdf" try: figure_calibration(cal_methods, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate calibration figure: %s", exc) # Positive selection F1 if psel_cases: agg = _aggregate_for_heatmap(psel_cases, "method", "f1") if agg: heatmap_data["positive_selection"] = agg # Phylo nRF if phylo_cases: agg = _aggregate_for_heatmap(phylo_cases, "method", "nrf") if agg: heatmap_data["phylo_accuracy"] = agg # HMMER sensitivity if hmmer_cases: agg = _aggregate_for_heatmap(hmmer_cases, "method", "sensitivity") if agg: heatmap_data["hmmer_detection"] = agg # Speed (mean wall time) if all_timed: # Normalise field name for c in all_timed: if "wall_time_align" in c and "wall_time" not in c: c["wall_time"] = c["wall_time_align"] agg = _aggregate_for_heatmap(all_timed, "method", "wall_time") if agg: heatmap_data["speed"] = agg if heatmap_data: path = output_dir / "fig7_summary_heatmap.pdf" try: figure_summary_heatmap(heatmap_data, path) generated.append(str(path)) except Exception as exc: logger.error("Failed to generate summary heatmap: %s", exc) # --- Summary -------------------------------------------------------------- if generated: print(f"Generated {len(generated)} figures:") for p in generated: print(f" {p}") else: print("No figures generated (no result data found).") kalign-3.5.1/benchmarks/downstream/hmmer_detection.py000066400000000000000000001143531515023132300230070ustar00rootroot00000000000000"""Pipeline 3: HMMER Homology Detection benchmark. Measures how alignment quality affects profile HMM sensitivity. For each Pfam seed family the pipeline strips gaps from the curated seed alignment, re-aligns using each method, builds a profile HMM with ``hmmbuild``, and searches a combined database with ``hmmsearch``. True/false positive rates are computed against known family memberships. """ from __future__ import annotations import json import logging import os import random import shutil import subprocess import tempfile import time from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Dict, List, Optional logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Pfam family list (hardcoded, well-known families) # --------------------------------------------------------------------------- PFAM_FAMILIES = [ "PF00001", # 7tm_1 (GPCR) "PF00002", # 7tm_2 "PF00003", # 7tm_3 "PF00004", # AAA (ATPase) "PF00005", # ABC_tran "PF00009", # GTP_EFTU "PF00010", # HLH "PF00012", # HSP70 "PF00013", # KH domain "PF00014", # Kunitz_BPTI "PF00018", # SH3 "PF00022", # Actin "PF00023", # Ank "PF00027", # cNMP_binding "PF00028", # Cadherin "PF00036", # EF-hand "PF00041", # Fibronectin_3 "PF00042", # Globin "PF00043", # GST_C "PF00046", # Homeodomain "PF00047", # Ig "PF00048", # IL8 "PF00050", # Kazal "PF00056", # LDLA "PF00069", # Pkinase "PF00071", # Ras "PF00072", # Response_reg "PF00076", # RRM "PF00078", # RVT "PF00079", # Serpin "PF00081", # Sod_Fe_N "PF00082", # Subtilisin "PF00083", # Sugar_tr "PF00084", # Sushi "PF00085", # Thioredoxin "PF00089", # Trypsin "PF00096", # Zinc_finger_C2H2 "PF00100", # Zona_pellucida "PF00102", # Y_phosphatase "PF00104", # Hormone_recep "PF00106", # adh_short "PF00107", # ADH_zinc_N "PF00109", # ketoacyl-synt "PF00111", # Fer2 "PF00112", # Pepsin "PF00115", # COX1 "PF00118", # Cpn60_TCP1 "PF00119", # ATP-synt_A "PF00120", # Glu_synthase "PF00121", # TIM ] # --------------------------------------------------------------------------- # Result dataclasses # --------------------------------------------------------------------------- @dataclass class HmmerCaseResult: """Result for a single family x method evaluation.""" family_id: str method: str true_positives: int false_positives: int false_negatives: int sensitivity: float # TP / (TP + FN) specificity: float # precision = TP / (TP + FP) e_value_threshold: float hmm_length: int # number of match states in the profile wall_time_align: float wall_time_hmmbuild: float wall_time_hmmsearch: float peak_memory_mb: float @dataclass class HmmerResult: """Aggregated result for the full HMMER detection pipeline.""" provenance: dict cases: List[dict] summary: dict # per-method aggregated metrics # --------------------------------------------------------------------------- # Pfam seed download / cache # --------------------------------------------------------------------------- _PFAM_SEED_URL_TEMPLATE = ( "https://www.ebi.ac.uk/interpro/api/entry/pfam/{accession}" "?annotation=alignment:seed" ) def _download_pfam_seeds( data_dir: Path, n_families: int = 50, ) -> List[dict]: """Download (or load from cache) Pfam seed alignments for benchmarking. Attempts to download seed alignments from the InterPro API for each family in :data:`PFAM_FAMILIES`. Successfully downloaded alignments are cached as FASTA files under ``data_dir/pfam_seed/``. If a download fails the cached version is used if available. Parameters ---------- data_dir : Path Base data directory. Seed files are stored in ``data_dir/pfam_seed/.fasta``. n_families : int Maximum number of families to return. Returns ------- list[dict] Each dict has keys ``family_id``, ``seed_fasta_path``, ``n_members``. """ import urllib.request import urllib.error seed_dir = Path(data_dir) / "pfam_seed" seed_dir.mkdir(parents=True, exist_ok=True) families_to_fetch = PFAM_FAMILIES[:n_families] results: List[dict] = [] for accession in families_to_fetch: fasta_path = seed_dir / f"{accession}.fasta" # Try to use cache first if fasta_path.exists() and fasta_path.stat().st_size > 0: names, _seqs = _parse_fasta_simple(fasta_path) if names: results.append( { "family_id": accession, "seed_fasta_path": str(fasta_path), "n_members": len(names), } ) logger.debug("Using cached seed for %s (%d seqs)", accession, len(names)) continue # Download from InterPro url = _PFAM_SEED_URL_TEMPLATE.format(accession=accession) logger.info("Downloading seed alignment for %s", accession) try: req = urllib.request.Request(url) with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read() # The InterPro API returns gzip-compressed Stockholm; # decompress if needed. import gzip as _gzip try: content = _gzip.decompress(raw) except (OSError, _gzip.BadGzipFile): content = raw # The API may return Stockholm or raw alignment text. # Convert to FASTA for uniform handling. text = content.decode("utf-8", errors="replace") names, seqs = _stockholm_or_text_to_fasta(text) if not names: logger.warning("No sequences parsed for %s", accession) continue # Strip gaps to get raw sequences, then write aligned FASTA # (we keep the aligned version since hmmbuild can use it) _write_fasta_simple(fasta_path, names, seqs) results.append( { "family_id": accession, "seed_fasta_path": str(fasta_path), "n_members": len(names), } ) logger.info("Downloaded seed for %s (%d seqs)", accession, len(names)) except (urllib.error.URLError, OSError, ValueError) as exc: logger.warning("Failed to download %s: %s", accession, exc) # If there is no cache either, skip this family continue logger.info( "Loaded %d / %d Pfam seed families", len(results), len(families_to_fetch), ) return results # --------------------------------------------------------------------------- # Stockholm / text parsing helpers # --------------------------------------------------------------------------- def _stockholm_or_text_to_fasta(text: str) -> tuple[list[str], list[str]]: """Parse Stockholm or aligned-text format into (names, sequences). Handles: - Stockholm format (lines with ``#=...`` annotations, ``//`` terminator) - Plain aligned FASTA - Clustal-like blocks """ lines = text.splitlines() # Detect Stockholm if any(line.strip().startswith("# STOCKHOLM") for line in lines[:5]): return _parse_stockholm_text(lines) # Detect FASTA if any(line.strip().startswith(">") for line in lines[:10]): return _parse_fasta_lines(lines) # Fallback: treat as space-separated name+sequence blocks names: list[str] = [] seqs: list[str] = [] seq_dict: dict[str, list[str]] = {} name_order: list[str] = [] for line in lines: line = line.strip() if not line or line.startswith("#") or line.startswith("//"): continue parts = line.split(None, 1) if len(parts) == 2: name, seq_part = parts if name not in seq_dict: seq_dict[name] = [] name_order.append(name) seq_dict[name].append(seq_part.replace(" ", "")) for name in name_order: names.append(name) seqs.append("".join(seq_dict[name])) return names, seqs def _parse_stockholm_text(lines: list[str]) -> tuple[list[str], list[str]]: """Parse Stockholm format lines into (names, sequences).""" seq_dict: dict[str, list[str]] = {} name_order: list[str] = [] for line in lines: line = line.rstrip() if not line or line.startswith("#") or line.startswith("//"): continue parts = line.split(None, 1) if len(parts) == 2: name, seq_part = parts if name not in seq_dict: seq_dict[name] = [] name_order.append(name) seq_dict[name].append(seq_part.replace(" ", "")) names = [] seqs = [] for name in name_order: names.append(name) seqs.append("".join(seq_dict[name])) return names, seqs def _parse_fasta_lines(lines: list[str]) -> tuple[list[str], list[str]]: """Parse FASTA from a list of lines.""" names: list[str] = [] seqs: list[str] = [] current: list[str] = [] for line in lines: line = line.rstrip() if line.startswith(">"): if current: seqs.append("".join(current)) current = [] name = line[1:].split()[0] if line[1:].strip() else "" names.append(name) else: current.append(line.strip()) if current: seqs.append("".join(current)) return names, seqs def _parse_fasta_simple(path: Path) -> tuple[list[str], list[str]]: """Parse a FASTA file from disk. Returns (names, sequences).""" names: list[str] = [] seqs: list[str] = [] current: list[str] = [] with open(path) as fh: for line in fh: line = line.rstrip("\n\r") if line.startswith(">"): if current: seqs.append("".join(current)) current = [] names.append(line[1:].split()[0] if line[1:].strip() else "") else: current.append(line.strip()) if current: seqs.append("".join(current)) return names, seqs def _write_fasta_simple( path: Path, names: list[str], sequences: list[str] ) -> None: """Write sequences in FASTA format.""" with open(path, "w") as fh: for name, seq in zip(names, sequences): fh.write(f">{name}\n") for i in range(0, len(seq), 80): fh.write(seq[i : i + 80] + "\n") def _strip_gaps(sequences: list[str]) -> list[str]: """Remove gap characters from sequences.""" return [s.replace("-", "").replace(".", "") for s in sequences] # --------------------------------------------------------------------------- # hmmbuild wrapper # --------------------------------------------------------------------------- def _run_hmmbuild(alignment_path: Path, hmm_path: Path) -> dict: """Run ``hmmbuild`` to construct a profile HMM from an alignment. Parameters ---------- alignment_path : Path Input alignment (FASTA or Stockholm). hmm_path : Path Output HMM file path. Returns ------- dict ``hmm_path``, ``hmm_length``, ``nseq``, ``wall_time``. Raises ------ RuntimeError If ``hmmbuild`` is not found or exits with an error. """ hmmbuild_bin = shutil.which("hmmbuild") if hmmbuild_bin is None: raise RuntimeError( "hmmbuild is not installed or not on PATH. " "Install HMMER3 (http://hmmer.org/)." ) start = time.perf_counter() proc = subprocess.run( [hmmbuild_bin, "--amino", str(hmm_path), str(alignment_path)], capture_output=True, text=True, ) wall = time.perf_counter() - start if proc.returncode != 0: raise RuntimeError( f"hmmbuild failed (exit {proc.returncode}): {proc.stderr}" ) # Parse hmmbuild output for HMM length and nseq hmm_length = 0 nseq = 0 for line in proc.stdout.splitlines(): # Example: "Number of sequences: 42" if "umber of sequences" in line: parts = line.split() for part in reversed(parts): try: nseq = int(part) break except ValueError: continue # Example: "Model length: 285" # Also appears in HMM header: "LENG 285" if "odel length" in line or line.strip().startswith("LENG"): parts = line.split() for part in reversed(parts): try: hmm_length = int(part) break except ValueError: continue # Fallback: parse the HMM file header for LENG if hmm_length == 0 and Path(hmm_path).exists(): with open(hmm_path) as fh: for hline in fh: if hline.startswith("LENG"): parts = hline.split() if len(parts) >= 2: try: hmm_length = int(parts[1]) except ValueError: pass break return { "hmm_path": str(hmm_path), "hmm_length": hmm_length, "nseq": nseq, "wall_time": wall, } # --------------------------------------------------------------------------- # hmmsearch wrapper # --------------------------------------------------------------------------- def _run_hmmsearch( hmm_path: Path, target_db: Path, output_path: Path, e_value: float = 1e-5, ) -> dict: """Run ``hmmsearch`` to search a profile HMM against a sequence database. Parameters ---------- hmm_path : Path Profile HMM file. target_db : Path FASTA database to search. output_path : Path Path for the ``--tblout`` tabular output. e_value : float E-value threshold for reporting hits. Returns ------- dict ``hits`` (list of dicts with name, e_value, score), ``n_hits``, ``wall_time``. """ hmmsearch_bin = shutil.which("hmmsearch") if hmmsearch_bin is None: raise RuntimeError( "hmmsearch is not installed or not on PATH. " "Install HMMER3 (http://hmmer.org/)." ) start = time.perf_counter() proc = subprocess.run( [ hmmsearch_bin, "--tblout", str(output_path), "--noali", "-E", str(e_value), str(hmm_path), str(target_db), ], capture_output=True, text=True, ) wall = time.perf_counter() - start if proc.returncode != 0: raise RuntimeError( f"hmmsearch failed (exit {proc.returncode}): {proc.stderr}" ) # Parse tblout hits = _parse_tblout(output_path) return { "hits": hits, "n_hits": len(hits), "wall_time": wall, } def _parse_tblout(path: Path) -> List[dict]: """Parse HMMER3 ``--tblout`` tabular output. Columns (space-separated, first 6): target_name accession query_name accession E-value score bias ... Comment lines start with ``#``. Returns a list of dicts, each with ``name``, ``e_value``, ``score``. """ hits: list[dict] = [] if not Path(path).exists(): return hits with open(path) as fh: for line in fh: if line.startswith("#"): continue parts = line.split() if len(parts) < 6: continue try: hit = { "name": parts[0], "e_value": float(parts[4]), "score": float(parts[5]), } hits.append(hit) except (ValueError, IndexError): continue return hits # --------------------------------------------------------------------------- # Search database preparation # --------------------------------------------------------------------------- def _prepare_search_db( families: List[dict], data_dir: Path, ) -> Path: """Create a combined FASTA search database from family seed sequences. Pools seed sequences from all loaded families. Each sequence is labelled ``__`` so that true positives can be identified by family membership. Negative sequences are drawn from other families -- i.e. for any given query family, members of the remaining families act as negatives. Parameters ---------- families : list[dict] Output of :func:`_download_pfam_seeds`. data_dir : Path Directory for writing the search DB. Returns ------- Path Path to the combined FASTA database. """ db_path = Path(data_dir) / "search_db.fasta" # If already exists and non-empty, check it covers all current families if db_path.exists() and db_path.stat().st_size > 0: needed = {f["family_id"] for f in families} present: set[str] = set() with open(db_path) as fh: for line in fh: if line.startswith(">"): fam_id = line[1:].split("__")[0] present.add(fam_id) if needed <= present: logger.info("Reusing existing search DB: %s (%d families)", db_path, len(present)) return db_path logger.info( "Search DB has %d families but %d needed — rebuilding", len(present & needed), len(needed), ) logger.info("Building combined search database from %d families", len(families)) all_names: list[str] = [] all_seqs: list[str] = [] for fam in families: family_id = fam["family_id"] fasta_path = Path(fam["seed_fasta_path"]) if not fasta_path.exists(): continue names, seqs = _parse_fasta_simple(fasta_path) # Strip gaps to get unaligned sequences seqs = _strip_gaps(seqs) for name, seq in zip(names, seqs): # Label with family for ground truth tracking labelled_name = f"{family_id}__{name}" all_names.append(labelled_name) all_seqs.append(seq) if not all_names: raise RuntimeError("No sequences available to build search database") _write_fasta_simple(db_path, all_names, all_seqs) logger.info( "Search database written: %s (%d sequences)", db_path, len(all_names), ) return db_path def _get_family_members(search_db: Path, family_id: str) -> set[str]: """Return the set of sequence names in the search DB that belong to the given family (identified by the ``__`` prefix). """ members: set[str] = set() with open(search_db) as fh: for line in fh: if line.startswith(">"): name = line[1:].split()[0] if name.startswith(f"{family_id}__"): members.add(name) return members # --------------------------------------------------------------------------- # Per-case pipeline # --------------------------------------------------------------------------- def run_hmmer_case( family: dict, method_name: str, search_db: Path, work_dir: Path, e_value_threshold: float = 1e-5, align_timeout: int | None = None, ) -> HmmerCaseResult: """Run the full HMMER pipeline for one family with one alignment method. Steps: 1. Read Pfam seed sequences for the family 2. Strip gaps to get unaligned sequences 3. Re-align using the named method via ``utils.run_method()`` 4. Build profile HMM from the new alignment 5. Search against the combined database 6. Score: family members detected = TP, non-members detected = FP Parameters ---------- family : dict Dict with ``family_id``, ``seed_fasta_path``, ``n_members``. method_name : str Alignment method name (key in ``METHODS``). search_db : Path Path to the combined search database FASTA. work_dir : Path Scratch directory for intermediate files. e_value_threshold : float E-value cutoff for hmmsearch. Returns ------- HmmerCaseResult """ from .utils import run_method, parse_fasta, write_fasta family_id = family["family_id"] seed_path = Path(family["seed_fasta_path"]) work_dir = Path(work_dir) work_dir.mkdir(parents=True, exist_ok=True) # 1. Read seed alignment and strip gaps to get unaligned sequences names, seqs = _parse_fasta_simple(seed_path) unaligned_seqs = _strip_gaps(seqs) # Write unaligned FASTA for the aligner unaln_path = work_dir / f"{family_id}_{method_name}_unaligned.fasta" write_fasta(names, unaligned_seqs, unaln_path) # 2. Align (with timeout protection for slow external tools) try: aln_result = run_method( method_name=method_name, input_fasta=unaln_path, work_dir=work_dir, seq_type="protein", timeout=align_timeout, ) except subprocess.TimeoutExpired: logger.warning( "Alignment timed out for %s/%s after %ss", family_id, method_name, align_timeout, ) return HmmerCaseResult( family_id=family_id, method=method_name, true_positives=0, false_positives=0, false_negatives=family.get("n_members", 0), sensitivity=0.0, specificity=0.0, e_value_threshold=e_value_threshold, hmm_length=0, wall_time_align=float(align_timeout or 0), wall_time_hmmbuild=0.0, wall_time_hmmsearch=0.0, peak_memory_mb=0.0, ) wall_time_align = aln_result.wall_time peak_memory_mb = aln_result.peak_memory_mb # Write alignment to FASTA for hmmbuild aln_path = work_dir / f"{family_id}_{method_name}_aligned.fasta" write_fasta(aln_result.names, aln_result.sequences, aln_path) # 3. Build HMM hmm_path = work_dir / f"{family_id}_{method_name}.hmm" try: hmm_info = _run_hmmbuild(aln_path, hmm_path) except RuntimeError as exc: logger.error("hmmbuild failed for %s/%s: %s", family_id, method_name, exc) return HmmerCaseResult( family_id=family_id, method=method_name, true_positives=0, false_positives=0, false_negatives=family.get("n_members", 0), sensitivity=0.0, specificity=0.0, e_value_threshold=e_value_threshold, hmm_length=0, wall_time_align=wall_time_align, wall_time_hmmbuild=0.0, wall_time_hmmsearch=0.0, peak_memory_mb=peak_memory_mb, ) wall_time_hmmbuild = hmm_info["wall_time"] hmm_length = hmm_info["hmm_length"] # 4. Search tblout_path = work_dir / f"{family_id}_{method_name}_hits.tbl" try: search_info = _run_hmmsearch( hmm_path, search_db, tblout_path, e_value=e_value_threshold ) except RuntimeError as exc: logger.error("hmmsearch failed for %s/%s: %s", family_id, method_name, exc) return HmmerCaseResult( family_id=family_id, method=method_name, true_positives=0, false_positives=0, false_negatives=family.get("n_members", 0), sensitivity=0.0, specificity=0.0, e_value_threshold=e_value_threshold, hmm_length=hmm_length, wall_time_align=wall_time_align, wall_time_hmmbuild=wall_time_hmmbuild, wall_time_hmmsearch=0.0, peak_memory_mb=peak_memory_mb, ) wall_time_hmmsearch = search_info["wall_time"] # 5. Score against ground truth true_members = _get_family_members(search_db, family_id) detected_names = {hit["name"] for hit in search_info["hits"]} tp = len(detected_names & true_members) fp = len(detected_names - true_members) fn = len(true_members - detected_names) sensitivity = tp / (tp + fn) if (tp + fn) > 0 else 0.0 precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 return HmmerCaseResult( family_id=family_id, method=method_name, true_positives=tp, false_positives=fp, false_negatives=fn, sensitivity=sensitivity, specificity=precision, # using precision as the specificity metric e_value_threshold=e_value_threshold, hmm_length=hmm_length, wall_time_align=wall_time_align, wall_time_hmmbuild=wall_time_hmmbuild, wall_time_hmmsearch=wall_time_hmmsearch, peak_memory_mb=peak_memory_mb, ) # --------------------------------------------------------------------------- # Worker function for parallel execution # --------------------------------------------------------------------------- def _run_one(args: dict) -> dict: """Worker function for a single family x method evaluation. Parameters ---------- args : dict Must contain ``family``, ``method_name``, ``search_db``, ``work_dir``, ``e_value_threshold``. Returns ------- dict Serialisable case result, or ``{"error": str}`` on failure. """ from .utils import cache_load, cache_save, clean_work_dir fingerprint = args.get("fingerprint") work_dir = Path(args["work_dir"]) if fingerprint: cached = cache_load(work_dir, fingerprint) if cached is not None: return cached # Remove stale output files from previous runs clean_work_dir(work_dir) try: result = run_hmmer_case( family=args["family"], method_name=args["method_name"], search_db=Path(args["search_db"]), work_dir=work_dir, e_value_threshold=args.get("e_value_threshold", 1e-5), align_timeout=args.get("align_timeout"), ) result_dict = asdict(result) if fingerprint: cache_save(work_dir, fingerprint, result_dict) return result_dict except Exception as exc: logger.error( "Error in %s / %s: %s", args.get("family", {}).get("family_id", "?"), args.get("method_name", "?"), exc, ) return { "error": str(exc), "family_id": args.get("family", {}).get("family_id", "unknown"), "method": args.get("method_name", "unknown"), } # --------------------------------------------------------------------------- # Aggregation # --------------------------------------------------------------------------- def _compute_summary(cases: List[dict], methods: List[str]) -> dict: """Compute per-method aggregated metrics from case results. Parameters ---------- cases : list[dict] List of per-case result dicts (may include error entries). methods : list[str] Method names to summarise. Returns ------- dict Keyed by method name, each value is a dict of aggregated metrics. """ from .utils import bootstrap_ci, wilcoxon_paired, holm_bonferroni summary: dict = {} for method in methods: method_cases = [ c for c in cases if c.get("method") == method and "error" not in c ] if not method_cases: summary[method] = {"n_cases": 0} continue sensitivities = [c["sensitivity"] for c in method_cases] specificities = [c["specificity"] for c in method_cases] n = len(method_cases) mean_sens = sum(sensitivities) / n if n > 0 else 0.0 mean_spec = sum(specificities) / n if n > 0 else 0.0 # Bootstrap CIs (only if we have enough data) sens_ci = bootstrap_ci(sensitivities) if n >= 3 else (mean_sens, mean_sens) spec_ci = bootstrap_ci(specificities) if n >= 3 else (mean_spec, mean_spec) total_tp = sum(c["true_positives"] for c in method_cases) total_fp = sum(c["false_positives"] for c in method_cases) total_fn = sum(c["false_negatives"] for c in method_cases) mean_align_time = ( sum(c["wall_time_align"] for c in method_cases) / n if n > 0 else 0.0 ) mean_hmmbuild_time = ( sum(c["wall_time_hmmbuild"] for c in method_cases) / n if n > 0 else 0.0 ) mean_hmmsearch_time = ( sum(c["wall_time_hmmsearch"] for c in method_cases) / n if n > 0 else 0.0 ) summary[method] = { "n_cases": n, "mean_sensitivity": mean_sens, "sensitivity_ci_95": list(sens_ci), "mean_specificity": mean_spec, "specificity_ci_95": list(spec_ci), "total_tp": total_tp, "total_fp": total_fp, "total_fn": total_fn, "mean_wall_time_align": mean_align_time, "mean_wall_time_hmmbuild": mean_hmmbuild_time, "mean_wall_time_hmmsearch": mean_hmmsearch_time, } # Pairwise statistical tests: compare each method to "kalign" baseline = "kalign" baseline_cases = { c["family_id"]: c for c in cases if c.get("method") == baseline and "error" not in c } if baseline_cases: p_values = [] comparisons = [] for method in methods: if method == baseline: continue method_cases_map = { c["family_id"]: c for c in cases if c.get("method") == method and "error" not in c } # Find paired families shared_families = sorted( set(baseline_cases.keys()) & set(method_cases_map.keys()) ) if len(shared_families) >= 5: a_vals = [baseline_cases[f]["sensitivity"] for f in shared_families] b_vals = [method_cases_map[f]["sensitivity"] for f in shared_families] try: test = wilcoxon_paired(a_vals, b_vals) p_values.append(test["p_value"]) comparisons.append( { "comparison": f"{baseline}_vs_{method}", "statistic": test["statistic"], "p_value": test["p_value"], "cliffs_delta": test["cliffs_delta"], "n_paired": len(shared_families), } ) except Exception: pass if p_values: adjusted = holm_bonferroni(p_values) for comp, adj_p in zip(comparisons, adjusted): comp["p_value_adjusted"] = adj_p summary["_statistical_tests"] = comparisons return summary # --------------------------------------------------------------------------- # Main pipeline entry point # --------------------------------------------------------------------------- def run_pipeline(params: dict) -> HmmerResult: """Run the full HMMER homology detection benchmark. Parameters ---------- params : dict Configuration with keys: - ``data_dir`` : str or Path -- base data directory - ``results_dir`` : str or Path -- where to save JSON output - ``methods`` : list[str] -- alignment methods to test - ``n_families`` : int (default 50) -- number of Pfam families - ``n_jobs`` : int (default 1) -- parallel workers - ``quick`` : bool (default False) -- use only 5 families - ``e_value_threshold`` : float (default 1e-5) Returns ------- HmmerResult """ from .provenance import collect_provenance, result_path, update_latest_symlink from .utils import tool_versions_fingerprint data_dir = Path(params.get("data_dir", "benchmarks/data/downloads")) results_dir = Path(params.get("results_dir", "benchmarks/results")) methods = params.get("methods", ["kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo"]) n_families = params.get("n_families", 50) n_jobs = params.get("n_jobs", 1) quick = params.get("quick", False) e_value_threshold = params.get("e_value_threshold", 1e-5) align_timeout = params.get("align_timeout", 300) # 5 min per alignment if quick: n_families = min(n_families, 5) logger.info("Quick mode: using %d families", n_families) # Collect provenance provenance = collect_provenance(params) # Download / load Pfam seeds families = _download_pfam_seeds(data_dir, n_families=n_families) if not families: raise RuntimeError( "No Pfam seed families available. " "Check network connectivity or populate the cache manually." ) # Prepare search database search_db = _prepare_search_db(families, data_dir) # Compute fingerprint once for all workers (None disables caching) if params.get("no_cache"): fingerprint = None logger.info("Caching disabled (--no-cache)") else: fingerprint = tool_versions_fingerprint() logger.info("Tool versions fingerprint: %s", fingerprint) # Build work items work_items: list[dict] = [] for family in families: for method_name in methods: case_work_dir = ( Path(data_dir) / "hmmer_work" / family["family_id"] / method_name ) work_items.append( { "family": family, "method_name": method_name, "search_db": str(search_db), "work_dir": str(case_work_dir), "e_value_threshold": e_value_threshold, "fingerprint": fingerprint, "align_timeout": align_timeout, } ) logger.info( "Running %d cases (%d families x %d methods) with %d workers", len(work_items), len(families), len(methods), n_jobs, ) # Execute from tqdm import tqdm all_cases: list[dict] = [] pbar = tqdm(total=len(work_items), desc="HMMER detection", unit="case") if n_jobs <= 1: for item in work_items: result = _run_one(item) all_cases.append(result) pbar.update(1) else: with ProcessPoolExecutor(max_workers=n_jobs) as executor: futures = { executor.submit(_run_one, item): item for item in work_items } for future in as_completed(futures): all_cases.append(future.result()) pbar.update(1) pbar.close() # Aggregate summary = _compute_summary(all_cases, methods) # Log summary table logger.info("=== HMMER Detection Summary ===") for method in methods: s = summary.get(method, {}) if s.get("n_cases", 0) > 0: logger.info( " %-20s sens=%.3f prec=%.3f (n=%d)", method, s["mean_sensitivity"], s["mean_specificity"], s["n_cases"], ) # Save results provenance_dict = asdict(provenance) hmmer_result = HmmerResult( provenance=provenance_dict, cases=all_cases, summary=summary, ) out_path = result_path(results_dir, "hmmer_detection") with open(out_path, "w") as fh: json.dump(asdict(hmmer_result), fh, indent=2, default=str) update_latest_symlink(out_path) logger.info("Results saved to %s", out_path) return hmmer_result # --------------------------------------------------------------------------- # Load results # --------------------------------------------------------------------------- def load_results(results_dir: Path) -> HmmerResult: """Load HMMER detection results from the latest result file. Parameters ---------- results_dir : Path Base results directory (the ``hmmer_detection/`` subdirectory is appended automatically). Returns ------- HmmerResult """ from .provenance import load_latest_results data = load_latest_results(Path(results_dir) / "hmmer_detection") return HmmerResult( provenance=data.get("provenance", {}), cases=data.get("cases", []), summary=data.get("summary", {}), ) # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- if __name__ == "__main__": import argparse logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) parser = argparse.ArgumentParser( description="Pipeline 3: HMMER Homology Detection Benchmark" ) parser.add_argument( "-j", "--parallel", type=int, default=1, help="Number of parallel workers (default: 1)", ) parser.add_argument( "--max-families", type=int, default=50, help="Maximum number of Pfam families to test (default: 50)", ) parser.add_argument( "--data-dir", type=str, default="benchmarks/data/downloads", help="Data directory for downloads and caches", ) parser.add_argument( "--results-dir", type=str, default="benchmarks/results", help="Results output directory", ) parser.add_argument( "--quick", action="store_true", help="Quick mode: only 5 families", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Enable debug logging", ) parser.add_argument( "--methods", nargs="+", default=None, help="Alignment methods to test (default: all)", ) args = parser.parse_args() if args.verbose: logging.getLogger().setLevel(logging.DEBUG) from .utils import METHODS methods = args.methods or list(METHODS.keys()) run_pipeline( { "data_dir": args.data_dir, "results_dir": args.results_dir, "methods": methods, "n_families": args.max_families, "n_jobs": args.parallel, "quick": args.quick, } ) kalign-3.5.1/benchmarks/downstream/phylo_accuracy.py000066400000000000000000000575251515023132300226550ustar00rootroot00000000000000"""Pipeline 2: Phylogenetic Tree Accuracy. Evaluates how alignment quality affects phylogenetic tree inference. Simulates protein evolution with INDELible (WAG+Gamma), aligns with multiple methods, infers ML trees with IQ-TREE 2, and compares inferred trees to the true (simulated) tree using Robinson-Foulds distances. Methods that produce per-column confidence scores can pass them to IQ-TREE as site weights (``--site-weight`` flag) for continuous confidence weighting, or mask low-confidence columns before tree inference. """ from __future__ import annotations import json import logging import os import re import shutil import subprocess import time from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Optional logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Result dataclasses # --------------------------------------------------------------------------- @dataclass class PhyloCaseResult: """Result of a single simulation x method phylogenetic analysis.""" sim_id: str method: str nrf: float # normalized Robinson-Foulds (0=perfect, 1=completely wrong) branch_score_dist: float # weighted RF sp_score: float # alignment SP vs true alignment n_columns_retained: int # after masking (if applicable) n_columns_total: int wall_time_align: float wall_time_iqtree: float peak_memory_mb: float @dataclass class PhyloResult: """Aggregated result for the full phylo accuracy pipeline.""" provenance: dict cases: list[dict] summary: dict # per-method aggregated metrics # --------------------------------------------------------------------------- # IQ-TREE runner # --------------------------------------------------------------------------- def _run_iqtree( alignment_path: Path, model: str, work_dir: Path, site_weights_path: Optional[Path] = None, n_threads: int = 1, timeout: int = 600, ) -> dict: """Run IQ-TREE 2 on an alignment and return the inferred tree. Parameters ---------- alignment_path : Path FASTA alignment file. model : str Substitution model string (e.g. ``"WAG+G4"``). work_dir : Path Working directory for IQ-TREE output files. site_weights_path : Path or None If given, passed to IQ-TREE via ``--site-weight`` for per-site weighting. n_threads : int Number of threads for IQ-TREE (default 1). timeout : int Maximum seconds before killing IQ-TREE (default 600). Returns ------- dict ``{"tree": str, "log_likelihood": float, "wall_time": float}`` Raises ------ RuntimeError If ``iqtree2`` is not found on ``$PATH`` or exits with an error. """ iqtree_bin = shutil.which("iqtree2") if iqtree_bin is None: raise RuntimeError( "iqtree2 is not installed or not on PATH. " "Please install IQ-TREE 2 (http://www.iqtree.org/) " "and ensure the 'iqtree2' binary is accessible." ) work_dir = Path(work_dir) work_dir.mkdir(parents=True, exist_ok=True) prefix = work_dir / "iqtree" cmd = [ iqtree_bin, "-s", str(alignment_path), "-m", model, "--prefix", str(prefix), "-nt", str(n_threads), "--fast", "-redo", ] # Note: IQ-TREE 2 does not support per-site weights. # The site_weights_path parameter is accepted but ignored. # Weighted methods (kalign_ens3_wt, guidance2_wt) run IQ-TREE # on the full unmasked alignment, providing a baseline comparison. if site_weights_path is not None: logger.debug("Site weights file ignored (IQ-TREE has no weighting support)") start = time.perf_counter() try: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) except subprocess.TimeoutExpired: wall = time.perf_counter() - start raise RuntimeError( f"iqtree2 timed out after {timeout}s ({wall:.0f}s elapsed) " f"in {work_dir}" ) wall = time.perf_counter() - start if proc.returncode != 0: raise RuntimeError( f"iqtree2 failed (exit {proc.returncode}):\n" f"stdout:\n{proc.stdout}\n" f"stderr:\n{proc.stderr}" ) # Parse the treefile treefile = Path(str(prefix) + ".treefile") if not treefile.exists(): raise FileNotFoundError( f"IQ-TREE treefile not found at {treefile}. " f"Contents of {work_dir}: {list(work_dir.iterdir())}" ) tree_str = treefile.read_text().strip() # Parse log-likelihood from the .iqtree summary file log_likelihood = float("nan") iqtree_log = Path(str(prefix) + ".iqtree") if iqtree_log.exists(): log_text = iqtree_log.read_text() # Look for "Log-likelihood of the tree: -12345.6789 (..." m = re.search(r"Log-likelihood of the tree:\s+([-\d.]+)", log_text) if m: try: log_likelihood = float(m.group(1)) except ValueError: pass return { "tree": tree_str, "log_likelihood": log_likelihood, "wall_time": wall, } # --------------------------------------------------------------------------- # Single-case runner # --------------------------------------------------------------------------- def run_phylo_case( sim_dataset, method_name: str, work_dir: Path, model: str = "WAG+G4", n_threads: int = 1, iqtree_timeout: int = 600, ) -> PhyloCaseResult: """Run the full phylo pipeline for one simulation x method combination. Steps: 1. Align unaligned sequences with the specified method. 2. Write site weights if the method produces them. 3. Run IQ-TREE on the (possibly masked/weighted) alignment. 4. Compare the inferred tree against the true tree. Parameters ---------- sim_dataset : SimulatedDataset Simulation output (unaligned seqs, true alignment, true tree). method_name : str Key into ``utils.METHODS``. work_dir : Path Scratch directory for this case. model : str IQ-TREE substitution model (default ``"WAG+G4"``). Returns ------- PhyloCaseResult """ from .utils import ( alignment_accuracy, compare_trees, parse_fasta, run_method, write_fasta, METHODS, ) work_dir = Path(work_dir) work_dir.mkdir(parents=True, exist_ok=True) sim_id = sim_dataset.params.get("sim_id", str(sim_dataset.unaligned)) # -- "true" method: use true alignment directly as performance ceiling -- if method_name == "true": true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) aln_fasta = work_dir / "true_aln.fasta" write_fasta(true_names, true_seqs, aln_fasta) n_columns_total = len(true_seqs[0]) if true_seqs else 0 iqtree_dir = work_dir / "true_iqtree" iqtree_result = _run_iqtree( aln_fasta, model, iqtree_dir, n_threads=n_threads, timeout=iqtree_timeout, ) true_tree_str = sim_dataset.true_tree.read_text().strip() tree_cmp = compare_trees(true_tree_str, iqtree_result["tree"]) return PhyloCaseResult( sim_id=sim_id, method="true", nrf=tree_cmp["nrf"], branch_score_dist=tree_cmp["branch_score_dist"], sp_score=1.0, n_columns_retained=n_columns_total, n_columns_total=n_columns_total, wall_time_align=0.0, wall_time_iqtree=iqtree_result["wall_time"], peak_memory_mb=0.0, ) # -- Step 1: Align ------------------------------------------------------- # For guided methods, pass the true alignment as input so that # kalign can compute pairwise distances from it and build a tree. input_fasta = sim_dataset.unaligned aln_result = run_method( method_name, input_fasta, work_dir, seq_type="protein", ) # Determine alignment dimensions n_columns_total = len(aln_result.sequences[0]) if aln_result.sequences else 0 n_columns_retained = n_columns_total # masking already applied by run_method # -- Step 2: Write alignment to FASTA for IQ-TREE ----------------------- aln_fasta = work_dir / f"{method_name}_aln.fasta" write_fasta(aln_result.names, aln_result.sequences, aln_fasta) # -- Step 3: Check for site weights file -------------------------------- cfg = METHODS.get(method_name, {}) site_weights_path = None if cfg.get("weights") and aln_result.column_confidence is not None: wt_path = work_dir / f"{method_name}_site_weights.txt" if wt_path.exists(): site_weights_path = wt_path # -- Step 4: Pre-check for all-gap sequences ---------------------------- for name, seq in zip(aln_result.names, aln_result.sequences): if all(c == "-" for c in seq): raise RuntimeError( f"Sequence '{name}' is all gaps after alignment/masking — " f"IQ-TREE would reject this. Method={method_name}" ) # -- Step 5: Run IQ-TREE ------------------------------------------------ iqtree_dir = work_dir / f"{method_name}_iqtree" iqtree_result = _run_iqtree( aln_fasta, model, iqtree_dir, site_weights_path=site_weights_path, n_threads=n_threads, timeout=iqtree_timeout, ) # -- Step 6: Compare inferred vs true tree ------------------------------ true_tree_str = sim_dataset.true_tree.read_text().strip() tree_cmp = compare_trees(true_tree_str, iqtree_result["tree"]) # -- Step 7: Alignment accuracy vs true alignment ----------------------- true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) acc = alignment_accuracy( aln_result.sequences, true_seqs, aln_result.names, true_names, ) return PhyloCaseResult( sim_id=sim_id, method=method_name, nrf=tree_cmp["nrf"], branch_score_dist=tree_cmp["branch_score_dist"], sp_score=acc["sp_score"], n_columns_retained=n_columns_retained, n_columns_total=n_columns_total, wall_time_align=aln_result.wall_time, wall_time_iqtree=iqtree_result["wall_time"], peak_memory_mb=aln_result.peak_memory_mb, ) # --------------------------------------------------------------------------- # Pipeline entry point # --------------------------------------------------------------------------- def _phylo_worker(sim_ds, mname, data_dir, fingerprint=None, n_threads=1, iqtree_timeout=600): """Worker function for a single sim x method case (module-level for pickling).""" from .utils import cache_load, cache_save, clean_work_dir sim_id = sim_ds.params.get("sim_id", "unknown") case_dir = Path(data_dir) / "phylo_work" / sim_id / mname case_dir.mkdir(parents=True, exist_ok=True) if fingerprint: cached = cache_load(case_dir, fingerprint) if cached is not None: return cached # Remove stale output files (e.g. IQ-TREE checkpoints) from previous runs clean_work_dir(case_dir) try: result = run_phylo_case( sim_ds, mname, case_dir, n_threads=n_threads, iqtree_timeout=iqtree_timeout, ) result_dict = asdict(result) if fingerprint: cache_save(case_dir, fingerprint, result_dict) return result_dict except Exception as exc: logger.warning("Phylo case %s/%s failed: %s", sim_id, mname, exc) return {"sim_id": sim_id, "method": mname, "error": str(exc)} def run_pipeline(params: dict) -> PhyloResult: """Run the full phylo accuracy pipeline. Parameters ---------- params : dict Configuration with keys: - ``data_dir`` : str or Path -- base directory for simulation data - ``results_dir`` : str or Path -- where to write result JSON - ``methods`` : list[str] -- method names (keys into METHODS) - ``n_jobs`` : int -- max parallel workers - ``quick`` : bool -- if True, limit to 5 simulation cases Returns ------- PhyloResult """ from concurrent.futures import ProcessPoolExecutor, as_completed from .provenance import ( collect_provenance, result_path, update_latest_symlink, ) from .simulation import ( PROTEIN_GRID, PROTEIN_GRID_FULL, generate_indelible_dataset, iter_simulation_params, random_birth_death_tree, ) from .utils import ( tool_versions_fingerprint, METHODS, ) data_dir = Path(params.get("data_dir", "benchmarks/data/downloads")) results_dir = Path(params.get("results_dir", "benchmarks/results")) full = params.get("full", False) _default_methods = ["kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo", "true"] methods = params.get("methods", list(METHODS.keys()) if full else _default_methods) n_jobs = params.get("n_jobs", 1) quick = params.get("quick", False) depth_filter = params.get("depths", None) logger.info("Phylo accuracy pipeline starting (quick=%s, n_jobs=%d)", quick, n_jobs) # -- Generate simulations ----------------------------------------------- grid = PROTEIN_GRID_FULL if full else PROTEIN_GRID sim_params_list = list(iter_simulation_params(grid, "WAG")) if quick: sim_params_list = sim_params_list[:5] if depth_filter: depth_set = set(float(d) for d in depth_filter) sim_params_list = [sp for sp in sim_params_list if sp["target_depth"] in depth_set] logger.info("Filtered to depths %s: %d simulations", depth_set, len(sim_params_list)) from tqdm import tqdm sim_dir = data_dir / "phylo_sims" sim_dir.mkdir(parents=True, exist_ok=True) simulations = [] for sp in tqdm(sim_params_list, desc="Generating simulations", unit="sim"): sim_out = sim_dir / sp["sim_id"] tree = random_birth_death_tree( n_taxa=sp["n_taxa"], target_depth=sp["target_depth"], seed=sp["seed"], ) try: ds = generate_indelible_dataset( tree=tree, model=sp["model"], seq_length=sp["seq_length"], indel_rate=sp["indel_rate"], indel_length_mean=sp["indel_length_mean"], output_dir=sim_out, seed=sp["seed"], ) # Attach sim_id into params for later reference ds.params["sim_id"] = sp["sim_id"] simulations.append(ds) except Exception as exc: logger.warning("Simulation %s failed: %s", sp["sim_id"], exc) logger.info("Generated %d simulations successfully.", len(simulations)) # Compute fingerprint once for all workers (None disables caching) if params.get("no_cache"): fingerprint = None logger.info("Caching disabled (--no-cache)") else: fingerprint = tool_versions_fingerprint() logger.info("Tool versions fingerprint: %s", fingerprint) # -- Run alignment + tree inference for each sim x method ---------------- # Allocate IQ-TREE threads: divide available cores among parallel workers cpu_count = os.cpu_count() or 1 iq_threads = max(1, cpu_count // max(n_jobs, 1)) iq_timeout = 600 # 10 minutes per IQ-TREE run logger.info( "IQ-TREE will use %d threads per job (%d cores / %d jobs)", iq_threads, cpu_count, n_jobs, ) cases: list[dict] = [] # Build work items work_items = [ (sim_ds, mname) for sim_ds in simulations for mname in methods if mname in METHODS ] pbar = tqdm(total=len(work_items), desc="Phylo accuracy", unit="case") if n_jobs <= 1: # Sequential execution for sim_ds, mname in work_items: cases.append(_phylo_worker( sim_ds, mname, data_dir, fingerprint, n_threads=iq_threads, iqtree_timeout=iq_timeout, )) pbar.update(1) else: with ProcessPoolExecutor(max_workers=n_jobs) as pool: futures = { pool.submit( _phylo_worker, sim_ds, mname, data_dir, fingerprint, n_threads=iq_threads, iqtree_timeout=iq_timeout, ): (sim_ds, mname) for sim_ds, mname in work_items } for future in as_completed(futures): try: cases.append(future.result()) except Exception as exc: sim_ds, mname = futures[future] sim_id = sim_ds.params.get("sim_id", "unknown") logger.warning( "Future for %s/%s raised: %s", sim_id, mname, exc, ) cases.append({ "sim_id": sim_id, "method": mname, "error": str(exc), }) pbar.update(1) pbar.close() logger.info("Completed %d cases.", len(cases)) # -- Aggregate: per-method summary -------------------------------------- summary = _aggregate_summary(cases, methods) # -- Statistical tests: pairwise Wilcoxon on nRF ----------------------- _add_statistical_tests(summary, cases, methods) # -- Provenance --------------------------------------------------------- provenance = collect_provenance(params) result = PhyloResult( provenance=asdict(provenance), cases=cases, summary=summary, ) # -- Save to disk ------------------------------------------------------- out_path = result_path(results_dir, "phylo_accuracy") with open(out_path, "w") as fh: json.dump(asdict(result) if hasattr(result, "__dataclass_fields__") else { "provenance": result.provenance, "cases": result.cases, "summary": result.summary, }, fh, indent=2, default=str) update_latest_symlink(out_path) logger.info("Results saved to %s", out_path) # -- Console summary --------------------------------------------------- _print_summary(summary) return result # --------------------------------------------------------------------------- # Internal aggregation helpers # --------------------------------------------------------------------------- def _aggregate_summary(cases: list[dict], methods: list[str]) -> dict: """Compute per-method aggregated metrics from case results.""" import numpy as np from .utils import bootstrap_ci summary: dict = {} for mname in methods: method_cases = [ c for c in cases if c.get("method") == mname and "error" not in c ] if not method_cases: summary[mname] = {"n_cases": 0} continue nrf_vals = [c["nrf"] for c in method_cases] bsd_vals = [c["branch_score_dist"] for c in method_cases] sp_vals = [c["sp_score"] for c in method_cases if c.get("sp_score", -1) >= 0] nrf_arr = np.asarray(nrf_vals, dtype=float) bsd_arr = np.asarray(bsd_vals, dtype=float) entry = { "n_cases": len(method_cases), "mean_nrf": float(nrf_arr.mean()), "std_nrf": float(nrf_arr.std()), "mean_branch_score_dist": float(bsd_arr.mean()), "std_branch_score_dist": float(bsd_arr.std()), } if len(nrf_vals) >= 5: ci_lo, ci_hi = bootstrap_ci(nrf_vals) entry["nrf_ci_95"] = [ci_lo, ci_hi] if sp_vals: sp_arr = np.asarray(sp_vals, dtype=float) entry["mean_sp_score"] = float(sp_arr.mean()) entry["std_sp_score"] = float(sp_arr.std()) # Mean timings align_times = [c.get("wall_time_align", 0) for c in method_cases] iqtree_times = [c.get("wall_time_iqtree", 0) for c in method_cases] entry["mean_wall_time_align"] = float(np.mean(align_times)) entry["mean_wall_time_iqtree"] = float(np.mean(iqtree_times)) summary[mname] = entry return summary def _add_statistical_tests( summary: dict, cases: list[dict], methods: list[str], ) -> None: """Add pairwise Wilcoxon tests on nRF to the summary.""" from .utils import holm_bonferroni, wilcoxon_paired # Build per-sim_id lookup for each method method_nrf: dict[str, dict[str, float]] = {} for c in cases: if "error" in c: continue mname = c["method"] sim_id = c["sim_id"] method_nrf.setdefault(mname, {})[sim_id] = c["nrf"] # All pairwise tests active_methods = [m for m in methods if m in method_nrf and len(method_nrf[m]) >= 5] pairwise_tests = [] p_values = [] for i, m1 in enumerate(active_methods): for m2 in active_methods[i + 1:]: # Find common sim_ids common = sorted(set(method_nrf[m1].keys()) & set(method_nrf[m2].keys())) if len(common) < 5: continue a = [method_nrf[m1][sid] for sid in common] b = [method_nrf[m2][sid] for sid in common] try: test = wilcoxon_paired(a, b) pairwise_tests.append({ "method_a": m1, "method_b": m2, "n_pairs": len(common), **test, }) p_values.append(test["p_value"]) except Exception as exc: logger.debug("Wilcoxon test %s vs %s failed: %s", m1, m2, exc) # Apply Holm-Bonferroni correction if p_values: adjusted = holm_bonferroni(p_values) for t, adj_p in zip(pairwise_tests, adjusted): t["p_value_adjusted"] = adj_p summary["_pairwise_tests"] = pairwise_tests def _print_summary(summary: dict) -> None: """Print a concise summary table to the console.""" print("\n--- Phylo Accuracy Summary ---") print(f"{'Method':<25s} {'N':>5s} {'nRF':>8s} {'BSD':>8s} {'SP':>8s} {'t_aln':>8s} {'t_iq':>8s}") print("-" * 75) for mname, stats in sorted(summary.items()): if mname.startswith("_"): continue n = stats.get("n_cases", 0) if n == 0: print(f"{mname:<25s} {n:>5d} {'N/A':>6s} {'N/A':>6s} {'N/A':>6s}") continue nrf = stats.get("mean_nrf", float("nan")) bsd = stats.get("mean_branch_score_dist", float("nan")) sp = stats.get("mean_sp_score", float("nan")) t_aln = stats.get("mean_wall_time_align", float("nan")) t_iq = stats.get("mean_wall_time_iqtree", float("nan")) print(f"{mname:<25s} {n:>5d} {nrf:>8.4f} {bsd:>8.4f} {sp:>8.4f} {t_aln:>7.2f}s {t_iq:>7.2f}s") # Print pairwise tests tests = summary.get("_pairwise_tests", []) if tests: print(f"\nPairwise Wilcoxon signed-rank tests (nRF, Holm-Bonferroni corrected):") print(f"{'A vs B':<45s} {'delta':>8s} {'p_adj':>10s}") print("-" * 65) for t in tests: label = f"{t['method_a']} vs {t['method_b']}" delta = t.get("cliffs_delta", float("nan")) p_adj = t.get("p_value_adjusted", t.get("p_value", float("nan"))) print(f"{label:<45s} {delta:>8.3f} {p_adj:>10.4g}") print() # --------------------------------------------------------------------------- # Load results # --------------------------------------------------------------------------- def load_results(results_dir) -> PhyloResult: """Load the latest phylo accuracy results from disk. Parameters ---------- results_dir : str or Path Base results directory (expects a ``phylo_accuracy/`` subdirectory with a ``latest.json`` symlink). Returns ------- PhyloResult """ from .provenance import load_latest_results data = load_latest_results(Path(results_dir) / "phylo_accuracy") return PhyloResult( provenance=data.get("provenance", {}), cases=data.get("cases", []), summary=data.get("summary", {}), ) kalign-3.5.1/benchmarks/downstream/positive_selection.py000066400000000000000000001000101515023132300235310ustar00rootroot00000000000000"""Pipeline 1: Positive Selection Analysis. Measures how alignment quality affects false-positive and false-negative rates in dN/dS site-model tests. Simulates codon evolution under M7/M8 models with INDELible, aligns with multiple methods, runs HyPhy FUBAR, and compares detected positively-selected sites against ground truth. """ from __future__ import annotations import json import logging import os import shutil import subprocess import time from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Codon translation / back-translation helpers # --------------------------------------------------------------------------- # Standard genetic code (NCBI translation table 1) _CODON_TABLE = { "TTT": "F", "TTC": "F", "TTA": "L", "TTG": "L", "CTT": "L", "CTC": "L", "CTA": "L", "CTG": "L", "ATT": "I", "ATC": "I", "ATA": "I", "ATG": "M", "GTT": "V", "GTC": "V", "GTA": "V", "GTG": "V", "TCT": "S", "TCC": "S", "TCA": "S", "TCG": "S", "CCT": "P", "CCC": "P", "CCA": "P", "CCG": "P", "ACT": "T", "ACC": "T", "ACA": "T", "ACG": "T", "GCT": "A", "GCC": "A", "GCA": "A", "GCG": "A", "TAT": "Y", "TAC": "Y", "TAA": "*", "TAG": "*", "CAT": "H", "CAC": "H", "CAA": "Q", "CAG": "Q", "AAT": "N", "AAC": "N", "AAA": "K", "AAG": "K", "GAT": "D", "GAC": "D", "GAA": "E", "GAG": "E", "TGT": "C", "TGC": "C", "TGA": "*", "TGG": "W", "CGT": "R", "CGC": "R", "CGA": "R", "CGG": "R", "AGT": "S", "AGC": "S", "AGA": "R", "AGG": "R", "GGT": "G", "GGC": "G", "GGA": "G", "GGG": "G", } def _translate_dna(dna_seq: str) -> str: """Translate a DNA sequence to protein using the standard genetic code. Stop codons are omitted from the output. The DNA sequence length must be a multiple of 3. """ dna_seq = dna_seq.upper() if len(dna_seq) % 3 != 0: raise ValueError( f"DNA sequence length {len(dna_seq)} is not a multiple of 3" ) protein = [] for i in range(0, len(dna_seq), 3): codon = dna_seq[i : i + 3] aa = _CODON_TABLE.get(codon, "X") if aa != "*": protein.append(aa) return "".join(protein) def _backtranslate( protein_aln: str, original_dna: str, ) -> str: """Back-translate an aligned protein sequence to a codon alignment. Each non-gap character in *protein_aln* is replaced by the corresponding original DNA codon (3 nucleotides). Each gap character (``-``) is replaced by ``---``. Parameters ---------- protein_aln : str Aligned protein sequence (may contain ``-`` gap characters). original_dna : str Original unaligned DNA sequence (ungapped, length must be a multiple of 3). Returns ------- str Codon-aligned DNA sequence where every position is exactly 3 characters (either a codon or ``---``). """ original_dna = original_dna.upper() # Build list of codons from original DNA (excluding stop codons) codons: list[str] = [] for i in range(0, len(original_dna), 3): codon = original_dna[i : i + 3] aa = _CODON_TABLE.get(codon, "X") if aa != "*": codons.append(codon) codon_idx = 0 result: list[str] = [] for ch in protein_aln: if ch == "-": result.append("---") else: if codon_idx >= len(codons): raise ValueError( f"Protein alignment has more residues than available " f"codons ({len(codons)})" ) result.append(codons[codon_idx]) codon_idx += 1 if codon_idx != len(codons): raise ValueError( f"Protein alignment used {codon_idx} residues but original DNA " f"has {len(codons)} non-stop codons" ) return "".join(result) # --------------------------------------------------------------------------- # Result dataclasses # --------------------------------------------------------------------------- @dataclass class SelectionCaseResult: """Result of a single (simulation x method) case.""" sim_id: str method: str true_positives: int false_positives: int false_negatives: int true_negatives: int precision: float recall: float f1: float sp_score: float wall_time_align: float wall_time_hyphy: float peak_memory_mb: float n_sites_tested: int @dataclass class SelectionResult: """Aggregate result for the full positive-selection pipeline.""" provenance: dict cases: list[dict] summary: dict # per-method aggregated metrics # --------------------------------------------------------------------------- # HyPhy FUBAR runner # --------------------------------------------------------------------------- def _run_hyphy_fubar( alignment_path: Path, tree_path: Path, work_dir: Path, ) -> dict: """Run HyPhy FUBAR on a codon alignment. Parameters ---------- alignment_path : Path Path to aligned codon sequences in FASTA format. tree_path : Path Path to Newick tree file. work_dir : Path Working directory for HyPhy output files. Returns ------- dict ``{"site_posteriors": list[float], "n_sites": int}`` where each element of *site_posteriors* is the posterior probability of positive selection (``Prob[alpha < beta]``) at each codon site. Raises ------ RuntimeError If HyPhy is not found or exits with an error. """ hyphy_bin = shutil.which("hyphy") or shutil.which("HYPHYMP") if hyphy_bin is None: raise RuntimeError( "HyPhy is not installed or not on PATH. " "Please install HyPhy (https://www.hyphy.org/) " "and ensure the 'hyphy' binary is accessible." ) alignment_path = Path(alignment_path).resolve() tree_path = Path(tree_path).resolve() work_dir = Path(work_dir).resolve() work_dir.mkdir(parents=True, exist_ok=True) # HyPhy FUBAR outputs a JSON file next to the alignment # with the suffix .FUBAR.json output_json = work_dir / (alignment_path.name + ".FUBAR.json") cmd = [ hyphy_bin, "fubar", "--alignment", str(alignment_path), "--tree", str(tree_path), "--output", str(output_json), ] # Build environment with HYPHY_PATH so HyPhy can find its batch files. # HYPHY_LIB (set in Containerfile) is not the variable HyPhy checks; # it needs HYPHY_PATH. env = dict(os.environ) hyphy_lib = env.get("HYPHY_LIB") or env.get("HYPHY_PATH") if hyphy_lib: env["HYPHY_PATH"] = hyphy_lib logger.debug("Running HyPhy FUBAR: %s", " ".join(cmd)) proc = subprocess.run( cmd, capture_output=True, text=True, cwd=str(work_dir), env=env, ) if proc.returncode != 0: raise RuntimeError( f"HyPhy FUBAR failed (exit {proc.returncode}).\n" f"stdout:\n{proc.stdout}\n" f"stderr:\n{proc.stderr}" ) # Parse results JSON if not output_json.exists(): # HyPhy may place the JSON in the same directory as the alignment alt_json = alignment_path.parent / (alignment_path.name + ".FUBAR.json") if alt_json.exists(): output_json = alt_json else: raise RuntimeError( f"HyPhy FUBAR output JSON not found at {output_json} " f"or {alt_json}" ) with open(output_json) as fh: fubar_data = json.load(fh) # FUBAR JSON structure: # "MLE" -> "content" -> { # "0" -> {"headers": [...], "0": [row0], "1": [row1], ...} # } # Headers typically include: # "alpha", "beta", "Prob[alpha>beta]", "Prob[alpha prob_col_idx: site_posteriors.append(float(row[prob_col_idx])) else: site_posteriors.append(0.0) return { "site_posteriors": site_posteriors, "n_sites": len(site_posteriors), } # --------------------------------------------------------------------------- # Site classification # --------------------------------------------------------------------------- def _classify_sites( posteriors: list[float], true_classes: list[int], threshold: float = 0.9, ) -> dict: """Classify sites as positively selected and compute confusion metrics. Parameters ---------- posteriors : list[float] FUBAR posterior probability of positive selection per site. true_classes : list[int] Ground-truth site classes: 1 = positive selection, 0 = neutral. threshold : float A site is predicted positive if posterior >= threshold. Returns ------- dict ``{"true_positives", "false_positives", "false_negatives", "true_negatives", "precision", "recall", "f1", "n_sites_tested"}`` """ n = min(len(posteriors), len(true_classes)) tp = fp = fn = tn = 0 for i in range(n): predicted_positive = posteriors[i] >= threshold actual_positive = true_classes[i] == 1 if predicted_positive and actual_positive: tp += 1 elif predicted_positive and not actual_positive: fp += 1 elif not predicted_positive and actual_positive: fn += 1 else: tn += 1 precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 f1 = ( 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0 ) return { "true_positives": tp, "false_positives": fp, "false_negatives": fn, "true_negatives": tn, "precision": precision, "recall": recall, "f1": f1, "n_sites_tested": n, } # --------------------------------------------------------------------------- # Single-case runner # --------------------------------------------------------------------------- def run_selection_case( sim_dataset, method_name: str, work_dir: Path, threshold: float = 0.9, ) -> SelectionCaseResult: """Run the full positive-selection pipeline for one (sim x method) case. Parameters ---------- sim_dataset : SimulatedDataset A simulated dataset (from :mod:`.simulation`). method_name : str Key into ``utils.METHODS``. work_dir : Path Scratch directory for this case. threshold : float FUBAR posterior threshold for calling positive selection. Returns ------- SelectionCaseResult """ from .utils import ( alignment_accuracy, mask_alignment_by_confidence, METHODS, parse_fasta, run_method, write_fasta, ) work_dir = Path(work_dir) work_dir.mkdir(parents=True, exist_ok=True) sim_id = sim_dataset.params.get("sim_id", "unknown") if hasattr(sim_dataset, "params") and isinstance(sim_dataset.params, dict): sim_id = sim_dataset.params.get("sim_id", sim_id) # "true" method: use the true codon alignment directly as performance ceiling if method_name == "true": true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) aln_path = work_dir / "aligned.fasta" write_fasta(true_names, true_seqs, aln_path) t0_hyphy = time.perf_counter() fubar_result = _run_hyphy_fubar( alignment_path=aln_path, tree_path=sim_dataset.true_tree, work_dir=work_dir, ) wall_time_hyphy = time.perf_counter() - t0_hyphy metrics = _classify_sites( posteriors=fubar_result["site_posteriors"], true_classes=sim_dataset.site_classes, threshold=threshold, ) return SelectionCaseResult( sim_id=sim_id, method="true", true_positives=metrics["true_positives"], false_positives=metrics["false_positives"], false_negatives=metrics["false_negatives"], true_negatives=metrics["true_negatives"], precision=metrics["precision"], recall=metrics["recall"], f1=metrics["f1"], sp_score=1.0, wall_time_align=0.0, wall_time_hyphy=wall_time_hyphy, peak_memory_mb=0.0, n_sites_tested=metrics["n_sites_tested"], ) # 1. Translate-align-backtranslate to preserve codon reading frame # Standard DNA alignment can insert 1- or 2-nt gaps that break # the codon frame, causing HyPhy FUBAR to fail. # Masking is deferred to the codon level (skip_masking=True) # so that backtranslation has the full protein alignment. # 1a. Read unaligned DNA sequences dna_names, dna_seqs = parse_fasta(sim_dataset.unaligned) # 1b. Translate DNA to protein protein_seqs = [_translate_dna(s) for s in dna_seqs] # 1c. Write temporary protein FASTA prot_fasta = work_dir / "protein_input.fasta" write_fasta(dna_names, protein_seqs, prot_fasta) # 1d. Align proteins (skip masking so we get the full alignment) aln_result = run_method( method_name, prot_fasta, work_dir, seq_type="protein", skip_masking=True, ) # 1e. Back-translate protein alignment to codon alignment # Map aligned protein names back to original DNA sequences dna_by_name = dict(zip(dna_names, dna_seqs)) codon_seqs: list[str] = [] for name, prot_aln_seq in zip(aln_result.names, aln_result.sequences): orig_dna = dna_by_name[name] codon_seqs.append(_backtranslate(prot_aln_seq, orig_dna)) # Sanity check: all codon-aligned sequences must be the same length if codon_seqs: expected_len = len(codon_seqs[0]) for i, cs in enumerate(codon_seqs): if len(cs) != expected_len: raise ValueError( f"Codon alignment length mismatch: seq {i} has " f"{len(cs)} nt, expected {expected_len}" ) # 1f. Apply confidence masking at the codon level if configured # Each protein column maps to one codon column (3 nt). mask_threshold = METHODS.get(method_name, {}).get("mask") if mask_threshold is not None and aln_result.column_confidence is not None: # Expand protein column_confidence to codon columns (3x) codon_confidence = [c for c in aln_result.column_confidence for _ in range(3)] codon_seqs, _ = mask_alignment_by_confidence( codon_seqs, codon_confidence, mask_threshold ) # 2. Write codon-aligned output to work_dir aln_path = work_dir / "aligned.fasta" # Check if masking removed all columns (empty alignment) aln_len = len(codon_seqs[0]) if codon_seqs else 0 if aln_len == 0: raise RuntimeError( f"Masking removed all columns for {method_name} — " "cannot run FUBAR on an empty alignment" ) write_fasta(aln_result.names, codon_seqs, aln_path) # 3. Compute alignment accuracy vs true alignment (codon level) true_names, true_seqs = parse_fasta(sim_dataset.true_alignment) acc = alignment_accuracy( test_sequences=codon_seqs, true_sequences=true_seqs, test_names=aln_result.names, true_names=true_names, ) sp_score = acc.get("sp_score", -1.0) # 4. Run HyPhy FUBAR t0_hyphy = time.perf_counter() fubar_result = _run_hyphy_fubar( alignment_path=aln_path, tree_path=sim_dataset.true_tree, work_dir=work_dir, ) wall_time_hyphy = time.perf_counter() - t0_hyphy # 5. Classify sites vs true labels metrics = _classify_sites( posteriors=fubar_result["site_posteriors"], true_classes=sim_dataset.site_classes, threshold=threshold, ) return SelectionCaseResult( sim_id=sim_id, method=method_name, true_positives=metrics["true_positives"], false_positives=metrics["false_positives"], false_negatives=metrics["false_negatives"], true_negatives=metrics["true_negatives"], precision=metrics["precision"], recall=metrics["recall"], f1=metrics["f1"], sp_score=sp_score, wall_time_align=aln_result.wall_time, wall_time_hyphy=wall_time_hyphy, peak_memory_mb=aln_result.peak_memory_mb, n_sites_tested=metrics["n_sites_tested"], ) # --------------------------------------------------------------------------- # Worker function for parallel execution # --------------------------------------------------------------------------- def _run_one(args: dict) -> dict: """Worker: align + score one (simulation x method) case. Simulations must already be generated before calling this. Returns a dict (serialisable result or error). """ from .utils import cache_load, cache_save, clean_work_dir sim_dataset = args["sim_dataset"] method_name = args["method_name"] work_dir = Path(args["work_dir"]) threshold = args.get("threshold", 0.9) fingerprint = args.get("fingerprint") sim_id = sim_dataset.params.get("sim_id", "unknown") # Check cache if fingerprint: cached = cache_load(work_dir, fingerprint) if cached is not None: return cached # Remove stale output files from previous runs clean_work_dir(work_dir) try: result = run_selection_case( sim_dataset=sim_dataset, method_name=method_name, work_dir=work_dir, threshold=threshold, ) result_dict = asdict(result) if fingerprint: cache_save(work_dir, fingerprint, result_dict) return result_dict except Exception as exc: logger.error( "Error in %s / %s: %s", sim_id, method_name, exc, exc_info=True ) return { "sim_id": sim_id, "method": method_name, "error": str(exc), } # --------------------------------------------------------------------------- # Aggregation helpers # --------------------------------------------------------------------------- def _aggregate_summary( cases: list[dict], methods: list[str], ) -> dict: """Compute per-method aggregated metrics from case results. Returns a dict keyed by method name, each with mean precision, recall, F1, SP score, and timing. """ from .utils import bootstrap_ci summary: dict[str, dict] = {} for method in methods: method_cases = [c for c in cases if c.get("method") == method and "error" not in c] n = len(method_cases) if n == 0: summary[method] = { "n_cases": 0, "mean_precision": 0.0, "mean_recall": 0.0, "mean_f1": 0.0, "mean_sp_score": 0.0, "mean_wall_time_align": 0.0, "mean_wall_time_hyphy": 0.0, } continue precisions = [c["precision"] for c in method_cases] recalls = [c["recall"] for c in method_cases] f1s = [c["f1"] for c in method_cases] sp_scores = [c["sp_score"] for c in method_cases] align_times = [c["wall_time_align"] for c in method_cases] hyphy_times = [c["wall_time_hyphy"] for c in method_cases] mean_f1 = sum(f1s) / n mean_precision = sum(precisions) / n mean_recall = sum(recalls) / n mean_sp = sum(sp_scores) / n entry: dict[str, Any] = { "n_cases": n, "mean_precision": mean_precision, "mean_recall": mean_recall, "mean_f1": mean_f1, "mean_sp_score": mean_sp, "mean_wall_time_align": sum(align_times) / n, "mean_wall_time_hyphy": sum(hyphy_times) / n, } # Bootstrap CIs on F1 if we have enough cases if n >= 5: try: ci_low, ci_high = bootstrap_ci(f1s) entry["f1_ci_lower"] = ci_low entry["f1_ci_upper"] = ci_high except Exception: pass summary[method] = entry return summary # --------------------------------------------------------------------------- # Main pipeline # --------------------------------------------------------------------------- # Default methods for positive-selection pipeline (subset of all methods) _DEFAULT_METHODS = [ "kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo", "true", ] _FULL_METHODS = [ "kalign", "kalign_cons", "kalign_ens3", "mafft", "muscle", "clustalo", "true", ] def run_pipeline(params: dict) -> SelectionResult: """Run the positive-selection analysis pipeline. Parameters ---------- params : dict Configuration with keys: - ``data_dir`` : str or Path -- directory for simulation data / scratch - ``results_dir`` : str or Path -- directory for output JSONs - ``methods`` : list[str] -- alignment methods to test - ``n_jobs`` : int -- parallel workers (default 1) - ``quick`` : bool -- if True, run only 5 simulations (default False) - ``threshold`` : float -- FUBAR posterior threshold (default 0.9) - ``max_sims`` : int or None -- max simulations to run Returns ------- SelectionResult """ from .provenance import collect_provenance, result_path, update_latest_symlink from .simulation import ( CODON_GRID, CODON_GRID_FULL, SimulationGrid, generate_indelible_dataset, iter_simulation_params, random_birth_death_tree, ) from .utils import tool_versions_fingerprint data_dir = Path(params.get("data_dir", "benchmarks/data")) results_dir = Path(params.get("results_dir", "benchmarks/results")) full = params.get("full", False) methods = params.get("methods", _FULL_METHODS if full else _DEFAULT_METHODS) n_jobs = params.get("n_jobs", 1) quick = params.get("quick", False) threshold = params.get("threshold", 0.9) max_sims = params.get("max_sims", None) data_dir.mkdir(parents=True, exist_ok=True) logger.info("Positive selection pipeline starting") logger.info("Methods: %s", methods) logger.info("Quick mode: %s", quick) # Build the simulation parameter grid if quick: # Minimal grid for smoke-testing: fewer taxa, depths, reps grid = SimulationGrid( n_taxa=[8], tree_depths=[0.7], indel_rates=[0.05], psel_fractions=[0.0, 0.10], replicates=3, n_codons=200, ) else: grid = CODON_GRID_FULL if full else CODON_GRID # Collect simulation parameter sets sim_params_list = list(iter_simulation_params(grid, model="M8")) if max_sims is not None: sim_params_list = sim_params_list[:max_sims] if quick and len(sim_params_list) > 5: sim_params_list = sim_params_list[:5] # Compute fingerprint once for all workers (None disables caching) if params.get("no_cache"): fingerprint = None logger.info("Caching disabled (--no-cache)") else: fingerprint = tool_versions_fingerprint() logger.info("Tool versions fingerprint: %s", fingerprint) # Step 1: Generate simulated datasets sequentially (avoids race conditions) from tqdm import tqdm sim_datasets = [] for sp in tqdm(sim_params_list, desc="Generating codon simulations", unit="sim"): sim_id = sp["sim_id"] sim_dir = data_dir / sim_id tree = random_birth_death_tree( n_taxa=sp["n_taxa"], target_depth=sp["target_depth"], seed=sp.get("seed", 42), ) sim_dataset = generate_indelible_dataset( tree=tree, model=sp["model"], seq_length=sp["seq_length"], indel_rate=sp["indel_rate"], indel_length_mean=sp.get("indel_length_mean", 2.0), output_dir=sim_dir, seed=sp.get("seed", 42), psel_fraction=sp.get("psel_fraction", 0.0), omega_positive=sp.get("omega_positive", 3.0), kappa=sp.get("kappa", 2.0), ) sim_dataset.params["sim_id"] = sim_id sim_datasets.append(sim_dataset) logger.info( "Running %d simulations x %d methods = %d total cases", len(sim_datasets), len(methods), len(sim_datasets) * len(methods), ) # Step 2: Build work items for alignment + scoring work_items = [] for sim_dataset in sim_datasets: sim_id = sim_dataset.params["sim_id"] sim_dir = data_dir / sim_id for method_name in methods: work_dir = sim_dir / f"work_{method_name}" work_items.append( { "sim_dataset": sim_dataset, "method_name": method_name, "work_dir": str(work_dir), "threshold": threshold, "fingerprint": fingerprint, } ) # Step 3: Execute (parallel or serial) cases: list[dict] = [] pbar = tqdm(total=len(work_items), desc="Positive selection", unit="case") if n_jobs > 1: with ProcessPoolExecutor(max_workers=n_jobs) as pool: futures = {pool.submit(_run_one, item): item for item in work_items} for future in as_completed(futures): try: result = future.result() cases.append(result) except Exception as exc: item = futures[future] logger.error("Worker failed: %s", exc) cases.append( { "sim_id": item["sim_dataset"].params.get("sim_id", "unknown"), "method": item["method_name"], "error": str(exc), } ) pbar.update(1) else: for item in work_items: result = _run_one(item) cases.append(result) pbar.update(1) pbar.close() # Filter out errors for summary successful = [c for c in cases if "error" not in c] error_count = len(cases) - len(successful) if error_count > 0: logger.warning("%d / %d cases had errors", error_count, len(cases)) # Aggregate summary summary = _aggregate_summary(cases, methods) # Statistical tests: wilcoxon between kalign_ens3 and each other method stats_tests = _compute_statistical_tests(cases, methods) if stats_tests: summary["statistical_tests"] = stats_tests # Collect provenance provenance = collect_provenance( parameters={ "pipeline": "positive_selection", "n_simulations": len(sim_datasets), "methods": methods, "n_jobs": n_jobs, "quick": quick, "threshold": threshold, "max_sims": max_sims, } ) # Build result sel_result = SelectionResult( provenance=asdict(provenance), cases=cases, summary=summary, ) # Save JSON out_path = result_path(results_dir, "positive_selection") with open(out_path, "w") as fh: json.dump(asdict(sel_result), fh, indent=2, default=str) update_latest_symlink(out_path) logger.info("Results saved to %s", out_path) # Print summary table _print_summary(summary, methods) return sel_result def _compute_statistical_tests( cases: list[dict], methods: list[str] ) -> dict: """Run Wilcoxon signed-rank tests comparing kalign_ens3 to other methods.""" from .utils import holm_bonferroni, wilcoxon_paired baseline = "kalign_ens3" if baseline not in methods: return {} # Group F1 scores by sim_id for paired comparisons baseline_f1: dict[str, float] = {} for c in cases: if c.get("method") == baseline and "error" not in c: baseline_f1[c["sim_id"]] = c["f1"] if len(baseline_f1) < 5: logger.warning( "Too few baseline cases (%d) for statistical tests", len(baseline_f1) ) return {} comparisons = [m for m in methods if m != baseline] test_results: dict[str, dict] = {} raw_p_values: list[float] = [] comp_names: list[str] = [] for method in comparisons: method_f1: dict[str, float] = {} for c in cases: if c.get("method") == method and "error" not in c: method_f1[c["sim_id"]] = c["f1"] # Paired comparison: only use sim_ids present in both common_ids = sorted(set(baseline_f1.keys()) & set(method_f1.keys())) if len(common_ids) < 5: continue a = [baseline_f1[sid] for sid in common_ids] b = [method_f1[sid] for sid in common_ids] try: result = wilcoxon_paired(a, b) test_results[method] = { "n_pairs": len(common_ids), **result, } raw_p_values.append(result["p_value"]) comp_names.append(method) except Exception as exc: logger.warning("Wilcoxon test failed for %s: %s", method, exc) # Apply Holm-Bonferroni correction if raw_p_values: adjusted = holm_bonferroni(raw_p_values) for name, adj_p in zip(comp_names, adjusted): test_results[name]["p_value_adjusted"] = adj_p return test_results def _print_summary(summary: dict, methods: list[str]) -> None: """Print a summary table to the logger.""" logger.info("=" * 72) logger.info("Positive Selection Pipeline — Summary") logger.info("=" * 72) logger.info( "%-22s %6s %8s %8s %8s %8s", "Method", "N", "Prec", "Recall", "F1", "SP", ) logger.info("-" * 72) for m in methods: s = summary.get(m, {}) if s.get("n_cases", 0) == 0: logger.info("%-22s %6d %8s", m, 0, "no data") continue logger.info( "%-22s %6d %8.3f %8.3f %8.3f %8.3f", m, s["n_cases"], s["mean_precision"], s["mean_recall"], s["mean_f1"], s["mean_sp_score"], ) logger.info("=" * 72) # --------------------------------------------------------------------------- # Load results # --------------------------------------------------------------------------- def load_results(results_dir: Path) -> SelectionResult: """Load the latest positive-selection results from disk. Parameters ---------- results_dir : Path Top-level results directory (e.g. ``benchmarks/results``). Returns ------- SelectionResult """ from .provenance import load_latest_results data = load_latest_results(Path(results_dir) / "positive_selection") return SelectionResult( provenance=data.get("provenance", {}), cases=data.get("cases", []), summary=data.get("summary", {}), ) kalign-3.5.1/benchmarks/downstream/provenance.py000066400000000000000000000221321515023132300217720ustar00rootroot00000000000000"""Version collection, hardware info, and result file naming for reproducible benchmarks. Captures full provenance metadata (tool versions, hardware specs, git state) and provides deterministic file naming for benchmark result files. """ import json import os import platform import re import subprocess from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from pathlib import Path from typing import Dict, Optional @dataclass class Provenance: """Full provenance record for a benchmark run.""" timestamp: str kalign_version: str kalign_commit: str container_image: Optional[str] hostname: str cpu_model: str cpu_cores: int ram_gb: float os_version: str python_version: str tool_versions: Dict[str, str] parameters: Dict[str, object] # --------------------------------------------------------------------------- # Tool version collection # --------------------------------------------------------------------------- # Each entry: (binary, args, regex to extract version string) _TOOL_VERSION_COMMANDS = { "kalign": ("kalign", ["-v"], r"(\d+\.\d+\.\d+\S*)"), "mafft": ("mafft", ["--version"], r"v?([\d.]+\S*)"), "muscle": ("muscle", ["--version"], r"([\d.]+\S*)"), "clustalo": ("clustalo", ["--version"], r"([\d.]+\S*)"), "hmmer": ("hmmbuild", ["-h"], r"HMMER\s+([\d.]+\S*)"), "iqtree": ("iqtree2", ["--version"], r"version\s+([\d.]+\S*)"), "hyphy": ("hyphy", ["--version"], r"([\d.]+\S*)"), "indelible": ("indelible", ["--help"], r"INDELible\s+[Vv]?([\d.]+\S*)"), "guidance2": ("guidance.pl", ["--version"], r"([\d.]+\S*)"), } def _run_version_command(binary: str, args: list, pattern: str) -> str: """Run a single version command, return parsed version or 'not found'.""" try: result = subprocess.run( [binary] + args, capture_output=True, text=True, timeout=5, ) # Some tools print version on stderr (e.g. mafft) combined = result.stdout + "\n" + result.stderr match = re.search(pattern, combined) if match: return match.group(1) # Command ran but regex didn't match -- return raw first line first_line = combined.strip().split("\n")[0].strip() return first_line if first_line else "unknown" except FileNotFoundError: return "not found" except subprocess.TimeoutExpired: return "timeout" except OSError: return "not found" def collect_tool_versions() -> Dict[str, str]: """Collect version strings for all known external tools. Tools that are not installed are reported as ``"not found"`` rather than raising an error, so callers can always serialise the result. """ versions: Dict[str, str] = {} for name, (binary, args, pattern) in _TOOL_VERSION_COMMANDS.items(): versions[name] = _run_version_command(binary, args, pattern) return versions # --------------------------------------------------------------------------- # Hardware / environment helpers # --------------------------------------------------------------------------- def _get_cpu_model() -> str: """Return a human-readable CPU model string (macOS + Linux).""" system = platform.system() if system == "Darwin": try: result = subprocess.run( ["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, text=True, timeout=5, ) model = result.stdout.strip() if model: return model except (FileNotFoundError, subprocess.TimeoutExpired, OSError): pass elif system == "Linux": try: with open("/proc/cpuinfo") as fh: for line in fh: if line.startswith("model name"): return line.split(":", 1)[1].strip() except OSError: pass return platform.processor() or "unknown" def _get_ram_gb() -> float: """Return total physical RAM in GiB, rounded to one decimal.""" system = platform.system() if system == "Darwin": try: result = subprocess.run( ["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=5, ) return round(int(result.stdout.strip()) / (1024 ** 3), 1) except (FileNotFoundError, subprocess.TimeoutExpired, OSError, ValueError): pass elif system == "Linux": try: with open("/proc/meminfo") as fh: for line in fh: if line.startswith("MemTotal"): kb = int(line.split()[1]) return round(kb / (1024 ** 2), 1) except (OSError, ValueError): pass return 0.0 def _get_kalign_version() -> str: """Return the installed kalign Python package version.""" try: import kalign as _kalign return getattr(_kalign, "__version__", "unknown") except ImportError: return "not installed" def _get_kalign_commit() -> str: """Return the short git commit hash of the kalign repo.""" try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, timeout=5, cwd=Path(__file__).resolve().parents[2], # repo root ) if result.returncode == 0: return result.stdout.strip() except (FileNotFoundError, subprocess.TimeoutExpired, OSError): pass return "unknown" def _get_container_image() -> Optional[str]: """Detect container image from environment if running inside one.""" # Common env vars set by container runtimes / Dockerfiles for var in ("CONTAINER_IMAGE", "IMAGE_NAME"): value = os.environ.get(var) if value: return value # Check cgroup for docker/podman hints try: with open("/proc/1/cgroup") as fh: text = fh.read() if "docker" in text or "podman" in text or "containerd" in text: return "unknown-container" except OSError: pass return None # --------------------------------------------------------------------------- # Main provenance collector # --------------------------------------------------------------------------- def collect_provenance(parameters: dict) -> Provenance: """Collect all provenance information for the current environment. Parameters ---------- parameters : dict Benchmark-specific parameters to record (e.g. dataset name, number of threads, alignment options). """ now = datetime.now(timezone.utc) return Provenance( timestamp=now.strftime("%Y-%m-%dT%H:%M:%S%z"), kalign_version=_get_kalign_version(), kalign_commit=_get_kalign_commit(), container_image=_get_container_image(), hostname=platform.node(), cpu_model=_get_cpu_model(), cpu_cores=os.cpu_count() or 0, ram_gb=_get_ram_gb(), os_version=f"{platform.system()} {platform.release()}", python_version=platform.python_version(), tool_versions=collect_tool_versions(), parameters=parameters, ) # --------------------------------------------------------------------------- # Result file naming and symlink management # --------------------------------------------------------------------------- def result_path(results_dir: Path, pipeline: str) -> Path: """Generate a unique result file path. Returns a path of the form:: results_dir//run_YYYYMMDD_HHMMSS_.json The directory is created if it does not exist. """ now = datetime.now(timezone.utc) stamp = now.strftime("%Y%m%d_%H%M%S") commit = _get_kalign_commit() name = f"run_{stamp}_{commit}.json" out_dir = Path(results_dir) / pipeline out_dir.mkdir(parents=True, exist_ok=True) return out_dir / name def update_latest_symlink(result_file: Path) -> None: """Create or update a ``latest.json`` symlink next to *result_file*. The symlink points to *result_file* using a relative target so that the whole results tree can be moved without breaking the link. """ result_file = Path(result_file).resolve() link_path = result_file.parent / "latest.json" # Relative target so the symlink is portable target = result_file.name # Atomic replace: unlink then symlink (no rename for symlinks) try: link_path.unlink() except FileNotFoundError: pass link_path.symlink_to(target) def load_latest_results(results_dir: Path) -> dict: """Load the JSON data from the ``latest.json`` symlink. Parameters ---------- results_dir : Path Directory that contains the ``latest.json`` symlink (typically a pipeline sub-directory under the main results directory). Returns ------- dict Parsed JSON contents of the latest result file. Raises ------ FileNotFoundError If no ``latest.json`` symlink exists in *results_dir*. """ link_path = Path(results_dir) / "latest.json" with open(link_path) as fh: return json.load(fh) kalign-3.5.1/benchmarks/downstream/simulation.py000066400000000000000000000533501515023132300220240ustar00rootroot00000000000000"""INDELible sequence simulation and tree generation via dendropy. Provides utilities for generating simulated multiple sequence alignments with known true alignments, for benchmarking alignment accuracy. """ from __future__ import annotations import itertools import os import shutil import subprocess import tempfile from dataclasses import dataclass, field from pathlib import Path from typing import Iterator, List, Optional @dataclass class SimulatedDataset: """Result of a single INDELible simulation run.""" true_alignment: Path # FASTA with gaps (true alignment) unaligned: Path # FASTA without gaps (input for aligners) true_tree: Path # Newick file site_classes: List[int] # Per-site class (0=neutral, 1=positive selection) params: dict = field(default_factory=dict) # All simulation parameters # --------------------------------------------------------------------------- # Tree generation # --------------------------------------------------------------------------- def random_birth_death_tree( n_taxa: int, target_depth: float, seed: int = 42, ) -> str: """Generate a random birth-death tree with dendropy, scale to target depth. Uses ``dendropy.simulate.treesim.birth_death_tree()`` with birth_rate=1.0, death_rate=0.5. Then scales all branch lengths so the maximum root-to-tip distance equals *target_depth*. Returns a Newick string. """ import random from dendropy.simulate import treesim tree = treesim.birth_death_tree( birth_rate=1.0, death_rate=0.5, num_extant_tips=n_taxa, rng=random.Random(seed), ) # Compute the maximum root-to-tip distance. max_dist = 0.0 for leaf in tree.leaf_node_iter(): dist = leaf.distance_from_root() if dist > max_dist: max_dist = dist if max_dist > 0.0: scale_factor = target_depth / max_dist for edge in tree.preorder_edge_iter(): if edge.length is not None: edge.length *= scale_factor nwk = tree.as_string(schema="newick").strip() # Strip dendropy annotations like [&R] that INDELible doesn't understand, # and remove the root branch length (e.g. "...):0.5;" → "...);") # which INDELible rejects. import re nwk = re.sub(r"\[&[^\]]*\]", "", nwk).strip() nwk = re.sub(r"\):[0-9eE.+-]+;$", ");", nwk) # INDELible cannot parse scientific notation (e.g. 1.23e-10) in branch # lengths. Reformat all `:NUMBER` tokens to fixed-point decimal. def _fix_brlen(m: re.Match) -> str: return f":{float(m.group(1)):.10f}" nwk = re.sub(r":([0-9eE.+-]+)", _fix_brlen, nwk) return nwk # --------------------------------------------------------------------------- # INDELible control-file writers # --------------------------------------------------------------------------- def _write_codon_control( output_dir: Path, tree: str, n_codons: int = 400, kappa: float = 2.0, indel_rate: float = 0.05, psel_fraction: float = 0.0, omega_positive: float = 3.0, seed: int = 42, ) -> Path: """Write INDELible control file for codon simulation. If *psel_fraction* > 0, uses M1a (two dN/dS categories: purifying + positive selection). Otherwise uses M0 (single omega, purifying only). INDELible ``[submodel]`` parameter counts determine the codon model: - 2 params → M0: ``kappa omega`` - 4 params → M1a: ``kappa p0 omega0 omega1`` Returns the path to the written ``control.txt``. """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Build the model block. # Max indel length (in codons). Without a bound, INDELible's power-law # sampler can hang when indel_rate is high. max_indel_codons = 10 if psel_fraction > 0.0: # M1a: two categories — purifying + positive selection p0 = 1.0 - psel_fraction # proportion in purifying class omega_purifying = 0.2 model_block = ( f"[MODEL] simmodel\n" f" [submodel] {kappa} {p0} {omega_purifying} {omega_positive}\n" f" [indelmodel] POW 1.7 {max_indel_codons}\n" f" [indelrate] {indel_rate}\n" ) else: # M0: single omega (purifying only) omega_purifying = 0.2 model_block = ( f"[MODEL] simmodel\n" f" [submodel] {kappa} {omega_purifying}\n" f" [indelmodel] POW 1.7 {max_indel_codons}\n" f" [indelrate] {indel_rate}\n" ) control = ( f"[TYPE] CODON 1\n" f"\n" f"[SETTINGS]\n" f" [randomseed] {seed}\n" f" [printrates] TRUE\n" f"\n" f"{model_block}\n" f"[TREE] t1 {tree}\n" f"\n" f"[PARTITIONS] p1\n" f" [t1 simmodel {n_codons}]\n" f"\n" f"[EVOLVE] p1 1 output\n" ) control_path = output_dir / "control.txt" control_path.write_text(control) return control_path def _write_protein_control( output_dir: Path, tree: str, seq_length: int = 300, indel_rate: float = 0.05, indel_length_mean: float = 2.0, seed: int = 42, ) -> Path: """Write INDELible control file for protein simulation (WAG+G4). Returns the path to the written ``control.txt``. """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Max indel length is set from the mean; INDELible uses a power-law # distribution with an upper bound. max_indel_length = int(round(indel_length_mean * 5)) if max_indel_length < 1: max_indel_length = 1 control = ( f"[TYPE] AMINOACID 1\n" f"\n" f"[SETTINGS]\n" f" [randomseed] {seed}\n" f"\n" f"[MODEL] simmodel\n" f" [submodel] WAG\n" f" [rates] 0 0.5 4\n" f" [indelmodel] POW 1.7 {max_indel_length}\n" f" [indelrate] {indel_rate}\n" f"\n" f"[TREE] t1 {tree}\n" f"\n" f"[PARTITIONS] p1\n" f" [t1 simmodel {seq_length}]\n" f"\n" f"[EVOLVE] p1 1 output\n" ) control_path = output_dir / "control.txt" control_path.write_text(control) return control_path # --------------------------------------------------------------------------- # INDELible execution and output parsing # --------------------------------------------------------------------------- def _run_indelible(control_dir: Path) -> None: """Run INDELible in the given directory. Raises ``RuntimeError`` if the executable is not found or if it exits with a non-zero return code. """ indelible_bin = shutil.which("indelible") or shutil.which("INDELible") if indelible_bin is None: raise RuntimeError( "INDELible is not installed or not on PATH. " "Please install INDELible (http://abacus.gene.ucl.ac.uk/software/indelible/) " "and ensure the 'indelible' binary is accessible." ) try: result = subprocess.run( [indelible_bin, "control.txt"], cwd=str(control_dir), capture_output=True, text=True, timeout=120, ) except subprocess.TimeoutExpired: raise RuntimeError( f"INDELible timed out after 120s in {control_dir}" ) if result.returncode != 0: raise RuntimeError( f"INDELible failed (exit code {result.returncode}).\n" f"stdout:\n{result.stdout}\n" f"stderr:\n{result.stderr}" ) def _parse_indelible_output( output_dir: Path, ) -> tuple[list[str], list[str], list[str], list[str]]: """Parse INDELible output files. Returns ``(true_names, true_sequences_with_gaps, unaligned_names, unaligned_sequences)``. INDELible produces (original v1.03 naming / matsengrp fork naming): - ``output_TRUE_1.phy`` or ``output_TRUE.phy`` — true alignment (relaxed PHYLIP) - ``output_1.fas`` or ``output.fas`` — unaligned sequences (FASTA) """ output_dir = Path(output_dir) # -- Parse the true alignment (relaxed PHYLIP) -------------------------- # Handle both original INDELible naming (_1 suffix) and matsengrp fork (no suffix) true_phy = output_dir / "output_TRUE_1.phy" if not true_phy.exists(): true_phy = output_dir / "output_TRUE.phy" if not true_phy.exists(): raise FileNotFoundError( f"Expected INDELible true-alignment file not found: {output_dir}/output_TRUE{{_1,}}.phy" ) true_names: list[str] = [] true_seqs: list[str] = [] with open(true_phy) as fh: header = fh.readline().strip().split() # First line: "n_seqs seq_length" _n_seqs = int(header[0]) for line in fh: line = line.strip() if not line: continue parts = line.split(None, 1) if len(parts) == 2: true_names.append(parts[0]) true_seqs.append(parts[1].replace(" ", "")) # -- Parse the unaligned FASTA ------------------------------------------ unaln_fas = output_dir / "output_1.fas" if not unaln_fas.exists(): unaln_fas = output_dir / "output.fas" if not unaln_fas.exists(): raise FileNotFoundError( f"Expected INDELible unaligned FASTA file not found: {output_dir}/output{{_1,}}.fas" ) unaln_names: list[str] = [] unaln_seqs: list[str] = [] current_name: Optional[str] = None current_seq_parts: list[str] = [] with open(unaln_fas) as fh: for line in fh: line = line.rstrip("\n") if line.startswith(">"): if current_name is not None: unaln_names.append(current_name) unaln_seqs.append("".join(current_seq_parts)) current_name = line[1:].strip() current_seq_parts = [] else: current_seq_parts.append(line.strip()) # Last sequence if current_name is not None: unaln_names.append(current_name) unaln_seqs.append("".join(current_seq_parts)) return true_names, true_seqs, unaln_names, unaln_seqs def _parse_site_classes(output_dir: Path, n_codons: int) -> List[int]: """Parse site class assignments from INDELible rates output. Returns a list of ints: 0 = neutral, 1 = positive selection. If no rates file is found, returns a list of zeros. INDELible writes per-site rate information to ``output_1_RATES.txt`` when ``[printrates] TRUE`` is set. The file has columns: Site, Class, Rate (tab- or space-separated, with a header line). For M8, class index equal to the last class is the positive-selection class. """ rates_file = output_dir / "output_1_RATES.txt" if not rates_file.exists(): rates_file = output_dir / "output_RATES.txt" if not rates_file.exists(): return [0] * n_codons classes: list[int] = [] max_class = 0 with open(rates_file) as fh: for line in fh: line = line.strip() if not line or line.startswith("Site") or line.startswith("#"): continue parts = line.split() if len(parts) >= 2: try: cls = int(parts[1]) except ValueError: continue classes.append(cls) if cls > max_class: max_class = cls # In M8, the highest class index is the positive-selection class. # Map: highest class -> 1 (positive selection), everything else -> 0. if max_class == 0: # All neutral (M7 or single class) return [0] * len(classes) if classes else [0] * n_codons site_classes = [1 if c == max_class else 0 for c in classes] # Pad or truncate to n_codons (rates file should match, but be safe). if len(site_classes) < n_codons: site_classes.extend([0] * (n_codons - len(site_classes))) elif len(site_classes) > n_codons: site_classes = site_classes[:n_codons] return site_classes # --------------------------------------------------------------------------- # Helper: strip gaps # --------------------------------------------------------------------------- def strip_gaps(sequences: list[str]) -> list[str]: """Remove all gap characters from sequences.""" return [s.replace("-", "").replace(".", "") for s in sequences] # --------------------------------------------------------------------------- # FASTA I/O helpers # --------------------------------------------------------------------------- def _write_fasta(path: Path, names: list[str], sequences: list[str]) -> None: """Write sequences in FASTA format.""" with open(path, "w") as fh: for name, seq in zip(names, sequences): fh.write(f">{name}\n{seq}\n") # --------------------------------------------------------------------------- # Main simulation function # --------------------------------------------------------------------------- def generate_indelible_dataset( tree: str, model: str, seq_length: int, indel_rate: float, indel_length_mean: float = 2.0, output_dir: Optional[Path] = None, seed: int = 42, psel_fraction: float = 0.0, omega_positive: float = 3.0, kappa: float = 2.0, ) -> SimulatedDataset: """Generate a simulated dataset using INDELible. Parameters ---------- tree : str Newick tree string. model : str Substitution model: ``"WAG"``, ``"M7"``, or ``"M8"``. seq_length : int Number of sites (amino acids) or codons depending on model. indel_rate : float Insertion/deletion rate. indel_length_mean : float Mean indel length (protein models only). output_dir : Path or None Directory for output files. If *None*, a temporary directory is used. seed : int Random seed for reproducibility. psel_fraction : float Fraction of sites under positive selection (codon models only). omega_positive : float Omega for the positive-selection class (codon models only). kappa : float Transition/transversion ratio (codon models only). Returns ------- SimulatedDataset Paths to output files and simulation metadata. """ use_temp = output_dir is None if use_temp: tmpdir = tempfile.mkdtemp(prefix="indelible_") output_dir = Path(tmpdir) else: output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Store all params for provenance. params = dict( model=model, seq_length=seq_length, indel_rate=indel_rate, indel_length_mean=indel_length_mean, seed=seed, psel_fraction=psel_fraction, omega_positive=omega_positive, kappa=kappa, ) tree_path = output_dir / "tree.nwk" true_aln_path = output_dir / "true_alignment.fasta" unaligned_path = output_dir / "unaligned.fasta" # Skip INDELible if output files already exist (deterministic given params) if ( not use_temp and tree_path.exists() and true_aln_path.exists() and unaligned_path.exists() and true_aln_path.stat().st_size > 0 ): model_upper = model.upper() if model_upper in ("M7", "M8"): site_classes = _parse_site_classes(output_dir, seq_length) else: site_classes = [0] * seq_length return SimulatedDataset( true_alignment=true_aln_path, unaligned=unaligned_path, true_tree=tree_path, site_classes=site_classes, params=params, ) # Write the tree to a file for reference. tree_path.write_text(tree + "\n") # Write control file. model_upper = model.upper() if model_upper in ("M7", "M8"): effective_psel = psel_fraction if model_upper == "M8" else 0.0 _write_codon_control( output_dir=output_dir, tree=tree, n_codons=seq_length, kappa=kappa, indel_rate=indel_rate, psel_fraction=effective_psel, omega_positive=omega_positive, seed=seed, ) n_codons = seq_length elif model_upper == "WAG": _write_protein_control( output_dir=output_dir, tree=tree, seq_length=seq_length, indel_rate=indel_rate, indel_length_mean=indel_length_mean, seed=seed, ) n_codons = 0 else: raise ValueError(f"Unknown model: {model!r}. Expected 'WAG', 'M7', or 'M8'.") # Run INDELible. _run_indelible(output_dir) # Parse output. true_names, true_seqs, unaln_names, unaln_seqs = _parse_indelible_output( output_dir ) # Parse site classes (codon models only). if model_upper in ("M7", "M8"): site_classes = _parse_site_classes(output_dir, n_codons) else: site_classes = [0] * seq_length # Write FASTA outputs. true_aln_path = output_dir / "true_alignment.fasta" unaligned_path = output_dir / "unaligned.fasta" _write_fasta(true_aln_path, true_names, true_seqs) _write_fasta(unaligned_path, unaln_names, unaln_seqs) return SimulatedDataset( true_alignment=true_aln_path, unaligned=unaligned_path, true_tree=tree_path, site_classes=site_classes, params=params, ) # --------------------------------------------------------------------------- # Batch simulation: parameter grids # --------------------------------------------------------------------------- @dataclass class SimulationGrid: """Full factorial simulation parameter grid.""" n_taxa: List[int] tree_depths: List[float] indel_rates: List[float] replicates: int = 10 # Codon-specific psel_fractions: List[float] = field(default_factory=lambda: [0.0]) omega_positive: float = 3.0 kappa: float = 2.0 n_codons: int = 400 # Protein-specific seq_length: int = 300 indel_length_means: List[float] = field(default_factory=lambda: [2.0]) # -- Full grids (for final publication runs with --full) -------------------- CODON_GRID_FULL = SimulationGrid( n_taxa=[32, 64, 128], tree_depths=[0.5, 1.0, 2.0], indel_rates=[0.01, 0.05, 0.10], psel_fractions=[0.10, 0.20, 0.30], replicates=10, n_codons=400, ) PROTEIN_GRID_FULL = SimulationGrid( n_taxa=[16, 32, 64], tree_depths=[0.5, 1.0, 2.0, 4.0], indel_rates=[0.02, 0.05, 0.10, 0.20], indel_length_means=[2.0, 5.0], replicates=20, ) # -- Slim grids (default — faster iteration, still publication quality) ----- # Protein: 2×4×4×2×3 = 192 sims # Codon: 3×3×3×3×3 = 243 sims # # Codon grid rationale: FUBAR is a Bayesian site-level test that needs # sufficient taxa for statistical power (≥32) and enough sites under # selection (psel ≥ 0.10) to produce meaningful F1 scores. Previous # grid (n_taxa=[16,32,64], psel=[0.05,0.10,0.20]) yielded F1 < 0.09 # for ALL methods including the true alignment, indicating the test was # underpowered rather than aligners being poor. PROTEIN_GRID = SimulationGrid( n_taxa=[16, 32], tree_depths=[0.5, 1.0, 2.0, 4.0], indel_rates=[0.02, 0.05, 0.10, 0.20], indel_length_means=[2.0, 5.0], replicates=3, ) CODON_GRID = SimulationGrid( n_taxa=[32, 64, 128], tree_depths=[0.5, 1.0, 2.0], indel_rates=[0.01, 0.05, 0.10], psel_fractions=[0.10, 0.20, 0.30], replicates=3, n_codons=400, ) def iter_simulation_params(grid: SimulationGrid, model: str) -> Iterator[dict]: """Yield parameter dicts for each simulation in the grid. Each dict has keys matching :func:`generate_indelible_dataset` kwargs plus a ``sim_id`` string that uniquely identifies the parameter combination and replicate number. """ model_upper = model.upper() if model_upper == "WAG": # Protein grid: iterate over n_taxa x tree_depths x indel_rates # x indel_length_means x replicates combos = itertools.product( grid.n_taxa, grid.tree_depths, grid.indel_rates, grid.indel_length_means, range(grid.replicates), ) for n_taxa, depth, indel_rate, indel_len, rep in combos: sim_id = ( f"{model_upper}_t{n_taxa}_d{depth}_ir{indel_rate}" f"_il{indel_len}_r{rep}" ) yield dict( sim_id=sim_id, model=model_upper, n_taxa=n_taxa, target_depth=depth, seq_length=grid.seq_length, indel_rate=indel_rate, indel_length_mean=indel_len, seed=42 + rep, psel_fraction=0.0, omega_positive=grid.omega_positive, kappa=grid.kappa, ) elif model_upper in ("M7", "M8"): # Codon grid: iterate over n_taxa x tree_depths x indel_rates # x psel_fractions x replicates combos = itertools.product( grid.n_taxa, grid.tree_depths, grid.indel_rates, grid.psel_fractions, range(grid.replicates), ) for n_taxa, depth, indel_rate, psel_frac, rep in combos: sim_id = ( f"{model_upper}_t{n_taxa}_d{depth}_ir{indel_rate}" f"_ps{psel_frac}_r{rep}" ) yield dict( sim_id=sim_id, model=model_upper, n_taxa=n_taxa, target_depth=depth, seq_length=grid.n_codons, indel_rate=indel_rate, indel_length_mean=2.0, seed=42 + rep, psel_fraction=psel_frac, omega_positive=grid.omega_positive, kappa=grid.kappa, ) else: raise ValueError(f"Unknown model: {model!r}. Expected 'WAG', 'M7', or 'M8'.") kalign-3.5.1/benchmarks/downstream/utils.py000066400000000000000000000673111515023132300210020ustar00rootroot00000000000000"""Shared utilities for downstream benchmark pipelines. Provides alignment runners, scoring helpers, statistical tests, tree comparison wrappers, and consistent figure styling for the kalign downstream benchmarks. """ from __future__ import annotations import hashlib import json as _json import logging import os import resource import subprocess import tempfile import time from pathlib import Path from typing import NamedTuple, Optional logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Per-case caching helpers # --------------------------------------------------------------------------- def tool_versions_fingerprint() -> str: """Return a short hex fingerprint of all external tool versions. Calls :func:`~.provenance.collect_tool_versions` once and hashes the sorted result. The fingerprint changes whenever any tool is upgraded (or removed / added), causing cached results to be recomputed. """ from .provenance import collect_tool_versions versions = collect_tool_versions() blob = _json.dumps(versions, sort_keys=True).encode() return hashlib.sha256(blob).hexdigest()[:16] def cache_load(work_dir: Path, fingerprint: str) -> Optional[dict]: """Load a cached result from *work_dir* if the fingerprint matches. Returns the cached result dict, or ``None`` on cache miss (file missing, fingerprint mismatch, or corrupt JSON). """ cache_file = Path(work_dir) / "_cache.json" if not cache_file.exists(): return None try: with open(cache_file) as fh: data = _json.load(fh) if data.get("fingerprint") == fingerprint: return data["result"] except (OSError, ValueError, KeyError): pass return None def cache_save(work_dir: Path, fingerprint: str, result: dict) -> None: """Write a result dict to *work_dir/_cache.json* with the given fingerprint.""" cache_file = Path(work_dir) / "_cache.json" Path(work_dir).mkdir(parents=True, exist_ok=True) with open(cache_file, "w") as fh: _json.dump({"fingerprint": fingerprint, "result": result}, fh) def clean_work_dir(work_dir: Path) -> None: """Remove stale output files from *work_dir*, preserving ``_cache.json``. Called before re-running a case to prevent leftover files (e.g. IQ-TREE checkpoints, partial FASTA files) from interfering with fresh runs. """ work_dir = Path(work_dir) if not work_dir.is_dir(): return for item in work_dir.iterdir(): if item.name == "_cache.json": continue if item.is_dir(): import shutil shutil.rmtree(item, ignore_errors=True) else: item.unlink(missing_ok=True) # --------------------------------------------------------------------------- # AlignResult # --------------------------------------------------------------------------- class AlignResult(NamedTuple): """Result returned by every alignment runner.""" sequences: list[str] names: list[str] column_confidence: Optional[list[float]] residue_confidence: Optional[list[list[float]]] wall_time: float peak_memory_mb: float # --------------------------------------------------------------------------- # FASTA parser / writer helpers # --------------------------------------------------------------------------- def parse_fasta(path: Path) -> tuple[list[str], list[str]]: """Parse a FASTA file. Returns ------- names : list[str] Sequence names (everything after '>' up to the first whitespace). sequences : list[str] Corresponding sequences with no whitespace. """ names: list[str] = [] sequences: list[str] = [] current_seq: list[str] = [] with open(path) as fh: for line in fh: line = line.rstrip("\n\r") if line.startswith(">"): if current_seq: sequences.append("".join(current_seq)) current_seq = [] # Name is the first whitespace-delimited token after '>' names.append(line[1:].split()[0] if line[1:].strip() else "") else: current_seq.append(line.strip()) if current_seq: sequences.append("".join(current_seq)) return names, sequences def write_fasta(names: list[str], sequences: list[str], path: Path) -> None: """Write sequences to a FASTA file.""" with open(path, "w") as fh: for name, seq in zip(names, sequences): fh.write(f">{name}\n") # Wrap at 80 characters for i in range(0, len(seq), 80): fh.write(seq[i : i + 80] + "\n") # --------------------------------------------------------------------------- # Confidence masking # --------------------------------------------------------------------------- def mask_alignment_by_confidence( sequences: list[str], column_confidence: list[float], threshold: float, ) -> tuple[list[str], int]: """Remove alignment columns where confidence is below *threshold*. Parameters ---------- sequences : list[str] Aligned sequences (all same length). column_confidence : list[float] Per-column confidence values in [0, 1]. threshold : float Columns with confidence < threshold are removed. Returns ------- masked_sequences : list[str] Sequences with low-confidence columns stripped. n_columns_retained : int Number of columns that survived the filter. """ if not sequences: return [], 0 ncols = len(sequences[0]) keep = [j for j in range(ncols) if column_confidence[j] >= threshold] masked = ["".join(seq[j] for j in keep) for seq in sequences] return masked, len(keep) # --------------------------------------------------------------------------- # IQ-TREE site weights # --------------------------------------------------------------------------- def write_site_weights( column_confidence: list[float], output_path: Path, ) -> None: """Write an IQ-TREE ``-a`` site-weight file. The file contains one weight per line. Values are clamped to [0, 1]. """ with open(output_path, "w") as fh: for w in column_confidence: fh.write(f"{max(0.0, min(1.0, w)):.6f}\n") # --------------------------------------------------------------------------- # Alignment accuracy via kalign.compare # --------------------------------------------------------------------------- def alignment_accuracy( test_sequences: list[str], true_sequences: list[str], test_names: list[str], true_names: list[str], ) -> dict: """Compute SP and TC scores of *test* alignment vs *true* alignment. Writes both alignments to temporary FASTA files and calls ``kalign.compare_detailed``. If ``kalign.compare_detailed`` is not available, falls back to ``kalign.compare`` for SP only. If neither is available, returns sentinel values of -1. Returns ------- dict ``{"sp_score": float, "tc_score": float}`` """ try: import kalign as _kalign except ImportError: return {"sp_score": -1.0, "tc_score": -1.0} # Quick validation: need at least 2 non-empty sequences if len(test_sequences) < 2 or len(true_sequences) < 2: return {"sp_score": -1.0, "tc_score": -1.0} if not test_sequences[0] or not true_sequences[0]: return {"sp_score": -1.0, "tc_score": -1.0} with tempfile.TemporaryDirectory() as tmpdir: ref_path = os.path.join(tmpdir, "ref.fasta") test_path = os.path.join(tmpdir, "test.fasta") write_fasta(true_names, true_sequences, Path(ref_path)) write_fasta(test_names, test_sequences, Path(test_path)) # Verify files were written (guard against race / disk issues) if os.path.getsize(ref_path) == 0 or os.path.getsize(test_path) == 0: logger.warning("alignment_accuracy: empty FASTA written, skipping") return {"sp_score": -1.0, "tc_score": -1.0} try: if hasattr(_kalign, "compare_detailed"): d = _kalign.compare_detailed(ref_path, test_path, max_gap_frac=-1.0) return {"sp_score": d.get("recall", -1.0), "tc_score": d.get("tc", -1.0)} elif hasattr(_kalign, "compare"): sp = _kalign.compare(ref_path, test_path) return {"sp_score": sp, "tc_score": -1.0} else: return {"sp_score": -1.0, "tc_score": -1.0} except Exception as exc: logger.warning("alignment_accuracy failed: %s", exc) return {"sp_score": -1.0, "tc_score": -1.0} # --------------------------------------------------------------------------- # Tree comparison (dendropy) # --------------------------------------------------------------------------- def compare_trees(true_tree: str, inferred_tree: str) -> dict: """Compare two Newick tree strings. Uses ``dendropy.calculate.treecompare`` (imported lazily). Returns ------- dict ``{"nrf": float, "quartet_dist": float, "branch_score_dist": float}`` """ import dendropy from dendropy.calculate import treecompare tns = dendropy.TaxonNamespace() t1 = dendropy.Tree.get(data=true_tree, schema="newick", taxon_namespace=tns) t2 = dendropy.Tree.get(data=inferred_tree, schema="newick", taxon_namespace=tns) # Ensure the same leaf set t1.encode_bipartitions() t2.encode_bipartitions() nrf = treecompare.symmetric_difference(t1, t2) / (2 * (len(tns) - 3)) quartet_dist = treecompare.euclidean_distance(t1, t2) bsd = treecompare.weighted_robinson_foulds_distance(t1, t2) return { "nrf": nrf, "quartet_dist": quartet_dist, "branch_score_dist": bsd, } # --------------------------------------------------------------------------- # Statistical helpers # --------------------------------------------------------------------------- def bootstrap_ci( values: list[float], n_bootstrap: int = 10000, alpha: float = 0.05, ) -> tuple[float, float]: """Compute a bootstrap confidence interval for the mean. Parameters ---------- values : list[float] Observed values. n_bootstrap : int Number of bootstrap resamples. alpha : float Significance level (default 0.05 for a 95 % CI). Returns ------- (lower, upper) : tuple[float, float] """ import numpy as np arr = np.asarray(values, dtype=float) n = len(arr) rng = np.random.default_rng(seed=42) boot_means = np.empty(n_bootstrap) for i in range(n_bootstrap): sample = rng.choice(arr, size=n, replace=True) boot_means[i] = sample.mean() lower = float(np.percentile(boot_means, 100 * alpha / 2)) upper = float(np.percentile(boot_means, 100 * (1 - alpha / 2))) return lower, upper def wilcoxon_paired(a: list[float], b: list[float]) -> dict: """Wilcoxon signed-rank test with Cliff's delta effect size. Parameters ---------- a, b : list[float] Paired observations (same length). Returns ------- dict ``{"statistic": float, "p_value": float, "cliffs_delta": float}`` """ from scipy.stats import wilcoxon as _wilcoxon stat, pval = _wilcoxon(a, b) # Cliff's delta (non-parametric effect size) n = len(a) count = 0 for ai, bi in zip(a, b): if ai > bi: count += 1 elif ai < bi: count -= 1 cliffs_d = count / n return { "statistic": float(stat), "p_value": float(pval), "cliffs_delta": cliffs_d, } def holm_bonferroni(p_values: list[float]) -> list[float]: """Apply Holm-Bonferroni correction to a list of p-values. Returns adjusted p-values in the same order as the input. """ m = len(p_values) if m == 0: return [] # Sort indices by p-value indexed = sorted(enumerate(p_values), key=lambda x: x[1]) adjusted = [0.0] * m cummax = 0.0 for rank, (orig_idx, p) in enumerate(indexed): corrected = p * (m - rank) # Enforce monotonicity: adjusted value must be >= previous cummax = max(cummax, corrected) adjusted[orig_idx] = min(cummax, 1.0) return adjusted # --------------------------------------------------------------------------- # Method colours (for consistent figure styling) # --------------------------------------------------------------------------- METHOD_COLORS = { "kalign": "#1f77b4", # blue "kalign_cons": "#2ca02c", # green "kalign_ens3": "#ff7f0e", # orange "mafft": "#d62728", # red "muscle": "#9467bd", # purple "clustalo": "#7f7f7f", # grey "true": "#000000", # black (reference) } # --------------------------------------------------------------------------- # Internal helpers for runners # --------------------------------------------------------------------------- def _peak_mem_children_mb() -> float: """Return peak RSS (in MB) of child processes via getrusage. On macOS ``ru_maxrss`` is in bytes; on Linux it is in kilobytes. """ ru = resource.getrusage(resource.RUSAGE_CHILDREN) maxrss = ru.ru_maxrss import sys if sys.platform == "darwin": return maxrss / (1024 * 1024) return maxrss / 1024 def _parse_fasta_string(text: str) -> tuple[list[str], list[str]]: """Parse FASTA from a string. Returns (names, sequences).""" names: list[str] = [] sequences: list[str] = [] current: list[str] = [] for line in text.splitlines(): if line.startswith(">"): if current: sequences.append("".join(current)) current = [] names.append(line[1:].split()[0] if line[1:].strip() else "") else: current.append(line.strip()) if current: sequences.append("".join(current)) return names, sequences # --------------------------------------------------------------------------- # Alignment runners # --------------------------------------------------------------------------- def run_kalign( input_fasta: Path, ensemble: int = 0, seq_type: str = "auto", tgpo: float | None = None, terminal_dist_scale: float | None = None, refine: str | None = None, vsm_amax: float | None = None, realign: int | None = None, consistency: int | None = None, consistency_weight: float | None = None, ) -> AlignResult: """Run kalign via the Python API. Parameters ---------- input_fasta : Path Path to unaligned FASTA. ensemble : int Number of ensemble runs (0 = off). seq_type : str Sequence type passed to ``kalign.align_from_file``. Returns ------- AlignResult """ import kalign as _kalign start = time.perf_counter() extra = {} if tgpo is not None: extra["tgpo"] = tgpo if terminal_dist_scale is not None: extra["terminal_dist_scale"] = terminal_dist_scale if refine is not None: extra["refine"] = refine if vsm_amax is not None: extra["vsm_amax"] = vsm_amax if realign is not None: extra["realign"] = realign if consistency is not None: extra["consistency"] = consistency if consistency_weight is not None: extra["consistency_weight"] = consistency_weight result = _kalign.align_from_file( str(input_fasta), seq_type=seq_type, ensemble=ensemble, **extra, ) wall = time.perf_counter() - start # AlignedSequences supports unpacking and has .column_confidence etc. names = list(result.names) sequences = list(result.sequences) col_conf = result.column_confidence # may be None res_conf = result.residue_confidence # may be None ru = resource.getrusage(resource.RUSAGE_SELF) import sys if sys.platform == "darwin": mem_mb = ru.ru_maxrss / (1024 * 1024) else: mem_mb = ru.ru_maxrss / 1024 return AlignResult( sequences=sequences, names=names, column_confidence=col_conf, residue_confidence=res_conf, wall_time=wall, peak_memory_mb=mem_mb, ) def run_mafft(input_fasta: Path, timeout: int | None = None) -> AlignResult: """Run MAFFT (``mafft --auto``) via subprocess.""" start = time.perf_counter() proc = subprocess.run( ["mafft", "--auto", str(input_fasta)], capture_output=True, text=True, timeout=timeout, ) wall = time.perf_counter() - start mem_mb = _peak_mem_children_mb() if proc.returncode != 0: raise RuntimeError(f"mafft failed (exit {proc.returncode}): {proc.stderr}") names, sequences = _parse_fasta_string(proc.stdout) return AlignResult( sequences=sequences, names=names, column_confidence=None, residue_confidence=None, wall_time=wall, peak_memory_mb=mem_mb, ) def run_muscle(input_fasta: Path, timeout: int | None = None) -> AlignResult: """Run MUSCLE v5 (``muscle -align ... -output ...``) via subprocess.""" with tempfile.NamedTemporaryFile(suffix=".fasta", delete=False) as tmp: out_path = tmp.name try: start = time.perf_counter() proc = subprocess.run( ["muscle", "-align", str(input_fasta), "-output", out_path], capture_output=True, text=True, timeout=timeout, ) wall = time.perf_counter() - start mem_mb = _peak_mem_children_mb() if proc.returncode != 0: raise RuntimeError( f"muscle failed (exit {proc.returncode}): {proc.stderr}" ) names, sequences = parse_fasta(Path(out_path)) finally: if os.path.exists(out_path): os.unlink(out_path) return AlignResult( sequences=sequences, names=names, column_confidence=None, residue_confidence=None, wall_time=wall, peak_memory_mb=mem_mb, ) def run_clustalo(input_fasta: Path, timeout: int | None = None) -> AlignResult: """Run Clustal Omega (``clustalo -i ... -o ...``) via subprocess.""" with tempfile.NamedTemporaryFile(suffix=".fasta", delete=False) as tmp: out_path = tmp.name try: start = time.perf_counter() proc = subprocess.run( [ "clustalo", "-i", str(input_fasta), "-o", out_path, "--outfmt=fasta", "--force", ], capture_output=True, text=True, timeout=timeout, ) wall = time.perf_counter() - start mem_mb = _peak_mem_children_mb() if proc.returncode != 0: raise RuntimeError( f"clustalo failed (exit {proc.returncode}): {proc.stderr}" ) names, sequences = parse_fasta(Path(out_path)) finally: if os.path.exists(out_path): os.unlink(out_path) return AlignResult( sequences=sequences, names=names, column_confidence=None, residue_confidence=None, wall_time=wall, peak_memory_mb=mem_mb, ) def run_guidance2( input_fasta: Path, output_dir: Path, seq_type: str = "aa", n_bootstrap: int = 100, ) -> AlignResult: """Run GUIDANCE2 via the perl script. Expects ``guidance.pl`` to be on ``$PATH`` or pointed to by the ``GUIDANCE2_PATH`` environment variable. Parameters ---------- input_fasta : Path Unaligned FASTA input. output_dir : Path Working / output directory for GUIDANCE2 files. seq_type : str ``"aa"`` for protein, ``"nuc"`` for nucleotide (DNA/RNA). n_bootstrap : int Number of bootstrap guide-tree perturbations. Returns ------- AlignResult """ import shutil as _shutil guidance_bin = os.environ.get( "GUIDANCE2_PATH", _shutil.which("guidance2") or _shutil.which("guidance.pl") or "guidance.pl", ) output_dir = Path(output_dir).resolve() # Clean any stale output from previous runs — GUIDANCE2 may skip steps # or produce inconsistent results if old output files are present. if output_dir.exists(): _shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) input_fasta = Path(input_fasta).resolve() # GUIDANCE2 type flags g2_type = "aa" if seq_type in ("aa", "protein") else "nuc" cmd = [ "perl", guidance_bin, "--seqFile", str(input_fasta), "--msaProgram", "MAFFT", "--seqType", g2_type, "--outDir", str(output_dir), "--bootstraps", str(n_bootstrap), ] start = time.perf_counter() proc = subprocess.run(cmd, capture_output=True, text=True) wall = time.perf_counter() - start mem_mb = _peak_mem_children_mb() if proc.returncode != 0: raise RuntimeError( f"GUIDANCE2 failed (exit {proc.returncode}): {proc.stderr}" ) # Parse the base MSA msa_path = output_dir / "MSA.MAFFT.aln.With_Names" if not msa_path.exists(): # Fallback to the filtered alignment msa_path = output_dir / "MSA.MAFFT.Without_low_SP_Col.With_Names" if not msa_path.exists(): raise FileNotFoundError( f"GUIDANCE2 output alignment not found in {output_dir}" ) names, sequences = parse_fasta(msa_path) # Parse column confidence scores col_conf: Optional[list[float]] = None col_scores_path = output_dir / "MSA.MAFFT.Guidance2_col_col.scr" if col_scores_path.exists(): col_conf = _parse_guidance2_col_scores(col_scores_path, len(sequences[0])) # Parse residue confidence scores res_conf: Optional[list[list[float]]] = None res_scores_path = output_dir / "MSA.MAFFT.Guidance2_res_pair_res.scr" if res_scores_path.exists(): res_conf = _parse_guidance2_res_scores( res_scores_path, len(names), len(sequences[0]) ) return AlignResult( sequences=sequences, names=names, column_confidence=col_conf, residue_confidence=res_conf, wall_time=wall, peak_memory_mb=mem_mb, ) def _parse_guidance2_col_scores( path: Path, ncols: int ) -> list[float]: """Parse GUIDANCE2 per-column confidence file. The file has lines `` `` (1-based). """ scores = [0.0] * ncols with open(path) as fh: for line in fh: line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) >= 2: try: col = int(parts[0]) - 1 # 1-based to 0-based val = float(parts[1]) if 0 <= col < ncols: scores[col] = val except ValueError: continue return scores def _parse_guidance2_res_scores( path: Path, nseqs: int, ncols: int ) -> list[list[float]]: """Parse GUIDANCE2 per-residue confidence file. The file has lines `` `` (1-based). """ scores = [[0.0] * ncols for _ in range(nseqs)] with open(path) as fh: for line in fh: line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) >= 3: try: seq_i = int(parts[0]) - 1 col_j = int(parts[1]) - 1 val = float(parts[2]) if 0 <= seq_i < nseqs and 0 <= col_j < ncols: scores[seq_i][col_j] = val except ValueError: continue return scores # --------------------------------------------------------------------------- # METHODS registry # --------------------------------------------------------------------------- METHODS: dict[str, dict] = { "kalign": { "fn": run_kalign, "ensemble": 0, "mask": None, "vsm_amax": 2.0, }, "kalign_cons": { "fn": run_kalign, "ensemble": 0, "mask": None, "vsm_amax": 2.0, "consistency": 5, "consistency_weight": 2.0, }, "kalign_ens3": { "fn": run_kalign, "ensemble": 3, "mask": None, "vsm_amax": 2.0, "realign": 1, }, "mafft": {"fn": run_mafft, "mask": None}, "muscle": {"fn": run_muscle, "mask": None}, "clustalo": {"fn": run_clustalo, "mask": None}, "true": {"fn": None}, } # --------------------------------------------------------------------------- # High-level method dispatcher # --------------------------------------------------------------------------- def run_method( method_name: str, input_fasta: Path, work_dir: Path, seq_type: str = "auto", skip_masking: bool = False, timeout: int | None = None, ) -> AlignResult: """Run an alignment method by name. Looks up *method_name* in :data:`METHODS`, dispatches to the appropriate runner, and applies confidence masking or site-weight output when the method entry requests it. Parameters ---------- method_name : str Key into :data:`METHODS`. input_fasta : Path Unaligned FASTA file. work_dir : Path Scratch / output directory (used by GUIDANCE2 and for weight files). seq_type : str Sequence type hint (``"auto"``, ``"protein"``, ``"dna"``, etc.). Returns ------- AlignResult """ if method_name not in METHODS: raise ValueError( f"Unknown method {method_name!r}. " f"Available: {sorted(METHODS.keys())}" ) cfg = METHODS[method_name] fn = cfg["fn"] if fn is None: raise ValueError( f"Method {method_name!r} has no runner function (fn=None). " f"It must be handled specially by each pipeline." ) # -- dispatch to runner ------------------------------------------------ if fn is run_kalign: result = fn( input_fasta, ensemble=cfg.get("ensemble", 0), seq_type=seq_type, tgpo=cfg.get("tgpo"), terminal_dist_scale=cfg.get("terminal_dist_scale"), refine=cfg.get("refine"), vsm_amax=cfg.get("vsm_amax"), realign=cfg.get("realign"), consistency=cfg.get("consistency"), consistency_weight=cfg.get("consistency_weight"), ) elif fn is run_guidance2: g2_type = "nuc" if seq_type in ("dna", "rna") else "aa" g2_dir = Path(work_dir) / f"guidance2_{method_name}" result = fn( input_fasta, output_dir=g2_dir, seq_type=g2_type, n_bootstrap=cfg.get("n_bootstrap", 100), ) else: # mafft, muscle, clustalo -- simple signature result = fn(input_fasta, timeout=timeout) # -- optional site-weight output --------------------------------------- if cfg.get("weights") and result.column_confidence is not None: wt_path = Path(work_dir) / f"{method_name}_site_weights.txt" write_site_weights(result.column_confidence, wt_path) # -- optional confidence masking --------------------------------------- mask_threshold = cfg.get("mask") if not skip_masking and mask_threshold is not None and result.column_confidence is not None: masked_seqs, _n_kept = mask_alignment_by_confidence( result.sequences, result.column_confidence, mask_threshold ) result = AlignResult( sequences=masked_seqs, names=result.names, column_confidence=result.column_confidence, residue_confidence=result.residue_confidence, wall_time=result.wall_time, peak_memory_mb=result.peak_memory_mb, ) return result kalign-3.5.1/benchmarks/external_balibase.py000066400000000000000000000131521515023132300211150ustar00rootroot00000000000000"""Run external tools (mafft, muscle, clustalo) on BAliBASE inside container. Scores against references using kalign's compare_detailed with XML masks. Usage (inside container): python -m benchmarks.external_balibase """ import json import shutil import statistics import subprocess import tempfile import time from collections import defaultdict from pathlib import Path import kalign from .datasets import get_cases from .scoring import parse_balibase_xml def _run_external(tool, unaligned, output): """Run an external alignment tool.""" try: if tool == "mafft": with open(output, "w") as f: subprocess.run( ["mafft", "--auto", str(unaligned)], stdout=f, stderr=subprocess.PIPE, check=True, ) elif tool == "clustalo": subprocess.run( ["clustalo", "-i", str(unaligned), "-o", str(output), "--outfmt=fasta", "--force"], capture_output=True, check=True, ) elif tool == "muscle": subprocess.run( ["muscle", "-align", str(unaligned), "-output", str(output)], capture_output=True, check=True, ) return True except (subprocess.CalledProcessError, FileNotFoundError): return False def _score_case(case, output_path): xml_path = case.reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) return kalign.compare_detailed(str(case.reference), str(output_path)) def main(): tools = [] for t in ["mafft", "muscle", "clustalo"]: if shutil.which(t): tools.append(t) else: print(f" {t}: not found, skipping") if not tools: print("No external tools found. Run inside container.") return print(f"Tools: {tools}") cases = get_cases("balibase") print(f"{len(cases)} BAliBASE cases") results = [] n_tasks = len(tools) * len(cases) done = 0 t0 = time.perf_counter() for tool in tools: print(f"\n Tool: {tool}", flush=True) for case in cases: with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: t1 = time.perf_counter() ok = _run_external(tool, case.unaligned, tmp_path) wall = time.perf_counter() - t1 if ok: scores = _score_case(case, tmp_path) results.append({ "family": case.family, "dataset": case.dataset, "method": tool, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], "wall_time": wall, }) else: results.append({ "family": case.family, "dataset": case.dataset, "method": tool, "recall": 0, "precision": 0, "f1": 0, "tc": 0, "wall_time": 0, "error": f"{tool} failed", }) finally: Path(tmp_path).unlink(missing_ok=True) done += 1 if done % 20 == 0: elapsed = time.perf_counter() - t0 eta = elapsed / done * (n_tasks - done) print(f" {done}/{n_tasks} ({elapsed:.0f}s, ETA {eta:.0f}s)", flush=True) elapsed = time.perf_counter() - t0 print(f"\nAll done in {elapsed:.0f}s") # Summary groups = defaultdict(list) for r in results: groups[r["method"]].append(r) print(f"\n{'Method':>12} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time':>7}") print("-" * 60) for tool in tools: entries = [r for r in groups[tool] if "error" not in r] if not entries: print(f"{tool:>12} (no results)") continue rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) wt = sum(r["wall_time"] for r in entries) print(f"{tool:>12} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f} {wt:>6.1f}s") # Per-category all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) for cat in all_cats: cat_results = [r for r in results if cat in r["dataset"]] cat_groups = defaultdict(list) for r in cat_results: cat_groups[r["method"]].append(r) n = len(cat_groups.get(tools[0], [])) print(f"\n=== {cat} ({n} cases) ===") print(f"{'Method':>12} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}") print("-" * 50) for tool in tools: entries = [r for r in cat_groups.get(tool, []) if "error" not in r] if not entries: continue rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) print(f"{tool:>12} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}") out = Path("benchmarks/data/external_balibase.json") out.parent.mkdir(parents=True, exist_ok=True) json.dump(results, open(out, "w"), indent=2) print(f"\nSaved to {out}") if __name__ == "__main__": main() kalign-3.5.1/benchmarks/full_comparison.py000066400000000000000000000154721515023132300206540ustar00rootroot00000000000000"""Full BAliBASE comparison: all kalign modes + external tools (cached). Shows ALL metrics: SP(=Recall), Precision, F1, TC, Time — overall and per-category. """ import json import statistics import tempfile import time from collections import defaultdict from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path import kalign from benchmarks.datasets import get_cases from benchmarks.scoring import parse_balibase_xml def _score_case(case, output_path): xml_path = case.reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed( str(case.reference), str(output_path), column_mask=mask ) return kalign.compare_detailed(str(case.reference), str(output_path)) def _run_one(args): case, config_name, kwargs = args with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: t0 = time.perf_counter() kalign.align_file_to_file( str(case.unaligned), tmp_path, format="fasta", seq_type=case.seq_type, **kwargs, ) wall_time = time.perf_counter() - t0 scores = _score_case(case, tmp_path) return { "family": case.family, "dataset": case.dataset, "config": config_name, **scores, "wall_time": wall_time, } except Exception as e: return { "family": case.family, "dataset": case.dataset, "config": config_name, "error": str(e), } finally: Path(tmp_path).unlink(missing_ok=True) def _print_table(title, display_order, summary_data, configs): """Print one table with ALL metrics: SP, Prec, F1, TC, Time.""" print(f"\n{'=' * 100}") print(f" {title}") print(f"{'=' * 100}") print( f"{'Method':<24} {'SP':>8} {'Prec':>8} {'F1':>8} {'TC':>8}" f" {'Time':>8} {'N':>4}" ) print("-" * 100) for name in display_order: if name not in summary_data: continue sp, prec, f1, tc, t, n = summary_data[name] marker = " " if name in configs else "*" t_str = f"{t:.0f}s" if t is not None else "—" print( f"{marker}{name:<23} {sp:>8.3f} {prec:>8.3f} {f1:>8.3f}" f" {tc:>8.3f} {t_str:>8} {n:>4}" ) print("-" * 100) print(" SP = Sum-of-Pairs recall (BAliBASE convention)") print(" * = external tool (cached results)") def main(): cases = get_cases("balibase") print(f"BAliBASE: {len(cases)} cases\n") # Kalign configurations to test configs = { "baseline(vsm=0)": {"vsm_amax": 0.0}, "+vsm": {"vsm_amax": 2.0}, "+vsm+ref": {"vsm_amax": 2.0, "refine": "confident"}, "+vsm+ref+sw": { "vsm_amax": 2.0, "refine": "confident", "seq_weights": 1.0, }, "+vsm+ref+sw+c5": { "vsm_amax": 2.0, "refine": "confident", "seq_weights": 1.0, "consistency": 5, "consistency_weight": 2.0, }, "ens3+vsm": { "vsm_amax": 2.0, "ensemble": 3, }, "ens3+vsm+ref": { "vsm_amax": 2.0, "ensemble": 3, "refine": "confident", }, "ens3+vsm+ref+ra1": { "vsm_amax": 2.0, "ensemble": 3, "refine": "confident", "realign": 1, }, "ens3+vsm+ref+ra1+c5": { "vsm_amax": 2.0, "ensemble": 3, "refine": "confident", "realign": 1, "consistency": 5, "consistency_weight": 2.0, }, } tasks = [(c, name, kw) for c in cases for name, kw in configs.items()] print(f"{len(configs)} configs x {len(cases)} cases = {len(tasks)} tasks\n") results = [] done = 0 t0 = time.perf_counter() with ProcessPoolExecutor(max_workers=12) as pool: futures = {pool.submit(_run_one, t): t for t in tasks} for f in as_completed(futures): done += 1 r = f.result() results.append(r) if done % 200 == 0: elapsed = time.perf_counter() - t0 print(f" {done}/{len(tasks)} ({elapsed:.0f}s)") elapsed = time.perf_counter() - t0 print(f"\nKalign runs done in {elapsed:.0f}s\n") # Load cached external results ext_path = Path("benchmarks/data/external_balibase.json") if ext_path.exists(): ext_data = json.load(open(ext_path)) for r in ext_data: r["config"] = r.pop("method") results.extend(ext_data) ext_methods = set(r["config"] for r in ext_data) print(f"Loaded {len(ext_data)} cached external results: {ext_methods}\n") # Group by config groups = defaultdict(list) for r in results: if "error" not in r: groups[r["config"]].append(r) # Display order: kalign configs first, then external sorted display_order = list(configs.keys()) + sorted( k for k in groups if k not in configs ) # Compute overall summary: (sp, prec, f1, tc, total_time, count) overall = {} for name in display_order: entries = groups.get(name, []) if not entries: continue sp = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) t = sum(r["wall_time"] for r in entries) overall[name] = (sp, prec, f1, tc, t, len(entries)) # Print overall table _print_table("OVERALL (218 cases)", display_order, overall, configs) # Group results by category categories = ["RV11", "RV12", "RV20", "RV30", "RV40", "RV50"] by_cat = defaultdict(lambda: defaultdict(list)) for r in results: if "error" not in r: cat = r["dataset"].replace("balibase_", "") by_cat[cat][r["config"]].append(r) # Per-category tables — each with ALL metrics for cat in categories: cat_summary = {} for name in display_order: entries = by_cat[cat].get(name, []) if not entries: continue sp = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) t = sum(r["wall_time"] for r in entries) cat_summary[name] = (sp, prec, f1, tc, t, len(entries)) _print_table(f"{cat} ({len(by_cat[cat].get(display_order[0], []))} cases)", display_order, cat_summary, configs) if __name__ == "__main__": main() kalign-3.5.1/benchmarks/make_summary_figure.py000066400000000000000000000203301515023132300215000ustar00rootroot00000000000000#!/usr/bin/env python3 """Generate a summary figure from downstream benchmark results. Produces a 2x2 grid of line plots showing method performance vs difficulty, with the "true" alignment as a dashed black ceiling line. - Panel A: Phylo accuracy (nRF) vs tree_depth - Panel B: Positive selection (F1) vs n_taxa - Panel C: Alignment accuracy (SP) vs tree_depth - Panel D: Alignment accuracy (SP) vs indel_rate """ import json import re from collections import defaultdict from pathlib import Path import matplotlib.pyplot as plt import numpy as np RESULTS_DIR = Path("benchmarks/results") # Method display order (true last — rendered as dashed black) METHOD_ORDER = [ "kalign", "kalign_ens3", "mafft", "muscle", "clustalo", "true", ] METHOD_LABELS = { "kalign": "Kalign", "kalign_ens3": "Kalign ens3", "mafft": "MAFFT", "muscle": "MUSCLE", "clustalo": "Clustal Omega", "true": "True alignment", } METHOD_COLORS = { "kalign": "#1f77b4", "kalign_ens3": "#2ca02c", "mafft": "#9467bd", "muscle": "#8c564b", "clustalo": "#7f7f7f", "true": "#000000", } def load_cases(pipeline: str) -> list[dict]: """Load per-case results from the latest.json for a pipeline.""" path = RESULTS_DIR / pipeline / "latest.json" with open(path) as f: data = json.load(f) return data.get("cases", []) def parse_sim_id(sim_id: str) -> dict: """Extract parameters from a sim_id string. Examples: WAG_t16_d2.0_ir0.10_il2.0_r0 -> {model:WAG, n_taxa:16, tree_depth:2.0, ...} M8_t16_d0.5_ir0.05_ps0.10_r0 -> {model:M8, n_taxa:16, tree_depth:0.5, ...} """ params = {} m = re.match(r"^(\w+)_t(\d+)_d([\d.]+)_ir([\d.]+)", sim_id) if m: params["model"] = m.group(1) params["n_taxa"] = int(m.group(2)) params["tree_depth"] = float(m.group(3)) params["indel_rate"] = float(m.group(4)) # Protein: _il{mean}_r{rep} m_il = re.search(r"_il([\d.]+)", sim_id) if m_il: params["indel_length_mean"] = float(m_il.group(1)) # Codon: _ps{frac}_r{rep} m_ps = re.search(r"_ps([\d.]+)", sim_id) if m_ps: params["psel_fraction"] = float(m_ps.group(1)) m_r = re.search(r"_r(\d+)$", sim_id) if m_r: params["replicate"] = int(m_r.group(1)) return params def group_by(cases: list[dict], param_key: str, metric_key: str): """Group per-case results by a sim_id parameter. Returns {method: {x_value: [metric_values]}}. """ grouped = defaultdict(lambda: defaultdict(list)) for c in cases: if "error" in c: continue sim_params = parse_sim_id(c.get("sim_id", "")) if param_key not in sim_params: continue x_val = sim_params[param_key] method = c["method"] val = c.get(metric_key) if val is not None and not (isinstance(val, float) and np.isnan(val)): grouped[method][x_val].append(val) return grouped def style_ax(ax, hint=""): """Apply clean Nature-style aesthetics.""" ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.tick_params(labelsize=9) if hint: ax.annotate( hint, xy=(0.98, 0.95), xycoords="axes fraction", ha="right", va="top", fontsize=8, fontstyle="italic", color="gray", ) def plot_lines(ax, grouped, methods, xlabel, ylabel): """Plot lines with error ribbons for each method. Parameters ---------- grouped : dict {method: {x_value: [metric_values]}} methods : list[str] Method names in plot order. """ for method in methods: if method not in grouped: continue data = grouped[method] xs = sorted(data.keys()) means = [] ses = [] for x in xs: vals = np.asarray(data[x], dtype=float) means.append(vals.mean()) ses.append(vals.std() / max(1, np.sqrt(len(vals)))) means = np.asarray(means) ses = np.asarray(ses) color = METHOD_COLORS.get(method, "#333333") label = METHOD_LABELS.get(method, method) linestyle = "--" if method == "true" else "-" linewidth = 1.5 if method != "true" else 2.0 marker = "o" if method != "true" else "" ax.plot( xs, means, color=color, linestyle=linestyle, linewidth=linewidth, marker=marker, markersize=4, label=label, zorder=3 if method != "true" else 2, ) ax.fill_between( xs, means - ses, means + ses, color=color, alpha=0.12, zorder=1, ) ax.set_xlabel(xlabel, fontsize=10) ax.set_ylabel(ylabel, fontsize=10) def main(): fig, axes = plt.subplots(2, 2, figsize=(14, 10)) fig.suptitle( "Kalign 3.5 — Downstream Benchmarks by Difficulty", fontsize=16, fontweight="bold", y=0.98, ) # Determine which methods are present in results available = set() for pipeline in ("phylo_accuracy", "positive_selection", "calibration"): try: cases = load_cases(pipeline) for c in cases: if "error" not in c: available.add(c["method"]) except FileNotFoundError: pass methods = [m for m in METHOD_ORDER if m in available] # ── Panel A: Phylo accuracy (nRF) vs tree_depth ────────────────── ax = axes[0, 0] try: cases = load_cases("phylo_accuracy") grouped = group_by(cases, "tree_depth", "nrf") plot_lines(ax, grouped, methods, "Tree depth", "Normalized RF distance") style_ax(ax, "lower = better") ax.set_title("A) Phylogenetic tree accuracy", fontweight="bold", loc="left", fontsize=11) except FileNotFoundError: ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") style_ax(ax) # ── Panel B: Positive selection (F1) vs n_taxa ─────────────────── ax = axes[0, 1] try: cases = load_cases("positive_selection") grouped = group_by(cases, "n_taxa", "f1") plot_lines(ax, grouped, methods, "Number of taxa", "F1 score") style_ax(ax, "higher = better") ax.set_title("B) Positive selection detection (FUBAR)", fontweight="bold", loc="left", fontsize=11) except FileNotFoundError: ax.text(0.5, 0.5, "No positive_selection results", transform=ax.transAxes, ha="center") style_ax(ax) # ── Panel C: Alignment accuracy (SP) vs tree_depth ─────────────── ax = axes[1, 0] try: cases = load_cases("phylo_accuracy") grouped = group_by(cases, "tree_depth", "sp_score") plot_lines(ax, grouped, methods, "Tree depth", "SP score vs true alignment") style_ax(ax, "higher = better") ax.set_title("C) Alignment accuracy vs tree depth", fontweight="bold", loc="left", fontsize=11) except FileNotFoundError: ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") style_ax(ax) # ── Panel D: Alignment accuracy (SP) vs indel_rate ─────────────── ax = axes[1, 1] try: cases = load_cases("phylo_accuracy") grouped = group_by(cases, "indel_rate", "sp_score") plot_lines(ax, grouped, methods, "Indel rate", "SP score vs true alignment") style_ax(ax, "higher = better") ax.set_title("D) Alignment accuracy vs indel rate", fontweight="bold", loc="left", fontsize=11) except FileNotFoundError: ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") style_ax(ax) # Add legend (shared across panels) handles, labels = axes[0, 0].get_legend_handles_labels() if handles: fig.legend( handles, labels, loc="lower center", ncol=min(len(handles), 7), fontsize=9, frameon=False, bbox_to_anchor=(0.5, 0.0), ) plt.tight_layout(rect=[0, 0.04, 1, 0.95]) out = Path("benchmarks/figures/downstream_summary.png") out.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out, dpi=200, bbox_inches="tight", facecolor="white") print(f"Saved to {out}") plt.close() if __name__ == "__main__": main() kalign-3.5.1/benchmarks/mumsa_plots.py000066400000000000000000000166371515023132300200270ustar00rootroot00000000000000"""Generate publication figures for MUMSA consensus precision analysis. Reads results from benchmarks/results/mumsa_precision.json (produced by mumsa_precision.py) and generates two figures: Figure 1: Precision-recall tradeoff curve showing consensus support thresholds vs single-tool baselines. Figure 2: Per-category precision bar chart at selected thresholds alongside external tools. Usage: python -m benchmarks.mumsa_plots [--output-dir figures/] Only requires matplotlib + json (no kalign dependency). """ import argparse import json import statistics from collections import defaultdict from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt # Consistent colours COLORS = { "consensus": "#2166ac", "kalign_baseline": "#888888", "ensemble": "#4daf4a", "mafft": "#e41a1c", "muscle": "#ff7f00", "clustalo": "#984ea3", } TOOL_LABELS = { "kalign_baseline": "kalign", "ensemble": "kalign ensemble", "mafft": "MAFFT", "muscle": "MUSCLE", "clustalo": "Clustal Omega", } def load_results(json_path): data = json.load(open(json_path)) return data["results"], data.get("n_runs", 8) def aggregate(results, group_key="method"): """Group results by method, return {method: {metric: mean_value}}.""" groups = defaultdict(list) for r in results: groups[r[group_key]].append(r) agg = {} for method, entries in groups.items(): agg[method] = { "recall": statistics.mean(e["recall"] for e in entries), "precision": statistics.mean(e["precision"] for e in entries), "f1": statistics.mean(e["f1"] for e in entries), "tc": statistics.mean(e["tc"] for e in entries), "n": len(entries), } return agg def aggregate_by_category(results): """Group results by (category, method).""" groups = defaultdict(list) for r in results: cat = r["dataset"].replace("balibase_", "") groups[(cat, r["method"])].append(r) agg = {} for (cat, method), entries in groups.items(): agg[(cat, method)] = { "recall": statistics.mean(e["recall"] for e in entries), "precision": statistics.mean(e["precision"] for e in entries), "f1": statistics.mean(e["f1"] for e in entries), "tc": statistics.mean(e["tc"] for e in entries), "n": len(entries), } return agg def figure_precision_vs_support(results, n_runs, output_path): """Precision and recall as a function of consensus support threshold, with horizontal reference lines for external tools and kalign baseline.""" agg = aggregate(results) fig, ax = plt.subplots(figsize=(6, 4.5)) # Consensus curves consensus_methods = sorted( [m for m in agg if m.startswith("consensus_ms")], key=lambda m: int(m.replace("consensus_ms", "")) ) support_vals = [int(m.replace("consensus_ms", "")) for m in consensus_methods] prec = [agg[m]["precision"] for m in consensus_methods] rec = [agg[m]["recall"] for m in consensus_methods] ax.plot(support_vals, prec, "o-", color=COLORS["consensus"], linewidth=2.5, markersize=7, zorder=5, label="Consensus precision") ax.plot(support_vals, rec, "s--", color=COLORS["consensus"], linewidth=1.5, markersize=5, alpha=0.5, zorder=4, label="Consensus recall") # Reference lines for external tools and baselines (precision only) ref_methods = [ ("kalign_baseline", COLORS["kalign_baseline"], "kalign"), ("ensemble", COLORS["ensemble"], "kalign ensemble"), ("mafft", COLORS["mafft"], "MAFFT"), ("muscle", COLORS["muscle"], "MUSCLE"), ("clustalo", COLORS["clustalo"], "Clustal Omega"), ] for method, color, label in ref_methods: if method in agg: ax.axhline(agg[method]["precision"], color=color, linestyle=":", linewidth=1.5, alpha=0.8, label=f"{label} precision") ax.set_xlabel("Minimum support threshold (s)", fontsize=11) ax.set_ylabel("Score", fontsize=11) ax.set_title("Consensus precision increases with support threshold", fontsize=11) ax.set_xticks(support_vals) ax.legend(fontsize=7.5, loc="center left", bbox_to_anchor=(1.02, 0.5), framealpha=0.9) ax.set_ylim(0.35, 1.0) ax.grid(True, alpha=0.3) fig.tight_layout() fig.savefig(output_path, dpi=300, bbox_inches="tight") plt.close(fig) print(f"Saved {output_path}") def figure_precision_by_category(results, n_runs, output_path): """Per-category grouped bar chart of precision at selected thresholds.""" agg = aggregate_by_category(results) categories = sorted({r["dataset"].replace("balibase_", "") for r in results}) # Methods to show: baseline, ms=3, ms=n_runs, ensemble, mafft, muscle, clustalo mid = max(1, n_runs // 2) bar_methods = [ ("kalign_baseline", "kalign", COLORS["kalign_baseline"]), (f"consensus_ms{mid}", f"consensus s={mid}", "#6baed6"), (f"consensus_ms{n_runs}", f"consensus s={n_runs}", COLORS["consensus"]), ("ensemble", "ensemble", COLORS["ensemble"]), ("mafft", "MAFFT", COLORS["mafft"]), ("muscle", "MUSCLE", COLORS["muscle"]), ("clustalo", "Clustal\u03a9", COLORS["clustalo"]), ] n_cats = len(categories) n_bars = len(bar_methods) bar_width = 0.11 x = range(n_cats) fig, ax = plt.subplots(figsize=(9, 4.5)) for bi, (method, label, color) in enumerate(bar_methods): vals = [] for cat in categories: key = (cat, method) vals.append(agg[key]["precision"] if key in agg else 0) offsets = [xi + (bi - n_bars / 2 + 0.5) * bar_width for xi in x] ax.bar(offsets, vals, bar_width, label=label, color=color, edgecolor="white", linewidth=0.3) ax.set_xlabel("BAliBASE category", fontsize=11) ax.set_ylabel("Precision", fontsize=11) ax.set_title("Precision by category and method", fontsize=11) ax.set_xticks(list(x)) ax.set_xticklabels(categories, fontsize=10) ax.legend(fontsize=7.5, ncol=4, loc="upper center", bbox_to_anchor=(0.5, -0.12), framealpha=0.9) ax.set_ylim(0, 1.05) ax.grid(True, axis="y", alpha=0.3) fig.tight_layout() fig.savefig(output_path, dpi=300, bbox_inches="tight") plt.close(fig) print(f"Saved {output_path}") def main(): parser = argparse.ArgumentParser( description="Generate MUMSA precision figures from saved results." ) parser.add_argument( "--input", default="benchmarks/results/mumsa_precision.json", help="Path to results JSON (default: benchmarks/results/mumsa_precision.json)" ) parser.add_argument( "--output-dir", default="benchmarks/figures", help="Directory for output figures (default: benchmarks/figures/)" ) parser.add_argument( "--format", default="pdf", choices=["pdf", "png", "svg"], help="Output format (default: pdf)" ) args = parser.parse_args() results, n_runs = load_results(args.input) out_dir = Path(args.output_dir) out_dir.mkdir(parents=True, exist_ok=True) figure_precision_vs_support( results, n_runs, out_dir / f"mumsa_precision_vs_support.{args.format}", ) figure_precision_by_category( results, n_runs, out_dir / f"mumsa_precision_by_category.{args.format}", ) if __name__ == "__main__": main() kalign-3.5.1/benchmarks/mumsa_precision.py000066400000000000000000000256541515023132300206600ustar00rootroot00000000000000"""Verify MUMSA claim: consensus alignments at higher support thresholds have higher precision (fraction of aligned residue pairs that are correct). Runs kalign ensemble_consensus at min_support = 1..N for each BAliBASE case, scores against references, and compares precision with baseline/external tools. """ import argparse import json import statistics import tempfile import time from collections import defaultdict from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path import kalign from .datasets import get_cases from .scoring import parse_balibase_xml def score_case(case, output_path): """Score a test alignment against the BAliBASE reference. Returns dict with recall, precision, f1, tc.""" xml_path = case.reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed( str(case.reference), str(output_path), column_mask=mask ) return kalign.compare_detailed(str(case.reference), str(output_path)) def _run_one_case(case, n_runs, max_support): """Process a single BAliBASE case: run all support thresholds + baseline + ensemble. Returns list of result dicts.""" results = [] for ms in range(1, max_support + 1): with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: kalign.align_file_to_file( str(case.unaligned), tmp_path, format="fasta", seq_type=case.seq_type, ensemble=n_runs, min_support=ms, ) scores = score_case(case, tmp_path) results.append({ "family": case.family, "dataset": case.dataset, "method": f"consensus_ms{ms}", "min_support": ms, "n_runs": n_runs, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], }) except Exception as e: results.append({ "family": case.family, "dataset": case.dataset, "method": f"consensus_ms{ms}", "min_support": ms, "n_runs": n_runs, "recall": 0, "precision": 0, "f1": 0, "tc": 0, "error": str(e), }) finally: Path(tmp_path).unlink(missing_ok=True) # Normal ensemble (no consensus) with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: kalign.align_file_to_file( str(case.unaligned), tmp_path, format="fasta", seq_type=case.seq_type, ensemble=n_runs, ) scores = score_case(case, tmp_path) results.append({ "family": case.family, "dataset": case.dataset, "method": "ensemble", "min_support": 0, "n_runs": n_runs, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], }) except Exception: pass finally: Path(tmp_path).unlink(missing_ok=True) # Baseline kalign with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: kalign.align_file_to_file( str(case.unaligned), tmp_path, format="fasta", seq_type=case.seq_type, ) scores = score_case(case, tmp_path) results.append({ "family": case.family, "dataset": case.dataset, "method": "kalign_baseline", "min_support": 0, "n_runs": 0, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], }) except Exception: pass finally: Path(tmp_path).unlink(missing_ok=True) return results def _worker(args): """Picklable worker for ProcessPoolExecutor.""" case, n_runs, max_support = args return _run_one_case(case, n_runs, max_support) def run_consensus_sweep(cases, n_runs=8, max_support=None, categories=None, parallel=1): """Run ensemble consensus at each support threshold for each case.""" if max_support is None: max_support = n_runs # Filter cases filtered = [] for case in cases: if categories and not any(cat in case.dataset.upper() for cat in categories): continue filtered.append(case) all_results = [] if parallel > 1: tasks = [(case, n_runs, max_support) for case in filtered] done = 0 with ProcessPoolExecutor(max_workers=parallel) as pool: futures = {pool.submit(_worker, t): t[0].family for t in tasks} for future in as_completed(futures): done += 1 fam = futures[future] try: results = future.result() all_results.extend(results) print(f" [{done}/{len(filtered)}] {fam} done") except Exception as e: print(f" [{done}/{len(filtered)}] {fam} FAILED: {e}") else: for ci, case in enumerate(filtered): print(f" [{ci+1}/{len(filtered)}] {case.family} ({case.dataset})", end="", flush=True) results = _run_one_case(case, n_runs, max_support) all_results.extend(results) print(" done") return all_results def load_external_scores(json_path="benchmarks/results/full_comparison.json"): """Load external tool scores (mafft, muscle, clustalo) from saved results.""" p = Path(json_path) if not p.exists(): return {} data = json.load(open(p)) ext = {} for r in data["results"]: if r["method"] in ("mafft", "muscle", "clustalo"): key = (r["family"], r["method"]) ext[key] = { "recall": r.get("recall", 0), "precision": r.get("precision", 0), "f1": r.get("f1", 0), "tc": r.get("tc", 0), } return ext def summarize(results, external_scores=None): """Print summary table of precision across support thresholds.""" by_method = defaultdict(list) by_method_cat = defaultdict(lambda: defaultdict(list)) for r in results: by_method[r["method"]].append(r) cat = r["dataset"].replace("balibase_", "") by_method_cat[r["method"]][cat].append(r) if external_scores: families = {r["family"] for r in results} family_datasets = {r["family"]: r["dataset"] for r in results} for tool in ("mafft", "muscle", "clustalo"): for fam in families: key = (fam, tool) if key in external_scores: s = external_scores[key] ds = family_datasets.get(fam, "") cat = ds.replace("balibase_", "") entry = {"family": fam, "dataset": ds, **s} by_method[tool].append(entry) by_method_cat[tool][cat].append(entry) def method_sort_key(m): if m == "kalign_baseline": return (0, 0) if m.startswith("consensus_ms"): return (1, int(m.replace("consensus_ms", ""))) if m == "ensemble": return (2, 0) return (3, {"mafft": 0, "muscle": 1, "clustalo": 2}.get(m, 9)) methods = sorted(by_method.keys(), key=method_sort_key) print("\n=== Overall (all categories) ===") print(f"{'Method':<22} {'Recall':>8} {'Precision':>10} {'F1':>8} {'TC':>8} N") print("-" * 70) for m in methods: entries = by_method[m] n = len(entries) rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) print(f"{m:<22} {rec:>8.3f} {prec:>10.3f} {f1:>8.3f} {tc:>8.3f} {n}") all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) for cat in all_cats: print(f"\n=== {cat} ===") print(f"{'Method':<22} {'Recall':>8} {'Precision':>10} {'F1':>8} {'TC':>8} N") print("-" * 70) for m in methods: entries = by_method_cat[m].get(cat, []) if not entries: continue n = len(entries) rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) print(f"{m:<22} {rec:>8.3f} {prec:>10.3f} {f1:>8.3f} {tc:>8.3f} {n}") def main(): parser = argparse.ArgumentParser(description="MUMSA precision verification") parser.add_argument("--n-runs", type=int, default=8, help="Number of ensemble runs (default: 8)") parser.add_argument("--max-support", type=int, default=None, help="Maximum min_support threshold (default: n_runs)") parser.add_argument("--categories", nargs="*", default=None, help="BAliBASE categories to test (e.g. RV11 RV12)") parser.add_argument("--max-cases", type=int, default=0, help="Limit number of cases (0 = all)") parser.add_argument("--parallel", "-j", type=int, default=4, help="Number of parallel workers (default: 4)") args = parser.parse_args() categories = [c.upper() for c in args.categories] if args.categories else None cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) print(f"Running MUMSA precision analysis on {len(cases)} BAliBASE cases") print(f"Ensemble runs: {args.n_runs}, max support: {args.max_support or args.n_runs}") print(f"Parallel workers: {args.parallel}") results = run_consensus_sweep( cases, n_runs=args.n_runs, max_support=args.max_support, categories=categories, parallel=args.parallel, ) external = load_external_scores() summarize(results, external) # Save results for plotting out_dir = Path("benchmarks/results") out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / "mumsa_precision.json" # Merge external scores into a flat list alongside our results ext_list = [] if external: families = {r["family"] for r in results} family_datasets = {r["family"]: r["dataset"] for r in results} for tool in ("mafft", "muscle", "clustalo"): for fam in families: key = (fam, tool) if key in external: ext_list.append({ "family": fam, "dataset": family_datasets.get(fam, ""), "method": tool, "min_support": 0, "n_runs": 0, **external[key], }) json.dump({"results": results + ext_list, "n_runs": args.n_runs}, open(out_path, "w"), indent=2) print(f"\nResults saved to {out_path}") if __name__ == "__main__": main() kalign-3.5.1/benchmarks/runner.py000066400000000000000000000230441515023132300167630ustar00rootroot00000000000000"""Main benchmark orchestrator and CLI.""" import argparse import json import statistics import time from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path from typing import List, Optional from .datasets import download_dataset, get_cases, DATASETS from .scoring import AlignmentResult, EXTERNAL_TOOLS, run_case def _run_one(args): """Worker function for parallel execution.""" case, method, binary, n_threads, refine, adaptive_budget, ensemble = args return run_case(case, method=method, binary=binary, n_threads=n_threads, refine=refine, adaptive_budget=adaptive_budget, ensemble=ensemble) def _result_label(r) -> str: """Format a concise label showing method and config for verbose output.""" if r.method in EXTERNAL_TOOLS: return r.method parts = ["kalign"] if r.refine != "none": parts.append(f"refine={r.refine}") if r.ensemble: parts.append(f"ens={r.ensemble}") return " ".join(parts) def run_benchmark( dataset: str = "balibase", methods: Optional[List[str]] = None, refine_modes: Optional[List[str]] = None, max_cases: int = 0, binary: str = "kalign", n_threads: int = 1, verbose: bool = False, adaptive_budget: bool = False, ensemble: int = 0, parallel: int = 1, ) -> List[AlignmentResult]: """Run benchmark suite and return results.""" if methods is None: methods = ["python_api"] if refine_modes is None: refine_modes = ["none"] cases = get_cases(dataset, max_cases=max_cases if max_cases > 0 else None) if not cases: print(f"No benchmark cases found for dataset '{dataset}'.") print("Try running with --download-only first.") return [] print(f"Running {len(cases)} cases from '{dataset}' with methods: {methods}, refine: {refine_modes}") if parallel > 1: print(f"Using {parallel} parallel workers") print() # Build work items work = [] for case in cases: for method in methods: if method in EXTERNAL_TOOLS: # External tools don't support refine/ensemble — run once work.append((case, method, binary, n_threads, "none", False, 0)) else: for refine in refine_modes: work.append((case, method, binary, n_threads, refine, adaptive_budget, ensemble)) total = len(work) if parallel <= 1: # Sequential (original behavior) results = [] for i, item in enumerate(work): result = _run_one(item) results.append(result) if verbose: label = _result_label(result) if result.error: print(f"[{i+1}/{total}] {result.family:<12} {label:<25} ERROR: {result.error}") else: print(f"[{i+1}/{total}] {result.family:<12} {label:<25} SP={result.recall:.3f} TC={result.tc:.3f} F1={result.f1:.3f} {result.wall_time:.1f}s") else: # Parallel execution results = [None] * total done = 0 with ProcessPoolExecutor(max_workers=parallel) as pool: futures = {pool.submit(_run_one, item): i for i, item in enumerate(work)} for future in as_completed(futures): idx = futures[future] result = future.result() results[idx] = result done += 1 if verbose: label = _result_label(result) if result.error: print(f"[{done}/{total}] {result.family:<12} {label:<25} ERROR: {result.error}") else: print(f"[{done}/{total}] {result.family:<12} {label:<25} SP={result.recall:.3f} TC={result.tc:.3f} F1={result.f1:.3f} {result.wall_time:.1f}s") return results def print_summary(results: List[AlignmentResult]) -> None: """Print aggregate summary of benchmark results.""" by_group = {} for r in results: if r.error: continue ens = f" ensemble={r.ensemble}" if r.ensemble else "" key = f"{r.method} refine={r.refine}{ens}" by_group.setdefault(key, []).append(r) for group, group_results in sorted(by_group.items()): recalls = [r.recall for r in group_results] precisions = [r.precision for r in group_results] f1s = [r.f1 for r in group_results] tcs = [r.tc for r in group_results] times = [r.wall_time for r in group_results] print(f"\n--- {group} ({len(group_results)} cases) ---") print(f" SP: mean={statistics.mean(recalls):.3f} " f"median={statistics.median(recalls):.3f} " f"min={min(recalls):.3f} max={max(recalls):.3f}") print(f" TC: mean={statistics.mean(tcs):.3f} " f"median={statistics.median(tcs):.3f}") print(f" Precision: mean={statistics.mean(precisions):.3f} " f"median={statistics.median(precisions):.3f}") print(f" F1: mean={statistics.mean(f1s):.3f} " f"median={statistics.median(f1s):.3f}") print(f" Time (s): total={sum(times):.1f} " f"mean={statistics.mean(times):.2f} " f"max={max(times):.2f}") errors = [r for r in results if r.error] if errors: print(f"\n{len(errors)} error(s):") for r in errors: print(f" {r.family} ({r.method} refine={r.refine}): {r.error}") def save_results(results: List[AlignmentResult], path: str) -> None: """Save results as JSON.""" data = { "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "results": [r.to_dict() for r in results], "summary": {}, } by_group = {} for r in results: if r.error: continue ens = f"_ensemble={r.ensemble}" if r.ensemble else "" key = f"{r.method}_refine={r.refine}{ens}" by_group.setdefault(key, []).append(r) for group, group_results in by_group.items(): scores = [r.sp_score for r in group_results] recalls = [r.recall for r in group_results] precisions = [r.precision for r in group_results] f1s = [r.f1 for r in group_results] tcs = [r.tc for r in group_results] data["summary"][group] = { "n_cases": len(group_results), "sp_mean": statistics.mean(scores), "sp_median": statistics.median(scores), "sp_min": min(scores), "sp_max": max(scores), "recall_mean": statistics.mean(recalls), "precision_mean": statistics.mean(precisions), "f1_mean": statistics.mean(f1s), "tc_mean": statistics.mean(tcs), "total_time": sum(r.wall_time for r in group_results), } Path(path).parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as f: json.dump(data, f, indent=2) print(f"\nResults saved to {path}") def main() -> None: parser = argparse.ArgumentParser( description="Kalign alignment benchmark suite", prog="python -m benchmarks", ) parser.add_argument( "--dataset", default="balibase", choices=list(DATASETS.keys()) + ["all"], help="Which dataset to benchmark (default: balibase)", ) parser.add_argument( "--method", nargs="+", default=["python_api"], choices=["python_api", "cli", "clustalo", "mafft", "muscle"], help="Alignment method(s) to test (default: python_api)", ) parser.add_argument( "--max-cases", type=int, default=0, help="Limit number of test cases (0 = all)", ) parser.add_argument( "--binary", default="build/src/kalign", help="Path to C-compiled kalign binary for CLI method (default: build/src/kalign)", ) parser.add_argument( "--refine", nargs="+", default=["none"], choices=["none", "all", "confident"], help="Refinement mode(s) to test (default: none)", ) parser.add_argument( "--threads", type=int, default=1, help="Number of threads per alignment (default: 1)", ) parser.add_argument( "--output", default="", help="Output JSON file for results", ) parser.add_argument( "--adaptive-budget", action="store_true", help="Scale trial count by uncertainty", ) parser.add_argument( "--ensemble", type=int, default=0, help="Number of ensemble runs (0 = off)", ) parser.add_argument( "-j", "--parallel", type=int, default=1, help="Number of parallel workers for benchmark cases (default: 1)", ) parser.add_argument( "--download-only", action="store_true", help="Only download datasets, don't run benchmarks", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Verbose output", ) args = parser.parse_args() if args.download_only: download_dataset(args.dataset) print("Download complete.") return results = run_benchmark( dataset=args.dataset, methods=args.method, refine_modes=args.refine, max_cases=args.max_cases, binary=args.binary, n_threads=args.threads, verbose=args.verbose, adaptive_budget=args.adaptive_budget, ensemble=args.ensemble, parallel=args.parallel, ) if results: print_summary(results) if args.output: save_results(results, args.output) kalign-3.5.1/benchmarks/scoring.py000066400000000000000000000201311515023132300171100ustar00rootroot00000000000000"""Alignment + scoring wrappers for benchmark runs.""" import subprocess import tempfile import time import xml.etree.ElementTree as ET from dataclasses import asdict, dataclass from pathlib import Path from typing import List, Optional import kalign from .datasets import BenchmarkCase @dataclass class AlignmentResult: """Result of aligning and scoring one benchmark case.""" family: str dataset: str method: str sp_score: float wall_time: float seq_type: str refine: str = "none" ensemble: int = 0 recall: float = 0.0 precision: float = 0.0 f1: float = 0.0 tc: float = 0.0 error: Optional[str] = None def to_dict(self) -> dict: return asdict(self) def align_with_python_api( case: BenchmarkCase, output: Path, n_threads: int = 1, refine: str = "none", adaptive_budget: bool = False, ensemble: int = 0, ) -> float: """Align using kalign Python API. Returns wall time in seconds.""" start = time.perf_counter() kalign.align_file_to_file( str(case.unaligned), str(output), format="fasta", seq_type=case.seq_type, n_threads=n_threads, refine=refine, adaptive_budget=adaptive_budget, ensemble=ensemble, ) return time.perf_counter() - start def align_with_cli( case: BenchmarkCase, output: Path, binary: str = "kalign", n_threads: int = 1, refine: str = "none", adaptive_budget: bool = False, ensemble: int = 0, ) -> float: """Align using kalign C binary via subprocess. Returns wall time in seconds.""" cmd = [binary, "-i", str(case.unaligned), "-f", "fasta", "-o", str(output)] if n_threads > 1: cmd.extend(["--nthreads", str(n_threads)]) if refine != "none": cmd.extend(["--refine", refine]) if adaptive_budget: cmd.append("--adaptive-budget") if ensemble > 0: cmd.extend(["--ensemble", str(ensemble)]) start = time.perf_counter() result = subprocess.run(cmd, capture_output=True, text=True) elapsed = time.perf_counter() - start if result.returncode != 0: raise RuntimeError( f"kalign CLI failed (exit {result.returncode}): {result.stderr}" ) return elapsed EXTERNAL_TOOLS = {"clustalo", "mafft", "muscle"} def align_with_external( case: BenchmarkCase, output: Path, tool: str, n_threads: int = 1, ) -> float: """Align using an external MSA tool. Returns wall time in seconds.""" if tool == "clustalo": cmd = ["clustalo", "-i", str(case.unaligned), "-o", str(output), "--outfmt=fasta", "--force"] if n_threads > 1: cmd.extend(["--threads", str(n_threads)]) elif tool == "mafft": cmd = ["mafft", "--auto"] if n_threads > 1: cmd.extend(["--thread", str(n_threads)]) cmd.append(str(case.unaligned)) # MAFFT writes to stdout start = time.perf_counter() with open(output, "w") as f: result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True) elapsed = time.perf_counter() - start if result.returncode != 0: raise RuntimeError( f"mafft failed (exit {result.returncode}): {result.stderr}" ) return elapsed elif tool == "muscle": cmd = ["muscle", "-align", str(case.unaligned), "-output", str(output)] if n_threads > 1: cmd.extend(["--threads", str(n_threads)]) else: raise ValueError(f"Unknown external tool: {tool}") start = time.perf_counter() result = subprocess.run(cmd, capture_output=True, text=True) elapsed = time.perf_counter() - start if result.returncode != 0: raise RuntimeError( f"{tool} failed (exit {result.returncode}): {result.stderr}" ) return elapsed def score_alignment(reference: Path, test_output: Path) -> float: """Score a test alignment against a reference. Returns SP score (0-100).""" return kalign.compare(str(reference), str(test_output)) def parse_balibase_xml(xml_path: Path) -> List[int]: """Parse BAliBASE XML annotation to extract core block column mask. The XML ``/`` element contains per-column integers: ``-1`` (flanking), ``0`` (non-core), ``1`` (core block). Returns a binary mask where only core columns (value == 1) are set. """ tree = ET.parse(xml_path) root = tree.getroot() colsco = root.find(".//column-score/colsco-data") if colsco is None or colsco.text is None: raise ValueError(f"No element found in {xml_path}") values = [int(v) for v in colsco.text.split()] return [1 if v == 1 else 0 for v in values] def _fasta_ref_has_gaps(reference: Path) -> bool: """Check if a FASTA reference alignment contains any gap characters.""" with open(reference) as f: for line in f: if not line.startswith('>') and '-' in line: return True return False def score_alignment_detailed(reference: Path, test_output: Path) -> dict: """Score a test alignment with POAR metrics. If a BAliBASE XML annotation file exists alongside the reference (same name with .xml extension), the XML core block mask is used. Otherwise scores all columns (max_gap_frac=-1.0), appropriate for BRAliBASE and other benchmarks with hand-curated references. Gapless FASTA reference alignments (e.g. conserved RNA with no indels) are skipped — the C comparison code requires alnlen > 0. """ xml_path = reference.with_suffix('.xml') if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed(str(reference), str(test_output), column_mask=mask) # For non-BAliBASE references (FASTA format), check for gapless edge case if reference.suffix in ('.fa', '.fasta') and not _fasta_ref_has_gaps(reference): raise RuntimeError("Reference alignment has no gaps — skipping (trivially aligned)") return kalign.compare_detailed(str(reference), str(test_output), max_gap_frac=-1.0) def run_case( case: BenchmarkCase, method: str = "python_api", binary: str = "kalign", n_threads: int = 1, refine: str = "none", adaptive_budget: bool = False, ensemble: int = 0, ) -> AlignmentResult: """Run alignment + scoring for a single benchmark case.""" with tempfile.TemporaryDirectory() as tmpdir: output = Path(tmpdir) / f"{case.family}_aln.fasta" try: if method == "python_api": wall_time = align_with_python_api(case, output, n_threads, refine, adaptive_budget, ensemble) elif method == "cli": wall_time = align_with_cli(case, output, binary, n_threads, refine, adaptive_budget, ensemble) elif method in EXTERNAL_TOOLS: wall_time = align_with_external(case, output, method, n_threads) else: raise ValueError(f"Unknown method: {method}") sp_score = score_alignment(case.reference, output) detailed = score_alignment_detailed(case.reference, output) is_external = method in EXTERNAL_TOOLS return AlignmentResult( family=case.family, dataset=case.dataset, method=method, sp_score=sp_score, wall_time=wall_time, seq_type=case.seq_type, refine="n/a" if is_external else refine, ensemble=0 if is_external else ensemble, recall=detailed["recall"], precision=detailed["precision"], f1=detailed["f1"], tc=detailed["tc"], ) except Exception as e: is_external = method in EXTERNAL_TOOLS return AlignmentResult( family=case.family, dataset=case.dataset, method=method, sp_score=0.0, wall_time=0.0, seq_type=case.seq_type, refine="n/a" if is_external else refine, ensemble=0 if is_external else ensemble, error=str(e), ) kalign-3.5.1/benchmarks/vsm_ensemble_experiment.py000066400000000000000000000227221515023132300223730ustar00rootroot00000000000000"""Comprehensive BAliBASE benchmark comparing all kalign modes. Modes: 1. baseline - no VSM, no refinement 2. +vsm - VSM only (vsm_amax=2.0) 3. +vsm+ref - VSM + refinement 4. ens3 - ensemble(3), no VSM, no refinement in runs 5. ens3+vsm - ensemble(3), VSM in each run 6. ens3+vsm+ref - ensemble(3), VSM + refinement in each run Usage: uv run python -m benchmarks.vsm_ensemble_experiment uv run python -m benchmarks.vsm_ensemble_experiment --max-cases 10 uv run python -m benchmarks.vsm_ensemble_experiment --categories RV11 """ import argparse import json import statistics import tempfile import time from collections import defaultdict from pathlib import Path import kalign from .datasets import get_cases from .scoring import parse_balibase_xml def _read_fasta_seqs(path): seqs = [] name = None buf = [] with open(path) as f: for line in f: line = line.strip() if not line: continue if line.startswith(">"): if name is not None: seqs.append((name, "".join(buf))) name = line[1:].split()[0] buf = [] else: buf.append(line) if name is not None: seqs.append((name, "".join(buf))) return seqs def _alignment_stats(path): seqs = _read_fasta_seqs(path) if not seqs: return {} alnlen = len(seqs[0][1]) nseq = len(seqs) total_chars = 0 total_gaps = 0 total_gap_opens = 0 for _, s in seqs: in_gap = False for ch in s: if ch == "-": total_gaps += 1 if not in_gap: total_gap_opens += 1 in_gap = True else: total_chars += 1 in_gap = False gap_frac = total_gaps / (nseq * alnlen) if alnlen > 0 else 0 mean_seqlen = total_chars / nseq if nseq > 0 else 0 alnlen_ratio = alnlen / mean_seqlen if mean_seqlen > 0 else 0 return { "alnlen": alnlen, "nseq": nseq, "gap_frac": gap_frac, "gap_opens_per_seq": total_gap_opens / nseq if nseq > 0 else 0, "alnlen_ratio": alnlen_ratio, "mean_seqlen": mean_seqlen, } def _score_case(case, output_path): xml_path = case.reference.with_suffix(".xml") if xml_path.exists(): mask = parse_balibase_xml(xml_path) return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) return kalign.compare_detailed(str(case.reference), str(output_path)) CONFIGS = [ { "label": "baseline", "refine": "none", "vsm_amax": 0.0, # explicitly disable VSM }, { "label": "+vsm", "refine": "none", "vsm_amax": 2.0, }, { "label": "+vsm+ref", "refine": "confident", "vsm_amax": 2.0, }, { "label": "+vsm+iref", "refine": "inline", "vsm_amax": 2.0, }, { "label": "ens3", "ensemble": 3, "refine": "none", "vsm_amax": 0.0, # no VSM in individual runs }, { "label": "ens3+vsm", "ensemble": 3, "refine": "none", "vsm_amax": 2.0, }, { "label": "ens3+vsm+ref", "ensemble": 3, "refine": "confident", "vsm_amax": 2.0, }, { "label": "ens3+vsm+ref+ra1", "ensemble": 3, "refine": "confident", "vsm_amax": 2.0, "realign": 1, }, { "label": "ens3+vsm+ref+ra2", "ensemble": 3, "refine": "confident", "vsm_amax": 2.0, "realign": 2, }, { "label": "ens3+vsm+iref", "ensemble": 3, "refine": "inline", "vsm_amax": 2.0, }, { "label": "ens3+vsm+iref+ra1", "ensemble": 3, "refine": "inline", "vsm_amax": 2.0, "realign": 1, }, ] def _run_one(case, config): label = config["label"] with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: tmp_path = tmp.name try: t0 = time.perf_counter() kwargs = dict(format="fasta", seq_type=case.seq_type) # Alignment mode if config.get("ensemble"): kwargs["ensemble"] = config["ensemble"] kwargs["refine"] = config.get("refine", "none") kwargs["vsm_amax"] = config.get("vsm_amax", -1.0) if config.get("realign"): kwargs["realign"] = config["realign"] else: # Standard kalign kwargs["refine"] = config.get("refine", "none") kwargs["vsm_amax"] = config.get("vsm_amax", -1.0) kalign.align_file_to_file(str(case.unaligned), tmp_path, **kwargs) wall = time.perf_counter() - t0 scores = _score_case(case, tmp_path) stats = _alignment_stats(tmp_path) return { "family": case.family, "dataset": case.dataset, "method": label, "recall": scores["recall"], "precision": scores["precision"], "f1": scores["f1"], "tc": scores["tc"], "wall_time": wall, **stats, } except Exception as e: return { "family": case.family, "dataset": case.dataset, "method": label, "recall": 0, "precision": 0, "f1": 0, "tc": 0, "wall_time": 0, "error": str(e), } finally: Path(tmp_path).unlink(missing_ok=True) def _print_summary(results, method_names): print(f"\n{'Method':>20} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}" f" {'GapFrac':>8} {'AlnRatio':>9} {'Time':>7}") print("-" * 90) groups = defaultdict(list) for r in results: groups[r["method"]].append(r) for method in method_names: entries = groups.get(method, []) valid = [r for r in entries if "error" not in r] errs = len(entries) - len(valid) if not valid: print(f"{method:>20} (no results)") continue rec = statistics.mean(r["recall"] for r in valid) prec = statistics.mean(r["precision"] for r in valid) f1 = statistics.mean(r["f1"] for r in valid) tc = statistics.mean(r["tc"] for r in valid) gf = statistics.mean(r.get("gap_frac", 0) for r in valid) ar = statistics.mean(r.get("alnlen_ratio", 0) for r in valid) wt = sum(r.get("wall_time", 0) for r in valid) suffix = f" ({errs} err)" if errs else "" print(f"{method:>20} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}" f" {gf:>8.3f} {ar:>9.2f} {wt:>6.1f}s{suffix}") def _print_per_category(results, method_names): all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) for cat in all_cats: cat_results = [r for r in results if cat in r["dataset"]] cat_groups = defaultdict(list) for r in cat_results: cat_groups[r["method"]].append(r) n = len(cat_groups.get(method_names[0], [])) print(f"\n=== {cat} ({n} cases) ===") print(f"{'Method':>20} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}") print("-" * 55) for method in method_names: entries = [r for r in cat_groups.get(method, []) if "error" not in r] if not entries: continue rec = statistics.mean(r["recall"] for r in entries) prec = statistics.mean(r["precision"] for r in entries) f1 = statistics.mean(r["f1"] for r in entries) tc = statistics.mean(r["tc"] for r in entries) print(f"{method:>20} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}") def main(): parser = argparse.ArgumentParser(description="Comprehensive BAliBASE benchmark") parser.add_argument("--max-cases", type=int, default=0) parser.add_argument("--categories", nargs="*", default=None) parser.add_argument("--configs", nargs="*", default=None, help="Only run specific configs by label") args = parser.parse_args() cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) if args.categories: cats = [c.upper() for c in args.categories] cases = [c for c in cases if any(cat in c.dataset.upper() for cat in cats)] configs = CONFIGS if args.configs: configs = [c for c in CONFIGS if c["label"] in args.configs] print(f"{len(cases)} BAliBASE cases, {len(configs)} configs", flush=True) method_names = [c["label"] for c in configs] n_tasks = len(configs) * len(cases) print(f"{n_tasks} total tasks (sequential)", flush=True) t0 = time.perf_counter() results = [] done = 0 for cfg in configs: print(f"\n Config: {cfg['label']}", flush=True) for case in cases: r = _run_one(case, cfg) results.append(r) done += 1 if done % 20 == 0: elapsed = time.perf_counter() - t0 eta = elapsed / done * (n_tasks - done) print(f" {done}/{n_tasks} ({elapsed:.0f}s, ETA {eta:.0f}s)", flush=True) elapsed = time.perf_counter() - t0 print(f"\nAll done in {elapsed:.0f}s") _print_summary(results, method_names) _print_per_category(results, method_names) # Save out = Path("benchmarks/data/vsm_ensemble_experiment.json") out.parent.mkdir(parents=True, exist_ok=True) with open(out, "w") as f: json.dump(results, f, indent=2) print(f"\nSaved to {out}") if __name__ == "__main__": main() kalign-3.5.1/build.zig000066400000000000000000000073311515023132300145760ustar00rootroot00000000000000const std = @import("std"); const builtin = @import("builtin"); const ArrayList = std.ArrayList; const kalignPackageVersion = "3.5.1"; const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .macos }, .{ .cpu_arch = .aarch64, .os_tag = .linux }, .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu }, .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }, // .{ .cpu_arch = .x86_64, .os_tag = .windows }, }; const cflags = [_][]const u8{ "-DKALIGN_PACKAGE_VERSION=\"3.5.0\"", "-DKALIGN_PACKAGE_NAME=\"kalign\"", "-DKALIGN_ALN_SERIAL_THRESHOLD=250", "-DKALIGN_KMEANS_UPGMA_THRESHOLD=50", }; pub fn build(b: *std.Build) !void { // const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); for (targets) |t| { const lib = b.addStaticLibrary(.{ .name = "tldevel", .target = b.resolveTargetQuery(t), .optimize = optimize, }); lib.linkLibC(); lib.addIncludePath(.{ .path = "./lib/src" }); lib.addCSourceFiles(.{ .files = &kalign_lib_sources, .flags = &cflags }); lib.addIncludePath(.{ .path = "./lib/include" }); b.installArtifact(lib); const kalign_bin = b.addExecutable(.{ .name = "kalign", .target = b.resolveTargetQuery(t), .optimize = optimize, }); lib.addCSourceFiles(.{ .files = &kalign_sources, .flags = &cflags }); kalign_bin.addIncludePath(.{ .path = "./lib/src" }); kalign_bin.addIncludePath(.{ .path = "./lib/include" }); kalign_bin.linkLibrary(lib); b.installArtifact(kalign_bin); const target_output = b.addInstallArtifact(kalign_bin, .{ .dest_dir = .{ .override = .{ .custom = try t.zigTriple(b.allocator), }, }, }); b.getInstallStep().dependOn(&target_output.step); } // const exe = b.addExecutable(.{ // .name = "zig_test", // .target = target, // .optimize = optimize, // }); // exe.addCSourceFile(.{ .file = .{ .path = "./tests/zig_test.c" }, .flags = &[_][]const u8{"-std=c99"} }); // // exe.addCSourceFiles(.{ .files = &kalign_lib_sources, .flags = &[_][]const u8{"-std=c99"} }); // // exe.addCSourceFile(&.{"./tests/zig_test.c"}, cflags.items); // // exe.addCSourceFile("./lib/src/strnlen_compat.c", cflags.items); // exe.addIncludePath(.{ .path = "./lib/src" }); // // exe.linkLibrary(lib); // exe.linkLibC(); // b.installArtifact(exe); } const kalign_lib_sources = [_][]const u8{ // "lib/src/strnlen_compat.c", "lib/src/test.c", "lib/src/tldevel.c", "lib/src/tlmisc.c", "lib/src/tlrng.c", "lib/src/esl_stopwatch.c", "lib/src/msa_alloc.c", "lib/src/msa_op.c", "lib/src/msa_io.c", "lib/src/msa_misc.c", "lib/src/msa_check.c", "lib/src/msa_cmp.c", "lib/src/msa_sort.c", "lib/src/alphabet.c", "lib/src/task.c", "lib/src/bisectingKmeans.c", "lib/src/sequence_distance.c", "lib/src/bpm.c", "lib/src/euclidean_dist.c", "lib/src/pick_anchor.c", "lib/src/aln_wrap.c", "lib/src/aln_apair_dist.c", "lib/src/aln_param.c", "lib/src/aln_run.c", "lib/src/aln_mem.c", "lib/src/aln_setup.c", "lib/src/aln_controller.c", "lib/src/aln_seqseq.c", "lib/src/aln_seqprofile.c", "lib/src/aln_profileprofile.c", "lib/src/aln_refine.c", "lib/src/sp_score.c", "lib/src/weave_alignment.c", "lib/src/poar.c", "lib/src/consensus_msa.c", "lib/src/anchor_consistency.c", "lib/src/ensemble.c", }; const kalign_sources = [_][]const u8{ "./src/run_kalign.c", "./src/parameters.c", }; kalign-3.5.1/doc/000077500000000000000000000000001515023132300135255ustar00rootroot00000000000000kalign-3.5.1/doc/images/000077500000000000000000000000001515023132300147725ustar00rootroot00000000000000kalign-3.5.1/doc/images/Balibase_scores.jpeg000066400000000000000000005200031515023132300207210ustar00rootroot00000000000000JFIF,,C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222M" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( j~kJp_6e2z 5ngo&ס*x p F00wv0^[GskOC|MՂ%ſi|Gk!զ$#h72l'6"V8,/IzBE}3+b[Zۈ|WkHw}?޽ѢwuxwMЯ4f9I2 H+}?޽ѢKE-jj7Z]F_69eh]Tp~xo5u+R g$OIï&t?g W ^\ uӭxa!T@:rv-V@MCz~0%kf= 󃜑}^IhP mY̍g+?L?@_9|cI p̨%Qvvӧny{>h$KXN}N3YG| icM0(c@[7>6Yn.$QMU,BTr_ oUs 1O duzƯi:L[@Oo^1X_!da㿯}!oZx[×Lڽ]¨$϶^ NCOx=vq װ|\Я|CSӣind0$Sw W%;Eo떓%)׌uq{ᗕjݑpTDu|˟^>x'uxAϗ"GA|M?zdA,ۺI'| v DJfhČr',IW<E%ίt ʊ:v+OmVՅ|@}2'8#z񎯦j_x/GHd܌Gݪ !F|lv?J[|C]1}c'8f^pI3#Lԭ5.RKks'ԏWY/xV2$֋I  ̀Jg/>;aOHWnhթ-#}>xo5u+R g$OI?8i˫x[}6.}= ~ ]:٧O M|>!G|w/=ُB}<$q_DׁZGhs#Y}Ad`'y]pHk]#1Ϯy?ƿuk3n@+?*>/7YAҨmR.f|W!}j+mN@!@]?a+7@ߌޯϊX\gduꎽ=0Le]i^ )|keM >/hKK8 2`T~!~M@4~G?ٖ>-x큅`"(U?}g{kmyԋ&%@8ǯxuaj$b PE/a׃Q] anܔ= `Aψ_ Qgb⹓#7G'\ +i_'Rm\}{soh@ø# |hYI=b軦 * .?Z_ ȡׄ-k篇0^zN{{2pvPᏍͳje^GPQb}kŸjw!R ڙY`qCz94kڅeZ6T2xf7&Z^Me wmlz֢c3>8^,k,78207$c_ƱqA qmnst-ioEPI?dv=DqT(i%< {+U-&L mhIR0FGJ/,QӾIhɯ|+Ϩ}x"89?#߃oou "xv6)ڄKZdd p1_ ¸];o<)j_t#s<#bt wo}_ +67c߷jÿit"=.f|C“9~qvj5e[d̏O_G-4-HFެ}>4i-!@/ڤ6~<6XNcW5~[{^O4OKx2˵x=6j^jh6$1Џ€6i=Vm83rEA*POԹmϠ/t݇] \ LF=k𯀼S3_4:pegfQȎ4&(Jg<%TY!-:(nVO?Uy< ~-k1*"C.'&)PrT{V=dղ 6c:[B1x-N59J܌@'3WuB|6d߳^Uᮽo$8 7/ge[մ2O<|Q)fv(@I1[? Et]XU/,dY">m_ ֯/c!ڂզϡ;~T xw[/ImB-=n"_iF0r157𕯍|-u\χlgʐ}~@N} Ev`0Vi7"B:PIǿVwA˖Q"BOzg?4V460VYrp@/w~uya( _vOO+/W#u_gu'O[λ: 'y߇uU]P:oڣo Wp یր=_ szt=_ZҴd.̈8?zO? 5} { ,vPH@ 5_<;|wPk—2ںF޴ |e'X/jNjjM\jN ,ߌc|$|O[WĒapY䜐xkپ#xQg"Tm۳tyx/~!x4e.O&F=v2^ {K+r͙9 qn}~rk>4oO\x16ǟ1Zx'h[JOM*qG@F CV5.4o-FX2ȇ{#{O$uFCIe|Uvk,.n.<GH8vছ}|6llRap pp|a"N`ׅ߳4cR5|׌[#͸r6׮1*[kpA4FYlu$זzk˪w ,ջ\3P-w^mt=Eq81s`Q^Z¶žw."T/UhV^%o4}A7[]FQOPce^?/4MRFȚIA&Vc">(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((O/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTP#}~~luje*c?/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_4-wU΀!Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 0́WLL>Pi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK 0@U?~!Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q $je*i>Pi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK ,qRsǪ~e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPfUf>n[_U(((((((((((+:_ΗkhQEQEQEQEQEQEQEQEQEQEQE[?ZVx֪PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPV3sq@EG\<$UjJ+:_ΗkhQEQEQEQEQEQEQEQEQEQEQE[?ZVx֪PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPV'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%H Fا ge H<Z|w#f$o]/hI?$tϴKG%O:`2ybXww?hK7a9{ލhﲹp2~G]I?$tϴKG%I?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g1۟پW|yjc%-*R;s-נIG$g}_>/QIG$g}_>/?$C2X=763G%Gr;e W6.ʪWv y_ITeԧg||Lc'?Nbcm_~~ljI?h援K@kvE,̊I1UY6~ p`oXI yEjޛcxTp7C$t}O:g%|GhIG$g}_>/?I?h援K@s\nidι=xw'?uh淯G4{R!dΏILD4}_dΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$uxY46-8;{i "qሌJZ߈dΏILD4}_icl#'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?w F*%.-U9~'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IL'%CsQkv5j'>VΚ=UI?h援KVs$t}O:g%|'?dΙhD4IG$g%1Ǟ u'%#!]7`I?$tϴKG%"fTAI?iXVD4IG$g}_׵ Mx&)*(*9⹚Fuj*tQqI?$umRGnI<8KNqq&hץ?I?h援KRl?I?h援K@$t}O:g%]7^է7`i R\B$nO:>'?3|k2GO:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:HTUD5fWeVI?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4468UN iY \((((((/ak D v:}I),JtPtcUCWbec)*ʂzVBԙne2rĖ6rs֯Q@yQ&q dO+k~PkZu 3]M k-V 9I4V%נ}>6 Z& W++xG JtUTQ@gK5Y2(((((((((Si]WC&'+zZFU D/ -?Tg<U(:/W KCF8Fu]_:A]Uk+8*~QEQ@r֟8I#]Mr֟8I#]OYu4QEqQ@Q@Q@Q@Q@Q@Q@WY\WYX^˯𠢊+c ( sBsB/?4Bm?A?5O^ m#?Tm#?X(?}\[V?q]+c\qoXUw?^[F΢(c ( (9k~O]Mr7Mֺ|q 7 ((((() $9$Y:P8WCoJQ!V?xF>?w=j$ZW6 xޙi:uTZY;0I?i]u;Z+/-+~i_+Ois5(,J#"I=RPQEC_r-u6o\W-㯹c?kVB5Q^e!RyVo?{\iVo?{\]?zOF_ 7|kgU?#*rQEbzEP\#]:k?k~O]O/^SEWQEQEQEQEQEQEQEln<}]Urv:+?S(Š(*~_)gnN-qږk5{[i>~G%75ڟsoO?N$>[{V%75*Wv0c{E#Km?oh-k8?tR#Eu9VxEuxEy_& i )Ym!Vk AV q|oj7[l:"f=*8-JmmoTӺb9j? ֗?v?S/#ԩUM$#ğjθe9ˉNz][%vGO4a~_ vR}uw—7Rnl0i`F{c\)i\jjo9uQEpEPEPEPEPEP'm# ;o[Mu f??(Š(}-v5xX}$k3N=bp*XʥB? (*ܟAU*ܟATJ(2G"s 9?%Ƚ{\Ok?QE{EPEP-uBm#F{u5ێ"F%QEqQ@Q@Q@Q@Q@Q@rv_9KY\R'5mzYQEQ@Q@gYһԧuvOEyx_B(BrǢ}TrǢ}T|M"BY>&r _}NloOȧҺ*| ]WEU_2? +#(+?c?+[D/eu4QEqQ@Q@Q@Q@Q@Q@Q@#}WY\#}WYXcHQ[xQEWqָ՟y_NtLsǪ~RPJ(1U"1U"F*n }WJw~FtQEsQEQExC?+[q!]MvF(0 ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((f4 ^3-Ό=lk&㪢*p((zZFU qѿt:gl"} e(Bg<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEP\S\noGizMQ\FEPEPEPEPEPW?766Mf3oV??HpqUk:/M/Q>Ek(bibM/U(`?M/W-+/b]q;Y?ZʼR;곖&)fy\T\MhZdt7*7I1XK1z͜3:sB}&cx^Y$sWcUJ۷$ )YFVZ TG87I1]p)9esgʙ MZc@]PB7Z9]nq$5--:-0I$諿fxG| E]d3;>:tD_fxG| >-9c؋G٠c^ru }u5ZkOwOEWz!EP\_]q~6Ov_3|?QE`QEQEQEx&U=A{7ɿ'PO7'y?uᣳ2zAEVQEk"J Q_ǝl?Y?P`^.aX'v+ҏI+8B(>q@,H5^lq` d־ug)7wq)Ra8tV;.]-r 8Ȫ|8]`B'}eKEE?У0h\ܯ-`B=Y)i\jk&hQՇtuى/O!EWQEQEQEQP;Gcpp}+WXId F@o2Vk)i%sUa)\-E?RSOȂGV}]er ?0+((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ("YoXXjعYoXXjŏ(3+c ( w,>W㿹aek o>0b9soѤAC5ؚO,?¸+_m~^^v]Nu\p5Kԣ2(ez?_pY4Y:ؚO,?¯QG #à^i* y*A_ ^O뉯2EH?O+#֚StX״KG%{G^&z4DQEIQE#fo?֮tib$.R=+)lֿN/5HKeBN5Ӎ9Sj\<9gK M?6I1X܋ ?=JgkOmi>~b#r/(rP?ZϤߘXX"RR#r/+?Pt#K٭Rw.i)^/S+<(((* Mz)mn`Bp?R(OVy'uro3'{ z?O@W>=?;I$Hi{ ?!W9iJ+KR3|T?jxoB&W.[`[C N0sޝ[^*{ !Q ?_ ¬#r_hGJ_j>g/_[As,ar[i9KVҿ/4#r_kZ0_4*Sww+ACoGiȨ#s *DtK szm[ 9qQ՗,”ݕζ(8 ( ( ( *?jUԿy\A44H]]r?uuᣧ0y( (8՟:Ŗl`Fa3M}YVeAh%򎠃ƼejII2&7(}&uzO,zO,_?:yuhx#+q~b='±S hP]"PG;UV8t7Qh۰2iP4wPU'iV)k#*vj׮-IU vN+'.^?!ZՓo/?_}N|oOȧҺ*| ]WEU_2? ZzM#`E]>!#J.kCqʢ)xj!=Z~ +vBckB:*Vd8C//zx*Ah+C2bǨ2bǫ 8ѵkh^{ ݏ]s,c$ r{:Ϛ1l(9Š(((J2p>ikw)UmtsYN+vanJ)w;*+7?M?/&OS!o~J+_ 1,$ަ7Μ+:TiVjUGMEs?^&T/t](K3ޭkWa 3|;Z(<+g1]q^8?3I|2')iAK^aVT*[=S QEcEϢb7EVp8ws*6ye1Zҩݳl>7 R8ٿ$d\ 7ixc2mgk޷b@+Q0U)skC=k…LljXe3>Rn\sK, ?X1 k??_o&ٷg!^f,Xcc V4¿տ絟M6> 2X2P.qW?7V+ )`TeC;ٞfoԿoV=m!K{B"VcϵUY|.mѬ|#8?+eʾ }SƅroX Uk/?bje|7Z*?[/OT9wXY Q Uk/l?Q * \=w3^X(yF  o>A\^k-sQ߿Ɋxc#8?V%o#oϛ~g袊P(+~Z3봮/i\ܿo": ((((ʧ/7*2 !O7'4vf_S~H( (8u-dXA?]kk"J Q?Q4QE{GQEVG?YVdxEEa'?+Yܿ UEյ$P]fέȭg_r)^amm:I,Fc$zʪƞ_K_Cq8ؼuEFV~Glj4lj5_h_o- M5_8Glj4lj5_h_o- M4}n;~/?Zݤwb5h_o$^vN6iօK _c(:亿< Etx+Asckyk[(2 (8R=? Z?ާ+((( wFou\_/z2,OBOTuQElyEP\_XһJw#l?ޣ'4@򢝣`(UE}g:ɦ1=3[WU]&lyMdo~"r>69M"<9ݫWT¹_ V8Qt+_˽|ZݽոǥI (eG+t,F9+{o~"s_Y^Je `Ax iz\O(yzĽb|3Iy+.V24kCٽyyԟj'l?k\S U?k\S ]osԧ/@)QEy=5&v <*Cb_o? ׭_^?Q^IQ@q^/ݟs_k\W7g\B5ۗE`QEQEQEWRUpժ*?iKfiKG!GHö ƣ>U[B֠_y>[mǗ<}Ej?"e\0U}ݏ<_ ƣ>TqϽU\ Dߟʏ@O쪹kLe?N5[G} c'T?"eG-pg{Vn5"{ `:IWc5XSwfc5>%'ks{J|]7NoZ( ( Dַ+*CZ -lvDM1o-qkN? HQ#7OX._떟s_]8 Kt|pFk;nOG&)xCFc]Uk G>>:IjR7$M$iK?)z-}j},u#O޳jv72ʩd;?S^^Ma!MgI0G- 9=ex3@] kl9I̅oM4u|/K*ҔciMʹ4Ui_*f~ƧcM4u|/G&ogV?6 cTVmgYUUp{zl\\iӌ.j1L(0 ( ( () Ms]:n*o\o+/)+c ( o++g1^fq/D- )k,*ꟅTsǪ~R( ȷy_ UȾѪNJ[/*__ҿ߯xQ\ǰg)F527s N? sT+ #~ڋ5,?/Qa>6­Q\}yreߥ ?,?/U(ϹW2|mq!+Z$1$k3һ|O#=F023z Q\8QEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@Q@Q@rs?_WY\;@ozv?C+c ( (9Oǥto] ?{i]WC&'+ˡW&Z+,R?T?wM^:/U??G~uu| uU'?ܩ~EVQEZ'u5Z'v>fvE`QEQEQEQEQEEsʱoձsʱoՌPsngcEVǞQEX}dk?r'X;|&bZkE K_m~^\9f/P+QEO뉯2EH?O+|I"q5SȩI%qR?s9~]@*R a^"|&z4DQEIQE#fo?Y_+_5yf>A^_y|;#t7=Q^IQ@q~6Ov?-?uۗ|GAEWQEQEQETq\Tx&TCF_?ֺ< t?/QZ!EPζe(0}Gu-dXA?]xa8دJ>=&(h ( G7׬ȳ\1_ \"Z EtQ\AEP? Z?g!KOWC]R"QEqQ@Q@Q@u\_/z֍#Y[_ʼnQrꎶ(<(+_Wi\_Xұ g~34(,ףלiQcyYG7tw~EWPQE6gNWE{+ͯ?YӿCF O#;sGX:b:b^O^ER,({Ok_M| y+\U5&v <ZQd2::( ( _!?ָnk/7ɛ3+(((* UWRUpҖҗ#^?uux_ȻIT 졗L !nJ)B3b1.:ߖ7v=+]]6?y~[(?y~[+AϵI@~_(@~_+Ϳ5ߖ ?5ߖ >Gy_ Mt?¸-2EWUv`k ->*[VzVԩB<>;PUkKM;-P`3Z0sG?k?t +vB+ipIiw>"QHE?ƏE?ƽReZ?k?k?z5jVOt;P;`7q]s,Tu!Nob'%Š(< ( ( () Ms]:n*o\o+/)+c ( *gwnH3̒d?:+$/|F|U?MG#>*CM-GHc%Rc;nXHֺ Oyx$;.9$qRE[ϪkU<%"?տeB㽜[k1%iXբ+41U"1U"F*n }WJw~FtQEs?| 3][TZn~?տv7_~Q\FEP\Ogr?m\Ogr?nћ/QE`QEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š((Si]WC&'+zZFU D/ -?Tg<U(:/W KCF8Fu]_:A]Uk+8*~QEQ@r֟8I#]Mr֟8I#]OYu4QEqQ@Q@Q@Q@Q@\Ǭ75l\Ǭ75c?z?ۿEQ@q;Y?Z+ܰ5?ɘ>cB.G_(WYoK(QEexEk%8RUoqLWEk-\R~LΜq~6Ol[Y?ٮkڥsnH9\w 5 )EEPEPEPQFe1] ::vF9s[Y=$z+A-g,Rw"ߧ'Pj!7c2}8gJ쫓Gs>[YYцĪ|ob],IFI?冷y}qz5]I;K:q6J|DOU`?vu\^ŧxFm'cי4973x-$chc^O4oyz˹\'7<߿h~haÙw:zG7Fתߌ4?̕0ֲcң4gȭg_r)^&'+.VWe\AۂuCƷ"QZQEW#&Ыի̴jqH2ө,y<7'%q1 1Foni\"_G"_[UxZsrzA7JRniG?U+e=?iqd4g3Q >i'F)&z?Tni\"_G"_Ya?ʞ*m^dU0JzŏI";`[T5)7VQi( (B((( /gx=o?_kFA_ǭc/(TG[EVǞQEu+/X???əoXok4(,ףW:;P+((gW^ȳ+|Y"Bן,?J#x֬?k\S U?k\S ]sԧ/@)QEy=5&v <*Cb_o? ׭_^?Q^IQ@q^/ݟs_k\W7g\B5ۗE`QEQEQEWRUpժ*?iKfiKE/Ǎuʺ< 7_*:GNa?QEQEq>:?F?qkQOsuk'2HvqxYENqI3텡[- Mkcjp/{Dzkch_Ge/hW?ȫ{O k_P^xW{[2g=@QXΰ)J ;Nޠ&]ҿ+-?ʼE+Oҽr<kȾ`MEUQEWXSYRůY&\ы]-szb<+dLI!65樹fiG~OZTvH먮?G'=Zqm`WwEWo]o&Mf,3&>c&qG48ZF->:S[l<+nw׳\'=ZO#{aZKk??GA?| 3][W<|G^-Hrہ;@g CԿ=\=zuғEW(iڸi_7_#+(((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ("Yo\u]]}%o7n,=rGu@i$8BNJQwaqNtԭi_*Mu|/KϜtbi+V,?6?Ui_*go g|7cgo?4+٭ SR^m3x V]amJQm`= M8.?cjrY+6њ#+uėZo+r eNW:^FK :{>ιEy'Ziڪc}XZps=;#?:Gs'wZc_TK%]6G[ X|5?&˸c# 3XR/6h s@WſcAW"5_Uo\s%xiO,(=Ddp  3}#iQW )JQٙhK_ZTUb󿽏O8Ki|E-ǘ{to?ϯDo~O]Mu+T&]MNI>o#_FUt`dZ4W'+;ُvQEb@QEQEQEQEQEW' u#}X>گYQEPѴ ʯQ:pטXoH$hoH$kV?}VG 6O[Cky}$_ox{I{H]г"wN>㯹c?kΡ⬹z.$C#|o#|oZ+_r.|oioeiDFKMpOW?Wl͠Bбܬ `&TKU\ ץBTcұΥ\6:EJRRJ]TW?7m%ӈIaӝK5wBv=:>_j4ַ+l$ώpbz'g$(fTBm?#+tObz Q佾+dYjӄ*֭Q)EFWm+[L?u5i5''Sޗ/C+((((( e^ip9`9뻿uqkƇgAp>fc`Q\ԥ4)ӡ9UWWEIuIuºC4o}3JZV^'K]c_]c_+j? ҿ֣Vװ?#XW*w7sJgn@һC4o}>kIl b=15QFzXJi ^^qEfQ^AEP7?WpBE;'I^6gNW/</a _Ua _U{?zQH(?97+WqVֿܛC+@VjGL袊OD(+FZ_!?ܿ&oΆ(#(((?&U]KAWJ[3J_ď)x3Vם?~B*}SCZG@OoW&F<sGŦP+o??2|mOʟ]ϹF,ṴoGeߥ E.g~ʾeߥ ?,?/U(}CWy#?{Ҹ=7>ubyҽ\G_őȻfQ^IQ@q>'#]q>'#]FoGEEWQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( 򨑇Wk:_:?Wkʼ?oHD/<+Gj+,m*jW!R&xfxU>%</둫S ?$ƭܟV9?'o_AZőϕS&xrhO@OyxĶ|-/GAoş+?!^my"Ν_$XqfrFҀAȳ*p8uXJM<֬?k\S U?k\S ^i (EQ@Qfqk|g1< y+\U5&ZYB#6 mѕj.C炒_N_~Q ^? ~+2u|_=J_G%z/_2u|=rj%z/_wΥ{ Nԅ(gں0*jsɫR(J/9?'Z"o9?'ZOyo*s 1՟Pҿ xSNgz-Q^QEo~nV?U'όw [s"]'i^ip5Uw._떟s_Z?_C_w&*d(+ɴOZ9O^^Ma!MgIp[yX7w +vB Wt5/= (Š(+|xyޣ '7+ #xgQS{Q@Š(%.53Ů?տpzo|Ů"3>kpOҽlG_d_.QL} <B3Ѱ)t_?GOQf\Oyޣ '7+b; (QEo|ŦS?ы]mKINk {Wھõ908j+վg>߱Gl+?X)}mvfvE`QEQEQEQEQEQEQErgG]ergG]ecGza.‚(<(+/ k/ _94Lk\S UBm?zxuRxu`T(?\qoXUw?s@WſcAW"c z=oS :(`((?n?u5X7Zk(޾(0 ( ( ( ( ( ( 8w?kg0U :(<((ܱ]Mx5UxX_e+o_AZ髙7ftxʿܩQXQEW-sκߓnoKᗡE`QEQEQEQEQEWuqk;y#Z7/gx=o?_k'Gʧ:(<(/]q~;]cJ6wQL^^qEfryQ^AEP7?WpBE;'I^6gNW/</a _Ua _U{?zQH(?97+WqVֿܛC+@VjGL袊OD(+FZ_!?ܿ&oΆ(#(((((NG)ߓ+_F}OCU :(U/uP<_A{,7V;8ϭU4W_}w}4ϴƏe??biy|G,M/}o?⫗]h_a!AMV@ ay V*Ѿ޵>+\O%#o3~($(iڸi_7_#+(((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( (93#d3#dOC0_AEVǞQExEuxEy_& i )Ym!Vk AVU!R /둭*/둫S ?7ft@VjGds_T(O@(+?k~O]Mrgw? T7h0 ( ( ( ( (!46Q"+ik"ZA"9o݆>YΟ3N:F.?|S?ھ)ºz*=fkϨ"XP3]nrg%wVs_\FO(0p\#YI}dEET]?*U)E&|K 9SR嵕GQ^sY?_Q\k2GHӖc |~Vo/y; tWZ0ζ8!ShxOPhG:4iM=BrF-ihxOUM?JmR l|YOA5чߙļ^2%9.Ykucg_#}VJxnR72s!?+,U*߱a'7diY?CV¯ϒ]:+3'Ht =_prKF+?WC\o-I-Y")#qkM>ƵUQ\&EPEPEP=WA7ɿV7.gUMroU ˟c/Х?Tu4QElyEP\G!g\޸BX?C+y_?.O]2@c5kmkyE\KעWhyRh<mkyEk_+(<7w+٣ֿ?Q6<B?=yޣ '7+L 6hOnV<|[?_DOC?Gר{QQ@Š(%k`IEĭ}Z~rۼu*bq1]OWS^u7 咾?ZNgɋy(bi|ag&7' @pTtVn( #GB&/ȭ5;u=xjȻYg| &ۺ6}=bi|Q)ʢD1Ta):?((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P~J$X]zi]ؗ΋z)<B=YY>l7g1?w BqM} J]cϋz+u. ߜA@ߟʮթ/į{(} <B?cA~*߁& 0hEGdn1F"i2jSQI{EW)QEQEQEQEQEɟ%uɟ%uz (<(K?,+K?,+3NOmqO*Vk\S ]R >J>ՁR(n[y J޸5_Kmwݤ* zgn^nEQyu (((?n?u5X7Zk(޾(0 ( ( ( ( ( ( 8w?kg0U :(<((ܱ]Mx5UxX_e+o_AZ髙7ftxʿܩQXQEW-sκߓnoKᗡE`QEQEQEQEQEQEQErv:+տYXcS B(<䤏,עםXIѿYE/+گD:Q^aEP7?WpBE;'I^6gNW/</a _Ua _U{?zQH(?97+WqVֿܛC+@VjGL袊OD(+FZ_!?ܿ&oΆ(#(((((NG)ߓ+_F}OCU :(<((/_WatW_/)/ __EWYn_On_OVO\tB'.^?!WK㏩͍v ]WE\J誫FYg?D% (Mtwlb/a/aEo=~bl7zyޣ '7+b; (QEvxGF+CN+|QEsxC?+[q!]MvF(0 ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((No +Gq}OC.ާ_uQElyEPEP)?g\Ax=-?*tEyt?"TQEzx֪U?Ug@ GY%{j#Gg#WU\u]yG?O* ۯՊ65?zwE/ǝtʺw_*k|oč+Q\AEPuX>7?[_>_ޗ78Ţ+(((((??Ə= ]Q[xQEW%_?'ufh'nWg _UBWm?zxuRxu`TC("(vWf6=.SO8ʧ~aջ[8r»3f${mzOsW-(d0AKXI?I(Y>mי8DZxzʵ(]RaETQ@'k~O]Mv8zQ\FEPEPEPEPEPEP\_;@5k?٨D\8EȋhLtZ"]eJI&VN N&Y,:+Z#%%ty5iJ&AEUQ@Zl[_r-u6o\W~ȅ2j(BoC*[AU(Ҭ9_o? M\ρ 7|kG>UO((m3FuE%-J1#&#MIi%+5袊2 ( ( ( ( ( ( (9;o[MukhlOC1~EVǞq?RGtokkά䤏,עחWo"B(P(ş+?!^my"Ν_$IgW^ȳ+Fw?ZuqO*WuqO*vR(XQEֿܛC6A~U>_{<{1_*Mtu|Xe"VKH`]F>~UhaiKܩPviO_Uj$p'ֺ/?}"ɥ rޕ4jOwԧtn(8 ( ( ( ( ( r?;/~Ok0?_𳬢+c ( (8uvOEq9]eJ"Rȅ1QEzD VD @dEG+Z|M"Bt8jO?ٿtU/@'7$eAEVGpQEW-uWS\#!]_U7h0 ( ( ( ( () Ms]:n*gea:\4UUJ\SQWۺW}ּq{ Bҿ+pƅq7s?κ+u[NkN3-8Z!KעWQ%~_k+*O?!( (0|g"?5;fЅz'Uz4L7՞6y?_DOC?GYGdQE ($c aA_ ꫕/Vⳏ(rW?u\_UFSӎWS\?]Mox0+(J>J:Ey[KَT ':+jw,yyTW]k5{ *鴻,bF{άQErۻ=EB*1VH(EQ@cm"k$߯ZJR{QR (((((()'ZTʲ+cWZrG GXX?xz#kc3~=+/NHJ+ oAEWAQ@Zl[_r-u6o\W~ȅ2j(BoC*[AU(Ҭ9_o? M\ρ 7|kG>UO(( _YM4E; =5&na/vcYl/dGJ9iZkcW+cyc?\k#J4c& (Š((((((NGV}]erv:+?S~*_QEu]EZ)#75{UP+,(gW^t$IgV7IU @C z'HKg? TeE;_WkQ D N*_X֟>Q>WCa$/i@G,kO}_ӬaЮS!%r;x>Fw _a qZ%Eß9,8^EX֟ƴ|ح_77³|/<)0M G KkRMQn\U5gZ+/K Gݿ(9m#F{u5i3}dЫw7/@+((((((+_Fr?koSβ(<((3Vם?~B*}SCZG@OoW&F<sG?G!QS# (Q@io Wo\Gbq1]t⿈xaEW1=wB?!Bn?$oŠ(#(((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d.G)"LDCwGR!9 "bKOfι)Q=n:<4y wVWrjb{ؿ#G!k?1=y\h p̌bF/e1%jI=+/,+N<-!JTjI#LeYUS'+Si]WC&'+zZFU D/ -?Tk:Fn֡7&39n=[œDGc+lH " N2 Sҫ /r^/| i:uqwc|(+k+50I4;'V4dՎu]򮦹oǝtʺX0+(#"F;B-c^j-gXwTW !?B-cGj~~d'4Oh^#;?1_޸o[i'xOaEW1=wB?!Bn?$oŠ(#(((([o*T-ׯQEQEQEQEQEQPO*䍘}@:S׼sK?ELҬdFy p9?k:jElYZ]] W'6 ѭ+PDs/*z?CY~9׵xt閉uq*yn98Rjk:_ ?o*wL&Jȃ <4UE>(o|bEE[4_?|b*S&Q'UE{h9;'߿GWg\g>?:4wfR~H( (9yW>)KmEJ ;TCW|wBDH6FPV k^N>JONʹRvZZNWϲ;G}ߥ QZ]0|@5a['%g?hZm!%38>|_o~+(1qꪪ}rr?G'+@gϝ_z4 KX AGs\-.Mb=kv$M呔=/AȨ^ȿ׬S VUp=' U 4( ( #MzyxP3ղz-Zs/aƎu]򮦹oǝtʺX0+((o|bEE[4_?|b*QV(4_? UE>(o|bEE[4_?|b*UΏEƦ}h>h1G٢qzσlHsO#R%yob" RFs]٢q+Ҟ sWv]tw,ܒ=pG+zK*I{>P8<::w6f}/WiRh1G٢T/Qh1@(f}/PJ*٢fRh1G٢T/Qh1@(f}/PJ*٢fRh1G٢T/Qh1@(f}/PJ*٢f?ycSB~_Z|b*QV(4_? UE>(o|bEE[4_?|b8ir{ Frx 'fenpcc5PeN W Kl{L'u$z۫f}/W Z79u0Rh1G٢fIRh1G٢T/Qh1@(f}/PJ*٢fRh1G٢T/Qh1@E&-cSLߟ|b= (f}/VǞpv?RGtokkmT/ݠ E*CQEQEcxE{+/+7woZ,^ :V8{!^dc}Zh1G٢ze#zQY ݿω|7|"?_qY$$,7eSǩ#Xfo|bE=o|bEE[4_?|b8#F{u5豫x ^U/Wn;Rh1G٢q(f}/PJ*٢fRh1G٢T/Qh1@(f}/PJ*٢fR;/~Ok4_?65>9 wk0?_𳧢/Qh1[yRh1G٢T/Qh1@YһԧOsXml_ɏ?twkۿ ez*٢fǢ}TZ F9(dEG+4_?oAxfB _}NloO_Ovo]bxij|+4_?IetRh1G٢dw(f}/PJO >+j(Rp/etUE>+E[4_?|b*QV(4_? UE>(o|bEE[4_?|b*QV(4_? UE>(o~J+Sk'/?v_f f?ďQRh1G٢ly DD=T/y9IB/*O?Јu (BŠ( ȫyO j_$Ep%cjUd!μ#%Ԇkv(C| 4_?|bKp_vՀ\*[.$1y(&<]i{ I;_qYzM]DX}C| >k>(4_?{p1@D4h1G٢"KDT/Qh1@/q!]Ms~_W#/Wn?$oʔUE>+E[4_?|b*QV(4_? UE>(o|bEz?ʯ8ebH(((((,0ʰ ju EᛶArdEn Iw;FY/..DrP`Ϡ;O$n.+>ˆYnPԓiv+xPեp'S"MK]NCЃVW<9g1l/d8Q(0  8:f ~(GGI6zu[kO𭎝;v9d_e*p{=+r9d}s͜1W<]X^{c_28Fe ;5 %R0>NJ°WK_.ԩE=kU T-/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?E/ G{cxG;?TYQE{GQEVo^?*ҬȽU#RǗ"z"^"O+Դ/Ae-,o+y~Wmz_?ާ(#((((((((((m#?Tm#?T(((( 0 A -Ut%zB`TQU)J[ET((((((((((F,M&W`IEqw^7"1<I$]+SEDZx7[kgSZQEQm}ToC*QP]^Y {OM)ވR=RSbQHCsUkF(N3\w^AER(((((((((((uoA5W'm# = Q[y%?A)#nb PZ-п fkN{VA#퍀j!_7S)j-6޷BR[__OKt/'XiW?mI6f?{vLN>7Rq%sB [A? ‡c?,O;KZSZ,"{]+Vz_ȬݿAvMt6z}L99$P%WؔQEz~#zQY ݿև^V'wowx?k?(=((?gWS\#=O z#z (((((((;/~OkNG)ߓƶ= W,(Š((3?묿]_S\Yһԧ/w!|L}Q^e>U>PY>&r ֬ȹyЅ]/>67ڧ_S_Ovo]s +IetQEQEh1}B-uWnU} |2:((((((((OM+OM+hlOC1$‚(<_(/5z/IߗJP+,(T֪xKE8:?o>TpVu?/~}V(L_-}C_/tj>5tw,o+y~Wmz_?ާ(#((((((((((m#?Tm#?T((((.a<w=Z|Gr'd@Qk C{ҧ+Km<ɴxZJFFцC8·/MlI(Wx _һqXSp1QE䜁EPEPEPEPEPEPEPEPEP\GW;Z`&:ce%quϋ*%N;)+QR;uş1\Ǯx{ JV(  orwAH>Q˱2ןs+΂ǐ V&r _}NloOȧҺ*| ]WEU_2? +#(+?c?+[D/eu4QEqQ@Q@Q@Q@Q@Q@Q@#}WY\#}WYXcHQ[y&;cwDU"Xv?s֔mĆIVh&> !rT$onrl_k5L}[Agdoñ;ƏXv?s/BL}s]{^񍦭em600JpWfciBZ(ᰎ8cE ~X|%x=i&m(ݕ袊1U"1U"F*n }WJw~FtQEsQEQExC?+[q!]MvF(0 ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((MOO&Q'PчOfθ}5vuhޥQEQEr>;Qe!XalXy?w?,W|QSK՞EWpQ@fEC?*ڇpYb?/G y|*'Wh_/+dQ?KBMXF_"_3ʯQEVQEG-Wq#Mu7_G?]Mr;?u4=aEW)QEx>,o+y~Vbvw?1h0 ( ( ( ( ( ( ( ( ( ( >J>J(bKIZK #%8H#c -d [;jI|j ǽO'Tnm>e#*qӳ>HgQE EK$l "zhVĺspj,x].K8yl =T{ќg֖C+#Z5dQEy8QEQEQEQEQEQEQEQEQEQEQEQEm}ToC*Օ9Tt޹ҧmj(+I]-t-mkl/?[oV(B0\V^AER(((((((((((uoA5W'm# = Q[xQEW㯽aeƸ}-y_& [Ǭ_RVTGE[<*[<*EP_^V'wo䵡?]-t=O:J(`((崏U-uBq^޿Ľ(#((((((((((((ܿAU*ܿAU(ȹyЅkVO\tBSS/)/@'7fUW$:~( ( O k?c?+ *zMQ\FEPEPEPEPEPEPEP'qW'qV46~?AEVǞQEx>o++g1^fq/D- )k,*ꟅTsǪ~R( ȷy_ UȾѪNJ[/*__ҿ߯xQ\ǰQEQEr kWS]# ((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEVt_Z5/x((()ɿIT1ta#٫3~]c;?|$QElpQ@YV;+,+?_RgE!EPY!{PJ|C"\VXKB{_'_@Zoz?TO?Rп_?Qȶ2hQEQ@yqU\&F.?]xMy9GQWS\&+K$1]?-Syh{(eC}c+2j*[?أ(`9/şo/Ji,riȏl`qȮ~W^#L=/mSqEWQEQEQEQEQEQEQEQEV=߉ILM3Hk?$XDJ#$,~5ufyTzs; VSRmfGS®הX^Ia{DF+-Ǽ\2CxuRxuT/1N{xIqI5̶?0줽Ǡ; Ϙ~f jxd6> a n (:((((((((((((((( oC*[AU((((((((((((((տY\&Ɔ3WQElyEP\wOu>s~h'Ym?KQ[Ǭ_RסnO_nO_Q@~#zQY ݿև^V'wowx?k?(=((?gWS\#=O z#z (((((((((((q閲漍\p@\tV0]%%Ck8է)rIQ]ErǢ}TrǢ}T|M"BQWS"mAfXn)S]S_z0| ]WEPZgoDԗ4ܑJ.RoTQEAQEh1}B-uWnU} |2:((((((((OM+OM+hlOC1$‚(<(+}YWk\W?b_/̉'h: ZARצXU? VT(Q@*n }Uȷy_ UȾѫ_#h: (`((?!B-]q#|GQEqQ@Q@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQES&Q'Sc'߿GWg\g>?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—y-zJ${W*;_>>NvEys2(0 ( ( ( ( j6ll¨ڭ]rc@}yO7Gf%Ky g?ʷF J6r8enkgk dF >J^NIHmsQE|QE+{՚_YI{ <.$:?y|xSkKܚߏ>MB+h%'w5ݐ'7m4GL*LjU*Lj_@jT("u aM`EFPIE*QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T(((((((((((((NGV}]erv:+?S~*_QEQ@q:I?ZkްIכgezU-Em?K^~PU?~U?~ QEeEEg'@rvKZ#zQY ݿLߩ?3+(([H']MrG_Y?*kEK(0 ( ( ( ( ( ( ( ( ( ^%+$p9[2MGK–ު:?ϥr㽣aJu4RV>\EA?ҳH F jLQ!O`sª>ί}J+ΒܿAU*ܿAU((((([DmF;Bp«oKᗡE`QEQEQEQEQEQEQErzo]erzo]ecCgz#QElyEP\W?bZqV7g~dO;AR2­UJ7z@(WwEWo]xEϢb7E]+/GQE{EPEP-]4*T]X X[u׍gRwi(2 ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((MOO&Q'PчOfθ}5vuhޥQEQEr>;Qe!XalXy/]@Zi%>Ogn+ʗ=r΍?:+?7y}GFxe=?:GGUTTy'_@Zoz?TO?m_Eo ڬq DR0;WeTeW.;SKTDKch&_)mߔ NX/4Wmߔ ?6RDŽY}Y.?9jKM_P776efcd5w#iGi5w_*kGyA|oďFQErQ@gb`>,o+y~Wmz_?ާ(#((((MFmEVx1۴n[tT*J֍iњ7Gej|?:4 cq(9?ƶ謡B0w;19Z䵐QEQ@dxoK\ҵRUZSIT,+kǞ?RjUB劲UΪUΨ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[AU*ǐ @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@&uoA5V46~T¿P+c ( u>㯽ae77sD:oZ=bp*(rǚTrǚP(׿Od QV>U>PEPEPEPEP\#!]MQҭ.=c95tw?:4wfR~H( (9yz8_G"+AVct>;Qe!XalX2QSK՞>Q>U+PTpX>Q>U(PT)d+h7:2ĠnVo^?*ʽ8{)h5Ȩ]^M.K{B0W\@<|I"z"^"g(eԹ_sᆥ**0]{}G+#~/k~WG+>»?O&WzG^i[EXh]{}\X"8c䀧zyqU\&p'6ԙt(WúQIGyA[u]򮦲HZaEW)QEx>,o+y~Vbvw?1h0 ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((*wط3t8&M&)QEQEQEQEQEQEQEkNGV}]ecCgzO+ (<(;_z']q:I?Zs94LXʥ/kЏŠ 'yUJ'yUR( Ƚ{\OkC/^?+??Z7??ʆ8L.))nܹLoX?^I?+n6sn k?"!>bk`Q12ս`z?2ս`z&EZȪwVVck_ k?"Jw5=nVXLj+q֥G{cX 'H;?ƫ#bjߚ?ɚJSthO$(+7?/jiVo^?*)z?OcQ?KBMX^['_@Zoz26FU (=(.?9jKרח_k '26h<w_*kGyAx# (L(3g1[GҰ|ot}+/oSqEWQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*>2h.^ODm + ֺaym⼂06ܺ=caRv澧Uak HkX%F2 xzWG-mϦWJ:d9z%FVͺ( (9O25ѳƀoX;GRi|Azl"SӢ"oQ7<`cԊݮo֎m]%xx^J t݂(S ((((((izo\NǸLE$ةNRŠ(HQEW㯽aeƸ}-y_& [Ǭ_RVTGE[<*[<*EP_^V'wo䵡?]-t=O:J(`((((((((((((((((/z'UJ/z'UJ((((((((((((((((+}YWk\W?b_/̉'h: ZARצXU? VT(Q@*n }Uȷy_ UȾѫ_#h: (`((((((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gȎerաT$A#goʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*dѿ't,o+Y>sһkf?yoR|ty|u`'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyo)0sU?:dݓƀ*yoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@]OW7VZ݁U<~U> kӰZ'`]'G'ZS*n9qx:8rU_L D!>f?Wq CEcE @*O:OΏ:OΝJө`TO-ߕ[q*_:OΏ:Oβ;DQ9mDz5A|;`o~UIIƬJR[1 *XR|ty|u7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*<~Tt?t?'oʏ-ߕ/'G'@ 7KIIyoR|ty|t[q*|vaGugIgk N5s~h'YmRoIX~sTt?zQByoԊMp8Vӻ U`Np9oʏ-ߕ/'G'@u= >kSsͣ5 S@Ҵ?:u]|3zÏZ(((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((;_z']q:I?Zs94LXʥ/kЏŠ 'yUJ'yUR(((((((((((((+/.vB܌]G4e_#*x\Vf96jn`' akt^qq5$Sx?]e`sJv ©[A5sx8QZ!EPEPEP>U>P{k\L{I$yidb1M{$I )"aF+u]IdşP>V])E]u>[)U¯fx$+4/K[Nde!A#?yuN"c_rkt4!C>걲etm(낊(OZ]5lV(l EioHecF5tw?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—PEPEPEPEP5^X6vS20<ڹՖDzwixդvd2p1NbvZмS>o خc¶pA `>9?Z068k$ (L(((((((((}-v5xX}$k3N=bp*XʥB? (*ܟAU*ܟATJ( ( ( ( ( ( ( ( ( ( ( ( ( Mv}qKEQEQEQGA@q*S1@jiF^ep:}Ea M˒2MEV-RR `( a@REQEQEEqm F9WCoKzbEjQZFH+FM%b*P`ږ+2B((((((((((՟vx>o+?%ȟv-ze[=SnoO€*QE>5XWwEWo]W_6?S+(((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEVt_Z5/x((()ɿIT1ta#٫3~]c;?|$QElpQ@YV;+,+?_RgE!EPY!{PJ|C"\VXKB{_'_@Zoz?TO?Rп_?Qȶ2hQEQ@yqU\&F.?]xMy9GQWS\?]M,oďnQEdQEq7?[_>u]|3zÏZ(((((((((((xuRxtR((((((((((((((((((( oC*[AU((((( 8/a:J_ جʶa^5hȸԔUQES袲n(((((((]U>PEPEPEPEP.= 62_a5$q8 -i]uKd}1Pai*J;ѧJCԭ|]N} ]cjes ?kN48cTf (Š(((((((((՟vx>o+?%ȟv-ze[=SnoO€*QE>5XH'w`N<t|]+/EW1Q@aZ᱓c\~Nߥw`(­O RԚ?I&8qY]VGZTm`pkjoGwcԕ'84o^y.E!EPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEɿITd$t*0 Ofα ٟԾ_ (8(G,+?_?Es/?wbܩzh(ȽUY!{P,G!=/ED - E7cEyl*'Wh_/([Kx_U 4((.?^^\r~ &<GyA[u]򮦖7G_(2 (8şo/J|YVO-Q\FEPEPEPEPEPEPEPEPEPEPV9E܌z㸮NO Y-# O?ʹh7ά{y`sz]Q^aEP\wOu>s~h'Ym?KQ[Ǭ_RסnO_nO_Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ xc)V@QE0-RR ( ( ( (3uM-ICnQg#2xVɉ." '򮮊꧌No+?%ȟv-ze[=SnoO€*QEQEQEQEVfE`>3kNu%N\veFN.c\]V6# :ԟSSZU7S)Š(s0(((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?E/ G{cxG;?TYQE{GQEVo^?*ҬȽU#RǗ"z"^"O+Դ/Ae-,o+y~Wmz_?ާ(#((((((((((m#?Tm#?T(((((((((((((((((((ǐ V97QEQQEW㯽aeƸ}-y_& [Ǭ_RVTGE[<*[<*E($W'u v=I^:IS?4r`Gһ|ˈ*q * +⺋H{{T՝FJIJ;0)Q\Km/-k~=kFO%nh>MF$޷SnrkuC۰4x v5шΌytiRjQE`QEQEQEQEQEQEQEQEQEQEQEQE[=*[=*QEQEQEQEck:{!PӑOEX=Edң95XvGU >tz4Õhwnk<&̹?tbFgqUAEW9QEQEQEQEQEQEQEQEQEWqָ՟y_NtLsǪ~RPJ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEP\߉<} HkZ6ȅCI&=v$sZ֠4 Pԙw ;i. R+?"sWDnJmҐ.@={ų52D컀}k~+|!)I<7%Ϳ%O:`1^9/:%}jGuЕ ~%Ih(((((((ֿƴk:_QEQEQES&Q'Sc'߿GWg\g>?:4wfR~H( (9y{jWPYJ(;ʲJ/ Ӳ W l41p;z?d:+?/y纜zosχ$ j:Wn de"\?B3WU!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( u>㯽ae77sD:oZ=bp*(rǚTrǚP* ZD>SM;;(Eo++g1^fq/D- )k,*ꟅTsǪ~R((((((((((( mׯP^QEQEIuIAJjDsu(HeQD7X =s!;1?k?bU_Ht/x҉+5? |ntFIn-&IA&?:< 8᣻3zAEVQEEiDžr_+3,+N<-!XCo:^(<ίQEW1Q@yv xTUQ6w]eՆv~GVÿM_}c[u`-~ ĿnlOFUT'.iSVwqL?N%(_O·6'^G% o~?ZQxzO쯻*\_ON%+sbq*6'}}~,iOu֥ePg5/ >:^5#6HIZEW)QEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEKR .Uqk}=N/uVr ;uףX= zvoB 'sՍ\VG4)NQL(((ǐ Vukn#϶+ƾ.aI o摢e !ʐI,q<{2G4g)"SdW㯅|;˧DZ,RVUсMNѼYd|oU[޻}.cƧnB2H'>|w Jm&)o](W`:!jk篌MƑ štxjy.p>:޴4-B1'QC@袊/6xok{i}gu }7 qN?=_X2KIۇ*9qԵ"saOiiz'ˆk|  Y𝶡Z?:< 8᣻3zAEVQEEiDžr_+3,+N<-!XCo:^(<ίQEW1Q@yK/I6w]eC១Q\ǰZ'u5Z'v>fvE`QEQEQEQEQEQEQEQEQEQEQEQEm}m} QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[AU*ǐ @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@q>'XgaȪT?vVO`S*êjʴ9dv`q+W[lyoN'mFY@;<H5lxA<̛)#nAan!6ԟS\IJ=LneFt\)X+(;_z']q:I?Zs94LXʥ/kЏŠ 'yUJ'yUR((((Y<\m&)d%B" FOҕOg+o]O,(云JT4zG$~FtV=U$q qH(P((((((((((((ܿAU*ܿAU(((((((((((((((((+g1]q^8?3I|2')iAK^aVT*[=S QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_((' u²u$WoMR%j tu2Y rF~ HR2H S=? 77,?>H~WmJ{ϟ)6mM]3Y^aw=OE Z\m%qzVjpC4 E2Oe uR"=/Lgy TO]j5S!!y#9 ٤,|/%J`2A9 #*w$h. VE%pr3-(Ytaֵ%pHQ 09֠Ѯ5O×w^w˛v9Ӝ֞GF}25֧QvN8$m8fX'~!jǣkEǘ18 ;%َ/]6/b?M,k*GnCvь8zԞ/晩2: }~֗kWnVc3sW<):s͉3&?ij~٠ ?i%z g.g?1g Ӭֿƀ,yuU( ~uGo|J(ߝo|[p5R[p5\ } d$t*0 Ofα ٟԾ_ (8(G,+vXj IVYVx[%Be= tY[p4yT[p4yT-uU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(@Czb?ɎBTLՍYmP}jηjE K]QVV([p5+v]Vі:Roηh?E[?<Q@:P:ηjoηh?E[?<Q@:P:ηjoηh?E[?Ykf# ێަ1!f&[|ηj~uR3 aϫsǪ~yuU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(ߝo|[p5R[p4yT-uU( ~uGo|J(ߝo|[p5Rv⧪U~ ( ( ( ( ( ( ʼPk>P}bv_z-xEiDžr_+5iaImT(((*ܟAU*ܟA@(uI%ϧ:s`JaZD yT9cwtWBTQ"YU} 4$5 S袹Y$QE (((((((((((((((/z'UJ/z'UJ((((uw.wl8뵽_6Sa&>W݈W4%VGN˔ )kVm]Oү׉ZJSmQ\aEPEPEPEPEPEPEPEPEPEPEPVT*[=S QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š((Si]WC&'+zZFU D/ 9kп_?W1[?:m,[V2 rϼU6eU֕Hh/5O} ?cYτЬW#X?(g>BU;Jk(_ZVt/k%!UOP5:SfvZhIQ\gr֟8I#]Mr֟8I#]OYu4QEqQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[oA[oAQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPV5 V5 EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP>U>P\]>v#q*6jϭljRe~BI9<ЍKwU(–;?ƶ3,WPmn_ǁ+ƫ<74z$ҹ>?AOB4ҔLLҶ*R[VjE}QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU? VT(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQEQEQEW'7u#}X>oS/:(<((zZFU qѿt:gl"} e(B緆M9S0k/#Lu_?Ԛّ*PK#Lu_zǣ޼veAlg@ ̵9)*RVϧ2-`H1]/F@OX^uu)*hӖ q[v)di 46ZXaIOEb'qwQ_pQE&\S\noGizMQ\FEPEPEPEPEPEPEPEPEPEPEPEPVoS/:(<((zZFU qѿt:gl"} e(Bg<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEP\S\noGizMQ\FEPEPEPEPEPEPEPEPEPEPEPEPVU!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅTzrK$Y.иw\vw"95Ewz"@rѰңӼ̲N9p~׫|"*:wׇc?٬ݳ<H1Z Eyr|l48(4 (}c\{y0yE)RYrjTb썊+ķ&1t?u}Z>FSMm_ V.tevZ+((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((((((((((((((((kRkR((((j|AcTrZWL<&Gu`NW5OVfxɇSO[O+,.5Rg.!ɀz] 8Ss7B14N( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((X5{w˷]d"j$K={nhԼf*(KS{t^üHJlgYV>ݎҕ'o^\yQf( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((No +Gq}OC.ާ_uQElyEPEP)?g\Ax=-?*tEyt?"TQEzx֪U?Ug@ GY%{j#Gg#WU\u]yG?O((kO$}kO$}'S,ގ:(((((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((((rǚTrǚT((((w^ѥv˽àqWEEkFϞ%n԰Mk1oMK:u0fvEt1ӭKYT)Q\&EPEPEPEPEPEPEPEPEPEPEPEP>U>PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPVT*[=S QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((ZѪOdc =MAEMs=#9t 7zG4Tg??Ώ@S}H:>CEMs=#9t rs?_Wes=#됙? ~>k}S˷M7zGǞCEMs=#9t 7zG?g\AYٝs#t>iPgH>b Eyt?"ɨg??νBOx֪U7/NW9t Q ^9uC[NU??Gu]GM;~H:VqT?R*oGHzGs=#?pF\gƒ&xk 7zGs=#zGs=#*oGhg??΀!9t}H:zGs=#*oGhg??΀!9t}H:zGs=#*oGhg??΀!9t}H:xu ٹzuFzGs=#*oGhg??΀!9t}H:zGs=#*oGhg??΀!9t}H:zGs=#*oGhg??΀!9t}H:zGs=#*oGhg??΀!9t}H:"bO@*\$ETG\M(F{`X+*4QvW!29g5ZVe `zיU+-/"C1ھfw_';82UX9t}H:bW!R2f&9_zG4Tg??Ώ@S}H:>CEMs=#9t 7zG4Tg??Ώ@S}H:>CEMs=#9t 7zG4Tg??Ώ@S}H:>CEMs=#9t 7zG4Tg??Ώ@S}H:>CEMs=#9t 7zG4Tg??Ώ@S}H:>CV5 zGՇLܣCEMs=#9t 7zG4Tg??Ώ@S}H:>CEMs=#9t 7zG4Tg??Ώ@S}H:>%_öUmq@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( (0A[{r7rRo\|Z>(Vd@T8?ޱ+έ99}^IPJ4 ah'ǟ1zqRy@;2 =ƺIg҅*0( ilHWcهW訩N5"-=O*l1m"x!I{ֿ;sswMd6`Q^=,*s6C5M\( oC*[AU(5\ç080 ?=kJ;DWnjVq'"̬?1]Gq nE Z2OFKEV'QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnO_nO_ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[=*[=*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<(($c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEZ <##QX07o`֮S:)bQ\^ 0&}I5b*KDa)9;݅QLAEPEPEP!Ry fvE`QEQEQEQEQEQEQ\׉Yaur]2}D|= W<17wpO0Z9r]ѳx+x)YJ78h<(((m#?Tm#?T(((((((((((((((((((ǐ VZif͌,ּZ"5Dzxy)^G)Γ5$+B(((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((((rǚTrǚT(((((((((((((((((((ܿAU*ܿAU(((((((((((((((((((sǪ~RPJ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((No +Gq}OC.ާ_uQElyEPEP)?g\Ax=-?*tEyt?"TQEzx֪U?Ug@ GY%{j#Gg#WU\u]yG?O((kO$}kO$}'S,ގ:(((((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((XȨmGVRje(J[#Vj:ȁу)N ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( ( ( 9w+g2YEVǞQEQEr8KOʺ3A6?T|AIC G*FcbIa֕& #+RZ4J^dQEw[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((3WO6|@U5]~oaByGs@$ (:1ztZpV:O 9iXo_npkOYΥd޶2'NnEW)QEQEQEQEQEQEQEQEQEQEQEU?~U?~*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEn_On_OQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU? VT(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQEQEQEW'7u#}X>oS/:(<((((JR A5z?wMT>$c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEjhiQp ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEW#hҬ.5MKh-@'8MڶiZHn?P_E$HbyeuHK31P:@OTм;M*? }GiZM֯k$rICrVt_Z5/x(((((+Gqk}S˷eQ[xQEQEQEQElǏj[?Z@Q ^Tu_׻&1~0 uUA_ \OYǔSB(H(+OkO|5?/C+((((((((((((*LjU*Lj@(((((((((((((((((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEd7E,~Q\gї;K!5WWEQsq wŻiZ%ݥEp9T~fvP<9hK fSꦝ`2MxW[?w h^D?^yfN8. ipݒ<ƴla6} P y⵷yXKP9$g4axvN~dQ5ycW4ƃml}~"Ixy-0OҕI?=2Etvx/q{0Կyz ɠ?zŝ2P˵,i[WWBk`N~ Zp1j[Ww$rlH P:Ȋ*r[Pt_qZ/+=kWu=ς!^(H+k>fy4,@u_4yHQ<Qy G^ /A}Gfٶ61|F7R`;ɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~UL \׏oծ>yI;׏k}S˷_'ɓTkkly^Loʏ&O7R}'ɓTkkEd~U/G@y2q*ZɓUj)~J}kOoժӶ)UA|H?|mU'ݾ&O7\'h۠9u ZVqT?R/&O7G'~Z>Z􈼙?ߕLoʥ[uh[uh/&O7\1񔋃k[uk&w5ۄj^Sd~U/G\F^Loʏ&O7R}'ɓTkkEd~U/G@y2q*ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uhYQ*')X8>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕZX*I㊋oթ䔤* P?&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ?ߕLoʥ[uh[uh/&O7G'~Z>ZɓQ_VV"d~Ty2q*oգoՠ7Y sڮhZI0jQEQEQEQEQEQEy|A]k>[{oN)hݜrRHc'׈|IMWkooodwAe ng9< sq^E`@: (k9rϹ\Vo5[V0@蜀 Gc][J4}>z\ș>IkcI~!˫[&Xb#e_Q|C-Z/Kወe< s{e7PJ^cqI(hM[鶀P.=X$W袀 ΗkkFZeQ@Q@Q@Q@Q@rs?_WY\;@ozv?C+c ( ( ( (-?Tg<U(:/W KCF8Fu]_:A]Uk+8*~QEQ@r֟8I#]Mr֟8I#]OYu4QEqQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[oA[oAQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPV5 V5 EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP>U>PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPVT*[=S QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š(QK1@$-`xǛ*}0OqR!7erߍ-!Apj;=U G=LF]2B+XSz2U]5|cp͋g<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEP\SYEJc1]XjfL֜Rc^(S ((((((((((((m#?Tm#?T(((((((((((((((((((ǐ VoS/:(<((((JR A5z?wMT>$c aA_ ꫕/Vⳏ(rQXQEQEQEQEQEQEQEQEQEQEQEQEUӾ/?QV-?zi>/?QW fs~iѱُi>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW668fPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKU@>`=Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@I50iEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EX6kuP9⧢(}_~KU(٥(4E_(}_~KU(٥(4E_(}_~KU(٥(4E_(}_~KU(٥) I^ l@QEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š(((( g<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEPEPEPEPEPEPEPEPEPEPEPEPOGꞠTީ((((((((((((((QMO΀E7OΏ1??:u1??:<R ЃK@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@RPpHobx~t)bx~ty'@'JI Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ('&'@'G:obx~t)bx~ty҂9((((((((((((((^ O1??:u1??:<S|QMO΀E7z: ( ( ( ( ( ( ( ( ( ( ( ( ( lN4EPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<((((R?T?wM^:/U??G~uu| uU'?ܩ~EVQEQEQEQEQEQEQEQEQEQEQEQE]Tީ OGꞀ ( ( ( ( ( ( ( ( ( ( ( ( ()?ZJjQvDg ;+8.͝4ǚ:z+/>o}4'?z\K@ܣt.UҴ&A"&qϛc+*U=Nn7Jic8<_Z`־ޟsؔU~=Ao_OZAEPEPE|E%| A%ձb\)(wHr\7Ÿ>:ڮ 3YWhT ᘜǽw4QEQEQEQEbZֺŬ;[+&w {`TEPEPE#)>/mwǞ$ӵKM:a3ZŃv hh((*MYZC5̀ : Mu{IQ '\ڸ?SbcgG 'Xi ?wnsqָ{?e=I֖=:?=|$(\Ԑ{IW[%0FIP8c(WmR5$/E0V|?V֖s-4@$ 6p;{Q@Q@Q@p xN-4aέkی1a^8=GKi2#2(͑^(F^ w7HeOA@Q@Q@Q@Q@/iҟQoZF5W."b' ΀=Ί_ ֵwo>, $Ԍ{׬C)GzZ(U*os:7yl)1svzAԨ쑷Eq | |kjs-wq m*q+GF2HdedG$`X4ʻCVct{W P~ɫksyJッ~ǣQ\w,?'G,?'XZcg??GcVaW' [P[1k$N% YƲ%i#='tIET]ςMf8d:,'pA\O~=Ai ]TZ*حnrMzQ@Q@Q@>$kFMmyjGllٌme^u_ z5ݤSȨṔ'Ѣw¿sυ.m4䰊%(Km.F~Q?**((((Q^Q⏏Я$W2U >sz' }Ộ"/ *΀=֊ߊ4Kkq;Xc u lPEPx}*r-fB[+ظSQ\&ϛc(/mGE 뚕զi 2m̸b9 TWP!X 8$Z/tVtzi:2卑\&ϛc(/sG֭N|wAl=5\ҋq{&vaET(((((((((M͢WW-6k@JD,5h2AW&͢s~sҽ6 ( ( lN4EPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<((((R?T?wM^:/U??G~uu| uU'?ܩ~EVQEQEQEQEQEQEQEQEQEQEQEQE]Tީ OGꞀ O'Iŷ$[$[J/J+Ğu 5S =fΫ[=|N^7$Mp}2?^ĚÍ kNhc|gomu]'^K{H)x}rzc)?giOwa,~R|)|EF<_PN_c( ҧ8Ǿ@=ڊEtJ<EEHS֛ ?U+L\mCF[S{E򾕬-<^y%&l2ʀorGA [&.6 ]澨d̀񝡋n((#K^dNsJK9$VWPEP)wɫGABw,тO5wɪ}]-kXfD 9W=Ҫg,yuߥ ?/U?I4?G$?G9_s:|_o~*$?GMQ='uߥ |U Pkk IRB(;th¹O[_떒̲"#k*.:|+*ךvso_OP[SIPHpAy{kZIw{s c/,{90; VJAq׉xU~4I]M10~q/ 3ԞP=b .G7e>˷@jvfze8A uϦGC\W$ZG^!O5w1+نG۾65 ?t:8.:hZ]Y֝it#vɯ7\J_pim^>//mtI.nae@8#Z_M`iFm%*8%I{7VV\OF2UG'^C7ȇMTudֵO|Ne;Xf84N=}hn>0c#8p"J~h~'K Kjת|DeQP,wZߞePg?^Uÿmn "f%rCE&уҀ>Ե}3F'5K],SJ͌#_2/W-h[NQOk['UyHrqy{|El9o`]nS*F=>>`kuu [#?(}kjvlnt[2L(aeI~uzڵ]B < >³<7f5̖f՘TŌ\^YGȅM?T|Q'я@Em<Qgw`u$~. l$2r:aJMǰDshX&鵏v:htՊP+==A)>6ݵx98Vlo;4LA"6G_'|@^un gn>?HO: pH&sW~Y*[<a4LA* ׏jKᾙ{}#KtF9/c'4/ |e_1[x.V걜1}woiZh֒2{Id=1ک|,Շ[0K^l7nqv~jĺ-V7VZ($rHGQ[or4scv9>S ?O}v񕎑9=H*yJrc 7qcQ~ZƗ໭N Np)cA߉-`^(;s٫[}+Z_+ek~W*%c(B{0/ދzZߕ ?+G 1MYF?ɿŽZ{|GGeE"TdQ>C^ƼM#GaG_Zb(߲8V:m/В(c8_JA^CZf_|ZW80O$ZP3*#;UQY6>+v Ү_;a92p^+F]|$?4=ю=:O>ҷ6ˍbGnQO`;o .622EbЄ W/4>g6ϒ3.W=w?u? j!i"Yh+I8=?Kb`fOMuoF{oAWO2K{AsҺ7ǂ.,eiNw9"">_J#x)kiᏥүfgmZ5<oowd=s溚_ڇW^ VJw>Ö>h/x_hֵ{kGatz\=U=}nѻ}'>K'.,B<ۗxP1[L}SjvcΎwW_ӯƞږJ\ dӞ*g%|',x?[vwtҵKViVQ-g{Ɵ$zt= MJ3)\u+ko'-9RxGÝZE{XHWEg?*<+կ?gͧms so9*FcKm;[[. fKwg{k!{V]N UR?7Z1 .ci,Rv9P\>zׇmXC{ o"J2`D|m^m(.>W!MH PqϽv7x}+L wT;oJgϝG}ߡW-'cQ ?G+9jve/C(ϲ;?ªE ?"?οg-N̵e>ve-㶅*G*$ZO{ZӮK GP?05VUct]ʄg̮xSA)]MtxSA)]Mt+&Š( O]4Au[.|\$[qG*=[Ě&Z3H1'{xcGxH$Ece U>POp8IznSYj>NH{F Kd~&sLI:۬żϿnAQUoxVQm FOc{4|%ȁ|GrqU_?Z`hE F#+ nVU8e4O q/mF:"7~\1n};Ohlum~YaIw '3ހ=[C?\-pv&7o -u }sG%d6 @8$~YԵZS&̅Yʒ{}@\/4$q ,P:OA\eYܘ%%88Q"J~|Qx} Ep-jL>9¼64y\EAepp>h!Eln4}J5 gêQ.E5| kch{i_)\u8"+i4}2Ŀ)+?f/J~6xv)TwO C0'8zSS5mrq"UA#' [m7_w8H1$+Ⱦ [!UutxtVVy #NNsώYh^7dk9?lv9ϵ{y +곴v1msxr=ʒ7{=?MSh6((( mׯP^QEQEQEQEQEQEQEQEQEQEQEVt_Z5/x(((((+Gqk}S˷eQ[xQEQEQEQElǏj[?Z@Q ^Tu_׻&1~0 uUA_ \OYǔSB(H((((((((((((iS=䟴Oo EI^^ID8_x7WxLNҬ㵴=I<ׇ^;> CG/Q ^^ WK߁߳ң@_?_n}3p3+_ORx.3EnǾ>-ۃ. _{bzƯ[f59ەXw p 5L!Ug\ N?kmd_ OLglYHȷ;{8_-n>54\l-D/c=#8kcوȓVi?:< {ߴU[5Mr!n9k?_(m+x,q D?4I Pg]im“Yq ƷqxK]n|/pʸ݁@4~3îFK]O2ukoVk#?)f t^|a-] v+ xDž<[y>"O TFO4~qvU!DP+i9Rm32w x5i@ *" syI?bsl{x'|Sկ{߶ڵTQExAI]jf]:JHmT%?M[~ZRYEeB\c(/J?º*+Oe/qw+ ? ҿq}𮊊=;qw+ 5*Jm`QX9qҽe\C5jqnۗkT96 /§ _$Ztvo-:ݬX֩} !_@4Y" y*}-/_B?ֵSB>Lр6"' }-@:zhմ{+c]p +֋eB(X~/j [XLyhVbʣk y$%=*\veA:={g4:8.:>8bFw K=*ZӦ!>h>0(ŮY]C) dr3[ .dS<;zEd!_a|bgK 5?Go@*CԿ&*:ه&Of.`^5ya`HSb>G $?]3o/Ն |k"iӯe;R+ ށ+5zEec\BG/NԴH9-7^;%{+倥Dc.^"tH*W?]Cme3[[T>8>7Ҿey6:i}+_ٿGOc i+h.; +]=?@ӬEomJҼ'dzLMڻvVr/W|?&> g0,SyIT#?B tݺ m+j0澠[(bB;P2I>| o}]M|N9H!3}MEPX#ѯ*ڬo7 ^)kO{0t` `Oz GP|==|좺0,<ДhXsQ?º+? >Qz-?uQh9Q>+ݎa b.ý>w/򬟈_)o/L-(RTEWXW/ΕuHm~8Bqh E|RϏv [(Cє~F8pHw1JF@*dAk^*olc m[[DN :)#nk^\WP(("(Xt  z:Nld[tX9:յ+? ^Ic$_Dv ތĀ#ҷwͤ[ԬEf s׆|f[WTm{7bomij/F遞:lrt_ K<. WחUQ-^,-#ȷap:-CDT~amE/ٚR 8|}Fq/#Χ`ѱO_2< ow?ZoEI]W%r!iO%u_hc|ƯQ\}C_,xe'ljb]sА}O@5HZF lZO-IGIWIk ?GNTQB\w?]$Z'G@?t]COG+5 |?: ? 5y%?KIe|R|{[s_0TZz%i}s+Iem^ ڎxtoYZSnE8}NOT> d"¼d =Z|>< :|KP'y)?SyO_W\;y˾IO}J袎RcXB P8ŸEQE5շҼNJg^^q Y?pGK~gA?@4o]Yztf`[ _h޺+oS3>rh޺(F]M{z٘?t8<Xwyqui^Q/cSՏa^S~˧h`tyYaV}WQc101Þm"LҭD7s\t<+_? _xa촷Gcj_Qϋ\7S,~}[ ݸz pW5@4M} }.{7?FmR׌_K^?`o5?nWܳ+tUC5|?PrWeGixOL#RctRL *ƀ;φ62QSWƺ> k_ G{6&yRiOUIdnd~f Pj7$~_ ]_ݖ kݾ4#݇G@n]NI21 dGd VOz|ʿt?.WNGE}\4Q8Cpb g#uM:Xn۰P OxիsY\cs sd|6%ҵB'SϹ]]|/xz1'*qkjO WLa"B"=@UeQEAqҸO6ڭw¢l8]J:ƍYE롽&$בvi޸WAEasi޸QvtQϹzT5 XXi3fXOz'.B MgV)Y$ٟ@]5s^JS]-e4H񰢊+/gW?޺UUO v!-y3]*>R1I6] ѫY߳ʪ5ڄ?hsMH?j$ѿ_uA5WuB5uA5WuB4?%_-K$67EiGo]wSl,NDe %_U)lu^[h:%x-!i\p8߳č/ī>+:?o(^_kNߝ}A_+/_|jIIm5, ĢUEgjGzk|ѭA“Y2x෇IHvpa\ſ>ML홙.°Y^VyP Z<-u({:k3o|;Vcy .Q.qiQEP5sXigO;xӊECLҬl<>noqg=}jPEPd`I0pqkxSQPo~FN룍Fzz"ڗ'-M]}?W#]ީ4rP2K1?E/O_ ٖ@ܰ9F2uq)qմrj_SbkU=I>Z^#}T~=lyEPO5Oo0K6kQ'Aϙpyj_mn/':`v?S_JQ@wTG(mI$CHF@=GuM+IE.-꛶ʌybֻ( xcX🂧?5#Q$u2/vڶꖑ`xHO^O5P̺72Z4-Fd^~yj:&?Yx<`=:GːTP{pIU+xGW:.ei<ԓ(cA#~- ԵiKᝯy Sqk#3(t'8##{׸Q@4x~m/T(6V0Nhc.9紵^ !  6fmai%$ª0+C>y_W-ܷ6[g\K9x~8IxN6ݼb>I@3ݹ>xjm%&r=SwS d<|=p=w6_ƿ .{ޮ3u$ pT1ו9+:(ox[M Ͳ%04Eh$}+ =O|W}{dŢGDRe { Xx:6ɘed_Gz?|RaY$< QtG$|} }=E|ŨOQ 2Or3Yk_=hpn0{WQ@yO2[uY"3O>"zeRr2zV<)~x_c©Z!9<HQ בH] 5 mc9q }AE|)} >Yn/| ?_LRW}8bw*vʜ7>+t[b#)2Ҋ_ּ]++w1_ΞjG\Շ]}@}izElǽ[i.r uTP|[L3+8crf9ֹ_i:^2e0}\y z5QEVJ|Kɵ 6J]soT4L1zxnE8OBo G}Q^A( 譾P<𾧧w%2DWl:f0P0s@$.>FIA-h23֫/d<ƒOŽcJ?Bo G}WwEerztY/n.`l$sV0k%QGv K JKM(>"i753Mϼ,QUwՈs\/ ~ơg}[*'ev(P( K}B{;[yh升 `WϾq㯇Pд4X]B{v pw;}h$bYvR/)v ǒF 0$ׁ\F⟍Z%zPЯJ#I.4?x0ԓ_IQ@xMxUh!qef~YI XVË_>\> KxZƪ@@=(Ǿ9#^1m:w>.h~CZX!vWwq+'躇jVoہMؕ=My%uHML &|󏂾|%˻ rR_ʞjI1:袀<,|=H 7_I}ݘۺWxv>,ލ)0μ:r= آ>b_|Rg3ZH,eIGmqcS]k?QirVZ0&Q"Xd+Frq3׬EQEAqҸKđWq'8C[QN: 'ƣ?}5j?WYEo_q&p7Wz}V\i  V#'mg[hvR#!u[R4(wIF-#ԍmA2b]m[3z5Ӭg:rkf'_[INOvbn(C^!ztS?ۻg|X]XA ɸaEqKĿ/"7E"ީ)<= j;iէnܒy~b?BIz STK"t@苜d /mZ}wM$w"D|C~~5TP+SU8V " ~z-ůg[;h7Pm z^5>Vp_īID0*c^6kkZfi混HPXUTgr@(+\]{O!h ?MC1kׂ >6PNKclp죜r{_ES_[g8)?)OҌIT4Pٷ6~ 6m$(ͿCEYWh}ݳVꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š(Ꚃi|N7mU=\/[>>TFӑk9S9tBS$R 8nim%J{\Isq Gqo0M)mP3 Rޝ?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= ((((((((((((OPZ?TQEQEQEQEQEQEQEQEQEQEQEQEQE% y)ԔP~Jz~y)ԔP~Jz~y)ԔP~Jz~y)ԔPU3(((((((((((((((ѫ>OO֏%=?ZOO֏%=?ZOO֏%=?ZOO֕cU9}QEQEQEQEQEQEQEZNlPOS/I̭ Np3*A]sF幃G-ooS֥PEPEPEPEPEPALS(hS(hS(hS(iFJZ((((((((׾xKVo2A?R>HgNAl\9":(-PEPEPEPEPEPY|jJ(?%=?Z'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(^VX8~'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=򩤕pN(j*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\5Wrz/HNT(((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@Q@Q@x֪U?Ug@ GY%{j#Gg#WU\u]yG?O((+R32< ULk*O钼m N}6qGFIX𴌺ђG#]u`xsL^u(6zҷ3N*9QEqQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[oA[oAQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPV5 V5 EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP>U>PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPVT*[=S QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š(((( g<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEPLx{hgi|2SXĺq1ݜu֫hm 3;**ǩ]FLU% 8jfQEH(((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((((rǚTrǚT(((((((((((((((((((ܿAU*ܿAU(((((((((((((((((((sǪ~RPJ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((No +Gq}OC.ާ_uQElyEPEPEPEP?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= (wtڻ~Uw&Okxbɴ!{\e/d= Uȓ{q|bڥiivUquVv9+MNwAEW1QEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnO_nO_ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[=*[=*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]eDmmy#77~uHs[ɝ8jꋓktKEV0QEQEQEQElǏj[?Z@Q ^P_[ `L#F*$Eʜiu]4 [ibyR!q1ZuQr崧G u0+#(((((((((((((((*LjU*Lj@(((((((((((((((((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( ( ( ( ( ( ( ( (-?Tg<U(((((((((((((((((((m#?Tm#?T(((((((((((((((((((ǐ V ߳ vq1|m^f=H ~i`V*@ ˸l,}Go@MS i۬+\82hOO K-c^0Y5 K=3NV:i1U>[>e:[wʼs:ZK_ n@e Rk} !@D^ ~T^izn*y}5}x1eaP'B=3ȣFnݬK ٭x ~ux.%6P@o`l }ND>fWv>T n'ֿZ6m@?co >Z6m@?co >Z6m@?co >Z6m@?co >Z6m@?co >Z6m@?co >Z6m@?co >Z6m@?co >Z6m@Ʂ7mSIxU Uco EYxQ6 Uco EYxQ6 Uco EYxQ6 Uco EYxQ6 Uco EYxQ6 Uco z?ʯh9f ( ( ( ( ( dx?CO<$XnnB|\vA݆Dp_Ըg^$VF{~4uaZƭMq>'NJ@"4?"ؽKQV$l y /??rk?,cM63o+B_mw4/g;˟l0[𕇅$Ss1˹98~7y,z"c$?d~Ux=;081?lgz٢kaNω 1fѻ"wZs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?S^aa j ʂ)A>lg+,<[aĒCZY\8nD>^MzW٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP>0֯>.\r{m.9fEP@U$|Q^h]':՟˟M CNY\J a^٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~A񆵨|W> D"P@LoZJ :֟^48J4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Eh Z>)jZ>o-GA?s5N|RԵb[ xJ JO?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs5񆵬I;딓Okb%R6> ++<#kGwĶW pcew@h>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-0e\RQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEw/5[y5#}hW:U,u|i-dW8 _{_G5muoy)=' ?1\e1G=t_:m4P l1+2^=NN[8/>d)*TMZ/_⶙Q9A,WP[NHy#YT.}<Z1e/i7/?]oIsVDԤX* u 4Fc d=qO*yj5F&K&RX .<+ݶF͂A8MHF^GTEJdx"s~4]AӬdefl4@EPEPEPEPEPEPEPEPEPEPEPEPEPEP}޻XN`lm'5%>yYk_PζMsQ]Ye1G!-SzO]푖Up>v4W^ p׼E^.Y`8}j G1~vp,s8E((((((((((((((>],'0^j6 kF{KƼYҬ cC[In &dA2#\POmuoy)=' ?1Qé\KgQg̅%Vtʃɯ,]8⎛g)8 J׫"V5Z G137j wXIw1k4ϰ'U7E4x7hӨ5zOKr4~yF:uh[][D%xGW0~x@d.e⫏]8⎛g)8 JנSS inϙ J郃#VD?k#@cgo(VS(/mciU `O=GVn$bZhoM罀צ;Q0TPY4ʸddCu?hhB߬6yHU{p^jR o`HEy^m {Z噠 8qӀ1֯OP|?wow O2i͌n=SZ( ( ( ( ( ( ( ( ( ( ( ( ( (3"sco0D`9) ku*ZЖ:>un+FHz/=̿UtV mn"3p:p_[Ku|RUgL9ӏ~(z\қ@(x]Į?z!m3Y%s<#~YFPzGy{mnFJ\y?:^cp?'_zG3zn^==:)xAGH'v)s3O kD@EaL koF\PqHh^ p׼E^.Y`8}h(:𞡨7eݎ{c?BQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW}ZD[.)N,?2z9A^E )0{_=Y?)X@R$v9'יV(|uR}I2/;Gt"k& G  x+CN03O%ߙtRēʡJ4gM~b?}C_,mc@'&SJω?פWSJω?פPcDy??k^?/W/cDy??k^?/PQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@C9k"M+n<8R<=iOS`z%?(צPx>SyPg_@[ۼacE}Hۜ䞝'S̋\.:7I+L @[II$U U Esb: 4] › q\3n=oWqyyiς]u'fr4OmX;9?ɠ WVowo ʁd:V-vcU1 *78Tto5P"gxF zu f.nsm'Ӡ(ރeQk=>qe '?@3^YV%ЮXi4K15@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@yխ04KXoŸHs/l^3KQ |PuՙNA|Aon Gnszzym5h̷U'ԓ"Q@?OO2.fappߡ'b: 4] E,I|3#iz3ۀ? [%ӵ=*!-e9>[{/PLzf&Cߌ~4{ª/O\g?wnhUN=7H?M¶wZX$sxgk3L|_ak\ɦpi6>Wl1f#PEQEQEQEQEQEQEQEQEQEQEQEQEQE>#V$-bˊS#9̽޶X9pǪ^ czeyG:u E4P?ԀII|N<<ȹɅ~±Dm@ e>bIxePȥYOBP;x+CN03O%ߙa)W76A9?p7&!IaRlG-#NH%#C דxiPh-ͷ=z @{k4f"grqd{2`1j6sۼ1BK2q J|{ ϒ޻ t`> qW.>ɦI-{M¶wZX$sxgh?C*ץWi,6k4.-&?L{ (((((((((((((+|G>oIZs~-Ǘ Gs{gzy^ cXmc=ïu_? {w,h;sDm@ e>jx:ys5 ] ?^мKπ'lmȍֺ)bIxePȥYOBW^^hvWy&EIٱhSa!ү۞n5+mǺr- ψuTɇg_e~ #NH%#>^cP,|Swb׳FՉ8P݅x1\Ekw2,AOxcRҵ ^߽"mٷ;n\}]/4kVSC, !X__ t`> q(/ U8 <6O Y_AkybL3c2Uņխs&]@ a@EPEPEPEPEPEPEPEPEPEPEPEPEPEP}ZD[.)N,?2zmc=ïx5?)X@R$v9'I;Gt"k& G  -ƭP6d_+e'CG"e=<@X|;dl?<@C~d~C_ª/O\n+>iFVCWؐf'mﯠ_&Hq^1?wnhUN=7H?J2%_XmZ2i\>ZM QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVaf&*ct>qVVF[- Y] #k@cxWE#֧W#*6Cpq((6@,ukRewrBp:(=blEcG C-ſWBHZQ@nUʲͽ+}+b(iոR0rו>Ǩ+2nVHJ*3I7hu/sW7WTRN,̅Fޢ*izU[Go5ϩ>j((((((((((((((-ͯLUv }:(W>wowZn-H?Gzע ǸuGObFUm9^[PEPni͝-o1cdTuJҢԼ^^QI;2>ikRNҭ;x}OTPEPEPEPEPEPEPEPEPEPEPEPEPEPEPaf&*ct>qVPF[- Y] #kEcxWE#֧W#*6Cpq( Zgl⹈"i+3O?aYY٤}Fq]VjfJIY;H[P[ :J[[ h^BF>ޭQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEkalign-3.5.1/doc/images/Balibase_scores.png000066400000000000000000014651061515023132300205750ustar00rootroot00000000000000PNG  IHDRMgAMA a cHRMz&u0`:pQ<bKGD pHYs.#.#x?vtIME , IDATxwU7~g z4KATzTUAA\PT %BI(Ι:~d3Ok k-@rK[<TsH<TsH<TsH<TsH<TsH<TsH<TsH<6*"cL=aZkyvSZu$I BkKDq?0 ݝ($Ea؂p\6'ySVD#ZYǵZmK,K چw6uRRfY!>3B!1DkV-BK$I8d2Dߟf( @)EDq_V}|~0JmU{Ӹc1+J&y389Q<(;8L ('IBA8DZ.] jZd8I(D7#DkR E4PMZ+Ӷ]MR\& >;?X/9]Q)RjyyC3\LYԭa>ʗlwZt:],04pH m5㘳^ 8<e3K^)eIMAkmpc m*J|~K)V ym֚7Ʉ1fJBDJ _͙*:c88k ðJnZӃ棸:R"5qԴrS.}GcLQTrkc"AcAϝ84Z7:OW*jnKmmmAٝr刨ZiR<m,l)Z e3pɿL&3lr߄{{{y.d}Nnْ[3 Fq{0 HR<4>!p}`1O} bEl u$-BJj*"J8ֺViTDV^MDLyB;QZj1lKrkkk:T*÷z|8hii<3=RT[ze^slx2J|rbhܽAZZZƎ{q}k_ۻx'R}5j 7@DQR)T.Z_yBl6;yٳgRZk b^"Bў-ehjjzGŕܚ~`VF7J{.NOJmƭ^.H^Z(wtx12˝tI{ǿ jdjZ:M5nV*NzQGprHV+ bq 1IA`ۛ=_?_\rN;fw8s\___sss*It:24ڵeSt:e8=ݳgϾ{/^t8=8C;O<1.[lJ\.ΛLDJ)5dCf 0ɔT*ŧzZ~O,Y_W.2eʄ N>;.J)y2JQvY:ލR\.sϟWKڊ+viQFM4c9#'LWk6qD>,= ÐkAJoe˖=#r9'N<?e2~{`AXkֶ 891k]dɪUV\9mڴǟ|'|r___SSSss;y _bҥ˗/?~|ssvaw\___65jJNj|>o~O?䓯jggg*JR&Ms=O83fP=4j(%܅CDR)WU~!~< ,O8CS+cc2 Cx;$I~[ },ltG>Ocx4qVG>'gϞ]ժjs+k}'? RąڮJkmOO05g~j\U7bw{O?O}õny%,dXkūN!#?9A _vuu}3f]JT*5(p饗6\.s=lՅ[|90 O9^[ZZa|vqǟ'ǽ]bxs.w O},^;p"DtWx/vժUn5.^^k?~1=ވ n&4iҵ^;3Vs˄ןPd2gܺr!?p^>'I^0 oE^qZ>sl6(ϱ0sh >SvQNkmTZ_tEf~pJwRʏ|#8y}M7 =CKðXP:7iOMH(?ckG!}UWS832\Ǜ4G?%KzZv\p[V(8v2ǩ9ZEz|nɩTʝ9n x;.59FcMw\\K/mkksHfnۃ Xx1yG :4^xښ5k^}ն6OpxAcfOZGQIVVKࣳlٲv%٬F4Nk]vb0 ֋/n tMjLސT*7yCR'v&/9 C>Z%jWW'|K)9gKs~37 q.ըbqy?xGe2DD'pLE;Pyw?7MgƎ篹2F ϱzk9溁׿8qӧwuuzp{A6 !=aBC9)}}}Zk֭R?Bb>pR_WK)Zqw\SN^Www^.爧Pr&f}ٍ6G.Ds+qyW\qWwqn.x Jƽp r8c_;oNss^\B"qRA)uqKhۻjժM "Nrby?<8D_Z[*foG{~9CN9/7hR L0ᦛnjY;n%\¹^3OQZ?.i޾jժs=׶1WV`رE]1WWM&{ǗeޮѣG7&l\.טiIKmr unN >ԐktO}SJEa6Tcмy5K^l3П6EHaG!KҙgNk8 az|߷r낇~׿5}?cO~!]Z^zժUz룏>$رcW\i⋏;3gnКs웃y7*3Zn{N8;~鶶J$ɨQ\J~kkWcvttXkWZO?O=T:v]/O~oXvDTV9cq?}_b_vjVy?~JzoRT>Zlٹ{?/^% /;c޼yt: O?s5j1fѢE .˗.]dŢyVOz|"I)ju„ ˖-Oz|_;+W>Sͻk^z饕+W򦵶V*|O_ѣ'L xWwvvvuuqO9nu'3rRwi 'kD$T"x{—T*]tE>QJ% o[T[:[̆Z7Ð_:t1|Mꨣ!N ٳgS֮OOOϪU^ºn[$ ?w___%y]cg~XceOӋ-j|Eqerx"o;z~=f'zvil> 'x5<;s83Tdssx]}|>S ~_r ewHNqtwݸ|>+?ܤ1 е KCneŮ?x=ns=w%5?;vkи[… ^ы>Q&9#WZes &wuu{lsN/2W 63ύ4AG'sV_: )nt!?#Xt q+|up+Oq3(ֱc>᧼ oDD{ϟ?68Sx0Ϭimm- l[o 0"^9?<ъWvV+n $Iϟ ?uO|o~JRWW{{)[[T\O7Ym ۮ\*+V,Np߈}ه'xWsqO:եg?ACwxǏ>h}n -9V]l6˕8JΧGRq'\.wr;xKNsuä_җx?>gn&Ý'_jmv;Ok6$\WMSڽU!>)tz]w2o< WSOuiAپ$IO>TW3N`cJ ϱZoCE]\Ga1'^~ƨmhˡ:႕GǏw3-]scMYz˵p`ڮ pĉ9߸z|:J巿svA0&ϱQ˵^kws'OȔ)S֬Y3|{׊[3ύolwAkiii9(ì?[s1BL㎣mR.=X2L/͞}Y'%dƃ?ߘ8cS(!w+g=~j{775q՟|OR;찃g/8;{'tsp0t8)M$Neچڇ~؝]γֺYMh=#x{Xr%WEܱ-mpXqV֧\PPk__˹@\VJ._Մq˹\RQss3W|'>vڽ"7ns]1-Z/}(I΢R=qgO?=cĪ x ')Aj5N!:T*Ix(STKKˠ_륵} y %JBJi9蠃M6BL g_xs?]yxz^{ŏq.Vp(J{ק?aW8d2f5f-tE\.ӟ4lЇ‘MOj a0=Ck=|Hsι{/ 0 .2kmKKKE~8'eqyNfQ.sKA׬YCD|[p577GrynvaJ%SZ!ƌ3k,Z'A[>\.w1vm)a !xCpЦ;.>`wwѣyJ)-]}ߟ6m^rĉ~xO. ֍GSOr5vѣG/_C-${8gˇ>vۍcBpGj֬Y---|y>.cd2^ ~Ư歭===$ܪ.te]0~nz?~JiQuqs4>JZ랞|#th欵B!pϽaD9/|ҤIRJUUVEwr_x|klK yX S9P* KR<\^xyr)<[JE97nܸSrlEy@=CT*yn]Ű\.i ^T*Jj2A)fx ^2N?g9j T*e?x9̝;w+9.RO,YODG^z!EQR) ,X, YfA=0(ۿowq<~x l/ ^CWy{/U)?ND>yB9)|;V RqK!&"'O0aP+90=PJ/]d83/MDr=I~?<${0|Fc"1j$Zq}3Beݏw,_g}s&vtV}+qW#RIwAU+O:$ CѺ>;GϾtٳgcvwwK_zꩧe˖M0!zn1>npS(vm=C7~&'c32 +!QG+(rC}CەJ%Jqނw[PCZ{7.ZVBDmRTTMMM;T*(k? 4H.BpqS#E3gάjV x/J?~;tR5h"ο#lې瀵pl裏g>J0ˑ{!3}vjrexآQuwwΛ7owu-ommj7s(1x .;S(2Ɯ?Oy* <3Θ7o^>omB#ϓBrM3f|͟ԧn&_&Io~SO5jx1]cХRiŊ˗/g~ŋ/YdٲeEݶy^5Ҹ =5tjEGbjoxQJDaql֠moolWQV[t /c-^gy9;'kdVJq‰L|܇y0_/]a&/X.;s@֒JxV5\uW׿xb'Ir_y/sT*[nu]rźƘnͰ+Sz$\ÌU.cƌ;1J%r\(ZZZ8$ZH)on鮻Zr%q4wPR\i*cnZvGvifjkk b+qIR!1y::\t_gkm\^]]]T'똒d:r\.S=v czh\< :;;]u xTET*"k;pnoP}ûd8X7Oi,6zqb5 jL'jy߸NRfEќ9snٳgs7j#;7^LƥR:9>kΫ|rNvwwOyRԲex[?G9餓f͚uWwvvNוRa{ZÕxXw&v52رcx?G7q[Np{SOa/q_ cyKDGQd\qnNpsNDQ&4\|_b˷yL3-rGˍfns$Yz5ψrOŢFoԟ'x+rJ&՘SR KAFs;xT.y s$.g8fJ5g{bHVޮ%zqy>̃k8>ܶX"GyO5ku]ʼn1Z_ΦCk5kV&o>|s'JQF=7x#:" .]/ٕrc Px[(z/8zj"H7WJ-[l3gd2tDeN_K.-JDT,T*]wuwy_ɓ'R)=a\Ask̙3yJz9Jr;'o(ƏχϽvSN8SV۳N{{{$>}}}IDQ8@ ML7hRD8z) ^"Ð_oڸ7sܸqW]uE.rSS.2eʔ]vev>}.r)^+bDDw%Mpé,BpSa׼y"Zq755555xG}GM9SwkC҄BaԨQs- |/\y啜BNR/E]t1Ǹ<.ׂ$l;瀷 :JiGkQ=T/?ĵZ}֬YDChҤI_?Ossrm8NrpQ{{.+(z饗xņύ9~kiE:h~6y a'?%su/N;mIF'0 ]I(k-$VCr3b pm]kkY2LOOo;G]fMSSOAxG .WV\nҤI<ZVվ|>?^+No4N,(\έwY؂Dž&NéWj3u3MMM}G|uW>kѢEiwsOf7׍ӊl; 0/Z%&N~K_}7 <@*<ׇF]w5n8"D_#f*?_~ʠ?9l;Пr sJ㈛O>OWyv}\.Ǜ_?ꫮPۛ 뮻A0nܸ]wK.gq.lXMy䑼 Z$I~߸fP;M%p r$vm1)YǧO<H>RD˖]k/Zwgc3R twwd2ZW;d2K.8Rq_=W's빥7f<-a(8GUVQðqp[onKRy睹RECf\ur\/&Iguuu}_v'"wmݖ.o!;nw,qwm7Cxy _Mr]X8G8E:Cp"㏟\.[kG!:+ 2>8&h'P;gztq~nY>=ͭ$gRTKK=Nn0䳚Jҧ?i>"S)6WsiKwV=p9׈Z{kbxwUWr9k-zOOoym7HPJRl6ݪ9v.9T"gΜ9{l" 0qxҤIH尝Rjԩ?J\.Lc X)'-ZDDryўU*kRغ r@0Jq}TREy3]iN̏r!Xbﱿo10"(e˗WՅ ~39蠃vqG_(>wusssX|GjkZNDLopR:n cǝlfSTZ#PJql+W[8d2B\q/r\jrG4w./6~x\.կO_FTgxֺX,~_$BقK/+3OBl6VTG}_ۿ[.B3}t֫\.d W8.R={>ɸ 0a>~:d2fe0 $imm t&MdmjjZc-Zdύ0 9ET(0 bȫ$,|Lzq̻7v%_A|N9ɗ\Wqկ{饗>\pC/.k A*o'* /s=Bjjoo_ziӾqUP}`OO?6m' !~G?O\ݨvίUTx>k:noo!RJNp(ygvq'??+J[[[wwwZqJu!?c؉'?N;tg 89ںhѢN;kvaf+JSS0aWNy0{.va]m9Z. T*W\qWU^loooss1CD|p{yZI^{/QFYf=^~guVRh|Wo_jRimmyW9 DkϤ moX>6S8'+sK6_q|[ɡvK8I)9u7~j>Zq/)ӟtg}ѣݜ'c=<9eRJR>Gj5>|/ʙ8U\=@>R} _}ѮB/|y(]9Q---Bpjo0 9d\%uqUFT7ͽޛ\GIC=tycǎ=Sse]&MҥK,X0{srTۻxJҗgbx!Yy;Vl\.Pzǯj]]]Z6-Jl#}}}SL\fM*_!R(8y 7llVk}gNASN9sy:;;ygy 7uY;^{%Xxܹs?}) /'08py]p\xx!u]4?}gGq;::֬YkW_}uTs'o}[vǼ\.Su]GDo}s=7cƌ#8b̙&M4is=W./袮$Ix5k2̅^HZR[ ORyG~c*Jwww.[hӧO~GvatW^y'Νjbx7%ε0~b,q~М jEB+1x'wSO=zg_|ŧvZSSԩS-[裏>/booRs&L7'.wX6u̥P( ?q|D!mZ-IO>X54rI'O;GTW1իW \}7!_e.>뮻P(pC|~ĉo%wޙNyT=wrJr 7PJ͛7qDQıWlCD/.B|W2/twwe5}݃|~э477s/{/C&Gm~_^-ܲN;x[Hm7QFK|[FQt)4qܒyNSwrsk-'CG]7\'x\8n^w?eztcU%wpԯ~+cLZEq/hB .t=x+C5~)mיڟ|>֜_Whoo'E֮\uo&ډ'3(;O<_qϿ+nW~8B0m4K.s>vɓh*D7Md2zm6}r~EQaE]^}T*w_wv͘1ז8y x}vlvĉO</w vqGT*Ł 8"rL&_>)?裏QlKp"5jĉ/g]vW"Rvttpv~iޓ|4yZvm7gΜ}cDT(8Wv[;o7qq\'?~mpS. FQkk+Z~:R~l65lxTEBJ%?^{-BxVJ==Cw6x~UsssTj kԨQ?=|>qt:&YOENJaJWqQ*˖-k'(֚'Wuvvr?by^POa!uӱonAđ\.߿t \;dXkg͚o~88;ĜB 暙3g[ceٲel?$T rtPpFODV+wN>axY?;nSVjWjoorJeٲewPyvq8H u q*NqwW^_}z׻Vyv8tvvzꩋ/lkk+F(ՋpGD|rnE+O~͞=@]]]a~"wfG?zbQ)5(pC'_x~lWkMֺ?|c֬Y~_w}SL+Ó9#]??z(;p wsk'LJ)N \˝Ἅ)!UV!SO}K_?~d(hnnϧ~Ov:]?bSSS\O{>Z^{mܸqĉy]2I)V^SO2xf8vgPxqڀ$G}ҨTo;or&%J&.ݶf͚|#|^|T3DD&MJZrfu0pG|K)ݜ*0֯ʎ;v/;ާ:T*\.wTJYkΝ{]wK `$,lSx9ajT#z`#N9c7A9 䇹vm%KƏ?}C=t̙ᄏ[^7pŊSNy睿/pܵ$ gk8^[< 滞>̼ZO_hѢ+Wq<~va?{]cN}5woq?<7x֬Yny%9u}yy5?sV[jկᄏ\.===cǎf}{gN:4|_ۍLF9.D·Ϲy5\svuuXwqƌ{)awt:ͳesq>'ֻx/'"]_(NXo衇{իWXb„ A|vر;zhH)r3//s3T75D[ΡmmmIBme;s)e*rf\fs)7t?yK|5j„ \tEK,Vcƌ>}7iӦoǎ_gl%IDs4LB&ֲ#0Ra'TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<Tsl(21Fk7JVZKDqqL8IbQ l`_(c\[3km[z-`gh<:k}NdKlƘrDcy| 􆔒O(`RV{<`k>:B',l%SwlU%)S5~ºt]$Ic\va?)k. |ߗRA|EZk>y`k <ϥֺX,nUB ujjZnzSiklURA N] ^NFQ$aFQߧЈ9c1 |EcRjyZX qykKp?`kDZ}4$I1l=,4I\[KȿHkq$ܘ_?r<zc8hB9BƲnL_,(NrҌs.W "4Blw(8\5GCS"np Sֺ:uI\[wR;>O6u ať&d( y 0O@~݉` 6\[yJ WR )֣{N`dм qO(>Q4֪{Ќ`k>[>:% _8:)CJSW q$n3hQ :X˧'<=[z`+xJ JSb|7A>o[8h[WO Ӑ{u ~Q_ar(#I\߷ TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TsH<TbRDQx: CE÷YNTjgD$ X,Z"*Ddm| V??Rk5$IDZ[7aVgQV{`Qe S1T*A d?l(L8VTOWH)FSSyrOEyhii!r~EZZV(HQTVayT3J}KT-+As&(2p&p—^zP(uQD8y!RZkyE*r)886(Jʊ+ƍW(jZ:gj5!?ZB!8HOKIFXiGo5޺aN !M++HZ5p +Xŵ-{ iM >+ʁWSBDd% /"6DO[Xu',#7ioVͺSK!A÷7-ͽ?ӑ{T$- 2wD%yS}q>善kFd߶VAd!O~u漙~lk<Z .IX'1%FT*c:$a0RJkd b໛i"6WIKR ) iҒVHJ֒6ZA-EQ(cOHy%ed8 s؁ d’RI)VZVN4gϠT:Ey"I$a\}aA4OX2q$~3Fj!R$Ok16-@Vȷ[ٯ !!m'=Z2Vha$-JKd D"$I$,%Y# g)EVqiCO/LcL$>1(_OC2RƐZI#Hi+@r4 k&"1&- d_odF!CIeIZ2B6R\G%INBZ~ VkaʓR*"eL!U}VÐrߓ%YG*iN=丱Ya=K "eIZlBjiD"H2Ғ$|(+-y<# XXR"ڷ:c4IO4RX! U!a(kF?7Mv D)1(\6 X677s)TPVN0A.৞zR?ydN6c|1Rӧs^x-X}.p)ܩSAE… RZá\.GFܴc>,ЉЉM$NB҉'|tBpC_Bgx1DF^L {FPEq-D+!U{$dV!ja_j|!2 㡌KKJHkE5 +e# ,+,3CɁb`q0$3 B6 9i5Crj/ ]&QljV$dQL0p b$&֐0qEZCp`u1sy=bJ .7<Hr'4?x@Hk$$I[g~f٥$Ya5|Fɷoma侕dL}m0lo냁n}ʷ݇0שdnʦ}`? +u V*#VD"2rD]gVrOX9NxgjF_zao\\[JwknnX{o ]'<'Okx"El" C3gbѢEZ-F$ɓO>IDJ/QT < `C}r<YVHA>Ya "ϓBIeI&&җ"% P}"Zo$eIJ)-I+ )IJ JcI*!=E!|!B2щd ) ITf%HM9CJKDIFHKHKVP$$DJy(k-9[dIJkFaD|1_ "%Ħ=Xⓝl=81JBxBIAZRJJEV̴Z$HH _-gcgzz$%ʔ!1dhl"Rn5WH R$B K i%$ @IB R4PdD$ȓ#H əaPDf b#JVM,Ғ!-c ckILRG’Nڊ&sOǥz\]y+&7#Z+%YO`%Rz<QeI*DZD$g'$9 OT_nP$$ CgQ|50Z60Ƥ]vEq}kmTZj~׻y"m<3^zĉJիWcMFn>&ߘ>}:"z{{{zz8)8¯7$S0 $}?ɄabIZU,’1X-))%Ir7uZ#$Ղ%kVRJIRbjLQb2^6!+(1VIi Zdi`DnoZXkFg="cH*a$ABXk19[% kc!b?Y ).[<0Kd7fMVZïb^*)<@`!H1RlX$!R1pg>O lUB_$ ҼL"lZK\j!$"2Zk%XkP-v &+Y˓0I  Hneov{=p'wmڰSA?W}ie<:Mƒd2(-Y ",Y"aa? !k$cIX2fSΩdZM?d[_ VX"\\݃Up>m2WwOBװ\Mr06:SJ^7i}sDZi!eM}1Rm.R+)4q\d,2^%"Qbȥ#jDD$xƦt웘{BjC%0ܧͳ1PTr%V2ҒiJy;,%[sl)ʒ*iIҒ)ㆈii\WM]^<2DYK"%뉔5^ ()Z#d@"Yp3D t|P YoT| H+ )"#Y҉MO˭w*#P&8,lJMG"kR"T(kq{D~ݪ~n]o'΁׭ⲢT*z2Vo 3B)@%4G$R*1 2$AZY"++)Z&I6R"޴qo CbՕڔRom ^`RLD}T-Jܜ`ϧOa}[o_Io6Ā[kS[+xrCkf뙹_aPS@кRZƑ,>F i¸O"q 8n|G?~*&o/sʇe)"(/!J2oQ؀d05I* 2""r^&v4O1O$7^)}3$ )_1IEDF BT^ZGLUF-F2Fd P"HJL?c ISn*kP\*,%DL?螗^2}LtqݕGiyϿ=!.mω ۗDJ dhw %̹ǟ  MÒſ-^J۱}#ǐ2kIIDAT!me%DEC,/ZR~dI + ]>v6.U}W#fHCloM͐*$cS߂.J0Qd++zV^$O+ٚnmHհ=%v<]4q,\o?FD2"O&QTH yY*g׬6*qW^X%R$#+%Ϙ@AHI2"OSJ a)Q O6"Yʏ%&П8j(j5Xa%e<"!gu(kՠV+JL"lj()'Lb#T$!RR)?YiDJk3Yש6"h-)JQoI[\("N j5N0dYE߮3G(Va+V\._ݕJ{o] o!kNT{5R>6(&QY+RRQTK5Ga[SaQT K RHi7AFYmIikZ)Bt"uwyV ZL:]m-;"U Tvk u&vTNXaJ!kU NiFU*q(59H%L:1G8H*sX4yJD?O$ IZ ֳT42~[+'- Y)֒ƌowR$:$OPBI+ff80 9Vc/P[XKl6 ŕ7-҂ NZWmqȩ޼.ѣ0qrNoe!DT3f ڒ$IRQU*uZe)4+Ȑ5+(F%"-""2ORֆBG$IDkIi!,Ie5Dv9.ձPZIZXkJ NH[Q,#dHt)Z_LGxe@Kadbӥr۠DH KkV[Y1f2"UH.~2FV+٦%__kyM kV$&H$T[(D*lM SŤXGd ?5$R'HSPDVDS@FD ^qZZ+}BVfra&k_$IW^R:%F[K*JTzp$Iyia!d"(%ch_$\yMvZ:MBKIű6Jhkƒ1oFM [2 i-XmVSB/SEyc)MYfB$b J=cx">fd 5J&Dt$MIR)gmBDBXFJR'IH*k5ژGh ӡKA:I5Z @ROJ3aEQSSSZ7 ^[(J se0ҋ"mCRҒFYRGVBLIw? kI$Y-=V2C’$D!]&H(2՗ȒB*)ȳR%gZ I W'+V=xZBAv$T"|#<#b+#Oh6W+ajF&a$0H "k>2VPb)", !HȢw07Rf\.NARKR[H(OH$Y+DB(j!~䒈$y"QZMB$Bj!IXABYY|k9 @DJX*UUjkDդgllH/t!Fp:AcP)HB$+H(I__xʦ}[|mZ9ki,%m MB 3&dO̴ EU$,3~˳䥓xsޠS`3 YaH H(# +VKc|#R+Kr:ERI%:g$`’L!7ß<`_V #HH(%KPJJ|򤯥g6eaZ9M2IR$$K’""̦6,H VjByҒo(FZfrAIV[zʟܓxKlE\AnY?L <[ǝ$QJ '.YDJcZk8ʬ*Wa@Q5@`a,x?%[?O @n. M#ׯe]K]\׼[D9>8EFyG'WFĉc:ǜc|_l:b/}KM y饗^}v1uf/}~ǎ^RW꯶DO|/q(Z 7nk1Ma}wi"H`!j KfT&JOqۮVd˧X8ܵ&A Db1"Yܫ(`Ε֥mT9FIDP.hgH !HCU" #bt혥Y@[O7}~ypчJg hrG]MBiMtTH DKXnkgY7-;1ʛy(S!Huƒl]e̪[T?hRRJQjv=+~OI Ӈ!Jk֘2!!*tp}:u=URJ&%A)!ƨ'<᠇u(7Kڍ+f-8H[¨n~OBhp9ʦέ4c*j9ޯvuRDXmH ,vѩ1PBR 3 I%%JTxWA[Q=q**6Ru /}9Q?BoOC`f>Ԇ9ϫTc738/|+OI3l6[//Cs| _h:#0 G43ѳI}O~n [E2@HhQL,B1zS~oqXFKt3~f?k(e-eI!hɛ>_  yUS[cpf+$̋G9^3N.&=H %Q]$,45g} NRĩQ=5niX.3 ռ`׿L_m8>nDT(YۊaֶZ*KhynW5Q.s PtpLXiRMS)߾7 |ǻwZۓ7ZHb;@5,}$>6=f@+459ؚuu2AV\:]W6}E+Y$s:+À1򶧻{|`iZ-O 4MQb\^3֕fE4'0eb|S` 3%P %0Xne z<>c2KjZ2Ln]7[4=.Uz@K+U]vW*cܫe21'`.X-a2l8˾1@@0 ˉy> 0SzeHj(T7:Otеv}W_5e;Wĭb]I YikФ*-? ~~)ZHH%%"DTiijga˷ H0 4mLq1y}D}å}{ <#YꭗT=yhϯwl]yg%Wh=ǒ5n{kN-p%(LmǷnݺj*5su~k_~t]^KKoG~oSr?gſ+rvv?of~/Esϵ鐖G gi_iEK/'9ڲr2[b׮d;Rd%I;ok9LGX#"ySg=ΘusWF ]XZA؛ܢ^%&<&Dgчl@ cbgش؆2ZEgLTl>G$;xy $TGvD%\Nd~䛬b p9u> 8{o 2uS%ЬdcieXzwen U$(;3fmO,מ*JElNpw*^ۛ}\m&Q8CnPr^3hN̫OK]5kd}&z`Sv.ZK+ş# y_Mw2=G.'ovG) U1鮧8O5[n /|_+տWK)$ `"W| `,aHC@R\ G!̪gI ?C~s0m Ń +ACk!<[jOYo"!,k Jx=WCi0 a9"hqrTB08P ݨAAF%Bi,9bzv%7n%$Pbp 3O'}z=4ߡ] L ӂДt+/pG. 4t0KBΓ+S?'bTNyB6@\(V<4,џ@fҹT=~ڭ398݋2T8NSa@LuLYLӇ%t,ӥͻՐ~aP s`1)P=(%ܳ`Jdd|^w+w|L^of7+y5x.!-\ta"JY .XP1Yd]ʐy4T(sXY=Daڇ\ )N"R ˙>;{Ǭ[`:|o{ qIu?QF^#/\}sj8,Hfuwi"ǏmE>,__}v)ѣG7^{ɟo 붩?3?[[_O?CEWDOOO_~o+|%aO0LWՅE)[?ܜT=Y=z4l; WV;..,8$wG}F#nS&jL9FmAYy6g/,k66g*d!'o6n.3|B1 é _dbR|2{b++++>-- x2|(gҷ% ]"3tUm[/Ly邷|+^8( 4dugNNNNNN_W9}_^^޻w^\\WkfvΝ___ݻwGs޽{W^ 7|饗4{O/~s3,_ ~:J/cJ{M{ϰt$1s3W_cy1:g[6nT6Cs˃[{׳ސ++" -'P>Ik1HPoH)"!*̉yy}/K/Er9΀&7UEα{ H` f[mɍv5r7<[? o+3k_P=tKEZDЂ) ik/߭g sR*pT2OI:;;-<#3l6gggDI.EGD 9ԧ>_oeKwoy 71W_}W?/Bhx7OlS&M}\|TE*-`lQhA$stCW9ma@oK5뼲2ᨋ,5XztŴ؏GR "i R>8ҧzᛠ;&-YW^sRQ߲c_UW.EMpDnݳ^euUS5TE.ؾ4D!$'GqC̖wz[ϙH $f.X&Q(Ey~Nد*f@] n<]Z3Mr{DDD{IMDM8q4 )My;w"MizLuܿ޽{uZbeeeeeee忟Z[/5uYR0 d:ͬM1^!F(C.:̧'!fI YgWV>k$A۪͌ⷡ `Ұ? 2>Nw}F1> \lgi@W%;`I$oNsWY i]ߍ>sڈqcq{sPҠ`͡=P3Yio}7D @! a++++?>|w~'es~;d/?Nr3$clz+^%O!Z#~GNj?;1(-\@&$搯: K3l`Ze0>~rv\v6%kI4tvkS|~!MNn$85.'G6 $n D$D[6yj_YY~xF>b<"J˺mɜ}f1mq7I`PԼ Eb~Wn0w4#rٽO`CdZJ++++)W'g?v(XCƇjŃc!]*cɾ/U#G*=h(lܕb,pǻ2#1=_ Z1^`eeeeeeeeC̈h`;+(it&`WdkQءyqr2, m^K3\AI&0r%Rd<1]YY6|?{Z9c7YX ϼ6bl9 &ڠW+[iZm #̞Wq.Փ/u:mO}oZDqOYEz3n9ϛuD;w^&%!.2,u:ff(90)s'vr6w;Om;`)Lѷ++++++++/*GܫHzR@B" )!ҙ'LN}&3$w~wjwEw0tEm^1PʐU [^H &Q2z:ʋRx`T.]$A?}4Mv숂!*bY9?<Ns~ۿvkq~w?z{{ҚL43LCZ5Y²foiދܜLV*؄6 pN/P"G~䕕Q306M9gJIA03 ɛ)y""w;qOP>?:씊 )FW8/NN=4ۼca{`z&: J}>ďio y}ǻ3a#(6ȽOGg o4.Ғ/mN]W[;?.k_-gB++/ϸB @J172ܰؽ w?nlcΐ3/XTM̨Q)o{݇<۩ӏ=C JO0i. ) J(JkL k~.`mVҒqT/fh RV3&! i"S6DϒvYj&Wj!iI&Tl#Xj\ğ`OFeUq._|Y57Ѣ=&R"vajVL) d?ƴLT]b]f7F5f+u{;=8;c(H)p)e:C'Dk 3מ컡s3u#j'ZYYY8c@)6@Ξ@Q(wn2-A*_|?q_o>7Sw3///[?S^N͌gW,6LW61\ ?XVceeee&T C ZuK(Xn6{6G fC:l&R#Ky F0uy6kXM,V t]eT{+lvT/ 24t(iOV0(B$[~m()D @fkݝbhu[Gn/y'/]phgmJ/['ON_>ܮfTĊW rap&f u9q.~yD?M4!tJ@46.bX@D&XMETZ|͞d1Ly"#e9{dIm s9nyK{ɣ{8i$LID|ˋ J^+ N\ 2"@ s۵λ|N.r:63GP}TB6ϳɨćb$"6}lzv)xs1n)O=^mw9Y'R7owVyٙ]Cb1Q[dUXYYYY X2H4 fC1h]`dړ-ۈ !!tВ<^)B]_!`TpkQ$[dL7CMjodFd4F;M Z:FCBVZMi\9Tx00-Kԕfmh5i%:eYKggЏ5YpaWZR&0"JnyW{߹ԐŶs&]~w?Φv_|gWrä|~nRh__gWw1F>X[?9Tqbޥ7?SۗM 569Qmi)Me OUDҐt 2do:eS5+##&KϺi!+=A@)PO y^ޟޛ+jS\JWH")p:_zyzz7/a ׿n;ޣmUwݾ͠(yM|y˼ ?A21]6þZ,;9`lgСQ+͒81Iqэ`mNd'AvO꘽fCDuiCrm]YYYyQfNV2.Dx`qMP'vߨPj{z,>p %krsE0ř{ gO Y2!u0:ʍ hc~nB`1urf~"jX HPa V27ʢd(h&I`~篨mѷj)3*,C0[vṢ8`4Ozb +.s͙fu]P\bU4W0suo=`gGE$8}/-elέcn!6o~3ƻg)z&%эQ38}w2c?u>= ·Ca_j<:!!EK Gpcm2R@@Ɍ6$eaqz5c]yzH"L% -\,9BCVKP EDsnKlFP}}G[F.L Y8ZlXstHM =$ܥk-e9DN6/'愄{9m{Cg΀G_DZU޴oĽ7捡+'覴lfJ$J7{wbQG q>DtgM`Sr=e1A#˩5ìYeA|7OuC9ۖO@i6eN$ʨps%%\>f;@I,[_!@%۬iJy|NC<: g)C`ČMJ䜈`b(]Lu 9=Lsk1v9]S/~?V3l#R:MhHQ lɲU_݂aY FTC$-ܥ8|A"MHqqNtŕͥ3mpfbd>l6ղzc룔D MH" TZ<]brC@].E rh&Xh@:@_“U&ԥ궹e `Plup|H ܹGi}$&[Yp8Oȭ w}NnWy >p%d89ᚽ0,DOh f̢sKdߎBp:8>uY3ut#0&) A O4!pUҬ n<3!ǴS; GjHD@*L2gwUdGjz%ԟp:igL݄TtWux\,6#D 4.3#٧:J)Y4LWOͷJ++++++++++gVceeee`lFZ{ 8E&bg3ڌ`̀^-?V֜3DЌ(RxAK6U󽗮,)7$2c6=Q)ެ:M`Nbr,U-u ""M4YЪd%,he6)Et͇Ҷ ̣V2[zq8yҕ]@3$ ؠdJjC.Bb^w d|ZZЇl!ˌd=8J}eAssBZ4U1 #PüeI,Bm?}mpeW iH+&@Z{S+Y݈Nq 1!.K1]OftvxO|黟v׾%`0sPڙ %J;@p@ xoW_wf2al7# LhZRdzDg˰9fdRIdaQlK-H [,zs&!J&ь Ni%a1Pd4[α.ںEn !w.cfXaQYm]YYYYYYYYYY+++++<Z&G\hExtRüuÃ%j46bҒp%8y: GޛKaLxh`S\qDw29D5̎[2ۜGdy2%ƃZV8JJJE<[7wT6 l|xek7Tr ܑdLp ]wf: +KKi{Dz{GtJZz m|Gy,&D숁R]? ei*q\ mwzi."J0L{nLf-xHҞAA\|Oqx|eGxEldsZ-\w.h3[/Hb+++++7BDt]!@\9UԊ s"LhuX n:M (q7xBe&n6a>_=P@K-L&LX~|Į&=yuWVþS޻}pd;I[7S'9y܂}di_o|͜{֮)4[yMH}GO'愘~2ːSι1q+c$;]///,RjMI'^OxQtP 2a [э,-!*\)Y>]ǯB>$h;iW1'PJnfi .)Sh(u;+@)^)ݣ =al6Tj9 . r3~*(H Gdh^ T ,z7[1y.z2n "aʃ ]9Zh*u|Yu$Ț,{rS'>x݌% }ѓr5xp^3c$~+xt-{M[CYf({Nzwya!cB"]&ڥy]/è3xb*[;VwME[w>W#{vtr{f绹G=W??؟ޜN^W7S)gV?y3~*HRdoNOpjq4[H 6t $F|Eh)|yh4|n *Pa:T `~0q!X Iz @GU@,IRbv"4 ˴H~!˖ʱ/X2ӪLUm8Dw7|k-۳`q0bI@ II'Nx`zOb>pMk@CE0J禽P0;YDm0p61~Xf&t8ĕcX:lk_: vxVLGP |yWVVVV>2] LXw8; "1-ͮ[؟<|}[`w911|d<9{Vkt)3C)/0 ig= :H5K֟n/2'#?~w~w}+ޝWyfgp:yۓ73w̗Nnw݃o=7_[o|g|0!S[Q/4x7K3\iK+3 rge4ŷ>i Lpl.h)U1|$ٗCzh/V< u=bt~p` Z2r,.8_1+FV(@-_fO {DEXi5pzc ڗ_{nwZY2\0Jv(IEe0c1g}9596Ͽ3=_r.9eISaeIh+Tmmnwoqd'6W`W8\-:ZU e49#s=igH[O !,fXYYYYYYYYYyXuTaeΚ Xv7L̔@8|qTFݳ=PgSMyȘ;1d>}N6C6y {#BMZGM7qv{6&90*IBἄ}/|~m89vl7ws|v!#OIeĂ(Q2n%K y>Φ}'oqό=vݭ N݄U#19w P =*'COO.jvI*, (I[ Pӧj]L6Wη&|k{w- f.(x,1)KF4x^d|T%Y)Uj.&P]CDRE#'Sxt/]{Еu27H`]}@?ymh{DhI|?GUXy9V1.iʚ=0͈+++߃ҁ NNS>|`@k/rY$fAgqeH,KH@$׿At8dRu:W)MǾ쌂CE0t(*phVl).g RLdFqoMdO !.V0WHKU!f$NoR·~ۯl^1DL"ūKBK ,+Ltqs\̺|ԍ87>xߎ5j v}Z<6;Kfh?[t>x8l.590l p L&L yju:/=կG-_-|s v1 %am-(WvқhюDDV!C$0Lص_ɯw4W]$VV=1_Ocj 2$%O$94gT;+<]Neb{cdr9&;^Qz,bK!1#8%<ܙ;5) HC$3fц-Kyٸα"cO=Y 4i ef0L5gC0OڝlG?;vX4hw_K? <@`wͰaKt+xHO^slci|N9췷ƨPVg& oJs"- PL&Xm'@9AvPR:O e"YxVS.c`G zqvX@Lj gR.x*-,MYj9vsnF@ 'sbOӛ/a`~w\#bd:Ǣg ^z{P*7%fXu*u])W ;;_f)b&2* Nc(AnM ] A{ ȥֳe=ΰo'ݳ]_P8~;:=;@_.bT^0 b %GŽ lPq´R2З D'2&Ahfy?{8j'Gܾ!@&)OK8U#|Bv;g0n%L €l&beLXDp_i^M _ZmKIQ.zKDjpv R@Z&`6g Ա2OQH*͆zw_Np`z{\G:Sܽ+9 1sH"KرC(^<K:tߗOZxyDwbt`|?1O[:2#ԱQa9V^pGHҝ⭬\Xhcfq9$ζ=>}f'D|R,sw(\8}L;dMqEsȯ>}osH6O1z['\8/\slD0Y"H:d϶++hFAA9 ,b'JL&&3';S'&ECZ,Q]&蒀ӽ.K2 Oi`0fT\|y[z#}d!c3t3??UbT}9@<3Vcƴ< Gv"jZkXS0Ӳ®\ T`Jr^lW^0{W;?x"HdK82q)G)MQ$WB: ԃ]:p*(( Qּ *-%z,Ǖl)۩[VaJ1b]NšWfÿ3\n}Ka'7VV' Z=i>̻?'OfCnp@b;5~_oGǮ.1'8f-*+ϓ؋UwP &"@n J)te.>ܳC{f"xe&-VY "@y&ŷ| k!I 0_>~t9Sf8m(.jG*"9+۰]&)k!s·o<}V@dJoGUXyq#ZsxVKd5`YX,YWVVAø-UR}`}M_0`0Dt(@.eom8bIjM 3^a .vu%e<&pTy$6пKp 98)z`4/ f$A. C=Ƕןy?DgBT`6CNr{7^|8 ]x^/n6wCt\$p6~ހta&v5`C")W " ,~q [@Ғ%bTCȮRJf$*L7YkN+A ]d5#<68J+syV)37}ooo9'ކ-#T9Du^̋#ֈ02Xvs͸sd:m ..{d6I5e`GUXyY|N9Ku9p0`VV.\YY!.aTj w,0s JCx! Ae NiHr:ZX>AA DꁄtGidVej3 v&ZrMC *۾ꈺ޻v7/04Zpǡ6O id"B+/&`IacD>Q4عϰݠ=as8jVخYYK%ؖ~WL%=p3$܀`ŜP4>),ISuߋmlpH$5UK@CToS$vi; 2B[}VV>ɧ:gE r)>q}oJa : x!מSrwl: P *aI!`i+++++!VcF$4?9EP3;O j9VVa\ӎ1.0LYf؇u9Ɑmb2" ruZnB Tϔ\ƙI3&)0uR8t K!uɢuyCEi4 Ij~e!.@\q)i=WOQ8 cv4HH'ZPAqB?l1®ؕţ?d-m@47 Zs1Y1v0S̼yk1Fq`@ T(`8k @AXsiK69m O+Vχqw,H~1:Eևy()[|S o~ħ:J0g9M-d%@2Э|Y8W^`i!SLvXՕ嬌 ~ftռjez譍GǓb@y7i_i[YQVkwyͽS4W t7%EhIi٦% %ԾǻGY 64V%ON|K'۳i "$_RH! ̼} 1xWD=H3v˓p`n nvV(]OЌT m[(=؜{39`D lgzߕy~ڟOvH\@-Շ#$\qIb d0۔`)k^S5OUܞlsvxb@Mthڳ5Q;{_ī`fr*2$WEc(M1 f:?ﷷAUB @iWVVVV>2: 6LX#ߍE1>Yg++Zջ~d:']T*,*퓳rKKa db :>*.FO@ as2&ay7 ԝll.abe"u0!! 21L&²NuNJ+ :~oKpsiNW<KZA޸9FWVsD .8&xfI/ &kK ڼ\f10mp@< T(ẗInźޠf!TaFc 87+QC}b4P1ߞjdbo>x0NCiEh$ ;W?7G\%:GWy8.W }q:z&sCA W>Ż~/{26}8La$LXGUXyIђe&b/o}N?]Y!>wwd+.>e $I\# ()bs.9J|`3u_, BIk P2ӪZ!e8Oac<%[p Xlz@2E!!CZ݄93ɳ{Dw*[;qزXnK,oiʴ2m ݴY n 𸝳bSsu=FBn0[ h;Q/KQqCCVF1BL+8j dd7a!)39dPYq(!)弜Gu껓Qxm۰0p8։03yGj2) ڝP)d@xMw;MSD$+["Gcx/Z@ÏWk 5 VIFkeeeeeyg9V^h\OR8d3`(3׿\Yy1ry3S{j ;t.H7wF`yx`,Q| ڽ/.ƣ =+(g.PY-\0X).kϿ9Gתqj}wR+ #K60$ sDX>9D(:?jeqژSL[pt2+/sʋ %4!GN6bu^oʢf`f,FRJLrw:׾ յñ/7D2jfOo"#iRVγwRaǪٔ_2h㱻!b_ʦm:I9"͆HbJ!4$` ғ@ 'RvO=(Ԇ9V|y{U2 ,U"f#dT\nn7 Zz<8Wu~ZF- ReI-;[ks#"3uo$ 9P@@>H!ohACpDqDrd>뙯ևcYVfuyUf-$NeEF;gbH { 2U5 ({_L~R&vIl}ru`LdH@#.z4?cα4-I/?>W;s9 a`k"2ϛ tyhugL^0UGU]lb>%`u _ sð/yB'u *p[7w9jf!H (?NS9$v#vw6HS\:#B"\~SU{G&Z, 6I!<˸`7 y$'l޷r@ WQ6fτ נ9G@$bk!_XIϩON@H!@PȉPk}0Q P0rfȓD~2 Lk) %pq:9Q;<`|@^}l M'ӪsGޑq}x0ZVJ?hpN)HdYh)MUWv̠Ta({E%my. 0>[霆h̍:M8 .#~H\ T pIvthU.- hz4 *Hy,m-V;:;ʸn4J P-"yu9lV6mmH^fr)1VoLUX1v`af/.ʘ:`x+|  :&Ylh:$9N$9cm˴r$ʢP3Jf8eEQ&P4Ef\?I繻_^?9j] gKd5 XLjHcI YmܝHwzmc٠ ߾VFW?fӝq {jxVgiJKCdD'!YLWPն)@ƼFvv+qʧ߮yzw,#1sb^:IrJu:%'^1ouA||d48N9f/DjEgc<9l26q^3;I2b1[ _ $v8'ONדFBkmHNG$LDBQ@GxSboz"~Ĭ~;;㸺q,/Y/foτXͅQ=XW$PzLu8sgH7 7DQtȼcL6HɫxDwtp .߷o=Yu"`5-A! AwO+D8Jڪ mVWZY;THNV{Z>k \V2G r}g9p'`TNb$.3&mnx+@RH!fpPBpp &kiY4Nc']n(Eo\ ;`[)qݬ!Ћ[e8дO)=$ l5!섃B946Y0HQKM SNNVP9g1G)ƬFVJ+P Ӎ"]o?qqKl 0 z;Z Zغa}v#U\FkX Ә(xqRBi C h@BPc 1ݩ|Ƙ,u7xCW0 Bps Pz'qNwd\HRUH(*ޛ}PkW8O6}$=2enifmfuF pˌ&fZ7cRIz޷I5J{3yé qhqh.ýUŝh9"/"K)!['4fD @[EFTqsV@Z"z?})/y9_ dvsEH9emIu@(eAs1Qm rN0569yI9n[s/l͘oχXEcIDATW[V,` `FK2` !LC͓9ѷ;;6TO QN/^;zWy?YbF;g}N-JE1$ f~GOOj.p#-)6< n[RH̭xV*=޺˲vWuRZ28]]"Ɣz"%f&C0`&k;ve&\&۔R3 0ݭ;S ta{f}{g on,Ϲo951hNo{cP$mfi;/EUZAԹ|7dC$B c-$t }Mf8!=jROEۙ7 KrиO_uϝ6u>$d=;^q P$G g?`wvD2+/'gSsx)FUC5\HF ĄzJlh"Be$1֠Q mYӸx$ՋGq7aSwL 󃴮T-9Li$HNDe|o#C\ 㖜m ;ʏd̉[XK,4d>k<ވv[K=S3IDt!1+>kdHJUW+ECR'HPd0ѥ5bǬŎ@4Ȏ9#6!SQ|Nx$ wqA*BFzؗ'Ga~4J2M IS5%A7Oʭ;ct: ' 0ћpdO:jLS~Q,;7pH3{p=޹G/}$jrL$Fհ1!M:ㄠ:嶮MF=aj47%,i۩xTYQNۛAu[|sgODx3a9,X`U$V<Iqt9$Aгxt ?nBkJoU41_( {O(Yȧn$,ZH}~R~}:);^K9BF.j8Lk鼩7?pRGkt*lj*cZuE5&L4a832f6_Ƌ/p jlvGi? =W /[ZR{S$$B8)B1"J뷯 ,X` Ppoi'==bW&N zG&@n"@uT/.Ǔ[kW:`EK|aX 帢SHl(?r*Xj!G fv4x:PӸW%0]RTc;`Ԙ6۽v]ND)J2o| /6yS47'֚z`XOCBԲ46$hWN-`!˴q q[ܱe(E8yh\Gr#W+HXɠ&hF7Q\W`hj$ aQ͋Sn 1偠gLwGwbq_uN#ua\67C~*sBP|)|eLGxU.v?OCA*Lj%2=ZՍH JpsQ>6z͛͠cOx˃9%YDv(Hz؏;10>GDLdD0q,ؖodJٵGG "}i%IV"psLH2eF:]\$wNDRQaPG0 'i}3-T-pqN/'GwșEAc Rkkhĭg3=eEBD$,xv/^4ex{F AvOV1Qrh}ƹ*xy2ji*Hy)t̰W2O"XqB$cV^ݏ \Q8k mF| ⥹ |tB^pis4*_zr &Cr(.S{`TOOnd[Aևl2Dї]ؕK%Q?dGB <`%< ylgcB'p{7{jH9nTWZ$ [9k$&_|2;Bh^TB=(A8/fH!+5Usa[+7 E 55搐7`x#s}26\elka.5^zt Edc MD)s6eR JSϑ9DGToQ9OR=6vu7Ϛ~6L嗊cRM?3Gxnrgk"g$GA:~;]׃O;Yًhߨ%d{]a5t6h}FlA.Rv'*dU(ЍuܤT~gl`]ϫ+bg*=`OS )6ٍ]&g="\b)ClDM#|lmy ,V6b`~,9zj!2^"d(<X4 ) i FrxOpINtYN7XЂH'S_^Ҕ v{`(B{pE>u1 oDK* "0G8EiUQyhg{/٘릷"AӜӓ=~g?굻7}rSM HtMy+j!b]?=%r!{So pATM+tVU fFc, 8hۖ:ڸ;˱~/Dm< _ |~%'WAk&NMgWыn !c-ttz^P{4 "/:tҞTTqh ʼnID1Q"$ph9cSf@'/:A3mp ϲ5`-X50ӖӀH^Φwy͐ qw|X` ^n:L..e GN܊l0f@Q'xŪUZm&LJ Cl9{4¹_TSl{I4g5cDV:F@gTEYwPAIǂC%w;V E A ,1\W b<]N@1МUoġN`%L?zr9N !&˃yW?p//m{:ݭ!0bR][60qD"e5m6F37=(, !'s13TIEUbJb#SXSVgxCxR@حXɟWG::ze<ݯ?FW2tsÅQ[* vRg,"uO`[2LO&!E}aj+A\^ Hpc"<)v=x#ܴ?SʁC|~-9TWD¼z`eAZ?%~%@ZypL$npϚ fMϚf]z/y/}՞J 9$qui܆x&)p PL,>E_6 ͳ ,H&pK )!G g'B8('$dq;p@Yc ,x{nm\|טZ"4Z \1s-b&=zql^Tǧh ٥t9 95e9]Ra#Jw>_6;4US/צF4`if4oz~$b 1@bճKaȪB7'G*dD+dDJc:Tן<Ê`Tk7-AyT{;Kr`[(*q,6*\[m+xckL{#5FXE}&̑2W`<-rx{ _btMB%{p}x;bʘ@g?g':0oKq}:As68mQBx7Ra= CXox@x=c]Qd+XJlK AP@R S@,@ Gp}V*D4%U2Z*/NRD rD.ޏ-MO9.ͦDưۿ3^jc mbA~t9 7{ӃI\mL $8۬ gxRkv򫈕Yf;fÅVED1FuV @2 zztlZRLܞӰ.!s$8}ts@yn{]H&2!JPNצ-Ҁ)$m};[ŶEcKp(,'e ,xmOB$sPP$bW}՟!!*`=H jYu}9@3 SR;$xI ǪP͇,g*1I(2]2Weoh>32,SOhj[a~kaeDZ*;xeG\Kж3wI1"cӝt'؛̔9pwA.7SE^9t?ҽ2щp['Oh;tW0J4"?9y(y őZYiP cV}lz^u_p@ V Q|u.vWxLB/x ffp8t]V4V}-]r,<ǂ ,Xi0s% q10d2L]X:`+`xF̙kGu|Jh=Hh!Q유1a8u2H@"Re1˴&W?RIFUE$jf YV&trW"|~T5W*qہV-0#W76ޖ3N#Dj`ÛwW.tU$ 3NB/] VaO^||0ZeKpjo-u|uЋ-#WT1*d4aVMlVpa)?<覛)Gzuqy`{ٵ@^$ROG`C ] Fa_@W~Ϳb&5;F2"Ň̩Wsͅ]?0Gz kǓ.h62w+* &s\@r.^Tp1!e@w5z|@&+??/ 8vɵ#eLw%FqH fF#pLB=pu8~/'D&fޫ~? 7bF*sO?Ӕǒ"aUxw=2?7X'TAUkg& q2˃4k1Tϲ4@jI9#p*(/!"MJiNͩȁ焀 %TFƀ{R*Fɝ ϱ` nW8둁;;/|f=; XSr#fG U_'ıdzsX8ódTV:w _u4q%3# ~58>O աZMv4 L`A[ƀ ^3pL 9SI ciPd w HtDD%T$50jOS8[ 10g3Q| is뺓N-A~Fo2wJ}+}?"eɩS5DԨ 8aZy돌6og.~ԥvfm׼&iFw|kus߹Mo C.>3.NqҼ([SN@ "$d8>]`#Ubv?V @xPe ~U_UX"(2 l! qƘy T`p&Pޓ_cn_wۤ[f>g4Uh}{bc;Ѯ3hV<9 |${u2?L *pbټ^"ѣǿ'\[EjZߚvW *bE}^dK*֡EZGޮ}i'E;zA_mZx/ApIE=)Iʒ[5lsTɾaf\Wޫޏ]"#e3И3$z XJ1O}Z|̞s,X`nEl6cx6k lqXM܏'Q P;o*btn吘 'C߬Ev1yZC{䮵g~cCq>s/pA<}OCע5zIO!X8{p't22C<[:~Sιܡ9ۏPߗ<8{Wi/˔2W} q_@&wnڅ9[W A\aRTHBƒpqhm 84(Φry(?y"!FsȣE0 p`dNiwWX>'F䧌|:]v>[ QRz aZaG6>UTi `^?|MebU&||_?|էkID<&R.a 1(/)y>J!ɲ{z3EzD6evWWo]og u-X`BJ P/G)YnT3rqJ 46̅HKKDjj98A|F8^gOBLbECVx^+Й0Y.{DfHwl<5fX umUIzM 3`R9Z=tT|]aՃV~쇧N$:$bOH 7Ob~#7S}޽\ī9BX']I97ɦr_g_o`^H l}NO^ 9tY-`6ǭ_8j~(Ǒ}PJ]޿u>1N=sͨԼt]2(5gzL߻ 3d9>X` tp]3}*xv~V~Nh[kiUtw߾P"(eOŃ>'\g~2!&dOF3'XH A/C. 8,θUZmav!Yb0US1N֟s|l HX$b2?/b8^x !&fY־?pN oLP 5_9p9< Ӯ/zer .@Dr;ܥ>SRXzeF˪A%hҥ AfUg^@>AC˫>WNW@F@`SRZcZ%tK+ƼrSjszrw. L@:5dW.כ@W~ƾ )b8V6`Н"W@DǷjzsf-tS7nu P@b弑UzHqo}@N5E_N;W\dXB^@L a<%ְ*l"fDy2ԈLbaVK6o^/(ȝj==K$DF+Hؚҥȸ{{cDTdNjN{0+*Oeqȝr.,Xx ,X0L>]OPKcGPD[y]z]p *[lIycoo ȀG7lr5aoW..!;j IFgoZljSuS)!3Euʪn5{6!x)fsqc9?2G٬:Hue3fYS$PQU$dUI8!!||hUiPOJl?jRRX^yId-R䣬cý:z8lx(6f yKt\7_c?=SJƮOk1 Q@om/3Og; ކVm!snYs<>ii5үnP]'㍲.yuʪnx?e5Uw-vNF#0̹h iH1XٍO:nyYC0N{J\#tk"P Wm28Pܦa[7™y$|TjIE50Jw\kX%c^5'(]Li8(\А(9Jn@8P8'?ُe6~Xp_0 ) IĄn>?XGbό;&{}&hjFEGr.S{{veLzuq_,X`@QN[MtJEɁ*Cyܽ7 @$NL*NŽU8 G[s獄{fr+qC$QbpO (D$qy2):lW%c xdNQ:U=L[) LjmI~e8=Tx`X%=W tCaGD-%'<a0C2W~aHпh݌9,]H Mpd;}t4#;(pr52CUoEq($7$uQP2 Tp[3\+E$Iw[Q)[_-[_ߺ@T8NT,z. ٣7hiiu ^1)M?(~fp2^à^e'Do}N%9_q,>ȫqT;׬JOvtg 2uk޵@R!M=3jHg HN#(AASkzUMūϒn+7o_XaH[1}֗ສW`BUE >SsI'.#1j5i-o bgl6.5X J ]*¦^UzԺTlT?rz\z)J?Mj2!Pglz ,X Sv z WrG5ۀz^wĝuǟl6m._p~n k Dоs Dq13<1Wh@qsV#!̔F*kOޖuј62V XwFu[ Vpi26ZLdJ:I][! 6 <r d`1i-.ݘVO%qŷБ[/x8QlEj0??)9U';7%~)Ff2,,ǁx?l6kIwK }@3 !6husdhju4:E6C$ a#^yd ^8nheN]<)Cc@ed//:"U҂)S'|(z0Md/ڣ0u[GN}1G=&Rk]i_b}}P4wHpp%暹3(@'F8xFBD}Q~Cv8_"]}WfEn-:Dqr th !P̝&#jHm%D@ʺ> C$N(_mC[{+Ovqo?zcI"yi DB'D q}0#@->/$ef_ik ߬EƂ ,ֲ !f'@yvgt8a2b3d?v񣏓{g.`ltC)^S?'Q[2?pO"DZ7dO XB7}څ뫸fVuǕnXNm2Fr T ܁J٩_?ϻm>i'Qm͇H ^(⽣{ַ~A&c^ Ի2\zfBIw o/<;;r17>ECuGxIbBPAHP2m!!^Ty\dU$E*UIz;p&<HpB4PIS"50^k@G \GKIAZĹ`ńBB#+KVW}fC[TDZ JfM56G#  T F<={Gȁxϯ=GIXmkqn;qO/?w>=[\a"rƭ0aTTˡG9T׹IW(c5(OjbX6bDnsNϿV!T:;GUZ8]_vbkʝo wA%9}AϋAWHp&oVH"r8JT`LN,'η޷:tXw9gP|^U-2BZ4YwO?i0Yn.W}\_էCZFɊOt 7Nz njmK@u}(Dx"$Zm9)I Xj"k!OH]!]uH nnnZ*#nvH|MYΤlIl5)I9bqÁ[:$d|z5VVOK}5%5V6cŭV:Q8;iW\zW LCf,Q!9aX]O?kr@ZդVX {qW'`OڿK+ֵiI9S F+/Ŝ187pv.kU_ |AD넸+ nau[<ρ;AxwD=Xx ,XpF Vg.@rױtG7+JLzTxF9a5WNX&D@SGR"դ亳b#YxSQӈO\\>4ܓ{EȘ?^=߾nN;O͂¸F?K>ίa'c |)hm@s#yiڧгKc֗\UI.bE~!Ml;>ޟ~"d;Q@ 9'7Et-z2eZ 8+ET I!A4 ! ZB1~c;prLLI1&/X`N z!iMb"0dV9"WmI\*h"! \0#t2- >/X: n*K d$$\ 5FB+q0B(0$EC"EĭRiG=YN 2*@Bu`!J/ூmNUty\X +O7G{=d,XWI_Ց>y'@H(hH>p׫``ތjҴGQ\wXV};R)ѫ2` mMSd7+e盍\-f%438|I%dDaP< ԑHCq#Rr0BXED{ sA Jv)ruEAI&pIr=AIF8$D*&,x5}tɃRGD"rSdD/-P Uԓs RdP\Z8uAp# a?kk)֩*05.4w{1s%sIqrUL$hYw7s84G<'hSU3<1[o*aɆA5L!^INB(ZaA%YR{g`QU`deDon196p/R\7sE!97T 9!2o"psy;J[|MEp&}Zj Hnq5F-8O]٦HT5C.eiXV/s,X`d t= D {:KMbV(B'u(S#SlzX>6v?5ھӹ<;p5E5Vx8r9]aRHK]kz#6zg8^c}C]9ƾB;_4k8u7 .-SֵF " :ʰ?S=FIrsv,q2j*vAތnv(fehj!bReFq"o$=C}P7mP~8fPPksȉ(.`yM1n.ݯJV"‚'RUJD`JO>=4Q:HEoY(ZN50/iU}};˫2ڭL.m{i^EPQ[vc[8DL,99eZq8t=MU\X$0҃k H g_mb| g2c"JC?_wwǸv@)g _ wfAQpH{4}L_{ ,X~hNxADq.u1{W3 0Ik-רc*^Kf/Xjieж~ (D"/|E wq|s c h  ҂Ǯ=Il;8B0VDJ9s"Bg,&@≮!'q1|ˋᆝe_3͇ KjXG1%!?{aQ|^8#|SrYw=1pJO`%GD]g'C |<7'}R+qq`Hmؤͷm D1/~ss'"^%wl]𹡎A1PX`1"wD[d\k7}EԳ"%SN2қQ })=}U =9Г4"P@y\Gql=qgE(uvcecQl8(eSML0sdE\sTx.%t}Jqĭ&.Ⴏ$-4iYQl,yw=C$;R:fL_:Ӳ` Έ9Skf;殦75RQгnz u6}J]|_m҄BDp"H$ΫOu}A#I2 IIu* N㷦 L01Ǝej~o3Zxր?N:POO6!LDcvPS 5=G@5a[~~C8Rr."gҷ5gޛ~ﶇ=8TSbVs"\O|"?hJ/{,G-9I#88b5q˶o-ځp )5}Wdtop`)K>͂ lV$ V p uѽх$JIIYCi1Dܮ^^z\o\X erIh63]5&O-3qdj#S1|+*$ &f/s,~*z "_9* >8;gZ L{j)e葿M3>JjfVian;޺)?㜷 i *'o:c  KPr;kB1osㆧE[t'O?gPփȍN"*Z \gұ4zޙoSqYoGr(¤Py:<6A?SӓG5w`bZ짱l(eY-J"Һѳ{W=6Xh?rFm. ={gۛ]7\(` 1 C>HW8bݔΎ%C̏yo϶oql3~Zb~tn\j<'{5'W:c<$b뢓2C-& hLf@Q/4# bQǃg>gCxNӤuB[wchu 6@[+e>31(-[T³#Wuu`_Jy34a<)p9i'VgZ&E[m6|Vt5g^Âg஝"~0ggAlܒ/s,X`/-NN!kk\3 vΆa;< (Gw~UC.| њ! 'aK!%$y% r9EgVȞSpY>OVt ?w-UQUNݮ"uH !Ns&ؼ?ᏧiLکgN'\➎XB@-U p v߿_w4 µt@; \LӺ4$4-ܸ^_'T~_|+&V1jT@".nʝ`dgSږpةQў7?iͷN.D'!?>ܔkFty`"b^燻+e4s4Wa|+#>;+%Jep9dF2Cf+2*DL Wu&kQnp;f(܌y3?@!p>d+:'XަG}VjNvl-! 7jN]W0v-" inwo&\CӴJ4opaؗXx ,Xp O:H.pJox[;hF%rFg\:T`8iޣ^vu@JN/\ ݻG7T =#%B`9S>")i/ 'o>C(q/W~;6q;nSOqx@GÊmM]\@gmZvR Qq0^ī2cԄش5NC 逮;ʫv|\9&ѥ}E;t;ǫ[?¯eWȁ8nE@ ɛfRhA_k ȱPhq;pǾCrHGG͏LUzQ ~f^}6߸wuM"!T# pY@u '哏a=ᠻ|NT*:%I&$ZzZPXi?wc_wA *BrMCѕ.3M TwMq}D27ΞluX#'Cs2Ai;Ǹ |cwJG,`3}v7~isfNa3BʃXx ,Xp==jz \%kALj1y q+0A 8xQUa2I U~߽;.$sl_ l/Gn85 ,'X?>'f^=T!0 <c~j_` 3x;%W[@Bǐщ6duY 7b/eW_7W}`Ⱥ_(9\3}k]a =9qE4(M04NJQc< ]wu9@D@""˶tzqd D@)F[ p栜/uuC)Au s>G[髃cB@C#:0VY+HZ4MPPZ%>sn)U9qJ@Fh&IJ^x@^StobA݇n蝺2#"z<]n ɵMqU /h-z/jeg7;or1U>u)c !GtkP ;$n@JrNǪ#q"]J/9 a`Jf㸷S?IuGGyp\艕F%U?NK}DEBy{OB#C`u[#r`8)?@zUݞ"k} ;+YuiqfGr+b/K3a86Վg8ܓ+yH]o]+?g|҂;nLݡvhL G"W_P8;6.X O~a@HZJ)eN)PRJAhzPDgxvOsԔ##bM%1A4jw7JJNi!Nx 1 *CJsϮgs'Wb҄E0&/p.S70ٔqx5iĥp qK~cLzOMk 0CX<85YS}~!b__v:MϼN_{pDRZV/s,X`s@܉-X0jMQJ5Uw$|I!~tfܑ@$0R#du?jDN~_!*jHT}Mc'uhch> G**WHpIH V D$D :ebH!2?XAV]ny>d dfG^@(*ծBw';ί;qOwRh^ 8qfN$>7!I@_ę~3? #Eg%޿Ȗe9p|VEnv 6Z!ug4FD#MM$@́D zljX*2323^7uw3;gﵖ߈ yF_zk~رc*iإ5~t:׏ZO>,V,d=F)~k8iv4embD &"8c8 #=N04gfܽ@p h+É`h@CR_t ]\Cb#*P-# av ج;#{!pa@ҁĒ"IE 5܇Ǎ}&-wb49l}U`st~)\,i*8F(z~BQAlAy-b'$HvG2񒼚P`!X'NuAV-i7C˔2 `ujp$Cގ N kKj$ wR|k % "?u~6!5=ӟ~ÿMղU"ydD :u@FO4H^P9Wi1{3D&Gpؽw`:ԝģR`~Cۯ0Xq,hĐ@ih%zt:Nt:Nq"C$VB490"Zb󀻳% ,4B as*m}Pt?cu7Y4/ NAxx(K潤'93Ӱ^}΃zt@ve??;O5O8֑DBdBnB ɠ0$ lzVo.ń0/iZƳ92."US;ڌ47yJJ9:Nt:Nt:'B3d&ꂐ:!U x:0 q2Jg$f '(ud< A.]nt6'Ay~!hO6ƍ j/ w n?q)5r!VG{<4 Ah7K͹lƲN+G:Sx nnˏ?}7ДޤI9:Nt:Nt:#ĐLJu֛3TĐѲ)1  pfC1YB*(:y1d)HWJ4<1Vo5di`Enf;+ O}u@TT+sDdv"8}"(2vQ8spK#iYmHի>3AHN(koCp_o_~밯g@B !Ձڪ DU@5KDpPY-L1fWRmƴUwqJ9:F`wA^]};|zt:Nt:Nx! Ó{TC%_Zz~MaEyfMn]lpPIO~c1 (͓-\xJ B:fHuPCP߽?"i*<扂`Ou]j>~Hf8Pwhy &hD^t ngy<}/o%];J'9:Nt:Nt: R@C iJ|_1aM7Ia 9^$p'U5V8j:j{)vV|8au:o=)n"v]pUӆrcF0 8=)-q$ %j7ep&ak.yhzt:Nt:N ¤:KexBC%Y$+jbIdMuIRBED AdW9bQ%#fE-%qZfcy0x/0X/??/VG[AgAA|9%3.PDW8Nd*PJȴ|F:9:Nt:Nt: @3qG8,L* 0BI,`ycCD(rO Ds* S3%$\ `V('0Z-!-\:;$C%\ G3$U$!nQeQK2Qr&8 LV~VM:(8(h H ou~-yNt:Nt:ΣFZ\5pT C\@,ƌԤfyyP"FZEʩ! BI#F> \aF)`S0kmBY<3[ tɌ ⾳& "wVu,lx49?mrd` Ƀݠ 4dpπE`jeXwsYj?j3ߣ]?v ^9 A'*0$ ت`?GK~r9&nw  逜Vmq"!%PB *QaȠ 3D&1t-G59u9'7aL0cH*|-4*VxX!NO}%>>k>Ȁ9NpER舄P@鉀}Є'}18}ҿjA9^pJAiPFKNE+h0f:/vAJOQ|L(0_T'ɿwn# $`=KOvvw&( ܳO&{R믭>=YfN:zwɭAaR]` &@:s5%r2y.e99LmG$ͣ) t^E`glyYu.WA2 8_[~5pWNst:N h8\ PgA"2Ir|aTH[eT%!bLR&)qAӒ 86'x —֓&kZݕO6ŮF€&ŗ..I<:ߗeYFFwZ\ڲ-1-" AL!)هf&@.8pAPG (+4C4t =`JBw56Sd D>ì, i>=*(@Q1@ YL~x22 KX}>$bSZ[:ڵjv̇bIe2R>R-RuS ƕnwU(Hz[_v:)R'{L_,`x 3xU+~]WLQFR64Cɘ Fb"Vo%6I7=\86B0 I[bX (gȥrN"1zdyֽOb97HMe̤ -+8Ʀ T{Y1`%3&R.8W^ݔf&"y{ s\;txdW6֞+|?P:}i8./zK X tdiUn5\}IP ]xG ꅦg%pibiXtdeThc,2^kwշtQ:2C_[^k0|k,]?wl+Y}H"F ?NSX/)O6Krg>}KyNt= ~ u_=ۘӘ0[\l@&^s$`Rz\b%0)-'w lx#Bnk(2&S[` F YW$@Na PՈ(V32ʦ?_}3Y*W@p h :϶N}z;RSՈKc ѳ&عgaXڍ吓CL&(kǶUŨ8soG 26ӣKr4*˔ $ʉ"D-C3 0I)ߩA0DMjI*5H W$MřT=|>,Ѳx"fIHt|@9EE!Ɩ%"8o2<:8CIAɗ*ap\̤(<[,BT `+D#Wd( 7qay _uztNzt:N'%9Cy-B1'CCUt1iڪ_Н@͋).uCK@%qȨrteqfxVYj8.Bp@W+lrCLA%w&Y,Hb*γNt+奉ͥ.Ş/1+L\g94ϟ|8hޫiZ[q9UdfmڸPJIA "C}8;٪2cѴXKϓi>VbUaI hqq<"{ѮM4F٠@KVV&'5kST7`uPڣL8u:ίP3bqk | 8 G9ojI.B@qR3l[߷Kq)&p%^̳,+YD0ZM||ZBA @`똑/U2_T? vK֜^L,r??s$'"RB4!+Naf'A5̩}/Q*B0*= k \6O5`aҍWponfai)R֛mXM':d^^z]r*@e#Hq'>^i3!KcH50̧AW@#e8ums>_z(*a`@"Nb+UtDBX!T.-rz{Kaޑ徲Тe:Σ4EC:[&!SHfzqA F8i,iR+[r8}5pzrNs:!Nu:wzt:w[ $iu3*+h7;<[6quDŽRUbwmEiLVE4$2%Tg Zg6$?TL7V3H+!\ԙũuy" _;#|Dvw-!e"4Zo|⃛aNv\X6}c=%]eiNQ3qnuw6_ަjդ/ӧꢌiI̊CB#5}\iK Xc5ϿG[ӥ,t̀CS|\H|-^?a" 0@IH1 2jT ^[2U8Yp|<Z!iȆ 5LC#O{|h",ZI $<C4zmdDv !ECfDr.KѝD=!TنM'Xx0HM"qi75e2I51#s*.{I2HjS$ VoJt: q.P5K }HBQfB*ɗ Hdٽct^#78ԙcGɯ]sՐpߕy码v[SC4(NEM WCst:N!hQ!SIQo آ67bcc Omԑ- rC?}վ=K4F1((+6CyZ}j|NE3GHGဓM&5G;f8 q&(~1-)777"zC|ǧL t}=x J -2ĤѬC닗ACL3L:!_哝ڼZ:r#8bܮv.V#Ph zLckga .ַƒ(Zn qM^B[Ъ? adઆXx{L"eS)p[]zN0& 93Q+vWH~1F eLZ2U+-7}QNӼB=tgu*% !Npf$U e!VT9L5U5&&YdH&_6ypwX(5gOT+c[ vwt:N@xWR*tJ!4 %IDATjvWs,%Nڣ1Y )L$}eSw2XW "ncL RA1Qȡo2*V9_,6QmqK\<Q}O?E v4?4pҖu8 !cGRF"oWO1`sY TH0;V^( kP[1!S;9v-^' rAKe~-_;ò`k%0͜,"mHsx9 #b%N邞meMTCS&}%J=c JzS۔bV&X=N븶{0k ý - YVHp0UƪT| @LΕ1P?deCT(~^ kFDˢ)ح"e 2 *!8wWAiz`M  ` :5 K C," 9%L9&Ӄ5\iE!f&ibV'g8؁|]7 "pǞp&>XC oWWo.ňdۚרJgoyo}`q |feN @. 5QP;wy}MV$ %tsA1klzC b!q2cNqR $0 6_vb6(,ȮZ.FW҈ǯi@%*hNW- R!糝.40 @ܸsnQ?_:%Ƌ?S|uՓBӜq_Wc\_cbP 9rJLǻAst:N! w d"$ v@HANz~t .]P9hduz=tʥ.f0WC3lƃ=%0$ܮ*&L:@N>W`c Rfo Up*'O׸9/6 UݹYGͧ4~iqw=p@As!1WrR)@r2GCk#j]$ASyi5 6d8ҕZpc @%PD´t:{DVln\_@l5X!yY/fgۋxt:oHȒ8m1pw?0zXo9LO6ǔbd.aA֛wN̋evvuZrv;._?y~ -{N  T]"pQH"I"gV[8INqH ݅DFQ1ٝF#BtC0A psWOowXO+k*O(AJ%-BDy|+)ÇD ıL\h&*%d 2̽: TBH~tDk5Ahx:P l #1Gha:W ?f2:U|JhXou:;7vdHd}ybCsT.Z 7l.IXo9: Kwz[-`|Syn 4=XF rv0EZ"ħ/1lSf?lotpQ*.hCX"MruT:92E̳0 l搳*/og׈ȠN_K -ܧ9:N)y5e54W ZS >+I+g+P#5 "cNVڤsLtEY$-~ZOGƘY c7:B*X(,.-wU!* !@[ 3'5&Cd5KVG,z s PB$ $'OY1rJ2R+WPƮjdދ}u]Z2p+mМӕH|L viM!urcr?J@ 9)<@Erd?_|-AhQ;$@z@IX-?Ȗu 0,äp"r|ͯ5cN!О}e˿?g??Z]VIJzg+PAyÜ>*̰Rļ'?͊'O׷7{ 5 Z) UJz)VO?|=ʸZB־@q)D$:$ %?zP@.8I`@eQJS mgOoA%z%;Kݗ_r,hں4:kM^V9rZWQ Dr\u) 9D~|u <7đ !QGAclW`ų$zX<ѡB KPz l2lTRŤKt'!NҪXڊ\˗}JͧO*@DL٥Dݯ k`-!XjzSZamſuuf.]FRנ!Aw.%"["C @ˎd%$Ct?Տj9l8yr/~ow~&=t: 2B?uÐ:Єev?EɚkݵGE.h, >w`zdSeP$pYf7\B0@@~a .uЖhIa $DZ-1"R*"WnR1hL}\\o?|ǰV!$V FGξ4L|fRp@rnB bݍ8EHDEKǽC'p8?i[@Qp6@Н./{ _18Ad G g\wث6<u Ou1c$;O|7ni;}_ZM=iN]#w~=KƢ!w4Ld-20CF"9}PoN*'$t H$uxnRHEB -M*9.gB 9/0&||AL ͓Q* O:}  dS`z9?gq<=pM \am6վzƻCst:N"N6԰YQf)bE@7(P Ex\,^(Jɴ R:-hw(0 r"㍊F3Z ԁ C3}\1zhb`Dǜ[{oJj`m>?O_^L7o~L`U"FCDVpGΐ 8|`ZP|Zܤl\sOD*ów^_k%gYd#Tza|;k?AvAĶH'83w@Gc~@:x/ 骕rSQ6H6V:pF{y5p*ƻ{J/G?x_gZQ[vPZ?1=oVÖ_|PR I.VKJ$Jm1/:זּڮ/xYzXuZB< *b:96hjjSRzLЃwziˇ1".˟pg&a#S0c7Gl/[϶o>Y 5_ q6#40ܾ\ O[뚰a}8KF"ͩMlמur4xoH\z3Ԯi4re.B;o3qzGP.m Ty.Rh<\C ᒎ Qn+׬ I7j_"?%̪E^wh]Y H`"9r(0-D kǪfV*p{}%-y|74ބBR@fI2. q&hg~Av@Lr مBn.tz p R@(>ڜ%#*YήJt|OeZm p|b6Ȯr0nmZI0Y8ϖ< b᭷#UAUIz.vf W/_\T0ַQ1q0Ƭ3QtITx@ RRLU72nWٹGЃn0q=J Ef9q5P5Cp-[=ZXyQ_2jl~X rܑY}'ax M`xoxF dE}/>bwv ,>Ф%Ɂ쮑<)~7g[ʿrAYɀR,PrTt9`εVLR~@1NypIdCPz}t7K nXgnhzW@+8\pPbDvdhP'Ŝ"`&!bq5O6=ۤ5엛kjމݟ4Eω̟fg+VgKmnpj#'YCh X#"YT ^$`pT.q b@sTߺdPԘZm0*H"X:Cy[1"IM1 t5rZJk 'g=yRQm5pdvf,ԌZ+m-Vl h*m5W GaF3h< 8,/61:竀Kr2SKR&ӃNwyPP+vpsjM能ct 'VJ:"xKdeJ{T%oǔtt:r.` 1{̪pqBp\ V-7m:vZh2Vpn[`zzXt{Oώ{k}>ֻ = PPiN{K_V u.?"`fåv;zt:S@ِWQ/b?pȱګނޜyP&O\8%dz@r& o0)HQN1e"#5btH\)g->9#t ،%ڃ1 FCGC$d%(& TY)g'(5}lo /_b^- GA˭HRݽzKDDMW:_31wk&تtpG2,xv;tj}WwZqB gD]r .pK[EjUX_(8?W4 E')H hr23G :]N"-(RJ ezt:Hf5+Twڿ)(LGgcV/E|34,MCQj&D┖l81M *ZwW?;˼O޷0ʥ!_6-$"2QE T_IEpKET8"DDRT}\_q!uS"opǼ09/=1-* C#z|5kݭOo(SFkMlڗlM$S$50,\V2N6źTc(|HZ!uVD)a!嘞)/TeD#!uKgRE-!@J,'Og9pK5 jI˞ P~-5"%e.l/2BWjvo`':iŧھM@g 6jKl}7+ @Fd g@cr TjT!QqNrob+s} IVWezP[b84/0 z+RP*Q Njf hi8Q4ZPMGBso% ygjDDf]9:NC@&YE]?Xa/@PZ!&=x_ܾ.lgO']9ơ& 58slE3YA0 s\ooa\ׇdyWېdGuHFVd8# x%fXLI?dt+ c f.7I9a2c3\!nq/&ŠcnUCnA% U@8UIX䪜O!z+`}=: &V;6yӇOgb2@Em֛O|[iPhLќ0et9̒I@?M3QEzR#y 1:MʜW>ϿSn'8j;o !GΠA7AdJ;br<[P:лp ]08: yLiV`9:@y|xBۍ T.N~"Hڲd6ܨ9#8d "f-  "A*ԋ2Ux~4^'r/z:Nwm^`ϡ#S@|=/>}-n'>zpX<X5 i/a |sQAɈs$(5l)\qƖcI@c~`%nSXb1Tb]8/sEAB&p؃%d ]S{~q<{ my?zyjwV<ʏgPUZK^Sj!DYJʇ۬u9Vʹre(#k1!›ƙ۟K~j=3j9%&y{yO%ǶeovS=\yU#0WHoW(inz.zH" :2E=j9qr+#N^Y86fͅ\~7+zk`5<{F) .wqiB8{ٟG (t:o}ĄHx@;*&)RǢ!0"2BߠThO!R' < #73i+GAeA!)&<9A:T0$Te@ @:Z-"wi<)t:o xӃbͭ)$ps)"<24 JUi4Ĥ, 7 HNxYWb1>?݌CЦ<X%c Tu'ɖ#$E@p,7G脞?f aJܤaH<{ÀzgufGB!/OҶ%6jpaX*!Ljp_}<1'q_4+vS?P ='\U`WEk`qߺ#eZH7bIʞ"2)(}r;Jv/a3fSW/?rXa  n_J.>3 LDfw6Cf>+oϮ6Tj o&"5Q%sg°NͅCT`wYTHZ>Xyy;V^Fd~t:"ż)42Rs$Qi`2| *zIui\ߣ`ΐ]gZk3KD(xUDhw28uÙDjVa"XD5"rQUF3^>$Dҗp,u6qt҅;c(ުɢ#H".! =o o|o򣛛Qzjy{58F֞ c3!_E0?yO_|<#*7݋'WQ*(0$AF)0NѨS~w3g[]o] Xnp jSEIb|Ƚɀȯ-' 0Pwq#ݧbX\ PT TDZDNoueSw5] #$jJA_?zF8:V&p 0$(ZT`!uZʱb!~=N \Z'A]%4S3U+m KWǝsyo|%R";M5@Kմ ɱ`9LP7Kj~ # ƒxv1}o`_ܧ4ڐQh5> Y#,X-&UsZݡi))Re|pD1~?k**(cG6邆dߥZ ydDjE85JD{d)\gb`U(pt2` ӄSNg3"" FD2)lT1ܪ% Xaj4szH )E/+ք7~3ٶL O[fgC| NaqUV;??\Vo++DjUP]K6̮HE: 0xYidY/TmъK)#p (AG*˃퐋8$#%P RmNs2tD,7fA(@ ! <^@`>F9^Cfvw^ nnt\ʭj j0v> eB6gմm_W u([Ԩ|,v9 }Iw=oRt:_?~}&5 F0ŋ,fQhBx Re7!B L.*$t؂DH qxo~'L_otD!eNS82.Xyͭ:D q;}zs~~afo|f\y%LﯨOKOO.1,c}=e(Pxp(U/֓<ϰɨ~/8O-oWYP aPȡ *XYBxQK)ø-a&aH:\c|h$*|.LH~(4p_!D̖C=8 9P2"Ek|&Isx'-׌G2,O gl |,-bΩ92W mx6rpEj4.JP  C,]G Bx[ZL v]>,%/|bQ]ѧ}z=n.6;~p 5@po 'EC(hŽ2c;˭=  銲?5eg)%f־WUG7+ "94a3#kmkH3h6fK41"Zțޣ]UfqQꦚ!Z0 sIW 89y?xAi F:9W<"oH,a]΋'.[0D3ٚ@%ڬEawAcMɋzo}?n!\<ԃ-+b"#tZqryGuZAp K`×ѩ7 w.׻}^"BD$SJd533cIUm}3+ Û\ !$tJiERhD$(BM`WpJi0f_}*$L"woسfyH`1qzS#\43&@NW6F& ߑ{wҳJ$ypB2/3ܡ1aEZ\$k%3SeVk"ARlQ)[WGO_^m?/$ao>i)FA'ڔ8 iv 25*a&p|=t*@)!.R #̠ȉ9ʼ] ()ckxXS /hs:.ԟO'V]b0Vc.U2!`>fq~ ]S7D8R rz6xUjy!#e=!_<+M6?tP("=qeddb*]muv|<"TpOpk25cǖPi5+#(@ẁ \:8ߑ^3#ٖ# 8N])jFlʬE(|'bUav`R\ԡH'BYgxWWcͧL] Ci0!DҌX ,\D4%j\+_`) SOC9xV=gY&NӮz P< Ms2 znqqmگ  pĬ 6hrK n{9ar)a@"͞ciRj wWjbWW}O6GWՋt ,6 1ʤBfāhzX8;-ZbȒ& @Nb.qƦK#@Z &.ժlRZ^_W+;"rQSڦ˧YvR Le`ZT2ՙt@ t 8}IjTWdD{l/j\-V=XHB]wV盻2Ȣ6XHjsT ǁaIW4 C-1]jRW"D5gO$"ACʤ`sVи^N˝$$ʊk3<ŒHӪ$FMNɒI렁aZ %1 ð@Ԯ؝A׫4=a-ANEOw|ȏlcAñx_@&dp'ՐBB섣,Zuv ۱K& j{̭Sdq6{>𒿝r"ׯwR3"mNP7g-&⒫)8?*6!pt#Kb!6)D$as{s_V`KmTy0iMvwCD"ZuRNG{pa`@f՝<PY5|z;kűUH.j-mKrOÕs;КV1[@E~A}3$[gj#q:% pݔ5R2T+F :F@4Y6 So%hGVvz75ugIr\&:AhB$$5gAGhk1FӨHn&r)IGthM)+їqˆ%ɨ*" h;T5'$NJ+QAgߞ$ɶ>="2ݧ@ # dF3]22zՋCOzdh"HH fU{#Ogʪ޿'+2ï Ⱥ<+{9gY" "P}h->?"a~:N*)!C;Uwu>eI.BD_9Ns )%M͇Tdu])۹Ϛ_{y9zjk !%و<[fua@64qʼn xUӃe?PJ&pylN3s+MTFVh؅Ð%gdd9EDDhON0 arIke<cs@^/! yX> h ʴ>Dʛ|]N biU挢*$c!N#f3@f'wJ9.kZosEkjND?Lo#"!ElBPi.=1`,%'a1Zu忌 bo@o?[I(!T! U:Ӄr^߂ӝZW녻d C "k `,V It1" i.l~-<.(Hwv~ߔ HTy OoC*Rt~Xyn$Vos"^V !b>>"?~80lfnR3#tO&o&n6wiM?=yRn"afsl8̇T̡e?lfazm#z?;CõTX,vIE`gAߕv[yge`G+"G BNΊoMkR$ɪ aH~`=G45#4MDR&7M/GVZ]j\iJP/. ~oos"9t4`q*LqeZ,\954MS42;kH7zҩvC4Ddi0.3ݵ"M!:͌$ w?rssZ0 QJM)&IaDklo^sxQMDUpC.[m'Xi¡H3krĜ޽ob\LZG/i|W~lhe<ۉ+=IPN:GF 5jt{J)>0U; Y9\~{/IQV]on7"2y9"0ZCUyx8)ܫQkU7*}DZ)1pDD=9bSxvvR>psa!Vjj]*x[Hs4i"-qS<*Z_g×㻸uXᨂJԀqThio>>]kEQo Jimi149^QJ+p>nF,:GΑ ؄*b*ynJ?DEI{x2{B4gQJ91<4WbS>~HUA&œ 21B4Iʩ)|c) ).nd D!5,b9NsRRJ'ċ&t4րݣVa)zt'c6w_l?~L 2|o 9a f0*[-͒~8gx,ݣ .\o6dqjmHoU "7w%$WÌaqDG+'@{;#\nNncc#S­s,m,FElu崠9 ajm qW!kIF5_M0f>>U)Xs10 3z:ۢ=9,N4yf0Wz=}8O”vSM*߼Vs{#6sKѷaa0hfp;݉?d1bڂpIVL>^얯hwQvX+yw^ ׋a}|&'2:;邤hkPS}:’*]kZ<)<č%(ݫmYnR5Yph-D]cV2d\cB&P\-M[enN3x{XVaߧ<6 rM~;!?fI3/2b6˱F)49j}Y=Y6fOc3rK:t$uw+}9Tn'W.=mw4!Cm %n!˷.Ay~BiTX #YZs{k18'4H>T xKqB;Jֿx*QEBRvxuoK|/\!X~|- QU<"HT$Ѱho}\Vwg.+VP IT/քuz޶{vS~һz ^,X osHiO8@I8=%t>_4%"^ }u  k0:˩kxH!쏈m/m;LtѾ>=4B yIM>=|-OǓFҦնy-"CItapt ypyTr]i/ZɋK= 'ͪxΈ ϡe<[.0؋yHSY^`ix3D0h۞BW4yKrqݘL͏vB$%F+w[&~K(q9 S >BtI>lxط=\I% fZ"In<$yH:T 8Z/s, (m>(`ANquq U:$ΎUSW]J94sH9Zko>Cgp+u:$qO-`vwywf&Yj^m7LuܽNdZ{_ ! bL$#evsRæ@il:WTy9fuL"h^i?"\yf8b-pk^-}t:Nt:N"ɘhI/u +D p ;\Ux<9fP0폷D E,F[q2p)_P>Zq.`0@wVY<fns_~vf|ϝNV@dXZ\`YY3RdbHDm#*2\8}u%@\r֎$ij$D!DUuNt:N\&8zC@@@#yԹyUE*%mPϦʲY0.YH "2xPX"?^V-ux!;[B8#j'`m0Uˋ9mG䌐%FrM֒4~?'b$xc'.Q%D\&]I\̀ Ž"q10 (.NC!z"b~E@4)Fq(qqXU12WD<".9]uمLD2p·K4$ "Uc΋=k-q a:-,aAGЛ+>OOTAu~2pD֊} ̌]<@СV)z(]t:Nt:o6I ̬7 p);3P  q)!2*JA6 C#mx_%C α8,['no~fh3c"+ٖY)qmB_ ߡ&cEQ@%$A Jŝ̨so <,huDdDR`ad4W;?.ow8@(\ZdD S#wuNt:N\z Rp@t{6׌$CE%0Il{Q-:Iʙ18+mB=f]$ 2A# DKkÍ4R"cx仟ldgOf _x+@?gb ]_4Whn'-a)$C;K@ u+JZW/]uͪ _lB=iS7DcqRii̧wqp,$\N# YOB,\9 Ką{`%E_!uNt:N\σH ՊJLR b DJAzxW6.rܾ 7#K RFH$E *ikE@ ::F^@ݾx>[ '+,zcxq>>~u夛q=х,9F_YvCw^TkRavڸ@Tm>Qp/o} Q tf@8쥻Yth*AmҦ(;t6ݥlEu8ke[} ]>:Nt:NӹB*Bõj{?J$Yf8Ĥm.ʐ|>z>}Ur1}'/E畫&\!*Bs }MX8V{h$q"pR>+ld%?d|V@૳|lC{?4lExA Qe>)# J*-pm@mmP@,j9z`_͌W%$"j')co\f|u '.w:3 qk4pA7:GqpBi ~ݱ7uNt:N\+mW6o?_J).?'wL8vpxBvT0o'2?-ilEz_~\J*p -iD? ɕԜ "QR~ٯtW0@+hVi.d@ʵq<փbB r6HPQ1oJg?\kU!a m.Z4$ Fq$Od6X$Tn3$\B= 6K\;~Ǐ^VF\`HQEFЈA[LČS?c-NkӞN&F2jD|$Zb\KoIPR:\_tp"*KՠVw 5UB܅Ŝc\r_j;[ԂVz(r(xDtkEզ07G(@i?v< NyK4׸)L5MJ@.\IK = `!'QBw!2D,Ҹ~߷auNt:N\ Y$ @c2+G"!n3uKY̬vR;Lfq4o6~rKL@NH"#Rfdxl_~F0eTSҍa\KD @z<~vx0jVdJWp䜏6NsyEMY,ܦKJfn&u#R U4޾i(z;]}Uw>j^󸕔sy.x$I $<"󮋷>kՙCBŝ%3Ny '5č9~~ŴpE9 Ŵ0F=|2?{IKNt:Nvʌ&z"3J.7 㗱gK5(C- 6kV֪^`8y 8"!u(?8aR%"^QwoEmףi?7.1:F$lL$#<0'@2wۃq {K "A6'O7mBSu5 $H.V" I2@Dڻ;·C֐)kܖ c~|yo-F ݵ]gN=\?B~j2I-z=]t:Nt:`s>@o=#{Lv,K, 08M"dwß}Ȁ k i4ĝj >y?&1f9"W-' @6bOWϏ֗26! XS=TWc`DpQ*Kv7ͧ:Rr?mBқi"0Q-sQL(ü/S9>&M7~K4I >2u#"mg?K?{Rep(Ðv\*+R\&47!^쩫N.Ը!voۗO8`s }r0NV'Β7@C8U )9^C9:Nt:Na@ g~ `g' @Ʃp1_BbdͥkVl~O 2 [FXe^ R 6I$[Z4OrQ 3=ϚrZYfuJ@j0`9w"X>t'jx"=s/bk{HL#|2//~n a7+2|*Abӿ9~UUpW`MYv2p3bUt <=|0N$rM3ˤ[C.m9J80`DFpTHPTHutt:Nt:@baxg%A`pţB#'#`k89o.qhQA1"rJRO /SImKJlf+A:~]./)0&r|hf(VIǍYqJ .jk aXM126LD}swO?ݤkn#FbS.' 2%cJL~}È_2w׈ @% H)GHZePҁaQt~4FUJiHJD @sd ",O` 5l6:Gt:Nt.@1ҷKד5KBR+@[V0p&iAec'WJf"HaԖ3 .^$ @^(*X, .'%x8IXRUӬ~U^R uy#SQFѧIhM,Ejd f٤zx['W$ۜ-\*ytЪdC)s=jk';^y[vp80]m;W( U]\`7SC&E[&# )% Nt:Nӹ A: @$LXg$h$@SAѷtGh{ 6T<(;c.H*qTL:TDah'SImqy MGWgeEK~0NRy/ AD)%`P՝zHыD @AL@D\ "^2O%GmaVII%vo/c⑜&p8-M4zvE?ן '8Ix[>VqcK&@maiv !H,GZ\oTͫ]0)>s @p_N~1g92A W6Fst:Nt:N99Oq!!Q4FDxJxd"W@a@“ŕ!GRUT|iݷh(0C Kx$`jՈCj2CrW&׏"HJ)F*igM[5l<4mRc$@R[ji cU(佭":t lrty*dM7Go#hsJCa2w9k6Ӝ9 ̙Ol*KC]֪]v{`|?D٘ҴO#&u ze$ UO w}NQ&S4O 4d?Ceބ=b=.jsHs2*xTTKF ,ߵ{GW cT0DeHt%tujTUVPD hMY[.Am>DSRH*?^rJXH3Jo/5Zt:Nt:N*.wb@("Ko_T97ި `c`WɥL:P)IȤ415q0?WO??knw䀨jݺat-amy͆bp ,,@45  (@y(w_˄~$OC6E*A;>']t:Nt:NYS1,>ǿ~{/Nv>(!y&s,bi?? C x@B; -w8%3Խnw$QҺ G:WsXO^]I!G@:Gt:Nt:ΏN ?SBJyQýD!)b@{c8Dj7~ᙗ$3O/=TSW3ON\s; ga\:9I_PBmKDy/#z ך!i9? Y,"bY0ݲ[!7 z3&pqKq PUbuz}Nt:Nt:6R|UZBF޾8dk9?/DQ hԏ *8'H5^s'i?g˽*i$ _;{|IkEBPq&+YlO~r}rիme=p:GmBBJTsy.s/tt:Nt:NB59 BjUŐTKI1b;<4U{RPom.18-UTFKi؝ !UDH L85Jﻸ7landA=&"~0,@-<ֳ)cy, >o4Jb$C5 P"DN\uNt:Nt:G:Y8(#  16_DB 0.[8'Ai`gm]ay QMB|ds 7TPA|r(.g!r_'qj0@ B@ng-oL|{FQCjV E ^_{RB9:Nt:Nt>xVkHt@_'Sip~:XB3wk HAXjY2.[VEcfZ ]dDj,c z$ _;+f`RC N2ᘎ9< @=uZr~=! x$"D9:Nt:Nt>lY. w\cZyH@rHs*Z <5&rH$ II ]~TW,\A%XLIƝUZ QTx/m7 XХDL|N4 A8RQk@exXrq&34<9'HJ!euSt:Nt:N~_^cMp_L  iJ>K^/\|xb@$ 9c9: <h_w^[HM ڙu2J%ݢ>~y,u.$a.J,uNt:Ny,t3Q$Hu9SO7j p.e^Su7pLcJPr%:- b1^eJuNe 0=I! 1 ##r@Hk!3A X&@JNw(*h4XVkڢj|_[ηQu$E8A[ [Td݉=!N  "Եݧv]#y٪́qaA, ~o^QwT ӭY3/oAd3yܦA3"BkN|əH"a!.c*S@ rx#udU!mAQYztWf%AwI3t:Nt:kNVw᫯a0mwt60]uHP{o0j0xUHl1-ˁ³s|W{L3*SrTx^y-;j,A@\9+-2[JoXG P!rB-X*DH҈WOR!Sm3`b;Bl.lxs'q&`-+' 99En b Vj&*Yg߉ʁMlgJЗh$# l}/3 dЯJ9/_?V[|v,Of\J}ytk5[511k.P=rP@Gا98,vOIWT_Ss+~ O/0MG _y:Gt:N\/?C=\m$nF2 w&7rnnb(9.1K)!!j۞[鳧5x L;l! "E>!92֜ ZC񃠈3V I!>.K|9hX.g/h rIC\e6)!Ȇda5Ƽ I=}( ".9e֚H`kD`K;D\'iSh;y KB&\)-!v3@ɸMvj;Zt5 $q 'KuxJ$#mt66cDC N$0ð/>܎Y\q~Hst:Nt:C 2de>ZN\ªp80CNtX6OgNqK H-U 8x`TАT4I$8?Wj~}A:z+ߵw[zV# M]/N#RD[$C>ϵ}K칯#AoNr;@ "zDLW~&@~Hst MopNuP x'll> /BUeԧ`#G]0>dh|Y+e"mժeVG[dN i@{H ,3LL!@Dn}t`~:sx=wrKCRTj$WXdn|-3''iiP$(G $=8b4y7Yxa,9 ;-"(bȹI;w9osbUg|37L3^/E?+,ly2n <€`V]H!9ۈ&̆Q)1qZWXv`{cHvtZ_Dӏ !F,ɍA@gB<oX5<1BSh5@ۼI]~>ܹNt:N@6ϖ#aL8Y b@(cenA|x $ Ȥ *%eW84]M@@%f8tF$qȑ_$&$ kqV PB9sy,zX8}Hy /c_=XlƧrf|9rX-F`(]dr4x PUJ&,`m-4n(%t2!0B@#|Aa՟ CtVT=aiCMMH@W7G0 = ƐCHqV!N@@(֯h8"Ԙ뤇*zs7yXYt:Ntx59K8I=Ub%8kL}Ry&.Zd#b5c £ DɉCLCK|rϐ/@/US!o|f-9Lp'N1T5"()$FDGfA8= &-\HMu 1m|1jWEi*F@}ysh jq"X@13ʪ*!י}uCAxkG 6۴ :.:[D@[6boÞ-wa.a$JV5DL'Dѣj<†o,$R!b L<P !4f! ,`5U4zyX;Yt:NtN8mm:H]=CMWl5Fz.BQTQ0ڨ.\Ɲ.kNm@!GO{/^ڡPeKr$G6Q5KOy#8YSq 0 L"lx|Qڌ/< lbQsH񣱼~2mq[Ǽp!H;}*KyN Ho*{/Ǭ.?f3 *.X#SN712`1[唡8 ð4NUpN>P{ )!kB1h!Js{s@"ms!`w@8Ax9$* S0T%ouN@_`X|Phy\,`1@%$WD*ENU]` d`dh)\X"1(H3! n"PVB>ps㰯 2 LG;qEm6} Q>t.Ow.v.!b~w.nlը.j{9V6AMPPK:O)|+ Yok-\6N1yr rARL*a8CÓX'h41-MBw0׼FP㯯?OЬե rΠA_5s(&P0wqȉq%戇Ȓn< Gِ4³ӉJ *#3OQUM @A PC0ƈ|7ŷNٶj WO^,z]t:@{% X՗2D1j0H!!Uic+$0$0y LheK=1VzC t8\$H'"JVKRvza_>_mļlSt:? ߴ`n}֍>3B"Z4fuwQ8Шl}1fٺS%Oډw:EH׭HutqjςJQCn>GD+ä: L"Jdbܖ_@¿}yaf0qN={kt:N;Bc_XDO?zߓώNVlu$*Bq5b0ώ G]_Kbxh9<%&7.p8r wN0:Gy9 N ` b- FB("rO C FHUCUw&O.AƘFVː`R po\ BE`N2'xl#rzG׏ϯ·i,i+Gt:NyGGsQktpuo@r q8戀nv󿒮~gprΕj3DBDsS GJR:?ΨJcUyt鼯keV !&_}EW0:%6o+¹V2(UQi<0d(٤wHW#a#65O`Ht]oΏ ,9éC9JN)u.3`ВB^m+#N9*GȝJ0 ;'P (lWIc% <ԝ ᖈBț5vclYIɀ ^EVNV @n@28o,F7(j8\U*JRT*96N:`3A(e dȍm 検281M| @rwmR|*UT*=fDaP{Np @$fҟR:Ae]LE\&0fp@A 0 >^ ZKr*ؽO 2";٤pL 4MS]T*JRTVJjYC'') pi:u-F)'F#u 9;dtJr2<4 pG:N̸ R#8a$QBi;#9ȨW~&ND ^P&K,!e){ƓNۓa)QG@T` ̼a //Kv\(/nχ*=xo,YL#8d(c0'Jxi!`W#XiʆlX>|{̅IX7嫔OK1% X(_%dB"x j$Y<{acg)LZ4ߏpQbrurgJ=LM?vN~5g/7doyp|4O0CĄ2G5{:B9*c" IFffNdeP]`2]Btb@Y M|77KVM 6exQ-U>/8Hhw0+Wa9Fmcf) ̬YFf!O-b0r#bFT^68LN-' i>W,2(via.+szP:0 ͘" % Q1zpFXʜLywTi@`H)g\K{Wp c[2y25#v*1l89xʘ<|N.l|,'2~smԵ8@K6ٞtPBG)e>ST{("pPHTx}+YƌjR<ʅ(ico.8 fqڴ*^Q<% qZ,1@0RF$yLc0庤TC9*JRyLEaqDFܴcӬq8Z-.[f w<M`qZEp8NU?Pɮ'dLpe> sQ9O|*z09` vT=3A :g9IdkR/O:9AW0SZB ."]Y2 o1fR-7x-e0`@i;R&S2$)aCXdfࠆM1|0rP9 ^b96K3ѹ'ÜB";UrDփ"WwJ圜h֫qxoΘ7Xo6w4Yg(8Z@L#0 Y[Ds.x^ p>>\m/vfaeH"GRyH Og?ɧq*3<-9C:iaИ`Rã;?i+dC+Jȝ|rٟ9R6?ш v2e#3vwzF_7vMtl|d)_->cS*w<N\p<.Jg'vHٍANGy4piѥD;o [34N54;!4G*b_YރJ)El" 8!3 &"4o'ڸ 7KIټ8:jwvMk1amͅB+,PD :81y>%PLt4p~ϸRy<0G12_}y~}κ76g@[2SC`}N rXgZm]&" 2[TJR\ !pw8>fr"%"^ ALho޲_y`! DK&^F ʫ{__?^.0\7idQ笟lXk.BӍ}XTz%gao;ycS :2+2P+0<8uYhDH2Dcزj0Grn8H3&hs|-_cYb(Oqf쁑ʈ}3{o|'yprHfFќ&ﰱA`/;gHV{V*&:x3nٳ&0Hk8f8悁@"5)!e[0k: m^/U*JR\ f#U53UUU8*:S$[<0OG:ND&W F?>~G A:fp E !~Gּv7&4 +co&nC^0Us/\yBsL!r`>{ #@޴D*˪o%waRyJ9(@(# 0m5H89"H&8štmLG_:GRT*h(0;GE,vt-J .<ޞogm e!vc `d"d޾ދ[۩C#f8*Q-R?8aDxto<`XƺF m׊7/ҕ~y~{ZClۙJx\Wt8+qܱt`ՑJ<0`J֐5ywo|hHø gִ r+Ke2jZv07(M) rNn2HэvWwh~q5`d~\s+hkclmp2ac%2vh nh m j91ODj60ɍN+4!P& L]Ԫ]T FK7Ew%˜2ψ. .NZ0ONFxD,Pfix.ZʓB9*JRX|9G8Q8qo/%NȪ -޹2' J!;`3VԴVDpKMI7ը)(_%mwV,Cd ̮7?\< I/' JITRT*6@a_&R$zbrMVƐir\_@ӕyONɗӬzc^H 6F`iHNXz3Սvpfw W1Q0 b8j/'39"#8Qȧ@)sOM}T* R#)@V8t"r)÷3!wq=N XY܍`Dɉ, 0r1 0g摅&RyZ.WrX}:GRT*:9ئ1L^;aG8 !D`SizRvc11m r3/,c dN2a@l`9 㰽:Ң^ly6yF@C 6%@[؈Q~uV i7Ebm췛/9|a-@ʻ1,j~w/իM(Y&1s<^(tcQݍ542Z 1tYSdɦ9: V3HN}V~JP v-qLCfكzcPvhݳh2H Sq'`1`7'I[Móf`WT>sT*J9*'`d}wWgIp̃2fјtmv|rع}쳯G@Џ%--xJᙄ,nQT2g#`="K Kqo:Na=^iژN';C=X#FI`d3-ˆ AuY\٘<!4M`G-@4hi1۸u)I0I F" PB4Yvri)'2Dp2b'82qF f@ YtC}:94)Lwӹ4)$f?gNxAl1{vQ( Nclb0'8y3{!d0** "(!34Y`Ay i)3j?50 9gX1g09q)gRpΤ͌A8 >D!b;.[,։F8aH~V|*?+]`%(BhCA)FED.E| D'*lt aLvN2ѱw8;i[j=, e! qv(UJR9!fuf&Chk癯|ZHdJg:n/rEĀ&w[{ͳ!88Fe6 A?Vff3#""93^صP}:GRT*O <'k[W~5 }@Flom:t]_?WƍziQ-P.M8HDD_)PbT"O'3t:A̪)dfL`_17a;:< n0i{4ϒrxy~[̺ݙ![PޭTz޹QqH62JCalcҕs$C;8ɕ ,w_7䳜noEv@hpɋE%$D5RT޸E#bw^⟾|̾.@=dZ aX.)0#g7 Wi~;Ryԩ:GRT*O;l_/O~tvHu8@cfnCzҥ_~?/qyGQ'Ik pI(/.s^",&RD"gxgϊ`f83nffW9Xy!X0`m#pvIb U}LgWoҴ[v|$W g0Vg'#' H9‰ o~׸ @jQ,\^)0;!^ (#[E8i-GLe"s)Q !6RV*J,e6fbw kvȜŔݧo97]w2rǚ% ]$xpIY 8@B$__wUuwjZ­TC}T*J1xuv|IV ]wcڍ^⯿+}1]aFfDR>?w|Bi^,=up`2\̀>E@eg93xdcѢE_m]jα-P `5nZgKhwʮ3fTҼoϊq B 0܍` `39' sV!#I6o'YZ|rqȍ,32@FY+fbb)(gnyMLcjRȢ)he&14'̜ ߒAq^}TtsԼȪh;%P$!JIsCs 9nsT*JRUӅB{m|4 C ۻ[La=er_D"7WUa|B̄%`̪R*5'rvE6ёp9Fr2(fgYXޓ;-3J9T=,QO4PH0ꕔGKqw0;5rN}+,ZCbvIqcwlUrl˶\Mm5]w砦9avv;"RT*bc)+R +Me$ ng35bfw38!g'q1҅{܌JpJ qtCn5ώ#?JKi:^wCcYSSqѺB>FPH91>wAs 1wga̪0 3a?ʃ!4靷|}{=fpL F`frkytuH 7Q͉Q5n򳳉%\b-؜ͣc|%ٵ:qXCj0\f`F&Oq^0c5Ŕ`d<|.-_C1Ya0Ȋ :^YT*,)A9Kh=i*p(A ɡ8Dk_q7iyE#3 `-d xq۶1{IC9FJ~TRT*UX됎Y0H+»By,X fbu2%*%yWA$Ih 34idC8yb\.fbfRjfffjevbdv'*=wNh4Lŧ ji)[a[9\}Kۗ085U7j'Q͵`[M>8N3,m;aL[_w`~odܯ#gPb - j5?ݣb+-^x)}psFDZS8tks/8A yR< 4kDut KoV1l'Qz(2iGȀ7Q)S* /<tJ婥JR<(xpk4#M &fʖ 1e/~Q8xZ08/Oendq3V~^`f3I5rgw"w/z3ؔ}Թ Ec-m% Jk` wFfiGu_!8S) `r8̑\sWVMyI] ˂>p twnߚܵgݯI' %s` |lX׍wz9,cA0D;RTd'AKol>ؙad|-hnn%Í 칱Hewi<:W܏sT*Jc39ÙD p@,8L8>9d[eax$RSL|XO`fdsG$,{Rg72QĪ$fZ2G BIR&R10C1kh͛ lo\NsR,P@9 l.m% Vк/:I#mmgrF0)lw>p{Y WbEi^IrGE6CAqhb>Yg0ˤ33E0py!]ָt"| `V>3|RT*O7S9^h [ʒǨLwL2Ęə P6+, %#.=ar TK9*JRya&O)OJ#V)&uH'x%CNErRQçOlr*E(ԊS037'Bјd0B7m E g2% PˀR2v8]"88{JŠRiXb]Nhg i>~1*hE3(6R~pb09d#pB@/.̠T]PI2yp% wvSSG4c$Ȍu(IC ^hGXʃK2_15p+gS*O N%`GQl$j!5d SsrBw =b+;5(p1CG]<(5{$!rGsfV2D|x6zݰ@w9:gU^{|RD( axDOhصGM~e<{]坃yJCe(8ֵ0Hl";y]Ba_+D| vΧaUZ+_ ׷L]`bPb9j0{ҋڂ$~/dX T<(Ak0 L&Vvѐf;]S`cr23:Gt eHrf) ezPM:Z/Ƕ[ ~=rV}R@=n9OD{W*ʣ$C{(eXŠ!o<͌0 x!3\|Q0T+lzRNt  'ϫ&VY$g&85v.ȏNITHw2K 7Qo~f~pC\ ]̱shudrc)c#<ٳyr g?_B ?^9w9Gլqu^]Q#Vwp"gvm>b'-9 ܂QD g\؁M{/ȍ<[ts2e8CJ8Wؘ!Jl@dV\t B$DՊȓ 9 Ղ2{CJH5'7VQ!8'7 OIW=s`/'i~ifVdvýݝ8܉E瀓E a8ת(1]Lػ%poo~ ;햁E`51kuW,xkͿe^woF`,J/S283Ϛ~?L!r5H"/f +ym^`oi4[p4-ƮB=V~qg u"xN '/{Cw*'/:'1NB@y,'V%vJ݌ *3#;ypQ4Rv`rZ$h=v=VRTb#glƀM*AЍ,|~9NԗlwN9YLXȔ'w7=sTK79g]h=n#4eӐ ,d30(S3ݽe`qg+,+XMl3Inymb؀YŠ[4jlfD'b5urZG:"ȊH.'l3r voG&O,/xWqGZUC0ݸ~襃=~?݄n]_#c6e;[מCxmtRT*J8^Ov)Z+zB:G1L$"jJӘζV at78(tIAN+ϝ;ڶ^ZGLdm$c'0w=nHFzL_eəp `bp\M(necCEdg( <n9N!+O"i!ljPCNHkriV`>xz?Rǧmq.ouO@h-;-vk_6VR4a-B`j ƖKEvv^@|GtV ִh\_Λoڗv\|15t.=Slg#Ƙ3@6];;7 {?80 4ma wu RNg/]inj.ޮNZsfm:qT*JR<(Oxee!I~b\buO:!U<`fgVX.}+5m3#f=iT1s4*##k0);9Fbj9¤?\~Çwij2 J&X$Daو `NƂQ( fhfI`!QMftT13+Vqt(gVV% ud L.0k!ɥ@b/wqh: ԩ$:G]b~a߸_As fdYi[{o~?җ;;M,ǍGdIJ@10 ǶCVݝgv|{~9 y_ʗ35lb9Q1%(1T,FdfH6:G-"FI^S>n=w9DBn:Zlf_npY2Keu{jF[T*Ja6n KJG]\j>mr8#TJ13@Dn"bf"7~OZ>E3f,Ĝsi 00G-:& zHU\ȚXC gWmǵ5%goTߌ{RT*cGi5f(9.t ޖhqR%rrotR^yĩ:G屄YLOd4zt9b %#p6a!}O91a>_AV)7ƣfVBRMxNh9 F4;8QPl*B(g#72'rbc`h,uqkژGϪhONMu4chI @0Cf֬AU]u\ O.8J=Ö@bo6)v~a:t{ʛd=<ﭖ uP`>5ҀQnӐ`_&"Qf@@^|. ۜҩtAvtTΜ33ҕv0(cf J:k旯?wqHg^Y$:I-9X(wRH$"՝)iv!DR:8G 2YdnW8[0p1rW{a^ͥDN9%#SxHT*JRy|aX00`ʻ3H0mdb== Z|4(l#W &tGXWQy,af!K'MvI($9:2L fLuOx"ω66?Bpf bAZN(M."X32@vH% R@<0rJl ehZ[K4 216cȉQᄓ$y+a1)~'@^zg+W7O 17zu`Q>w'v8F!0x8@&05خgn|/BJr9CDasNNus`fL7vy!Ot!*B1R \$NdLpgv>Q0(uqD7D+NCb"rw3c(03r;(w/Oݒd4d|eZ5xlmaKu"Vǁ™2|꯫RGRT*cC)'3L']DV?+rLIz]0>TXbf"^pC9N v #Xbj d!HfbZ6_"m/ct]U'7U)l6KiP3P䡉hu^#mͯ r a{i>g+*ll>_owc^wY1z.都[MX/.]c22 0q:#2b0+!)tڀv.&7ZfV O0H·4_E$^94(; -AO]ٞrf@JiѡM&lzko[.)QrjҠ3O^m´?.db `E|ĜY!'ӊzFæ#Kʵܵ =?ѓMћF2@n\btv|3QlZT8A7DT^TVz4{qtjNϧ@@ t>7p'YjNdMD+V~ۇ+,㣣v;wp(a#XV*JRT5ٝY1_af@Đ,]AV.x+܏CXD!S\~.B9*OhȀ$RzI1'iSg| ᅪ}<uW%I9y׶[+`nmA+yg(Z(-f|C .!ͺ*6T 32wǣ@b:_̢;<4YJth4nk\֭go3}n&d2,qc[YVm̳ ^_vlbJa<qA#M Sk]I)4&ta+ pnV"7L1u\ˣ*'R'gAqc{Ճ +A3( Rqw,4]KGNNpsD`h}I<@rvX6Ow%1lc&ccZ9v$~w|TZ7HgP0= {=p|qvsV 24YJRT*G2s\ZOa7|1O(/tuH B`  '+ Nl;"`@9!~Mku'JcE>g%?x4YB"/2e˜ב}`65p!кlK{;^]e ~vK clш؍ !` @'([)H2}=~|W .֡E=o' 16Di:=e3kdj@9LTc*vhL jշ`+_Ҥa}OD!f޸ +ﴗU5_: m#v3S<6 ~ft$04٧ qm8NeuVb}noaw޻>ЫE5va!i:*)8v냽 RVF6]xuRT*JDDJ&hl c]j"o7kpQ30sJwt]0>T`A0D U"z`, 8tFk@?C b LO:& /6e&=w|&6[ϩi0Z畴k+4)u)#4w,̘ȱlR;Hj@hH9XNKA 1C00y8B66;8zksߟ`h3 gMQ`dD8HƖ58pg@,T]xL(Ѹɀ GRT*JT *W1 0H2_8ѡSHJ̜Jkߔq)Xy\:GɁ(%Áb{F )n[|65yV6o<ǣr1 M24n/֝g-h=ϒ<&ݐ]!Jg[p# \Id̃u<`" G ZPьCa,:Ǡ("`( 1R|n'bk9Yω|?ݧ&3Ld 'd*]\tf\)2ƶyl֕Nw{:\߾KWZ+BzP8utdy`z)rl(UNODwgL4m!OsS`M0P-ʕP0A3l=̯p0C纖r^d2@JJRT* Ei۶k LCԜŤ-aPqȐfY1XR#RXy0EPTS~IgI U x2r&x3 "mk=maLALfXf0f6 .PA!p>uLx`YRT*1>L3D /_&3{ΙZέ ((I~%bCH̷تF˄S#?yvkrGZ?m(*yM.iEVOQfڶUݓ/;/í)0X.Ţ{wf)'ym޲ҥ%f1~<󌪊'=WO~\z; alR89R{:olXl<4970X ;,B+0י(Ąq+otEfHd(°<޿}}y|28Q 6ӄE:4z`>Gr{bw^^lDp٤obQNRِ~F:Գt` ;lse VixpO $)VtVJ&z&w0Dn4%0 J؎W(̎NN̝B6L>N̛}~q?Z[wt=0| v=@aqQjA):x" JRT*sp2Ь+?x'{F&$f|$nI!DK9k?ob?}ZpD'a=!un_{ٝK_t}{a r 3"pP>8T2[4]m ns2pـdP䈖wTYTsdL/;vM' Fs>ӉupSpwe=1O/>ݾj.GKW@ BإF/}mkty"pc*R=w:Y~v] F)@,n*fkXs!()@v|`3?ʧpߓi3NkEqϳ~KQ+ONI~I46 s"c0E^,v'BDB's5)oQT*JRxoԛ;mPwy7o(6,ϛ͐<ſ Vkz|ٔ,6"qqXԕSJ0lmmο֭[/כַo__;\z5l`^pҥa?Ei۶̎ܺukwwiaH)mmm0 1|~ttX||]VDcއ_+#`A2bۥ=8XZNS#> M F:c&BmD2Q֜ 8b l18QʽȂ$b{wxxsOa`xC#?}7vV%nfŦ[mPf=yͮck6s C rrb*nN(68NL.{mEЂ .L:Td\ݭݽ[fme/.ŗ/]^Q4ͬimHIMSϊS @(Mtܼ7=bLHĩ{hmh*~Fh۔ziejraP^յ[Ͼ7޿6{etLw0H˸hˍH-JoGvoJRT*G^!A@FLB&ɊCnbi (5ܵd;p_D| E`NjjBhBДQyxtU~o}k\nooooo|_w/oo7M7'=NT~~^?[RJǿkf?{f̪omB?)i{NDxT㑅aNl%s}?~rGhZ<,l+{ϥ,5w+{C0(`#[i;޶$dPԪqf$vg8/«ucT0Y<[ŗw?8:u}O4_~|nxakJ{9ذA'y"@m\]5۫h6"Jp}s÷ǫ5D W*bkWo']DjȊagruvyŃ!ChciEXT*JX@yhrPt6n52EvY|́s>( L-+1es3RK1Oj*i^xaQ_;c;_9\*M'ȍ2jO? ! ж_g3TBN|9 ߽uc w5lr/]rl aȞ !BV`( V,Dș?>mhZ{K~ϗS51EJEK[:rȎ!l;ND Ӱ1,^ks#!b4@B(/*@Aq¸_w!@,kJ=SQ.i# ] 9k\r!8<޸۹u W{4QpPܴVЗ\y˻cIrwsWFPEJRT*G! dE֔tM&ڔဦ "Mf܅̳ڦ!"Pt$vds>> 4Wp3jcSsRJ׮]k+_ɉԍ7/~;߹u|Qtzc<O`~{#իWW|>\.n߾}^{SD#HGa'CwM'8A@<%O1@<I$@>`9H/ٖdJLwuٗ֜s0޵n]?47v˪]2F#Q(oaӽWA'WaiB B׭Zu}yT"#9Ku>ݲ! R"[qHnF@0m%G;W/Ȭ  GU#sٞ<{|^WD\Գy?rS}BY?R5RG4FwR( BPjHjIEYD<\5%V0XceS[RbXr\mb\[j̷8OLDjH!mubip9:Ǻ1bŗsνBJiٟ__d2򗿼V#rڼgɱ&7gd9c~<+_KO<ʏONo^pi1ϫceoRRU}JOED6g2s13U}ǟ(T,Df"BIC]ǧ2/B=]KOm۽mr"Z jX pG@hMZg@A0P*'fغ[tf-t7+۵巻}>4, fw 1P3Pq~ '|l"+|=Ej0 ꢎȯlpC-h`|:뺦9#&>Y{8x<>8@u.cB6cBz}4# ED₡jt>hx]2f:p#t-\pYD 3d).㜛="1¹]""\aN (N('SܣB.iQYUUISj9B,w=&,Ǩ$D@iš<;afb9h"c }W$_ |$""zNj9@)pFU5O{'geBwJӒ=m}n2e0 Tɪ߲U!Df̢@bCxykjwc01듁A`wבT}c"X="Os}c5]zCuQ |l\QUU>hN ˗/籸\zu4MuftB!W94? 5~{;|^&Z1Ɯ.\.۶ͿE~e%ZqHg9+uͶskkm[u1ݽٻV y tG-~3lFrUM)}3ࣄ30P5P`y*'47xP ٞ 18ĈCūz+ƝYʵJ * u D~7ߍz7E^qm#F>9%"'&%1JQNf%ě$ @b8wo)'w:_'#>Zꝳ]j ' C0Λ}BUCjtڎbwYՌUxϸi|| 7L |$cWxK4MPQEPǻ 6 YTۍ ynW'pC7#% X۶,jQ1O}ňcDC/fIin4SU%'XUlok "fɻ'gfG37aq/V?oAt)h.N(YӮ:>@!F1JjzYqu,>Ư=s\Vȕ^ǩ]hsoa_?,[$7hd "!ȓ̷ھ'ĨĽOM!oc4v-ƎT>r4VsXKj5K6N:W}?RZNРU*.w}B&s.]r /TU^B9\./nUU?S?cZVիWs+O<1;/}_r/<>O|3yꩧ~YU[pʕ .Y7:ZF;!K#ϵ'% ڇU4)n\ef auv? P0Opf󆊌@0WG 0j~U`#躎rsyE]EDsi4/ߕے= 7R"noMhG(`(HsMo!h}ٌ3$PaLް{{JM5}8sp0]ܿY?`!b©%E^onjԜ)d,:1L1Od1GM0̷ɐ (ǻDc6Nyo73U's*.ϗ\@xGEa&"_ג bf﹮4MmQY9;IfC Hlcܺ ?5n NN|;3c>ExgDY ~o8_Oץ%{|=]m;P{Qb6n#s,BĜPyj8^s$a1[;88ȹ.7UUeh~WGOαdT+Wxf{(l^_}|z{t:$;e]zҥK??sέ'Yy㝝OW򕺮U۶;;Ї~g~~Ξ=V rM!O8j+fq` flaL!:Cl+q Ĉiwww~-Q-Mՠ ]  n[AeY5[@ 0H3ʹfK"@)L Ύë ϭ$uS:6W'g)ݖcrVRZx;RJS̜uѼא0-$F"e1Ȱ ǎͱv8i.^w5c:nB+'ߌjקCVSVU];AJOa=5u]Yz9e$Babl,ьHȘ4GюFo]DRbM 3i&BjAoO`-,+ '5(S ӪEaD_-K1F,^-Yu<,ם`Xq˨Y1IQQ4 nXՐV3\~Hx[8NS"8! 4e^'6gC-S8:fz'gfH_ LL6'}Uu{{;;)m"rɟP]rF~zoo???j~ _瞻z/d!iܪ9R[o[[[C,w{;bρ[9"Lą? _1Ȝc@ch͉3Uju#~vÉqݝw< ٝelRk`3oBlx0#VlOma#},|X l SȔaTeۺa>uqV0QIalRB  {xV"1g,v剏j DӻۼBx3UDLY HFP,^`dѐsP# W6d 瘴+ǻt !4s;HGcM6um^ɦ턝$ & 9 2s$I"B縛9P#cC/ߜ木 ," &7Bcd!ҝ7C’K jH$Hd80m$ >6Ggrwtc"(؆~J Kphas^)ߪ{tѼLhDuwϑCsTrȑthj\w'[iYc?0xVr-4XMdC$n}[ IxبyeujJ~P3;rx֟ջ,Y>:G^Scs36NCC;j6x1ƘO"w]7y6+?gΜιÜ7߱C?CKty{'?_\#˗SJ^ca<_dkkk<YUo3{Uea殸-޲Tbn;1iyOيHl(z3hlSc$E{k"E `%0Rq:@f@>V-_B1k QݰUz gfd +h`nr /H,`ލ32rjJ,|W9"4fi'gVq2,AAqw֯8ݬpN4zRJ,dtCzf]97hJrD1[Rs.aU\vUU ^se asA67<ޢ#1E443bU:PԔş~bnP/s3LFQz]HΑQVZuZߙ D`C{GQL )Yn (\țֳx@ϑo9]Vd}ŗ~up:9cmȳʔ|>j/Y<8<<#֤FQ~nu?dbf?s?ޞﷶP!__a‘BHnԧ>u?Kg|r}3ɛg}f̙3g~:^vm{{{XFHp~DnY\4fz9@qXF1]zE拷*i_DM0Λ 4 .12ܮ'{୪ȡ'=DQj:1EG@,~۲Ruw:6V*&)PCȓ lGsqڠŷ1!U;5^ vb!#]Oeau.5W{4~oP1*&fyo{ {3V;)C̒ ik,>CŔf!G}5}7M7Χ\sS5hLThysl䌉vZcRՅÝg#5MsʕhRZ7aT*߳[[1FOO+W 6;.n~B>OO_y"׾}nȕ-~~m?k׮Aի"9\0.m'VkU{C'ְ+Y-[wÎ |君%ӛR0s \ͯhP!l)&J$N 2k_@ I)xkS>(K |`E(-f6ʵDtl^W{l(vW֒EE.\.f _kh6(jh?1Ӄ.HW.T)Zv73;2&sOaF cJ&tGzf^7 LyYlLƢ,dD+RLY4zȘň כ!~5UF"N}LGz>L֦[͠<n1R bpΡ .Io9Nu-Zmz v0?n6j'5 yh /*l$\L$ޒg1s>iQW)v{;E/dkRi}Œυo8-$]\ɥBmarqqyc#jE }b Zc;۶ef"5Ô$E-}f~ܽ+ڶ].kXzsDIڥ,jf}9W ]`=4B\$s$ b9$2CxɉxȖ-c}SׯwV!ݠs;9 ꉆ5/Y[Eǒ\\l&OnaT|5n*K]]/f*D|ǙgvwP]\3)Pۃtvf\9 9Ƙ].Ϟ=Ν;wpp3Lr[sx /<2;QfS[Xc9[ZݐtmX[i5L"j>&[ iENgA0 `%n<>S5\M򑌣 gZ}6p8Z|Snm 3 FΣJccͮǹ ?|EYlJCqc~dww*^kj j4 iBjF<,W,AsX{`a]:%k60U4H0"],ٝ Bpyմ j*boH@ٵYV`CXWηkYo;15v}/#.~}gg'?o"\۶x>gSMܺqpp "ŋF~BP(N=Y3_c)3Ro!2ԙ.$Ap`)#(1U7G7vBPQԃHQ&&A4&V#SU)Ȕ-EF/D{ADFXuFUkp8]i_i17hn2j$؀.%H$柮oc @"4'[.̓VR 2ŔP( Bi.fuJ'W__NrAhNxW>=iD*7|&?/؏XvzÍTow|?ϥ}lR?UGXW B~ {v1XR@0b5  V?1, M"׷z3WJKloM1mxhLP}+cܜd  JĉX u/{qi[n2Ϻ=(F<})[sYx tu3هn{{DE/ BPx?q->owvvo˗.]ZMs˿oȏ`m-u>?뺺kѶ˗xfO}SX,{G}t6e&Gٟw?ӬsߪBP('d?2lN87#dfFD,3d!pmQsYvQ8"gzl cW7 a#!`<"FJN"gyC!JV=%e\s.4C6r^́`7l(swLH 6 {L 7Vs9ӄ~E#!f3{>~=<<__)s_~xxs\~+u=Ν;̳l-{`/|a2mO} oC~xww7Uyx f?W\~+ Bp뮎|)43x% " lX;`Nsu_p za@yH!8"HP!t RVj@D0PR[5dC ~9Qxo~2ѺY*fQg];?!9砰귳d?ϕ R0r#XkS%0L lPŞsiLHf+~_R={0!<P(RGP( X y5$~V?y_m!_loo_pϟ?%"uݯ~[bf_坝u%i^jlOg'|oDww-˯}kwSO=5NW\?__{/_񺮱j@}$. B _s亦i3W7Vq_7ߨpl2O8 ojcvFY"mNPT d(80V 1l)r䯕W{YLmEcr-wϐK(x ʑdHSRdAJFjҵPWs!ѻ&f N=_ٮ,'X*QBP( 'Ss e0O7^??t΍}{1hX,~gE033ɟK/C8r#ZXp|_],DcAe?rN?S?/~_.>//={6{b|U'\t)?7o^9 BpuXoYerPEjqdU5$3"fqdl}!xF|>g׾ia~0sw껍 ؐɑ[7R#b4Fh`rBNDD59 F@* (F8K_:I DE^*9hHHHlM3 }J o$Q0 #xo>9W6HduT7}oOm /XDvS, BP(=Vx .] ?|~…+W3?3@J)y- ܵkrb3/hSSJ@x+MX4>DU;_h4bcr.w>)<Jr=Ūis~wO0T ;xS0z"Gxv5*y,ŁiE8ʷ lP2*EM< gy#sPYBP( ;#k[[[o/~_}ſG~#H^|˗/?|{{{ww7裏}h@D !^)Į_K'|K_zWSJ/^G?O}m"E( }jY7t՝PiYȝ,% f%,|x~grhw.$/xrG9ҙD fV>1W`g_3@Tƚِ d#97?*v>쯀zarVi%58@ ($0-{k }U/>"qjbx*A FW*ʑUp2Q{O=CIRH]XR  BP(w:h4IoR۶ι˗/nlN5L~W~e4]6L̪BpΉ?Gܹsb82ͲUwww_+?c?*3kVD#Z䛞Zs- BAr筋9aZ ?VYzoի/r幗^IF̹3ч?N_ERۯ폟,r;WICJ}T#WM=  #S朐)d*FkƉ  DŽa/GT)9N*Ra#1(D%~~>K?#"MM3i 2 Eu 1w-.}0QSJx BP( wNȾYu~\2s4="WUM-ft:m9%r<`#ϝ;̜Rrέ57$'CDh4ʪu%YP9 BpC,:+̨,ǐ̒&@)/}_wUV]w/سo?=|3Cc;QtI0f=sWUaRR ޴6Lj9Kp|"'J*0ѺTg%(*! 9Pp㜐&w6}\z llLY9WYf0[y"`j 1"1P ]?BP( sDR1z﫪ʊVzFn)]׉t:ņ0&d2əX5Um4noI}ߛY]Y5zjKe3gΨj۶9$? kP( JoWÏoznj)qdQ_1fϞ?o,^5iŋJK~`Ǹv#m&kF%V>yp_duPP_&%9nuŜeEQ4)sP( nЖUM)Umtc\,g-K9<#UGύ1.˭7۞cUJfrhGv)̼αi]U( )娊ֆ,^(0ئ|Y~KϽ 8.Sb N{Ͼ?6:w^A=l>o;תnw_s& 1(g}l$Ͽ瞻v0_Vpjx?3=7w  E~@{Auޱgq QkM)ծe 8%'p B^$'1fIyŒ7 TQF"D_EJp+ȗY6ym]Mjs)żp0Ȇ[(DϜN#>a4&%5J B$C2:H@U`'NFM׵}o:~@LTR_8n;#6{cEz{M+V~s-DM65_/(+(oBP(FVW:c[ xᔂZ'35$Hj,Dl)@J&IUd%Zb^(xgk3_xf =&46RHSƼ1R|}pמĔC9aơYKGJח^Ooަ㺚:B4tO~Neo,f1.w϶1ju!H@5 @R" C'Ƥ d43:di,+ V$ ahb92y'A*DsGh̐D.%N-1J UM]*S"8Q92uCnRK;; D$l{‰PTF %*zdVA*1 C\wW pN 5&%F]L@8zCb&pܧ:GP( ΍U~y a!YDRbȲmWΏ`44&Y@!).uӋU dXT3JJu/-_uQf*1O ]ćK;օaCϚ<)ӭwY*#f©[J_\\?xO~f2S]`1FeVKDG=Z6ȇO1]- 0( /h2O _r )Gy܍[C `9 >CezA9Hi ?#(qV X,,K)& QRA*Si-nhAT#$*ʬǃ>h>Bɲsc~;: {5+iƄ% Pop38C9E:1StBP( ' VaU)2STDD;]H2ÒHhk"$ fT٧FCⅪ:瀤SS pdʌfb ˈ\}/>Oֳ3'9+@j!ߘW_z10 |":H J ̺7_}wm5=<ԓQ=_P(Nd01 .f("kdM'Iֺ1GnۭI@J$eU;JE[F =:H:{'TvK,*|gD nL`N&=F1n-Nm`BajR@JY):GP( G#[E8dHɠNIpa#A-Ŋ+&R1BccS RJ1|)?R`{MW)ayuYqYj|m\=:??gvnQMq~أіTK_@PsQ}_55bg d@IAH5JȪU݉2~Ju ryz#'!2SP$"S3gM c7{h/Z1DP@f@T_U䖍VpD !Ъ o09P$E0L&s`KC{@9 BP(Lxm#\a|-c'bi܉M0 ƾO-7E 1T׿]XQI:_I=I]p`T `LP!E,CwN9hƒ)s,_~&l"9;{^x".b<ڋyBܙL~PtBP85 U(Y5ٰtxҼb*5~wԼMy3-`#f?}uOrt(dƈ{֮D"< Z(Uh #:>LD4&v(F?%y" C;L-1*CEz;w];lMRծC%̵|[Dtu{ׯ^$}p[E( UD SRz;MJ%h:|/|XBΠ6s0Lp̥©RB<`IH0Z^w `2 sBxթ솳1r;FI)'򲷾%d `dTfhHt3;)s BP8lsP$3$ȩ8Wg_g"3v,x}!<~?>L["V&F׿\8i5W"c?fiiA䌐;9@ء砊]x V+Јԑe)@̵]Ud]QU05KHqdB%P(NFPc۰$ &#ԫ eJTI⨮i1%cX"، z̑(^;]@eDVO>g ,k܃Q p̦޹by4ƨA\ŒFD**P8FQ( Bd8ڎ:,%祡^{{W_]#"p@{AN9E( Bp#rtX5bة|>&Bd`8 (3ĽJSnh=^AQ AMg>p]yϞ}iJHHW1nzmMD<ƾ{̩j( -HAjDTUMTҾtKW| IӲGb<#`VG/?sfx~O#s6)lɳ^2"!:l9)&ٝC"Cn1 R=TW J1]P(8ȑ7`Ԍ ;')B"I D0EV6L0"R R9{wax, L 9#4q4JJP@ÊfkYzJX 'sdm#o HA&aL<qp C4mNpJ):GP(  "&0aŵ+vpC̏%d scLӾ_{Bc W( a2tR@BF-mvp|f>B?i_G/Z3t~O;v Vߦ3P#:o:ታgO/FZF Ů Pb. Օ U}|j=; R, BB K0#$BRAҚ6K'qls`0j $mVIm AIDL)x ԑU䁴N8Ic'*sO>} '^˜bhڥJ@vjrхj_ N9 BP(49nLH-~&;ǨA!PR 1<~kg~,QQ06Q8 bJ484zGHm╧_|_Yȫ:_a6K[z>O\8I g21+1g]2o#x(]rmϚ-8x#i2۞NCH7P( wap"1]D\y{}U澩zo v  M 6fl)Y4BᎠL`)T\U "M0Hi" Gb1PQt"#ek1-ѬN$ XP; iv҂=&Wws BP8yʦ:T3:6󹾾GcG. g%#147MQ_9/M>ݰjWZ7~12d{|Ѩ3g }pgkW;Bڳe|K[͹KzOiB>ۍY%@MbW!4rO~_/gL!bDp 05"%r?D Bfr8{9|BP(3ֱl& 6(8'F2?pa#9Qn2yiX*R8Ί %B6>UaD$쒥SꮮTF!)AaVUf4!a6Q9a&cUPL)VP!g Q O BP8Q\s'4nFX #%ӈ!dp; %#843xxR:.96nؓ>! EK@GQӌ-#;/8sTExv]E}tS=y^ &tԘ'@ ]~^OΝ|2"F ]]UR+  [v ah^<; ZWzm:[R:Y )AHa P gv3@Jpх";u0ISGU.*PK=+ DO Om2U|iOUvѰ~?V/5ۻgk;AſBPʱF Ɖۮ"BCC6\oi5`;CCOH03K D:%؆*G&j Qv0$av̜}y=d S P3qGB&0+±StBP({Ik6"}ɪ`7`pق@ N|drݻ!9#50Ѹꮢiop{L-(@5Maa)af~v.^G%kE0̧HaԵp͈%`lh=M`aƺFhqfDǺ){VKQZ{>A5*u/so:E8[5O^⃏'ٚ7b/'c!|Bp1R58XP냢,l$-b'j0*30 ]֣/%unP )7L$ 0MДu+<<e̩ /Aw`,ޏ~}籙r2YnbWz&E $jpP( Ƅ+:; X_b56DH @nqз tX`]L}3LAj#Xb!_!Ą.$:mMNC燜$1qCk[uK+ϼPxcV> @ `ߤ)}ߵj&L0[FU9+MBP( BTQtBP(w**XI"=d W*Y6Iଇ0AU< wCFF떎FQKs=]~-yS ڮI<\Ѩ=i|ܖHyIL0̔M'i\Uu>\o9w\0LS"*H# dBP( Bp(:GP( ;lpH9҈XQ$bL(lpUG`nUȱF0b6A¶:m-G[:\^=84@#BLUvG?G(c3nR[r @Z*h}$`U,vKȈa &3"Q@)ߪP( BP(*E( B~G @+׊?ndF1I7JQL0xUe)ǹY`wݼ ˥J"]Zڏ/GWg+1S(' n g{%AB^$IfRv{Y RR$ ,H")G`1RL0vɉ* e4x$Q( BP(NE( BQ$ TLJ@Ģ*R!ȌDra)up#'` R#n~ݝv^kןpLR5⭳scO7lOJHmlP돼PxSۛmduEJ8Y 1iUi'QfQS0KNؑCQ BP( iBP(GH1LDU}Zgaҋ_~ZMa%ԥ={ձ]$[#*.=ʱGafv*X "_Ơ U/S>Ā@9z夤fDOFZp!& LH%4 @bdjG42sd*SBP( BP85P( U "YPU3K^J;}OJٽ_o=|dһ$)v48d)NlX)vbb,AAD@5%F el))0a:r›¡Y) DAWg CB +UBP( BQtBP(#֝fE39z7l={a Bw@v/toq=rً׽bB^Ǯ&Q{ਙ@aN!xnܣJ`dlZ+R23KAVLEޞ"uN(0˷HܔU1dJA %ssdY6BP( BP8P( 3vUY ""|Z~g~k_?P[U1eh~?}ɃL¼J">jGD C w [LiqV4PlH5KJFULlPªMP8( 6 @m8(V=j&##QJȪ`lJ_2Κ BP( BdRtBP(#֙)%3fNׯի/wvԏ0y\ ]7_7m?GQCsx"A-qA? L `2 F/[9 '|(, dJYXi>F` 0"HA XBP( Bp(:GP( fQega1UvHrH1[|Ow_؅'UUR V`dJ;]W{R*d-1 To[Q8hK Q^uJb#Y*1l:Xas2ڊuUP( BP8=P( 3*]ׯ]5V9H3kB_UAbDUգRշ^}K3X6;rbPP:ǐذK9`6]pr@f`"10u˻:!Pᄒ,)UC9pb2f#83Va#e'BP( BP8-I~P8y1ǰ3Tp'a#V7›~Pk߿*WWS^j"#[`ԭ~J&=,oIA&.2Rk&5 ڃ( @bMA3vla;g@c4N l$fXePk>Q}r:!Ec#0MI F^qU,u4k!`PkC CBP(2@CJh=ax$c68Sj !FQeegH#$V3%U #cGP( G9 i%&'֝ϰzBDBƫN*k#&T^uN='ґoY( l S(Sz`@w#50A׮l@>8m}2DX #(i&q>$I̝f2e+ C9 ©卪h qse եSIYC doDI &,Նp,8b"Khg2˪{"Dd(Q4 zD, Ā(Đxc~v`9 '!r[_d6=}hBP("VRPR((* 8+YnWZ5m[ ٘ؔ`%yVaG V ])JQr\m܅BP8 P(V`d4)Aiw©"QHt4[ f_*um]Ej'^6#QЍt*x2"MITD'R  YEu02GSe!_#M)J FHeX( BpʉVz(Ǻ~JǷ[^ pi ΫDȔ%2Ga UJnfLh.h" E weGMY# X^r59`7 N̚BbGޤie3S0{1#h: BP(N9dW l&W_߭$K`+a%$S芡<zp"k/P6JP) [StBpjQbqi(@6y{;5vslz} }<X)z95m]Gsa\*ƣ )κL4) w+r]* x!" #~ Yc69,P2 b3MjV( Bp!c21L&#dmcpd?(P8Vc, 22LoN=W`v$i ɡB4cEb_vmUdc[]o)D*CkX %R,<~moFܹgw,Ŷ)dw|2 Ȉ%r+jEv\3 dPB骨m2e +sBP( SХaL #0L&TŘl< XA61*J` (Y~0;z2HeV( 's S kJףQی8n0 6x u 9V)ظ7p=?/wfyT;; >?4m~-;mP%0wlNP "2Ac.2L~:$o&fZL1"yfP C* BP(r:su9wC{1UyHR241mİQHUR̀((qb[PAW9E( B9 )Fx9cnێu5/c'Hd<7篾Gߔfv̫/tk/'?=m=1 mG090~gjM̪NDd1u]zpt;5<"#2RuԸ BP(7U5=/eS{##"# Pts`#@I EeψӅ'{U]}npH@bĚċdS0*uW[OJXݚgL]hȄABRlM@P( Bcd՘-5^F*Ml)WKo^ 6FHRt"Mur&v4! 5F b̑ BJ0bȨ4. ‰s wmѓA@wӝpKjDd6m}9zDK9ih% E620!DCz@ C@ tn}r3U%""`fdxj~ׯϗnb :]=3g{0Z (`Fd`(y`P( )E﮻wq86 BZb$y.:1aDp΅EgI~ውzw 1bJ""J8Zڮڟ!Ѽ1`Ɖo܅,S,=UBRb%%aSQ( Bpᅌ^\kٵ+^~\+}΃sۏGIuhSb{"IfФP`5c&X3SUŨ``Lj`SQ+ F9 ©O1wصP%*Kr2e yt H_E[$Ǟ{,VւB(}=v4'CK>vs8==;Ԟ[,nf*"K*332净Wddp$OOV~{#px+&HB+Un7<BGi}\  .Ԝq4rI^EΫ}}5 hN yt8/S6R Q],v7ӟ02Ƿ+㫭#qus1qo עU-Brpk@~zg?pqSl֒߸~OyOkld 3h]ǥkswb9< IbB ͝DmqZ FmʱZ8Ld#;wFa֦qɩCZ|ǿG-F@"nJtb_պj1ś[,SA2$M歘GA,>;kL' p4Hc0`?2\׶v2$s%w`jGy۶kꐕ{/l~,@t&JiҬ]Yaw͛E<סWkǥ{puuQ4BuEC!S2k;[쳏ݻҜp`wos㵛֝ٴ9:8usE3;56p`JXB!EOtP u9Owۅ\;?(glΥ5#yƦK'.g,"ʼǹZ"(&mc4"μG34y!f2߆>TtƦPM!@ilxݥI@ug|]! v9`ٺnɍNSc_ӿ 'k}0Q@_|, w# ^LJJkOYq$:Q eYnsn5Xح@%$vH}?Bkal/FK&>䉂֔`V`9öwo^~GUk &5zIDAT0j)RUS GLQB!piաE`Ӟ㺋Ӄ g5dQc?l'ogƯۂPajm8v#5L-Dv piFubL(!-X+H7:H5B; uyo1{Cx opݦ,f/+7S)@)JpS'b.(5$"l2_Y؈ܤ6u?nOMH>Y]LAUhlyVfn ?EG㬹eᖚ|Դ޶)%+eTss3v;*B!}ONX+}2U9@kh'`l* t@Cp]fvê)wmwwgo[׏%PM|uF|' ߣ7 ,ƈ.tWwkԝigaL)5MLB6f*p!EH":لB!M3$ F0,{eOGدkÑ}98ƈaßkIC !@%p^M$|m!!Bc=~Derݳ"wOVM QY\;W~^!dSv ] AOW9B/+VF$#=nzCFAš&SF.vR|S H1NÀO>vH+e0`zUy7gA HA/:2/m骚ޑhruI:?[XZZYfR9f6*mPOQ!E$I XaB!W afS ZʺvV <v5 HΔ`C !ĒPIT$R$>j)MUU Iq;ҫu~B!blӢ6'RAɧ#ާlfPx~c.h[=߈-!ykho7E˳]6@k'1rrZ,Ko}uKG!eqe.z`\s$RJL8;;;>j>砶^+Ţ^뢝 Cdg/R,_>!đFr#-0Z p[R@@g@kDM_m*`3xU8M @2:RJ eO>=s7e&lA5A:&poL5 p^?ɵn>;w;Y8X f3@1RJHU7)!{ :v|17"o!BOmD ][f;B[n;aVD4KZcaA{tr~]FK ^`>+H+by !S߁CPM|wCq 0꠪A{x^uK Sw=xryϛY,88cb>.qZȘكm][֛߂w]s|B!+&s@#Фvfc{<[?7o`8tm, 0C)c٪v4kxG}wQ*rMkoKy~rd SOe%崁>%l)DRM{X#h2$l:H>*WU&ܦjT?[b(& nqIF;5xhފSIrG)>~" ;{szá̐Ҭ욫C6uAϞW_#B!"Ci袣ۣ[OZMeڢCD@nìkt(0N> )7"Bx k~&g2o$gVO Kk"I0r"SC+6MjCg͆$|&bjOn"M 3D$ui-p+h0"+m6 oV/(( FWTKrda 5ڤ:JС*o,[߹qٕ9lb5edݺ7n}6́Px gţ^_攋0G!+@>O;o܈Zv~~D;Qc"$#%š:5^:U=kG/LCsoٶMCM8!s@y)矢x*C/+mӘMWe6r6;-:.z@m?[RGFE ],j7hp(ͧ !mKѴ tJ#,xBJIL'$a4/d$qWZL9?>˯b)Pڔ[~o}ݿw tv ɱM$ں/(p쳯{!BxΜd9l, 1r٠"~K|XW L8pG?ҼѰF!ˍfW愴v[dh mx"¾ 8#PsobyelZ^Cp]D_WzIZ`ElÄyZt>*)ZaS]JqփHػT$Abbݬki"(c=5F`nxwSʫYT1Y-8Фte$"p:2e78P ,MF"ti]rMiOV1ώ{w~?=7zt`@n\z:Kw Y.S3n#ߥB!D\'pHnFaU|!Sgr?:?_.y?_s}b:GEq5[o~?or@g}: lH&(Ƅn kI$|N pVNaL]Tuz$pZ_".+2+L`K&2+ќ㩊8G4 2=-;@S|:) PAAݔ { wHQqH27&֬qmA@hr`7_aȫ$+DA)9 A BaٝN=P\Os&I0tt8h+47u>jZӏk:&3)$zyߥqzϥ8 hZhbK 4<\[ cQ8/ `"6 J2M%[{nrZpr͏`|ӵYZ#U Q)mx99d $Sgp9P$@j9k>pD#}% ,uԶɀyy8ԡ[E馌R$cp`+ pC2xkȶPr4י,eC %jv[$|ZZBC@0}]^WNhL'ɛ[/>GM݂úw~o]Lbu)峋u&nC!tqLh| RA2|1b{o }v|Z{׎~YQl>;^ode²Z#2%ɐ60y/$̏^yGDYǔױ&ذ0 /-(/n#Dc?"3V l*mE[=S= AJPQi 3$95&kI.{̓%P>UGΊ,̊0J\*HWhNʽ3]|.{UKh[4$YGWzW!4g[[~7O|y7޼q|t+lD6wj2f.!BxV rqw(/&/NX OC\>i;9]9Z}ցт=Ρͦ\jvnb.b<9apiRd2&w]a+^CاlJF=d#0^/*>΋&:@RC%AsId@6$ʔU9$JV_o0NMC#Ȇd||{ӳoݿGWeb,Pw,>{=wݗ`=!. <ѓj2kfiI0֥!f]7}!Bx&Zui4mAeMھ+IZ83L<;1-"&,hf6{)-5ґ,V|P+ e4wm6`=' Y3=l:2 õpڼ皀Dϴ IZEhV5K@@YBR{q@k igSzNYlOMTim;;{:gW>;?`-l60e)xpx7yW,h`\Ws/RAq?Y`!Mpx*Bb'mu9,vcRR$J:܋q6mU_̦2GӅI.9 b0"64k슛9P z5 a"aSf'O0"mR}6蛲ZBi0MZhZ @!_t(ۤ~[{CԩNNݽ"($.CmMmgRnI Ϯkswu<ډ泙.W^׾^A@6؈| #VホCJ̆43"=R.B!Uqݭ!R0^>m!X$3j)%N(D6YqgJ)t?%nLbNM1bㄹVֽbffm@u?a.*L|iN #dXFЄJnK=ՊSSCU}8@ )f h ,A YɲgSP$$pʸqACB1dk. 6k\3*}Oiq312revVRup1f68hu#=*u 43wMLjj*ƒExN[&C!_.0],:Y Bn%K x) ww@!snb\k*i ƨea)$"Ir'a.Ztl76or/"K)~J@ 0f8IN76n°NQ{%<)Pf+Cm˹fhJsM-Z"NqM/mhկץMWF]3Iyq~=X,rggf#ُEgg؁!f3%)t3ziVRbӶNټvL"B!B2*Fs5)lJ23kc.lR&Ԇa$IRn֡fsXN],49UL9Fi*6U2̀DRJ^TuK!<#q pZ 6i~/\Osd!O)&&=׼#>3@^@>3>ܢ(Z$ k3,b$M#вeoMYphG7$2@TYP"Is9aS#nt,x`6[Pr|~In,URrޖߪGsllj* cQS9mS s5jX>!3eR7߽xl+kd,țIlbĀ;LxԾiH ل4 0oZ{~ y+rkj]Д8sOwۆ7q_ ![9B؟ ow y X kH~!F]mlJb-+^׋JAon)χVz=sko,}mg 4Q98,9.31!2}S#QZxѐYMq$@V(, y0± Z$sBaw~zf^ucrUW$v\cˠ~i`1ՃQ1ױf})#5:bY8G{UWd&*萂m}ma^]Gr@C+3ٽ tݢ~q 4H)*z6t`gYXӜbp/oy]viQ`L*> c[Xkr'cAq?/ RmMG ,P8xΥ1+ő܃툱JJ>c]/ήuYJ{pV\܋sˣ$yf1$!*$@6R;xhtՁ4U ַ$G6@EDkg6+0(lt–)"lm e9kYcN-v]"if@i`9@"QtƁMc[jۜp|ȞeQ/M:t Ob-؃#=B{ToA3h=R37c6&Rw'c4:uFwlNhʳ~4ϹAThS$?'Mn8Cxf"^="N-Kϡ]z<]6!fį`۬1Q*yqSwr FM@/R ź. mQW7;1D^ `DrvfМӯKdO j:j :Δ6媦~F<4N.0F#\{$5l.xv !\.Nw yX!{6> 7aZmwh4^aKb?<:7L~1obB8Sʍ8<@}]̎n.W~Xf/pB dAokGtQpBaf`]3Dhb>< ./3Ս@ԞM;|]ru{]Omx@"c84dSz4mLvΩSwJEf|xb^[] 0P(0]{ð)tl_VF1aQ$ܾ-BVgwOǫ(rI=S1Ñ/O!cwPspuQ !|BjBEXqX}2;ךOx-R/gu]ip>_ͯn?62qP9M_ܛ]I. M94h.+YR;av ~cC13< 2v9X8ةYo}* ВH-+_/x_6%v:?o3`P׾*@u<MPyC! ڦNGJ!B!|=9fu`C2uNJwpc f B0͇u*XblҔӿ?[`$Ҟ{%=5Q<8oƼuXy,B^-ַۚsc GB!qLDrNeӞr9.BB`m^@M_I )y SѤ7k1ȵ][:1oeYz/=BP74 !B! !<<tTi.u)y_|z艼(SݪJ@HcRt$(!g:@{tN=@ RyƢ[jSf BٙvVw&$`x~Хt89qBAB!Sq*Z! ""b C.`ȓf`A!i {kh4!Iǔ8Z $x (a ny (YՐ3MZ0oGB!geiZݐ !B!%!<3FNۼ^g`@8!,i IRs1a퐡ISu7O75S L kfB9ժ'RJə}!B!"^&sW @ACt;~5k8p`sZ's(J)2+rM  ɘM"]bQR8B!gux@VnJAWߵ$B!H9BxV`yWonDɑ 0aƋ{?KtBL7nP .%7E:EW\ !—!עU$)IHWB!B[9BxfYrwnon[L2mώn d2b L H@̷}Jq`JCq>T\ŌFGrk "B!<6 +v6%I$$"bBsB!—qgSꋧϲW: р B) Pq#, Gܐ ?bi0 ɆcRB{lrmg)5I>Z{B!Bx%D#|o j]i=p(X"+@P0 7$eRMq15BO)XMѦ Fqv"0!BxFd7syΝh:""Mn.v7B!pE#\\T E7 @B(Eq=[t5!qi[b73tw:aRs8H趼'ǃ^&9hJ ӳ9B᩠#9К.BԷ! 4>7[+0Mp⒴5V\ۿj=-Ag|tW:r6-M%Mc:+OŽom9\ݝ.SK)#JV=eJI7zZC>uQ}@v >8<;B! &D:݉nָY6jUO9KnYtEJ7@/1RR7,JB8'l\."Yf9WWwIG?]̎W_7#_HfA\ye~6~K/W7}sM]*^>oIӎJY[zy෷H+9gYr*|zrfnF//!BqpbLXs:QDAᨍNj`39|_E*ZSl`jTNqqڕHƸB!LDINw/}׮\ W8qe=~ߟe鲿qɈ-fEQ tAo=YḼ{v֔"? 0; Q)q98bF4-ݬ_P۪8A7Ā5B!"^".t13:6Se3F  DL̉٭ܥW;(b!BL J3VY??Ӄ3= Mް<' ŖX ̞g?7߼uQjFHk6{2/1oRN!oKrMZ?;;Y r3}Mxp3Ha=Jd6cbvcߏ | zB!^T/zZZ6  S}p z^q,@OŌ)Qk*9B!W^tu9$mL}?;}ÃeO0G B\蟔=>:<:hvY~w4w~˳ͱW]7pqd\'o9A-0;LLI\: uζv;sh}ΥO/B!8Gxd SRQ)i%2ɚE ԜRFݽiKT !BHc8ُN q&6͐[A:|4"24I`]>9Z 7޳wMT:v<A0n,qtW>܆@ƞv{oѩo>7B!BxD#<Xa+L  S2G6_^/B50GaL$S*N)Y{!sIҌ:,d(d^~G}}H8[uВkƘ 4OIV"ePZqt[,$$-Պ]ri1u#OpErvYQky޿@mTĦO=&a B!Bqu !!Sc"^WF"cJGAY"0`{b^ܒafRTC!W94)ټa* 9%YھHmcf:̞xA{F3rW>Dmagm]zGڕf.%v$`4!juWt!nϭB!b8GxyS+ip?C\f$8u?e /pNvvZ.B`(D B! J$%!jKkNS)`ԑ\: B z֩5jE0Hr5}mgW~tOV9Lka˔Ţ=hfeѼ}Aa+zb9S]9wkB!BxD#T|(VEE;G;CZES#@@8G=sA@w(ʘƒT 5#`B!<_(:Kw7&bj2`0GK.0!ƅPd慣#9Ji㫯]?;!0F hX-[ScƕNxjP֘ MQ~wO(ZB!^@/ltAC%5c4BSF4bqx}w63Z 0R!+Oʶa٨$XkwatcAVvL0otc.+Oyмύ&-ŷ}3[?ͼɍaJ|g[m+lh6i熩cG:)ͧߋc\mbQB!p)D#Dhg'2k M/F7.ɀ sޠhL$!B6sgsvz_0s3=@2a <$\[uWu f˞G]`;pzホ4e=niP4>7|wIM Z6j߈o<}NOMF;I%$QIތHE0&bHQS\7p"(j3VKAR/7]SM\){$oކg(6m!"rz;D_vj jww~⫮pCjN4AB 7-v{@ `zd8}ўjId.$ƌϰˌPU͹db#^zb-ׇ"oI۫aMVƹHR٤q|zg:q6Y9PPa}d;CusOl6Om>Ləa;xttsoSӊ7 YX)Ks?+8:/Mp@w^9qqasd?O|D$Ȩ~?gv-|;әi}5=|bypES>8xiy:Yd}\LE#lD/NR['krȥ{=ƽFhLL#@'p@3I8t4E)䬝888 . KzBظx ͚u;H+IVHz뽕R0,RDm!-"eZB @UsZsHk(A(Hr$S1}/D7tl"uk pKPB;P{o!& .|5ٰ_>lg܊݉KHsl^k ]HiJi"K58@@1?w>ͽ{oU;0'3y?!lYD ieXs6-rw7{?_/]3zkok>[=k"s}jGNwN逸@;7C Iva!6ӈ4CS$ĕ ԡ+Xdˁ o(q65!; &N1wp&$ ⍄|zBOr/YD% "9 Pߧ '=E)9O' 0e D7/&\*4`zW,6 >OvsO9= s>44S^K SXu '"$瞟ԚSe9J T]T˃!~@ ٥':YwlkS.fQs\"Zim9^Q|v 9-_ŢE=.SݞG9Df- !oؽ ! 6uӺ"̐7ޒ8뿼s{9 `됯̝8=wgwf)kgދid=.!Snjje6 MjܵYdԭ2G.XJ3mp(r>'#3R||>vEfܫ;=5X+:K9~;{u'Go\ fzI/3]:!.]RKPp>X%:vyNCujT$']fh2΄Zi*؞K{9^_v>?C$vvᘡ7v|6u k)ov Um OqI=1vNBPK}8+j`rm] VXU揋s\"[z&n{Bxf_o)=^8:C2#\̪a7߹z/>/__?Fo;G׿p,MS#dzfVmprtB@BiBIf: HsR^d^9Ke! ]RSN\@M@P\HfP.^۾ʚC#N8F(*ݚ/sj4"$)e;ZݪXGOV08""L)I9Ӗ"ZCJ\ˣu"F9^Q0 PWjj6ȉsJt7lcM %^J)/&DIpϗ`c {DG"E3S J)jKpwڦoL%zI Umש!P ŭH.Ʉҵyw|evu}'<8׾}xk}h\bH, kj/ipavKTͼI|:Uq4M Mó4@hͽNM8qڌԥfJ>ln" =C"~;qs9Cxq9G}jhz"/&?_utб~xn}\^3)>9W"7Mi,A[^]b~DPVz\VVWNs.ۍ]UsdВ;账9TL)BNݪS^NiEZ5'Q0朅Vj߭M~ad=u]"=>V9NA` Cd?utHZ=K#9XjJnRmN-S%36߉͚=1nѪsuvё4ŔP=Y@j,߬"0LIrB"^L9M4XK2۶$xWMӐ 81(.;44GӶm =>Re9.Ҷz4~VG9^] h4mv\j.ijAzQϑRB)%M}TP.MBJx2.pH~i)%kSKmq]oO2$[-_BZ@@F:,:0%4As 37Y4dL4NcH c:6Oۥ9$ 8=&'^KM z$[i}@3j.O~F8mOI)sd c7 ddv[4"tFvj 6%H))fR6αs~Kxhu^s$}` 7LM}}Z%3KF) Q|_}CwKH=WB$͐kzH%& 5FQ/OZU#jAW>/k0h;8j}Y^V+ ݪ9IFrtUDZ!& PJy!̬뺶m[a&:CbG7q";ڒ+@(FG>-rR{S[e0*N#OюҰk簝8;:z*0iL!t(HJ xa\_R@kv6fu邏I>:Q /]N+bN -8uG3IMugw(F:de^b1(@',88$@̊, `(HPE58jHz~h0uZJYOsxdXK?.wMW:˫ɒ^4u1Daje_d,&F]#~Z7sZ'xQ$nu)9״Kv/.>X̞lR|SZX@-w۞1A=^:!ӳǎ62@@19M%"%ÝF4$l:rN6J_t` a҅kB A\ 9pS5uL\7unF`swdt;@: !nj-HNS*M^HKIt%,01r%:LRVX)7L$(J8 Km۟czuIZb1S%@BogHr6Gs(||ݸ͚ +b+R OUpk|n^La^IUDۦx]vת]EzoO6?%u]jOV^תN, ]W̴F52z3sz>m7K=Þ]pbz-۴d&KCΝ#"_Ѷ}LOV]!ZL##N۝2۵z P![fwݘTcijp(0+1Ч=i`:~^bg9vzP7^^PtfN f&/ "."" } t>Ƕq}?ݖ|u#xUku T*PB$|)w.RHpU۝ /E>6c{^q4FlP_BtQ&>=6m7A庬3NWPFNiXnsl_۾ R$[znxO4ǜG9>/~NgJljPtZc1xd5%R+Rn.y*wqWvXMF{B_ϱ9Q^]$2{n .&s']`=y'g'NŻ7o׾Y{"`5}%P*w zTa(cۥ Osd 6ƃ>6ҚcD^,n<)NM 6]4M$f!ðm9v^nxo_T.WmZ bE)yxN/q<ubj{l>>:MAO("*K2 q4*&Tˆ!CsF@ TXD4B<5=xv4bt[al:K` 0s1BuJh M\74٘@q09{g;eD{Xg:ϖw>O[L=Kr~ݟ[џ p$SR7>縸.Di"`@vKL>{W_ɽejƮp\_/{W;wWO3l ķ*wԞ6NGV59-n^"IrJm O9дnCw{r|r<<5~{]?mU~MxE~m||=$4%vv>'0 # g3}hjԭ 6[pn(5z 0y_z8?bkŌ괝_pӯk3 d/:u84 a;]n&3SlևowE˄~}m12UUR.@ul9qWV}ֲaҧrJ=ENԦLq8Qy &.S"0+D0MMƔR7m$G9~Zg+u$ҫcm/wbLmZHZ=cL{B4D~u '>nիBؤq'T rflKRJ,"ZDŽ8H@Y0_`7}")ueHť.2Am(wAמg7׎r3.\@!{?GN)lvY̶v/^JsǮ:a_k̺%}Wv?G8:pAܔA 3CMu{\NΚ|`_DŽ 3_x=[2=%B3*8`/@/'@T]'DNU_a^{ouݶܗ?.wz<·%ַmkf`]IUU\iIj<ݛ)t]`WqB!^ΝnMsbDe-Hg1PH +nWvJܫa/J?4g?i^hv)uFx}Yu۲kc~~ ϻYSJ3e#}~ё2QP\qK w?>=GWQf> 9 ڢ57^ 0cmEfC,q[|3z$C-bkYV?'7x[Gu_zW@(M5^& !B!Bxhmqr9ź$fEŌ=kHdZW{o31ݍhm{!tL0ﭜbv>oyr?Ζ9bT}d-a"Ppd(?m],zb <% }J:|1~ ן$zѬV5"}v~>4GcJ 4OM-.OB"B!B!9}6~5i16%٘NڐH38|Њ@iv|BH" g&G30%{٬хa<-!?9{w@ɍ%=7s)+~87og.lDMj.d4|;mMW.n`+^>!B!Bx`&k=\pvvLH>IY077ujqTMWpQwQ&izI? t Z"wD;Q$z.i;HpnTuռ:t+{o~|n}7.hMBи-ri~ܽѧ,?͢9LH,*X@jWd!"B!B!\\\KOI>FŊ0REjޅYY.4Tj s\Q˹uISOs5-AnDa=gAj1wWggAE%3x;d'_9rS kʼȤ QHYp맟Ӭ9HJ>'AbI0B!B!4cH}A!}u8@p1-U'An8(fɍt+gQz)a#.ZdY/%^ @"jJ44Z܅`kH>?q着#|$!0Mt}]NcD@sBx)E#B!B9K)|6+PGGEՀь@04M3 l6Z۶ ěT@3dQiwIl9K)DM*YE3[O  ṳR]Bb cP2nw/_tzɌ`6D4e*9@5i!d~ʕX+ϴ-MpA"3\_F&$[ Oc1`2{^fι ss9sιse\I2ƥo|y>kO`}B]8J%VC;|ҫlij!b#|BEbB&a5)@W/}3Tс,J\KYS)/^M7&^@Y53nLy$ZVD 6?T*'ݠlc`,`_t]`s9s9 VYn'kLpx: lIO̧/y~9zaB4uK -4w_?dJG") V'GNɽޮNŢ U9YZtN׿{ 9HDԥ&,*Y:J*Zde >dC-U4 nK.u @i 39s9s9ܹ2.ņ(õW[3~^uET5Mݫ&2z9B5 &p(tB$TW^~'ew0_3hr~iWAOĩ lCL&ϲCE⊯إ}x,CȶAfb[nz&ML% ¦/Vis9s9s0R-rlTİi.JaI9)g#SG86hsED*,<@?ílvNj쌴TD`6?/a2buVi1>YgM3ޤ6:} F ϶F!J X藫rFwOMҴ՛WKӾ ]ꗋkV}*ҜJ r a@@!: 8\uƥiL%8X,9wxs9{: Ӯ+ IIxh8=V-TqV_ݨ&תUN#i$4s63qV*0#el眻pTK4 h*aԴڝ}J]R* B\Łg֑XUUA7; `dL$e@ K3guH/}H}P6ʥ2,o]>Ͽ%kY-cT;U3% ,9b:[j^<3R+KrÓ brrV!k_j.+R%1>f_ wNw4*HD R1+МJ:f]/uTs3F5q :gS!Sds|6>DDTJ!s8 (UhB]Qa*W9}1))2֦a$9,ys9{:B^ N>8|w%}ePJ0Bjj QkPUn=i@'>9 !`(kgL)JB@F@ cjWml6O: W8(j3dA`LJ9C6VP*cl9s/9s9sOa:7PLe7 @3+X<[{5EI;Yn0U9" >~Q r,h;ƻέ٠hyUmaWT`# ` bFRS-TTŨb^6|*9^s8s96e8Lπ3lp&s9omǪ1>6s ȘĔwRX rLdBpqι9sι %is]yU897½9EɸڼQ!5b/BS H <.[f:>9wm{%q.>V90,(A %笚aBQ,R a\.mj|'&b}~ *ys9{jr~HQCM`&T}3Y.̗C`|lɻsι)BH-Dl%w~5NA5 9-U1ʹ~$120,dc~;-/Bz{qys9{:LcS{$T3DFPui!@n_slYɣK*LT) ],\BE e/!LM &P@uOE H F_؎|&P;{xs9{ZljTM[oPF`DPJrNTa1<םޝsΝUrH""hCN/$Mv2ug03X 96b20Vjj{|xr8 X{˻{7.^\G ) Sd:[@t,8;sss9a*tywrc Y*)F vxA_7ĵxl`5dhP6̝s=ys؆w胾?*HQjZKWoti8)2gןBjf0xU[{ys9{zy&Q a4NO5 YO10oj+zZHa߯#mxWKv&rν ևa}d ""AIy^hY{??[,VMrI)͓7_o_w\OC,|NixP1Aa аF%d9ogx2u_ ?x9>mm86[6Rv ta]N8P?cXw]|F"0Xׯݺ{A;Q{0__ `aI @` z/?M[]_lKK1L&`Z@\ qRr.*r""X@Fj4.!=ΞMD!a>¾_21Hep +O])ү0B#bB!btâ* mRXB|K9s9TM=A~nd:sraTe)&BU{u;9xm}Lr>[Fcww`>_;`܋`sqocB]שdbqY~d^oeV }yULC/_-* g}Y5:v;Ħծ X|դ: hN\E%U-(cj#sϭ3%t^:#IS#Ѱ5Ձ8z %u4-YV鴭ۄ 2i.0-`L!9s9s00jH)k4ic^>9~0ʰPٙ{,7^0qe;>>S_y<] zm7{']o>3~bJ0{_~ :Fvb !!|W<|~I&Mk)JBc~dv?}秥$V~C Z]7+b2,f>uTAm"*(('{wNw(3"Lۿ'νp~GP *n?<}wݑz%uaS^\`5]$VCJU Tjs_{zqdryFΜ* TEd)HU'f60` R?G[ۉe0ȢX4oIB \jIa"(hݰZ֓QzUTLAy"x|8{jHϾq勶o6C^ |6RUuH6 4KX7C2Y$@r8yHaA۝&4Lm4 2@E)MdqVӃKSĦ6툋g8lY2&_=jwzŤK4CN(T9 J/ќTӃIZ񳬧ŸDs9Sb2hjuK9 %e+@,QX% jfc=+i^d S@Iw}2R!s)eڴ9g!!h;<[Uu03 Y2!LƱj~0%R;=9uĤi&3]GwÌAdc3?ι'ftdlɦ\@ $lolWec bd P-Z4* M6Kep`5ff ̜`sp'WJwP߅!,۶>'B*$]9Vy?399F׵33@"CE#]/ f.WFQNtVu,"DD̤γ#f({ ` cN 2j5PK!C ss9Ӳ!N6.oG򸉔;nu0nA5ֻOi$0XHDd,ȊqPν6GhP d$,Mkdׯ|OV^PZc!2`%YF`9t):QiRSbUAE !'3cA9s9sOt]?.Xm޾uH14Vxt(ADBH=Lq2PVݼp[X]+R)!gd`t&qA~#{)(LlMC= q #1m+hRKh;#眔-1^qLg6uU* %b@GL`0de؂ fSj>?9s9#P6zF@cnAPd!Qɱ&2A1B1+%!eq=ӆιzzz.sRJ̫>ܸ-b! u9dR +=>"~W~׫{ПLĊ׌}:e]~8!cG!2z6 RJFjdD șY40TssSʱ\#sNDU)e_U=:{>4nQl.]?͆"EUxj L7UqS"6Q86dzS9s9siQ#T7j+(A }|-Ϻ1Df@_ù:\C fT ,`7C?<]_(Q% p~+XVSL=Ȼ J|',WH`AY 53 [At6CNdzuYwK!XH]דДTC!03Wwh?QfW"M &Ȕ ĩXa2MбBtO9s9siGk zUe|C80؏+񺷕w\dg0R#(VŢjO~w."qҥn^>]^3Iι ]dP&M5VWn]ŽALf-:k/~~a0xHlgReNG&-JM@D&oF0TPUԖf'S, R@J2Wsg`" L̈́y-ȔD }6Lj3c2( !>s[` AlTM*LA0E!9s9sia7pfM|elZ=ֱk輹c>N j&l 䮚Uմiuu_g?y?RD"|DOKm|[!V֓{<'8.2zMi]KGhN`RU׿ד'±iW_}H Mji?YsYSsO}קjBd `Ũ`Ub~;/ )/9Qe@++dФׁ#ð{uQu" g|~H 눍T^~+ XUR9QwKDB#Orwܴ^^޿rW$y;H;>YTTi""DF$03<9 JY}󭝢Ο;~ymvĂ01!e 7n| T8HRM%ǣۆ?}FW٦BU3k_~b{'LJMؙƆJT)3gno'W1,Y4.9s9s1" J`Nf6w~mbY6;M+%m뚋av.7; 0G΃uw}2"6RjcZ)m'8`33M>$W.dz퍉t>*CPUs][{""CNƐazxhXeR_MKW^٧}d`U4`C FR C)lϿsʀ) *+7vO?Fhڰ7ٻ5 2wj~pιT)&P%T$P";p-RYeygkY՜fSU beJR* }`Ҵm3:FG}~ F@ 6B7uUKaXiݠvd8"FVhk醁#O';b8xxs9s`czQ0q:O'=FP;-=( lzRɴw3CUdn\13s1DdF03UiΉbǿ71P3,"a2! EAd'RHTKCQD" 0s.1^p(`;t1YЧ2,bl&5YFJҤl&F 2b?z*ն>sƨ#WbF,RьLPj1!#20`j/*)V0S#w<*r( cQTĈ`բL@AUHK` ZM8P3:49s9sB.BX-5pY+A65` R% 2W5 )%he9`6h` c6躅t TY׭1s}.JuX8u] dS sDǏAJy&[c/ss9眻 hpABbvv) 㺗b(ip^Gۘ}s}c/[ҺaәP L {,;^m 99tf#"]f d|*̛֯"1@G%"Cs9s](]2 QbfaVn1iqz!*+ư R2_\~jEX؆dJ뗪p9sι M0f(ĐMcN_ޞ=8= D:aF;(|*&_t}&O8X(ayc*m׍o'x{__0?CL@e0P063f@d0slPVyUǗ9s9s]UCrP8Tb{''GˡQT7충`x9|R<ث_*&%f-ιO؈d@l(4Zv𸵜v%ι8fc0WnW+&\ +1:$/qS C9s9s]Lb  <' 7Ubn9:UIWW_]^DR s+zM`:)vr qNVDd*ֻǪ֊t PimaEv6PC(ҫ9Ǘ9s9s] R X|06k0yiX `f7^{{'^$66& l qYa`%diɭg7|ẃ }٥DG/gL:9{Ǿ'} LFZ$+)kE  h39(3` 8|G>C羸1`d;m>==P|ln`dO9s9wA(=z\HMPhz%~λ7oWd:tZPH;BPpyﭟW9^Rrf Rư`݉Œ$VP Jlޢ CJPIFo>cmWH16HǦW4l:3Zu36}6>A+EdATM.(+0"Z2wIV}6"P@Ƣ2Jc |(m?%L:/19Aj(si.6'm9)Ka5S[f3"703g SAPH3k!:@E9ˢUd`"l$on~JG_okçf*c,xz9ԑ!j $1?<89>I9mrOq'=/zgfWw90 xɕ\GJ+07px0E9s9s]tf>^c43UU|RT"!fA̅JK)(Upޏ0` # oB1C :BG*Ad̛]Oulodl/n2 *6 ]+25z"{ؘzګw~VaY%LSWu=JF 4M;)ke%\*쵙2, c 46c%sJE9s9s]4f}R"+L ffDLDRlx \XAVbF32 A*ldJ*h=/"#|F (1tXhB@ *@VVV׷mfI3@*.dqihVˁ4#'^O٥$!F-NtFcr y?J{ׅ+%f Km exhnu A@@R6z;y;^94btE#s9s9.#UZx 6  Uڬ6l=B"e<`~d6 ޜ7K?pu'-ZBjV:Ƭ P`__6&1f1KN.fWڦ;滷wYVT738[\L^JeK[[q=}lL*TTa 6 j`! BVz:P"}*0Vl1s=5/8]d6j?{?<েy vx@@!U-]ݠbt+ߺv<dh"]dIΙ "Ae}XL@ @K5< 9gf0?t P{y?99s9.۔c0:>KF26Pzy|8C Ue^?9qDG72?ݞ|cgs \Vey;ܝךɯ_4qBVAlL8lbl~D?oa: MNw$b$B"Au8.>\ r*q'65?ιNA0e`V6Dۺ;2J$o3&uYs)9s9s5 @"cIjl:"ZȘmۤD~'ƾ4`#RyY۟.NiM@D!1EQ}w~×!HBIlbY)sNB񐁌8p{ܿyfL. ,D& "@%4ia7dr{6uLٛ8L(6ǡ SVuAARj4$EmX ˣTRiݪ9.9s9s,zcH aWF0cn9zRTUZ޽?|;_lvnETޯܾK_>|=D3h. bzQXOF8J;Gˣa.Tk2)Li(ʈwoڭYӘuιτ`ref3`h @0(dvP"'S L=8s=Ms8 ) `P 5@04W#~At㻅sG ~'l+``%$Ya)E [f@3+sνض}q(Pz7#r*lm ι϶}ݍ#50>r8X!Q##vhAͼp F:VD QB)F$Ufed)je\WUK2u$=<9.t/w&61S.…X XJy?sPcLr:DtQ$bT@j4uVǪO:D]XJ:ۙa.)t:״oa.9 ] R=6~E;;^ O? s!:W"q`V2bڇ{|;0L?wΡUR3R\ є հ+(6D3TVZo߷!Jl{'Uݕ%#AUrb h!%0`y]9.%91#ma5-r Ȉ ĬVeR@l`H`FPis? z1.~ymad̜ϜY S߿Șu"Īd*h(X7PS^ܹTztc3`\+/3*=Yާk&KjSE32j"6Ɵr7 d+B69 2EF;A*cP)C)߻۵W.-*zB,=ys@u^ҷ[vFXDbB)=$[bz鍃Wߔ^bѨ;rrٶc.8!1(RwEi c-z<ns fj!VP)̖U2Jxkm#uD-0en&;lAg2|zO{a崌b`;LX.pÿԧ}RP r`eMfc'CιHXAʕ=;'Huhwo^2tr^i)SlL̦QH`uιi9g"X}aׯ_WU":88ý042^'뜣dB_WydQ$p[.4 9||e{HD/+\nd2jø[+و ES un:xa h}>ݷq9܋LȍZ{ﶋj݊ @ &A^w=L߫ vw(Fc*:s>)A69*Tp(޼q{R\,q6@T,7_ͨRPaHmEIǞ0|oc1$aXQ@dw>^䬫@.l+9@<$d̪( b1:,(i*)T~%$;;z  i) #eCYeƚJ)`b0_^93O-06~տ_G? !0sYUo޼޺uhs_clta\vu|![ !)N0ND>|pت7{tyݛx][buSUV+ijE/@&x!&圔͌sι5BVsM*uhZ13IT-2fM{ '? ٴL&)#V 3;gsL~̩$3#$)_yڇ{a:q4VT,ݘ7hC`IZW)t}?W3f$i7zs/U&>BX@fRKV꼝sOTDXa1G>p{?IJ)_1{zr ɐ!TK63¶ P2G{.=͜cXf|>޽O?_ooow1 0vf4&bELEEWw~e+դ։ZF^ιu;ꠓ'Z͜:!禪궅?\ Hf;HTUU]*}ӝs5XN;j@0N&ran~=X~xgrFN0V 1#;w 8 SYߚ޺z޽(uq)PLi;{=b&T%۾9>ޜkӃ;''eTdș㮱*Q.V$*dyشUJ nhOL?wSRH;?|xZW3>GxxR>߾TtX6qXpaD/VFeMQBl%Ij͒û~/iQNa/ؿ1ٹ"z+"/vY cQi?Ľ`AkR,pOFR45)?xgku3K43M@ #xs[5fD Tuu[R:88w^___u~yUL&)oomck9ţ~( T2'Xk*= 'PsAJ8ߵ9 EEd  Eh= VAIKNc:sEtCZt}IL$D@A2.BŤ@*{\r0 e dlg %9"=H2} a< ^Zj b޺wv7~ombfl) ]ۄOuliZEaPIejzSMv0pRArO}㗮US]P5(:XO(cs`uٝawogIe@ 5Qe"@#P,TryNn^~+T4" /֠ι <vvv_߽~j7>|oM7² 4[(PA~TjwxxbZ*V)kf3}@FQM}@~sΝ1T9B(Ed+Q(lЃƽ_1ι&Qd.1xWIx0h[7yv_}!Ү"[n|,p)e 1cڣ 3B߫207B `F9[e#ٕM'8R>˳K> А j+Ȉ=dD齮W (3EpVX)&Tn;oP`0"`wι sXU0v7u=ݿwǿmѿW^iVUݿjs=ZaP6HXso'a jH`%X(CvL{Z=$B*̻`H1XvݼisψI@sx?wgЀa@݌o#,064`/\:^~xiE d ebѡٕv8.'9QĶBU0 "{{ՊK)M0!ݸq?OOOns?eP&J@b7l }ȌMdB *Dg<`;sCyl幽gfLAacDSsιsE?O*kkcGJi Byc)޽{Hm^M܅CgJh] YMgp'zx IPzpH3,=폽EiS'6Qf9a Ӄt GA94 4QઞޮT")3)x\3&{> (3 ROfS\9QEbBউTZUT5J1SCX?ޟ  00,(Y.F TMM10#0abK;LkjThzY0 %#F(G@LdҢyHXHi3- 뜻P|U519aжmW^#c)NOOg߿_JNDd]8|f:r%@@mg'G뢍ꍋCʠ j=yNV() (ص7oÞi&?n;g{3+FcFSf!lweP3̾sVŗX c- bH@Pq*0Y5IɌ A_UsB}r9۳?b+{L:A!{;kV{^2&b\LRJҢPb5c*HdzȢwydd>u*m@LAZ RJ9ERrId BzBodsM Pc]i&ydJX*r8JhhItTxcla R9w|lrCU¦8cx20w'b1ms1uqof66 eP 0:>RՠK!**Xcc.BDXAX1lX_e1Py{'7ހ)1[eдn2S l~R-u/5~ւe`KVÃɥPrMԨ!/$v=ausW??/<'mZ7NT$L|Z\/B/F[)2*LXK0C1Nz'4X!ơ@ 26P3qDY@;8;c{*l|y{&k1 Zn%ۜ\ &cEylc eu(3aܣe@xj`5.P>v5Hj)XHŌKA*dfBET4nI=KdZUMmt@eb d|iUfX_Ԡ^MfUe3\L@譯s](s8PUU@1Ff4{-"@y*(=Z5#{cF6v;j@2ZbIDafUSυs} ئl]kAxUfOjHQK<}&n]:6G2ػn6H|ܓ Ea$qmgmq,ǯWؙJ(@EDBDdSQQw1 p!LܶvKp[m X 4㡾`uHCJxecP`,E9skDAgǀ3iJ6"ȽRiWV\ci(c+ @Flƛْt]oy7?PeVAk]"r0jqoD Fs_9r (fvw?s!/_y-Ini|əQ!c}) d((KPiP dci64VlMl3qhƶ  G&9.gH] ZIeC+3Åu}]}UU!6 kmi0a1(XK.`a + V( R6=}c1| vSMz$١1LX!>' DȈ"ؘ hQ"Z`%q*aT@ⱴsπWn:yûiAm#C9Tp:ߟWj֞!mz8nd=OsιyD9ؿ uUJh*n5~U CaC:XwRVҲ*Ђ`a0*Jc(/*dRa>ifiMaB:LdLT +d.n aeAs*:H!D̪q>xTBpI!{Edq,А˹P9(S}G2c#@Œ`σ&ɮ~9\ugF6F4$V CM2^5C!#H`"B̨g}yZ3U,r>j[[Uf޼=PUTv )T[ҪGU""1S͉ a\TPѵŲ_qy:Kt,#:XQotu7 Le aR,fAP30 t0,EI ye׭n<|||J%ׄvF{ uʊ\ $R@J N Re u)Md39X7F#WT>\7Op-!\gqC@坃BR!GmTKmTՉ'"GL!̈́{Q4EG_2kIDHxm4+E %TXũ4h5f("s"N:5+ e(;_|hC -f}zgf"1R]+HG3 (rœa_E_7ܹVzGDD9"R%iRʯ?rdƹU $b۶ n{`O/w X1:ۜgA*\^\_?zjL {\ߧ}0Ro&azgG 0.s D !jhY̞U$@T@Bl=3G~SHNcPJBɴx I`b&0D$9O%bbf0̢oCB8t#_|G RHEJ ݌''<}/ڻG1INƥ@ ZQSpZs}7N}l={>BơQהB۶ż 0R["G4OrY)ҡC~'NeY6M!9rd0Fm0sYDғ~Wa<YoC}ʲ)Z䊁L"y`R^{LrN٥FR nfíSkeD»q!v6K e-C4\ᙛyי2 "-E laW)Ciu ;>1wNRʱN aS'EMKE\0s8;+B[D)TERZG)۶|L *Jb-M#ϻQ I90,U24hnRia/ (([";_SBⳣ Bbv&4t^Fʺ9E ?h8|xu<ݫkԆ4I;:axm|d+ B.t)w V_:lq]d0XV$\OU[W"97sHq_U|"bqVUBLR!I @8a\(AyO"('j4 fCN+AgqMMYHֱ3g֕jP0mg>:Q_p!^R>vZ۽W ei+^4>%0S 5WhTH@ QH8mihJ6.0. ((Aؑc"8.#Z@H IS(6*BL0 B|rF1㵵5eYf=c6e9(,fd#YN^@J(x}be2PeC򰖹R2?.UP"\[^HURr`DG ͻ# 3W;% `@#0 '$ / 3hQB)6l0ΓܙE Rrr?;sjzD9 }Wc@JT `6kCZoOllN}X2فy8Bsř4a2jUqf')  7TDʗ>$>כhW-EQH IP%"U^9$ 1Y#88ѓt~ҞǏia\\Α 竫D9@UA~ish&ŏ\Q03M&~ODm:v>(ʼnH۶DEv_|qYwWN1Ɋ} ("EQw,\˸ߔ? P8H Bdy.)@ asą$QBOk|E3n#Cfz G+`Ndr0R0 7:9˛]d-k)"'RU_B%tfKWvZW J 6e/ 4^PooN\akPxl4 ]ˈ&mʙoa\R1|EZfֶJL\IHs հT6 ø:IacSEQT繯?O%ן;~B!]AI&'eG"4M.7(t"G۶yo0s'H~:q(lW 8(ǽ (@ eU;(\{.wF$e)EFT&HU)a08v,*0$a{?ɉ=^l`3FH$T"q68 wTʨ2#LHuFQQT*B X8\A %x6q{=eGKL%G.9FA(=U<؁,0 0˄Vϱ3`\ѶmY'91U9&圏tx `_.ea>⛑E,@Tsjݮ+[@SQ.%&T@amZ0BOV%ma\ptsNDF3,shy:PpXZZ˿˿<җB~0tEYÝf W0s.Fy61T\l6N\7eNw,{䐒'aaatP7D!$9I- 1!IHPeB<~`((D)0'fr^䊂5*B$Bq]~kdž&1SQ)9ɕQd%={8&LU,Vaa913gb8~ںoN9H)E\YY??zޗ+rW_e/]D6895Ϝ;?BYbYYY듭 Ι[N)s&0 0 0 | >+BdJJe%QG+{ш]gS2@7JiMbTJce iB@ 0VvlwuUIK68'$Nvlv̴]!r 0 ø,h-d2 z>_g> ^`G6s(2,oGX)"!BMd9"XgIv !L&,J^DT3gH)fr22 0 0 jY!-n _;PZ^5#+N8aRr 䴮7qQUlB +('::X;z4TLo#z.>i-ơPt$VgA3 0 Øs$'~W~?t1`<=z;B/r?~-itfN<_L&ob'3B 9t__~c;rl6;xw}=~EQ\Oz6fo׿KKKyd#OCy{ٿwWo\t0 0 0DH!(!"[[ qz,!es8%@o)z*o ~hg 8PUh/ᅤd(md/m ar{RNM%iysas!:x< $'FtO=s}sxw__z[?)%f>'m㡪mVU-Nxb1Ʋ,0 [[[ Ok{GDgΜٻwoJ顇zG~~ﵯ}?' QO)y>yo0 0 0y9#1 S"\Q[mnڊ&Dc\ IO6M^UD]g)+Zq@)XQ\zޕ:|э)E={ (׾yaZH&)2s<0 0 9BF^䚌~ !Pl6f/yKAιd YBX^^?1fes;\Pm8 ?Oc?cg֕>tğ9d2{o+xwGϜ9}/FD9+X:?d׮]>7|sD$4v3܋5EQvm?C?w:/Tr'yJȑ#4x7/BH^= @U~oΆ`Dt뭷Moj=y^8p{/@1 0 0 㪄T@P!^ RʢJ,U*V=q8no/-wWV7ݨi3GSJ"/s*BITA &LgJI咯=TFBQ'p@0)#9aaey7R~N`g?yyysssiilO~)dcmm(%^o{{u]GDϳo߾w1L>O0s׳

cǎ>|,K|۷}ӧϜ9sԩp9#<ɓ'q-EWm۶mSJYȋ=sGD5ɻkϞ=l|꫾رcbUeaaƓ@ <ʞ Nυ$,B]NR:V$GINyިQr|dVpPКqu@1B)Hۦ&,=~ӛG'ӭ$ *DY<DzKݖaqYpO" )riiG !>}[o "؅gH1}/[llUl6:˹(d0=zy`OnjWUEQ|1\VR@r0 0 øQV*NΎ,8:PSPiΗ($ewe@AUm^UiFQ5/&xJyU(,`(P@ :W]HGQ_d}rcױ $PP^㥀\sI X1>fPL|y BL[9GIU&zeMD$h ĬV7ߍ8ϕ1 <L q'LDt>u'Vpwn}@  OB@$43B*ufΚDUUMTUuȑ\pwHvRD!gBU?goxސE,oʲ̒G]~<<^xx"Yɋ9s.HUUB01sϢEg>7!k]A \kg(2=o2ٟ'>/| KKK~oo[YY)"OӺ}єR)J#yY9m[f޾/kki&2jy"mۮ:u*KPgΜɦXB:%H n€sEjc/CBl+eY:Z—B|0e`3N3BDt^/{W\1ƺ' fn} ucДSeesڶ %Y-zxnlAeu>7k\Jo&hEÆq!;TqĢ^fN%q)(9&FJ `e((ZKJRHHyiXηD0AHD-),ya&ar(T& 1;%+G\zveYSߦ >Qr'QAqD}93ފ<3ܮ]?u t0GIJ.b^ChAHq,pYh3&+>lqp @A22Eb%k_.vos7Tnj^K(PRW^:H[i'.@I;IW+JX Th~41Iq膕g6Ϝ.q;_1LmTU2r7\HewiO677s}rr#{}sx:L]]yW' ywDNx:.J+NB lI`mmm{{;$,Ə?jOO>裧O޽{w>oot: zf'/s*ĕd2E';љy:z,2//ofvz"|+[nv"\P%4)z_+wQA$kkkz!X[[MA^쨴/ )ꡈ\O"VVV֊5^_wNDqt^<@Xy{걣ʺ9w^:Gǒ h_aʲg:E;REd nsKn+vinC>};0. RZ!Iv 0̋!6d%^T0욨^=_to砀0(&CZRNa׮]k+keO"r&\\"߉n&s\vWp@ TnJ6izp n*>ז8xZf^=$.V@"jL<߱0lt]Ws<C@PN1w4aq\s¬l`10/8.ۜN^k ( u苲ۨˊZYJY_^/α3\{;/qsNNdȊu8'¸YtdEQx<+3;ZT/,__iw/BvnQOsxΡѨ˲׮$7mSJޏFn]zN>4sG m?ٳgmm-op8l!٬hZRs{[^^>s挈FnW'>vv;yݗg/;ϫDD(|@  YL$ <" bfp"zA𴸐8vUYm{Y1Ɣ\ys~<_ _^QũSPVRøs̅J8:14bǑk:۫E 9OwP >%m,- uO $q#]m |b=g݌|廫gS] &ڱrܼ H-xccc|`=*]hFŁs#! fy//7˙|=;Zaܣުex밋tzƕC AI-y=+x]2:赛SID2/$R` /`Qx&ٖ t_j9;_ !?~3|GWf=>YfSEQbuʼVq, Mc>ء[ґcIs}7u+K(Hw))R@ J40A]CNƵKz ][;ηߑ TOO""Ν RjeGIJ Dzng²Zϻq9i8m{UD--=K9QSJz'?ɪ~]zg N<8ŷmynيK({uر^7Nnf{T1IDyw/ysE;1KBN0 Nϸt\P2 J$r] %tџ Wq.~Vs:1" rհ+b~";)xq%+:L}x?W:^ U+]5};/zw5OqhŹDrvd=kƽu~c S=q*}c1 ->ݴ*iC}cqMg". .nhf.;  [9W[eNvB}+Bt剙cs?}|/* ?򑏌F#}+9@K;Ol"@ P!* %jtIK:H*(xrn2FDMtDD&:NDNVo!i cJ}nm X â>Ƕ?3[AHއ~Ovg6>y~q?xK^TraN Dc;&3oՇz/,g^}HsB~IH$J܍(!$] H.#_+Dz='|\ƳG)s:p׿DzTN4i>Įg;u;U#4Mk nFtgÅiY˝7 mo۩X_n3kf׮]4]U5+ D71vt-F3}yii)m۶mۥYn5s'NRMCN!$65s3 {};qQU?WRF;UyR^gҶmWxڢ0":zp8ܷo_eH.YG-$^gJHi4aIt:keB!4>9U0 SF1#% or6kiR(:ťmS"m4v.a<8rxYڶt:m3+ܪNyl6{5x6(Q)Xyxn$bJBH]?\eC,8*|)ske&:NHc(ǘQ㧒 tN&3h+B Las,x}y9{2iq3_GG >O8s9߻v|8L3,/`2pݫ۞g'짊۟4I5lq!":k FL&a:QYE׺ gG 8|<9AMz9s "kB09ݱR 4^wOB;!0:Eg=#gY4+e9uYu]?K*<_^e۶ι__o1O )g?w {7 mui: gH\v1> O9r02Nhss3ߑ+茳1tcccclu__4 $%;CA:c2tyv>.,`OY9oy<ollx|YՑ)oƲ$*Tv~ptիe b99[H 8HtĜ3{0EvLQUbM9<}6pyUbo0yXHYJPV^$\fIea̦s9N0ˁn_oǩqљg9NHBY0(;#sֳ'pH"a8&(+uJ5 RS:t#_8;z\#xJ5Uu.C6 )s7DZ9+`JYY=dmyB“ *d8Eo2"HSj8У c~F ,`xQ6/X;812ϥz#K3XELPR(lѩPrahqw9MNpQ|2س\ !d]={7ȕ}k?9ι}sozӛhee@]oy[v^rf%y7t|_eggl]5I7^R՚N[[[;j=cw!ndREE:+YNh6/k<caҕ-:ۨ|g%啕p8N}1&+ZeYnmm {쮻,$XugG\VaJ30."`SFUo.'N,//ڏ7p#<2꛾雦i"<TukkGvۺ.ή*ιlxuzHvzrS3g7|ԩw޻woJi{{ӟ-ReW=fՔ 7ܰk׮ΞWv0 0 0  ȹq3^/BgmgW Mԁ" #!_'`\฾~[,3zS^Z[_=K"P!⋘'@!Y^P:/Qg"nCjH6x[ߚb__~ѣ;#W'egoO|ιpWeC_30 0 0ka)@v+lr$$OQU[%9H%Ԇzp9" 2B/ii)$ ǒD=_ocNE ճƪ970.p1ٕI"o;~";( ́ⲛ#rG[M(R;#J qJ@tq 4`鰑*W̠ (DJr`VFG(dE\&:}Czg;"-1\Vssֆ@ieBȏt:_ir_|.)BQ]o?wfߞi({~.bXͽÇ?Cvt:̙3gw} !u2LDy{7z;} } _ž={;>+^g߾}:|G:Me::tɓ{|).0 0 0M:CIB.MʕUJMRmHPl<%+DEȑrԑ/Φk$nS bM\in%bEqɱ%_pM*0mL&iג2ΖnLSڍĚxkXR74 )Z0Ρp)q*z\eP- 7u2lRڶ_RoyX]5nl.PWKAbI4[8¿I&[u GJ, |@KX^^ ")/şE/{N*¾}~~ݿ[EQ9EwnQ:@q9PjzQjRTbw"zd1˳Nw VD#Sd1h^P&x!癣DpetUDP dc+}O NȈYk1 ꇔA&c_{bOn=6ĩ>ȧ# %ZTV,mTQCnJ8;" *9.~Fʨ ^O}SGPU{Uu2m<&9皦/| LӺ/jJ~כԧ~~!}׻ED[[[o~󛻕̺p8ٟӧOg~FQVz>;/yKrC=~w~w:`߾}]ߵ޶m.X[[{iouu-oy*M~fYJ{=p@wWg#faa5]*DM @6=wcz >fm۔ Po~W`i ++eU%B*1.x3 *yLRyu}tx D rnϮ7GIA`wMMAʕi9kBB)hx?MEDt(1$y%BLR$K6kR-1=4;DP8!ꑨTi*F7޵Veo46** âȯ $Ю㷫.V0 P:+9vmk./yK~~@ʑ>7SJ9d;pu-"wqk_ڕoo[76M3˲W˝)W[[> ?|뭷ȡC#꺮z:E]wOO8p@NfY !Od)!dF`0;^}~l6[˲l&(,lY:]y໪=d1K>yb"4" +q]aaa<V Dz'&'N^W{̕W'nruAYE) `׆*x/09cI8+0.u~p!U B) %QM>{m*DY Ꜭ]$,@RxʵvTӶ-*&UHB ( ]b0.'HVzCRaERh|c]&IXxlwMMْV~r.{MM/?8ʲ|+^qw/ss~~կ~__{%/y+^qw]???W.kh<m -QXHAu|gFR\t)qn)zx h\\s:G#2o{s!EddX]U.O{n,wIQvyv-ȑ'X__W՝3ɺKVƓhZeZJ@yWʯ g|gr١gbaa<+NԄ#G&_(qOT**c:=tqs/2ƛM77hbT'2|u{~H]f{βw罹E_F@ ӧ[e1z%R# |> P͚lk2a5sd:`69۹z+u)%f6S!mK ]4wFedsX,tZUUNV,˜YBDYhVDv*y&&ogO;ᆲn{ɾ^zlI+_"D 1 `Bdqp:W-",AD"Yȟ%Xd[- /@BP$ s!y5؆epQeAux!#V^9-0 0 xvt9]r>FmlQ>%wQz#HWq6KFmpIu ԩ}o_DhrJ`#FWfIKvbaa< Xe!uq[^9uz2F{D !Vr  BDU4M}5kYUNK( 9GMJ*ɔDKʓ)`W)a# GV񱍥%nASJt抁YF̠ 1q}JT]ɤpgKt~~a?֙n\ٸxU *mA39 .a:=qp#%RAQ" H W8_()-aalȁ<B"AjAC ѱwhLB $ vWV[WiP %f'F1N)DDPBxvvpt7gJ҆ٞhpUYXY[Oc(Ba9_nڻ+k+M;%wT0>L0Hrx -QI)9G6 B2 (A$H+/|^Q˜h,+ _B|5!w8DQUgj)aa#9nC5`!ܘM\]׎:fa&8b;apUM&-Ҿ1$S2[ݧ) ^rɎAӐ+CW5bzr*qlW[W{zHbrfAۀIfv]gQ>@g*B HL0 j:U+"ReU5ǥĦYKIl>ͪ($2%F"nfOzիc%!>X,õ#i:I\vJ@U^囨DP%Q$r0 0 ! ]^immF'Omoue:٠RJ1hb qAXY셯BU/QfeR/Ž|cEWaƵP)]wK}I]Iq&0b|yc-5MTx<32Aa\|L0TTMTUt݊+W8܃'CՕ~ ) aBs9]JQJ^cYO*nQIBH"qQQNRR:f%;aaA P"PۦOrfGDlp,D֭7nkRS Eb? DH,p&+H)1,=2ka)z.ɳ9Qעwh8bXJi"$&/콱+0 Yl|ك2Ef  P4 NjI\UIIӗX3ꤻ%P8GpP gp-iA|$,%%IdzQSCKϔn4 ,DHIsc 0 0+R U"P,İguu '4V<4[H> d:Ǘr8JQ,$ Q̣ JJc?^0 DH6z zqQ>rh4I!1qkזް%~LMHR_Uv Us3 t$Tz g.=atˢϾ 1()" aR`Ɛ׉b'MDr9#&MǕnĥsNTUr0 0 !!P@!v>VJ mnp=;qQqJ=vx۳w\MH4''irYL) ߪg?b5R`+MK ʕ`8XP&J7+Mf3{UmVw$g; 0L0.k70Y)@E5yz~Uc^p@Q4AYkHĢ¢ DT$"H!cr2M`E*D }F+*FkM  -&j,&DHHR&a" J p7n10̟PV+7 0 弊`2[ۛuJ/{~pcvrk:l<ۋkX0MiCI-O^U-6$ϫ)tq0 Y%@sb~P΅VuFhQ/qq۞]qblL[VVB*s"4~*:M"ZRUE`YNcqQ1ø|! qفNT8Q*2ruKy>Mִ\" keGEPb 2f/x<+ |K"($\*gSWeeSV'@caZE%pBBm%{T_S8.SS!ʋaaƵsPps T8iN&.j$8 1|U0T3r0ӪG\hg+{MCa4><|䞛o~VPHtE%ɥga/svVq* Nyqüwi+(A5 ( + (&N%/1 0 0 0.[XA[GD&bXT'{O:v‰,-~w_7pęCό5Cc9̲0 2t0RPt(4zMN! _D3:r?QbԌP8ĚEh*aaaƵ AXJ#*,o#GY;yg 0mfJX&=}Ɖ[NBR,, 09 x HY$PKV@O:<1dM=)0@)K ;fh᨝}i3;Z=0 0 0 B8i0TY{m~;V3Exт~vK#{ݒVH_moa\VaSB\($zM`o+\n \HS K_:&I%aaa\"*CSnyD1"!rRHÃg-*#ja hS!D̈DԲ#@9/ xO0["G` 4@fk;sB*s0 0 0 %"@.$~ȑS7c"8e(]p;<}쮯Ksm;8a s(_ X)``;@\sJ&-41`}EI"Ts 0 0 0/?U+"]1M#G HAV$*(! ǀe%'4fBz0 9 x*Y߅G*M&8uܮd$ݓ-fktVH `%!D'Pgth4UҊjaaa$L҂c8(&w@Ƥ D:xks,]!z 0t0DI!sp!T\(uL{U"\Ť$nfu&(84…1 0 0 ø!"q$-V.i )U( @TH5$miSųz 0aS"sRT߳6- >xMӀ+px!R HtNl8'pQNEyreaaa\3(AQ Wj*VzSp R %UHP T @5q .dEhiq9a:aOGR0'%h@2- @>i%geY"y`2)GiA,HQ%!k9)1H:G4(+|0:3(u%uOaaa\IKJ*{SʀpPQB T!S>>zFlgL@\oaq9 xr0Ke<]K?=>@IhljG8e(EfR"peeͫQSqD=Mq7QT%)W)񜈽 @X;QJyF[ fõN։ ҂b6E/_u c0 ø 00B#{m/n'"9ٴS(E P @D#@Op)|'#x>X۟k{I}?Aۂ' As* i hT_{Ԅx2v3 0 0 ø` @Ev_;3aM7/omx 5 j{uO}T$*H- 0 r: xrH1p@$JGXGJIAB xT|J21JHd(l]eoN75zH+afĻD(s=aU@I˲ -'M%Z>aaa5 :FA]_l-jAM3Fy3l"%qTMz_zguorBC$30 t0ɸ_= j6֫ZY49"ERL Eq+N|S95+mU|mI6GSonղnk\t)8a)1_P%8m#& 1|D9 0 0 0=jW8ENjoY{ǝ~tЉ ]A*{pLg6[Tzu;+頺;WᖢzVWb T\F87鎯{]wr֠MgC 0./L0 \zxFHh{\<)#9 J_fYEEK7q:mq1hpIFRU@@B@`GJI HeUBRhgaaq͡X`Wi VnX#>2M'1^Y!N|MtWI QgɗH 32 ølN@06rgBUyIhI?SВ!DҶ2+KRS[n\4I\\ql=A)ByN)\^1XKFRRlSY)0 0 0 #&v1 g=/XS'm frz"{yo~7 RPu" Ȉ*M0 ølN@0%l&6/\ c% (e%a'Gpp v䠳^7oLIC=WAmqLC\&QHs$7sY<wiϬJXEXH^QE@wj)g4  @(O >BTbUeUٜ>:w7׺'N6di2D|?p0w7Z")%*GںM#<"""""hpC]bH.F<9rO]˂8F4 K=&.hrPea*UDDgs"pNJ8qHI$*C L]4 ɵr ks ŠMT*馌 Ԣʂ*C1us(D4RKi߃DDDDDDt X{ǨfCSYMܧ`:z)¡M pT h`ADt0 ZeH0@hnR\Bh2աP3 1b,b&1 ZTH4 l u@*3qbu1&IPs > + jj!hP58B&Pg p1 rᥓ39胩#T14I4k뫺C-HtL wqYwGqk0 UM `9{ML]RQZWPaxN|$""""""8TC>`uĕ޽.PFԗC"9}0uhL Dsdabu_)P7B`@dD $U";.˩0  'BdUQqsK! 9.q* h,{JW3ʼn<#Ԉ$H+EDDOs"bb͐R"w`s /0TB\ X:x>bY@`R5܃UU?r+lJDDDDDtᔁV02 Ib&H`(Vƒn']Øs34eu- HrIQɲcYX Xub\DLEL$".qI G" E!梾70giT%XDDDDDD]XZv#+"(`x,C`0AiQU8T"@DDgs"`.E룵a(vC6A>VewjTf=C͵2 b5En-GKyf.h9:M e2JYr}. dDC)-0\ \Sd))Aa³숈 Dʜ 2M9.rK"V 0CT V0#SDq<|L[J]Їw(ey~ ˪ ˢA,Cj7&.} -Wg-OSeT.jwD&R /*чG\zB-M)r\T 8ֽD\Jrȥ X>Z? Џ,bADDDDDDDDDDgBVD#HZ=Ue~VcY3h1DDDDDDDDDDt&AWese(`qq0 """""""""'n{Oe&!&0uwq,AON88xpιĜNy)Ȃs8` AOYxt_$9#wDhͪ5)s58BN2J*q,9ƜNAK}C]Ԡ@Kɥ{ղih3JpA2DDDDDDDDDD97M{31Ho. -t]oк*DypZC YUyiX- UeǪ9}T5T>1L!]F>XwU8Ww޻=6moo7X KM5%"ˈC }=aADDDDDDDDDDs{UUZ"`am;jwaz{v-?o7./~^xŸ,{ճJTIgFs""""""""""XjpRY{_o ]}?_|/§C ǐh+R6dq\9cefev8RJeDѤ {nKUu7@mDwnߩ~e{Eԃw96*R7 s""""""""""XzDDZz~q]9 =jFAfW6_oMdK_~akU\M!z)k;5DDDDDDDDDDrRϑs!u=ߝmjRlWwq9ln|P#n>Ҁ&N; _6E﹆ "B/p) 0hu޼ qVW7{t!Xq6X7] ޽[?|{C$YZ!&.0@Q:C &Dt/=CW'DDDDDDDDDDk480d`PG4d Wmk~osG:M ^W6MY=M?Y"^!:#. +dGJ0#PX.o, bC9=yȿ quTCJ2j:^t!@%P^;;̭){]1,CHV3>A{KCb!O s""""""""""zLJ_ "л:DI}w<뎏C!p`o>p ܹ[}9ML=9<ru)`ܠuz˵ b.$UW^GI[C0 """""""""J9꠾/@UiBPhwH D!૏0f!ʬok@+9;`|cYo ; כ5Y]GOg8( bADDDDDDDDDDO2x3*xS.&&sQǪ}Q*1pn=k Ϻ,YNpyxP2]RGIZ_NX0YX_Z.=s""""""""""z\JL̤`&"A ,7ͪڮ:63AA}5UDpk/sݫ;\K*yu *;1_&֩2re"VQ0x"sSa}ֳEd !F 1 6 m_{QN%p M\ҫ2$ "]CAdY!03'&nDpbqArDCR[pք`ADDDDDDDDDDOU aH[:I_ys01:%B`Zh>{Z߅ TU$'`娎GcˀDȀ9trX*EY8E yY0QjLX$0 """""""""4qrX.:͢[zO=wh:=\tq2c\ϡ gD i[4V:[q* u{(+(\Qg wWq 7Qx oT! C*5O{o}PUUXNÜ4(T` FDSlQuj$Cں1՟rXe".(#]n U_&.zFvSꨪz˕6NFφE }/iЈJ!U 7A5xS*rJsSbġӉ 1lu6GO?7oyͽGWhj<Y9؎zW^|uGb&Ym x0|jyLhu6Haz0,ޞ\jFB"y9DDDDDDDDDD 98U [pL}_?yoܻ?}/^+_ҧ6L' <.?~^k< ,'QlHnw͆YLrߙhcꧯ5hFMN/&c,3K>z7-zs""""""""""zJȱ 9LoqDa̓Km{'&oϏߚݙMh+?sڧvvfc0ArɎ$$Cu#*c9l97\)xUn{wzxotws׳)%<>Ǩi-~X9`ADDDDDDDDDDO޺|/i!J!]v 7fwk]u]U؈:}/{/n&RȦY`"."nRᥘ㣎XS@cVۥuh7kYf$ Y7^{mvwn`L܆zQVr<9i9ѷ 8\ 81\L lpxlGЄfapCeN{&&f"P7×ʑ 1VuS{ャ{䝑46 萀!+U=Ɗzg|!COD""""""ǖG\W*lWbsq)}| NN'/?(/;&Hb)iK?h+ #gXLBk D "-Cp)kGp-!7jv;j*ˤrQtc4j~nnl0.n[c`ADDDDDDt :@ ] HM_XZd ,"9v QJRnY=R*KTz XM0@,gi0ha.3}DaK,GΒ &0 V`7Igut.&pHf XJDDtAޏZN&{wz!u6( Hr"0QoH@L9F9W @\XC`,䪑6h0TawIh0QHvuH5ˉYDDDDD=faH r IU݉; pJ >9!G0@E8rI=IsѨ€#`~1g>Uۗ7vZm-6.uYFBTasS+ݮV-' @LY Vuu+.(K\.U\ݴDDDDDDD獋[\ܼ aPGimCͷ7gfLFgDB>뉄6zAy<..3l]ڜsBm( urЈrAr x3jA8~ ѹ 9ٱ8b[o4;ݿdlJ$!uhaQ۾>B[Tw(NDDrƆRajj 7vnʾhԡ;hxX.dRݨ'pE,sȟ DDDDDDD瑇l@ Κ*z|nyxYR$k6θ7oݘVMkt V 5Ap)6/9 \:+u|\$zE,I8u<>? j~琋%ᇯwsfg#mơ )ވGUS_4<@DD9ġ%p PkWsߝ[m"WXd8 )RAkx@VT>#{ӗc cz,)!"""""": xк q74BMP~‚,1W!7E+ El$wJDD-'ƒ#uilk/\~aFt]ޟр^a93ōWvF[d񮛟֝[EDDDDDDtވ ba hSMwz$00`w߼w͎+JDD8ҁ,p0ܾs:l04!:O͓,ts~^L60K =I`ADDDDDDt`p(a:?{뚪n+8BI6 [롮p`@()gpex6C DDt?C!R&-vٹшh7==>MU'!F'OIu/+e̒-àcl5cp7/Д 0; J(u*^XOP7 ֱ9;|S[]?<@DD & Cl~l1پrS;w~a C*^޼^ys"fwAk3G>1!"""""":o}3/#:T]8(F"(¥0҉rF*$_Y8.k..cvϽU+᥍KZ'ѥ!{(fcj6<~J2|Ȑ _LDDDDDDD琸C  /HeY Ȃ,\ DEߣm^ԞC5X"":dM`Z]^4ըm6j..Atx]5\ApcADDDDDDtJI7VhR* 8Up-]>5[M! a%q#3EbT2 C "nixRW(DDž7ua{c> So6 XFcp0Ga,^I=s""""""sIT] 1*qOv/?h:t u#[qzcztx\J!c<<&9' ri`1E bz\iF MB_$mbxw.qpGDDDT0 """""":lոjE-] .=d@HedH21.P WqBDDDtʘsG<;Tup A `3PC 9Ο z CHp↶ pV3?@s9-$"""*sK-\!R sN^gU>D\O\h% """: sGe hh奶C|`8_\ K PUQicADDDDDDt91:X r+BPS|y)bADDDDL9xW ]p1,OIV@W2+`C 0Axκ9`s8bplD؉gTY6s>],ͯ۶Z50 UcIѠ*!E*n7:yh eUNXee8 y(0ve*QDDDtV0 """sVm[w QTD ES'Ѧms0C7MZ! 5D3= } r0 ""39[l͢B>l!fl dhpfonL|󦚌Oxǀ2""""""ZbADDDDɺ׊=lq޹% S~uc>:m#"""""%6 Ðs1crj1Xy]VRJa^UE$B _orY}唿c])s ƸsΧ}~PT"gsUU9ii)3z8v]ˬgo'Gat|\Px&w~_!tVk4U}S"UWR{i=C>~Yre '> *w샔SJ94EU. Yu^]Q6+_hߓgEQW`}WӾG?~8yϠ':u?N<:":# 4嫘o?~5;\՞[PԹ%TU!~w޾]G~Ӿs聓'O$:"֯w9yS0' f>byUy<$9 2.9+<$-R,qV!?բ`K#̬)KN)}ȡOt.!r "*TQNtuY 9 ,j涚+tLu{7 AI)N>NחdndAvn>^ Z@ߧow%0xt[JEa644[K=>K (Dgкum24 KΎy';6Z^5"ZZ8R^SEҫ|^ϟHƝbW1^#` 7WcN1 ab]Wx8+OP(,XTUbPܪ*3OODÖ9GpUQ * HUBc*ƠXڕLADT%J4U EUEeKΡ@4WGUbˎ@à{k3qVP>Kb_}^t52Gex=/{m'_usJX뺶j'߾sbWi-b\r-W^C#^)c9fu]CֻC=ZޜrN*QG y'1縠Tmu[ygU`6 c1nll`ggR,ȒP"&mۖ¸O$lnVQw3xPO[O_wʛL&. h2SD"chUU]jv<Ɔ`R!"P jW@\ &IcUU6e Wt_<_F#/CC pA@+WX.uRq0al1beN|UZב #:SaXPuUUOB"z"ʱ 弁iLYjh4 {=Jαn{2縸? wҺr a U]8{]xnŃ^G0GYCZXIuyՒs'HIaY(_~;OBj_p7bꉜP3n[|YZ|l)uj@Ytzg C"րt0DGzRu򅕏Scx|9=|'-CDg8.S:=,i9.|>/m+mri~[՛}<%9ܘ}off~zthc9r0-]dөMl1CCv(V[[[#pxxXZ>5vA6 H*jSxV CLy_w.S`_@y]31uC\(+ȃ/UG?.v!"]MӜs۶jDdf8}ߗi4jDhX,)vN!l qho?]S9uq<9|{{9 Xuƣi0U:x^Y{h%q fAu@V TDgM9_L,[>NΎr^e0 dY>NrWJibW\yf91X/ژU{7S_}*ب/|utz60,-0J W?)[u0P7q4 ?w~ Fm;9' l~xtJݪtOw8PGeB /߂Dl@DDDDDs""""z be hlov؟!"WC|O~Sګ:ިD9{#W됣bS ,?cxj5]#FLFursW>qe%[j*#ڲW⌤A%"9|QB bw9œKV4$xՋaSZXt#<}m4;8FU4yTI@(Q:x1.0@pWͨKym}i:?/o^+(hf}F k Z`z8j7?/}_8Tea0 ϗ) k> +C9&y0`ADDDD% rxcٜ۱E'*.p5 ܣ$Ti:=h xl sQ8 ˉGe!@M ^ ;4 Ak/>1Әo{oãzڪC/33+>}pc|d5w=d-Md~LLP/US;d/" 4=jU9 lTE{߹t(A f3d#|;agn0MQG:,Ҵlfӣ6^ss}_gfӡ'ۻ[[.}3/=sL<հ@.YJVhVYJ>[0p1`9# r0dApTIãMx'""""":sc;[ot 2N9)CdC6vvgyЌ] R@]3|VKe4:JaXM膷mw4͸ܻwKoھ:F>Oҽo{od{ʫ/Uݥ[n8<Ŝà.Hj}X /k(M :@EIsK*ip3Y$+CCLDDDDjG-h:%A*0N6wqwmK*>fIr6&>=]6%ʬrs׶i^ʣz%<[<)(C@R#f Ԛ, CgfmBPd,i>|1 aH} J;;6Fc """"s9=:qU/Q}6,3C @P a5=:hDrPb9cv+e7X$WdVCA\բxT{zA VWPCb:Æ٫6\ݜqgoa.9Q W76:.lEqghԤ|h `UKC` cqix,9P^$Ƞ2zmҟP*sXN$A< ᅍO^_ڹŝu~nMw.%um g@ܚݛEw]_Mhyha\[}"""""S˻_~_xT{fsx7DO 1vpbQ,h5H"(U Ip@"ZpU*䈀PAfҁzj)FoO6v.[(\kuwG}x<~k csyqWy ֋/\ղ± eDxcFO}Q L*CTE]A]Z˪SqbM"bCEwߛ#caHvλ?{xfj=%Lo=Y$!A$-3|CDDDDyѫ|o2쩳4wS'rw7/_궇s4SY-9T]U>ڐ / 0 *P:{uA 0q*J]&sua:d{!|ЌSm7^}80q{ݵ.bk&-ͩMi_h'uT^<""""Gg@H˯ ?yh$uEMx6;rWps&08xh'+aZ໮%`'z#z %ZrU1`aLg3F 4 fTY$MU;- LT&kIDDDDDt0 """ǡEz{wfNov%Ġ ݎK|SlENCUitn% , "CpiF&nEaGwzDvQ>h^a^KUgyus]{PAyb`B 4z6!t[HDDDDDD]ؾDDDD[`KR!0.k-$vzk=lDLTkn& 9  !_ޥ0*6.IDATGGOԍΤo _ qm CT5-/ڥ8_,E?c==IX2)#-:59\X&Caj(@qW=unw^}bn6ڮ }Lj$9V/_ys7xr N{ӈ DDDD&: \J8!f .S [%jvޣ>q(\/m?_:GlOQ,_K_W05&""""":/ss@JqCu]_P)pbsҢW#: `曨*&|K/|u< ~wo[>i6v+.k/|qRkq{J&¾DDDDDt0 """%0@?LPs_)Cp\r\+`m2%+JԒ{[mkW_荃{ӣ78~\}dG0 yRPH `1DDDDDt1 """GsU\eU+T`bpUrA 987)0q\\>K,Z"q1lŸSW6?;:[Pl8>;xԈRNpӑ4 8DDDDX\J q"&;4M P#p's/8 ,,zl Qf3tYLmߟ(1%O9 )i6ԍWһf(]9LXu{, _}wO-G?}uxi𲼭(wA9=C% D]CHw_\KUO.vww A~H!H]UVXb贷bADDoٛpA..͊9KV58&Ia xu|2t\}#JC\̖@.0 ?2$h&8ܠU+PGB zܹ4P:DDtAFduv26Ӑ+Q/Vr[视}<>{'kKg_VcYv򮃖 (?f=UCu6Y/IDDDDDs"":ʩrƷڪ @ˊn B{w܄*%P"""""""":gs17ːCY u,{0C !ƨ!vٲCPq 8s"""""""9%X:&#CҸ4E?b]#˲cB d8#"""""""'9 .)uŗ!G4&G24"aB_V|G\-"""""""":ۘsYQm`e-z5d@0 {ݼ3WglooׯΨb *wb'DDtC u 9MF0=70w~GC 0-TQ_/mJst[̓x58"""""""s9-uQBxHu?zwݿ2jvtE} aI\L7$lAGߺ`zTu>""""""""z*sYpkyar(JqL̢ fݻa>ۂ5}'p@>`Ed¼[`6j~Mio =9LXspq*iw?:ةfX,5.Σ#,*XQz; pGDDDDDDDDOs"":c\QCLJGU↡GBB$I5撑-U[0$0""""""""zs{Gpc1瞻w6UIkLY操?chȉ!DDt\`xo:QG"!VU *.PGP7f-a:mT1yĜL`mۦÝɦ8TTe̛lM:[4)sy7T9m""""""""9-^f/^,@*EO:ڬΨ2Ta [D݈aADDg˲#[m\^l ó"E*{e&d 9"_hcaADDDDDDDt>1 "'^/:Ṗud{y=,GyH B󐬪6%qaf—<"""""""s( XN/!A[K?[ӣuiRGh~f2Oi<ٵ\bADDO ̵(%c}bstyhkPP,U{pt|i{|~%88j|DDt&w@ZWIA%7UyK_ظ{ۯ^nf*uI)zo>yO^~1y^dᴷ8DDtRo!Y}ݢYW+ߗ%KԂ@T~|9<vҨ!E] """"""":ws3puC$cM2i7n=wuzw2L._{~mц7"qDDDDDDDDs"":}^8eUG*dT7dd]7pGP4 ]:>٠ѹÜN iC_f 9%s9;^T@> iUItp3f\#s"""""""s9>1#pK F6!]پaXXf77ڦ.$n"A.S; %"""""""'9>$ h`Y3 UuޡHhi+~i5-D&h {fEѹÜNC8< #Ats{kѧaH(` {?W Te|\  e9} ԽZ>Zh\9ʨs"":}˱EI5>bu"Cʖ<|eq}%Y.0iD_e7A"""""z"sѓDup- t]W-s"""""z2JIrWZ9@\aP*.%p(NP"""""œG(ruy)C:uѣcADDDDDO1Ǫn Wwzjr@ "s 9=DDDDDdb ű6\Lr"""""z<9pS9QU E3`8TH9lf1 D恈 3  `""%$*q[X/AD1}[CDDDDDDDDDD$$"{&k,|BDDDDD DDDDD`b (T*qSfZUs0TUuڛBDDDDDDDDDDd bLUHPZDF2C>=bUU))DDDDD`ADDDDDOkZQ#b_`湛ާ*ԛ[[1N<}9("' qԪVGؿ8<.ـZӨx<э4 +9bA $uˀ#EM &8rkLDDDZZ% [߽uxyj66 {O޹b uь压촷D,1  pI:¥@+ #Iy \{A_zy kSn^?O}ychN)@S%WgADDDDDa[g:'pT|\]Ww"""zB<>쾵y ^gUSqjWło!ϥ&d j|BDDDDDY^0Xk[DDDp0'oώ*uhs*u7 b"Y=o~?OUE&Ñ DDDDDD.Wz@ Uk ADDD{JZ XTדe$*CwnϧCJ,P͇{܋ kK0Hr sgh  e0 "">>mܨ f*XG CT#뱿7˽K\d=A$Cio99i'2  W/#eBKruS&ϳ'mq5RP7Mpzǃ*tuq*g!9iw/!.Z=$ R ""NDc!!{wBփ:B@!3uݘىb5`A D#O4xn䘳Qaz7jy_2טο5h߻y66BÃ7!HpoO0@-:2WC4mj$:L2յm`DDN{3K|<UDFP1!ÏfVL6z$nI+5_V|ADDDDD>|@2Њb8}+*Y󐓇\61ontq- X YCr(-D&uNH:m7v0@7T]UDDDDD^|@ 䤩o%_V^P9-lr!>DDDDOn+f<lFiP% BN{ƜK*U-DDDDDxsSgf 9q=XADDDDDO}+"""""z,|SADDDDDDDDDDD*DDDDDDDDDDDbADDDDDDDDDDD* """"'J~󮈈q}=i%I>=s#c[ """""z'HDDDDDU9YŜU9YŜU9Yusaf3)r}RwK{r:yYu'/ 7g("""""""""" % L&e9[Ow/WvaT5("0TUU~Dl-Umf>Frl6/Y UubafMӬ7p9GuMӔw/y]"Pl6夔ZDRJc,^U:Lh4 䜫ܲhtr*K&""""""""""P.\QUU :Rڶ-WfM|X Ur{ιwh4Zetڶ˗ܹs˥h4*9dnruu]r"""""""""" wҥ;w~߿ॗ^կ~sx<PJ}rQs%0|;G?x7Cۻ˿|瞋1իWSJoo&Z!;kIDDDDDDDDDDt]ҥKݿw?2ƍ7ۿۿknTfVBҐw~wܹs͛7o]~~?dueTU5 C* - BDaADDDDDDDDDDӅ9J[[?_|ygg[֕+W&M&wkEJE:/ſ1^{-}iBu޿7_'\t`{{bmێF?uwxxնstr7~~_x/ ׾ۿa 9YQfͬW>`>w7;;ͷzk<ʯJp~0}iӽ{o~exU7|*?%Xn.a(QMQ ֑Lq29ID+jc)/~D as.;u)J JP>  1BE1.Ϊ:4lֿ$ O@"ceNΠ~)QcO.S>H=_Ha}ۜ8qxwwSJu]_~K_Rr !Jk}׿R׾'*s[[[1??zϗ_UI,,|_???F:§?麮E$u],!t||\yҍƖtZUU1 C)A8y?)Ɣxv|Xs/xp7.Z=g-t0sXeɒMPQgg.۷777˽\'?oݺO~^(70c%) \,UUݽ{_z%o޼y-w饗J-aDa-)2w3 шE?%֨Q&5%1_ VDEAD+ `>uΞ03;;3{s󜽟EE!jMQdB1-0 ذl]d 70,MX#4EQJTyJRQ@Hgݫ(ʢvdbzOEYt8=( ܈| = d2bǎDvYhHLQ/+W27o>c#Csc̱O}*_WW OkNnܸ{yN;M2SII'455ur-~뭵'x=qk֬CIfL%vJEʒ0O˗/\R]c!egJ4U3yvDG{]=13#8b1C)4o4Ub7MQ>A?˄TQ!@hq(Ka+XU?Ueu4e !Fa7h)yȰToK$Z&A###۶mhǎ!BC{me=SdY&B9qW\qh-Gu#Hz֭[$iwu_wuJb;nu]yß猏W`yJBDmqwqʕ+>il6)?&#`#p lLQvc8ram{IAq1&I] ,G!@0%9XXÑIm Rv1&$1BQ#_Zy$8{'6amDqd6$5jQxP(djSEY:Z _u*ʒBB7^/MSIR QU*(K(BRST(b7MYBȞHt)os؃YإDʲlʕqzJ233#e9!޷Dt8%$Z+usVĄu]i-[T#8ju:Ce٭zw6??Xr1F$ }Hp,;Η /Vi?3G)QZ eB@@N` VdZꡤs;cYdICʼ`@1\5 o#RCQ:!Q$[ 9$4,iڍH鸂Z"Ρ,=4T*TQa=u>Ue{iޓn(;bKQ=CSɺI ]YhEQ$o؃v&(DZTvGyk 7^Wդ~Ʋe˶lْeY /PDcG|z($!jGYgt:~_ridQju˖-O8?{FGGggg,>رh|;E(R$6q,dYPV!Us fK"Dqא`u|hU L2?uOW.fc/Աf`4Mn{XKȔaI bSDqY>Xdecc5obT! OCf(K|#y >,dܪɱKmLEQ~*%K:Ne%6x8U&r.^rQrvEX4)*ʲ|8/1&rX9Ie)yB87orW^y>{W߱cNJ+$I3 $Dx< %qyxyCٯ'oS(Ldusퟨ,&mJQ8lc[(}SEY:} \:Ne—u*CBcIRt:dw*myFL tQMNN._|͚5֭'?yy^Vޗykʲv+V`f ?JeĄV %@|#9y~W?iO[|$Β&Hr+s1LjF{jdd$IPM{eA9;;+U+.[SIⰆ?T/_|vvK/?EQZuέ^G!RWVPLגR(߿_W{BnJK-qW??y󞷗:n7,Ԧԏ U$J^~:5 ʒ\ gvjȲ*"}Q.:l.EȣGKy-K::*AYzue˖-[""9tV8ΠI(*hWvP|[.!5/Q?o=j'M~YkKd!//O3`YkK7pAՌƠ ʋ;G#ch'MDEY@v#C)UwY* Wg{ۥ(J0$,evYB׽gynFp1k֬ٸq#x㍧rFBxb]rիα˪WD.S#x߾rJcLҌv-E|ry\^/"T@yfT*vOY &c! Y6C/MaZkؙ*sl`?0 ذAH/zЙR;Cf(K=J8U% `ETei"Ë e ! \:ua5Z{Gt_םNVR!7, Ҡɻӟ̌~k_+  r^'s.˲K.䢋.=^[k;N$jUyT*rKZ;a\QGCn/KjyA9D`XJbꐳ{9v5x:CuO<T{{p&!{8x{+җB#׉ZYtD2L"90JMSe'.22@Oe)xqǏ(N؜װu,H`?Y)Kn :uoQ/ExO|\Zk%rbzzZk{cX F1h ]9/e966vǾujR#-9"5kZZٱc׿;v`P\ @R˶o.g @)H8P&$<[$IReZm6Rd%Xwz$)$b2-%I"{( nEQXkgYrk!q2p=!~C( QYiT%E ~䳊,,aw-?,)SW(EQv2" T={c*hbrtΥiK䱏}ڵkfY˲EQBӟ֭[%ӳ,$Ċd $!{ƍsssEc4wE!ι,cfgg4> hz~;vLNN>)OYz5KP\QEQEQEQEQEQ59(^җ~CZre]f͚e8.wz>66'>xq9/#w!%3_333GydZ=S7o,J$JӴTU\T޷WU_[wOON=Բ,h6?O/s=k֬~%Z+A!}QEQEQEQEQEQeq8t,:묛n׮]}GEqK;v/~v(^V8޴iӶmی1EQH u8o׼5;vXbE|ʕι^v˲\bų??v3sZSBBN8'''+J^?#4M{ր~ff*llRC^7裏G?qVoޯ]nGxu]wz|?я~cs1DQTeeCH^rʭ[v:,m۶jժ8˲vvv%!͛/_nVv{nnh4Zh4I^h03;k_ڵ^aÆѹ,~wmo{p .)RTV522BDyWž(((((`siz`ٲe/zы9+VHHǣ(y;'eƘVf G~Xhxf;6Zo~|w@ 0ȃY * XoJ6o((((( ྑ}ZkME>2{&gWVZ.:6߹y;y]sՏexޭ&x^ f3|A`Clðe0((((,!TP%] OLȦ%l"$57% qZ0ƠRl{nM6kx[xs9N>pR[WlG _+EQEQEQEQEYdTP%*dT0G o:Sܾ7SowTF8/]Dj5Lc|Yl|5ݹs=ga͇vo|B:6"0j0@y=]EQEQEQEQei:,)~$ eyFclwuW߳6)("l(@Rv%EYzm<'j<>c0 6ѶNsV˔$Jg:EQEQEQEQe9TetH< @d1E.s33Lya1LzD䖷s&n_YKZ{=cv{0 2 ƙSlBQjA7((((,SQ:!>@DADg[M*hmmΦ39Fœ~W2ca } x"" jvC@" [O2˸]PO\N5CQEQEQEQEQW(K }M`v`Kɖ96nm9/"^v*((((h*EYr%l-ODVR&|^m7)k4vN$6+Q̚S<赿,)((((,$s(RBDGQP?'QٲljWW]}2x0򘝉,%0e`ӨyekؓyU EQEQEQEQEQ9e B<`sH4yvɝ9zm$IL^.7Phϵ6qґkN=,9*((((,$ZCQ Lt~i ĞἁlՒA8gf;akޝ*z uyvv =%+:L/kyvFcqgܺA`{rQQEQEQEQEQePCQ`ba`YېM$ZkkQks2 U5)|vΡ(((((At@"o0ِ *)3oL`,"2֐0`^TEQEQEQEQEY@TP%@?{}w03×1ya ) <JA4CQEQEQEQEQ9e opsrFDlybOpJdraEQEQEQEQEQ`TP%$h,A Qà$y'&'g%UYXOvNQEQEQEQEQeQCQLq$ `'?2@\34NgW,11!REQEQEQEQEYΡ(K' SdLYz&KDDd &33I1sY"&|(((((Ρ(K$%I7'p)`C }F`r/(| M;(K0ynS{/7",vE#4F𓊢,.a`T:*ҡ,!)tIV1Fafـ,v TP%p<ǮsK}Q0)~v( V5u#c]jTC,e!4l6TTsmןOezTQ{Oea T 2xGp ל}Fm((CcyM۵QX)Z[*t㔙˲\)'莻x|(Kj 䙐x`v0Y?D`9EQe7 ]$$Ii(dNE9; }tp q(K0NN:NeI1laq|!C9EQEMvұM$ *a88[> EQ! ϐJǩ,)ȐRǩ,HRɊ7Ԋq _[cBQC$o({B"]Km}DZ#7 E9 )ǩvOP&`tqcn,iEIxuݫ(K l2Wq{h@؀x˲r쏻^.GQ RQt`>VF9ٓw`M%VkEQEQAʲN9lƘيp9Fyv1K㭵ni{w.eYc1(2a6d8i24EQv((<@4w vy<yoQ?X<|1*,(1D{)w ̘ AAPL!( -ҁaR]d/Āg}uiQ) 6Lp|a3#C;ޓg#`0=`{Fd"c @ݡA(9̮D##dA<$ L@8EZy,'(Ts+XEYBΡ((r Ӣ(9#[.6 1 -JZQy.O#_/llx_G|R"6b@0 fgv ;0+ʒ@<Dd,2`Dl Ho &ie儲*+e mbmӛf*jf9j}1&1L0 yE 1,U"+<1a]x Vd*tfѴEN0)sy7\Zf<}:;$J3 @+EYtTPEQE9}'nXb0`6^ֈ=o[o:NVܵ9ǍJ5шY}`DN-cםްaQ\9 2=(Iޛh "J{foڴ#W=U=/A 8`?zְIQ#}1L@# 0V Dy(\i.^[yh%klM< /;]EodGqMTelzbCvX,zb4GǕdlY>sѕGfd<=؀,`y7#KEY|TPEQE9 x@{+ 0,y;w$%,b p_ju֖qBȺ #è:vLҐ?A.\AN~)1J!&|ucr c2voK^/o|q鑫1:h|2ߖR <! _IţcWY`~,q̞D $yGy5eL%f^~=OO!O_uhn㬈 Y_wvީp , #pD-f6h/;fG0x0Xu7"Gr3P!Ǧۻw4uFhpl{0 8PMA  I3 KkrTPCQEQ,d. LJyvv_Nښ:9dSSV;$IaP$ă'p?ۗ"P2"B vYUY0W#H hudx_f*g"R̳imd놩,+Lo},xZX[>D0zOi 3Q! !,qҳcCl 7HL&96ݵ3]nguoC0̞~y+oFR O(  lCw)u(PCQEQKpC`C =#Epހ8_38G(}.|R3y(̾v^נp. J?Gf[Vc\GZ%KZE 睼ͨUy}g[O`g}`Wet%5(K?dp$$C9</>"UT*LO6L=FVjbcWλT)Dfg2fd4:C&,t((rA}pb" ?biӬLpAH<-E=1yVvzTg2%g|az!6R]g!rh0s~9y$U @S!v<8ov!I&vz%5y|7`vn'Z\Y@0fͬ0l^̬/*D ())qL(v8D$̋1ol {a`MB?iJ-(ʃ KЃ@4 "8P*͐mΥE,92P/W03f&"~; Qr~(RgIEYLTPEQE90`~FoÆ Ñ!%cFR{[7=:S- #Qޒ:!K%.y Q$ՂـRpBCZٔ (>9A*j%3@ӶNVAiIK8>@AN*q$)jZ\9* {bHĔ{{-\UG,]]3H\YMYT"EVI|O𑃁-BLD36L ҆A8#x)$x)DWx 0l6  ,J vdQor ,VC:T}|9OLQ;#£,2s((pD>`0yG #t ];[kyfLYdIQDư!y?f( ϲԗ[KjQ}V~r!|AJPxv;8`Оٶv,8bL}M!Ata:~K~AX:="S/Cox`ʜ Sԛ8 bw qdy)3X; Is(KlT(6y`(oj(@EQEQkn'Vޙ$dY/M Ҡ0(, gS|`mF*D8 Lπ1j:"w),,ʲm6v;{//X6. r:xP7=ȟ{6y>== =óؙ9f ʳPLeP톟9_y3\x,bOSSS}f y0\%3˥ ,Kï+9'oy:G?a[$IT⚃e&9|LQ^۝+:q/c .25rnLcGe(Ak[pqWއ+|fS~~._ǃߝNhRO2r3qy.[^O~ A.NQy{ vcB_T8g'/}{0u@xpY3`$N)/ %2:offf򅅏-R8VI"w|ܜ ivvV^dW/,ϡ((rQ:x&"[e1_3q$!\%jiŰ5Ɠ5-ce$Wɝnɓ,WX2ۦ,(4_GGGn[Vc<Ύ-v+1^/"}$yW*j 3z=cL$˖-9I{o*KD86ޗޗ MT67>a&o"6ff;a$<*^6ۜI #C1%(7T )P K@`-jZͲ,"k-Ykt:Z7#fe^=kJce7s4Xk;Ny###eYʨ k~"[ZΡ((rQz0!br.e1Lys;hcɿku,eA/FjP1q5'禉MiPXG"c@l=LLq3 %]z}r/ ͲlttRlذ##W\t$p>9()F[X^t&&&b_و]QfƺJ♲nɀoxd.Aޢ̄02ڨ#Q,˶Uzmt7˒{|P@%LK@qXS)J'Dɂ4t:4MsVk }ퟳZ4MӴj5 kNCDb?EIeYXQE8CN6No4Úï ϟ`hh3lcJ+<.A tR7v"$DdPwi3(E1`m%NSQĕW,B6b{E\ZYSMb0g;inn^q,ʁ&&&D9/NG:̌4Ӣ8d1s###YiZEv^eypD$UDrcvIvG}@b| l [WXPXp/E~6[AZdvR P speĆ)g3ID =886B? $RVEamIȨiK{qwXXuPCQEQ@OLqbRJlg0'*3~bnsKO0$`+%g @gq%\T9vOޗ2ORƦٛ6orD%NQNs5u)*Z{斻Ҩ[ $8(A zg(̗G 3 -oy'?FjjWէ>TSe۽kԧ:O~W`mHQCOeD$6]ʲ oHcJ!bg^*G鵻S\:slu kFL:O/9Y=n0 a[ Yjfid#"883`v<3NF+9$Nk<HdȈv||[}s9?яN?4M+Ҽھ}I'$߬~(I׭[w'}Ӟv{:$I H\ѭx,]DZ-pb]ZӢbđl"!6 =3@51iOv*&RFPSZaT'P }>yćyoG>Qyn=,A s((pVZ[fS2b$0b0 OesysY{Y2lǝz Jn R1 Xt5ܯflI\xe8JR'f1H*$I&&&Dv_}{rp!=w0.ej F{yeׯI4žT<\X'QZw[7~7a=֢\0W뷷ݝYF;3A|rpn8A ruчpC֜J[Z0#Nө/nN#Xm_B Xu yC5*ؘVn\ʕ+e ._ТSEEZts6m{Z6777::EQYqgY&oc!//?׭[l6\C>O~O;˗W\3M;o$7lu i{fanGI%ҵoRk瓓#F\G˰%Uj֋ ,Ϸoo<-yx71<'GYIK$2/&&VYrBN|`>{,4R$y ) 5//efyt-$ fQl'۱c 7ޏ]qeG2Y-9Tق [TR(ƭו8ͻ.Wٍ3,Q0ay^kc&O=ƒR7b*%X{_tQq_vt:y{arrrffʑ;̆ >餓wwyjU|[kKP#$&>|OyܰyHI'y['#YlUXۣ*5m.яo?sO>8sju6_1{2.%45^$>|\3t9Xdpi3KcՈ%PkY\3xo\)|<8~Ys8>q ;O3[S:8njPGNz=IZB3RciEQEQ=J{q\m7l(mI2Яa<Qt^s#.céaYU%SZXؤK05bkj{6^IfFFTdHŘ"HR|ll}V|ؠ)vd2T*Zؗgψ Å+sܼ^_s8g۠v70gLl0d;x}_lvA!f3Ôcذ( \bmdx0T/:MPz d'<ϣ( 9.5vqjʹ?O|{ՓH!0.Muֽe/ʕ+GGG҂=РFM_Ac̯3ZnbGdJ>;U6s%11:UM%Z3kzlLZSZlGl=㛨td>:[ BHav>*8MROu:###RC P Rs((`&4yNwԻ 0%YGFfx ox~lɾmYH,1&NsLf r ω(4 KFxBӒFqØėe:m.D?;yZuz=q# hwI*ki>wcsn.gz]Vn1 e᳌+5ifS΄ai<` ,ȁ႘s’a_İYD\x/P-Dk-:jRTS4PA>ύ9b]{sL pĈ9ˑ^ˋÅnM2|eQ8kvŢ,?,``ʑz@̆wDQSqZ3[xq+Ib]A$:Ga6%؀cq<)!>g}! <nh/oql;NQE-7zo 5|wdx`YF#N:FҥJ: uuy3K`"vA',vE|Y*,T*DB^,ǑsNnzrSE~Wy,T9PդU6mڴnݺ+R ?ȥf~p '$IM,L N`-Â;ZJ_t"*57i:ney!6xg*#G֑m=`?Prö׵}k1|t-{]M錋 A8(m84|p#n3v=5B$,ˌ1r?^($,a+FHl6ro% z91EOwA$GnD_beYŐ3s 1rqd"C/y[NZmnnnddlG$Ir7^uUSSSbfÆ gvᇟuYRl`}D>.M!480lU'V^alze<atJhҙ" G'*Jɂ&20EƔ6~QK󎫧 v"+٦5X}##n]Ym a b4~j`h7AeY2s{:犢ؓ+Lx ]t `ž8 %3K]&59K!yK6Ä+RLr n7󙡆oqr]vB)CuEQEY DbO5dnZ/[??}x?55511!Q9M eOHc_YsdouDCDQ`9>6;J_p]W$M6Y@F֔Q}IyY9d[L؉ɓa&OI!)iض8$ ¯y#jDZ"g-z^l2ukBݶ6ȑk\m4klC)=Qz=)|vZvr-ȇYCB/< hfl6XG?iG=6xCp"~'@ D DoUB (gv-9FQe[-N#\ip/HC58 $+,|/Ρ((8x;50XCzey-}x;V^$(F:85G0fG֍Sא$\ۏ]A34AlХE'"A" cH ?uDz { \٢z,0psvbm^QZ )1˲v!x85GaDj#t:rI.Fc訸J=,D ޅa-9*YLA"a(* X6ޅdfW=:FrJ)97(Nypl"N`AڨC~95j:Iő؃T<%$aR q1j,J;c$ŪU$"X4Ƥ73EQ0``,14^n;RrnUͦT?9T.mHrc<5ccc'nd:#>O>O0==-ze79yͅJ hȒIDATP-0_?c_Go˾Ն_W"lx~|b%2jU|U~C u Wk׮5رc||)>,[lzzzŊ_8pcqK_38xW_W\q5לp r'z3 _NIY%"OsF%r "IU E71tt0"t:"KK"``0ItlBBaȰ= AannN%R <9PLsssFC0 g>ի$&+ x8ޛ 1E%1ִ K\tig<`lQ:UCHG0%NB_vhb@_v#d$>QLNG\Xvb >_o4 Rf odE ]ī}X b[۶mX?E^ 'Glrk`ö* FQ$2,kn{ŋ_p:X<]>k[zzKq A  (U/9o+E(m(vGU;d@~=^8AH DONEpW>S ;11b,0blqB>W:eYr)Hb`]!U>ND333b*ֶZpB1H%G{{󞉉 ̰NLL??:/[LNG̩r#zvWs9[n3և?;O. -}-37wڨԬ3`,=ԯ;ү.0/8cgO&ȐM~c⃡X pe 6=>؂1Le  WeYPE yd.p5Dr$X$hW_}-"ol6jSSSFCrɐy'rk^r+n?^NaubX0$E&Ӑ/Kq Yt#~)MVX$|u+/r!p||K_ Y=O}sl6$Wd8?^+_ *#v7 ÀQ8o**$Rei1IKd7%`aC8D€ jFPHmAmc>tJ2z8~++C| ¿k!1$fffFGGC*H nCD*^ƑeYJAnw/]KNY. O""Bytёd2B=rsJVd߯ ƄO^M9VEQC,'&&dO%e)Ē`Moz/Em& /.Ne0hdaY:0c*@¦$0^V9Ne/˘ -%T*90<8GGGCH{`LD qBc!$_\ƋgRH<SH###l||\:p%I""HC2=ᜓwM:H $BeC7$x/79AEQΆZV$ I/lVBGl6WYo7 s((" 5 ɯ&2W?}/˲fig>ַU69;uD!8O;= #ri ƀ2D ,.`N98V\u\2*.bffsܭzu׭_펎wqs=C"n_xXIb333AxVsȈZĹq?,XMW\q׋֭[˲\lي+N:G?O~O;4>uCj-Az]y`6lpe)VoꫯW2==y橩)f?O;>^w!%ۯ}{^{-[-[6::r38s=SC*K_D rU"`ؗGx09r3b|^f)jm9EfXR-83@.#*B0*oWHҪ<GyQqDQEQq ?QNG0XC֬Y#q$Yn,|$EBi$;`H JR ƀ~9./")zElz/cEy۷ok/±??z;yQG[ۥ^pǎ>xg>_9fwNl۶mgv#>h*R\,#|\r\LMMMMM_{<)ykggUW]'))qsW#QSr+Jp\5=2!e!;8 y޶zj??_Iȼ/SO}_%o}ꪫd&jvv믿G?w/| e %e,Fg}vȖEA楽{4{FFFrKЇ>/}?9RT6o|饗=3_׿/%#㎿뿾袋ͦڲ,[w /j/| y$\^_r%GdBۓ8e~mk\𖷼娣گAS^(("0GRQ"j1@Ǣ(jZju!ͅ@6lW 7WrڵVz-$U=|O?o|㗾iy m/LNN?3񌣏>Z^9ymX`(_3]2^{/~W^]sss!?O~O<5k֬\R^_moˇ>n˖-{vvGguQcccwܳ.Lr /s=GFFƞ'򕯼w1|ŊmZ{K.؁!Hs`0/u0ÉQHһfGsߓ6H>Oqo&%:ALm-ig>G? _D%||{{;1!X"$R(!)<*qׯ?s7y睒xJ83Mկ~uf/FQi(Xu%,'.΀rpI#r֯_'?~5\#yw),|֭o~Gu]I!9Bdyo8wN:c$vb#q͉uuQ7xw+-Jؓ5 aP4`@RH}( 8[pQ/iۼ_mܸ1\ՓO>_QG%<bOHQ҄T$"rH! * IZ|Qz_.y~D^_<̛o9b D^nSSSiE|oHZnh4!0P.IV2"'=L}zim^x;C71)4\+Deil6e7y~_%␖J>nrrgi&2"8֯_ IYj}\b Sf,W(AMe'Kf911̳8::l2I#Ysyi?묳:kYgh۶mw^%.)e#TV'''o<9333]rsϧ?;o~^ ß(?|{iNOO_tE_>};Y|__˿CoNNN>Ͼ+8~ғo~3}k/峌1W]udިoo/o۶-,DQ_o6|?3OԮD$Y>&''?'<[VѸ袋^򒗔eYee<77w=\tEO/"Y?oݺubKwi-/X<m4pXZSc9,ab"6 b |,q(v W j#F˜>}lrrRr˾EPC,Rߢ/+\/[n+fnߎ!;QhDNZbŊ[z|3|9;"I(ɰ4MScEtpu׽ o+R^)a-׮]i&6lxoWR9;>׾F#jVK1$| 6uY_'˶hPظd[:1y/¹0w|^ZH -5"S: 8ǀ.2ׄ.(!V'^KWnO׿.nrC<4]Vy7:::;;UDu'Ft6o|^ve2CT9e{~ $XDIR7~_.hʕ U jiuQ{ur^-<Ƃ_:|/}|tR2?J4MNN>Q+Bf+|ժUڏ}co~C!redvEoӟH4II'_a&"CL1vz.Rȷ#_5\# NZ.g(N<`OlnR f&sqo\GbXD֒hɀ#=[yPJܟ$Ifff;;nVɄ(IҤHNt?O>xų+""0NjȒZ:묵kb(Nw"iQCUo OxL@Jv:Q/_e˖r1ɧ Ї>wZ+od> 2;;;::*ng> _s6Ի,_l&IRTDhyӟ.ˏ+Wn߾߾},NsNvozӛ LTySOREQE@ϗK™ YhOOO'Ie8Je˖z>66&^0׿^.bPt:ٮ<Ϸ-d)˾awn{7owK>k||9'0*D> D•e)Vz~7~_e˲,,ccc`sNL07-[n<ޯZjnnnttZ{7򕯔gRTe=} 7S( 077iRUkEGEܲe˪ڵkO?ZVnƭ[fY&,֭[oA8H r]o~/xȈT@;s(tֈ&j ,Pj!0aXd0Qd!0Pål>O;evǾU:㋢[oEQLLL4M7]tE_~ytg/8#o-255uG{̼m۶?p`3__l޼@RfY'p=/~$7(l"GG=,I}s+T˗/udd*v%^Ԡ8?{nh|jd(Eѹ+^tz}ZmƍFhl߾믿[Y$?GQO{L=:(,fS6Q–^݃gd4#MSqkI2|M6'=IW^y[VADZtkHnW+W|csI'mڴ.{djj8 %/ABr4y'?vmW]u~i*A،Ii;'t:nd_rgLV/xm&H9ӣ>-r뭷NNNy7WU7t\CCOg㤪sg;]@,,Xbcc`.vņQc5 DE 4h@ "me{g8"dfr[yx-555_wMSN9GsΝ={  %ӧy}o|ӧcJ&No&[C@`%lp E(!-с)"_BJ0Z׾ Kk @\QSSs+***<3GAq<ϛ5kرc'M[n :sk&8 nZ뺯+unO>OD"Lʰ>7ߌ;G֭y~***9O>Ex<Ι3SKUUU h4Q`0y>̠䚛syz%%%ĠqrxG]x1;0?7޸!!@ ~…W_}5#Urt- ~gǏ5ei6k,#ܱ}m+p=!SnƂeI, @   *4d&gG-(SAǎuV  2̍(G"z .`A( _]QQһf?S.{w7\Wp1Tp.cjȑݻw> O>9rT*Jnj2ƚgy~ٳfYև~;B[---]t\cǎ1ˋi#FT?ₙfɭb=zZ/^>0dtz}gZ02|kaV5"Z) [ZllJBK;,#v B_^;cN~}lD46l:uK/Գg+V|>_VV6jԨ>lL͝;w„ w_]]i+**ƍwwS;YKRLVVV5jԨQlTJ&<ѣN+xIF{3Lss>}n_4[n9`c"ٔP\`P(T__7n?@֝ /?LZjϺy\knRѿjHp|/ 4^.Tqnbd׿(`w\*8#<3^zŋ9dyyIy׆ |W\qţ>z饗VWWs.))y~lnnb=س>۽{na=zAJKKMɼy׿555rԨQ>N"#G2cK.ѣ \.7wk?I@E]{v|VۨvO*n馩SҗNRs=GqD,cՖNy?|JqmhhN;ptEAZg:::meY3 }߯{g:wgϞd~1qt#ڶ{=޽{ii)y{)S*^=z0a.b۶9㯺ŋ766~;6tz ?̑GIWX&/G'[_|E4FG!O QGE\8ӟfm6UUU33̳>7ߔ1sg{챿dY6)]wu=?|ĉÇ7 ӳKzŊ^{m c=ƍgL<HR=_tE]tfĉ9l~.‹}  KVHDZ'瀎Jii4@+V|B/`ڈEvǃ8 -˒R>#ÇOR/))av`С&L8SƏO@ 0zc:qAwrm,L>`p]wm%/?`:}||L&3ahtʕdݻ7iJ*J^,$H" t:裏R9H޽{O8ͼO8m{у :ꨣL&NO>:}?\wuK.e'ׇ=2yWl6{ '>^y湂ٳ/[oկ~- j #@ X h!Т_PVЍZh-cKd5! BZ;҈Ol78qgm6L&ӳgϙ3gH9(*F0Ps]$l>J {o-l-AÇd2d`o1<]J/M!o$hlSN0`@uu5} C+DV@ @w#NP(6U#P3xfN8^ A{YB#Z pug},/`зmV!5Җ@Z@vNMM<OyҤI'pS+xGgI)E?lH~ZZZ@oݦb8NG뮻qh28eʔ<ʫȇL#Uum6|z|ꩧv}wNq,YBQF}уfobBzgU޻wk׮=X bfm / 6?& iŊWWWRK6뵆LJT @ccc$ ~#,Yb詧ZOMXIC@p,h  8}; Ej@hC pl ZX>  ڮ: +/lRr:cYH&r-?j STUU%\lٲ;(xwD"юϽ 7tS 7yސ!CSAYnݨg{g}oT^^ VWWo,ϣ8r.(NP(4cƌ-؂` gO4/,Fd_?#뎁@MħJn#6l'OdM$ B JKK3oks= [ uW^iAi?_ r:::7ehXe96lll+ I'D I`uu5s@}ѢE˗//O]wݕb`_WuGq,r0/nƏ>(/D} x$f.]>3}Ytd2E]DÆ믿oѣGCCE|~gyf(P35B1*dRj:눧{ˢFN!㋬BVA?2v<7;BN!hlASKPH0` DBƊ;>oI7SǏgzdt4hڸq!w.X>uH$BX([d2+W4$,g4J:M3R &Pt'1Q)--}g744Xu7o|R=?@@(*nYu4~"t2̑GSOb1ƱMMMz,XŞ}mن\F \yZf; *fe5resv~Ki,SX ,X&XH+@5R!vk5Pj:%W +!yK?KXΉF"5Lrq555qp?޽;0Pąj|BoYt:}w Cdb' 1:sP%_ǩK`}}ݩo2\5ՀRuuuG7qn}ݗÕݻ7\ڵ3 {YXAn|8s.]گ_ٳgWTT<=JA#1mrsk0OK/taYmbʔ)\0%/HPh li  C;.;.,l# 4# Kmh8na=hhh 1c2d~KKG6۰eY'Nd`n< . lRʚ [~K\:;|dZv;hll\|eYܠ¥RJ=ӦMKM w}7YD"FrA?^{ L { [MN466p8|קR)drȐ!gdM.]y^<dl1bĝwIf|SO=gc]:::mXss3F2T]t;v7b7;rHsƖX&C9䥗^"s f_~ә<駟D""d'w}Jg>>}{̶W^!T?0Q5sO---t& 5xt~)Sqk1R~]s5^0Ŷm~D(j1FrtXѨaJa&9r]w݅uu Ktiii(bR,O~D:B---b$`) Vקr_M2)ZXQzZ:4Rz.\: [6he>ۮ8J qD}%<u把 &lV@:9)6x --- {7!Ϝ_?˕gܹV1w܆r ?\paEEsRP < `.#vƇv)%RٰaÞ}Y~e-))yQPf}{_= l6TQuuu^@~dG}t㘑Rg}s(3"pd󟛿`AGr-9AtPCBxBoZHikɈ %T΅B!H:\ 'L&3sc=6ϛo Is(mjj2j|Yu} !=JvySN9]'5 ˖-4hG.~ Ǐ5Nx<>zhK!m\{fmV=[dybٖj2( t>Paҥ<3S*\~ Lx"J{`O7~>?/ a͂Wq66`RR+Ui@@  3Dw9쳍8z]w;fJ|>Iy^cc#$MJ$gy& >SD@Tx<$8p p8lv4 H't?LTGzIp?<3lvݺukjj")_>_x1oB, ^EU;.N/\]\)v-{jYi.oSCU%d% +m҅2L:hI+ *%Y{i_ (Xv:]o ?!45_ :a3LUUe9P rȬYByI'ۗI)E$V[Ϙ1t7Wu)Z;nr\U*˹Jc]v9s&իW%K7;RjԩF& 2dĈFq/QFQ&H!}(Pd'E(^uݻw<$w@}=G}Y;W[g:::mؓO>믓hS0& ,bLgC(  yE0d\&+ 0`)rK)#?_ +o%-y uB HFxw9餓ةqңG3fn1bǢE6\WZ?~1cpV`xf'䮼k}Q\ݬF{(Y9s7h|>"¤T~?s8}EAǘ+od6Rf|ӦM;Re*]ve%%%---ljG*ӒR)Hi=mY?b!-˴$,`-@*( ut5rDXhjjZpI'Iu]C V(izAȡCrxp+).rp>nKt6OC}űM}:ϊE~U֣#.RRΒ':ͺ\CŒ@2#-Y7Ѹ2$%R -r$D%4d&YRKe[$ ۥ"x ZY1bir(H>R:{lfXe-DmW^y%{)|Ȩ*d2[Blٲp8]sr-&W^ͅC=J]}}=e˖ttXd /BA]Ͽr]ׯߒ%K\׍TEk]YYII0Fkj)V oVNFs̠A xeG'<#ͪ뮻n=#ݹ#J[g:::mg}f`(5`0xr)(`#ó>XW8I*.sb oE] /}ԩŔPyGLut@|o&S-Zk`Lj x"($_q˲fΜ㯖9V> OS,.\}qv!D>}wDFe8&@]]]׮] 7< %!'-%,/%Ѩ#|TB4J lXeJ4t).ܬrt"~_lcMg 0`;'Xcx6l˲2 ϷCءdXd e.\HsS0mkvm7BtR0faƌ|3I\!U ’?D"L&CvɧNƹoirS/:ӨԕY s9?Q^5kV*jw}RZ) #4 TuEsSx8:]_)aR 1ʩCv43\I9RYH~@HGH p7ffLʿ[2y}~ᇬrQBtdz 5>վjuӭ[fVs`>l:mwm}}뭷 ɚ}tI6h#bg@);BzPVyWMQ9{SS 7HI'TZZJz."Z{.8'ts- ~-l޾g[n )t*x@ P>*dtLlԩguVss3=F!D^^uw3+jJn@ лwbA뺡Px"(;bfϞl2#T>}veIFAaxֵ2frY)u)8ua(*F/Bu]Xo2RH$vۙ@۳oFCkH( hkNRk2H h/X(Ҕ |]4`n C+Y P0 q-[uE W9o';'eeS_~ d8tuF G/:y l H) PD30z,[lѢElM6e'bB$`ΐUkP'?yd"8M ['&[2w}Ew}%{I Ԛ+/J+5?ŒT( 6_~lY0k֬vکȱYNNNpުq`BP߾}׿{|z[ H) ﭭ]t) mQ`P(ԧORrXD"Azx 8 n72mܟA1s3 Qxܸa?744RJF˦ٳK$Q./ݶi/`K  #A')ͫ|#, úE+; 4<&7t옅L )G`_<>cd2;c1jboᆑ#G U,c)Q?v>CF|>0(ofΜIk$i i^HRfn\.W,~IaollMWkM zR>[o]^^^i;h߿nX _Q*ʮ%_>k*qNZ’2YׇB Q;BD|?yI`7܀rV{r+/ԩQLnǏ" 5?gN;ߖaƌCܐGdz7Z&[0Lw+d5-^O>;w.x'sM0"v' BU܂ kWP*jtЀ %3 p}K4i;WB)/Lu D"F=kWu9"b'nF}WmݨQ.l6K66ށbv;Q )lOnHĮ2˲GSc&`be.={ np<7h?g1:iii,3fȑ#1Sxy6-.D"*1%>05$OBoabŊwaqO>am`Z㧲&dTdBj=w܏>/X|y*0]G"(<  ;N[#-xhZg9DQe~Z4,[fNriӦdhii)--$Rꢋ.s= h;W_} m%m~|1˲~{.m9~ܵ˸QFU~i>K.?LR]nsϰaHFM^P{*(B?t`vgI`r~/RBi@ ()eYޥ^J Sȑ# V>tm\[*⡊+^|yKKˌ3N7|L&0˲>bϠAX$ c,߶] `7Ox QYY5§_C1WHz1۶ɶ'q'͊1A#:z+V`0JNŪ_~?Et:ګzjҥ}g}6o޼Fƍ$B!\]<ɑ-[>7Es?Z۷x:uPv@܃hRYlT `#H 8\ xZ%ri@ltm]_t B \諸p1jvr9u8͛w73\ƆǶZmm!Hx$9yomqy4͑$ZM[3P o:r9jkkO9뮻gh2JkG\]ark{݋m! Ĕt о@V"!$ $$ e;TRuYx"~ʻWYY^{7tSӏDGlvҥ ,?>lqJ}4.\@J<o̼t4m F*CP s8%KKKM(a[zץ^xG}G~ᇼdrڴiSLg}ڱQjg쀲KOX ;t+.i 7sna"U8`ŭkFlZ̠mq^f >#,Nb1"g27|瞻KKK:|}ه5R4IP|8cfB'XV3!VΩmED8pl, K Y͹N%HgtV{Df>MQkhy"UIY a.]%^hmR5\ӿ ! TWW3aq饗}9)fTudX,,X77n_x,@c5*JAP1ڥKҚӭ[b %~BCG$ܔeDB 5Tt p8z\@F^4H\AijiZJ~,fĈfϸWfٛnCaVJ s]k2Z3ڦn/|yFu]`7pÔ)So1@ 0p[ou;dRFy}Q $l䦠a;~`װyB8n-KXY-,B h(˗ssXqo… }'Uw}oVk:H6%S(s%%%dr…?9sfΜurl6k*/0Mc*** COr`,d~jm 핝1M777qڀ( ? ȁx6<tIXSZ766_?#˞{9mڴvącnV]t3,p9"/}뭷k؃ƒ#&3$9N ?F?bQѾB( r6`H6Zm,)$,@R^- uX "]˱Zk!5DhZ9BpnC= =޽;ٺcƌAQ_Ps]NK6HhK@VxY;~?;ZL&ä69|iӦ$d2\H]B&a{'MZޯB@\Ș-BHh-?{JH-$ eksXի'W$1bgrQGqP((Imm1czwLer0iST8fm~_κ7UF3*x&Iv"q326& #_z9M!#NRsQ,--eP8a޼y ,hכ- Mi: iK%-ai-Q[M^\A(-jf(t|FdZ[0B@*ߗZڐaW3|4`0 @J@1brU,NG"E+N 4FQsTjk/_>y)S&+${i~_|qO.w}gΜ IyQ\*++bo&NL2uԖd֣`)b1JgrmORuMO9*ґ- :, -tkR|?;4 \]hW(  aDz BC4lPӖ;,z |?~G}Rʉ'>_PٳZPs]g|M&S$Fm uQhՌ9ۅ ~?B'/bke1'n$9d8&Se;g0J@WkmS;  kXrs9*c9@^hRڄ(}/@]m ېfӄAa/5Dq2`_8OK9I>2ί|>?bĈk&$P(ܼjn24 mA;B;Pa hH[}ίuC h L=mֿ\@LpB|C7}}t9INnƍw=yS]sd%E_k_ί:iii(*NX(f\"`q }Y&H Ӈ9PqK"'|B_3у M6لIt=BV[r˖-ŠΚ\7㏚6W\3GuSO=JU0¦CTcjdV"o#TcniѣVBh@ bi>lP"`+ !: | @e$TSWP3>ϳvL&b &G"3 k:N$1}̽,`OV4 A*l6[QQ!GO*H #8䓓̙3'OD|ŊF۷N4d2&%MMMŋ9vddЁ"H$}PGeILoIRR::PI(e13 AZ)l6˂)XauR@%BRT(2!e`e<.__`0<3N;4~?W_㏹avA)>XCu" űᚶT%&xeô`,}oӧϙ3?y睷`, ٶnaٲe`cDG)hxz I)kkkr;SGcrfm'h4Z[["z29}7tDJJJX:Fp"EP\ hKh HpxՑ?笄/ |_j%*4sh8q>illZWVV7{G:&[n1$cYnFQC-xbSk<2#z,H577~}]6FRX,&{_x pi+ Z'4U8ҸCYuyLE2_s5J)3 x 7?4)bvǻロuh,x=ܡ;%I$ YyuaaI<燹C$*]wqرcoGy뮣30m|Gt? Cq;wn;?R吀IK!:觕E ,t|(X6~mer Qd58i{njj2L$dx#Qy^:{ѣ~m^~ /;ֺk׮<+>^EEym;finm@ {GL]6w#B @, @)JZ$I)e$H.cW:[ ˿qstZuZuZ,Rq!3<# 8 21g  g}Axd2L(pwr M`AFu($DXG$;WafBmecDkx\[ջw!Cr-O<wO?`, 6I&w_}}= #<9r$C3]-N D"9\n6+/Mcs_5m+.>Ԣ{p- T* m @2$Z9Bc~$.! 3F@>`0x1DŽav#ओN|9qnwM1MMMsφko=z/on%KL4gϞr4w&Qϟeq 7 ֞R)%d_E~XxR*Q0(87ۭҽ7mzD߳:GuZuZuo,:1R0Ӭ붴0^yӟz)Ӊ|駛ROfeYƍD)S~m):BK,Ft| qtWB427pP(N8_qVVON`djc Sms]d!.//[zdr# b&kٲe-zx@`]w%ìYHp9 3L&Mw|>?ka:>_,mh[Âv4fQL  ԎL(OBK ! Uo<7ޘJ>Gb}Arb1FB?ꫯ.]Q055}`Hɚɠ'xGez(O>O>LFfs_Q;|B_B+4ԀfUB\C@ziu l< ̷~z7ݺukjj"Qnii)8J|mX4+6) de}ᇦVQT3ޥP(TUU X,fYҥKm[n'P洲cK@ pM7s1_5&|i*++Me -dk}]^xsyꩧɑf]׭Ɍ-I/k4ݪ-t+pCC(C3$< %T+~G KL}(VmK K?ϊKw~w7裏HY>0aH${i:Jb0Y)0J 5W6a~> q(>mO )+o˯暏>Xl(ޒ-pvtŶݻyLՇbryq>S4$ ( OQ|TѣGϞ=e.C39ZA㲩q|19z {Ǚ SB^ф zt9-%ݚSTvkp`Q8gR:f+O>('=z(Bs$,Zum٦{e-X`ڴim;qgҥg67sMf/gs())oF9||N=՟4 *9RSCdCjGYL'aرfuX,׿;Æ~͈Z߿?WWVPlھLCW;n8E"ᒒx;l:J3 < /X/n?Ǐ饗zϱYNNNmY,0`7ޘ刧={޿N;>s/_o0#FhϞ=>~DW]u.K|3 nVUUuwqq֘]w}w"BG]vR|nmKK * Yzi_Il W*АdQ$%[I55Ǫc* ld_]]Ǝ˦@?@sss<76$Cad\e0FQaٳghv\YYyǢ(!;nܸK[C1Hw}dTmdjSw}'_L2ŤPfKG&{PhYg׿pMMMxoO3GL>z|A[_pƩ9`wꪫBPcc#=^{ѣG{g}>:gҾ>^2d|okLԄ辘 ( a+(% y 2H&" Bv-_3exd ɟL2)dSȥK"lLYYi4h]`KKKf`Y=qP(篻+W>}ܨO|MaLȸ E˜9s ۴=;3 l6{&V|IpcKuII m?c<=ۗ`0H6 c&c7}2]B7x{L˔I8gqkXďKҌVQG 6Fw2LUU,1.qꮼJB\"p's[3h)"mI +5 (8Z@YmQ틅.XajK<,E'W+\,vR},B }TXb~R>G,E`쥞\i+Zh; hUh7ck5pHppdXy衇oN"cWN[{|0`_7yFP6l.^P} -lζr`| f"09Jr}d"wJ)/첣>@:&S*O<1h dXLFk]VV6|~fٲe`0Q633EY6ק66#қJ)^z]{uuuuяmN8#F1b_7YJrʲ2@~eYq?>Fz? a ]2l_i2i!k똗h*oWV_GM=B!X,m(-r^BZړv0I /* 4iÉ9e"m-I?SgEВfT.nbl=쳯>kBSk9诒dž+̇RVp577oFP5͞yTJҥ bc\}< 9fM}oxd2yI'=ɞ5j̙3KJJ zĈp.[sg`bKCvdd.[xc=vI'kR`n~Dj'qaicAAoR 2/L$M "۶WXqi}G*++}߿ F&F!жmE|9[ f2s!_P.Wxi]mpBIZ*)lHe[Bf!rJǜ@VBYH-\.[b]\)pHTC*4P֔KBg Ȧ" dT2nӷ5oii!R*\wu&MdCJ9ruh4;i#6)NgDn~mNp8|q}[m`2Svb %Qp`<9<2qIX2jgqu fYni044ĴC`/H@Y!](ohx 4P9(2T"k/s79/~(gB-:xtXYey?E·֑X<rg-}݉CO>W_}uvrJ)kjjwlٲ?RMU "}p>8SLZc̋R8+O?QFb/҈#n]rU1},Y2cƌ: ?Э[?Z(Xn ~Hz磻%u_[AxZx!UxKĂ9ͅ=x[p|@<-44WJ-,'4/݌ zBT\>v O)--0",>d4^%@F1@  q<𐃖&*^ҳ٬%Cc&q)G5p]Cꫯ8\YX2_or0U0t'JzÇO83Ŷ \}W\qEn 𶘦ᄏJ% JJ)mK`رCmDh̭(k+hYu4Z%l7:N봵2d6z4l6ǧMF5c@{؍u=>xpu]̋&777b16cG>g+KztV6()r ([ Kjú=6 +MfZ輥ҎA_YJ*Zf{ٮ3l8e3HDd cm_irJƽk="yD"qOSN9#/}Gl8eUU^{UJΰ{׻w%K̛7/Jd"=x|7lꚛyڧvZ8>ꨣ9~s}]FX| $r:#ƙf4A>[o/7o/uGydΜ9G9%{KKkvM7}gnv[6gKU\ZK,+Ci#p|sJ7Ә=K񫒾k[.}DV sr)K]AKh}aIGN"FxyO/{}F3;QZ#th<ȧ~?bo?? u?쳻g@eee}} 7PYY~y՚D+!}Kh %@ %YY<–Rσta|ڳ*9+  (P*oĠ|(+%>/lߖJS$+ uufзVnEEEyy9bZTխͿ8af|B+d!WpS0hc| t@.#A$innsl.ut:K.~,:蠃^z`Ix<z!1M`CtT1奶!u|D"DRjU:HD=L~РA~m*b-K?.]ׄa9Í6hɒ%L13ǏYgĈue}[n{g߾}-o}L&-e6קO6ޭ6Մ>Au gNk-J4H'Y޸ύ~K -a5W{4l/`9!Pv0g"a7es"{j+ Cڎ"Çүzn!_( G,Am;^~!CT*eY&lN >߿?!B;㢋. Ӏdǩ֭{1$K Z㏟tIݺu7;{.]xqbo4 ۍ !uV[[krk>j(εT*qV{kkk9}UW]uUӋյ5!/ g\^0}>/?ZeQ&|Ge6NZx R,|*l+ |~.+B0 -a7i:xUm @4)!y>ISO= +>hžm۶m\rO&p}s߿?wsԸq:R3O+**6x}]b[ LB 7H1:jԨs=~疖*k`$Yb7pݛ@xC1cF.cr8wu%\¥IJ,6z} ~]ƿ/AaMRR)f0$}]S+=ȑ#;<駟rTW+y晔%p!m{D"mQ87o|@"PԻ ovӰUF\.9[,/.s[zǃ2,isFc+Ծٮ(Dضt$@: PK(B+jvt(5p}6~8bѸFPЅ:cIIIc.>UbFf}ߟ7oMA+p [)g /4{-| /ps3h*++bC :t覛njjj&OꫯHsҥK{l=g у~ر *LfYӌB|m(jPI7H+X;6 k- ہ턻wmg}߭ڗPV9!|! a-9 %҆5m!Rnh[K_h@˰Td~۶eYBBX]#)NSOB{9JKKy6[0?"L7pn:lib/ 1?FH>g}Z˗/J|$U:fjB$RuKK hoOgq;t')˗/1ZZw޽︮;qD9B3aZ@*++kjj&Mo1e￿>s|O>V?ĝwiE<fx-wDPxǎ{뭷wygw&Ǥiȣn.F D"q766^uUDQq„ 5]6ͫb=_1W~Z(eЎc楶JH)I:2Z(_B[p!\-<-嵭<"PN;PH?8(·öe,sT/)c”K4SYM4~S0+NJ$A0<묳ƏA'\[ d hll+njjFr/555dY$h[կXv@&O<3woL͕(㫔8Zgq6? fmuuu:$***ZZZ)ֹȡe%, SW8;L%\[EZ{Z}ݼCEHNFl')E"CI ڷMlnGX|ƭ *YJywQKr{ɜmI К,`:# %6T*IG5&q0ل1VUUux(`O4@)sϽL>Rҩo1e:$ollr^x޲&.o6g4hq$ŋ_q sˤkVoe+%H$rG~c1YK-VBii(Yhljx8P^/rϲe 딂 ²r9eC /ZyekXZ|z5YP)5t^p Ht\qii)[4mhhxg>(b?'mYJJJ~iu_|E sf͚eDŹ\ȑH+0 _K.---o6lŋq$ɖӧϘ1y$!N:>ǎ 06Ӡ*Y1ٌH$kõh";4}S 9:4 ڏJLi  BJ#>BdqeOKKK8b+o L= L7HzLW s4j+eGy$Q͂<eoz ֚1j:gϬyz,rl*L,r$Ic3ES졧s1-io|S X=i)\E&K&-Oh%-*,M;?irVă*HpR`I6Aj!A'p@>B-eۑX[lucÇ~M8nݺ`cv:1KfjP#1]`Yl6{VTT}aJ2C1xquڕ,rf|?}EǖґV{Co}D/PrA[cˠJxRK|@i!}rEmS[gѥj@iwu["OHl!yY5@IIT{ȑ#/7<̟gF-VkLpB7Vvg\.i]]zsot( f'aA*ZIK|V[vJoEt +kGE,IY@,q ',u^}D4XpBi'c+LJ%ees=&]---6V&[ةO nf3ɱz E?ʴnahQ<9g}?#2 wW_}uwϊLCIcX__ߥKogy&?>( >./6%:ӅɽI l/\ ^z]veBjQ#BꫯꪫH+kd***ءB3Rzys.V'^U#~1bķ~K߀Z\:frC0a%KݛKVh } H`X&`$^VAV@pP?%GK,- X6TJ@i`UBNSRk]cd(agdmYl4FigJݲ,3$8(񫮺W^yK.Z`_!=?QtL^ ˜kܸqN<ܬ^DK.׸bI@竒ht6GU|@XВЪ5Ӣ^X&XG6 h|\K p Guԭ:~xr0ä8ˇ v]wa)%( r,_<H)pݙdE)9uJ'MtwS!Q 6xk?YjAj+y' tpJ86O VpˋֹV\YYYyu}G9ʕ+ *Ͽw~566EeY˗/'꜉h4jL/3X;řg9hРK/>bYi\μK_~y~& r6h#N.`h4J9֖HgzVk|4fb!3G( :iҤq= yk]T^o:GmpTU\?IN ś7l_OŹ9l]ɬu0WjBE2o9s#<'m;Jiٳ|^{yѕ)Ve`wOL&C"3ˑǡ ^1D|g2x^# &4<v?=[" *DSc<־Z %`+@)(0ٴ1X$Y#룲dV%CFeE:lW -y)$Kg)_ث<|iYZ㥕m:NZ__?~3g~'9|ͷv۽{=d>0`(9VGlR*(AT[AH$y晷~/uk׮|g1tP8lLtŊd0B#zn)+WN0O?%y&iii ݺu2dH׮]=]vEHyQ{e/^ǻwc1cƠ 8|_~wyoU`;wX,u.nَ :t'|r^m0?0)SfZc_~W^ye޼y˗/ ]tg}:޽{ӽI$ /Z" d>@" ϹM|׳e9ZmׅJB -֎oee$9߸ҕ5B#=b;6>O*Z[RJiCbp~O6'6[ L7E fͣ'fE,_,rkl2MǴiӞ~iӦ-X@)ճgm>#HziYŋk|G>3LL=dgr g'e$nN% K{1Q @\&kih%Y׉G[nPy,bsXJF-826PK}W9ܒx~vo=xh!;;ZyKAh(Ȥ˃A?ZT>l %iXr]d?\.JrnP1H_lXfU|: ?L0A`bx<^7mFӌ:#jgЅ ^fL zL09')Y3#Tݒdeee1*_G3Ĝ&4$|OP45k*ZZZ9e8fG" Mw2NsD ZhFky-i=:s(oMjpl47{-ad͖F-ZjҞHG w+gRKZ=u5KvtO?z*= k{,\iR Iie C8 _JJvp-X6hu5dg6Da/΢oBv?XeCg,2mO6$u;9n -Lbf6h6 O|>(hh<:*47fSL>V^ػhKN߽;5&E&@%!0$(BDAve^j[eKk;AE"2CL!PUxwɭTTrϭ~Vͭ{}{}~ 713e`#V# 9+$1Cvso?7MQ8/IA^:b#:5 cx5Ul6 :01'^~#w}oѾnUaULR=Tgw|ui VCw #m1+`*̠{XG:Fa`U(/$*{ROUΣUt^wOIӌrkk*s>m{6 *1;ȍ1#:[)u& ϺIj`| |5,nOgH&{/|3ӹ4.[&Db`4EKc:omCZkY痻 FK˝YvV8:v 8xCjunڎ7 eNuB[}WRz՚oFw+TQ*l w4ZmzgVE5괿: Uːc4zjc򒶲҂ՒяbDrUZ3O`B*:vXU҅{zuq'B𷏶FWW ? 3j;!lQ4;^}:<剫@%hՅǰ8#5^:3Zee6C~lʱTZ*zήFPw_~~Mw{y/VZ";:㈚0QmaȚ㼪jgtV/Muxse^^oFFL؝NVJ&zWLZ'R 7T6"$N xGt"T%EUcJ=rU7Wk.|6wC5Fi՝]h[rnwzz]zk^[j)mA k߹7Swk,ǫn%UUVrsC N VM"uc$ [^8+fuGS9 )99LaެT`~'bŕ$)r;9 W$/{2"5A" +1Do߾B׻[暷m^zŕ(tk9tvԩvp~&>εZo73qcL(r06+BNk%|"R?FDhuxX+D~BhWS=14CLY7oꫛͦjk3_nmnnN}ݻvz|g~w][nQi+EF?g$1 &פyYxr?<(b10V( ܠ4ZNZ+ֈ(kxÇVy$Zo ""GZ$IQeYcjzѵlFXkfxuv- !"`Z t 3,y V ,5 :mw閭ӛ6$9H0(atRsδc7Q^ GUS-҇q0 =tOO\s5<8M{~Ъ֧z˿{h6@S*~|O6 D#0FK/Z+6[ͷBdLL^/}KEQh/??0ƈsN;^V%I7DZy*NA 5j!&inҜrZTQZܘ1WkK;bEbDլAVw`#`hTFaI<_'72X?n`)q&Ͳ׃)0H [mQ1a #AV@8hBs#98,fA3 +BPfe;S>< FLZ<]Gi8<%LhO~-oy__v]F8zx\o9 fgg:h4o}t:V4s>̚j0lab׬>WፅE0N^89=݅42?Գϳi Vfeiql@C2A` V+X:W(1(˲lvړZO~~ieYV[XXzeYךr^Wf'''u1ާv86y| e6oZg?,lȃZQ#QDYHJ#IDqq!1AglqE!($GM[Jjuy2haޅЏġ㴧M4)Bĵ$)>CDtmhBfFEQ4Zd 5NB/Έ [axlԬ]|yMXW ʚx=:91V bvzѕnκ,uQ#0D8@(c XHS@}!1M μVs G#Mb""!q#;:>Ƙ:?WW_}?yin5z 𖷼g~ga>$ͪƩXack.|چ\`W u0w$(Iv&]3Pg&6R+%$ MV &H$AB01" HrǦ:՘D5X?kv0nezO}WZjFTL4zM߸NcZM;49g=4Ig(*Z"ƓduݪNuy< xp זwxtVSt9#":Yh*F1NtX5㫚'=(gZ3F8Dޗe$I~_}W>W_}]wum۷O'X'''ggg~/ꪫ=\EQq\M4@'FUGGx9:x@j !Vx}a5јկk]]7X'_7_җ뮪Xgs//~YDDWo"0'"9<$pG NүS])kvӳl#A;2chcFtbq\uu֭[z=}3IWߪn92jnZdk _NCV몫ꪫHQF#MSUNfb)-#UO~s;u\tE5E_bq8ZSZA (l4頟w7åInG&6{-3X:݅z `h; .Zs4tҫU`r9(O(W!(tUxE0jW\D8²,\8F~FO#cE1&Im۶y~Q?n<JA[iDcncN2F0,D"0@8EQvpo kQ8( CGM4Jt߻(fcN{7hG]t^r=p^:IF_Woa9xA rTsy_'gZ[E5ЉWkmQQN'jqmqqq87,8,BEI#!G4z4(qtܩ51"7<"3ba9$֗H$C)$ +={=#K\$qҺ(5,waI;xW󁩳v\)޲N5G~OD`ύ;H9Y XT\#Yi(s}D88鱌!-H1Z;jz4*8tmý%qh×hVGO[!I %lw(%@B/ʢ l _" /`LL9rݻ;s%&5Kt7͚rU>M|[5(Hg`;NV ZjǸXTo]:YiިBO`m3Zic'|9<mS D ₇#C\gbSg&B8$|Q(`154Ơ[捉f)n˟Q pi y^=Iithrwx|to9B ʲPMjo*!Z-EQhۉjBǏ4('h(ae&jT{_( r<>G_28k `65^ \11,IQ# e%Ƶ^:|nytrz$y{86ez?r""z6ӱg@x ކλX{э*-D&A5z|䍾',FF?{"x|χƇ6B\2r- )$aXg[^$X)ŬPxu-ֵؚDDDDDDDDDDd X-d%Hp{.F82Xklc HscEH8GU }MoL"""""""""q1#F0È⃵6v65H[c(0 7-FSA H`lc`1q:iG6$"""""""""X #FjnE! bHlLougۅ-aD70Bn4iE6-ԀGUh#bhl0`(ƹHLTo|z ֈ1AZ pREfvy3S:2q"""""""""ڸ ;V`ekmE\V;N?gvf[Tq5Bb1"lzv|B-&9h{h #0Vl`##e[\^4΋bncafeEH8c jb0, 0,Vz.[6lxSB1qTF}hŪa`ES7Q Y``*W%Uz {=1 {sHjO;gCK>8wŽ. vlnbwe`Lr2r8aT,0L"#< """"""""Gq ZB!fukOƧOn;wKzJ35ЋQ6"iS z?qfX->J&  1bb4ín#L """""""": 1{ι(VW"CFPglҮD@jE&.1i&F%4QZsJ~0 &, B w]-`H smc (bga LYYbMQd疓VK"e'c-`z?qdxTXFsp0X^ 60׸psmZk(,K-`Ux\޲E:Tk Ds.2&xB0G'AL0~A0a1Oq +b-GB0guj,b Da {on$I(0[#_|00"i>} ۳.cc !Q#2YQdq'q Iiܔ9$qCEIl1iʰJUŮGIQU8h\zxP;DVÃBYOƇӲ,< #zNDCG( O&? ":Qũpq7ц&0A `X. %YY-1Ap ҨM{Q=Jb/堟e&5k#$v^վ+T944%vǍzذ֞:DLKƍS=E_}!9WSOLլE;8>EŒsm`%ĔFA6~D!88 4*3I,Pt/!vEF^xk}ˬ%q e1pQ ڨX0ԱDQbqQ骱1:Cl`jDeYFQTkѸ\ hUY:)Ht;O!8իQyuJNTFOqJv~z`-t &V8Ħ#{x{W%T/}Y^'w6 `a x;c+R#Uפa=eE]]#jiUDC鑫x<%+U:' %+IoXPȡ䨦F;Fۏ ڸLX|0xqt{^05L5) &r6lahy%ߡer"WP'V`V[u65Kqsh`cX5޻FDC:0GÐDchq_-)*Zrwf+YJ5N D'cEo<Xя26!?TٽEN$5=8 a,<M7O?y`vٜjVL$pze5L`ccf$UU`Ѥџ:2DDcE?y0%CGRšJ4VFOzz$c$Ρjr7^5Dt gGRTsꖰ&ޒ {XMzCI!A`Dҏr{޳Xc㤖>Vkj 6'_][cZ5<ϫvD4T7Z=)-S- %7uSq3z<'hKQݸr?FTabs;w#"1I騽A'.KX+|8G09R&+qSIZL0c7""""""""":9@L.ϤS2[Ϝj̺_ZЃfrTZk-zB [Ϝ?ms=Q8FDBaز#ںc9•r0 3Lp3QXLrYn5 ncA VKW*Nhwh,#I^ j.5-ۑ,z^$k-|-5eKmr؇Vm""""""""":1 " .?0il>mitzJnfj}Zluzkl=N X%$$XcHY#X+LL\d|<Fֈs&\+zbat#! ADDDDDDDDD`hH16/AGBD|Y_Hak@40@>V """"""""zB0ADt 0@)  `DF~B؜8ѐK0?O4{/Ap9yi&,F?px,\EDDDDDDDDt1AD9UNF1C,XhsxDDDDDDDDDt0ADt!IpFD8+bRŰ-9 6X 0(Xch k%X3,4eX)ED +0"oMfm&Lp@ j興N*s YkKX`CY.81:͉G/ȴ&&b' @J=ɉN,9BI\ʲ#|Ii j42P,8!0` &v790sx8GHK ˽nZ۞<՞@mi9l;(L>v(3o8ѐ36DͺbX;o Z:߽dpܴ6B F381uAX X8wDDD%2I-{Y,kEǩs)$+ˢ@~Oݴ8E:|B.>A,DCp&#"""""""": 1ADF 7fZI>Jr7O5> y^”.ilS3xB:F9̕7[An;Oe1+""""""""q"!VO}{e+IL&h.ElFh$LT(}?֦:)%:,"#pA"A6$ADDDDDDDDtqq1H/~ll}uJA?GAj5fzp%v4j//0b z?>""""""""DD pQ$"""""""":0AD4J,g6rb eކX&2HƤl;{4lr`t=9#"bٌbhUEZ(M斧өt`v{f1ҤެmI ?dY&@a[""""""""":q "]z-E~+Z15l? ( ֓Zq&B;+eaSX2DV3x`# ^ """"""""z0ADb1>wymFEنuE(䒚2";^8#4aaJ@`3G`bN]F14Ю .`` PEE>&#v؅VVg VDs X#ֈ @0 O p<-f"/3iZ3<]՝;kJ S/Anv+ph' [)XA>!|`sЩ,҄,ii@G>,.|Q3?>P8a,{+tp@!El  Ii. r',aR1 1-%/a  ``fY4n uKN6& m`q""""""""q:o!FD &x#&DOd& @1"V @"Vl0 4Ú, lk i䉤y$GI y-0z3yz""""""""St검щcN 8X #8: r rN7VxkO.[X=IޏtJoAqvuX'X:*eؠB Xp`j6vk'YoSad$ҩn!b$8`.X,$pDhJg7Z 7,4Xx`b!& 0#08ܼ[Lx,DDDDDDDDDD'5sЩM, V` XHMT8  뽧O 0i(䚥-ZX6-j<SX1V F ^[׋L0hp1.Xh)6Ь* Bˁ!aF {=,9 1bl0| cK:(s5Ʋ1h>|`u؁`Äx + d y66|!8iH.1Z+x oPPPPZ$;> 4Sj;t<f5 AlꍴYEI|0l(C<$f!4ܢVX+֊ְzH""""""""""|:X-&x H|ӷOөA [a*deثF[5q&uiYE9lߢ#QlCnB k1ÍW"""""""""#1A41 ]hlq^^:$̛^Qĭ./ͷ&hFƖQ \Rz68h8VV099|0lgDDDDDDDDDpSk mMZ I z8 P4a Hd}PrgC֔x&jբnyp%Ę`  """"""""#0A4#ƉA @i '>X8acM0ݚؘ`Zli6*9hb6*9hb6*9hb6*9hb6*9hb6*9hb6*9h:!~ 2}9v,oO܅@Dz^u_EQۇeYʲv#NSLsDDDDDDDDDDD'S1ayn4MsEQei !8ք"䜫 !h #nkͲ,MS}vn@jlC#"ZcLE(f$iZ! F8O*:{A 6mt:nꪫ=.lΝ8F@D(kCDQ%I$ɽկ~k_R!+ C,kD+brrrϞ=_K/}+^1;;j8ww^ojjjeeerr;ӗ^WoլF8j>1#">%IrСZv7oޜe͛?ٟm6EQhnGPsoo$wޙV+x߮<ϝsz91??_e$eYs{eqMMM1faaAӞ{׈hHDF٬BsssιSN4Ddrr2c-I.q355EcAhLOO뉮 ^0_D4EQݮw:~EQA5*2M<ϭ6msdzS.!@{i|G>r}j5-oEQ?oo;=Z3>xT{~(ݻwm;vv~[o?|@Yi1ٳ(-[hI+\۽ $я }8<ѩ锫[U-w۶mIGV{ߟݻ9wgu^r%?,q-WC}ӟj=??iOvKKK\pOO/|~~VyVDONN6My;yxӟ?/zыs[ng>gϞ5$L""""""""""S)zn#yg~gs?~hA?tЮ]"K.DU9[> 8Y]jxC|fuX}-?8077?\\\ܽ{wy?y:ZM!߿{w1 DD.X>s.MSPqΕe9 CkCDxI4VthܬOhLɆg!]=!?˲fesh ,˪%'P]iÎS#C8,DcxT"k{1Z|w*TsF5Xe18i' X]٪Y(rrr 'VObUI$9xјV"$Iכ8" h4%!VK:rqK>r8Mt'D19$I%DFG3KPƇO:NL. ]]#)J-罼>QITߟ'hFylM+M~899yA,j Gn1V+-3Q0},כ-..zĄ.ʲq0==tGjKDFہ| _*Gz _?ff@sεZ-D!:9y?DQ4DDcbtH.IS1:{D㦪xPqh)XX548%LL,tzG;t:cL8^\\Jp85F*xZSz1ZgC_KfƇbu_h|e^D4,kʸshn,R0t29!ϝ1Fn/..NLLEl6,4~rrB^L*JY W=U("ӽ^~hhvq(zNDc$I^iSkm-HD.011Q)BJ4>y,:N\-N4>Sm: hhh$RQ `~WFVySd>2αyf WdYy[Zt:nWOpgggYYn5U ^[N1&EQM)}ɭn㜛ԽqAVKs>nw`099_^^(I&#Rʞj!O%Ƈځ@DcEKj5Ӫ_Dt1ٯ1FrRh1ק"ΥѐґNtq=d>Traݼy~vvvqqСCSSS4CD+Gi58o>ڒnV#(Q6 ^{\pffT}ŵҒ6,˶o^y~zzzqqq~~~vvV_*DC,A"ck81>9~NMM5]]׍K?3KGUJ΋HQU:h܌64+`Z]V=kD4gFy$:xkcM 8zkHMCE7W Mj_$ٶmFVˮ P׷lٲ{Zv 7h#MS@,nf4ͳ>[ %I2557xy睧9~ѸEDmviz-uvӅH333G{rF)˲dYl68|~hhZ94lqL4Vt8Fg'N4nUQ=*ѸQgeY8%Z?"1:޻FDCZ9W&qґZUO5q*Qcg~_k1Ļ f<)OK=H17 Ѐ V[t 7x+s-yxYEtlee;FSa,88rI8)ѸxhOSfGDc8aGhh~^3Y:3ܮ]9~?55U_<`KڙC, ((5Ghx(nlGkB"D44#k<$RiSΟf]!^-kDqZZHUhwC{BoUQy睻w^YY|ӦMvt>O~G~.˲L Li 7~7>я~sj$IoNgQ5}쓟߿e˖׼5~հ_ZZڲe]wug=Y/Ї@ۭj4ݶm۞={iv6iW|]v6O?|楗^zW-=hMӯ}k/o|vt:nwjjǯꪳ>[Dy[UA~jlB(Rz^o1Py{bHh m׫jqkiH-YZ*]9VW=ZiV~$M19yUA~}hhDmjj -,,e Fl6]sssιSN4DdrrRc֧jhE,~jlvw`$IRsz]ZUmDZ~h6FUJS.F7n9TE FT2 qέIcrEcF&7v(~ayh=C?%+qk&1FEQ(X։ya!˲*ƁoYe*DcEkGZ8_4^VXGQtB_)E6~΋^WV!U^t߿4WEQLOOWkJ33wK#ј(>9]v饗vb#1ozi%/y_|t~~^w,^hjN~~LLLb:VN!T-4d5o 9h¿ۿ (۷{DDky|geeebbb r`0^oyyRX]#!cLofni$ gQƇnٲ%˲啕y22Hu81x08EbB0jFQtCڼ @8%Y\uG5"hbbb޽xeeE/9NƇ6I(4-bee( =9Ƈqr^]1&D3AI3AGދH$5(ȈJVYkz=qcUF#M`5_D4tTѸ ! fuXkkZקD( mDWE_ZZ2LNN~ةUVz)z<UQι<=Ltw˼^qY9M`vkDcVc4uu:]ܣgD4&VK$H9Ɗ6H]/NW1Ռ~x@DG+B9-W5ƄVD45h,{nbhZZ_~a"H=O4VnWô6zet*QuwN<*Q6y*"eY:x"7GSkcscl65*˲NrLD@gNu@Ь"ι,zVEQ,/":LÐz]uʔ`] ":*W$7ʲѸ !PU9K]Ѽl*bDc^EsGRh|TKA:غ;ED2Y68ғ":* oXk$I1 KKKz:DcEKTct 9v{!nURDB,.G4nz];A4VVVZVQYU'\GOsѱ<)Oy+_TםsGDڋ.5yMeFEmD^_yv{_EmDcE/zST\E;wޗeh~h Zvc>KA׽uy[k8> SzԈh,4zFcw~EQi t:Vkw&:zTՙS_Dtf\i꽯D4V<8333 4e3x<˲Zfrz21ADLLh|TCRcEQ 9ѸWs1!ՁD4@CEQiEDUKDD'DqJ49/]s5Yj(4=Z֭[;. .زeW~Wn`p91TCo7{_? w}wn/䒙G7U5zw}=s駟s9)8 jzDrSFD2S!h8'''G 68%:8A1<ϳ,K$I-l}ڵhoꪫTezիc8pw\|Uc#iabbDYqE<˲8^Y/V.{k1:׬ZncfF;Ӟ "qk468Ͳ,MSRf~x,{&&&83< oxD3Nju :yvmpM7tp ݻwo۶m˖-O}SݿwSSSh)-GeYߟۯol4۷o߹s _;wV߿u~XSNhIՅo}[^{{^fsǎ/~񋯸 X}eF#qJ'qJ}Q~gggg>sg/Ⳟ,)B(.)Lj3W+++v{0o+fCұ1AR4Mvvv( JDҫjfYXXhZK_Ris96m vuַ5 B^zizHSCUb$v޽{59ͬF%qHeSED$sI[//7ov^oy{{>/F^As6NS4۷>(F;X\\?яի^5==]BU_$i~?>99{;w~~_WMoZ^^n4[nջ[mz$9t_{8^\\,}袋]xSSS ι4MC8N1n4%(dg?}s7o7m$ޗvN&6NJ{o߾(&&&ޏC UA~8ahЦKKK~3}vv6scNVz=^{wc>E/9y?[VQ_skZW\qEV˲LV͜Hk6^{bԝIZyͪV̪н{^s5ˣz dSM)Gyjl6=ܣ^߷o//q"8==gϞ??vw͛7z=-ZzWt2qUwZ:`0h6tLsm URa=tFh(~K_g>OO^?OUP1SSS{zU H=EDٳWhPhcT{oIӴ_UV[XXvW\qk_s=`0ַ77~?}/JeYW_o|c۶m{ _Wh}_Ї> SԶm^赥N|߾kn_-orq_?zOK^^ц0VT'Ox˿t;w|3n+++w|~w%hc7qJ'z[J!R7?яz=klQjNvS:8K88P4}sP+iyyOwXw8cDD\4M'&&fffF*m۶ ov:ު.cܯn+_JO{vء."7|7_o۶EBNFrO~++{ޙ6C $48替Ȳl_cq ]z88pnoɟ4Mk7b5#,OI0Nl,1w4m6{[[Vӹ ԧjnatAqkK߿/zыqh4(Z\\ tХizmgYk喗o~뼪N/袩<ϯm۶@mNNNr-ܺu`0x _;M5 ree^?)O8DQ `˲WZ8Nd2nTۏm:\}ViDZc]vEQqJ'qXJ]XXn?ψ8EIhZfg>S)D@$q'IAeP4OBWa*IFqwLOOgY655Z$IYzֳ8v뮻hW$1sݴiS6^y3ۿ۟g}W򕯜k9M5zY ͛/~oZU'eEpZn9l68־`˖-e=ܺusw:9xq:11v[}w饗"\\B[1V]wuyOOO_yKKKӘyլ??{n,fӸ^7==S3q:t@ !henCXsE7tSet2qe7^w][N;ZQI@W]wuZN&6NCdKKK NgbbB/<_?11qJ @CnVEQ{_h/߿EYg?ԬN?ӝNG3C+++t1V)ַ]l6uVTx3i8slj,˲^d @ ̎;m699ygq~>NWEQiz7 眵vӦM,S%}\ә\8!DQT׫eX]at u0l߾VZM hd6y]>X4m6V'wncUz<!4 j,8Nd2nTDtpyD__?Oͽ/K^$Nuj[+9N3n9߽{ںuk5 ,..7EQY\^1xұ&:JN,//eBM鹝~ʿ#Moz~f8s'&&z^WzWVKZso|#I/<ϓ$YsD68N5Qcǎ39S%O%i8=~offFzqw2!={>5g^߽{Cu9ޑW #Z\\ !pI1z]&X^}_馛K^ZvmeYn)4pb5F۽+gq=j+Ue89N19w^-z///ڵk׮]{ wsܳ>{ΝڇCnmff&2S:Dk6gq_~|; @(Zԧ>ֶT_җ4ۉX_j"rnݺut1DœBz j_աC;wd6Z yιI=9AP<xN333>W>:8jZs|^nffF+:t!iCqI4JE7q+_?~ڴi^;O˝[rg|Ʃ1{G͛7<Eѫ^ /P7bLԠܜrg|)7zEQ,,,||w#6I>iZk9N8A1x1|_ZZZp׼eYNOOvi[ugy>11YEQE_~뭷jťI]ՇozҗFѫ)N"ZcCS׍Wߊ@jq!_~rr+XӢ(1ZM/;u% ڮ]^=993˲Z$;FQdZZZsGq#:j//u]Ž;>쫮u{zxMqdƩ^{okkm6x+UέhAqs89Nd2V?A}~vnٳgjjjٳ~vo}[uHFQj99) 96`0Fv{jjjjjJBH7nC}v=nu.>øtc5ÿoy_apfFƘQщW_+++g~(ɟ2TeUXӪYVH4ཟٿ?}?77g} ^?(*"˲FQsn4S Xh 8~ {#WYq3gܷ-nimԂ~Rå4ADBńh/kx+%^ʵ-۲Ϝ͞;vG,3gζ9>9x0ʮW#˝yj2}I}+9EiF"uon:0Ν{Wb1ǑMgP)e?圜UNRRI6_TFH`ͪU֬Yx\.s{ݺueɮ82DNI-J)C=TT7(Xe J:i\Ȕts=EinUwFLs܊+ĄR*NY_%K&&&,я~dlVn}$x? D]׭jlvk׮[+ʣLn^~駟_"˭^zƌ~[Mҝ[nb[nݿ9sR۷ob}c{7pXAi4C釓H$RY꺾e˖_Wӟd4իW_~]]]ccc}}}CkJz^8D"pxppofVa< Iя̾c'K+j*/`>_]wuƍ󟏏oܸ/j):Iuh4׮]L/^x5x?>k,$$SyڵkF4MJ)IKΝ|;vX+sR*HHI+4M.6Ѷm0d9LE2, uYի={^|ENO˗btttϞ=HY~Ϟ=|޲  ۶!fS.Uu]׫c=nٲE~3fu]7pL- 9-r> 9s=w7o޼K rwܑ,˲,K6P2L@OjSCʺLv˩PMCӥ!4fϞ} 7^:F"\㺮,r۟j|fbbBA)%/4Rjd2y\ou.C s*KLr\:H$bY4xw5_lYZr_8'ph: "/۶-ӮtHAMd1H\ve|>H۲LiQJqb%%KH{ƍh0Fi&q2M:3SG/hSAϩ Bݻwr-O?R\.|7,Y2 Cn+,uPʩ?";o۶Xn]8,+~K_w|Ds+x<8p@)FV66jxubdlۖr@k*i!UJJ%T(<0b(T/rq4M۷os1c 0xP(tMHD&K3+RDd2eYrog͚U*,9Ei69R;J%=H$zzzFZ-.MӤ9E W)mۖN./3 coRΝ;[|M0Bg-8GPT aio}kk岮벉\4oRij39T*Tq0~_?N$ho⋓ɤ<.FQ7WTJ b|>/ϫ~syJ"{~{ .<79/ΝNGGGe91kZs8ܝ{ヲiJĄyCaXnd2N]?J%u)eӚp8\(٬R*WP($_htぁT*%rJN8JV3S@m eRqɒ%===MLLȞQo޽{e9ŀmɀV9 B }ߞ;wҥK׮]{Wɍ"aR䐏:4J?ᡇzWu]֬YL&-˒jgT;\םfL{|RXl޼y0ӗP(#\zs=' B冚0.6rX7zb1r L_NѨl ir:22R*͖宮b(2رC\3ON5ϧlvx<. B4v)-eYR0^i!@N1@_tڿT*T^BR]LF<?R/*ivRJ.Bjr)tGDMNlǎO=gq=,ZH^922";r(a;mϜ9s߾}망vڊ+/Ο?_X0 4-˒VG'I',}t] 4L&͛=r3gd:ziZ6,wޑf2Ao!Z&]FWW) ,wS;Zm̙ݹ\KӴD"1::?>>nƞ={;8]N:$9q>{。PP(Bj*Z:y^V?,e맞z4" @jL4X,a'I|rziڞ={^y啗^z)N۶}+tQcGT[Tbzgmۖf-Zt뭷5˦iX,&9(pM_NeOd-_tEs!t̙R8p~aCH$lz]~7m/t:pBg~9猍S@LFC(%vHgܹ͓O>XV={gi۶v4V98ju˖-2%+m[F111qR#lZs*sↆ1W^եJRB Rx_vq]W6<묳,X ǟx|>b3gΔc=6>>nf,[rŴ4J~鲐qbb瞓dRX⪝;w>ӦitI~?ŋSwّHDVRe9lZw]\.GQOJd"y$5kL!8MkNWX!E|;ZM&"yۼys$T*FW.]:|rs(4)`ddD5ޣhFӴ+W6zkERj*44I_n9bȜ[ x߽{;;۷o|||ÖetWcLwNC{^8s 'Jm۶ILuRٳw-J{ݷo_X4MX /rjuƍ?+4::nܸQ 2_:tԲL&se 6GyƦMFFFLTJg?׿o~q]pXL6ԑO}SDz}m۲,MȌoikH$reY~9㯔)eӝ4]vwqǖ-[ Ð۰a}*,XpVɚ]uU-hGL 醑JZ.neYxRʶmt 6 D"H$y'=ϳm;Isr*mKc޽Ud2yϜ9sdd$J9c۶lgYP(R.ꫯaZsZ.u]7MnVZU]m4pFp 7 +VZ[o9S(6lSO-\p||hܹu3f ^{ՙPӚSBknt:d^xᅿ4z^N<đjŊRސiJ˗+Dzx\ZLez]JyAGMvjB!:lٲ-[S˦;Z.2M[֌3v}]wɆ[n<ϛ1cF&+<Ȇb1wy\O9{5'~H$rꩧ:SVwرi&W3\liZ)"w~Xpܹse`% CL{i_\׍FmˎR-^/?i($ mn.ZvVSRʶ{ϲd2iYV</JDhD"x*n5k֤RB`YK/cǎa9s= _B(r])_T.m}ݗ^zi8)J%W*ugϞ}w~TM]>irRp؟.c~ӛx<>>>.brl6+ ):ITx%~XT*l߾]ZArr;S6鑏d29E Ge]ܮ8\!FOO޽{7n(׉J$rʮ]jZOO'\AmrV^D"2y4M)o 9se(.^ zNe7VP(a[TǑęLF}6n9FCtl6NOʾT*%IqLӌ麮^zŋ_}?? Ø5k֩z9,ZJGDm[N5M\.wמ{CCC۶m۵kmhtΜ9^x`___CStvTb& Bymrц9-JJt:}%,[lo믿.{.\x),Xq)>j\,˒Do/sׇaj5+RR)N+8PH">!\VcT_|6cccˊb*GRTfJCh%7͆aZ4 h]6iZ j#B韞iU8~R)%8k)E]GR洙8FCޥr]W:rLuz>?sLvNn<9E'iۜ6ofζm˲ Ð'P{طS@8V8bR4 J?&|2l Szaic_ ңh-^KQMVw]u]xG02rds)+:yTj2S'k؃EhsS!6_"wdUy\sɰPHfJStv!W68d~.rж9m^ch4ǔ\Lʿ܇Ⱘs- * s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:* s:tϮS%tEXtdate:create2019-10-12T07:39:39+00:00%tEXtdate:modify2019-10-12T07:39:39+00:00ýyvIENDB`kalign-3.5.1/doc/images/Bralibase_scores.jpeg000066400000000000000000005433421515023132300211160ustar00rootroot00000000000000JFIF,,C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222M" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( j~kJp_6e2z 5ngo&ס*x p]A +Z%4Fx⴫_{>(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*_ɫkʿh?&joBLHB:p+|mmZ76vYz_^%>xCu w0}cR:q=<ɠx;HҦ kkr`6?2h~+Agj(Oz @h،⾎a]jr0!cFS?@EV;NԵB<7i<1ePwd1J·~-xYXaԭt2㷔Aw+qҳ<[Esq=SB4$$T*Ҁ8_ mI+dI>s:Ey6aviì]YOBj*8$s^{om6EaIaA$?*?no-=k2y@# om֯&>(\rWSq_Dxk]75U)B@G^⟇3^WIG@UK5~8ԮkZMݵCm`88 ϋ]/lii$'s5xgPo~̥#o:\=wn%s'#yQG) (DQp|bР>jDk )~k~4hZ՗;=_O}aО+,Z m|/ᘇ NSdgTEXH=@/IqOOv,8eIMvwOC|MՂy:x+n_%FC|>'x0@{guI>Ѵdy}rOC׭|[&7vZlxIQmkn!e]![ vzI5߉3犥|T,HB%PpdgoQԜWPh7cd;dc5O?+_*$L6Cp :+ |j휚¦I#Pi;סq; Uc2F6gs8V)n=X9.s1;T7k?ޟ[kY\r9'@y_cPo4j1?:׏{w|oe&jL]͹91>={ sڟφzu^kKk)\r`:_񆱦k/;=2c@8HhJ(>[ֵh˭bHVDطNR|aj}ԚN!d?ZMTȀc!|r9owƹORYm/7GUqʓe"h,qϭLn[u=8ǰNrvjRWlz?~sĚ/|I⫿*;O ؗ FtWyCI\_Xo,ge;e^MKV<.n?E_5>'[k CsL5ܫ$=N*χ5ɫ|t ?gֹi0jߴMܠx˝pYMWԔo~'x5~3kearCIlaÏ$貨ý{_Yt,VW@uc?|WWN}!y}ix=9R<3j֯.!IwvEWA~4f{=yO-iJ8-;Y7؃z' s@M?9:iO<!'h=p1??_\8XELkkvi\(͂߂sMkw7v|ֹkKuc_Ax/JDV۠T=yʂ,IkЊ u&J',C$){^k?FMA滅mre`%Kr l~_MR㭲^|^mdɭ-lz\@ X j5xE N͹Tϯֹ7/~i^-R?/.끠!d;c.xk|FYm>̈́2Em&f.H@V];Gi3*1o~:x@֤Ao3{2R&ʾb;}OW1N" 32uXu_THdKB b/r15;NR|l#]֯{$c,wQG4_|c'BSf?e_{W8#|@֥5O ?љ ;?Nzy4&=xy`Ҧ'ڐOgS__YYiPYРHaUG@OEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP^UA3迓Wךr _ٴۏDU-#‚q@[m_SU5ϊSIҼ3-;8hv,??._OI5_ 5$+K$)=Kx3XLZ@DO9=w'<G↫n!31 pԬCÞ澋߇/=OR-h-IhnSD N~ߴF;k;g |z+V" bid8Pԓ]uiq\󴒴ieB8\kWZ寋?ĈdQԎ]XV‹sRno紟4¾oVSK{ #onq=k)$BO&o#A>jmb@ZLq줍&rc1]'Okm๴TI[ ˷X€9]/lii$')oPϣ_hڅ͜b}6%$׭B{[ZA,Hg$trMy"faҸ_jqhߴ'`A{fұ IGz78uM:W.uD̾Zdpkwſu8aTsnm1˷9>PҀ29Va>k0O,|' y8]Z [a5e;G Uv?e^?7j=Ң|[Cرc>d{7C=VԅGP9e pR&ƝqInҴj9^|5? vEȵhc۫1$#~|Dĺ%# {壓;3Gq@|&J{؟yǽ/I|MXi6Vq0}+uz<2>B 3O/ݾ"]جG# cf>tb?Z%|sS766Ow Wv߸^KBv m+O* FG"kwſu8aTsnm1˷9>S~(xŢ~"M.)8H2h+j fp k'i>LٿO -2:Q,Gk|n{#kDR#db;}ɯ|5? vEȵhc۫1$;Pl㸌G#25_ l}kݿO?Up~귚Š)W7ݒ?S2<3XYۋcñK~iExVLuc@׉!OVp"w,@I8⻿ dO6@q-?9־~ύ2 OPKcEƉrV%nI?}-km 68z*(_qwцxw[7z6oc$ׅ.et9+#=kךׯUc%ib]6=E-מ|sFtmB^[kWTpJ{ʹ7ZF$dhFF?&(n|sby{p Ćz|ɨx_ƟU>%$#2#GWG<\};EA}*!WE'8?\4mW [;׻I0Vc XvqҼ s㮇+ <"1W1၎Z$',zg~к\|ZM'fLxqʀ=G%{G~2%׿܋G%vR4z=:u2h/bRaZXus"F#.P( ozj/jա _CH|ڝՕdo*B8ⸯ/5톍]ZG ywHAk/WC{I 6k:,V_vk[g),8%A{zC\W~ vAJ`VA x j֟.ߴRd)@B;AJO ݥ=@Lv<2'W^5~5/JdIg%C8GvNk ^:eVR >0˹ӚMyfRa9S}OPi<*C[s3ɟ;JZXX~-o 8㸮?~2b}ž)w ixOaKS*<Eor)׫ ~ ï0~t>8-EU' m=(Z*ؿ3&_ʞ2&FpyzU(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*(n=]ֿƀ >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2teߟ ǏjK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi # ]9Um}~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_.-C`sU/AS?y/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_<-9UnO_~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_.#-?1 _O/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_K#-/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2fyY[U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@x֪U?QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnO_nO_ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[=*[=*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((_:Vk,װQIoMe _'g@p0?Z(4$8HRpI4+Γ&צ_ hOa[/nxK+.lnU]nǨ23^PSEs-xpZ[Zvq|dw$4!>V flϯPQQ/K/HeiH 7OuK W%0y1Gc|0{_Y3;˃Xkմm?]lb@x"8]~~ԍF?\F b²oV`YĖ$04£?!@O:>'?3|hdΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΙ'ewsQk?Q9PJo;IG$g}_>/Y?I?h援K@$t}O:g%|$^PI#@znlSRAr>;3g7KAYErĹb'F _IG$g}_>/u boy|nAI?bMhI?K3 P2I< o%: ۿi]؊䃗d^/8JidιHl~ >/UXrMxծ?I?h援KPt$t}O:g%|'?0E<,eʏK\ͽ oFRw\C?#$t}O:g%|$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_ռB:DӉ\HHR47ddΏIPy$G* y_$gLm7U_KVYZó0!$t}O:g%|'?dΫ]-gi-˖)!@i:4DT}8vdSU&筍/IG$g}_>/IIG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t(y_|#'Eons巙Ӈ*o;IG$g}_>/Y?I?h[׏j;cq@023UӣjMI?$u}m7?sia? 4qgi yω|A4||z}+jVE(x }kfYEqso$g- hxK54Lm|g?}Xam-`yY2v+s~u |ңJUS3kԕ;IHYGRN#/U]C=ZTVG<qߣ#5wG$nUO$uz;u'?dθXw_Vm/hI?$tϴKG%O:>'?3|hdΏILD4}_@b/TI\uþ~m]j)ϚN&qWxIG$g}_>/Y?I?h援K@$t}O:g%G1<3;~s Ej; +$ghR}_;$g&kph欻VO:>'?3|hdΏIY>!t YYX'NUI9gs.rz/hg$J8:_IG$g}_>/fu$t}O:g%|'?dΙhD4,[aCsO$u⛶d]_%}o}.iR'?dΙhD5IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t2ybXt7?haY|skCکە\~'?dΙhD5g0IG$g}_5)eeڪ#,sbqTR]4i$g ?_j?"gK\_q<줌C6X=7>3R}OU͵i5ek> ݇_ڴ+U ,%Vo6n9O:>'?q?x|%VKAkV5lռڊ_ps$Pc#$g:ƚZg]>\tҫ"B V^7^jgͨQ4];IM{vK;"IGj(lO m`zXY_V'Qn08L9#ulw$g9KZ}OGlH]>#SxY_FO;$I?/5XѼaKvɈӐ@^} *Ս$r~'?dι_LϺj?=u_dΘor,lè \&w?Zluy,u)oV0&q.)ags>'?dι_LϺjG(u_dΏI\0IGf܌_µ ZSV(|C'?dΙhD5'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?I?h援K@$t}O:g%|'?dΙhD4IG$g}_>/?DF*%2-8'?dΏILD4}_$g>/hI?$tϴKG%O:>'?3|hdΏILD4}_$g> L{`۳==W|ԧ*o[(1dΏILD4}_GO:>'?3|hdΏILD4}_$g>/hI?$tϴKG% t1ZpM#L͐jQEQEQEQEQEQEA{ \\@&@OLE`x÷~dIL&abWP=+0|[+LPFTԏʴ,s/ٖ$'z|4`Oˈn&2}>V[^:_j`~8Jh_Ş_Yj0\(bOx.f0dǯʿ^EGoV[8bP+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<(($c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQYڟemZФΜnekQE2(/y/y?5sD:M?A?5[OmqO*vH*LjU*LjVJ(3_Uo\s?}\[V?q]+??𳨢+((欧U4fW(Rtj.I˜9(H ( ( ( ( lF7PX:ʓب#h|Q |_ V^]ށֺKk8s#t*:nv9KGV:ꪺv'fy^a>VKJyO~ZoUgC/>}2|ٖm~)c/mo9!r3hw!Ŋψ+Ѭ* /5kh-͏qn3Vm>Eיrn'Ԍm:>ʝZQ3~{#?mأV}^6Fo#?mأV}^@o+/Z.mɡDL*G"jG7B6}<ұ?+Yܿ WFt2c_ ^Us"}z />_0ȱ<y14E>d3ɏy(ci|}\,y14Eyd$ր鋏 Z5]=,NGc]+k_ WKY?#ۭFQEsQ@W)i\jk-?] v?KoSQE`QEQEQEQEQErv:+տYXcS B(<(+ްIc\wO/?-/j+oZ#𢂭kRk@T(//^?+??Z׿OcOa%Q\ǰQEQErG_Y?*k?gWS]/Do_^EWQEQEQEQEQEQEW'e#u/#X>*eQ[xWH?+Ԏ)DBSrד0SH=Rb@('??xœQ O'?GA?ȫ{O k'Z_U6+88= Ni2է5cE+Oҽr<k]ҿ+񎂖#_#ɓ?ݯc KG_CץK_IFt::+OC !dk_eS_z_K+OC !dhU?/ޯ&\ы]&ALeCx"^|~gQ{9Z]gsV\{ |`x3@] k[O\_ʏ.?/N̆qq*<:.|T|Cfg\Cjk*^~Q\FEPEPEPEPEPEPEPEPX!ק"9Iߞ1[x>o+3:XiNO̙#)AK][=SnoO€*QESS]7O퐺nj8H֙jkX NzU_ȷy_ UȾѫey󥈨B-w: (=((WS\=wBQE`QEQEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPA 7??ʁwc,L5;4iOg-M>goF3>)ϙ5 UV=ZT*:r3?&礟5v0 F9<(+SE3_hSE3_h*װQw-UiO 6=$ i)ϙ4)ϙ514xL'cc_ԣ~ǧa7$jo:dVi]7NqSYalX70UF0RrJx4cFhorox|onwOjz+O_yYCMQ..!?YŐC)e8DP> zWQRryqϔ ֦˩YmeHek7?/j*-pq}5[ ɋ~ 5^=OųDk32dzd"z"^"l :WoJ8Ueٜ7c>S7Zŷ6A-9mWW#U?:/qS7Z&o2yӊ+%yc>S7ZxTmB{kRUiVQ~G-WqҔVU[ ^%)$f]khPdYMϬZ>;?u5")TpTևԜc'Tpi/~?M-uOI>Q>Vq(֤(~_Oo֮ꊟ}^*=oMCښ\#;Ex??ƏMCG=_X{R!".Ӽ}8}M*Š((((((uoA5W'm# = Q[xQ\f2x[ >CvjO'lǚC0 c\wO4s1;#ϵ/1>ǎg:Ƚ{\Ok*7v8.#mm2ۈj%IÙo:OGmwWo:6xy|Y}[;(G:ΥsOg.Ă`@]?-+9Ҕ%˹߃GOA4Ԟ햿}~a]VsG_Y?*kҦ½? ?^mz-<?Q Po{+h ο^mTysXʮrr@P?ȫ{O kFS)&7._:(e.# [ҧwJ4\8?*Ζc)Ir( cw cw½T`k -`k -Qvj cw§Ւ]EXjJk k?O vøɻ|,i8gU>spN; <Я%jʤ>xni7sּУχz} Z+?~zןyX^KEK!O3KУZrG^umPνq;&g* (Ә(((((n naVpxo'qVT[i߹ݏa8{( ( qV7q38t" tnoOªU? EP?[/*__c_-}C_/tj_E׉A?݊s3WC|o+_r8,Et?@6آE 1Y iO$Vr=몬ȳ\q8j*ڂ]ұ?+Yܿ WiemdOFc {ןȭg_r)^&'+'CġFL·zGu~D/G@6٢Sxo_o= m5E}}O >#= m5ZDkQX'UQ0ɭ}.?*Rn(R:qKOc_149MY[XASkWAN#0NJ}=LD&?k?k?z5n2>?#???j(ԭ~?Zzu֛qwQl0>W=? Z?1Jtw r[ ((((((uoA5W'm# = Q[yXIѿYE:?^^^W_Јu (BŠ( oȯ}Ѕy:w|O'ş+?!^my"Ν_$:7_y_=jAֿ?^Aֿ?=J (aEPZ5kܛCq5{'k0=kOk_M| y+^J(;= N8)5̭<_?~YEp{ߑzOx?񮲊>o=N'<_bH;xRc&q\cJj_!?*JwVfjRgCEWrQ@Q@Q@Q@Q@rv_9KY\R'5mzYQEq_¸/JA~OיTƈC( ( Dַ+*CZ -9wJ4\8?*;t?JOp/x/GQEgQE딟1kkɬ?))?bn w+C.$Qe·m>< Wt5?o-zfQ;o5.wd?cxGTQ.vp(8AhF?I\?bit5׊/CjB(#(((((o~J+o~J+?S𠢊+c ( qV7q38t" tnoOªU? EP?[/*__c_-}C_/tj_-!5 7-/-9J]s+2OC J:Svÿ-ChxOJ}(ҿo| 9jA0?T?@e[AmqVݱq<͕EME?TEZ7J,75ɕWovT^Vg%FRI>Q\'Z/tu~]__w:Z+EN_~~/9s?ȫ{O j'Z/uizsinҙd r`pJʄ⦮J+"]'i^ip5Uw._떟s_]O<# ( k k?O5딟1k~g|qwo+{WCYb/S٭񰢊+(Y#sO {ş1Y4ں _!EWQEQEQEQEQE7%u7%u f?ďPQEQ@q^8?k}YWK?AKH: Z 7zU*ꟅT(_-}C_/tj>5tw,o+y~Wmz_?ާ(#(((((?;J+?;J+?k3uQElyEP\YW[\YWg& _U6fi Фm}m}Q@*7?Elk- AKҟYQE{EPEP-a#tkZFߓS]?F^EWQEQEQEQETq\Tx&TCF_?ֺ< t?/QZ!EPζe(0}Gu-dXA?]xa8دJ>=&(h ( G7׬ȳ\1_ \"Z EtQ\AEP? Z?g!KOWC]R"QEqQ@Q@Q@u\_/z֍#Y[_ʼnQrꎶ(<(+_Wi\_Xұ g~34(,ףלiQcyYG7tw~EWPQE6gNWE{+ͯ?YӿCF O#;sGX:b:b^O^ER,({Ok_M| y+\U5&v <ZQd2::( ( _!?ָnk/7ɛ3+(((((+_Fr?koSβ(<+:B/'iV3ѢtQZi5Ώ>Co|ZV_Tϵ!r6NZB#X5jM/_WatWxpV\v!%Ft?֢a| +M" h8d!G O'ҿݓd<kКQ01ۿ5Q\QE딟1kkɬ?))?bn w+WC\? ݿ^[aEW9QExF+?WC\?bit5ۉ/CzB(#(((((o~J+o~J+?S𠢊+c8Cښj6*u<? RhQ%~_k+S矵jh&o?QY{>`c 0^EkS,V<}O7=#qg*_9_s%. fIl4Y`|Ole $f@i@UȫyO ki?Y!UV8G$1SPXշUoUo,~$c*  9n~0S_H~&/7??ʆ8?}5vux#٫pGvg/䂊((/ Ky) l3/z|wBK7,o+y~Vbvw?1h0 ( ( ( ( (93#d3#dOC0_AEVǞQExEuxEy_& i )Ym!Vk AV$eMk,u%d[V@$ O]_W߱\0I3-ZFz%j/(W߱Z}f7F#z%j/(W߱G`/ζe(0}Gwy_jd>^v\u[Rx1*8zTl( ( G7׬ȳ\1_ \"Z|%iiL1D\#3NnL+E U3gL+OY,?:gGΙ?Q9eS?,'g#_Ҵύ[͏Ry_AZ+aِK/_Ύ($(nk+kfh0 ( ( ( ;G:v85b_ RٗK^)hRKҠFw ԿͿ®jb;-[MIrS1]+}YWB: TR䗑xꎥ SQEtaxu+J+/_WatW}nD/+,"}+?Jep~?կj7--n<er`?¹@Gk{9ӌe+5s’jҥΥnlG} <B9?xBM;O+;Dx8Ti7e?EX}=GΏz'(} n!ҙև rߣ4F]Sq` ==Ώz'+4u=gI?ѬoJ`_a[Ppvy+ѕJ\.t^ +vB Wt5ϋ3Vם?~B*}SCZG@OoW&F<sG?G!QS# (Q@Wܛ=STU R6>hCʻצV6#:s?1kwuo^Og nwt9V?ۚ//_O5Q;7/ ۚ/!7ySLndpۚ/ߋȡme8'w\Ogr?֔'F=Z2M۔訢P((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?E/ G{cxG;?TYQE{GQEVo^?*ҬȽU#RǗ"z"^"O+Դ/Ae-_X{)3tc 'gdw:?,??Uv_qO)U6GR7e3G+O6&!fקQKRW$Nz?s|Gâo&Y$wg~8=>(0}Gu-d_h>ߧǜHדee:ahZ+οƏ?礿?)ϙǗz-ڟ4?3;~d9^Z9]eA#,98R`=yH*z0x>tVYF3p6qֿܛC {鸷m8v Q=[}~*?{T]VuAn?)0}Qw=[h֭)EF?kG˝J‚F+<(((?&U]KAWJ[3J_ď)x3Vם?~B*}SCZG@OoW&F<sG?G!QS# (Q@Iα\Z< 3][Wα\Z< 3][Ww~dc$47AT 7AT 7AT 2oYOGO{Px#٫3~]c;?|$QElpQ@YV;+,+ O+̾(?݉rL+8B(|C"\VfEC?—M}ޯ>M}פXjU3mޯ>M}ޯ>M}פG^0Wy]u %rA]ştk?yORuiSp'(FO̎?? Hb*o H<=#*o H<=#*o H<=#*o H<=#*o H<=#?;J+)珹]cGza.ˆh=#<5?G_CT]COe$VFE,yg?"1U'5Jj-ӿ//+kmJU>[OJyg?"W?+,Jb):NWj-v +_[( ʹެjkk3O|?^YȭW_x.Ɵ?ư|w4StȒ!UEݥ!`F s?.Fh%t.]0Ok T7詼=##*o H<=#*o H<=#*o H<=#*o H<=#*o H<=##Y[ #L>d|Bd=o?_k'Gʧ:(<(/]q~;]cJ6wQL^^sd3^\9_Wo3]>yy'zG}V46~?D4TATyzG}VǞq:/IߗJm08yz} zyyWCQEQE`E[Ϫk^wI OȫyO ki?Y!\ou]|3zÏZ((((((L+L+hS?PQEwԘ^^u5&?,WחUȏP+,('wיM"'$n)Tڟ_y9?Tz KAvhhWk_=? ((m/JuB5 k101{Ћ[nh-%ўu]H*R cO_?Oj;7Mֺ]Z[$i'tp$։-*rjϡ\>2;Toj ;^2X f?袊0 ( ( ( oWW\]]gCh?gt (8('_g1]+}YWB: Ɵ%= O{(E?+֝[VŠ(D VD @dEG+Z|M"Bt8jO?ٿtU/@'7$eAT5jPֿscBwo+{WCW3Vם?~B*}SCZG@OoW&F<sG?G!QS# (Q@Iα\Z< 3][Wα\Z< 3][Ww~u]|3zÏZ((((((L+L+hS?PQEwԘ^^u5&?,WחUȏP+,('wיM"'$n)Tڟ_y9?Tz KAvhhWk_=? ((ZkOɯ3]gs]ZGT($B(s?λJm:f#+(((* MzPY r5Y r5t5s⿁?G~,o? MW,|ʟQEQ@rgw?멮[L?v> zMQ\FEPEPEPEPEP{]F 5 wFbzܪz+c ( wW?qg~Y7J,75VPYx\\#c1kCyujpUT䗾~6(J?G7Q VA& ~Cފ6(J?G7Q VA& >CނOȯ}Ѕy:w|OoI.RI]@Uj–~)qs(bֹ(ׇgt8g*1v܎ l DU$1RŰ+ƴ?tߥK}Z+93Jʬ~?-\k4Ű+Ɠ^Ҵ4;bI2cEQeuf8J=)K]Xs |5k~]/j9+:g?c.0ԵYC+{s_AZI6Z>Lh?M_AZES &p9::(p(+kkFr|:((((_ Uu/^M)l)>oWW\]]gCh?gt (8('_g1]+}YWB: Ɵ%= O{(<(wk'5(㪖}j3EqD="T*jqQEXD VD @dEG+Z|M"Bt8jO?ٿtU/@'7$eAT5jPֿscBwo+{WCW;Qe!XalXy?w?,W|QSK՞EWpQ@fEC?*ڇpYb?/G y|*'Wh_/+dQ?KBMXF_"_3ʯQEVQEG-Wq#Mu7_G?]Mr;?u4=aEW)QEx>,o+y~Vbvw?1h0 ( ( ( ( (93#d3#dOC0_AEVǞqRcz-yԘ^^^[V"=B(P(+ğ-^e7WEk̦R@J#j?sQ?,??UڥE]|Lh >(P(Fk?W&6k_u }ujvGSo:z( ( m:+~Z3/:(#(((/7 M=Čy?uxMk _ (8B(7ǝl?[PǼ_X)Q ĿnlO/FU^>W ĿK>)U=!A5 c2qbq*ڻҒݗQӃ/aq/?Y:֥o/#eS]T*!MB1EWsQ@Q@Q@UԿy\A5j_ RٚR$}Qh!FXe68?N Ѽ=6 G:F#m0'55C|ᇶv>{G~.o?›5C|?U+e_N ?:|c)]?ɣk4~-y~&N>,N^X#r: 5MHIRC $mcݎ.m9?g{/[~u)4,6ê\`OyISQNj'dqDrNMjv20IEeT]D2(|]QN;`E [=*[=*o/?j7UscݪE?fW;Һ*đYOW7v3@p 隱Eg8C;;ZlFf??']]=^}Xd䖱K&sf-uWM T ɥi Q}dWOEs}~eyyhqKspY9"(+b'Z Qp+((( zn*ցg-J$60`Jw\Uo2A\TJr&+K?Bc+Ps}{? s}/? C}{? s}szEwoҽBYV~*J3kS=N=#Ɨ$>w?'yj~(PҴ@'j U53}O9->w? O;wk[1G y|:/XZG,-?|w}oxE/(iilm{(GnڹG@OoU jm1<ZI jW'V|<鷃4z?#t*}y2^U /`~?M__M*ђ<"_ g^M ?~S(_)SDŽY}YmQ ?~S(N?<'_7Mcx;@g>]v9f$Z V׉Wv.m( ( |O#=Fj|O#=Fp~|(#((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?E/ G{cxG;?TYQE{GQEVo^?*ҬȽU#RǗ"z"^"O+Դ/Ae-,o+y~Wmz_?ާ(#(((((?;J+?;J+?k3uQEly5&?,WםI/o#( (2I"q5SȩI%zo?[&o ?*_6Ns?OE]Z?,??UF肊(5 (<lֿ< rk#fo?Y_+֫/d?C+=(/i\Ҹs?λr㯙(0 ( ( ( -w+XgF-w+U "V˿oE䞈QEW?܏B5W?܏B5ۀ+ftTQEqQ@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQES&Q'Sc'߿GWg\g>?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—A^_y|;#t7=Q^IQ@q~6Ov?-?uۗ|GAEWQEQEQETq\Tx&TCF_?ֺ<}ig๸̻w]GޗA o+2JiNX4Oz_-oK4{^Ƨ򿸿EP!mz_-ࣚ=W:Oxʹ/\G608`xʲI[ħ%$QEZl[_r-u6o\W~ȅ2j(BoC*[AU(Ҭ9_o? M\ρ 7|kG>UO((m3Fu-sλpOCz_ (#(((((#X^լt{D]PT+vA_?om4x&8cZqhrPBp=GG%7¼#Ruo#RuoNG^޿J4oh? KKTx_z#?(ѿ SԤ6 B #ǨK¡Ԩ<+΢u(٣l6J%vOE:tS>exX}$k;_z'^no?戟uTzU-zQAV5 V5 *QE?]-hxEEg'@rvK]1w~SFΒ(c ( (9m#F{u5i3}dЫw7/@+((((* UWRUpҖҗ#^?uuxՏ!z]z1X9'v^)eO >=/DT G9c_7C%W2T4hbܿAU*ܿAU(ȹyЅkVO\tBSS/)/@'7fUW$:~( ( O k?c?+ *zMQ\FEPEPEPEPEP=WA7ɿV7.gUMroU ˟c/Х?Tu4QElyEP\G!g\޸BX?C+y_?ȥDD;KעWU:CQEQE`E[Ϫk^wI OȫyO ki?Y!\ou]|3zÏZ((((F`Y&#˸>Gq֕ o{V%vG%vS8e#dc <ﻏZy\#kU"wkOŻE#vy\#iG ˸G/_Rcz-yʰ2t 1^^~[o=B(P(+ğ-^e7WEk̦R@J#j?sQ?,??UڥE]|Lh >(P(Fk?W&6k_u }ujvGSo:z( ( m:+~Z3/:(#(((,0ʰ ju>|%i.' ?O𮂊vhbOTogy#w8w?k*l<]yҽs+O ?O𮂊qhbDŽ9oP-qc1ZEUZl[_r-u6o\W~ȅ2j(BoC*[AU(Ҭ9_o? M\ρ 7|kG>UO((m3Fu-sλpOCz_ (#(((((#Y[ѿuqk;y#X= ?U=QEQ@q~;]cJ+_V8᳿,zfN?{q:I?ZkްIכ'ezU-Em?K^~PU?~U?~ QEeEEg'@rvKZ#zQY ݿLߩ?3+(([H']MrG_Y?*kEK(0 ( ( ( ( *?jUԿy\A44H]]r?uuᣧ0y( (8՟t#{_g1]+ė1?Z(?RiX1YfmU=׭4Il9+k'5(㪖}j3EqD="NO&t N];S |$؃+£UQmEWD VD @dEG+ZUuZ2,(؄܎ե%zIG Q_CҺ*uYy82@+ ( O k?c?+ *zMQ\FEPEPEPEPEP=WA7ɿV7.gUMroU ˟c/Х?Tu4QElyEP\G!g\޸BX?C+y_?ȥDD;KעWU:CQEQE`E[Ϫk^wI OȫyO ki?Y!\oubyҽo)9T_~?4}'kh?_'oO?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—s~h'Ym?KQ[Ǭ_RסnO_nO_Q@~#zQY ݿ֖ˠ$h8U'T|3QmiTM.jZ+KrǢ}TrǢ}TndE>=p3^Mus5NsMz sk,eBF+/'Ӯ fڻ\>WUKA~bxY#b*^^[]?uk#i$sWiV_]A1Gry?5X[.\4{Io(Š(mF;BO /^SEWQEQEQEQEQEOUMroU ˟Yw\Uo2AX)OMQ[xQEWY3뷮#g8W)hQ%~_k+_(/5ÕyQEzQ@>3Vם?~B*}SCZG@OoW&F<sG?G!QS# (Q@Iα\Z< 3][Wα\Z< 3][Ww~9 R]|0tm.4+vϖxAt EexB&&s,DZ,n4Ey)M`h "[i2$W ʧ!w?CQ^c?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—U!R /둭*%Ώu (^GQCF F? 7|k \]Dbs1` : 2dQEdwQ@SKXLKqZETg(iQE" ( ( ( ( (+:5_}]F 5УSmQ[xQEW?Ҹuc;&oG?lNW㯽aeƸ}-y_" [Ǭ_RVTGE[<*[<*EPEPEPEPEP-uBm#F{u5ێ"F%QEqQ@Q@Q@Q@Eu-&yɟLTPm;ڽ+ F d3TQX}^':Vr՟MbH$⻪r?RQjYՆNj9%ÿ?Gӭ}".~ritQW jb1U+۟V,4Y'mP1?zݮO k4Wc\0]Ilqu2iڤ,|$؂fT'v8*:qvi-tQE~tD VD @E=l9WRBv [[z@3SE6QUQE ( O k?c?+ *zMQ\FEPEPEPEPEP=WA7ɿV7.gUMroU ˟c/Х?Tu4QElyEP\G!g\޸BX?C+y_?ɺt\_YZ]:ǸjOWaEpf8VI7{#8ϼ ֪+%Qs܀һqV7ˎucZN)&Kn}ou㠥_x_sFf6O[ϧQtc1Dz=V״MjݮUhF@]X "Uyڜ9x4L/R(1OE+M?_&>Zق%8O &A;oQ &A;oWxRMgkx;.9`===z.urpU{*3շvAj #Mu7_G?]Mr;?u4=aEW)QEx>,o+H/:zoşo/JK~e*ʷ`[?<Q@*P*ʷ5Vc 3{W9?fU?fgXB_w;(<(?r']q;Y?ZƿݖC3 P?uzMyXPKzgڗQE ('wיM"'$n)Tڟ_y9?Tz&1iQɨ+AB A;_+DžDy!%|  F**Ho G$Z7%|>s>ܯ>{$Z7-vW} dyG?,O_m$SXZ7KF9+O*VXEۊӵ.'ؠWmrteR#*)O=6K_C~U?+yզzއ ~ٕ=s?Ω_zYڎsIܕ%t0UiUSkJ;<*Wq*P*ʷoʷ?睊E[?<Q@*P*6%79`O\_;@5oz o}Q[|UR~UGo}UJ(ߕo}Q[|UR}ޗ_Tq˳;w T@*T7$`[?<QTX!I=sQyVC*[?<Q@*P*ʷoʷ?睊E[?<Q@*P*ʷoʷ?睊E[?<Q@*P*ʷF;qݐ>Kd+]fK.d.ӲV |975!'%(;ƔU.]wm]wmG/x7o/x7oC`^G~(G~+O!0۸Rz#z_oQՕcjpTZR_{{ogZ%0lqFƕ|;rqػY; *U!uoE0UU(e*' ƭd$9uuxX}$k3N+Cqڥ?϶XʥB? ([|U+,f 'yUyVUU( ~UGo}UJ(ߕo}Q[|UR[|TyVT-VUU(oDXϋn2긽#F{u5ێ"F%[?<Q\F*ʷoʷ?睊E[?<Q@*P*ʷoʷ?睊E[?l3㙔|O\R'5mzYVUU(<[|TyVT-Vs8A0I#[R`ްUJ&y^WQ>I<·=V6A-;IЭXf,%oj\2T6<*Wԛ 8E[|TKR-Vq:׍ I$}[z=,AMy5va(ws ~ZtgexЩۡ d#'5ޢȊ"! I2$_1F0JQ3ȳ*5]쮟t~UGo}UJ+bߕo}Q[|UR[|Ux}c>( p+[D/evUGo}UJ+[|TyVT-VUU( ~UGo}UJ(ߕo}Q[|UR[|TyVTGѯJϒ3 UWA7ɿV7.BQEQ@q7s?λzk_-}C_/tj_u]|3zÏZ((((oV??.oV??c= uhŠ(-vX}dkgv[K_m~^^ik#?+,޷QEz (2I"q5SȩI%zo?[&o ?*_6Ns?OE]Z?,??UF肊(5 (<lֿf'va.:quRodiM2o '¯רX%6.?WZLiM9ע[ռsۣC)4񎦜pImϭrJ( ( O k?c?+ *zMQ\FEPEPEPEPEP=WA7ɿV7.gUMro\ !(]c\$Q6zJ3!W [~M4q>ߓ?S3_y\''m7 [~M4}beb{/,u'm7NM\E$Ɔ1ϯgV%#F֚Q]gx>o++g1^fq/D- )k,*ꟅTsǪ~R( ȷy_ UȾѪNJ[/*__ҿ߯xQ\ǰg*_ V [bwuo]s _EWQEW?܏B5W?܏B5ۀ+ftTQEqQ@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQES&Q'Sc'߿GWg\g>?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—cB.G_(WYoK(QEexEk̦R@Jȷ\My*A_ \TmO/<~=WG%{GTX״/GQRjQEyb٭5x'A? Fk?WW_#*OEWz!EP\_]q~6Ov_3|?QE`QEQEQEQEQE#}]erqo5ϩ`6uQElyEPEPEPEP!RyQEQEQEQEQEQEQEQEQEQEQEQEQEkNGV}]ecCgzO+ (<(;_z']q:I?Zs94LXʥ/kЏŠ 'yUJ'yUR((((([H']MrG_Y?*kEK(0 ( ( ( ( ( ( r?;/~Ok0?_𳬢+c ( ( ( (-RR*Q]Qx]WJZoNG9eە'ںNGqJ5(C<5z$6H;0A2y?F?_Z` Uel4VX(>(+?c?+[D/eu4QEqQ@Q@Q@Q@Q@_&U~:մ}qj Ms]zR=\-iNpvwEo@G Dߟʻ**>?xo g#T"?e]{ }O߂# Dߟʱ5;]ٷMzmq7s?βJ;0kNWZGGEWYq^8?k}YWK?AKH: Z 7zU*ꟅT(_-}C_/tj>5twrp>4\Vfwg<ףB\aCsziy>q~iy>q~}~߉?WvW ?:4wfR~H( (9y?w?,VYV;+̾(?݉rM+8B(|C"\VfEC?—A^_y|;#t7=Q^IQ@q~6Ov?-?uۗ|GAEWQEQEQEQEQEW' u#}X>گYQEQ@Q@Q@Q@PEPEPEPEPEPEPEPEPEPEPEPEPEP'm# ;o[Mu f??(Š(}-v5xX}$k3N=bp*XʥB? (*ܟAU*ܟATJ( ( ( ( (9m#F{u5i3}dЫw7/@+((((((+_Fr?koSβ(<((((/z'UJ/z'UJ+ռe[B24~\ctzUş1ar$-G癍\7-:Z7t&X4c22pPkV #xz^I<;fd  ~%(ylDF5>(+?c?+[D/eu4QEqQ@Q@Q@Q@Q@_&U[ UWA7ɿV7.BQEQ@q7s?λz쵍᳻-zfy\Ty\T¹/y/y /:M?A?5[OmqO*vH+ ;7򮚹"’!け]Xo?j|4uz?,??UOYAl4ak4c=*Y6?®XZc Y 0svz&l&lOj/s#(?7c(7c(^6k_u }u2wu+/o 0ݞv }u+a r%($(s?λJm:f#+(((((+Gsk}SmW,(Š(Q 3U$)kĐʈH +EeF>n.gc !I8.0+CJ׬l%`2c~=k{+-/a"C1ھfw]TNvqd;EWPEPEPEPEPEPEPEPEPEPEPEPEPEP'm# ;o[Mu f??(Š(}-v5K \9=1:iSݿ4LՍ+oZliĉf]VInO_nO_J( ( ( ( (96~y!eCt4QZ֬˙9+"((((((;/~OkNG)ߓƶ= W,(Š(((( rǢ}TrǢ}T y}]yNQ-7q=߂>Wi;B+Ӽ1r]M+,ٯ"nn[(2@ZbKx &ޘE.ߥQEyׅQ@r'w_GЅu5h1}Bۅ_Cz_ (#((((( ʫxA? &7evQRr:(<(#go\G!g\qgttQEly\W?bZqV7g~dO;AR2­UJ7z@(WwEWo]xEϢb7E]+/GQE{EPEP-]ozЅu5ۏ/>0+((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((7??ʟLDCwF>?:< 8᣻3zAEVQEE`A#bwB ^.eGQNO/VzmQ^QEʴ7?/je'Ȩ^ȿ׬-ED - E7cEi| #*FQZQEW_kkˏZA5ׄ^4u;?u5x#7?[_>_ޗ78Ţ+(((=fqcc٫b=fqcc٫?3СWƊ(<(+ܰa\O6wePL׵8*8*[sW%_?%_?9'I6fi )Y)[p m}ʏS3uhW"oj:+iWߞ[Z*7?Et<i :o*?/GQ\={" :xP*8Espi:墕Ĥ䄛變8N*>氨଎g~# ?k}yvGƵ-Yr8W~? 2FߓS]8K$OcJ\Z[G?AeuMc. (uI}X +(((((+Gsk}SmW,(Š(kjs1çTTjE[0zYucPsU$\wMd6.GAJ("IF9ܛKQE{F!RyQEQEQEQEQEQEQEQEQEQEQEQEQEkNGV}]ecCgzO+ (<(((rǚTrǚT(((((((((((((NG)ߓ+_F}OCU :(<(d14DAc \Ǎ*0kSI׭5|{FLoֹ)g4*IQEu[=*[=*UJh\=vJBfnQ&`&½?-#xvɜ gq  z`)|?V7^\7o?5h ( O k?c?+ *zMQ\FEPEPEPEPEP=WA7ɿV7.gUMroU ˟c/Х?Tu4QElyEP\G!g\޸BX?C+y_?袊¸՟vx>o+?%ȟv-ze[=SnoO€*QE>5XWwEWo]W_6?S+(([q!]Mr k_7|aEWQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@2o>7??ʆ8?}5vux#٫pGvg/䂊((/ G{coE`A#b\؟*^((+7?/jiVo^?*)z?OcQ?KBMX^['_@Zoz26FU (=(.?9jKרח_k '26h<w_*kGyAx# (L(3g1[GҰ|ot}+/oSqEWQEQEQEz*nGb>n7oRzg޻UxAMh\o ?\Ԝ˹岥5_B&-4kr?Z?߱4 I~r7oe33]S?5?m|?Aa??ch-fkr?Yڮu<ؤuǿt?߱5 4U);KW?ƦqloD/{ѝuy\Ty\Tܶ>j sBsB3_94g?4Bm?A?5O^ m#?Tm#?X(?}\[V?q]+c\qoXUw?^[F΢(c ( (9k~O]Mr7Mֺ|q 7 (((((((8w?kN/Ʒ= β(<((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( (9;o[MukhlOC1~EVǞQEQEQEnO_nO_ QP^Lmn'QL}iwaJJ)3OXiRyR3I0c$}}* ,'́|0OWI##I#v$=Iצpq'E.^Oe9u%k,RO|?jכ(ɧ*5UZqi?(@(((((((((NG)ߓ+_F}OCU :(<(gƳzd1)!d}+cѻi(%_#̯I۱3Nim,dY~yVڍq]\~uTosQE~tD VD @ 5'QVךj.lO-~IQsǿM''p7 "S5%<]XWQIyO *QuO;JeHuL:>VCk܍B{)_~b+q+T$6 pjnҒ!QɹuޟGE}QEW-uWS\#!]_U7h0 ( ( ( ( () Ms]:n*o\o+/)+c ( o+[Ś]qlְ4 }a)e?2gQ^AVT*[=S QEcEϢb7EVU>P]L;iW|Ǯ^I^\>H G%hGg}lO-JJqYöbLi#>?LV僟."Iobxv*XњQEVU]Y!h>(+?c?+[D/eu4QEqQ@Q@Q@Q@Q@u5gA,blߊ4oK[ [q,H֊tܤ>*s2~v9N/ E}7_ְs\ϔ_٣5[M+M_ FJ>Is5p1#( 9ɮꊩQ*x V~( ( ( 7zU*ꟅT(_-}C_/tj>5tw?:< 8᣻3zAEVQEE`A#bwBaņ!b\؟*^((+7?/jiVo^?*)z?OcQ?KBMX^['_@Zoz26FU (=(.?9jKרחp,o+W`]|3zÏ̎|toyθh[g S*vjEM:7<@:*7?En /lV/1\oBOzEt|toy΀!?ghcx?lFS2*m3Ѿ:0!?gh,o+y~Wmz_?ާ(#(((+U?fU?fgXB_w;(<(?r']q;Y?ZƿݖC3^<kZ<kZlqa\YW[\YWk?& _U6fi Фm}m}Q@*7?Elk- AKҟYQE{EPEP-a#tkZFߓS]?F^EWQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQErv:+տYXcS B(<(((*ܟAU*ܟA@(((((((((((((+_Fr?koSβ(<((Wa3l:q{UF7R[!7erƷZb5q}qV`Ko*JגVɧj`/b e*TQQE}n_On_OKW-OkC)V0A\ {{P~ezՌo3:չjW5^\ dcr:hs38VGmo.P=J>ՁR( s@WſcAW"5_Uo\s%xiO,(=((o-a#tkwz@+((((((((((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( (9;o[MukhlOC1~EVǞQEQEQEnO_nO_ QEQEQEQEQEQEQEQEQEQEQEQEQE/#]erv_9K5`~ gYEVǞQEx܏ڌsN+wKӚ!eS2zg&:n2WGTD r JX]JcD`qOyM8r:q_UYB1wM+NܿAU*ܿAU(GryRI0c$}{ (ʵjtcRV^fM&xdcb m(JB^<Ԥ ((kȑ!yUGVc*jIn-q\ψoE_S=zr8)MθazU'Ki RtӃXQE!EPEPEPEPEPEP'qW'qV46~?AEVǞQEQEQEnoOªU? EP$91ȇ?Cl$:E~(Ar[P(QEQExC?+[q!]MvF(0 ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((MOO&Q'PчOfθ}5vuhޥQEQEr>;Qe!XalXy?w?,W|QSK՞EWpQ@fEC?*ڇpYb?/G y|*'Wh_/+dQ?KBMXF_"_3ʯQEVQEG-Wq#Mu7_G?]Mr;?u4=aEW)QEx>,o+y~Vbvw?1h0 ( ( ( ( (93#d3#dOC0_AEVǞQExEuxEy_& i )Ym!Vk AVǩ[y68t=p5P?.HF~~7 IRsGEzQPV:(3((((((+_Fr?koSβ(<((((/z'UJ/z'UJ;Z7 2D>fݤس'5ƳB8:a y]W <%QG%PU2 /t+Y$R$g9Ne*O2_PK mcb]?V6QS.UNumoWEW}hQE WWu{CT>?hlpֺ(nS^a_# (((((((OM+OM+hlOC1$‚(<(((*ꟅTsǪ~R((((([q!]Mr k_7|aEWQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@2o>7??ʆ8?}5vux#٫pGvg/䂊((/ G{coE`A#b\؟*^((+7?/jiVo^?*)z?OcQ?KBMX^['_@Zoz26FU (=(.?9jKרח_k '26h<w_*kGyAx# (L(3g1[GҰ|ot}+/oSqEWQEQEQEQEQEɟ%uɟ%uz (<(K?,+K?,+3NOmqO*Vk\S ]R >J>ՁR(((((ZFߓS\'q(#((((((((((((ǐ V7??ʆ8?}5vux#٫pGvg/䂊((/ o^ݟlq:eIVںoy;/Ky؜/dk4wTYs>G':o5QQ,Oq=Ӈ|FOzZFU K,Dg'{\9ֿ/|]YMk%JF+kEt"Jλu&y?٥p~ZxY4JCƥ|9ǏjzN\>4QP 5QZNoTOuNwW|iZmw󮒹kO$}h΍E'֊BP~jG%گC|7EEa?}{X)ꚅ,>mL?Za@UTbdS$QEsQ@Q@Q@Q@Q@WY\WYX^˯𠢊+c ( sBsB/?4Bm?A?5O^ m#?Tm#?X(((((?n?u5X7Zk(޾(0 ( ( ( ( ( ( ( ( ( ( ( (-ym}T(((((((((((((NGV}]erv:+?S~*_QEQ@=B-2KFv?U1.xHt~5J%Q Z+]hQm$u1v&,?v`k&德6 ڱ0ֲ %R1vl襄V<ЍPCyCCSU}Q (U cSM'O{;QƢN.rz z1׵KL{*z,lTV߇Mpq?!`}kʣPQB_ l袊 -RR+\]> g ג#+K#w$ׯO2)VbUIQY%a>߂US帒Y(M|*&gA^ݽkq)̌1=$g5Ӯ+ L[*ڽKOM>T93{Ϋ(.\7J:fW,E\QEV̝(i6s"xak qM=1׬e4;y4K$ndޟ\ׅnʀGc++Gp9*CN!EV$Q@Q@Q@Q@Q@Q@#}WY\#}WYXcHQ[xQEQEQEU? VT(Q@Q@Q@Q@Q@=wB?!Bn?$oŠ(#(((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?ey;/Kazzz(nךn~#3=*rjѵ(tTk@rǚTrǚze(4Lv5,~f6iMJ'֛ݵ&)@l~U嚖q]NOʹ-ffFE9 |*=]O`i7Z\x~`;pQWkSP)QEQEQEQEQEQEQEQEQEq%dF'lj3kh^0޹ӯZO,ZOa:$Ϭץ*Ie)^NƏqk}Hm:lyuGlWe&xXT9C`+S+.l%#Z驓CRxa"T=S&yZa{N(.1۞x"F]7cȭ]#A2!2F {T2lCEdASZ(6-RR B##ޖEP REQEQEq~ {}E|dַ/Rx>\'jsY5nocmzȮu^xEǙ 0+((((z?ʯ o*@Q@Q@s~$%"ŭj" $ڠ=kkZ4-CRe,+Kc|kEUafΪ8*uTUk5 q4SO5iyEY)QX$L7Bq4}MgZhu%vW4qlzpR+e`zr yH݋3'$ֿuyB(m+t'J9E ED VD @PH$PE `*A ( (9iAxJ/$jĭo=ƢP}ɯ{cԥ~Es%2yq=BsyaBH j+syspV;QEdQEQEQEQEQEQErzo]erzo]ecCgz#QElyEPEPEPVT*[=S QEQEQEQEQEQҭ#Ws7&aET((((z?ʯ o*@Q@Q@xW_)?Ka A]ƩoO@7Z|B6U<HtO|g剱[2㼃Ri>ۡcmg.$ X>ƀ>w?]QE5cFv8U@mfEg{.M'qxcog4܁"u{rA r -Yzo|D93.эs ?u;xe/#&# !AuJtZs΃c+GQu{M+dF2}k>(xsSK_20 Ļ7??ʤ5p~T1ta#٫3~]c;?|$QElpQ@YVx[%B\CFуh(4`J)p} >\CFH+L%u>OC0_BQKh5E.QKh4R0} %[oAL?Ї:E.QKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4R0} ZAU*ۏ!WQKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4[kNlk5 f??J)p} ><(4`J)p} >\CFrǚU5jAA@(4ɤC$J$"B:p)OZ)R@;^Yj:M;#=Wg{X'˫q,[(^=zSJ\y`if}4'SQ(4`E E.QKh4R0} %>C@ E.QKh4R0} %>C@ E. Wv֥VT/dEvڎ'u sk&ϲ8>~uէ.wX,]B+&NP1,#_Y.!{ 5F.0<}hU уkSJ]c%ޔĥ@90} cҕ9uWVC_JjZ=*['U\C@*AHpf>fU.ky]Y"Q>u[ 2F+%J5߂]S帒XA|.6XԮĶҲy\Ezx7H0$\yk49,tK[yTI`{IUlS.Uԝ?oW4h4`\CF#0ZX1?Ʋ|F}1P >i<) gp.X.AW15s%vaA]F /_J)p} >Dуh(4`J)p} >\CFуh(4`·GQ{i<,H$cBуi(s)J)p} >QKh4R0} %>C@ VT*ST(>C@ E.QKh4R0} %>C@ E.QKh4R0} %>C@ E.QKh4%_ǯQEQEW1_^G ZI1UA]g-m٣p\Ex¿4<]6-4Q43R %0;~T$Pq،?uu9tRJʾZ08#s=?\>i77<># ¸/ y*"ִ{ۛoa8SI5M߅-K R$7s]|> ^ǗUњYm6'~zIV?ZEyғm[JiS8앾?Z>?Z)?֪-GTMru%7v4vF?Z>?",gieG[ַP@Bې`֮%y:rč~}sV?֪@?֪@?֪@?֪@?֪@?֪@?֪@?֪@?֪@?֪@?֪@?֪@.DqC֖_Okdj.ImcE83:iՏ%Euf4[AV?ZE9JRwǖR^E~}sT?֪@狮m&%p1WNuaF()G /,Ɣk R'wo~L>gk JGh͢?Z>?Z+ȵ֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֦yvD87z@ ֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~V֏~Vϙ ]ϽOT-ׯQEQEq?%S%"~j-NP[?3VSנM wIȲE"taF5ZWYg9Q*E۽MFk^'-[̞I ԑ?r1t )4?Txgm9ʨ'.6Y M&c+ OAu4 ¶fR3yb.s@/A0MSFnP𽜚wk0BcPx߄,b!`!*WD9{QEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?ey;/Kazzz(7%+ohҠ?}n bek-ήשjjj{9O̍ y΁ZQ~xh?|sDj'{lf{{yc'zֽbfqGGܱc#ֻ\=JT+_dU4 'yUJ'yWhTF`Y {P^sg z#ԬfVDG9X#]k7< qUI* ;l⹈.=rgf{ђR(C ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR!]ZMnTd'#X=^v9@yC]zɻyWԜ]%)'b 9&WH:vojENOi֚e&8`9պxG-ʲ79 (h(((((((((((((((*ꟅTsǪ~R((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEVt_Z5/x((()ɿIT1ta#٫3~]c;?|$QElpQ@YVx[%BJ>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((((((((((k ~XBڰ|A[` 0˜nX׋4= )W:'+,@Hce`wGm:&ly ?zW-K3q Focf(@`(((rǚTrǚT/iS*H"" >S[4Z{:U jJލCQF!T?zT&mk E{]1Sj.$Hb `?CZWNMCO1sCЏέW6ܛgxh4!;(p(((((((((umX[[&#,ghVMl#XO,ZI=C4-\PJ?=k^VM{*Š((( rǢ}TrǢ}T((((k_esgl6\ K˘ޗ j޻ CϻYjTEYnN"v&ڍ qG;Ԯkn'?sWK^2iF;56QEdQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEɿITd$t*0 Ofα ٟԾ_ (8(G,+N<-!Y;Qe!Zv_o\ ŗ+dQEny)?g\Ax=-?*tEyt?"TQEzx֪U?Ug@ GY%{j#Gg#WU\u]yG?O((kO$}kO$}'S,ގ:((((((((L+L+hS?PQEQ@Q@Q@[oA[oAQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@PEPEPEPEPEPEPEPEPEPEPEPEPEkM{ <S)(Z4gZjۢk%ϻqtC$ 뎌=k:u7cաvz(<(((rǚTrǚTo!76W,ldb$x<2R)WBU4O~ďcPx[L*<)ʙH8?@Mcav>2\9֒YhV$J(:Rm(TƜvI/(@(((((((((듖0R=?eW{0jPwINzjSHGCե%;>*)=U~V:" ]Rtto.#sՍ]Ppcƽg8QEjrQ@Q@Q@D VD @Q@Q@Q@Q@&>W_hfn@e=XiI`;0b:J+#ѧVSCle k:^mcoSz\]HԬ*Rh(3 ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?ey;/Kazzz(*Ohuecn!p:z5J#f4eE›e(O ( ( ( 'yUJ'yPJ( ( ( ( d fI]QVcO7W>dPk uiJ59.>##^k]OHv$F&{1XJ{Uèǚ'CEWrQ@Q@Q@Q@Q@Q@/{ϱ+`+XII@ـVy}~: 'e#aЏWM\υl$C%UYv&{WM]/Ȯ|crL( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( d$t*}2o q~Hj'߿GWgX_/Q[EP#_?ey;/Kazzz(?:< 8᣻3zAEVQEEiDžr_+3,+N<-!XCo:^(<$c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQյ$Ҵqz Tג<,LPʺ"7~ Z+峜MU_٧dcQ?kwjHvϨּ'(Ƞ~u5ߒ*U%7{lʦAV5 V5 QEQEQEQEKP-uE]4]ixMWy[XlX`M?Sjj+ZUTJ ( ((((((((((((/z'UJ/z'UJ((((((((((((((((((*ꟅTsǪ~R((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEV|kաTdA#ctTloʝG/7ctTloʝG/7ctTloʝG/7ctUD)MLy|>sO;'߿GWg\g>?:4wfR~H( (9yK$o[|R_?c_G*EAo]e X:sgi@9?^]2?;ϗ>_ ,~ŌUv7OV˷7_ƫy|v7OTua=߷&<j4C &1~0<X 7O\tn?}u|5'?ܩ~v7OF|4y|7ctTloʝG/7ctUZ ;ߏ[\ͽ v>fvoʍSh#ctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hLر?VYZó@67OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|``*8?65[ϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ*67ONϗ9o WJ3 {W ^[e%-W sJ)H˚<Zuqwfd,bǮx[?r//Zd4^BR}k R9:7Ш&v7OV0sUj˻ U`~lk,*67ONϗ*67O\-VB +U<u*_ƞY8/ s] QǚǕ,;=Q*餍]%܌R:NsNv7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF|4y|v7OF'[hXa!g=k_kMk'XμbB[Z9Ւ;Q*-ؓd:}Eiy|ֱ8S98MYctT>_<i7ctTloʝG/7ctTloʝG/7ctTloʝG/Xh8WctUnGal$j/7ctTloʝG/7ctTloʝG/7ctTloʝG/7ctTloʝG/QN= P9jO[4x$U sNdbv`+2}'M9O$KVhNd067O\ׄ_@|8t|5b(%HrIn?;ϗ>_āctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hctT>_<hSmPsW2-8'MQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ݍQ*w/|4ua:uW(f$U(((((((((((+:_ΗkhQEQEQEɿITd$t*0 Ofα ٟԾ_ (8(G,+N<-!Y;Qe!Zv_o\ ŗ+dQEny)?g\Ax=-?*tEyt?"TQEzx֪U?Ug@ GY%{j#Gg#WU\u]yG?O((kO$}kO$}'S,ގ:(((((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((((rǚTrǚT:M| Ǯ*ӳ34Z+b 1ġcE v^D&ϾtƛwKAETQ@Q@Q@Q@Q@Q@Q@Q@Q@7[pʥ~Y5L:@9)"}Ea/n|5A=@$_zB|ͣpyEFn+ &a,#_U4>6ʄYV5nA6gsEV QEQEQEn_On_OQEQEQEQE4NnPaQX)cw$Rޛ wwWpYO E~ĺ{ɴPyҽ\6+0HV-.:ټlvMGY:0eaA4jNU&-iI݅QPHQEQEQEQEQEQEQEQEQEQEQEU? VT(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQES&Q'Sc'߿GWg\g>?:4wfR~H( (9y;/KfxEiDžr_+_#ЭKՓE眧?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((((((((((+&-1\㯰7Еz:L+̃}>sk%⽵n1\aYS)5c΄=ߢ+Š(((kRkR((((((((((((((((((( rǢ}TrǢ}T((((#^yYUأֳ+(kP?.dV #K15YTc)'c˲lĕ\2gtuialL14tJ2' 9(c (((((((((((sǪ~RPJ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((MOO&Q'PчOfθ}5vuhޥQEQEr>;Qe!Zv_o\ / Ӳ V,G[Η'+s9Oǥto] ?{i]WC&'+ˡW&Z+,R?T?wM^:/U??G~uu| uU'?ܩ~EVQEZ'u5Z'v>fvE`QEQEQEQEQEQEQEQEQEQEQEQEm}m} QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[AU*ǐ @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@p#EEwUJYE_ĊO D[T?:4wfR~H( (9y;/KfxEiDžr_+_#ЭKՓE眧?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T(((((((((((((h~iQ@\((((*ܟAU*ܟA@(((((((((((((HܡU ?̚ɭKkR.WcחWg`莃NPv~3]}r>m`|뮮?Ϟ_(((( rǢ}TrǢ}T((((((((((((((((((UJ7z@(((((((((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((7??ʟLDCwF>?:< 8᣻3zAEVQEEiDžr_+3,+N<-!XCo:^(<$c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQETpZIs;b4>Ws;%&8<7+G eQyEsZF:ݢ3}_KZ{[<*[<+q(((((mciJ˒;9#rS\RMjcWUk[ ,èb+Q3T(+B(3((((((Gv}1X{gUi;&?Zů2ϱ[t~qs+𝳛"0$?Z4|ioϰQEQ@Q@Q@D VD @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[=SnoO€*QEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@2o>7??ʆ8?}5vux#٫pGvg/䂊((/ Ӳ VgYVx[%Be= tY=Q[yx=-?*tEs8KOʺ3A6?^]2Q^e?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= (ZG ?kZG ?k 7(#((((((((((((m#?Tm#?T(((((((((((((((((((ǐ V{]66 +IOf68Aݳzբz(S((((((QłHH`=+DR@`YVm # 5mϟ̡b$QElpQ@Q@Q@D VD @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[=SnoO€*QEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@2o>7??ʆ8?}5vux#٫pGvg/䂊((/ Ӳ VgYVx[%Be= tY=Q[yx=-?*tEs8KOʺ3A6?^]2Q^e?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= (ZG ?kZG ?k 7(#((((((((((((m#?Tm#?T(((((((((((((((((((ǐ Vv7Y{T1K$;$r |_v{GF:otucWIٟSNjAN;5p)QEQEQEQEQEQEQEQEQEq>&yuv%GXի<nOȬl,K vGE+S[dߏBֺ'9x9?ƻp>{5Ib](8(((/z'UJ/z'UJ((((((((((((((((((*ꟅTsǪ~R((((((((((( mׯP^QEQEQEQEQEQEG< y%#!>4^7:4o{BIIQ^DF}%u=zdG>njr7]S Ӿ/@`#I?z-Cwu W2 M} 25|}Fjtf1 7ހ8ejxcz`tY!2vɁ8G9DD,:]s95U}xQ#=8 xWum3M--n)'10u-i5tV3C5ZXa]ʵO,k>tw8Y4_OK^4I[r]nU T::Gl'iE<-RjڬֿƸ/^~~~뻴o+(/bf@@awI?}O-W}O-d@dG$zd$t*I?-9_ q>?:< 8᣻3zAEVQEEiDžr_+3,+jΟlrĿV,G[Η *$I?g?7LMpOVahs#tWEZhD3D/QV>'IP<U*f$ZTu_׻&'kvĮ5P#3xA_ ꫙ .tT_yk\OYǔSJUIyk+V>'I Z'v?d\L|k$ynoGiz5cyh$Z0+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+նΙI?9EX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhC*_hۈ3AI?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@ܟAL$Zf21@j;VXȅЌUϲIyhM&O{k*=GEO4h]UFI5WZ4e>-,X 9נUr{_v~TlN[ڷUԜW$I?&ϪN4^I?}O-"UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyh4f"-U~g=u&̏d1"''\#I1|hU֋Sw>VǕ7%.ݟ]Y>ŜOspK CW[d]PI?ԅLCp+V>'I孎cyh$ZEX$Z>'+V>'IRL*21P}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z*$I?^I?}O-W}O-d@dG$z7zL$ZHTdb(QV>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$ZEX$Z>'+V>'I UIyhcyh$Ze_[r%p=*QEQEQEQEQE 刜BTP|M6O 0OX&Z{QV Ֆ b<1JC=~xҼ\k~)㿹xa\F:uր;+H~eϕ1M,'G]Znj:wLe22e@9POR?*/ρmTln&F@sNXg@_H (*?\_w4 Nd}X\E~$XCim 68!@u@OI> A[K3+4h>f_Anv1[kp/ġxxF0Zx[T)ķb~X%Czzޏ~v@2``gGjʛ/Bđ5Ax?<(†;-*X.kyr [iYZF#"jsxO? T[x?w Cb"U? ҬֿƀΓ:OΓ:OΓ:OΓ棞i<>sO7??ʆ8?}5vux#٫pGvg/䂊((/ ֱAR!Y>;Qe!Zv_o\ ŗ+e:O! lV5?qsS+${lSss,brUn7BT*:rQEYQEQEQEn_On_OQEQEQEQE^/2@I OXY&EQp'lgb@+6u7M9xhGG"MF : 9ےJ.}k/Gp9jCN!EV$Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[=SnoO€*QEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@6E-TNNͬ02`6KtH|G>]5Ҳ=9I6s?}ڢ>*INʺF՛Q$!7}ںj+Oe/g/iϘ^sj\ˈ+kFaXp۽̱mEE.EV!x=-?*tEs8KOʺ3A6?^]2Q^e?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= (ZG ?kZG ?k 7(#((((((((((((m#?Tm#?T(((((((((((((((((((ǐ VfvE`QEQEQEQEQEQEQEQEQEQEQEQEm}m} QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[AU*ǐ @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@7]Z[8EPLgY5!vu skjm&ϲ0>~uէ.wX,]B)&NP1,#_Yz.4OGA+Rg ܡQEQEQEQE[=*[=*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<(($c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEQEQEQEUΪU΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm}ToC*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnO_nO_ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[=*[=*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEnoOªU? EPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((+:_ΗkhQEQEQEQEQE#}]ers?_V5ϩe (<(($c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEUӾ/?QV-?zi>/?QW fs~iѱُi>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW fs~i>/?QW668fPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKU@>`=Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@>/?QG٥*Cs}_~@I50iEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EfPKQiEP4EX6kuP9⧢(}_~KU(٥(4E_(}_~KU(٥(4E_(}_~KU(٥(4E_(}_~KU(٥) I^ l@QEQEQEKmWꅷ_(((((((((((ZѬֿƀEPEPEPEPEP\;@W'7c[]O(Š((Si]WC&'+zZFU D/ -?Tg<U(:/W KCF8Fu]_:A]Uk+8*~QEQ@r֟8I#]Mr֟8I#]OYu4QEqQ@Q@Q@Q@Q@Q@Q@Q@Q@m?zQz(((((((((((((nO΀E7OΏ1??:u1??:<S|QH7B -QEQEQEQEQEQEQEQEQEQEQEQEQEQEQH]A Z)bx~ty'@'G:o(u'hh(((((((((((((( o:obx~t)bx~ty'@'J# ((((((((((((((Bz)<S|QMO΀E7OΏ1??:uO((((((((((((()M:/tmQ@Q@Q@_UmW(((((((((((ֿƴk:_QEQEQEQEQEW'7u#}X>oS/:(<((zZFU qѿt:gl"} e(Bg<U*J* K꣬A5P#3_]_+WWUZ<ʟQE`zAEP\S\noGizMQ\FEPEPEPEPEPEPEPEPEPOGꞠTީ(((((((((((((}zjn$F|У"?3|Yʬ"K ZyL7Q _ ^ޟsOW~C7]^]%Ļ 0>^{WI^}\M+Bd.01mg?²Z*>*ʳt?3|OKkZ˩#5\2gpǶ@tQEQER1“+V|yK;TӡC:nX:n0ƀ=(((M՚\%3\ HJQ\'τ><:?/8^><>ky v/w?wkQSJԙMicӨC?O?O` ? ?Ȯx}I\g'τu}] `zކ2v)^RObQ]# (mwm<-ii=#I,ndB`PQEQEQEQH OK[]/4RNm ֱ`;+O~t-&k-3(RYyʊuipstΰ)v^$EQEQEQEQY>"6M)f-ڹo^EJN˧r xyV"% kWqjVpLϡq}Hǽz!2Aw(RW3HcxWœ*7hWNJtW Q VVkw2wQ2tc-ԆIVMrFx-CLl 1pqUo=OkgyIuJa욶8'Pd{z5ժ;p?v5frz?յD RXuk)Pv;pXswDQEIQ\5ۯ /TlKH Bw$Χ^хE2]f$QEQEQEQ^]FaX7v͘_^UޗΨ]E< xɠ +|+_]>-\RNK.Y"ĸvܒgb((((@( K=> y%]`?P/[Op,,?h Hnv0ѷYO QEAqҡ>W,.]kZT*U"%?a?L"7VQ/_~]Zj& ˏ#ʺ*MUu B@NsOL"7WElGN 1׮Ɠ.X-a?L"7W?q >jdwɨGpTS؃Z(2ifQEH((((((((++ڜ/u}Rci즸ddBq(V/uXl!Q;;=+h((4l@QEQEQEKmWꅷ_(((((((((((<@Ax]ֿƀOΏ%~u 7?:<4PJTWzP@y+=S_꟝CEMOΏ%~u 7?:&A ]מpWS\;@ozv?CWzG?:ɼOΡ&WzG?:wK;<:Һ-*t# x=-?*tEyt?"%~ty+=ShPA]1׷ZOΥ?Z@y+=S@hWCy8Vj KCF8F7꿾+=S?WUZ<ʟMOΏ%~u y+=S_꟝CEMOι+4ƒ.nz?pFp Oz;KOΡ OΡ&WzG?:_꟝JT(o%~ty+=Sh OΡ&WzG?:_꟝JT(o%~ty+=Sh OΡ/ۨH -?z+?h'@nqma( (;ox[M_2א LB0\ᚻ~:l;{ܑ4Fz:EzhV4.5:Pt}'"5d$عβ ǧ-OFú6dlSIT=+?zwE$+I q_k[xM|O<Qo slfonC| ֈ r7$R/IiwgkZuӀV B&uqsa)\u|qť׵zjӭ$1y"< J51hu}7YKQ4,$gqS[[IsuQھmҬuO. y#w 4'// {~6?=GZ#QpB:km uA&?wgy0G-}:g7Ҿey6:i}+_ٿGOc|s ݭɄy( ~|b jan2|a^[&Zw$~%Kk- Qgf`p Ϲ^.VV8U4;Ou-täqዛRmpl8IyN?^\Zgɥ?2s=0TQE/_UX#5poYW';;k&姶V@ ֺ?|mq t7L"vp #hMp 'Șb^>6?|mRChomh諢^>6?|mRChoa?z ǖvUAm Lg(IOnxw@s~4tKM%uq ֺO/<4,uGe+fQ^a_2Kӿ}5_2Kӿ}5Yڟ4]MWWi(.R"u3X~>-5䠭l4}w?FcrxŲHt*~?1Б9 +MJ.n;&@8V Zmepy}zaV +TxP*".PҬuO. y#w 4'// {~6?=GZ#QpB:km uA&?wgy0G-}:g7Ҿey6:i}+_ٿGOc/J%_G`ݍ߆j抺RChS2sosӽyg"a4RWUIF\F=vM4QF݂Ԓz %(nͳ2}+ג|[6oM٢`tV,>V+B">>J45=5BFA1$d Ҫ26kWvXݲzl{qwk$-&3סxo}m㋻fyD%("'I::h~$;7kkk9E hϢ'WU_/]Mg }oCsdef1+w^=.w?e-I q\|J|; m[r<#;P;Wx߅9GiiZHʸy"Y%,~jeV1o—GSk.IyݻG|i_G᫏QXZkP pg9Fs+knu˧K!L2>aV:G4W )Ɏ,ޙ}E=h_;(u9Ĥu~$ހ=z(WxB_fGm8T>{/U^>?|-QBh_ojg_4~^Qυ_?h_o 1Mr࿚?z/ek~W(UK4/7Q fA& 9ja_}iĉR4TE@F z,?6+<)B;i~Zt5 J.5iOGy=վ&/Dk{,p銟Bh/_4O>H@\ \gްI7egKxF A$3/◂5VV8Ut9>x5_5^<-:gyJ?gkm:ʅ䈊c{`v_]*oȍ襯>_J#x)h6|?I[?iϙ>lk_rjJ~-_x2WZb+dn+}X) 9}ZV6mrW> xSgkdBwFWW|8cs.KP,n\} :p=@+o[; 1O_Aُ:8^Ma^~FNZJk{jY+1rl6gwNztW֚mwPZGO D\ r@ko޿J/ZiYF0q{5Yt-5+;$똧Wq,+M$/f8uJt}k&R|=a#]_IޕOpV<6Z-ϝԩ?A€=/Kմnot/-,gӎڸ_! E9[5u8*l}UHkj~ |74iICp>`_I^}'/u2Ƭs5߅Rtj+JGie>v_o~U_H?G$ZOg_Hٖ>| ?G(I#{:;2}ߡ;;X p 0_i?³ukN.`F @<ִ!YUw*2aOu55Ou5XKԚ (33=wHGUllpo3gohkkk rI Ĝgzf[Uk/[S׮=EW-ZEE)nA`85~~,Ѵ gfC >յg'Xm"F2NOOzmIpVk=}XݾWK_3|O63%FayD$:{_-gR<]kLc2g*H AF urHĀ@I=q_|gr`Ė1G$}*א|UF''ci1`M0 OӴg_ ç,zsxW95@G+k2)~GǿNK`󇋼7|񭎡_e~ F}r^%־j49@z:giOxZ[`bN!>W|,|;%YR*JS\f6AMSh?:<4PJTWzP@y+=S_꟝CEY ! o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2(((((9w+No Ʒ= zQEQ@Q@?7LMpOW=?g\A_-QE[?ZVx֪PTu_׻&Ug@ Čq?:A]UruuVqT?P+ ( ?pF?pFp Oz;Kh0 ( ( ( ( ( ( ( ( (.?TO@y'$QWג~?N-%%s ;JtYdX '] a]c0l`|/cJh"Ӵ8m"?2OR}5㏃-PTp{r׿ׂUoWiz + owT&6K#F:}71-?k f˴؃s^sYMdNvm?\k Gƿ5?zYžz+(=>ӏj?4E`[`~5x++|S>!4R2-e>}AO~M#W.7 Q w}zlXvb$կc=|5򎹏^:cVmqx\[FcZi?:[h#% HM/Cdm5YzZDpldxs^urm-c}_'r7`sPM0둆ic{̫(6DxxռZHY;Wj| a`C]n18q^1Oƿd";vD$I;!lηk7:i<5i-%BW~q爯lq @ WеOGxTЯ-`"͈7_KPN}^9mXj=*G\GJ5{YDPg @'_ڇ|Ckh7S4mc}Eja{> jOv-PpΏpfοN.%/?Xѯ]$R~;ʶDr}vJ51kVWPC)+;VgK'p.5#$.?@?zEGX_prDP9# 9f/ aɦ?˘'f.EXyƑBI"z,WrEa33#kytNԊB7pJxvQyzgz|$ӵ-?}GKM嗀#Ў|O9Cԯoiu7n]B%R@Cs5@MWx/)y%}$6jcz3>$Jqx \-6cqX3¼/'4_꺦mټk| n%z7L ր7ggâJY?tpX ּZuly`)nE nq׭}mime("$ ŽFPj-fY~9䏨h7fu?JoSkJŚ='ˎ; CO4(@[G 4އFݕܣUAj0 RU0?Ѓ@-|nE6[|ڳ /a(9..!ET O+:(fS_R5L%@SQEV/tkʶ_ *—9/ xnZ%60޷sTu_( <4%(&&1V9@\z𮢊ϴ>Trh޹( G]EgTyϊ7cC=˰`F0OuF '-k[ÿ J#ed$&lEK|l]Ec_nc?}Q_0|TbH&9e%A;px6C:5LRP9 :{zᗊ[+X[tF8[ׇw×: ,*:>f^}}Γ[$)9~my2O—X<Cf71 2Hi>+'YC"$`0g5<U5hX@[q,Ѻ`g?=܆R!KU"|CF|KdWKvDz-\+sk;kKk(D6A U~-6Qk2f$}Fp}@Qo|75luW̿:[F@"a4RWUIF\F=rZoEI]W%r1bn>i7@'s_P| m:14X~3$'_S~t#SF|E}n?|~ZOpӴUP?]|oGIP.ih-P }C_1Nzw }9@cIm|RYi_3T?/_%7@Ziv3__mk Y[jׂ㶣^s[y?>S++↿|C:p0Y#z`ֽO d_J{@Sு;wWtu-TscrW_5VXUx|~#XLc ?t4w?z灼9[H4tài\<= _ov{--&Q++ƣk|2 nywn\ w g~_K^?`o?iWx3Do6E-x9MG%tۦ0w,J@s_5?~D^5HԲ²2J፤v ;@k$:=1MɞT5kgb6Y&>_%vhe 0{WƟ$z~j'We*o.=+Zo?Ha17ۢSyjL}B+?g/'wEE^@2/ďUu|xjZ?j=4o%zF GeMx!{Ѝ{ GeMx!{ЍtzWGz+{ GQZQ?[]~x>- {#(~W}{8K{HZW$8N5w#K*FϧJd=O7薗VԴr󑱹A<go9Ct(ohE¯}2lq^^-*n'Y"u2~ڧ5" ;UYP:?75'X[[*F6tC7;^݂_ZΞbKW\g >xRjZ兌:e~j8Jpc33[\7!w?O8y1:)?_PW WuFMg~K(}Q@ڶkZ$DkqpzVG 8-nR8]DW9omπ!a!ffK$kp$5~T: PsU $wgomK⚤dK}ǚf/Jc>KQ xy7#)*; ߪw4Оeګ4}Z]uK DQ#`{Ѓo>U6Wc¨4KWir@*.=~Umy@t[X^y% >pS z%?MSh6((( mׯP^QEQEQEQEQEQEQEQEQEQEQEVt_Z5/x(((((+Gqk}S˷eQ[xQEQEx=-?*tEs8KOʺ3A6?^]2Q^e?lǏjGY%{jQ ^|H?/W+_]_+q?gQO (= (ZG ?kZG ?k 7(#(((((((((OPZ?TUMGJӵamZ8C 'zECkkockV."@TQ@P/4|n&G<FF#U]CMլQOOj1\42%Aio1k#B@TP4^}u F5.m˴)E[W-D]ow ?!]_-xMbP $4m9?gǚjΙϔ 5Ǎ~=~-F:uR%I%C7^ݩzF"c~"Ɍ][3p8L +Zbi=vs:$O*+3[K1i^Ue\>z+N4 o:6^ S}ustb@vtPmt]JP,o!]պ#8*( zAƮt1}ݿzqW袀(iZU'm-3 '_(4, F8?` j*[5v ֒(ݝtqÒOC[RE/O髯5w_r:4ڶCmKjt^LRMw*'8޺+_y"ۯ /§<(iM |j$9#nby 9\㍴OEDLݧkJ(~|J(1% $Aipq8}7➇Ωi6hŽSvQ,@zcEy kT~g[yD~j>Pr__~5VѶR3BOI`IbF_ XӞo-G@z$և+/xRG[G]:r n =}>x{ C=[XĢd@x s_PQ@>G|n},GO[.5ර/tfn7:Nʃ:=E}GEq ~aYDm1RzOy?)u/شHhL81=t1|UzFT3 qї_=ϊ_ 59ë=ݤA2J;n !^nG H=X J/_ 2nu[(&y?SϠQEVvm[m|nqZ5Zo_P8V!/QXJw!c+/g9F?²?~|wmw5In߿oˍ~W3-O4OWbCu_}QnA (7g#jRKTa7c*H֊`hە5[h(+q:k-~Kw. jJ()Vڞnk6FcީOL*NFOBP7x>O'-sek[]闺 ]7R@' NAcyS8NJ?t߰ެ"SD]6=rOҼGw׺KylZ$E&_za׺Q@kº׋%ec}+H7ː:o/SȽXm%؎Tx# eultsnTtà'pq\ KtӵYMb%+iOԓW<{8Yu J@>ws 2xLм7e?c Ҿ3@Ϲ}%0Z ns }z4^&yCŷmt3HK) $b=imm=q^E]ϦhiiqO,@i~{Tǀmb׉49RG]xWJj]ZJyA<1-ᄙCώ_}AE|g/?zzk-'KԓR^=̦^o8!F (\|964:FѩRmp扆#OMӨg ^A( ? տ%]֪FHq U}Ql9Z|Aą¨5%ИMFZf``3b%윺QXI򸷻Bo G}Q^A( 謾P?OCΛ%rl $1~fcR;)aaIi%QPtM&]iy=껎ztk%=o>,/\KzeDRL:^EAyioYOgwo[:.ê[<|H<z+SDJЉDi%ƞk*([/j.,/!)!~U\8sqk'#ˇ÷?aokVxC`trG}3Ex?x-NgEǝۻg;OOJ ϧSG˸)S u@Jբ?ֳ$S bHR=؎3rs((.>W|)x10y\}kj8ѿ'RRP?念GSj([4OK ccQ kvmNUW dd1+c*F%4i(ţcQ-0^FAY+y}XOF:uv6GNMlDu99[m݅QR#~9+>1Bjg}{wy{~ ҽ–WoK k!75 28<ҵ/ⶃx[}W[2E' tR9_¾m;Z-ٻO/GH\#A*js^Hs^A?~%~OdHbOcƽ~!6?J} ʰ[J!$\>ּEy"kg}V=7 CҾ>x~*'BEnPޅY_VK6;A<6*eB}>PWq ZK:cn}St=ּJ/̴5m#$$B]ƿ= (;ķW0%nPZľp5e|1m!B[Cv+<zGX;Vq'1x~l4NF6$`.  $PKxuI4VH0^,v_N@ÿY$c aA_ ꫕/Vⳏ(rQXQEW-i#5-i#5ۄj^SEWQEQEQEQEQEQEQEQEQEvQz-?z(((((((((((((gi(((((((YjogZAum'ފxé qr|}$V'8[ |9xrQcǩkR(((((( y)ԔP~Jz~y)ԔP~Jz~y)ԔP~Jz~y)ԔP~Jz~P-QEQEQEQEQEQEQE<%kqhv7ޙwDd Ʃi Nvȧ 6]t((((((iJz~%Jz~%Jz~%Jz~%=Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@6_OSeT͢((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( ( ( 9w+g2YEVǞQEQEr8KOʺ3A6?\?7LMpOWC!|LEWYlǏj[?Z@Q ^Tu_׻&1~0 uUA_ \OYǔSB(H(+OkO|5?/C+(((((((((iShj_ʀ.QT'T}OEOrz/G_ʀ.QT'U1=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(ZVL POEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/G_ʀ.QT'T}OEOrz/S<@N(j*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\yY`Wd\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʏ迕\OE\P*_ʦVHU8 k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr=rz/@(k*>'Tr/t_迕#];)Rb ((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( ( ( 9w+g2YEVǞQEQEr8KOʺ3A6?\?7LMpOWC!|LEWYlǏj[?Z@Q ^Tu_׻&1~0 uUA_ \OYǔSB(H(+OkO|5?/C+((((((((((((*LjU*Lj@(((((((((((((((((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE/x֍gK4( ( ( ( ( 9w+g2YEVǞQEQEr8KOʺ3A6?T0؜ݜ"ma50nƩ+RZ4J^dQEw[?ZVx֪PTu_׻&Tkv66M21I3:r(g9_]_+F:-;6z֫%*-:8XSQEQEZ'u5Z'v>fvE`QEQEQEQEQEQEQEQEQEQEQEQEm}m} QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE[AU*ǐ @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@D VD @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[=SnoO€*QEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ΗkkFZeQ@Q@Q@Q@Q@rs?_WY\;@ozv?C+c ( ( ( (-?Tg<U(((((ZG ?kZG ?k 7(#((((((((((((m#?Tm#?T(((((((((((((((((((ǐ V2+"|mⁱ朜Lq).k|sC;gf'>Erٗw@tib h^ p׼E^.Y`8}j G1~vp,s8ª/O\g?wnhUN=7H? aj:ߋm<ݽ2~`1qoXV~VOjCHԃ*ׇy_g?2?v$U jtLh]l4P|yeÆR<~5㫝~æO #(Fvs#Ҁ:ZΗkkǿ0ϭEׯ陓J{/39%oem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:\;@bIjB{seV.VsVݯfoem?:*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀??E~tR~tlT-G@({-GFoQJ*oQѲ~tRkO$}e "כP7Hc,#k R0ԞZrIJUޣeȩE[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:V~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@({-GFoQ/z'UJaceoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*QV[z΍ޣ Uޣeoem?:E[m?:6[z΀*U? 6[zΥq *oQѲ~tR~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@({-GFoQJ*oQѲ~tR~tlT-G@_U58(FQEQEQEQEQER0 H#R@]K<#էKayLvO~4?`fcH.<cc9p WywHוFaNC`H揠̰ܸ81N(MKUӼ?`-aRy{ EQY7kr7ϮkKX4zZjv|ā7Tի[XlഷMA䝪dPixV [dw/fi,6k4.-&?L{ /sW7WTRN,̅F԰ӬE$kS}5+A\4J/*Iq6C&u. Y`8$N: ]ꖍk}m $\;^nJ'Svfrn' $#ae:!Pzb[~EVt_Z5/x(((((((((((R?T((((((((((((((((((xuRxtR((((((((((((((((((( oC*[AU(((((((((((((((((((rǚTrǚT(((((((((((((((((((ܿAU*ܿAU(((((((((((((((((((sǪ~RPJ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPYht_@ (((((((((((JR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( >J>J( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-ym}T((((((((((((((((((kRkR((((((((((((((((((( rǢ}TrǢ}T((((((((((((((((((UJ7z@(((((((((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@gK5Y2((((((((((( g<U*J((((((((((((((((((*LjU*Lj@(((((((((((((((((((>U!R ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 'yUJ'yPJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-RR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7zU*ꟅT((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQE"I+5%٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP i1 5+d|E͌O4_My> ֓!/57`W^@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢k,"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Eטh>0ֵG!tHJ 0^^a'Z+x40~4_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@WkZ-K@GmH( #0W/~> ִϊZs Ka٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~0ֵ:}rip#DFڼzy|hu=n lNܼ}(Ҿ?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~#XcF =J((((((((((((((((((()3ꈣ%@zIcwosh$$v)QuHgW#-PQU/5];O ^M23R]^Evq7g([fS?m![O-xmJn_ȇjOoN/ǤBMKJwADD sKi뺾&2 Y#'|¤vh0vבy9$ONq@EPEPEPEPEPEPEPEPEPEPEPER:Ɩd:<:Ϧ楎km"b ^ WޝG6k=M;%AxtCO=jmR@m4<ۨm<ɤɩDWF 2=km#nEI+nn:sckF ( ( ( ( ( ( ( ( ( ( ( ( (o7p}.#f]q@;hSY{_G4_w>pN$Ha*x8x5?<S/9 +M yqkZtz*]!'i}Gmi%NRgm H"|#k EV&c)smח2[Z)|E`7O$oR+FVm|u?F_l#w1;sUy솽xmv=&+>~,_R[BvD=眊_=\U]5%(?:%~5Z]zi@=2:~(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@G=6n&(WO'H=.[o%ZSx 1+ͫ%S9{d1 ޶MŎorYɲ9FS*?{<@DtCKVSA >[63zPEQEQEQEQEQEQEQEQEQEQEQEQEQEQEy>ilIՉPy3v[Ə϶1ϳڽ€+Zj6WAgyopb;deW}ל|)]/5t~ت_L-DW7owMZc5;t*K}Jrɂ aӟZ-oh/"R׷ w;kBy @JztdG>*xb}e.#R =)ji Qnm.57f7&NN8cyV}W@o$J[i$b(lP3@tm9d ]''>~R^!x[%ӆ #@:I $lU:VĽRowLiq*ePHyw2d|?_5Tp-K;$Ǵ׀hF((((((((((z֓]Q^>]p_4ۛRBO溎zf$U er&`ۻ3x^+ A^Җ[Xbrz?xx*8 \aou|!Nc~I^}/\x<1m$2Rx}N0Ql|e;5Z+=N}[?=#ct[l|xPXKNa@3m4?uY."T43_&AG6F j~nYSvN9ltx&Z_gPPHtC+>&F/@1Ð?0+{2EWp^y$c8x KN|a]RH^Lٲ;ƥ ό;mg1yDTIxR񧋴]G\m/q<|=2@J0 _ 44i`FvܙXu#81>yYg< 9!@uvw/W Dٿ((((((((((((^Q4n.s "Y=ی׫W?(e4/tx{SK,dً>4?Wv~{$o^1/OD s\;i֐>d֠;?}|q]7Ëm E&H3d@0ou$l2 nzb=2χk^6I !QK'!67MXJ2zx{ ?e~/-nxZaFЙmx$\p( G^!Zg$F!뷫c\ouWIZ4Rʄ_Ğ1g2XiA8k!O@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_Vo7DxO+ԭ>ߥY,nR?yxrÚ ثJ08n(+?jҬd=F`ˆÖn>7sdd= 'dh~ jCw96,$<{SUYG!;$qrA5ᯈ0Kua6+yJsLVG]~&Xz!ȥIIc`ѺR;ȧ{ oi֡y,yb ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( }vk}m $\}x'zUQ$rϴ+CJt)S4{6\''U(̺<a;dRҬuKk{k2"Lm#jڢ35i:Mm<$ qNt- PGu999ݜZ4P9m/ ] txxPMcEлX x(aR5 =( MW?i7y(Th:V&c oP|'Z4P~jvq H*} ϟ-t[;VGAnRӣuCe${#?Os.P}͐Ż6~9:(=W֮ )df#?nQ@Nq4zeVxO& M5+MF̻9M6s5E#(e* vf֚e9o { Т ( ( ( ( ( ( ( ( ( ( ( ( :uѭoۜ~E2hR !wG"adT}Nlo@\S{7@=hQ@di^Kˋ:5%rHZPni=Ņ-뇸o1{dN9ckJ(š%Ž~J> n.vtiZ^uYwwD23N9J([zN=-&:#=Zբ ( ( ( ( ( ( ( ( ( ( ( ( ( ( hfRWP1 [$M"H?&b+(b8aQS OvΫmk4c ': ӢȸƏsGKhN6̲:p9^wVe<<pr8QZtPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPYZφyږ)pb,*Aǵj@W~/tX{@m6ooV.ȢA.I2}J(c5B[yk$d}G5NzU6.0`]nr?ZPM3L,!mb[$I'5=ż7vFC*tn%KD/,l6t90GRsIѴ zmq}rzM^ ʏzL:qmdyEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQExsG0u->a0ZԢ9/xcOMo)2EEx[D\Ii 9WǦ t"ͦ0\eM'EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEkalign-3.5.1/doc/images/Homfam_scores.jpeg000066400000000000000000005126101515023132300204330ustar00rootroot00000000000000JFIF,,C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222M" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (*V:Uomq*ƀ,@M?WN}m{om2ȹFTϏKn ?䙟-z2Yci8Rp$z }dE k'm@i u4cOx.))PzV|5o^h#_FPEPEPEPEPYztmMoM8"b1jԯh(_ F@@w?Q ߃kc^\f$܌J_f# 6>+v~]ܾJoy8PI]K.ĖRA,/ eU$P٫Cz{ExWBᦐQВ{kf i?iFiCO\mG(5y-T4? >f?=5 VOg Zy~ ?2>?kx~H@B[NI4xF -Hᾠq@񅏍5d6$< rauR{^945u <ݴ>u=+;$[)AV?i?:Ҹ׾#|Py<"qS(t$nU/W-u g3pkT2g,`|#۠H@9?y?)rԊ)V|@}<~ pW$d'\|S[Z˪7pIF`Wu> `e@%L71rWր=>&Ν@&s2#?h(_ F^cI&9ƿh(_ F@O u|޿#x{t}xşZ3!Q=;ƾ̳;IH zn߁4#gW_|oxvzLRk[q*iUROmإ.ూʡ ־{E-:=Mg?Q@>7k6ͪ˭B7mx`eBF͉_ |v|?mi$]J$+jddYu珦kmj5hbpYP?ܛß`ȍ3JjGV"%K\N? }|7Oᧇ|7v,gW>f?NIj75;7ڢ>Oι^xÉڧ:7snNLO^\sឭiך&E6W?μpx|aiLP>~9x~A~.Vc0#vҘqy'P]?C)9=\6 (:VO1^r^}& -X#-3ʼn4kvIJnN5~d~5e)<@$p{wV?x_j؉Rg&B_(ŏ[x:8֮Sz9H9$<Z姚7O{r÷~ D5n m;VB zeWs_NPx<;y .LK<8t!8!?g d|B[|A{Tt.ϡKce[2 "X̌0_lDdq@>|6񮵢7^]k蘨+ 1]N்p۶ $.~kkҠ0 7WW|RÞ!a-q (i#g9c+kskZ WO Oj"Mꈀ*N$V~h_|{] Bka-RX266o8$?-~.?DoEssm+$c28D`rHMuy#H(A>*ƒ. A|`w,l4/ i-vHɾlɻ!A$Ld?_ mrėw6ek{b dWU6XoIx!fP?75/&V(}#ٵr{q K&5+)S Wbyz¥ |?СRfKd.7;c^;Wo~/fkJdVN>,>3x5xu;Ih[_eW_UT}~,|-ZVL)q]yO{u/x~,Zz%8dJ ڼ'?xQh#MtiY(0vN}:f=Z7aZH'%F_? u ZѴȵak3l6H*X|W]WIG^=ٯ~+ڡOg` Z~&|J6! !#,{~QgzXݬr 6Nnk6FҭtVш@:?s^#LǗ=$^ d45&_KA߇مK:\dB|S1gEb-l`i"ڽnA-cTQAh-5 " WRmJW-FTzТ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((Kn ?䙟-hlo_uiܜ(&GmX\}Vah.OxOڵ/đI?xaK)YBu4WB5k-4MP/%,`֋koIoFx n5_| <.E&n$p(55m*Q`]BFGu`tjzNw0X-ai'UNo$M[Dy#bbK`rASѮxǿ]I >FTVǙ+`q^me񆷪#$5{ sTmh|ukٛͻz3O;t gZҴN$6.̘8?{. 7bEy'"΋_ -95eaQEygjׇ6Hٝ-`iJHPp(y$ @пMS^o[Og\$a IU  W)K5mX]^}6i6NhCMٿs׍~џQ,%ޛE5|3Q2Sד{ֽxK5+M *Hqgq@E u58E ủhq\jA۴D0#mW$d+j~OD{6Y$xcrccp(tςӑwO{ S":/k &5[s!u<Ot|.Ag~YuhQ~"xv6)ڄKZdd p1g/8o^sCK6@,nK.@^gúݗ}_Qѵ {&)s-l\19@@͖_u_膯?kK664ߵk63Zl_%}'_?xúv6.vLQڻDvn%<@@E?±k঩KH-7JX1#*Zρm>uԑ;XZV J2Bq9VQ:mfkV@DAě}=_2X̾+>hDG|_GBPc)Əxm DkYg9Yz8~_ImaTki刁;r8,H{JXȚ懨mܶ}ѷ:OZe~ẁ&FI`gwitCr&}${Wh-_GgQ'V(^0z㟠9dM[?m1!y dzU?KUM! 4crW+־"x⍩дM സf\z<Wק ɶ]J鄷r'LyI=i?:]@oz@u+I$Cklɏ3WhȌѫ+ B($_qԮxm7eRWznk#$0F]xQ:Gm; )"mS <2/?iKk#YﮡRxY~1."?Զ m(_:cy[O>/WN<_6y\r~_}'v'R ) .2NHxQM_GGwI:&7(hqߚ]30uIeѴh">\@sNz YI7v䑴领P1@UQ5?Wֿ^^A?hڦZ-WMKdK"`"~>n__>9%տ߃G%pmEj:>WrZm!x?}S.]+ fGlJ@$3@[t?xVG-6Iqesgp.ecMO ߈7[Kyn.%e(PrMp_E^Ey#SѼiv3=u| ^Xº0K%ʈbPMx[)j?꺷|[8HCcWROn~:WVno8qO֮_~ n%n#wI!ȣLA :?4.@IZ4ϲ}IO>߆~ħH[ؐIc{p'YV>ׅRihti?k7ž婵Yi1C7 s8zO$Z%yWO.s]Okm๴TI[ ˷X¨XbѯtmB8CAjV',4 x71^:_CMt.$AknLgh8@'xy>&m ^9n` 졁~Q>Gn/˿oOA;g0؞⾍2[OFh9cHe`AEyxEZҭmZmhR<$$9ݽֺ^wXo<(gFN$$,1z%QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERy0 '_/x(_Q 2}_%Li ~/AGe**(3dju$VǥEoi_ oe(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK M'nc80Vz2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%LI <ګTր%D /AKuh_Q 2}_%Li ~/AGe**(_Q 2}_%LI ;0fZLi ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2adcj_?65Jyte(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_i9 n@㊭SE_Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_T<ZF 7AI }i(_Q 2}_%L]BLjORf֔_@(((((((((((((((((((((((((((((((((:_SZeQEQEQEQEKoi_ m]( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ǫP=[(((((((((((((((((((((^Kohn~ Muh((((((j!(((((((kGTkGT((((((((u}kd&T@z“ijʌ\V +Sg8xܨ#5Οm~FnP[^[&yAz(5fQE1Q@Q@M~U/P4QEQEQEQEQEQEQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@(((((((((((((((((((((((((((((((((:_SZeQEQEQEQEKoi_ m]( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ǫP=[(((((((((((((((((((((^Kohn~ Muh((((((j!(((((((kGTkGT(((((((lnǩ?:M\qQ[/\= Od\t4I]۫14M+3#nw9'ޙ^mJl&xYoՅQYd-XddqЩD֗Rʗ r:0ԶZEQE(*hK M~!((((((((((((((V}_jϫY}i)_֒ ( ( ҋR+6TuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@$̮B62{S|?ߕ:I\J;c'7͓I~Ty}:((괍nG;E8jS<zrV4G,+ݝͿ4)'z.8tV,d5Wz\ʑ$zxd{3|?ߕToʠO QloκӾegyRq*8E(*r@'cR sLD^Toʏ*O7G'͓I~Ty}:)vc_$ss+}\削jzrWG;˘c[6(]:?J8\UWp,8tnTҝ)QE*ǑU*Ǒ@(((((((()ERTWSXdSiTe2C?xL[n??iQUnw#*cҲ]wym'?\pV}uVU (;yKǠ/+1>߅W\ײ>_:.Y?cnyKKzF{]ץ׏C{jԍ9C:z/RCSE_( ( ( ( uE-wg3ZUxk ~G:kO:~Wil3[YRj:(8ĒVA[˖cIj¢qwFU´&O;u@r6bkԋ (EPEPEPEPEPVUj՟W(Ro%QEQEtVmiE?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3Ze>_PEPEPEPEP֝uZu€ (((((((((((((((((((((z L?տޠh((((((((((((((((((((*[QTրwP_ ( ( ( ( ( ( h( ( I5ɳ Gouws}iyXrMrf]Og(é՗MD+ (ODN8ywݎX.WfoXqJ՗4(v0]ui| y}κyF]kGS0AE\_?CʔQEQEQEQEQEQEQEQEQP][G\L'(J RQWnȚDMI=3qK[ 4gk_cS~Vqh/ތO_3Ad|x~ ,֬;~XY#;v6ɮa)֢#؜cRtݚfHu=*O^T4}BQ&mYcWF*kRoaRA3ќPgZJ((+J/IҋR(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPSoǫ@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET֢m׭-¡~ QEQEQEQEQEQE5oMCS[t4QEQESՔv_)^y^eX`W`Q*;3ɪ+J]QErQEU Qj%ev֫Bm⥺ (:֞)* ﴍ^ SMm,>x{Zɧ%D/q[R{~G0'eI95,ROD{4i^+.mtkM%/(@=,4QJo6`y{chOZޟ}.wc=T+h#*cUմ;=Bת25xo|&Inkc@,ho4#CWMe}`Vm:' =(ޫX:klVi d; k !Už 7vΞV< z"^!Io.^V]O#PG"K SGևteTQp5םSx[ΒM235?+K⦛ž?$Zu g7sбVoh K^ ~OM495WL%fܣRp@һ? _͎ %z@QEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPSoǫ@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET֢m׭-¡~ QEQEQEQEQEQE5oMCS[t4QEQEhRK3Cҵ詔T*58n3'FD(pTLDm/#~5/c'0ݲGLW[j}ڌ4g+Eu1AA#c [h~[8? K>5æ]3+ ( (;JeaZz,D3~^?$($M) +)}Ta',vY[HN/XW#~O=:Trߘʼn,I'4F*áEdw[xق]<^ǿ]ts A敻E8WU>Y>aPui+5p kEBJ(kc 2$NIHɩ)NS}?Q(>* qqS{skoByF(? ( (,y%G$oEzW C^L^1\Jq\2rQZ*)=ɭtW3u/u b/uTV1spQE{SE_jhK Q@Q@Q@bxHpfmJ۬BadG&"~GcRqQEytd؟Eo𴚷O׊ҍՎLrϛQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@((((((((((((((((((((((((( {A5|"-Oy?\w/K@~  ҿ5?UNnﴫ=SOF{2_4*;ટiOÇG;6~s@ݟs||ׯJᖏyma(uMa$1[oSW\ @BԎwKO7_һm_W~6~ p(Y|% ^T qH<_3Sety*FjFF=M?MתxWo":6ޟa@|3pA9ݿ>F>3Xf&YccA)Ὗ\7IףÿZ|A}"V;7%#V8z>qlA^e,呝v94xrXޟzKe*ǪNͯk"XwڵI8l0i?{4 8ScPڤͯj&nmfHqL0Fx?=2ҹjTpɮ + ( r9Eu8e 6TQNOx䵒%|Ak5 cgOo^yq"u@_hŸ M7[2LhqGE+)V*=+pBVKN8+n=̹w2:~J՟:b0E%z&ᨵu{P\DWeםPFzfx `85(Yɨ_EkޑAZM0?{/$$t`FxSͤzU6V[ ͯj~Jȷ_oћ_oJ(o6ߑ6ߑP~F ֨(\~F~FQ@k3kE[ͯhͯjo6ߑ! @(hiT''e`^әUԫz2 RmNflϰ9auYQmUG mk5(#gs^iUb*4uY}9~λfQ@sZv%"ڡ\k$i,m0 U4 zu * U(wzN"y\iW#"Q@ 45$TEUQHX9]aAJuNHl$2Y֖z0W( (3Ze>_PEPEPEPEP֝uZu€ (((((((((((((((((((((z L?տޠh((((((((((((((((((((*[QTրwP_ (fzF!Ci6cڡSc aaϧ\9ݤY$mysϳЍ j ((<32J,&ls=uuQH!) 6zH+ ;WQT(<j!(((((e XPih t-6c@%6?鑜`'֝Y+s(@/yuRyuf VT(֦}/Q~*9:` q\.IYAkf?#z{wGbceC.(C ( w;w݇iL0f+;uڋE<AӦ'QE|FH;}8oY/K|n@jif5&iGUNk)W9?%`zMmU+bBM/z0iZIH5 0FzH͹Q9+zHqS<$|Q"uUK^Cyn֗[FzO}n Kw3`爢M]{-A^]]o dxUmm5m$xULtuDXw>W6*z0SK{}EW}QEQET*/RCEPEPEPEPEPEPEPEPEPEPEPEPET8dq |#QbD%ǭT{.v^ꕥ'a2thCjP0"`:<9Ũ;A(\^OWT6*C+Ɣ+J,Nh+>禿o%Ags8q1rGusQLAEPZQOEf֔_@(((((((((((((((((((((((((((((((((:_SZeQEQEQEQEKoi_ m]( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ǫP=[(((((((((((((((((((((^Kohn~ Muh#E?_/ Mwt*>Z4%RTFJ ( ( ZyRFI8"C@*ճΤ#2j(O h( ( ( ( ( ( ( ~J~J( lr(ta2 :; Za'XnYwkr=w7WcEbA;`S GcnŷϭI%ͼGOFp)cc`kHPWHʶ*oIEU\ufÑuҺp3^us;\K3uv-^]AT-цQEU?PNrfREM8a4= 3qjJK !'>[?|2f dp|z^Σc˜ydg_:nav_ƳZRIa.OLR#1NTs#+˦3yMTNknԁV]k|3=kR*p-MU%Ir%dQEfEPEPSE_jhK Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Et&Ě-1gs(}KkSGJȯ*QqvgѫS(4}*mZaGi$۲*s"':4K`pOXҤX% ڣ Zb>.J}aES3 ( ҋR+6TuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2/x((((([N]Sm׭:w@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET=[ꆦP4QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE-[@ ujk߅C@Q@g4kvn_rq-XusC-`\~ۋFi-M^̿Q\5Y~aERbQGJ+B/×l\<3~'deZ:1曰 i]ȿ&WeLX@iP^";۠QE5oMCS[t4QEQEQEQEQEQEQEmm QEQEAww M3aGAܟA\wxU1vDVk$ƺm*H(#TAzY”i;!VI\}IV`QEQEtVmiE?( ( ( (2/W.5 JL8r2289?0U9-|GH"-ly x8>? ڷ/@:^ngzdM^~!.'αx"eF b'PRx#\#^*q(9* XGY'"fxAԥXkzmԄ$H䟠4EPEfI= ˭iygkgnsW. 繚8aAF =@Y!uYLZvH:HG֕QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPSoǫ@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET֢m׭-¡~ QEQEVӬgyٿ1Ƶ(pV ѓ_6VN`ySI-'y;QL(j!(((_6t>d:ҹ^H[B7+9Ռ7:*ƊĚLƿVƟ∦q~QD$mW+S\ֿ@ h8(*ǑU*Ǒ@(* L6OP^fuh*nequs( ( UbN9@4="7#W<L5ݤȱC)U oVk-3R,5}=t%1U |Ulqtwֹ\%WRzp+)-#+B]H8ЌҸX??]-RKqh9M5 B=C#YD[^iu'#:?ḵ}]ԩŖ-NƊ(((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEQEgu;=Q'ZA$ , <\>/^G &\La=컇GNƽSvP^CO l{΍,S*TĖnǮ|1φ>i:eTifO3}Q? As ]'C~ZW_KG]E|;,S~r,&:&@\-ӿ5'4 ׵5ybf@ΠQfq|jo[!rLBg*}Ofvoyq@mo^Zž0Ԯ1L:!PtNzc~>@ _A6Des#F~y_'|3y5+{Ǖq>[f1xM/2%nG ь~# yt ~'u^\I[#<\ 8$~ ٧|q;s>?+dq⟎|u\D,`l5ؒ*&?/%xŶ{;EǙnQF8O@~?,5{${s+?)'=^?aijS`5㻺Aɹ$ץϿL~1Fx˿X 4yo#kKL`F:1ko[⶛^$/6  ,O?t2rp@Ɵ"?)?g'EE@/ŏ ÿ K@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4ʚH21pI2ptTL4y2ptTL4y2ptTL4y2ptTL4y2pZuˆ"uIR:7irHVdɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#z&OFfesWdɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#dɓ#m׭'' >fRT@ uj=GSnVݒ?T^.>G.qZ\P%uoFhI [*yTL5Fm4c+g1WM^y֌LGd2vTuoc? ZHG`+委z=+\,Ti]_ymĉ-̒F\e˛"]9 ކ#s&&_e~"z(j{U&Q5FCV{;ckVG*tz6KY)! Ȯ>&Y)O\_q2;W ZQ3pxʘJp~*lR~'+VZ!?`Kjk£UVijరzܖ?^~<.w_kV|3gOqcA{ڮZ_ G#.BK{e8msY5#%szM^p᬴Ba?u~:=:`՛֮%GOd)QPש1{Ry2p[tTL4y2ptTL4y2pu4_ o' In#9#EIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GEIG' GVP2pՋTdݹH(T zy2ptTL4y2ptTL4y2putU&O"5EPEPEPEPmvI}EQE࿳w}xo5{4|OGE%&00z(,^tdH1LH s<\|+>y#V ˞UyN03"F ri&fed kt?]P3OEῇIIuiA.>ٯ#oQ'`z"@  ~?FN%6Vbc0 3o |S/Nӭn`emۀ==?LQO=BqΛ(#O]9sqR>ߓT* 4jdQpF:d$Wcwwaiyr3F  >>g'O.:kմ\$s[:(濅_,~~EݤͽbUaAg95xuǏt-G@>{v5X,gDzj:^$#geiۋ{+Xm^E<[|n]dQEs5rmxwS,c-OҸ+HT69qJX{=AfgP!*FA -e2ٶs+u,{tg5BMثog>֖!['HQYV\g`i*xx_ (:Š(#r+u9uNkn7=CלWa9 iN?+ +JǓR?|~u,,O{f[cPujnM;0·'\}ºjV4gkeݝǘoΏ1ߝy^\\LԹկ1IF;k%]I\O1ߝc}:4G;,W9H>WC]f. rV?41֭#(u*#G-FR3]4Vɦkv?#{z̯.QqvgөSхR` bpHO[O9A&>4"AhA:+b#ZwoΏ1ߝ6yұ995Nytܚ|B$?hg9ָv˻f9$jޫpnKIBכVB d94keҚlJ vE(?CH]1?SIE;'w`)yHR@M:{V<A]χ<0tM>DsYUjsJ+$=L@+N 0 A޹MW2##zLY~kI, pT&sSX[lFR*K{+ rYǼЄzl~ `7>sɬW}[ۇ33_PEPEPEPEP֝uZu€ (((((((((((((((((((((z L?տޠh((((((((((((((((((xr\?;GW2\<6c{ <]pw ~3\.^Xu ^ +((( |~lW]73yY٘^ ŭ\g+UxA#;)Y֏,ӀaM>ࢊ+3(+&K _=x~K"ƃ,(OӐY5'y81UpF-ܷ/<͖n݀V-Vv**`((+ΪeOJ*[Y)׬l%sU8=zGJ@C(#V`M ްU="GС*8@xydQEys>r@)K$2 "vG^S(6j3ѼE^$<,C]EeQv832OtT+HT`n=zrOe7ceOSuG_/4Sc=0=㨯ͣkjq}Z[ݦ%Y|GVǑ^[ A('dqЩBSQ{i(q?¸1X R\p cCUΡ_,CY5_9V˖H=ERf+3bE_x) Wb5V[9zlJԚk~W3RwjOhSʧJ#oʽ3⾫_ёRloʧʼn̼AT'e8D!Mdf炣#Tyk`>@ =W_^.*gU?BptzQ\Q@Q@Q@Q@Q@"EmYUǒI$ޯnϬWVnRw>B4۽XV -'r9ڄVATBn.ڽփOU/UhX1ueUf/Rzŵgb(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@`X٭,ǝzޯ<Ջ^_5,VC0UWP/ʷ=~K$kmK J}8Te{# J7=*Yªի>W|Y}i)_֒ ( ( ҋR+6TuQ@Q@Q@PzoyU( ~bxQ'T-GR'bxUJ(`ij(>ԴQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE/x)2 ( ( ( (%^߅6zӮ~Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@L?տިjaCEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPUou]>='XG)ٙH #| .%˻u&[E`r2zG"'suWϦN4QE ( )H*H`AA( i0 ;>lW;{2NC>J+ MyoF=7ѺF k$tH{bxoC1EokZ Qqe(i;%Q^qEPի/a?Z)ĸ&Ԟ(EQ@S(%C %7 }LYYyYG9f9'a>g\moݸzz<~Lris1QEyQEM'Z˯["WEWܞTO%4G tRi5f%@SҟGJhb>Jñ uS`5Dd #&^*))k'c<{|ѡN+wy&ˀ=)|08cTZQT 39UwhH' *XdAUSw,JGsZ)ĩiZvKp)U\tVI-*1\ӶTGq]6ST:[,Pnr{ִ>f}HeNӡ^Ocվ4o҄"*hK M~ϙ!((((((((((((w_亐ڮWEEDবͰB|<\m݇5hZ :]ݦg>Һ+(avJp­Yªի>WA֒}i(((('kJ/IQEQEQEPz(= R(dED;`P4gr`F'O`\]QE1 RP}h((/7Լd`>}w/<<EPE^ 7Nb<Ұ!TN^XmgIex|q"kW ȸTZ~OɌY5V<&'*t]ڵxl9ǵAA$h9WrZP: ni)p;Oj\q(l%ҏу+ W1J6Qa;z{Tmm QH;zYG$I=tUSΞ!E{EPZ:jD2pk: X{ jg(ǚixʕ >D`deF~#jzT]Bb0z\τݍgd+Zvq3XJ<}3n4-B'H%Oγtz iQH)22)=S|zџ^ϡ͈)̄MlN|3ZucQ^!EW>=FL7q,LIQof/H;Q_BpTrΐ%vgV:0s$nڜFF*F~ży4pׇG^ABifyf2jrJ#1=A%u"-M30q{\g7JۧQEQz%m -3HzrNum,սV%Ҡѡ6zrc յj@׉RrzF-A'e}:o-Xcc=oҠĶl.^A\  Z)_}&{:4idXeit~tGzF.עی[E TQ_4QH((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEPz(= R(7յ)u+ו`Q^^iXI^ Qrerei+*20ebAWh7i$2(Ԏp95>{ )RQI{g:bQEw6XKQAO֥jڭj+Kh̒9=M]+?Eb'd, @-~#FDmOvͤn"9/e#A;dU*xK6O}3\.lrG\I-+Ch["= U26 I&{Gɜya@y‡fᏍ/[k2jPZDlMfF\:yaaCRQtS:{.`H |1qdVXinU $xU.ÏX =f+6pLTO9N3@OMмuᨴH"(X0#%sdgV<%aA+ωq(Ӄ?=ŋrONo_s_Ҁ=((((((((((*֥ge=7 /ŒyvZ(<2YVVoh K^ ~(wz`ꗚ7YT'@("K SGևteTQpQT5m6FPu̮G \( ֿƙOkiQEQEQEQE-])֝u(((((((((((((((((((((*aCSo( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9w\L3rVϭd再B(ࢊ(3蔜ZZfz?]g^lF¸HMݼ}2ߙWguG9BQk6ycKG1EW}XQEIW/ cqUgxdt8"fEîtQE#@UVv 'z\qnG*PP0U_iUժSP8LmS[t55oMjqڤfm.cl¼>=oLm:$9{~ɊG֊r(|*[hQD]UxcLgҮ#N#_®\95F_1J;Gzě·jw,2rAa=WgPT>? 1+^Ö_:nQV2iV$)c5 K$U\eYGޖ)oM\b3޹oA;[ߥR{n$9yj*0я*> EX;#+3/b>Vc5eq9uN[w7v14+r>s4k )_pG#*It!ShYHZ[.Y'گ%Utj5i+-~G-ӾĶ 5t-ceǡo*uviwCsk,^a5X6vd|'uẉe17ro> (~%IUf †9ҙE $ cy%w<M;;J*IMGY]NU Si֪xDBWGMZM )QEQEQEQEQEQE4_ TA@EQEQEQEQEQEQEQEQEQEQEQEQEQEU>UZg }I@Q@Q@iE?ZQOE:(((AhQ@CsiowˈREtTPƛ)[i}d6:1䏦zU([sQE2K}j(>ԴVG;g^\7T`r=ֽ]/XYi:ޟmq4Yt8p+u-[Fu/x;jK\)vQ(5P|%!,:ޣOuk_~Wj2hI^FH'#gQ@=x?|m-Y]"AA,jy / KW;Z.{×q[kzSK2rs^ExԿ tOI% F ;c~ÿ )В{y[S,Tz]? t_x~mFC9ݤ8n3udh^i^uYмt$Yy#ҽxGWNՍޱerA[ᘍYW9=Sn|@ߋ|_NSD5+k=\ XG 7=;^EQEQEQEQEQEQEQEQEQEQEgW:$UBy9>ė>"[SፁU]ܞ s^^oO)|tQEyok4i&8TBƏM495WL%fܣRp@җÿZ|A}o")h?B|n=7sxOEּB^_\T:G? .O?zv#m@WBtiD% X\ _4-i"ӭMv#8,q/ogW?Mof;8'|k+& nQש8 d]ufRpljIVA|I}_l5(49ɏt\׎u7#Ǩyw$3OҼwP4]31iVq׉,n9[@({FoAPJ*oAQ~TR~ToT@Zu¦.4! F~ToT@({FoAPJ*oAQ~TR~ToT@({FoAPJ*oAQ~TR~ToT@({FoAPJ*oAQ~TR~ToT@({FoAPJ*oAQ~TR~ToT@({FoAPJ*oAQ~TR~ToTz.oASA<ހ(Vzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}^7T/}:k}&-gk1g9eY oGoF]v#+(ɯGm >p o4Uiqd}}.ۆ׼ϝͱQTR ߅QKy:0ƵhCotufxR[o,hg\0{0^JNʸ]{pACڹ˟&vD?¸~jRUg`& ~PPDIkN40f$//y$oҦ8yZ^fv:ؼ}_z W· rq}m?*7zʻ=yyzolևҘ捣qمG^4v hc}3T4տƹgO9qo+U&@6.3u}륂 :q;Lfm?*֞ErbI֏$T]T`8g'NRdWm5?UT֧fzkU's-g:=NGZ<3H.(ѵU%kS!䯺w\YOKVGPJCF'd]R±$,ӅOzU*W7zʹ&Ahג F=~FGZ5fK_r2#^X E+$K,JJ,rI% 8WߊEV΅Q@Q@_Ѵ5Hm; !uƫGm, z+˥jpsNTOw*1s_%wjU@Vzʍރ>+znhbV[En(#aw\rMɦQEqQE\-"wdqӧ*傻"HӋE:4.ՙO9v=tbp<p?Aַb[(cX)#fqW]T@({FoAPJ*oAQ~TR~ToT@({FoAPJ*oAQ~TRTAR=Z w(Vzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}o}m?*E[m?*7zʀ*QVzʍރ Uރ}Z.oAT4G>XPr~@({FoAPJ*oAQ~TRT}PEQEQEPz( TT~t>!>ߝAEO}:1@QSoΌCO֥@Δ(((((((((((((((((+i%3"OY\s=H?$d~ftiڧ4;tv̅;X)ϩz;IoMouuӒnmmP._sHvry4e+_yR>tgi%#4OAIf{E z uE46DWFeaGҪimK=:VB(>8JnwJwt#BM_o3,ܠqjw,}˯Fr?(xb4(VcyL}(B/V*}67mӍJG zLq1q"(ª=FȊ 2Pxǚfˤ3=U"=s:{]MӟlI1%$UM#MgZ[¨O\( ֿƙOkiQEQEQEQE-])֝u(((((((((((((((((((((*aCSo( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( *).8x3K37utW,{P@ 2j(I}[/n=]"O j~CPuQXʄ%K2SVN2/ _(ǻgV͇-I)3:nQVF#W1UVn(ڰyV8VcUb{zǰP'2 =YCN`ewzEu41Dy͛ =F>ee8kZ=7[̯Y3i ey v&58 Ɂq=EtRsբ+Š((6cnd0dʈ5; LǠ݂Sƥ] tQEQ :jպ@5xjXВm f]=yF5Oak/#)f?)x;Wye[OٿvH=Mgp\)vJ}NMs|_MTw)ѩyW߇tָR/b9\ZU(9g%ݝ}Q_(y% iKh u۟ȃ\\kdE6UR=y7okƝjrQ_߂שL;RTVS"=v#W-Z#]l70 ]єŊJqn[UQ^!Q@Q@Q@Q@Q@Q@Q@Q@M~U/P4QEQEQEQEQEQEQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@(((4PzEPEPEPEP>ԵtjZ((((((((((((((((((((((((Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((((((((((((((((((T50Vz!((((((((((((((((6<:jW0=5E X?fWjNc|)EN RQ\m#`$C 㹅f J΢^}S}oWM >Y6cVk3+Т(Uْ-"\b+G^zVuyu]ϲB*]ٲa(l7SvwDN qq{3訚dg`&Kė7,RԘ!x= V0Z#Ju;ﺯY\eX y;;v,ǩ'&B⑑u85ּOO_O'R@P2I\^M{#En })5ɴ-%!`yQZ2M9o]_O (\{M+t&ꗶl W,rtSRkTDKJ.Xl ?αŕٮ㐌pz2}OS-r?jS\ᚤݓ zzB_kUZg }I@Q@Q@iE?ZQOE:(((AhQ@G4FdT;?ZZeյ %w>P$DƸ3 rA;]OLԬne2U?ZGh:1VSA?z:K)7>cȊOIӚg} z(r}j(>ԴW57~66u%N@ c޺Z3$׿܋G%tg<'j)Oyi,@ N9z#z_~h6Mc~*u5Ik:u5qPض1(^2Q^OP&K>S:{.`H |1qdRiOMмuᨴH"(X0#%sdgn|MΝorXs|k&GP7>]6KG\oǯ/ ]/įj^ miZKz-B3m^9]9hxm Mӭu4Ed8sʎ=?fx.v$ef X+^QtPRHH(C~GQ^<|_O---/GݓrO## (((((((((((((((((Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((((((((((((((((((T50Vz!(((((((((((5opI24JUf fTD^X'6"b=Yb$ydi$b%4&mTr1_虀zn8KkQ^qRi:uE8bd4uJZjvbPȼ:ښjT-UmBSs"Y4lKfί8+>()Ha%_~|1%~&)ZFh Q^EP!C]JIhsq_ZnuW it&\:5pb).ef.3IE:(X*hou(b+s:h-幕b6w=FWL%i+Z4pcqM)i.V ιڵTcaZrfJJv ( (+ (Š( 6a-3Hi \*9N[K3Do_x$#,f_~ER *V9cdQEjPVupݘVABġzsʭlx\GF5pM_- Z)W'8NJɢW˸fɑuV޽>YWÖT</aYf9&/:|p#a\2OJ<bb3NGEٟRy?W-I!Ouuo{QEl;nYOO5vsmc<ê!#ڼbI'$0ܧ:pf[>x"VA< 3p$^Z*`6巡*0JYt U0쀷8^XqUUÐysYG,U3;}fGp@:^k]/ugynJ8w5p:Z(((((((((((*hK M~!((((((((((((((V}_jϫY}i)_֒ ( ( ҋR+6TuQ@Q@Q@*( m[MK%HPIzAuem}uJ ^~cX$ؙG {W_NMё&RFS9Zze[{8AюXzVse[MԛamB(dtjZ~-gIJ+!wiZ]/n1[7bDc4w~z'ACwiR@:| Kþ#/u)=12> $jGms;dq\7</Xœ]6qa%{m>C }h :w~>x"i1YXg*yRpFq"E3\x}$#J-qbܓӹ >+Ön9P9/8 dzUxGWNՍޱerA[ᘍYW9=Siz(?Z~-~9NԭEpat,Mh|Yf;4D؜s@q6c6BPʭK{3#d`C+ = p|%c6/K}7['@#)A;zr>/չ5H.˸beiY:1P^KxHJ@s ޠR| k6<Өc8)CH 5!Wºּ5u̷uQ[uq@8h#TAװכeiv%ڦPY_5jaQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE/x)2 ( ( ( (%^߅6zӮ~Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@L?տިjaCEPEPEPEPEPEPEPEPEPEPEP^Zw'*~w[4pWHg&1RD2jOUEWQ@>0j,)R=#m^ͨzwa|q+N]ӚW2}=Eeפ]C{nN`^YhfAEx}.0H(TvLz)H*H ;TG1d9ߨ<14_lw ֦a%0o7W]QE ;WU .#-7#+29 e8"{s W~:3WMU甮~gg51Tf8HF%(RIo$H+{7ƍJw"󧯖\ ''Iw}>k˕Yù>V6chGQ=s ðEҺqܟ[#,4 I}w~>AEW Aylut }{W1V08 55n-B@+|+} z8 ThΌ=U ZÚlyhGV?hent}}kԩߚ\A-V@s-oNq8ͥfex'IPу™E[WVb=eeRU-!=&[>j|D6IY(QEQEQEQEQEQEQEQEQET*/RCEPEPEPEPEPEPEPEPEPEPEPEPEPEPVUj՟W(Ro%QEQEtVmiE?( ( ( CE TQEQEQEQEXKQAO֥((((OΓ u%uc&'dd rWZ6}-.;˖x-|x?9ǣz x7Ö6,ޕV?((((((((((((((((((( ֿƙOkiQEQEQEQE-])֝u(((((((((((((((((((((*aCSo( ( ( ( ( ( ( ( ( ( jioF_^$($5皕_I; ^ՍjsSV_j:euG~Eyϩ#dQE"+KLѮu) !֜bT8M+dd{$cjV?o]uEmmA E-zT%}bNEV(QENJ9>h(4F0=)QaE 'uE-OTL<3܌u?jE\1/1׭To^I؜UZהS˩҂sW~=kQ-~i>$[ X<+!ED*/sj5g>z>zb42Fv1CVſJ2RWGU[n~ MuiQ@g5Je}kBN*J̺u'N\vg_٭'61fUh\ҨWaQckNQ[QES_p14ګq12~9%u'ќxlptIO@#h ( B@?,)J\&; &U}3hRdzRQ\ͶV*bXuWR2 c)*+b7tiS8WJxWuOb6~F4Fze ʧ]oz8;cֽܖ@O^ViM'~& 6h($ ~J~JBnL^[0v?F5j4Zϗ:+㯱V}R6GF[4;EN \3JiVnc@w?a1/vZ4.2 }S֮ӪNߝAEO}:1PQ@oΌCP~umvtTP}h((((((((((((((((((((((((dbM%Fo5czfD4yp7FD4yp7FD4yp7FD4yp7FxafRT)n"w*S f3InX$q@ %ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%ȗ:7@%DOv];:1+ypKL?pKL?pKL?pKL?pUc`7̐uHb>#ZʋĎ$jk"=U:_՟E.Z=M*b&eXf_3R6ã R4"_'YL8$9 #Q]t9᥮|h%oo΍֧"_<i{xt"_<i{xt_UX F!n z˄7XD*y597e$kL Kq6(S (%;xt0P+x`%C( |'d9/Y~bvF}|ooλ3|m}?"_<i{xuyCh%oo΍h% Ⱥ=YfϑJ;JtjUvKG/ sk |N`kRX 7 J5!-5M^qv4<jyPl~Tѹ:s٦y}iߺmӝџoO³ɒqvgөS(U ,piuGeRWx?UC2YZ=itp?38*=cXm2n!J4bp:֭kKtqJEۓkĒGFF,rXi4rP^6ZhIRn!O Jݐֳ(W/QmoMnTסi:ZE:dV/ ykpdCknk(lgxU>I8T# Nϧfr){}Ag*B̽}]d5+&>d' ##pJs/RQ0 + ( @qZ6{v}Ƀ)#85kr6vktjK{ܧ$FV}uӮ^v#ګA$jft|>]8y_6q0ZY;={3 >7U/x ߠbYIܷ>W2ͱSH (fA2Uּ*RS mfCcWU5>2e,Eeg-o0|14$KFzdV}i>e%V;͎ùDCYzNn{VqؕZ~疛!D4yp7F/ qω.. ZZHcRAe8/WI$zU+6DgWW.&mZ(4'zWI'$ֶXʫ+炬rG+2qwGV*ǖj쏳}#ٿ~xیKķ3hGFy5n8y °+f2ESWi|K_+nBkѼDL׸ /N} rV0(wҭVgpKT˩.tyYfAZ?zI\ N/"_<i{xtKG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KRNHoΦ^OAހKG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀KG/ 3{xtoo΀rgn0%p+mmZP?h?5=fMR\hX<VZnsS†ue)eEWѾOGF-{iCGj꿣\I$LVxLfmWK>4'aIKQGp."YrcSƝ:WFh%oo΍D4yp7FD5z0DjPgooδcTuQ@Q@Q@*( (縆3$H:M(ڝ춼W'tkqFq]QE,AO֥RE|@ ߅< v1%ͪDICES=>=#UH{Hb(fPHqϭkEPEp_|ox1t3Ai/hFl/5wZGLJtϗvMQ|cPaEPEPEOi&յih,z* ֢w%m2n ׄ+c޽1hl8FG?IEll4sOW%[׬e|gkU_,ͨ^NٖVqQyf;WqUj+sPug u 1 yWWmEFPnNVms1mW1=s?/񮞊ՙWq}+2(0z~3nЏRWOMOa'fsusMfԮDq Et5H?hz xmbAUS6#SmGWߠGmnD00IEݱͶŠ(AEܛmvSa~?lғMJS5S?++˔thŒ UfF ATǯֶδiA08?NECQ:>_3F[fAwgr.'pM"v 먫(sCH=;\~$KЭ$'͟t բ(.Ҿa^yd켊mE=ϯҸfg.ԚVEȣ|ReЦaEWiQEWi7. :_O4QN3 O:iw b|Gs}kPz{֯.LUckv~o*1zA|ͺ*yQSWj'^"jwL(fZ˫+㈔k" *}wn}f19(?= 1P#fD55W=UܮE WO?Z˲<_Z0׫z-:G") Pb*mJ;0Zlf]N&Q-MpH`zסRm}Yų+,(xx$SםZKerLe<Q+(-5<-A 5j\MCYyXRoȹ/?jXi6p <c54cVfjQqCv~ k5x\I#Kb?ZVޱ9r1+=rA,浗Z6 H+{Iz{a,ʿJ''BcBnwV6dV(|ܟP)(((((((((*hK M~!((((ֵx/5i[EJ9$ ع=6'"AN*ѶU\e.ǧWTrU̝GMoM`]s[*Oz9Y#,/Ǖ>WN:U$YZVÍKvQE{ƁEPEPEPEPP^3%/X؏*zk)I]4V/`Kk١EWVۚ_.- (m庸/,Tw4mfbkTVce .}OsS8x8R%K:VEV ( ҋR+6TuQ@Q@Q@*( ]oVWd@vU_MJeHPI25ۂw$u=9~Mutb!K:2M1ѱϩ"Hd>d4DI\[cyy<u>%Q^a>ԵtjZ+$^r/WI&_9(-#z_~h6Mc~*u5Ik:u5qPض1(^3~z'ACwiR@:| Kþ#/u)=12> >~(xX%E=wwRm0$>e8pp)4߉'|S^:ZyԤ[]Zɔ,H3ҠѼswǂ,Qe&R''g_$^3>'eNj'B>Nx-=;=} PJ~%xRkh#N_ o/ja=\ǯ/ K?ƒe^7&wO4[!LsH?tg G"&'>Arx8>'7mnl[2?5JgE6GPVz~xǧ]hm㶴3c`F5{GE4 mC]K#"VH7YU/(R·#?TǍv4=(i?3 >(aZZZ_8;'%T FFA57n= YܙoFS"H'hqz?hp?F?EgRڬDlq碨DX G k+ľ ,RF֎14b0Wo7c؊t|\A>p2Y ط>Ѕ=k|9/š4Z^l Yf=t j( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3Ze>_PEPEPEPEP֝uZu€ (((((((((((((((((((((z L?տޠh((*jo4Qj.$GQ]<2f& W=^\ϱ^*Mº ڔk|_}KҦAXA>C v$1.A+M6EC8}~C&qB ̭o@adl|XTG ӡ vAETZ=j؎#ZϬ@<⮟ƬaiQ3+>,((((+_Fb?ԟkbIv-`ӹۑqQEyQ@ $k՘צW9oEӯ9ջ~]k4Z}Ϝ*TO(IRMR#>0sItUX;"}7bQ(?4o''֭Ͷ:v޿CgR]V.L((&x'G'ga*wh,Tcwk̡Vtvb%K1n˩N:ђQ*HB&$ ؊XF2?(ΰ VI\*eH#gpkR*/>ۅBx[P+SW ^%s5a%tĮ =3 D Gֹ^=D`I+!vrGniX;g7k5skpT)֞)WpPºaqU<&TVttR#t` ▸p(kGTkGT((((((((((((((U/T54_ ((((7uʙ ڏףWHS[ c^{6F+flQ+LB(N/EdcR8iVЮiO[9)odt`+aEPEPEP\yn/7+' ?J+u\W搸 ;8J;7GNDiQFY՝>m v_)IEu14 ;mC0Z3}$DDc#ZЫV}_x|<֯PH}I^AEPEPZQOEf֔_@(((4PzEPUoISq}=*kT)EIr]֚c(4tc#鞕E99j4BIyhQE"}j(>ԴW%7E4Ph^i^uYмt$Yy#ҫ,[Z3xliޢlN9NٯI8/>fci Bϑ 9CGMj蚤zNhj2pgWj}[o0@RY_ZKZ]2bFA%qZiw2㱑#joq99U0Gu ]x{/-n|}'G\WY x7Ö6,ޕV?|W.DTj 4m@3 u">1[ž$4 t"=BOFΤ( _} ~bMMc [tYWMv;bQEZQEv>$钃Lq R#[9-Ky]_3JvEGU(1UIYX ̨8ThOr⠸p>ǥV#d$AEj X#dȻVJ{9\FQDjӓzN-2 (((xYD$յP@Fxyg) C}~ =*EKZ2K0P2MjGc ͒;k4G=*|aTݒܖ4p]Oe+.O(ZmC.h=B՚*n#ݪOORDzRGCIad2`}kب8;GcjqQpr:צXF7GQ^oF{G.%@@j׺})dSZW|>mxjMwʣ߹>TU4M^> p lx}]Ib$d}':elj)4[I9,U2-W~Mҹ3neTloO\q>=\*JhIi, r"`Wsabѡw3A5_3f\\x}*޽$wWys;nY94[a3ˑ^Woihzi1;0KknYm 4tpyelD2{QeD9=V(1QJ+dnQE0 ( ( ( t;}b!A ԢJb5t'P0')[b-ipF ErP0' g V]֒}i(((('kJ/IQEQEQEPz( TT~t>!>ߝAEO}:1@QSoΌCO֥@Δ((((((((((((((((((((((((Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((((((((((((((((((T50Vz!(((((5tRUtʬ ٠guUUq|A\ KYlv+2Uo~ZB -yՖsa&$ wSʟKԢ|]xt ڕe=:̾xzrQ[EPEPEPEPEPEPHʮX`R@fmK~wO]O]1 crQSON)UT֓sNpO;] z0gc?z%oM}f2%}yJNgh-/(3uh-Z[yNsG7'z ck)!r# }.jVqC+CD- n?@*0q Q4M#>3,70q#Җ2iSiZZkW5E 󮖹o7\ydWmAgLyl?YMjW)x^,^$5\;Y`J/+h|=Ebf( ~J~J( ( ( ( ( ( ( F`Y {R7n6ӆUTʣ&q0$[K5EUjʬg;w (VχuBH천8cQZQ:3U!v|kois~]f; kstWeL7~kzξpv&kԭ5(Yz{+B/B?6uZW_G+7Eգ선x=үR5 t͓T*/RXEQEQEQEQEQEQEQEQEQEQEQEQEQEU>UZg }I@Q@Q@iE?ZQOE:(((AhQ@Q@Q@Q@ RP}h(((((((((((((((((((Yɨ˧5H$~eSПVGWDWIW)<>?| r=^u^jufPMU6H^@z$*"xOEּB^_\T:G?JD57WF 3ʨ%^k;$efkQ!CVM7|k5Ht] n*c@V-hrjI2J͹G^ǥv~IK$%[6(Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((((((((((((((((((T50Vz!(((((=ծSRr *I w8s ŽvAs/1#T4U{`@wA^f' 1 αTz/dUI H@aXi}?k󹖬U:VN$\wqo}=*́z5{:񪹡<܎'-g+]_fj(LڬrZ7o~@+YZ>O~F1ӷ6^dyHQI94TA- 11U&!OX*'iUD]]1Ұ?+cBe>JXnleL\EϿ˨&ڳ;wb}[['AEF*E kƒ :+iQ{ԍ#DU1N*9d jJ(m;ɵB~#>dHu*?<};s.<68(4PEŠ(yuRytR(((((((txH'ZJ׬q \nq[RWGEg0QEQEQEQEbcq^^Bc]zWWd5\Λ?cjo@U/T54_ h(((((((((((((*՟W*ZVZJW((Tͭ('EPEPEPAh4J(,@#QLp)皦&x3,#^WV ɫ#ZT;/.[l71Hފ^^9]~Hs"}HZ^xe+_yR>tgi%#;m_W~6~ s 7 zyc]gǚfˤ3=U"=s:{]MӟlI1%$P?io뗷󕷤#g#p+̿ A7׹Q@`6U7=p"N,@c皵p5^_P6ͷjoͷhm?E[m?<Q@6P䄸7oG_j](O6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷiH|vFPmGo|J(ߛo|p5Rp5VTӭ> gPmCPwHv=ʵNH'jY,j>#rx$\5rI&''vϪF%h+-5[' ?e?E3đ\^NFE#VP~G6+JvzǛo| 8A~o _Lru}:qb0ЎbXh9P:0SlD=y+]&3{ H<+њ{G ~jzcU 9Dͨ\/XOs)l<*A&?tMtj@ [Q(J-o.ymU(-mU( ~mGo|J(ߛo|p5Rp4yT-mU( ~mGo|J(ߛo|p5R}̟+pTJ부ʒєtփ퉘e$NIMvRʾyv=ljUC@p=( {?M""0ih5N e\r}B($)ѧ*&໘.IfT/.~DʮyaOA՞wnlje׺v =5L&u=:)w_z' (A^q!K7WmziBkE_SZm[֧ZG|WWŠ(kt*{ΎG)uxk>BUz^L9t#;qTݥUY2}PP+>A.cI,1jxoLյ -nD"^k Hdm.ss4P3 Os< qjsNjtI+!&_Rby?ʷE IN]ϵr:OؿYK$$vw=K`kOJ6I.hE'gGԸ>ήyyUlĖ,9kv8g_-57sxk+ŝo|'bqMo^IKp4yT-mU( ~mGo|J(ߛo|p5Rp4yT-mU( ~mGo|J(ߛo|ԡw6zbǑ@o|p5Rp4yT-mU( ~mGo|J(ߛo|p5Rp4yT-mU( ~mGo|J(n-n^Imj5^c%;1?_9):~?Pݗm)GH,>dl:Egfboof9_c_ݨܠ )Z F F# WҪq4QE (֛oN d<<>m\/Ru)ao__[*r_Czjȷp4UF^*M~|6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷhm?E[m?<Q@6P6ͷjoͷjX6ϖV@2Fݤm?}oͷhm?E[m?<Q@)A1fV_@(((4PzEP^sɧ9CkѪX.qHׄ{ ;mFgI{'eF.{g*{}&O2dWG5vcUhEYZ: (?ZK@r?xW]\tkΛ]4qG.=H44NB 1ǩ5x6'(7%q&,}Iм隔E%ezӸH={|3]kO]Yyz]Ky;OP:P/!zI#SQg%_f|+x}egO5#q|B&ђuv+,8C;G@7A>u @=C ?-n6iE\AeF*`1A5oxGzժy|阫n g@ǀ]K ԥ3\-GP 69R4'čKK״/ I möX9{6GUQڭQLgtJ%zn ܎~O2)a˟ͫû;k ۵3eıdqk ş-=o2.3{ v+όđEM#E ~+?~J۴W)c1|)I=UsjK]^M"?Gա|n 8˩8\A:&-_xQdh)8q C((((zV{ KV?^ӎOL ZKn Z:?>!x*k:7$ c?Q@9w'?cU&cn  2Gq_DwEwP$`:>|%%ۇ<eʈGň{|3i^O *( ( ( ( eow-6vdCՀɫ4PT6EOuBGE\i kH0aDr"0+ >(Hdӭ-haT'QEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPSoǫ@EQE^zu˯Uyz\ u*iqRAW)j%kkh=(e:&G?':Y X}[uV&mw (9(((((((($(FS쫬&UP\HqWv8KI坋+IYESrB1@■!~z 46+KIԫ.U܅ui11H`i O_KHPr`I%sm>hI(3K_-ܒ}^,0c_ÊqfP%GQTΜzIk/inۯoB>9^3=zƙEdRtf 1*ͽQv5V½W,4ePz]Ѫ# ={P$t8G)"ps^Z6qi)Pk1X]ُRsO<{1)ASPgZJ((+J/IҋR(QEQEQEC@(((?ZK@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@:O.'g;QA,p9<ңm?ZK2 gU#IchEtpUA"[x/MWv ǟmo?/dt.AH?/4zOß ݮM+n&v1$z@tWVV\F7`F2VgNJ3点Wm{+d0)OwuH)aykWgsirw!@+ ?F s;Kʍ+x(QQ)#AEƎXXB:+QEQEd뺩mŏ>NS)(JTVjݗ.+KU/S (ӷcz;gcI4x_C)T}=w[̯uZ^gA 'dqєԏyu#n{YKTa(U !87?V(lV]^% Pvz`[T ѿ1\ԩ5x*{K9g@|7Izⴠ=D站/ݭLVQG{oI /I?ʷ&MC# #uAW:2U':Ӛ kȆ(((((((yuRytR((((((*V1׽ViTS)F?)tR)$m$d)xX{ư7Zqis_DLdi_x')gj*7k_ArW6W淡P݆ވoJd֥RxUG]sE0R#U`kEEyfPs8?ҹ:CmEEWEP[h#oe<{k <3 Iqִ~ucvQA8=+>8+l̖B9cڇR1YFsח7Ms`3]k-ZӫZ"&M+qcͺ>.ݶZ$l}GBkb7+΅12諸O ;-/)Si~iU{[^'ZRH*.I;''w1h5Z $}8F.8RBaҸi䑞BK$ָJ)=/dzRi7Wdf\EV#$CU+ [QR}Ox<eVZ6z ?0ԵnCWRԃzp>)L=JJ*\]^$pV7ЁUIoT"a%.~a:EEk֝z=ۻ ('ґM脢-2@_{IgnU.sʱ+Լ=j֚ N073:œ|[w'ϳYUμl|sSĽn r.!pg͈>a5գVwԠ#*S7(Š((jk(((((uOib8xSکx^k5 vT ּ,6tdQ߫g)DtsP/n Λ4lAS=ExO17~vgM}THx{~5͎֡GLJ'G.@BkNJ~?.3ʊ(5 ( u /\<Ӗ~5m|-m}KppLYسI&L6TS݈lxy?Kb{rg#Exߟ_Gvzjl1=S̱U)YZ͔]CmWT:曻0n-Hdxtd85)7%G$7C\UW >h=;tcRh!FWYرj 2|>w\fŻ rWO#_:J~KLEWnFFOD)c?Ұ]IKvGmvePQTw;(8.J%&6e?JUԫF# <apy-#5Q:Z=wFRַʟCHݾtV G{eT 8x8עejsW.4RZ/RCSE_5!((((((((((((((V}_jϫY}i)_֒ ( ( ҋR+6TuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@VGt~ֹ@Ŏ@C*?'>X!|r"gntR1!s'amz!HH]q^ּK#Ggږ.UcFN ?h:}=wykր6(((((((((Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((((((((((((((((+/FHHD`z)r٥*nS [פ\y`Eys>ƅQ$UKۋ=f#W;j%%i+I{i]ر袁 (Q@_Pɬڅ?5B`i4_u",TߏAC{5z#lNPUXGH|OvBpbI'OzJ}'i7sQDO!VeE,$¸MVNUf&}=t>'0b4Q,,H$te85G[>u?뙧#*J0>&F#I-buDѝ^T5N. EŴrĈQY:ֆ -tOo<6kBQO *280i{)O[-=\L>{z tn%K)ajkMykRs%ԙ1cc#2WG^{1SR}3{yu:ϡᠥ+z( ( SgsvFWL;i+f0`G⻊nqQ멖3VQTߘQES.4pa~ry]޺ֶm8yhĖ$ԚBrrz_OB(ǖ'N ȢQRV4MQZGUfe9*~d ;Rr-Fm.%*0Y@^' ,$nm%xM|I|&1֊W 6pa]=b)*MXF+J9s%%&HaBy ~U1WC4hf }nFFyPsҕ>i'}X|J Õ^Z_m$܎Ց9Vehԯ#uwƔl} ;}nC:KQ;zm#=}z*N)\{zK^tQEg P Q@IYui,|{:+%#=oftQE|Q@Q@MoP}4 Q@Q@QdEsZ]Aq ? &F3g>Xj^3!#)Ȱ Xy^%la3nm $2$g 4O=9˫C+rwfERA-6 roz|!O1[I!7H\ _|t6g w&gѬkF$bvBk}qYҫB=/~"YN9K#,31P@ ;WJ 2kz((((((((( ֿƙSF!x$_~"N֩BR#SW36mN 9 Jw}?]e}:2՟Wd0ѷͪe9 [[8# ?rNA豨J5KC}2j=E;=T}=+F<7˞"omSUk袌fT\=Յtzڼfl*~֬WG؄͌zuWk('P75 %e 9ETyXL!RzGEX=q5mFJJ`mlnRz;M+ηJHӭreXyj2?J|^bNrHHHŝI=鵳KySVU5r"5;G2_(ޡk ×v,zVjIjpcN97TgsxU痑ogY~$+fZKf-8^W&մgVf((#2vm1M& 1Cyªk.v"JSӡct.kws:L0xḅ%)>E ! ZM5tQE9+R>FEEf \f%XC *40f'+Hb]UQyb]T*QR_~W1/(/?Q@T}4g!Df,ʑր+QR_~PTTgsq-9m0atkO$g<5rLrŘ^&u*j7Σ(P(((( rMFq}/?Q@QR_~PTTgs/(/?Q@QR_~PTTgs/(/?Q@QR_~PTTgs/(/?Q@U>T_gxn(To)bvg~/?QG(**_QyTy"5EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPY^'mh xsqZ:}m&FUP6HmPu_w2b ~cm".Ik"_2k1G+hOctWy' (((((((((ΐ5#M4ֿƙ@ h4P2}M%>SIE.O'Q@ۓ4|֝ur}M>\SFO'ѓi(4dJ(r}M>\SFO'ѓi(4dJ(r}M>\SFO'ѓi(4dJ(r}M>\SFO'ѓi(4dJ(r}M>O|Oin Cpé qҡR5sw'ѓkOm[Z~iBd&6cVvL֮ %8hd2}M%.O'Q@ƅGp_FNl\DTÐZ=I.&'~?sV筗Bq'x=&(B(@m#VOݭxa#sjz*Xiwi ٰ2O-T3wʛvW3y/dB~c«QJN+ˊI$g%q!UsO ׫MoH_޼Ugʰbjw~o w#_&Hc{zY#WUK2)y%[咹f quhV]%:Z;P~iH쾦? δ@ :WSuh# ÜRJ35 T:.g'fzK1g4Q[#> ZEgnP~˯߅CXTm؊e=4dJ+\SFO'ѓi(4dJ(r}M>\SFO'Զn43G 8.gIE]SFOgNV-2yc1O8vSWB4r}M>e oq@'K*٢Y'OЌW[Զ 6ZIlU4dJu?ZYElipH8AB<6j3Z~Y=^]wi5ԖjlTQU5dU-Ud^x8e{dW8.-'i.Fe&bWNry>#)xҧ(URA$;QP#ear=ty>z(4wF=+pyISp]ֿyf2}MV;(ՊHI)GfjexVm+M/d~5wiس1$]GckEԟryTĸtQEYNKfQK8b'd{{WZM ̣{y*٫)ltY>SIE}I>SIE.O'Q@ h4P2}M%>*^{ TA@d2}M%>SIE.O'Q@ h4P2}M%>SIE.O'Q@ h4P2}M%>SIE.O'Q@ h4P2}M%>SIE.O6g; V@؝IhZJ\SFO'ѓi(5tVmiE?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ihlV;y b;R(~jVNͥȩ3l(á?{1\d0ּ._ٺ)4-^ kCL?׃37. ( ( ( ( ( ( ( ( (3Ze>_PEPEPEPEP֝uZu€ ((((((((((((((((FW/Wfnu;U*mpTJRp+*u9vQY-_Fz|5>9\*s~c{G+ӥ'(&ώRT( GE0A)hW&i#RÝƲk`*<-#٦TU<5{|嬯;BTB)ʫ_]0hP0[#5ӉY>h({U\XVoK-O6a|PMc7d$zm?XDy9nlߙX9\GyNW!%vA2º8{;FwY] p 'OֽPm(| >뎫\߆-ʌĠAҵRxKѽ,Ɲj.3v*(9(|*j2c?®–u` ?ˍiP̫SgM\_ZPϏep0G봬}vOotKU>_jPevaR(h(_y\}[=P zBt 쐛k3*E삲]t؂ pSSZnFI={˹'~r+i[#>yk!$Ii(B(Hd;8MOsO[q!` =3%Mt8qS](, qt#Ӡ-,G\WS\GվdQVez`fr4SR@:k??qi&E I=Id>&U溝Ă\CEM#sYŘNi֤O]OjezOj|vI_`<)kei[HIkvI?C]-xU '}VM֏P*m׭ERZ[߅CS]*((*ZPזjvGIs'8WW^^OtZF?ڼ1XU d'+..X G/-1Y/{ˆ>E|\Ujf-^Yԭf͸~G-F0ΌtPwOa4zr$tarWg\m>mg|q14؞4u_jo(㉫N~ݬQE` (M 2~F_1GʻJ< >,R" $ciIxlr7b-eFccZo-"a>ڮsRU-?$p}prW^+{2*G[QExfa^kFС0ғ)^xò%Sx2{oz: L(^jӏP+B((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEC\:tY~Ͻ^=kl?huNVMoֺi o$1\iE u8 W)_z<%xwBHČ(U a@q[h?Ih?I{h?I{PEPEPEPEPEPEPEPEPEPt_L4((((zӮ~N]PQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEq(hoҏ0?ΰk-⺅AV·kGYSk~hCi5NG=Ej'uFlmηt i"t,ʨ}}(ќml} Q5dZ,ZLQ ĒoAVW#juf-QEV̸h=Sm%vLc)>X  ^BI?~6_ S;wem~_eL?տެ NQ VP, s>/m~WMY֞u=OwҢ\:0UcKo {RW}V:Xd[}5%\4l`whjaV4]>󢠀Ad(HtK(&Q=fWsʁѸ <,ZLrNߍ{xl.BOi,-"c=X[jx[c𣊵n}ngZ|=Mq.#v,rI<m]?,@(d׫O_QEl*#RU#D-J ;kSux~YC?P+ATUx)C52FtG_)yzYTzcZJ..*5aZt±Fl$/X䯅nLj,#,&qy#f?;%ͫת֟בݖQ u^8#AV+ϞnG`0VƋ~{QYaRZ^_ ]T4QEQEx˨Gh #\jxxYuxꎦ&r}'vQErQEjoa#Z(sgyB!WXZBElqQEf (#n h&'rPߘ[ƚ{IW|Oc3Q!K,I4Oa?(:rgKWVoOnkN=E׵N?r *#Nrbt6&{'Of_kR ͉Ef,ŘI'%|f'3VԚ]جŘ9$QEyQ@sLӦ/זn=M^M58j%-QX*HaegTrj> a ]ܦۉFOT_5قKWmՕݗ$i66CFjQE}*QlҰSd%2 :ѫ3o i1o'@[oV;Ef%Es&KA_ Q] ( ( ( ( ( /RCSE_h(((((((((((((*՟W*ZVZJW((Tͭ('EPEPEPEP^s⯍>'om+(`g8#w^u]6{hiTA_)* [x2Gm@VijzmﷹfU#5F2]䵍A\dθ<x_?ٲ*S l 5ß8xuΜEXA?wPwn57Xd_E I_uƗ!lv}?L$Hbyd`f=Mqz D񭯅$.u+G٣FEipG'{+Yn xUu$zO  & fyb,H@ߐuG~6ӾUKanߏ sAzӏ7p6tXB~Z{\? &yhnR?^> NBDYOrOa8h^dz0Rd pz9<;m#zZ:5\ۓ#u MЯu{2ȱ\ ?x?_Z*7l#`F*XP$_'  ]WK#"n Nc ;G^E9:.*+[;)ӵ̃~S^jFP\B;Avn~!gN|)vs2B(f`q'NkK/i?S𷇡%AD`g*GB84'5o 7J, #8Z?Ľw.xnUʟY3ǷKC^Ë"D-KiӠ#PxǺ_lmn[{ɣĂ g̼V։YxEմ|[Ĉ{P}9zOE@4Wq[DGoF.QEw8fj:K1=wf[ YEe*0Ge,~"yc- ; Z8v30[QWF*µz5Gp*(((]B{5I^F2PmNƹjlERZђQEjEC&lf 1`AlykQS& IF `3][.fQC޷VT̈́$AA;| ;*!ז\a#kَ.5dWsN kKO͑QEψ (7!vEu(F-^f!*1)}NO (QE}T}hGWB֧m;gs͙J1V08 YMӵͮ!7L|D+FߩӨ(WmqbйB@s73*9uWcRUyU(S m׭EPϩZifc^p:e(sIrwP-㸞cK6eV3x7eȯ>Y;s~ xa>r>##]kҬ&EV<Ŗ zG2^) ,63w5v$UBA⽌ C>ַWY :HxrOj8jyM! (9 ^2W0D~TӴRqdsWkѴ2*mw=X׭%^%ȸFYh;LZxKhGEI`_Mrf(3(Zk~Iv=?\Wו*_XUY ۣ(6W5UcBGA8kzZl֤\|ч"Ѵr0VY,47rc۔ ]X;:v#{Wput.2OC>v9{Y TҖ/d]$&> 5ġL'o[Q]ӣZji%QEŠ((((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEQEG5},ϔg?ᇆ~_ x Vjm|eAy_+#5 JJY^[LB~XѲFDȡVR2=E|ΩM+]MvMMwȘd}s> ˼&?tA(;^حhzL~Hz__?Q(n/e;{'Ixv:yj-]~"&(9>h~xw ŭĉScv< mp3M8̐Ky?k;x" HtU~Uf?hxWMq3?o.E![ր>Gǫ_ EpryfQt7N:ׯG"MKYNCЃNm/ kA*݃ z_LE^2*[AmY?^4 @J/qڽg?׌ʀ8I$qWב~N?)'v ᷉MWUZL"[/y__W#1m@?);Uៈ5_:沱`\[ױQ@w_%!Sqt+/G#mu<ުr0שb>.b8߅ÿ|?mVD̃CSZ:/~7kgoĺϽ,#Q;גǫoHcvbl,s}74@YcI(ZtQEQEQEQEQEi~;un7]3dVÿxis}u>_ 1@Q@Q@Q@Q@Q@Q@Q@T}ws$0F29OE2c扃"V# һh#*c:ii4vzïUer?jQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP=Vi,˕G< Oo[O#]YxdRrexnݓ9J)E$.RTda0Di*)f=&Oנzѭ<<G1XZ?$2-Azv#=kgJExEPEPE ])\DqM& \EeuܬzZ@QEQE!u/ 6LD譨'E Tr@ؖ REk<툡*Mz-jGc|4-ʣ(ONB@-N: HyXAƥ9Nڎ4 U@i:fXȽxwr|b4 :ֹiwV#f8IV ;Օ}EV(QEnhdZ^F,?ץL+ɛPԯ5 JNu&F F}X⢶;x$h0T֩'~ׯQEQEZ +]Yܼ}+^te7ty,muoFR ^n\~1zsEwO43wvGilD0??z4[dDVv|QDHV?xqRI?6ruf^xݷxv7r06c-,g}ǿu z?ң1Nxy:MPV( ( |4ɧ1늭 c :6?ɮ춳]]HWu7vfw:5v鯵N,יId[乸Jm2 r;;`~+9v.T2bG kS袻IYWR]CNՎ<>?ZkM[,0\8?__qU)ӣ)Ul&Zo3* VR3f%O&ERv^_CcWs\5[6&5WΟՒ}MkQ^fn˙aeN $72 5U#?θv~\S8{/k۝cMuC $Xp:Tє#OFj)ly5]q!HōCR\@2A OQOnshhmg[J\+{}^WFs 'O*s{Xt5TQE~tQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@M~U/P4QEQEQEQEQEQEQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@(((0i2"f|\5b_ukzƽO#P\Y$-Hġ dk(9/~txmZ@̪rO#+9þFmZvkSG hے>F8n<z"a]]KI]2y ~yQ6К/_ }kĶf0[vBY$lc(#WQ@r> 7laQOø@:|af??tncH{V$<\a"У2{>JOin%\4wǠz>ؼsִ|뻛G(p2H5Q@6cÿ>qClZkG;Q[hw8χRxM6$L>cdO^\hZBBXY j>־<#AT}(?;?*ފ|}_k~|ۋ}ǹWsdq@d!Ŧ6f[+O*Hk ' uY=߄չӮ|cXO4@xo|&Inkc@,k^?/W -> vqJOM495WL%fܣRp@һ? _͎ %zRo")kQhsϓLiƵi}V<^e O e ֿƙKiY](9np1y^|ETsysRtQtPJ*ߟ?G?@(~|E|ECoi_ 9bgWK$#'@h~|E|ETsysRtQtPJ*ߟ?G?@(~|E|ETsysRtQtPJ*ߟ?G?@(~|E|ETsysRtQtPJ*ߟ?G?@(~|E|ETsysRtQtPJ*ߟ?G?@(~|E|ETsysRtQtPJ*ߟ?G?@4aXz0"E_DtbysysXwvRT|Eg<"µ8ΣK?H1裧f ڍ>yk6RkTQ_CF VGJȖ |2mꦺ=+fY Сp3\# ߹2H*PH\ʣcu#X5tW.q{˒m(~|E|EH ݽ&[$O='O{#0+5JUk71装Qj顇uu{FgCE^ooSllV(i0Yb/?C2p)TϖWohCo-mISOOO^Ia\kqk!GA0#VѵM_OK+D;[+l{fm g%i)Ͷu:f\q^I^ɪ>y!u_+nVyM) +(Ɠn-Hs9sqA$zdiJZ[u0v}tWOˈkf<OjA #yRnx])d #N.h0RٽG_E[:(:+>E[:(:(oσσ x-&?.0$aN=OV桩i|ODk1y̷R\Lۤ1#5Ǽ4T)ODNV}[F^+e'''v`QE ( ( ( ^ E[qyw>, vQ~vdz|vMj]7ft{sq\h'y3Q- *?s!S񭽴Fq>^32º3VRV9(@(+wVmz7 ְ$IKOK*ʶ"/upWfszGi*7Sj5=Wv9U~:z\Teio՗9DiO곱f};K5[voeoi:̢w5̮E\W-ڬ3 ~`~"yѺoI?Sֽw\Fk4Nכxv_Et_zDOeys,W^x0*WpTz&ZtQE|ِQEX2r zݼuR1^Qm\c/#{$ooI G;W _}4Mi~|E|E}RytysKT@wXPe}j*T8MvEs ?3AOS0[W4Th)dz奘a˒RL_a:8Qu?~5נxUm*c\/qk泈8ʭ{_7ҵU uU=qT(:IS^znƲ!0 wU9:;b$\~tWҟ=8ϺL[(~|E|Eh2oσσ U>㢏>㢀*QV?㢏>㢀*QV?㢏>㢀*QV?㢏>㢀*QV?㢏>㢀*QV?_PEPEPEPEP֝uZu€ (((((((((((((((((((:PErdiZ .2[XfqwkXej(KWP{ÛHmlnWYzV |&ORA(NOҒ4V+ȱr@u,67?h\t:ֱiGՓ3M^-UB(U+^1QJ1VH)&u BqMzaƫizβ*oa!-֩h袊S((]jRb`7qR~͟d'WTkz_S&6*kCgܧZkIe#pҋ?82r]e66 +IO,f2*hrz(#((Lp7Jѓݽ?yt3iuSBacf77n( ( |1nBOo^߆Uoo Hq7 {TҶw0y'+qQU.{?c^,Is"yrIFw%e퓴_zҾo16LWJ?x~J&?oו!g'5flA[:ቄy4a-6N.JLB5^źUWb?Ue(jpڜbV0B0U fS$E*]ٓ(AEPEPEPBQ )HV?oio.4떂 {0W }BC{<9A+ྐྵ E;8;\cy-O'ba0{0Zm:=@~_MlJ.N:.QEoLkN ө*9p̬:8" )'-zq7> S>9TG:ZdՕ=vV=XYFZKEu7~ I 9#$XCOT@ӠuHI{W,_y#-x&㴹auTa$  *Ÿ__z1++Q] \)k9<ȜxW!յ#?ιqxJxrOg[}P4 ]^LF[lvEyr:P4̻lBUTEE*; Z(p((((((((((((*hK M~!((((((((((((((V}_jϫY}i)_֒ ( ( ҋR+6TuQ@Q@Q@*(*b$ƿhs]Ṁ ,P:!OMy߈7zhC4?s^-@L~~Gsgک?%u]]C`M7vCGP}5oxGźu-6R M?$X#ڀ>Ԣ=bdŭ&zP}>(tjZ~-QH̨@U$WKѣzk hզ*=.r[q$zn%Ɠq;&Vb?~ j:&x_xQ)GǓ\ΩV{ Z׈uz[ Vg@Ee'ن~i#ۻg)DI K2n.3MQHgph(7ySV ;O Nr #H((((((((((((((((((( ֿƙOkiQEQEQEQE-])֝u(((((((((((((((F]WbX^'db{Z޹ܕCP7QSKkq HVZ-4qQm#Zc&jF3\Ϗ(O+6QXZ%]$}jzTWU9(`(|4Fɋ8ұ~is5W˩%̅c~S'9\ʥ,#J{n wG9j,"Y<=A5LjNUƥSbb),d3J:C8>dbSYι:ҎsL<>ta#o&OblY:g]}"x]έ3 +(җ4Qu)s',mdA)k* TC)=+фԑyFoK1%W_K<*y1SM@M{8Y2d&Ἕ?/69$ԟmGUe&g41RIy6ǍU+B`ϧAOa^.屷f=NV 6A q'(cZ=O[54:;U;|cJE&ϪN4d(XQEQEQPKykmϣmSi/ӌ\݁ͪ[L(6^ww=Oq!y=mx~Vх܌,~UקbR>}Yswv (((VMbnY"!A4V *ãmKOx$.y1Y c1 pXOz]}smaIY($(}XÓ |!*u };ƻ+PZVD4QEz圏l{H K>sGL:kSWMu3#s@q^c\ΐĥs@UvP[ȉ}As]-'+B((yuRytR((((((((((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEPz(= RJTO!ڥ@#@ {y=iivc3P׋3GH yFOFyFTŠ(gn<so!%m#RƽbƁ.R{@`㪧A }j(>ԴR2V zKEx xwGu-ޙiw"%"6 9K¿'Z?/f ȟׄ-h8NaYq3~f+ǖq? 6;{0B4a~\nlV+>& 뉘)/ ظ[?U~ʒ]}>qcP@vXזC{-id:L~zG­Zcច,L mdZ1Ep-L<+w nBK'N {Uxğޗ f )VB^@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2/x((((([N]Sm׭:w@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQECwrNu8=4vCJȯl`wH~kjė1a_E?ۛ.i[.'** >8)ЌVE5I"鏳lYxnПB+5gQFFY$hv4 ( ( o˥ܘeGv?VhApf<YtEWQEU 2"U);IՕ<Ɵ/]5|gمh-Ҽ*0yNڳJ:mtU8I5_5#3åG\XΥ)җ,՘R7{⥊9ƒwR#˥IBUY=#%׽;E,p$עx_Cm.զ\\GkеAjI3Ư|jeu # eI/w=ћuNbEgz:lcp+}OBS,Uы4(:O_e|s՗G|A^1WRyoT'vtT0\_SWi)QEWP-Vl(Տ S-!q{|C6 ȯ̳9՛M+1 UbIt"1Mqhl٧>4/ ܫ =EG[ ocYm? oP{g]4ɿZ0Iu~@ٖ,XrkHY*N$y7wKUӴ; 3,=߅iW>^֫vE՞iU+{?謪U/v!$[See_?2  5~0E1'Ҥ/sj޺1xcn%ͪ8+J}Ube+{7ҖR`x?VVw15idΗxTՍ1i..MsO,)P^.ƱV*kxІ(5%Zk,ɯP5{o._E$?\Mׅu[w!!f?C0jӪN7CAW&m^ iMR!wi!vVo0˹ƌWRӫ7_H72F :}j֢TJ<ՑV(aEPEPV<:V<:EPEPEPEPEPEPEPEPEPEPEPEPEPEPSE_jhK Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ZUV}_ IJ}QEQEV_YtP(((= )QE|Lgk$t-l3d^=l^<3pjUŸS.ѷ󯵨-,n V\#2^א+ȵ^Su=EEAPb: (,AO֥REP/sw~-Ҥwcom*3G<ϲwmP3[9OJ͜ z^[6k$QیW)y/mW5M~ٷGi6Q k.z BAvqc{+)OM[X2pˏlZZ(G+dyۃ$=q2IMuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2/x((((([N]Sm׭:w@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV'$+"[u( K2?:´E放5s_ҀyfHϑXZ^èM5tsU"_j c׊yዘ؛VY' ?czr~a^ZoٳuW̢"K;5m4{\ƀ;pZHjӟK[ryxn.渑,q; :UL7[gּڙrU(uW8j +q8تi9?89MLPJTGB+Wsf47]SCR[KUB)/Ro2M:Fʑ,=벯V&~D]ªjs4T#*Uԡ7]+6‘\w*Z:4tͧduQx?"eF*\OE؇_p֯.6py<.q^׵|[S[theYd9GP}Y鯭M5tlCES((((((kGTkGT((((((((((((((U/T54_ ((((((((((((((Yªի>PgZJ((+J/IҋR(QEQEQE(c_< U=h+V<_EX=hKMG ((((((((((((((((((((((((4BI=i>/Zd_L /Z>/Zwwh /Z>/Zwwh 0"JGޖx]ʌzV֝u4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oK֏K֡&4h4j(oKֱNhGkcAZuY5$H3"zTM Ekb#jJϴSPqu}W ?tOjJՅQ]Q@'(u[/d `szNIjsY˓[yOC]_ 0cUixAMb4d%ccЏC\OYOCVS F_ci}4s FOZSOT|ܢd”c#'Z+XYs~vx>4|1T{?ԯ {,*wtHeQ,(q:d2vx>ljtM^-E*?3]_Z(۩e^[[BvOv#(Q55hdF ]W6Oe2: [UZVp*GW[ ksBxҘ\MkpGOAi4W ),> %dMiא^I}pF'Y-mMu aeGKlQ+ B( O3/lu2m+Ծ/ZO [v&27^_WE<DmOboK֚:#;aUFI' oϴy'5VSG\y-ĖV5^+K (u^ %:a8BCΏL^ 1#AQڽU=<B7w-}_}_M1,#^޸jw.|;,c׿̨a,dh_ffyl^#աmaQ;V)ۅaX{KTĦ}_ Zr(z*"p2k>m<BҹU( c<]ʍ)MLO[֧֯i XqYQ_RIn( U) GjIo-uS+$b9?G\z7 #$2,kh}a΃٫. sEuiϕ!?YCšl}@uLN\ j'Z|sK*GM #ҭjzdUA;H2ORYFT-֊(QEmBSYeB-"FuvƕH4k%e@aDf,:jAS[t/Z>/Zwwh /Z>/Zwwh /Z>/Zwwh /Z>/Zwwh /Z>/Zwwh /Z>/Zwwh /Z>/Zwwh /Z'vU>Pmo)bvw֒&4h4j(oK֏K֡&4j=@kJ/IQEQEQEPz(= R({?ƨt WZd/G wP?=5x^ˍҐ ?@lQ_ Z72m/n-VB?k|^g]vt=CѺrj-ŤPGb:گP>ԵtjZ((((((((((((((((((((((((Ηkiֿƙ@Q@Q@Q@Q@Zu›oi_ (((((f 'r$24v *<'*. 5bC3`_\erb*b{fƷ3c/,7Fx}G^YWBy\R=>]i(c}Oֵz12L:*QEFaEPEPEPEPEPEPEPEPEPEPE pg"?Ji9;!}VF}ʼnWC~zmAGpv C#fZVUV>]5V34eeE@Ui/cC˟n ,bSTu%*o-3ƗP[G.bxTi_d>UQ\¡8SR]_IJ* 35]¶rĎ$EoeبRāfO Wڞ?-(xEIWF/2e{g2擲4I$YymhD6=XH|ojֲ=T ^iMҊQ_4dydXR@&hb귿f7+ oa]iCtH^l`ҽ[S-#qd^lyq.B;z¶(ƍ(т V+Űt H\3[C HX{E/kJTjǑW5=:m.9F:P9Š(-&TdGk։Nk5-Z p89{< ס_:}_bl}pk8ѝESM[P}OQNz(*WNr2 :QEH jvB^7{SYQW:{QP (opA9M6*#\[G9 {6]Gq nTגWo{;rr"p?׿cj{Oa7t5P->Gf}/^Om7:v5z{HDxFTe UN:ϢѼ5uJN 9,QR)oc_hY$w%7G:uujT*FđFQU.zjZ}΄jk Q@Q@Q@Q@Q@Q@Q@[_?C[_?CQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@M~U/P4QEQEQEQEQEQEQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@(((4Pz^}2Or/wA,+k?.G>hI2g o.;!ٽX0 ;P:ů6ϬSzg@(J9qcP@vX;Uv!8^AF1^^+4S·p$"|WPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPt_L4((((zӮ~N]PQEQEQEQEQEs- ȍ>W#^qw0<2QR.$b3W"2>*C;5E77+ʍR0[5;#իV7dnZI.dӁ mc"cB^#WՔQTdQEQEQEQEQEQEQEQEQEQE6I(G8TjM+/Eu r4"^WIr},{8g;g ܎VZ>g2JK5$>2/ΤKp((P2k:cJKdN#rsNT~> Q\GIS*;4wWE*MKGq^E]ǂ[9C'2\l7֜ƩbӸ ?^myy=Oq!wo^z Oy P<*竗6JgM?v:|ɜQEy$ZepREG>:.b&,Ȟ yoxF?$Pq>8QOr+;^{}] Ѡmn滝FsmLy"f\q^Q^{c1.QE|QE$=a`}zskd@$QUD SWd<5_+B*Vr\pRS@p~-iPT_'5V/k+H*|&)݅QX ^-Aμ)߹;Hn~58B,\%fS4)"|%oU:Vmzm[.SruS q ɵ9cµe5a7**z5YD=Z@k~PHif0+Wz% iw:]3=pd!ET(n%H碨ɡ&ݐ] x;QK2Ǔp@fTWl.3섏κgBz+]CMr=p2k3J,rP~]+455 0:^QԅOoUZB.c##+H}jޡ5-RI-(`5oMCS[t4QEQEQEQEQEQEQEmm QEQEQEQEQEQEQEQEQEQEQEQEQEQE4_ TA@EQEQEQEQEQEQEQEQEQEQEQEQEQEU>UZg }I@Q@Q@iE?ZQOE:(((Ahx/ɿE֑Ik+zWV?|9g]G:U}(Z+_ľ)MCqe^̧?n@RX$ hk34԰7ࠚVÉ9j Rd0sԊ?ZK@e^LJu~iܒZ /)1{j 7촛Vs1& 뉘)/ ظ[?4-nH=rOxT-tve|`9$S}!1]c5 huz0& Ughbh" Gmیc-_Oj~mL\{g$zNğޗ f )VB^\>^#u}tw\!aN?2k((((((((((((((((((( ֿƙOkiQEQEQEQE-])֝u((((((omw9QEp(((((((((;UQI^xUD㓡+j4*Vv.?A|Qz-,=6koMmFG1x?CZVЩPU5(2)L%bQG?Ҽv펧 aBАy`1 }v0b%KW{?8(OB(E91"+O =+~ ZFIWGQQ9|[0ӯ=eۯ˿sVυs-Qyp)6$Si?r)){XuRT\i~f5I#X-&|S<<빿¡b)v7Se4T óRxӱLmr}!qzM,)_Ï\9OaR;h0|{Nl¢+(_0K\v~ e}'~Ueps5*>$R ݁|i0-A*˓c? <“}QEHVԶf˚ͮs=̗$`V=!JUqԡ7e*25>%[ND_id#=ֽ)F5MEŢ+LB)E$Q!yTu&v@ZҴ5=B;di9v½zQQ=ghDZM30W>AZUyf 괯/FQEx+[P~V%3]mq8YˆCx#ם9,$?((*ޔ3'RymIr!ʰWM7Ə\MLjrN@E֍~NjAN;3;QV%)1HȮS~-Νo&H'#j[\؜%.|"0s#uƽ.x-l$k6 Kÿ֧{/G uݚ6 (((((jk(((((((yuRytR((((((((((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEPz(= R(^ѼWYK(LJGx9V}^Y#(k_Xy@lbC#צx?#2^,o+ss_'޻((tjZ~-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP+kyŒ1=u RABRzR*A#%hOIN#hW;L?ܸtU(VVǗV*3iRլOjp'GWhiTjA[='WU*Jƙ]noF \of θX^*7qOpRMy5(07it9#>^MY2p]-jΧtYAGV)$)N폊Y e$Se8"Úy x#W$+ D8Wiag!Z8X|/;GvcCr WX%V5S$ ;]31r4e}}Xƴ\&@U۸nNRm)EY Eu1x5"I#xP28>٫ ]\<ף 7w4Pg/Ў0V y!(jkhv㶰8!_P_M-_gtQbY`xf@)\ያi-է GAVWP ZF9E3WOmA#z`Έt+v 0^7zHޠ$[u, ZⴷH @OwHмpf?4k^?0W `VIشMZv0PiMrS]ׅHLQm.^ńF 0Gԑ DOS h{O jL3na^)ۏï]m$ynv{e-ԿuAԞ·|'u}0ZDe"Aݎ+]`K#UQԮ5;=ÓUJE+y>k:~vܫ?ߑWe$r]bk[c!oKVNqgETu}95M:Kf 1W^pHKfi6Z\<IQWj:E[ȼ25w|+E|'& ~_xCuxJv5¹kIIh&9G㭀P"\ZWfuSGY2I2NŶ殾|;~COc~_sҕ,4!=S*((*[QTրwP_ ( ( ( ( ( ( h( ( ( ( ( ( ( ~J~J( ( ( ( ( ( ( ( ( ( ( ( ( ( /RCSE_h(((((((((((((*՟W*ZVZJW((Tͭ('EPEPEPE"=jc_<鬽OUk&ԯ ^LF}9}OP#"?z瞿4y^+mDy sjq!$7 |7 Iqy3zFEuztMW-uE?Z ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ({xq xrJ~ v^Ѣ1$oc^EiW7o&HO bۛzrk((:_SZeQEQEQEQEKoi_ m]( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9_X2N/QIG?kHtY2d<,&?AױEASkds*I8wl4O7Ba!>lݙ+>ӻ5"Z;&OH~~+799KvynN+e,)jk2dWU;nC+?5PʩֻpTT|y $:y&՟?s\U T`ƌR/M*]ui|c.kY":{9CE~YU< !*5Vk7 9?Wi;"柪]钇(yVMVWd^$LkִLק2tTקpF;h$V)f>¤34Z'Μ%^i: !R.zk}oR:^J6YW&sㆣzb;2z֖hIWzv^e?z@?z»Tsf8hЫhl$L??0pBQ[ >*o0pB E[L??0pBQ@(L??P >*o0pB E[L??0pBQ@(L??P >*o0pB E[L??0pBQ@(L??P >*o0pB E[L??0pBQ@(L??P >*o0pB Egx-׆4;k;"mdFG=yO_nPZݛ1Iz8 /;ӕ ],rWZɏU7)J>ڕXUM=WaH.8\kƥ |-<iZA =w^~e;Qr4 [yi}UJ+9Ki}UJ(ayg4VO-H }aoڵnC#Q\ brlLtyEN[bS k;mm2B+R|F 4ANDiQ1?wZ)9F?>v:/&[W5̭Tas_DK sMyMF5uI{[{ X~_νl HEZQ_.bCRq:1Ǡ T+vSJUbsJЕj{E]abuhCD1Bj*9lѻWV<~ {;hf'?\o.s#8 zo0qh]G7jzLp!?jMB>v >-j/R}Oz^]QUj+%*w4(L??W)Ko,mnFYJUp8-o³#jy ]=\)V*:^MdF9eR}xU:s59rZny}}1SFQפZme䢙%~d4W^ ,|׼i}UJ+,!GaT-Q!U( i}UJ(aiR!GaT-S7pT[@e4|2~ϴ wP >*o0pB E[L??0pBQ@(L??P >*o0pB E[L??4nHU#=*Mo@Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(aiR!Rÿ/+>yt}Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(a=fRQU/Pi}UJ(aiR!GaT-Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(aiR!GaT-Q!U( i}UJ(aiR!RE"I{U g q R}UW i}UJ(aiR!VPGB3YtP(((= 2&67Ɏ6q ¬ĖcOs_w*TPk[NH/t}T(Q@Q@}%;ܼ ԭؒ__mȼ~|_PẑH7rLO֥RYxGIai*]3P+No,W%TI#2 q8=T7t:6g[L6gF8#s\iǏ*j>9KkU&L|m a}t-Z {AmLWp,kgWqZӚjq%uIY nͧ=h~Ӿ"n6VDC D~ ߙJwCo" k-xk?[$բa:7}zf d{xpK6.?hbg9_e<3돥{Gë8~xr(:|2?V5^rA7Y%ߟ3^VXgK`[YyVl@7@?iO PSo^^)'7kŢكprvФE׵EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPt_L4((+6M=w1so@#dT*Qfz΍EQt<Š|/kq!x$0@]@t̿E\/R>9/ Xγ~|p?] 1AT_ O6< n)jAEV QEQEQEQEQEQEQEQEQEQEQEQEQEQEQE$Hi$p,pOPqKdUr4ؘ1:b3l*8wknΙr?\Q Ɯ2Rupg2 Tl +݅KJF~yK^(I9y]>7YK;1έ ӵrU" 3a(+k&^]OS$Zl{4W+ ]#m>v+WU\x\DqH>;Q] ( ( ( @m%PS_wLTxR8}?ϊѕFd&qM]HmQ");]BҲidn!~)"Gb+ ~~h3רRuxחNŚbǹsJЋh#/u6s\ 7J4\)wBy,b}=z9Xđ7!uyˤ(,Zzet aGq^ \.ktis EWqAEPEPEPEPEPEPEPEPEPRZ^_ ]T4QEQEQEQEQEQET}5 Mo@EQEQEQEQEQEQEU#?ΪU#?΀*QEQEQEQEQEQEQEQEQEQEQEQEQEQET*/RCEPEPEPEPEPEPEPEPEPEPEPEPEPEPVUj՟W(Ro%QEQEtVmiE?( ( ( CE UǏoYa ki¥Q B0j;xn%U)$nVSw9 wHؖ6>?ccּ{Q-sasg 8]_]72Hzd̚( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3Ze>_PEPEPEPEP֝uZu€ ((((((((((((((({oa;?uG%'hFn_n߽?jͭc5DhrGU-(6n”8ΪѦR)UK0U8w|{q|;6R L̓؞߀kW^JUGeQBߞ\&1hZJ~']s4ЃȊef#Y{(ܶ{cJs3 Q^bbǽ:~M'}Zl}P˺MH.;Q`:F j_ȶ".FXIQ^u<]jt8J4^nйu8SO}]0ƹ)W.hx?k3[vwŏO,bhjfX3W"'13EuJX2A"][+gzfk9Q}pFv4QE}W/hYyȎB?TYm($\oC\غ>ބRWV<eAp=ET% 8Y(:*D%Q@Q@zg$i|?f!_67T5,Bzh,t-H{d0ZƔ,EdZy8>XRݒ5nƵV$??\ֆ9<ŏPJ<yp-{QMGY]2 E:RŠ(((((((^Kohn~ Muh((((((j!(((((((kGTkGT((((((((((((((U/T54_ ((((((((((((((Yªի>PgZJ((+J/IҋR(QEQEQE(c_< EJSjמ4y@ FO]jǞ4y@ztM}i b@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2/x((((([N]Sm׭:w@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW^v=w7!_QFaGzTb)u:]QE{'hmm^$QL o N&vGaAj#cqq/ ֮}Y* |\ T_QE!L!xePѺe=3R@dځ4<[&.ʐZݏ޳Opo rx-(oa!w`g~v&|/FRI۵2dd})ie~+}vFAEVC ( ( cy%WQ^3C*'@up0ea5iŞkwލe..Kr>᳚MZiXCt 8=Vk#%(GflCuio{ f>7l$~:+Z5{v\;#ç;Ax?ծGE𮶂# UrF6a8X+Լms#Kg'PYC:o𯗩b+(83[ojAY-J rcUNE*P~롯C .lC)Sah~J"yXMu'kv+(ЧBՑIl:j3ܹ8^^S==lr*snH[k QE|x2$s!_e$c{' ^!E}xK.yջqсcZUwc/[MCO$ '=\&ioM˞z>mAO֥REPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPt_L4((((zӮ~N]PQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEkJvu'WGEmF˚%nfFF*UPF Yo dxQ|ѣVӀ`tBYNvtE9=YjW)97)nVwaET+#s4ZK8!uY^!6I\LB{iF)SԺm)&*@AikM]RFbPNBz*ҫԕ-I.&JU[AEWs*;PKcRLiĒ. oLWAcXiu|ߙbK^RI?Sl˝QdEF?^QMmIĊTs1]2˹nTvY]o(OC_7(=̂(p)x1ٴG}04 xaqОV} :cQE0((((kAX@żYڵ҅X8M]15sHyd~ 7MBETHrO+dXKW'١jQF([bŠ(((((((^Kohn~ Muh((((((j!(((((((kGTkGT((((((((((((((U/T54_ ((((((((((((((Yªի>PgZJ((+J/IҋR(QEQEQEC@WO7 q6ۑn}$s9+fbY${_Eçd|@Q@Q@{GP<73>oODe?}䌏Pca@aAO֥REKqq" H^I*I`x֞ K 4g~ Yw+j3PNd3|Rx6\ţ0&e# ?f_|E蚶 ZGy-Gr?1sNRMs:[1k^#M})lZN:\s~a/?i\L7+"!"N?oץx;!C`fYO@4q'?CEMi?4t 7٤g4TfΏI@ oi_ t0:JqK<,dch7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?ꆫm[yr i6숩R47d4W7/$ ےOW[\ʱ^EbMOҶxj^ǝ 9) #3(e*Afΰ=B*oIG٤g J@Hp^asjbp{++sDEmkT}b)Zy`ǵGEycOF{]'M+_@;56[HIvq>j(PySܔV>qA>e10i]?O|_'f jq+ԆW*u;Gr-2oX阊DaX ߻kۂ޾kUե).Mo-Û{?0TmѸG&n5 ,sJ?ʽL15ɕ ;ٛH"Q|pujvnD0?1^i?Z%'WeiZo۴\<7\OH6xúEs7\$i5Oy1VCݏC_7B*59y3$gQ\/E5vwrr]1W}O: 2*x4TfΏI]c!4t}O:i?*oIG٤gh'?f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?zO:|0:Jq@ ]T5jxY1f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?IRC 18H@h'?f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?~_fά,l-JqEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g4TfΏI@S}O:>'?CEMi?4t 7٤g54_ >'?I.2GZ*oIG٤gh'?f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?*oIG٤gh'?f΀!4t}O:i?*oIG٤gh'?f΀!V}_'?OmGv9C@֒kw,O֓4TfΏI@S}O:>'?CZQOES4uq@(((4Pzk;[\+cl}ɬiVy݌w D :熵۽"6lI;0#}h2(( [m(?WUɠxfMb"EaB>M}j(>ԴW{D|4 e{1V=ЅbFX7f(ό>mX/F#Q\yasɩ'ۮ'p$1z'#yVߕWsހ8kFp *h~"n^-1 ų_J͜ z^[6k$QیW)y/mW5M~ٷGi6Q k.s^rA7Y%ߟ3^VXgK`[YyVl]p!F {Fݸ1 =vk-t8eǶrGdC!L }N7O ؽ4mtj}OVrAAh5֑Hrz+~=eZj.rͯkeQA?J CgvduϨ 3ZZ?B'>sFJ" 'b%zgl)a#?@+|-4f8TDyx`?_K`gM8VE\bc2kzpp_AʚÚI]X*Ɲr\!VfWJ.-J[q4f??bG"3QMz'4cY˂½ *؈mYpWfyeg>Z k`\/Ўڽ2j69:(8+tZ}*W$WIyu+$mW¶@v4>b .$+MB((((((((((((((^Kohn~ Muh((((((j!(((((((kGTkGT((((((((((((((U/T54_ ((((((((((((((Yªի>PgZJ((+J/IҋR(QEQEQEC@zz%nApG :}~`tP_ j/aKk:>Wz0_rFٛMN`C+5/ 30NiUYQ1d_J[@:?I3]LJ| 0JaęO$~ c^oVd!ᰐ|A{u(tjZ~-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\3-b!ԟQZS*rˌu?x')"280E [dupc?~w^Dec>tY۩WNu+rON0:SZ@zꪮca|sVT] G\U(koq}:Hlm`#ꑅT!<tu}k'FUR rFH٩zJ=Ӥ:=>tǫ[JJwC!) ͥ^U)*z0ϸG*+v®WzO&)iV:pETcպO5r+XB0\VC x" IE6V`qw d/a2ǽSڤƾUW< )sY+ѓhBS0HF8kQEz4BT)"QEŠ((((((((((((((^Kohn~ Muh((((((j!(((((((kGTkGT((((((((((((((U/T54_ ((((((((((((((Yªի>PgZJ((+J/IҋR(QEQEQEQE`zQE`zQE`zQE`zQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEgK4|_L(((( m׭:wM^߅AEPEPEPEPEPLhMȨqQ_]?!GO}9w,{=vaprvFԨ;1饶38Wc9PrO?Wk5Lc2Dw;W_AI:]C Ӆ(2k: 6z,VgfM7wZ)V<9( ( ( ( ( ( ( ( ( ( ( XGMԐ Of֮QS8)[0| w?ξJKMs/`龇 $ 'L6J14ǧʫ~JM8?Aڷzd" jh((FsA&K˹n%9y Pz0'[iF6#_=röyB PH :IJY$z#ԴmtUڥ5mlyOj~CG{++`+QQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Koj*z*wPEPEPEPEPEPEPS[t55oMCEPEPEPEPEPEPEPV<:V<:EPEPEPEPEPEPEPEPEPEPEPEPEPEPSE_jhK Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ZUV}_ IJ}QEQEV_YtP((((((((((((((((((((((((((((((_iu{q lր,QX:o|9][-V2>p+z (3Ze>_PEPEPEPEP֝uZu€ (((((MGðݹd< ۢVt4tr#׻eVAbYΘt$`/VLujoCIW ( B(((((((((((((T50Vz!(((cA30GqZVuiB\&~ qlSԖmd?p>~޷:9^|ĠQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE-[@ ujk߅C@Q@Q@Q@Q@Q@Q@MoP}4 Q@Q@Q@Q@Q@Q@Q@[_?C[_?CQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@M~U/P4QEQEQEQEQEQEQEQEQEQEQEQEQEQEjϫVY€+?o%+PEPEPZQOEf֔_@((((((((((((((((((((((((((((((1"-yOʿLקW{@>f%s*i/k?OxjkNPe%IqƮxԟ4RI@(jO>ԟ4RI@Zu¦… YgXiRMQ?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ_xiOsIp\\^.Z|.┬mN-դn|*?J:g}s,4r#~*m9f1~YTt{RpSSњroIGړ T$jOe>*funIXTiJݕmGƞ\*8$ ik h>,?2s\qn|.<[o-Wzx7EӀq##S6FI]C*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*QVԟ4}?hoIGړ*T֦RpӣGpH& ]T5zY7TLRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJRp\ *ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' TkGjOJ >f8 *ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ/RK᧬)z*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJ*ړ' T?hRpJg _' I.v1@_֒O' T?hRpJ*ړ' T('RpՅ;f((((((((((((((((((((((((((((((u OiwO.[۟1*+(.l/B}%W0%9 Bh:ȏ $֝QE/x)2 ( ( ( (%^߅6zӮ~Q@Q@Q@Q@Q@V~ic3~e;"*U(v^fˏo {-Q 3\N*G=~i6^+3(((((((k$@"f.Ř>Z^"m>K7b=? !5c:̢+LN1 a?ڹ ioeb3..0BGO?ʺ*lbVAEWPVvOJ1 :ghN5 -5sd④J#mza6ߘm<;$r[|̲RV~f䲳u+$ڧQ9Uԏoǫ_G8R Q[ ((((((((((((((((((((*[QTրwP_ ( ( ( ( ( ( h( ( ( ( ( ( ( ~J~J( ( ( ( ( ( ( ( ( ( ( ( ( ( /RCSE_h(((((((((((((*՟W*ZVZJW((Tͭ('EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPt_L4((((zӮ~N]PQEQEQEQEQEf뺟N%N$ח,93kclp1kSug"s{'mpmfIr!"ڹ=cISaQ :|l_PEPEPE VV< >NNi_DMEq7˝c챜ϭA[pkYeWt,,V֝uWMI° :}u NN2ZJ..̂($((((OcS<xvFEyR4r)WSR0Ac{VysaOXaO7^-ݤ< ngHaB9¨Z+4h[L*cg?s}8oҚlp"s[Ez>42\2wc֯QErϵN4ER,((((((((((((*aCSo( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( zU-]T55¡((((((jk(((((((yuRytR((((((((((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE/x)2 ( ( |W3f~^X]5axͧL9OzJ1#Z *%EW vї!~Zɮu#Pk]W.K{(`((((?ZԆIp20{9+KirO&5w{g끏\]|w^R{>1TַSY$9IP^:n.s34Դn`чQW+QcL+]%}B1wAEWHŠ((((((((((((T50Vz!(((((((((((((((((((((m׭ERZ[߅CS]*((((((*kh((((((*ǑU*Ǒ@((((((((((((((*hK M~!((((((((((((((V}_jϫY}i)_֒ ( ( ҋR+6TuQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2/x(((F4Q@w!ڠˆ/QqGEu,muU#U^ViZ *{߅6zӮ~nɻQE((((êioC?r;~#5掍:e8 k/SBH ׍?"p:|1I<H^G8UIx-7Ϸ[zne 2#[*KSD*oOf Ap2wc֯QE}]8FT#6ZQVEPEPEPEPEPEPEPEPEPEPEPEPSoǫ@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET֢m׭-¡~ QEQEQEQEQEQE5oMCS[t4QEQEQEQEQEQEQEmm QEQEQEQEQEQEQEQEQEQEQEQEQEQE4_ TA@EQEQEQEQEQEQEQEQEQEQEQEQEQEU>UZg }I@Q@Q@iE?ZQOE:((((((((((((((((((((((((((((((((( ONIIv~tֿƙ@}go'P@}go'P@}go'P@}go'P@aUbqig 梷^߅'v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7v~u 7Ԃܹݜ=[>OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΟ ,SCU[@ y2GGv~t]*;y?:;y?:;y?:;y?:;y?:;y?:Ha*K)ʑ5oMgo'G4Pgo'G4Pgo'G4Pgo'G4Pgo'G4Pgo'G4Pgo'G4Pgo'V2-JdgTjǑ@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}go'P@}I%cn^GUU/Pv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~u=f=*sU>P݋:}D}7v~u 7v~u 7GZQOE:((((((((((((((((((((((((((((((((( ֿƙOkiQEQEQEQE-])֝u(((((((((((((((((((((*aCSo( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( zU-]T55¡((((((jk(((((((yuRytR((((((((((((((TAP(( ( ( ( ( ( ( ( ( ( ( ( ( ( g V@֒}i(((('kJ/IQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE"I+4fh?SG٢jZ(/Ehs4}/~"4_Mfhg75ONa 7#v2/& (N1/z52UgTRyT~^]?RDK; c@O٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh_&=v(?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/E;B@^i"! Ȫx?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~a *Zs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Eטj>0֭0EḮPiM,Ha4a͌'zyj^#̣v0EzW٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~CzRQ@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢k~+xVi'FX ɔK5|ُ>K9_ʼn*(6_3|1f:hӕntIXQ* ?f^K@BVh/Ehs4}/~"4_MHR@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@V~y!AnqҀ.DȲH\C0׌T!x{dt\@Af"-Gοl_ˎ(iᆻmqq^?43|Opr=V!@"O8ʄJ=}k|*-c/ ϐ?MϿzM|;ֹZ$BO*f=Xs}#ƀ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( +amW#|gfA% X KSWdw< ±$ϵz=u]J=_VΧrO1LĒT9g54Q\' wPM1RpU818=sϢ~6KhΡKJ@r}hc^)]kV!dW[Pd`55ķ5l;kX1q3z+̴/kMk}Dnf9188 kWZo4y7ړҩ 퓞}wEyѴ2]KGuh]bRH ]_êZΠuK+E0Fx#Ҵ5{r^0q kWZo4y7ړҩ 퓞}nѴ2]KGuh]bRH @EsJ^ %$#?DѮ%ֱ_C`%1Z#d^e}?Zh3_Mw s1ɍ05Q\@|5ᛋ7 {t?+>êZΠuK+E0Fx#Ҁ=:5{r^0q> ?:v`߃wl>``U>z/Y:ޯZtwm$j GKFjZO4͝!03?GiME1ϩxzy 6x+ZsiS*Eʐ~_xxS:f<}'ѡ 7߅W&6pZ0GUG- zQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE5\a0g0u_YMnbI4?h^*m\2~U^7l4 ;Y1"8 qJGNWᚪ?JIỶLr#|ÕV]ox{Ww'`RrpOSP|T5O ʧtq(yJW<%۟!uu>$hZtqe>yHq& A" ;FOVoʀ='yӷKբSmºMk-=k4#G>^T>i>Y.ג)/3kEPEPEPEPEPEPEPEPEP\Gb*|w">\W7㮔xKS?C{"\Cj<|h~0мT" *3d5fzך;tZ%9n+ּoh[vJb>Dq9랕C~6 My!kB?v y2}\oxJZu/ې%XmqCW\ωڵU3? ?*$*5PG\W]X`?J$xPѣ4ooE1:B?|/#|5_2m1# 9} BUy;^?-F4]k_vSd @<HOZ [Vn1Q>>-d7KBxU ,d}|s:e^ [.-u_C3n=,_*|Sźmz?U>4i-Ŗ|NS;#=ʣkV'iaXҺ?!4h *+Lp0|(9䎄O_ mm̛LHŽ_jl~€;Pz^mkסQ?WC`"%++3ֹ+8U۳ nTg( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3ot 3Q5o2 <9@pyEwHוFaNC`H揠̰ܸ81N+RGEC+ # J cA9|o]-D55TEUQ((((((((((((((((((((((((((((((((((((((((,aA~RiͣB9; OKk;x>qP?*tk6:nX8F=''aEEqmݻ“C "V /iv.bAWGEOBfjZlSJJ/*MŰ3ޥaIaC+pjZ(.Z<>h9ٝAt?J袀8wk\I3 6 n3]iհԬ⹌qʟc~z|%S6(f#a%bđVP9u? ]xLrJ3 'TVvom pGUTPSFӵaoYs9P>Ǩ*-BͧiC1 .{$¶((((((((((((((((((((((( 6ڝW.qQ7NtlD[9I=Ŵvos M X{X6عG#{3?G]V6= ]MiM(({ ٢(iZ.[48c9cO'{mikEm }W{1({Š;W9u? ]xLrJ3 'TWGEEmm[C00ơUG[TѴj[Vq\T8O? Eb>t)v30bH+j(.<&V9%@+z ;t8aa#B`*Z((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#KÚ5m]moSt%{H삛h a gD5!1.Ĝ4Cl{?þ$4#QMݞ|fiϡ%7I?u}EP4=J:Tp V"&IjvQT\V]%K!0ȳ7 7}NhvV^q+gX1Qր/Q\kg+kk'-1ک_ր;Z*eGyCum K VEM,4h 3P:OJ}wVFIldDU+Wjv^i]?ݖ @:ivVFrV!s:FzxV PCEPEdk)м1 ˭jJqdo9?}+⏂u Nj>x4?,A:T?Z7uwPFy"/8(z+& Ʊbs˟=J?^=+'Hv,~Km]d(A Y=kY/&=v('tx[y:6ms>3'򮊀 +}_ OkյɄfIUSxgmfP3VLz`? (]oO;vֺmtu%^]30 =Ma5ͭRsn&e>z㞵z}W'sdh+?&m,DEv<\}<>xE'.6?^^ 5}mh4Kzt++[D?p+ڿIx/? n>翛g8϶1\/\I /VJk 2_2?..^m2!n~ ëj^)ΕK01F 0ꪣv;*~~ۙa'"FsX]^m"a,LZO8+׽zŗOZig&ۇgKƛ]Yt-`$ov*ܮ15¡mgU > j/:XV%l@|]Ғ#EIJ'LnigGkK¿[x|Gjdؑ8$qVW1~G^* @ U5MB'In3@ɏ?ʭ3)&q4@ҀW~Pط2Eل{;ZZ]@vqǫHGSE'o7sF(ڴ]=BaEry$5~(O̞$xw\k]!j˸cϰHԾ GHKYdEs]w~9;mȆcm›~޼PzM\S?@ x=4&Ym)$p )/~)cOtk?aT~ݲv Mv %k]Xu싷P[r ~O j!?Oօxn Eڀ=O}0 !ہ:h?O_ |Ywt6yvܜ;ҷP}Mǩ+/MB4w,it /7:_V~htVywܪA_^]/GHfm7&ws=[5xcrg%\c }@)hxrK}>5w.=>gFCPxh<8^/ۙZ=Ko)y*7OΏhXzfPa7>bARU8h ۳Y'?jbY|mn qܜ?ixܟ HsrП+Ҷ'⾰UA 6MR.ԴxBIl) YQ$u9(0 ǡH갍Y[Ϋ~rm~C|N.KՑw8zC#{~ԷbDJ dSe]_lZ%$#Nb? pO,qճAwR 3*%?;}.XKz4؊|)>15|(ea.OdԮ/v9d` $o#:3 ;RFqȯBG$R+CPh>'"K׼ aw̻Q"rH ӎ{c^6Լ=z-v緊100Tl4[ⴵPIMy}g~ sd03aʹ׊.x~/ԯgDVڨO 9fnwKR6,tʐHq2xk5? 7_u[kkH3.T!%tzKGPϚѰu;Z7Gh) -zwaqxC~}[U5=Iu)%l@rG99^xk_v2CP$r+:|/Qg&Ӝa}T;/\}V%K]H {|sω o-'t_`vq&P`0lr7:6Zu%2F2 QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEx4b;I!oӐþ-ĸS{P|`qnWq'ĺ,,vv `2ky⏡WU`No>Oy׊(4=_E_jWsۘnoǙn8^E|Ϯ;wAZu3Xo-fTPUH$w⛯x7}Wk Q:XxᎱxr c*/x־8>2>r}' 60Ԛ#qPK.0ǁyw/߽^WQ@Q@@>(' 2"6:a ?T0,<#p ,fr'{<`8w9`0<j/!мmhsd.vYYx r>?E/\X#1*wZ? ow-F7wW+<|}3^Exwſk*[ȶ5P *-hMF{Qf]h8bvmk(t~wMW^x;(S逃zǯj$GIKǐ #(H(8X^?sѲWi_ e[ۍiT!HnW>5xc ZҮkB,aIhr28^~& x2i:G} oHj ExjWQ3ؠH*t Lu_B@8߿>:0eˤnm Mz&Y~jVe4ΰEb8ފX^i ;I.33fr2ddMվk\\EmHrpj(<4?úۙԆT MЩ|YNK\ь"Bz|8 s|E|lC)I4ˑѝv}xd<a|5 W v3Ș*2~(?xmþ%zt.fPI'k)<3'oƣohZA9^S?:-)<5O 4v,beRI#COZFz~7ci>Ia+LK lp aFIzJ4{6LVۡ=JʮQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@.4M&RQ즾U̖ǃ'W袀 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?kalign-3.5.1/doc/images/Quantest2_scores.jpeg000066400000000000000000003370121515023132300211130ustar00rootroot00000000000000JFIF,,C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222a" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( dO,,qwsrI=>O"^3*w?Q ߃kc_/,{kQ-[XEh6s5i87”z~jޅiL$c xH$E 4jaܓx'#8q{oM% Y Q! 4QEQEQEKR4Tlbvدu:Ĭp V-nmc)PGN!ڻ߆L;^1*ٳIccW}H.T ;S״}լl<jH3Wf_ﬥYm{_]IcY#uxVS ӫ;@sKHZ4QEQEQEQEQEQEQE>kj6v|\$[qG*r$n+)`zkiѯo?\/PQ@Q@Q@Q@Q@Q@V,G/--F$Y:$V~jV:ӯm-ؐ&dBGQ$U/K-?o 6 ( ( ( ( ( ( 4aN=?B+˵~*,KWXlO"hawH5 >%2h-u[0|QS<sss?]詠?d[u=+O|mzMWZ6siCg~qҽ: X[ƨEI)'f>êx{b"ID;©`H$9^9gondg{#O͌g ~Uˤ#Ψ Ap+ th{+C !z2[@9ifլtiT=zŸK5Ig G[#*v6x3JORHeLkV5i6?׌ʸ/GD'Mg$ÿ=qk5}S:nLPݒ_T{_FvݼZ?h(_x"9ltcK2 Ӆ^q;WÍwud>'\x9to-('H3 omM>%6Tn`+?#!X88ϡU)C(e'ԯY$ W?^o^Դ#!dhqشM_3^{om6EaIaA$?*=27WX慾UuUNqHޫiɯ,l." W\|B7E[cO4Q0=~2|S54O"C7`Ӧh<5vժ! BrPe#,_T5𯅣T`i~c+>6)z|E}'W|IMGYd;v &{0phTx+u?m}v9Hsov~⧈?[wҠYbtV @zHD]O&;u"?WR+i|!k[[,s%Lzci[oGn4VI<,qF*I||RG&escP%u齝}1+~8j XG\x5=Um[KY"T d#@<7v&{{wo?7iZq›AԁهqiyCI\}gH<38NԢF?|0˿j(iѪQx?+JDrV0 !e(i# 㯵kx_ukվpxg gI r8 q'\x9kmo|݈ vۃG gЎٮT,/"Ym#hFF |EKB2F{gD [4|ue/BIG;a$z>+|\k6DlkC\oYI7R2JIG$@?JS*m*)V0A@_kMߍ\!,B(3 q"o>Я~q-I2ycˉ| QG=E{]Oi%bD f D-4UHG{cpQ76 YcoUa#^V;NԵB<7i<1ePwd1Jȁ襯|[Esq=SB4^>a4u+]$̦8D#JuQ'䟠?c?9\?Wh^-Bε{6_߈Hmm])h{$u*oea9 x> }2fQfCHm=5|9|G5Էщ9 "r7FQ׿Z~0HQφ2qa'O{z |a't|;qY[ʫNTt\Ծ+þծ4 ()M*Y0\p/?j\]|DO|"q8/`=+[WϊotKi& cbcxYXt|= ;Ij~3xzυ4t:TkJʻdo_4k%+4Y" t x.9kG]_[Yȷ[.&t(# FG"?t$\eyvgvF=?:π;X0{8ձB[MM3qF($ǿo.-":27h|Aj+?h,h<*8 xVFB$t}~s=WV񕏂t+g 1Q4(luP IKUx Η,jdt.ʀ'j~7Z-|_%$E$l+"t0:-4hTi%h>ʄq&>4W~m"iobA$qn<eX~¶W^[H s=k?떦+$ eHBg [8c??h_I|.Wu|'5խ$Oc=0/wvm=q^u7Sx.m%-i9嶲</ks=s CGQy!s#ۗ 0ulԴ][Kӯ#Zʞ_5Ixைnnx$%82J0^;Sr'\햙~VP~nd}9qW]۶y!l!Wu>:)*&VC#nx}s9QE|M} .z5Huwа`qca0"um9_"-˳>V3g5[xŤV'TFV݈5_]㟉AѴWdVvq}z{W*z<KI5[²]:r@{rOxk;{VwzA땵`s[c9臧{2!~<;n Xu dV,, {m⇎.hoqՑG@ QۯڷOzsZvɾY $#$׾Ru%P̣ ~7oZ}c?>2:U^#VX?IȚJ>ѭt̓͌yh_G=}'_?|]|]ѯl4mB8mjDNX  }@MD71V84HGU?YGX6e71[B0P›+;ᖉi}k5qxg_>"j5# {x'/z及5jJA<[n<xO ; [6if&O}sWѼ [wwW3&TrÒAN~jqmR7dvĪNO4Ie|Wמ|oֺ͝JL74nn^@7~=kj[HȏIT#úޏ=f}OG,ԪIsj1N`8>ӠPWh`iYWq2G>-__k۟Bὥ͏Ý [{Udd(rx b|lo_uiܜ(&g]+O^|_SыQ|t:egqir1hn"1V#*@#j;+Cិiem5̑(HaާgO'7E\=CV}<Aa D $`>l};dףҵW6ڝՌQW0LT`0}c'n 3!@0s܎8i<Ă}F`9vP@м;6FibdVaAYU䞧}jů --<'HwL|h?#=ǘ>ɶ8@DxIk*N?@௎h?Ѣ'Qs\Wҵ;>ntP;TWU>kpAB>hWd Qp/8rzc+> ¾(N眤K!Sr8+lc}!^巊|=eq=XH}^r1} {|=5]wVkm冩nݢ20}CFEu4QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUk^9VǥYwGϴQ 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(c WLMkR%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_-[&1MyVPڒwL{h~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li R pOnexvdzUX'Ty~e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e(LEEK >/AQQ@_TTPi2/e*Ē2+9Un__~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li ~/AGe**(_Q 2}_%Li 0#̪͐}P^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹU"y$qcT4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀!4z΀%TZW6 U'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'*oI?:>'m?~׿֦L1ZI`v@'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4h'4XQS}SR@H֥e+=7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t 7٤f~t [<*/I?:$lꃨ(S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@S}OAi=@ mW0:J@tQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?֪U_OQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE=™??~׿ր#((((((((((((((((((((O>*'Ty~Z((((((((((((((((((((kRkR(((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?֪UfU` NOd(PТ((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP]OA7\?] ww]_#ʏ#+(((((((((((((((((((kRkR(((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#(u*#3zei1,|~?Pjŝ^]az5kRB)#i*2eFxwE??.1R>ѧEWXQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWSMp;OWE&?k'y]A2/(=(((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQP^JXM#fQ@Ƞ_ۓks4] nesOtOm;dA:b;kGgkq_kR~YF2I'ZT*\Ϯz-V\ʑAwsk"8wY=۹DfWl]j"/>s@m."&Uܒ!"8]4WzX\+?2feE,*It Yk [J&16 3@K*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍP_OrdT?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?s2eFxwE??^3R45#^漸_HFctU%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@5?Ms+?tzM?oMs^`UKxQuRoʍW1 E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPU~oʗBHV8hQ[ &K*67OHBQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctTRoʍPQK*67O@ E.?%ctT%_6u$QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEyπ'UX̖.Pn#=kYh:u FK(cvb$?ފĞ/ˬhOF11tRk6fV-G9y92H 4Sp?<$OlO@USc^ohBvT6݉'? .؋+|{ bf\26?^}m>~>&v3p#8k/Ίt͸`ss$S@7 ?do?Ã:+ib훦i1ST֏hllU-}}VggZ(?G?Uh _llU-}}VggZ(?G?Uh _llU-}}VggZ(?G?Uh Pcޡg3im?֪P?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP'.<Wn?~9d3LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkGJonW>W/uλpOCz_ WkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZEZg3hg3jkP?֏?֪@LZ>LZE\̐.gjP^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@$n" Y$9i) WWYBMI;q 4Š(N (((((((((((((((((((/5{ÿ/MQ/5{ÿ/Myp:(BŠ((((((((((((((((((( o?&Ǖtʺ-OA7\?]0GI~GYEW1Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@rgw_I먮_L?v> zEQ\FEPEPEPEPEPEPEPEPEPEPEPU~[_U(((((((((((((((((((((((((((((*\wG^((((((((((((SwfONƟ#aEVǞQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEs2eFxwE??>2eFxwE??.1R>ѧEWXQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWSMp;OWE&?k'y]A2/(=(((((((((((((((((((_L?u5''Sޗ/C+(((((((((((z?ʯ o*@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@SZ?ݫNhv+EQEQEQEQEQEQEQEQEQEQEQExBv) WWXzz>(Š((((((((((((((((((({__ȿg5G__ȿg5F2G4袊 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (* | W*?Ms+?t>#FT}%eQ\ǰQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE5'󮢹}3Fuۄ*zeuQEqQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (9ON(c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ޓQ\#]:|= |2:(((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUk7YwGݰFoʡ&oʍF(mFoʡ&oʍF(mFoʡ&oʍF(mFoʡ&oʍF(mFoʡ&oʍF(mFoʡ&oʍF(XAYbF=;wf1}6?~Tmz7P[y6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@^5XƄ/Qj#-Axǹ #W;"ח?hoʍF+,lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=h =lѿ*lѿ*6=huU"|1&6W؏.0=y\A5<UKxQv[`QߕCEs6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7\X/8}3Fuۄ*zeu`QߕCEqmz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U 6?~Tmz7P@mz7F?~U fJ\銷T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP) WW\?'i]]cOb0+c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9 #W;" #W;"ח?hӢ+,(((((((((((((((((((&?k'y]Ay\A5<UKxQuQEsQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW/uκޓnoKᗡQE`QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((((((((((((((((((((ʧw@袊((((((((((((c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ޓQ\#]:|= |2:(((((((((((( mׯP^QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@uw;_Rp*?_إt5DT?kG/Qt>Iv&]E}>m?bbj*_أv(Iv&]E}>m?bbj*_أv(Iv&]E}>m?bbj*_أv(Iv9NK,rj6$Vy*sk]E}ƛ\= Tez:tdT?kG/VG.T?kG/QtMECo}~"A.T?kG/QtMECo}~"A.T?kG/QtMECo}~"A.Ti<26ԕ+RSMnQE ( ( ( ( ( ( ( ( ( ( (9 #W;" #W;"ח?hӢ+,(((((((((((((((((((&?k'y]Ay\A5<UKxQuQEsQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW/uκޓnoKᗡQE`QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((((((((((((((((((((ʧw@袊(((ZOحx?6kwcۊ򳌌/]N [A|Ǭ(`]"^A'밢Ssib_q/O"DߟvQ),W ?E?z?_׮Š=>?G A~QG?_/8A'"^ (+G"Dߟ/OaEŸ`?_ףE?z(SX A~?]{ }K#"^A'밢aOib_q/O"DߟvQ),W ?E?z?_׮Š=>?G A~QG?_/8A'"^ (+G"Dߟ/OaEŸ`?_ףE?z(SXq_"YL[όg+vuj>@Ҡ̗sLN~RQ[hQEQEQEQMD6F 2N ?Yέ8|rKՊtVg$ZOGi?ZAtiYi?EQξF ?G$ZOGZAtiYi?EQξF ?G$ZOGZAtiYi?EQξF ?G$ZOGZAtQ/5{ÿ/MbxVDV)$j=0jމ53]HS95½/\ܽɺ::+3-'H Cފ4H ?"?(_z NI_-'Pu4H ?"?(_z NI_-'Pu4H ?"?(_z NI_-'Pu4'r+JXT~Š((((_A;Zf(JEoW M6T7 cTf0w[ф$)<^ӡkͽ͟M4xz?4ҿo }@j3_bi O[h?OƏD4'ErfJףM+?^h?OƋ0 OG&W<߿m!9??aɚ4?M+?^M4xzCArm4!9?/Ó5h~&'&W<߿i_~@hCArm4_ه&kLOM4xz?4ҿo }@hLˋ S$LAvHK328#bwh?OƏD4'VPQq9̧Z5ܡxi_~i"o }_ٝC1?4ҿJ׭D4'G"o~93_bi O[h?OƏD4'ErfJףM+?^h?OƋ0 OG&W<߿m!9??aɚ4?M+?^M4xzCArm4!9?/Ó5h~&'&W<߿i_~@hCArm4_ه&kLOM4xzڲPKv-xwI=qskf̥6f8z?]fŸaq!KEW1Q@Q@Q@g^V2/VrG?Ujq]XJP6W5' =?'[?ii_6G1'G$#kg#|o#|o_^fcOa7?ƏI=&G#G'G#G'G6ozM|G4OƏG4OƎli~z= =?'[?ii>{3{I4Oa7?ƶ?????9}f7$#hol9ϒM49ϒM4s`K oI=&G%׎r`sOG4OƏG4Oƴ|,I=tTjRi' =?'[?iig̓/oI=&G =sH$hsH$h٘zM|{I5>I}7>I}7̓//G1'G$#kg#|o#|o_^fcOa7?ƏI=&G#G'G#G'G6ozM|G4OƏG4OƎli~z= =?'[?ii>{3{I5vSC e1'tds~Yu_U*t'FSշ7(:z(p((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEP;WSEr.;u:?l?V1+= TYv(<((((((((((((((5i ?}+h/S4‚(<((({\`x[:֗%ή6`{[o뙨ook҅\|#5u2[Mi/]MD>Tri/G GSE_Aʎ[ #zhHh; 9Q[Mi/]Mga*9o@4w #zk/ G-=n4[Mu4QD?_&@4w?ȃ G=n5GvrH??_&("Tri/G GSE_Aʎ[ #zhHh; 9Q[Mi/]Mga*9o@4w #zk/ G-=n4[Mu4QD?_&@4w?ȃaC6եo;~1[Ǭ_\o%kXʹpTOZVKUh ( ( ( #5r Wa\fO9>a=(c ( ( ( ( ( ( ( ( ( ( ( ( ( (9ȩwb!"?5o.O Vw?]f;QEsQEQEQEW3s#tWM\?]_f7~uEWQEQEQEQEQEQEQEQEQEQEQEQEQEQEԿ /_/.k_otQEqQ@Q@Q@_UmW((((((((((((((((((((((((((-OZֹ>]%u M+b5#:dԚKnI^\f2sHq#}7/]zUFo_+oW"h ( ( ( #5r Wa\fO9>a=(c ( ( ( ( ( ( ( ( ( ( ( ( ( (9ȩwb!"?5o.O Vw?]f;QEsQEQEQEW3s#tWM\?]_f7~uEWQEQEQEQEQEQEQEQEQEQEQEQEQEQEԿ /_/.k_otQEqQ@Q@Q@_UmW(((((((((((((((((((((((((g4Ϋ@(˴ J^#մu^8oM. c s9&]oKҴyc=ө@p3UE-/b8 BK|IѬ4+ M{I=;PUZ'<~Y?o~}\\}rȿȷ^ tyogxΛdGr SF{P7+NEW_?z-ZZMs.|Ql ^u?ž)գ .  ?:i wG"u`sO]Yx:Y۫NxEQadSO,<9m-&Z2V- P߲Y=:o%G%@,ߝe_}_Y=:o%G%@,ߝe_}_xض9*t׼g;[l*׼cVzܩQ[xQEQEQEQEQEQEQEQEQEQEQEQEQEQE)K+,ߝrW䯏tuhcGyz?;{~t}OoΛQVǞ;{~t}OoΛQP'Gdhh;{~t}OoΛQPoOޱgU >!F-c5[&Vʟ1_C(L((((((((((((((ߤ-u6ֲXOq{W-iIZZ/ __',ߝe_}_;{~t}OoΛQP'Gdhh;{~t}OoΛQP'\GFa=(c ( ( ( ( ( ( ( ( ( ( ( ( ( (9ȩwb6p2߿FxEKCGH+`oOC6?,ߝe_}_{'Gdhh;{~t}OoΛQP'Gdhh;{~u^!|u?hrl_ƶŎO/zC+((((((((((((((d_\WidێZeR|!#${N2mj/×>'Gdhhq,ߝe_}_Y=:o%G%@,ߝe_}_,6`{ժ4ʬPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPUҵI#-9eyrqǭ$}J +|+ zs^Eex@.vF#Xtaz\xV%|Mk<鏻~5PO,-$G=Xj(ʧw@袊(((|]v*t׼[o]/Yblu!GiUw=)RSW՛tVA QB<{^ƯТ4[b+أ=WVA QBsǸ{Bҿ!o}?4[bxcW_hQYۚW-GA Qj+ +?sJ(ҿ!o}9=_qEgni_ۚW-G<{4(+أsJ(p5ƅBni_ƯТ4[b+أ=WVA QBsǸ{Bҿ!o}?4[bxcW_hQYۚW-GA Qj+ +?sJ(ҿ!o}9=_qEgni_ۚW-G<{9MCGz.h$Y#gL2]]eGziROPQEQ@Q@Q@o뙨ooj]@7_7|T?2utQEzeQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@'o%kXʹ߈{MKEm?yxoB-Q^aEPEPEP\fO+/1]43?ޯGQE{EPEPEPEPEPEPEPEPEPEPEPEPEPEP?o.O Vw?]f|m"OC6?v(c ( ( ( ntJ髙G+,ކ0 ( ( ( ( ( ( ( ( ( ( ( ( ( (/t?_EK5Oevv9|(#((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEP;W,-`B 5<Ux\*5sܡxl$$fxI|>?5QU?~?!4Oi焟^y٧O<$ z}Uw~?4}'kר꫸m?Ci焟 ?^EU]i'O<$ fxI|(O?'h4OׯQGWp< ?G٧z>O?fxI|>?5QU?~?!4Oi焟^y٧O<$ z}Uw~?4}'kר꫸m?Ci焟 ?^EU]i'O<$ fxI|(O?'h4OׯQGWp@Փ^g>i?i*sŠ( ( ( (3u3Q|? Ժo뙨ook̩#d?袊 ( ( ( ( ( ( ( ( ( ( ( ( ( (8O{MKEm?se=bp*ȅ2Z(BŠ(((\CW_bhg_D+((((((((((((((*]ȻhEKCa}mnQ\ǰQEQEQEm]#W!\58ݗI.&CM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;+}_kƏCM?6khίT;YԿ /7/Jh94 /R?[~e9B(, ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPT֏jSZ?ݠ QEQEQEQEs.;u:?l?W-WSEcЫO՗hŠ((((((((((((((PCGzƎ= K((Š((( @7_7|. k⻹ Cq,jNp@^aXld*5w3{uWs[hҾ۟SxTWi_oJnG=_+4i_o~Q^+}??Ə+4o+}??Ə#Ex?x{CڨҾ۟G?h{UWs[hҾ۟G?jWJnWs[h߇?=TWi_oJnG=_+4i_o~Q^+}??Ə+4o+}??Ə#Ex?x{CڨҾ۟G?hue=bp*gǝ4齋cVXʴ1 U+^8;h (((\CW_bhg_D+((((((((((((((*]ȻhEKCa}mnQ\ǰQEQEQEliq]us7?:[Hw`U$fov~EA_z?뱢C߂*w8A_z?뱢 G~>S'<]hb?A;y?^A_zhC߂Tq?~'cE}bsO_ףy?^(7;w ߯?~G#XO_׮Ɗ?1 N<G ߯v4Qo*w8A_z?뱢 G~>S'<]hb?A;y?^A_zhC߂Tq?~'cE}bsO_ףy?^(7;w ߯?~G#X.gW^OewMpYu_[EJzhJttQEy(QEQEQE-_U~ ( ( ( M8ڍ{Qڷ?Mq)7VciymNeٻvʀ9]?a+7G.߇0|*Fv@+}>ybK!*ynW;O oJxLh#b|w&=_FjV_e7`ydpS]pvm/I Z^g6-Y[vPmF~s:u +?P״}"hԵk )fT7)?nwiVW Np9nx^<1qm_[#P?xD5\hkC܂cEt?hj(_r #'1:ֵ?},F4_CƋ@Z֢PE 2G#/kZ>C܂cEt?hj(_r #'1:ֵ?},F4_CƋ@Z֢PE 2G#/kZ>C܂cEt?hj(_r #'1:ֵ?},F4_CƋ@Z֢PE 2G#/kZ>C܂cEt?hj(_r #|qYic;t7mfoߤ-tVW16"c2Z(RŠ(((\CWoXuˋ3c ع9 M#*F'9$TO_ychE?Ʋ?E}G'/<߱4{ x?󨢹O_ychE?ƏaSCO?uW/ O?h~*(i΢>=O<_yQ\'/<߱4}G:+E?ƏO_ych??}QErh~ O?§~/(_?>TPE}G'/<߱4{ x?󨢹O_ychE?ƏaSCO?uW/ O?h~*(i΢>=O<_yQ\'/<߱4}G:+E?ƏO_ych??}ȩwb!"?5KĞ.5]{;o?rnL_ja}e F*fԥuݢ+B(((-ҺjntJr 7;(((((((((((((((K5OewMpYu_]?ݪ3x_#+(((z?ʯ o*@Q@Q@Q@Q@w?)οf_=w_覯:K"ZPF#W-r}ǔB*Q}W[[~5t?)-ľ4g7VO@P +ž{OmֆYdW9z,O?4 qOs+~"l>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4<-wK4%:Oƽ~>>?ռ7K#?'G"Zk[4(((+G+fGKo/zC+((((((((((((((d_\?]}q/t?_ECگ7:z(((( mׯP^QEQEQEQE[y {hdytۄ(Գ;{WGoPu݌~]R|5TP_%Q݇ś#N]nX%8en@g|{VH]>̊3XW7/ݨ*cZJCtaH8c'khK7|mVKf H'+~5QW@ csĞMDQ]D8$qU⏍4u>M?öV.l 0 B>&x|qӠ) ݸP ^[|\4? ]"}[I\鎽kZ(g]vt >WqmmC kj?@h(((((((((((((((((((ʧw@袊(((|]v*t׼cňc *NH80la*IUzξ F?73G&jL/GoEqό_{4}be{~(#{4oy>1~fSOooy>1~fM?/aM?/ >L?=vW ?73G)^'ފ?73G&h0Q\9e_M?/}/GoEqό_{4}be{~(#{4oy>1~fSOooy>1~fM?/aM?/ >L?=vW ?73G)^'ފ?73G&h0Q\G&h|bXxߊ#?}+2mS1^4;Y һ:TY\=^((B((( @7_7|. j/[AZ*:(2Š((((((((((((((7~[Ǭ_\o%kXʼ7w!|L(P(((?3_'vFk?ÙxoW#(=(((((((((((((('+;.3Z>6RgxCEka۟qEW1Q@Q@Q@s7?:[Htm]#ہoCwwTQEqQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ Kk˯Z%R'ֻhU~fGOEWQEQEQE-_U~ ( ( ( ( ()N°Z}Jw*\²(>0Ǭ_\*8; .dQEzQ@Q@Q@q?+?3_'t0{zEW1Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Ty?1Yv'+;.3]?ۍ(`(((+G+fGKo/zC+((((((((((((((d_\?]}q/t?_ECگ7:z(((( mׯP^QEQEQETSj)k5)a'?Oҿ{?湠;n^t0i'OόƳMFVXkM2H؍^E7FMYTI?Sz0!A1aOόƻ))1/A+8#TM`[l:34Ě( ( ( ( ( ( ( ( ( ( ( ( ( wW*W((((((((((((G?٫Swf1}QEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ tIchP dY?ZTVsNSB>1~?ό_iQQjȾFo#O>1~EVAdf?ό_?ZTQjȾFo#O>1~EVAdf?ό_?ZTQjȾFo#O>1~EVAdf?ό_?ZTQjȾG%*I[$oܾ5oDti#yr{i|e 5^S^t(RДyU{ye/#O>1~Ez?VEY?G#O֕}Z/>1~?ό_iQGը"Y?G#O֕}Z/>1~?ό_iQGը"Y?G#O֕}Z/>1~?ό_iQGը"Y@ҕ0z֕V| /A( ( ( ( ?Rw4vݷFVE< GUBb\%sTl?Q @&mO?c?%+CSMG'a(c?%+CSMG'a(c?%+CSMG'a(c?%+CSMG'a(c?%+CSMG'a(c?%+CSMG'a(c?%+@@ӡM_Z>n?ֺ}OA7\?ZFT68SC ?~n]6ɨTl?W_Eg'vCA/_r j???G}CA/_r j???G}CA/_r j???G}CA/_r j???G}CA/_r j???G}CA/_r j???G}CA/_pZ5 _M[DYCd`+REDd$2kjR$cb}JUZnQE`zEPEPEP\*xݘQ$dpJRPGcb#nz~5׃M&5%>cmCm;5~&^a߁~?Wz?_?G%/}~Dz=~~ 9)7v?m&^D찿aKmCm;5~&^eJ_m?(mCqף5~{,/rRo~i?PGm?+M߿ףaÒ~cO >i?P\w"k?*K:>vЅkHapMƦCT-FmCm;5~&^X_1rRo~i?PGm?+M߿ףaÒ~cO >i?P\w"k?M߿ ??;?QO _?G"k?X_0߁~?Wz?_?G%/}~Dz=~~ 9)7v?m&^D찿aK0-qYu_OM߿iiZBibB%24#ʅ:3%vm0j.4p((([o*T-ׯQEQEQEt}jZ>CC`5@v r+Lw_óƓq2kwH_JjN6VMu|m3yj#G5wG'>e?6??ƏMu|4;όjs_w6{9Xw*Ю<2 yH6=I{xƾ.O >Iʞn,+!%[iFqQ+8' =ȧp^#Ca}, ,Й $񓓂'>?Ǎt1Zvz,ylNp12r st2됼7:R uͷD2?%O9[ǟm:iڎ{7/WSN0F{??(djzڗnZabD"6a@r1kl"!]:;d##9ճ$xñj.nf[BN:ZŃE$uB7&XfTO+ ,}\%ǟA _|AK x U"Q08 g xI `f|Lo%Z[eG4Ϧ0z k^5 j+| @i?,|)\:Fzq;K+5 {Y8 FGjM,5#?$H ( ( ( ( (+[yn&pDݏEP2O@T_ZO1$ IlzWEߎ fO-A UOxt-t&d[w2qP4W:ܾ/3 jnNѸ+⾁+<2wR2@EQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?'i]]r(5uu?GwŠ(<(((((((((((((((((((e 5^ST|e 5^S^\?c/}N(P((((((((((((((((((( ww]SMp;OWL?/?eG_QE{EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\#]:+?kzO]O/^QEWQEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( >-E?Z4I4,{45^Z .>"l'+*.=jW唦I-SO_G?ֿƹoC`?Y:戚JB3IF1SJWq& VjSO_G?ֿƹ+5Rt ?OS:4?o+ӮUom%\\?֗?OVE"G#Hd9fn(JrT燧:rmtnAEny\GPxwַi݁ͭcB8 ?Jw7ĖJ'lΐy/HIZgOV[-c7NpIU'q$IB9mwpx Bo-H'n"##p'EXxNER8`#lQ^Eyïמv~oHb4)vaX1cNgt_^,Zhs$)I?`FIW#=k(']-{]<X4q$V툷c g|ewYZU5Q@yxU"~Mcp150ɟ*Q^u655D1xIvA8ǥzQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP) WW\?'i]]cOb0+c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9 #W;" #W;"ח?hӢ+,(((((((((((((((((((&?k'y]Ay\A5<UKxQuQEsQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW/uκޓnoKᗡQE`QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((KMr #@(G@uf(# uM/ʍt~U35ZTj*chm!T o|_R3nz*?*7P"QQEQEQEQEQEQEQEQEQEQEQEQEQEQEUMSOV/tHWWRtPο Gg:y4YmWHے21烚}:Ez%?l ?!?ľWtlA51BtQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?'i]]r(5uu?GwŠ(<(((((((((((((((((((e 5^ST|e 5^S^\?c/}N(P((((((((((((((((((( ww]SMp;OWL?/?eG_QE{EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\#]:+?kzO]O/^QEWQEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( >-E?ZEqڗY-buu< @yͰѓޞDsO7CkfJ؝_k?fj:DGsҸٕyB)=M>ܒOsV+Zº|ecP8'&o5 ΎᏌ5 :|@[%֠`>LV3*pnjIuQ]Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@G<[<( RVW-&<%[g(빣`?S@ q/WN}/v; ̎G8<n3 mc} o;z|ďQhI:0P#ֽ>o>4Y.zfLwp8>h`x\-E?Zt(IC0SJ$@B r+HW] QjI'$:+HW???_*pΊGSErG'GkQty\A5x E]Εqcq GuHQq1T|G|;O\rROӒAE{%y#zEx<^`=umfSdOc_k[u:?,g~uB|kU>hh%qž[XOԮ@* 8]u_)5/20$\;pA?^/E|.XwTv|Rnׄ`gpi}3K/໵e5))~㖻>ndI/ ml\P><.e|nO;O8((((((+;Ip${('5ɮxgR]̏Ȍ+{o0O3N_uR<{\|?m6A+".'< =xl2ɻF3@Mுz/mF7$c&037.Gl_9!\jOxzy|B{ "<3FFú#@%FEb@a8}_9X#|Iem5+|mY݉p:knn"w H]U$ &}?c7?w]7.<5i>ժ4ins韩<'ㆱ>+jE|ϱ޻s_ڿ1H=d@zUrCEg@\(lv_~m$lFP{M65$0(SZ?ݯԿ^vŚj:ZNB4 s@4UX:E[,?e UX:E[,?e UX:E[,?e UX:E[,?e UX:E[,?e UX:E[,?e'wfFaNFWcX:Ɵ#eJ*c,?lyJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?r~2eFxwE??@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|t&?k'y]Aխ]9t3%sc/_#ʏ#?Ώ1(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tRX:>@(e|tR}3Fu?ι &5oݡ\FJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?T?ΏJ*c,?CmWDp"(((KQOGր<1,4e@35ioy&"A$g`p{rPq3??ƏEOEgm?gm?w<'hZ%OEOEm~gjAOGAOGv-:D]!vR3_Osj vz<FQRilz: (4KX;IΜc[cRo#nkj_tC=i ~ |4$!&->y%ζ\|/ye^'c~+Qw{s0p'-\[qK&Rqf5V'+_ PZ3H0 0Rx%O_WIxL|L[Пs-$Id$JGrsZVl[, y 'r‘j1յRvC P$ v;@띟~bl Us6^6KpZPl<'v=~ae)V8%Tn(%'#/O^C[.'13푑sp=5?ZgŽsOhheR< 'p_ >(hޭh"m$ԧif%ٲ;8ǥv>Ih5˹u|sutIXT؜pGGö.<\OvQvEPEPEPEPEPY"UƭBq-՜!0̅GkN>(m4[ Jz7na=X Ǿ=?^I,f'2M IO'>| }|de$~@xM|-g*]]Gy@ql,%/ke ~e@অ;5(cT Gʙ<}^@Q@MJX4 }&>59@'*?]Ť6|?X'|+c~u5ϛ`CE $w+r<ھq܏3YU#u[҆.۾]5լֲDA*tq¸Þ~۾g ? _ KgfЀ z=Aigoai) .ԍ=NhvU;EPEPEPEPEPEPEPEPEPEPEPEP) WW\?'i]]cOb0+c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9 #W;" #W;"ח?hӢ+,(((((((((((((((((((&?k'y]Ay\A5<UKxQuQEsQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW/uκޓnoKᗡQE`QEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((KQOGր9-C #2A[5~#bxpqckk~w[/1sAyW;,<({7Lk.-! &_5SP1SWD2/*5o?]9Ffn:tJ+z8 =sӍj)l](((((-@!uXv=+(ir9wLUmZu|Fv854PEPEPEPEPEPEPEPEPsȱwcI?xį5Ooo[>2jw:l//^#v^Ak_WOFĚFG t 1|6ѭJmhE̾Pi^,4{e 9\dFG"OXyj(L½3Is y˧[LjMlEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?'i]]r(5uu?GwŠ(<(((((((((((((((((((e 5^ST|e 5^S^\?c/}N(P((((((((((((((((((( ww]SMp;OWL?/?eG_QE{EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\#]:+?kzO]O/^QEWQEQEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( knJuE?Z7Q/ʫ@7Q/ʫ#㻉Xt Np?]8sDbV5{?*7V}uK3Dܐ*KVv7ܱ/ʍt~U^EhtPEPEPEPEPEPEPEPEPEPEPEPEPEP'!oj6wn7@'p *: xN1.AIg9 ]gL&ʣWL8=@=x^Wĭ#KM.o0BAVp}%W4wefFCAx[BOY >D3}X:Ş|g +U3a* 8d ;tߎKEix:2]ETs?Xi+[Q@(#¼rSװW|Xu-cg4&W:qnrAKMsu=umfSdOcᇎu_ˬh YLadad #Z ⾯PN- l?Z~ ZIaV^{,3*Is'ޕ>.|?ϠЅl7:֫S-盧})$7.<R<Ш}+<)J#BIs1ie⿎m#ִMCO\[GsqFN}=d$!^h/hP;ZM)1" qH#O]?o|IFߖpU.*1;_r>2PWki~:ֵ&ȹWG@NGG4|m|MV]F%Z3q'.5Ut{+'mㄿ+4G֭-lu+'ึDE]n3'،s-BU}~SY;us~訢(((((((((((*\wG^((((((((((((SwfONƟ#aEVǞQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEs2eFxwE??>2eFxwE??.1R>ѧEWXQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWSMp;OWE&?k'y]A2/(=(((((((((((((((((((_L?u5''Sޗ/C+(((((((((((z?ʯ o*@Q@Q@Q@E?Zyޙ<6.$T1$g`p{?45_t.ۙB3U]tjY[Nay jIɻu;36Ə36Ƹ@-_V;pܠcPFoK.?BRIyFFkh.-! q .E̤-[M}.WPfh\&RI}'4@-_Vއá,qJ4 3 t9JiSZTS}AErW'RԾ.>,{EZcwa+1R;yEs>u<)sU.?ZTA&y$ekR;Fiaw Oק@Qmĺ~8lAݟ3? uxKX: Au ZwQی'9zWQ@h|K;TH݌?0*ؚ<+KCo\hBDd^sh|1GXwt[+Rto-qH|UƹaHOj)oeon!NYʨ pp;u_:.{k.٧e.G<8j#mxPݣn[(9ܷ|gƀ.xzxr[hn-keA ޕhi%ZK{xr P=P'R>-+{Fm(Gc*W|=#_ 5hUqG.nasy'y +~1e',OË;ڮ}8'^+{x8b@P0 ( ( ( ( ( ( ( ( ( ( ( ( wW*W((((((((((((G?٫Swf1}QEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@~j~kˇeԏiEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUy\A5<Uj | W*%<|GK:(`(((((((((((((((((((+?kzO]Ergw_I T7(0 ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPQOG֥P Ut~To xJ~ѹܠy<+GF?*sp0a㉤OfQ8N=@MV7Q/ʡE(^/ʍt~T tQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvUKc(' S7OFS7OFS7OFS7OFS7OFS7OFS7OFS7OFS7OFS7OFS7OFS7OY֢t/0.esZR.9TwayW mgЯ;SBw6s?[z;ܥәz}k嵮z؜)]GN?E;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀE;ctTloʀ9 #W;"/ r5{Êd)?'.1R>ѥE;ctTloʽBN?N?N?N?N?N?N?N?N?N?N?N?N?N?N?N?N?N?N? zo?&Ǖtʺ=Qi7)P5x I']0GI~GYE;ctTloʹ`mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mQ*mrgw_I?r`'ŗC;'Sޗ/C*67O\FhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPhloʍPׯQV*G^z ( ( ( >-E?ZEs$Q xFFA$jIuObVi=56T&ζ,P[d;N3bKϔƨzֻDdֺ0:+XҕFif{(YROM\|^!"[4!(&6%??XUk}<ѯG/GEzy|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@jHt\ sR2Y^mb3"*5?Ms+?t>#FT}%/|5{|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|4y|tP|5inŷL;N?kzO]O/^YG/GEqy|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@y|Q@ er[_U(((*)Եt}hx?ڼ+tOu0s=og;zx*8#9ICLs|=7yk{:*+Mw??3?ƏWV}CA_Sn_W.]Mi4K獔e2G֩<ֺU)ϝ[cU FtP: +9+ɾ)x񗇭t|nF HEz_ hXҮ-ij§Rzg`$ ϹWFar(O:y<|r6q@+: 8N&BP@9Rҧ:|zF:B >s4W=NF.XMZI6 "7g;oyTd2QQ^3IĺTiQh,ey(2u v7uIo:5_[;_1V?a?ƹ95O2#Oi?5gNvmWh!Sϼtn, +_] b}ʭc{n_]1sxYߦҕtP: +9¼sHOgux -4:J`~P@x%k;M\xH!i6\^[<1 U x#LoKTevCfB;Po99?j;XԴѴBAheVWqݞ;m~&WudII@8F#q<ZԠvb̊/m_|Ҭ|Kws(kyT&T|Sڸ;~!uWҒcr珕98xxwᆳ/-Qu5}FeDt=CG<@'F +M?ķa4HŸK< /#¨`{mRQOTG'g ~BYëj5ĒGshۣ {/*1iOrNI>/QEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?֪U_OQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE=™??~׿ր#(((((((((((((((((((( ww]SMp;OWL?/?eG_QE{EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPVd5W sUܿA@((((((((((((z?ʯ o*@Q@Q@Q@E?ZsWK:OWIEuG^*FDsBhɥ$,4a#b.s} ٣QF@62Ii4ZNkRS[E)p]ch"Bm"*zR2W"z~jiZTZTN\䖭 +:#)]*eQ@(s0((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (-TvU((((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP͍|.}H 藚- #]1[O>*ڋFs ֍wooZ(:(((((((((((((((((((kRkR(((((((((((( mׯP^QEQEQERP -݉Q??*u݉Q??*u݉Q??*u݉Q??*uQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ڹT֏hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?֪U_OQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE2YR^Y*"b{I?~׿ָ_l 8RT3*}/W r[]UW܁+̎qu9.zήvtQEzQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@b}EOyPG> QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEn__n__ QEQEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((((((((((((((((((((ʧw@袊(((((((((((kRT(((((((((((((((((((|L8RqgjGEC+ zY֧)Bj +<1ad<K.*HPGJO򯏎Su9~} y·OW]2eϘ!@Y(ǖ)v7 (((((((((((((((((((G>bDC1/-n}96?*v{ޣhEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW((((((((((((((((((((((((((+-!3\1#QPʎUybO_rH5M9Usǰ[[r&'3V=B3PUbR9Ǔc9SnwaZi{2^jrAႌdؒ?M_+$f ۷g?ƀ;+e]>z\Qf9;yHb*\wG^(((((((((((( vU*ݯJ((((((((((((((((((((DZѮn#8( } g5Nr+YGiw%.'̀`k>[Y쳆~moזXI$I4$1YHkcq\c\6P\cljM_Q(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPGA) by+U!>KC"c]A]9I>06_qBGb3 [TQEzQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@)>wL{h:( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (/Ҡb}EOy@(((((((((((((((((((uxneS8?&x 1< |g}0+66 NJ힅*M,/-ıdcSMZW…p ]Uy*WqՂAEW9QEQEQEQEQEQEQEQEQEQE-_U~ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( wW*W((((((((((((ݯJkR ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (']S'^Z}™?tQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE:?=_A/Ҁ+QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQı0Ic=xcU-2 ~eV5 쥎J#XQS}Pj( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( /yUJ/yPJ( ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPT֏jSZ?ݠ QEQEQEQEQEQEQEQEQEQEQEQE[TZV?֪PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP dO]S'^Z((((((((((((((((((((G>#XQS}Pj( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( /yUJ/yPJ( ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPT֏jSZ?ݠ QEQEQEQEQEQEQEQEQEQEQEQE[TZV?֪PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP dO]S'^Z((((((((((((((((((((G>#XQS}Pj( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( /yUJ/yPJ( ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPT֏jSZ?ݠ QEQEQEQEQEQEQEQEQEQEQEQE[TZV?֪PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP dO]S'^Z((((((((((((((((((((G>#XQS}Pj( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( /yUJ/yPJ( ( ( ( ( ( ( ( ( ( ( (%^Bz?ʯEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPU"/ !q՚w@ ;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ&;}?:>OΡ/@#`H9=W;}?:SjMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEMv~t}CEZ,ƒXKJrm™?OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh OΏh rp}j[˲ǩO>(goG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoG4PgoV$Bs*[<(/(o(o(o(o(o(o(o(o(o(o(o(0XzT-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP_OnSjQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOiOO߅2袊(((((((((((((((((((t*{ϼJ?=_V((((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP_OnSjQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOiOO߅2袊(((((((((((((((((((t*{ϼJ?=_V((((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP_OnSjQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOiOO߅2袊(((((((((((((((((((t*{ϼJ?=_V((((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP_OnSjQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOiOO߅2袊(((((((((((((((((((t*{ϼJ?=_V((((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQENhvU;EPEPEPEPEPEPEPEPEPEPEPEP_OnSjQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOiOO߅2袊(((((((((((((((((((t*{ϼJ?=_V((((((((((((((((((((rǚTrǚT(((((((((((([o*T-ׯQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW|Mt-^Lh FI 2t R+Rnj~z﨨Ts^^Vo yǟ^Jm."&Uܒ!"ʧw@袊(((((((((((kRT(((((((((((((((((((( ?~׿֟iOQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@b}EOyPG> QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEn__n__ QEQEQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((((((((((((((((()εYzw(^T>g%fI/Jxw\ym4/nǨVve@J۷^/+ 4)aw©9;u.|T2cت}v髣d8óvGyߌL$6*I^9=獏S8]4WzX\+?2Y$Hi$uD@Y&_13.GC C[Vb4Ma1ր3/ ZGTZj: GdH\+Dk];{iZmH@Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP>Toʏ*O7S}(ctP핖6sUo*O7W!ȅ!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G!~TyRq*G de%H)Ff!T_iP86KI _ʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7EloʓQIo7Elo#q"vjaB38G'$I~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI~U77ECI̊@1LctT)X8)Rq*)}qsimKHdO8F1^Zxݭ E2CӸ88d>mOH(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (-TvU((((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPXQS}TO>(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (-TvU((((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPXQS}TO>(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (-TvU((((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPXQS}TO>(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@v0ʦqk#x`1< |g`Vm{ld= TaʛGij6XSV6~U^F*U;u`6QEfQEQEQEQEQEQEQEQEQEQEKmWꅷ_(((((((((((((((((((((((((((((ʧw@袊(((((((((((kRT(((((((((((((((((((( ?~׿֟iOQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@b}EOyPG> QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEd(q,LuޱL"3 v5n__X괣ȶ6yY^lcSۗSz+s-ٓnN(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGrz( ( ( ( ( ( ( ( ( ( ( (-TvU((((((((((((((((((((({O߅2)GEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPXQS}TO>(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@[<*[<(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@_UmW(((((((((((((((((((((((((((((wGr\qڀ*QSbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@SbQI@Tlalg|[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJޒ[zJ[O߅2y!l6Qۋ(6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-% 6-%*{ϼJj7 ?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEy_įk^eܤ0\DE1+#z٢k~%x'Z76x^P>٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-|L֯/deXnMDX$`vٵ麕 .#-#'qĿ'Xi8Iw 6~htU2w Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/Ehs4}/~"4_Mfh?SG٢jZ(/]caJYccPp}q KxOfgY Qc+vԬ=F ,@kY=Wš~hʌ$ wfs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_f0ߌ5O➣}6 n8J/zWWxo:֙OQal'фį_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴP_fs5-٢h4_MKEEh>?SR@}/~EԴPk hpGIEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE#2f dp.ml 3\m yHsP[:]Eg<)՛_#_3@:ȊU)՗g}iz{ۨm#d\d֚V 29PU,+y.bO"1%z4 w,Aա8#+m7^Эl5h"SuD;Wr} zw=7@yUY"sS"MhUhu +ma$}@9N"l2NA⯠b=5AP?P~W;z3+4n(((((((((((((( zIew+mf@BAV+>Ȼ ]ĺYkԘ̡=0)MM,QWv j^Zjvwr) ZX{y$`ܞ~5|z1X;W-,YSrZYw7uPa7P$G4.2F#IYimN'=J>kem!IX.U[#wڙw`W.s:o7p}&7ۜހ,QYk=Dgo͟LQxX3AQOBą\dբB$-#ּI_I5"'"ARi^շn/ү0NLd zr0@4QEQEQEQEQEQEQEQEQEQEQEQEQEQEW]Bnk]n$ErW9Պ;K^+]eF_^[j}2MY$l+)# {78f4je}my2}Ҽ_wv[ymC>Vk𮕢k~4kt%|d/OBO@U@3@hoi[q&hJ#PހԗWv^_[d~l7S@(irmuK(gb"sWDbC#րEy6Mu]F2)V6 '=IX3tF}*_.[i8gpr=j((((((((((((((u}ge 5G{z >^m{G6j1\"?Bs@T7pyW0@-//2Aq8 (z*qZMwoyw)95;2b@$@ EAi}iA]AsvsQ]jm;FCfPD)K,ȒF*"suog ("^W ?3@Ui +{goQ<]Y6:Ua\ ) =B@IPzU줟8KghQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@y:\<6a~7c={WיxoE~)kjNɒP2sŸ}(CX]'=1%ÐIbx58ߤvsF3]&LfԬ|b9w@Z K#τx?2 Dz.5/%w6_Z/}7KVIKɂ\<:{.O|T2Mty7VL%f Db3QSR_X[Eq/o<FN9#PEhBn7e4۸oJƩlM O@:@l:Pq 69kF|1ɤxcNl0z1֠̚(((((((((((((`5Q.=b=mQu/@Kj5F~X_ =:E纵IegfnO_RB:NeГ~8ހ6|O9CkR( tJQg9Sƚ7Ytxu8Vw 7q:ULX|UrcHd.9XةČ'mB!14 z95ZHY+e RuAն=Mnx_׆Rh#Q@=Aǭr>~/.O HrDEK*P1蚺kM ٠uHފbAYC^'A4+yO ^5ͬ&TJNH9'92NwbHΡb ?(5w&?/kѵ߃V(i7X]`3 uh] mQ r: CFѬUŔ/$T8vX\)OAWo] -f#IԳ@3@QEQEQEQEQEQEQEQEQEQEQEQEQEQEyMgߋzޡ|F*Ņ7ǏCLg&}8]asMGeX-hչF|dzR|?Z7>.Q1ƚ& "=?N77,Ad?W;y?6 ʹr*oI>Cc>ǵE2qyn- MBxu#Hh4? !pSmE 9I$_&JGotR"ǢL5¿H5q`GnOxƀ#e2[Zxy8H&xKxK=Ȓcc*{`gsտO=8]P|/u-,iJmclYRU dg71ٱXEgE*mp ob0'=ksz?hZ|1]]35LR9_ok?[n5i`,עWx%TA'3].@֫4F< pGl,Ԯu32oQm~G9' ?t ´:ome&la< ҳ$>!4i_z8s@*+XM0cP3REPEPEPEPEPEPEPEPEPEPEPEPEP|]%ϑq3%艘4Ɍq׶?ƫ|X]/5&͵#'ŝ;Lm0vWb@X㞼C"d :-Ww/ycҾ gSZ *c?k'[ Hk剒;;? Zvhs^WL/\phS z_M|12[,-݀Oe_iyZ޶|E 9XwjxGsxkS{]AfѮF`Xd`L⹝#NO(5]LɶW*_Oa oL_t]q%":֤fm:*+)ڌ9=FE_ɥ_Yj O]'aiO#ړNF xY=dԵ+B}JZ<'9 j>'-&~"ד:/=)s6Z!~K0nĂ?*YZi>u^=~LRBsLK~ZixSJT@Ah vWgϞ_(Kv#Ќb<k 84rbr ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( {ckڽw?ގE X9Ox^\C)?Jմ{ rj0yʐjYCO/q0s׏Zq9'%T+?q]Ugiz~6ih*GAڠռ)kyڎ g*{ ٢24 46$#f 6?95>jvq H*} Т3t.H:T6Q0>]NNNwg"@5Znqy=%d{dN:Ecjе|GMiq3b=H&xcEИC',rqZPEPEPEPEPEPEPEPEPEPEPEPEPEPEPvi 2?kzf,*=P E#g[.S{7q$t-6mrjKlۅۜ޴h c]B+Ip}Ga {m*̇(^Gp$gWKEP4m?Bo[/{7'{V]߀/{pͣ1Ll ):(=Ym=OQjƩr 2r?:Ң1 Zm;MHǘIvر$U M-5+o2DlEh@[_[=4r(`Xx¶xhK;3ItP*PP0KEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|`&\]]ʹ IRC _oJυ>//neC,<7rMxǏmhzrY\y]~/o'yCI\72WkO)xpB+C'ðhԓKVǃmxBNxDY㳻mb0 |Č(ϱ״}NkM?V>l6)#ǃA$s4EgZE[.>LW(s׀%s_Ҋd3Es M,R(dt` ӭ$!g='8%&9Ev][Gsk x d4Pn;{q= {W|3<fOX<+rp@JC_*7hk?5 WFL csHOCҀ=(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((;um9_B-˳>V3g54P6V`A w8I٢iOx >'|clu i׿5Di`CN hˮxV贖5wE\@kϾ J_ |taLO@&>D' A*kP 3D:zҽ.ZJZˍHO-B/O?E/\X#1*wZ? ow-F7wW+<|}3^EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEt _GI7ڍykB(ciZ\MV̯onbS֯QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEkalign-3.5.1/doc/paper/000077500000000000000000000000001515023132300146345ustar00rootroot00000000000000kalign-3.5.1/doc/paper/kalign3.bib000066400000000000000000000133361515023132300166500ustar00rootroot00000000000000@article{lassmann2005kalign, title={Kalign--an accurate and fast multiple sequence alignment algorithm}, author={Lassmann, Timo and Sonnhammer, Erik LL}, journal={BMC bioinformatics}, volume={6}, number={1}, pages={298}, year={2005}, publisher={BioMed Central} } @article{lassmann2008kalign2, title={Kalign2: high-performance multiple alignment of protein and nucleotide sequences allowing external features}, author={Lassmann, Timo and Frings, Oliver and Sonnhammer, Erik LL}, journal={Nucleic acids research}, volume={37}, number={3}, pages={858--865}, year={2008}, publisher={Oxford University Press} } @article{sievers2011fast, title={Fast, scalable generation of high-quality protein multiple sequence alignments using Clustal Omega}, author={Sievers, Fabian and Wilm, Andreas and Dineen, David and Gibson, Toby J and Karplus, Kevin and Li, Weizhong and Lopez, Rodrigo and McWilliam, Hamish and Remmert, Michael and S{\"o}ding, Johannes and others}, journal={Molecular systems biology}, volume={7}, number={1}, year={2011}, publisher={John Wiley \& Sons, Ltd} } @article{edgar2004muscle, title={MUSCLE: multiple sequence alignment with high accuracy and high throughput}, author={Edgar, Robert C}, journal={Nucleic acids research}, volume={32}, number={5}, pages={1792--1797}, year={2004}, publisher={Oxford University Press} } @article{deorowicz2016famsa, title={FAMSA: Fast and accurate multiple sequence alignment of huge protein families}, author={Deorowicz, Sebastian and Debudaj-Grabysz, Agnieszka and Gudy{\'s}, Adam}, journal={Scientific reports}, volume={6}, pages={33964}, year={2016}, publisher={Nature Publishing Group} } @article{katoh2006parttree, title={PartTree: an algorithm to build an approximate tree from a large number of unaligned sequences}, author={Katoh, Kazutaka and Toh, Hiroyuki}, journal={Bioinformatics}, volume={23}, number={3}, pages={372--374}, year={2006}, publisher={Oxford University Press} } @article{blackshields2010sequence, title={Sequence embedding for fast construction of guide trees for multiple sequence alignment}, author={Blackshields, Gordon and Sievers, Fabian and Shi, Weifeng and Wilm, Andreas and Higgins, Desmond G}, journal={Algorithms for Molecular Biology}, volume={5}, number={1}, pages={21}, year={2010}, publisher={BioMed Central} } @inproceedings{muth1996approximate, title={Approximate multiple string search}, author={Muth, Robert and Manber, Udi}, booktitle={Annual Symposium on Combinatorial Pattern Matching}, pages={75--86}, year={1996}, organization={Springer} } @article{myers1999fast, title={A fast bit-vector algorithm for approximate string matching based on dynamic programming}, author={Myers, Gene}, journal={Journal of the ACM (JACM)}, volume={46}, number={3}, pages={395--415}, year={1999}, publisher={ACM} } @article{steinegger2018clustering, title={Clustering huge protein sequence sets in linear time}, author={Steinegger, Martin and S{\"o}ding, Johannes}, journal={Nature communications}, volume={9}, number={1}, pages={2542}, year={2018}, publisher={Nature Publishing Group} } @article{sievers2011fast, title={Fast, scalable generation of high-quality protein multiple sequence alignments using Clustal Omega}, author={Sievers, Fabian and Wilm, Andreas and Dineen, David and Gibson, Toby J and Karplus, Kevin and Li, Weizhong and Lopez, Rodrigo and McWilliam, Hamish and Remmert, Michael and S{\"o}ding, Johannes and others}, journal={Molecular systems biology}, volume={7}, number={1}, pages={539}, year={2011}, publisher={EMBO Press} } @article{lassmann2005kalign, title={Kalign--an accurate and fast multiple sequence alignment algorithm}, author={Lassmann, Timo and Sonnhammer, Erik LL}, journal={BMC bioinformatics}, volume={6}, number={1}, pages={298}, year={2005}, publisher={BioMed Central} } @incollection{deorowicz2014kalign, title={Kalign-LCS—A more accurate and faster variant of kalign2 algorithm for the multiple sequence alignment problem}, author={Deorowicz, Sebastian and Debudaj-Grabysz, Agnieszka and Gudy{\'s}, Adam}, booktitle={Man-Machine Interactions 3}, pages={495--502}, year={2014}, publisher={Springer} } @article{shu2011kalignp, title={KalignP: Improved multiple sequence alignments using position specific gap penalties in Kalign2}, author={Shu, Nanjiang and Elofsson, Arne}, journal={Bioinformatics}, volume={27}, number={12}, pages={1702--1703}, year={2011}, publisher={Oxford University Press} } @inproceedings{yang2009improving, title={Improving Kalign via Reconstruction of Phylogenetic Tree and Iteration}, author={Yang, Fan and Zhu, QingXin and Zhao, MingYuan}, booktitle={2009 WRI World Congress on Computer Science and Information Engineering}, volume={1}, pages={625--629}, year={2009}, organization={IEEE} } @article{gardner2005benchmark, title={A benchmark of multiple sequence alignment programs upon structural RNAs}, author={Gardner, Paul P and Wilm, Andreas and Washietl, Stefan}, journal={Nucleic acids research}, volume={33}, number={8}, pages={2433--2439}, year={2005}, publisher={Oxford University Press} } @article{thompson1999balibase, title={BAliBASE: a benchmark alignment database for the evaluation of multiple alignment programs.}, author={Thompson, Julie D. and Plewniak, Fr{\'e}d{\'e}ric and Poch, Olivier}, journal={Bioinformatics (Oxford, England)}, volume={15}, number={1}, pages={87--88}, year={1999} } @article{sievers2019quantest2, title={QuanTest2: Benchmarking Multiple Sequence Alignments using Secondary Structure Prediction}, author={Sievers, Fabian and Higgins, Desmond G}, journal={Bioinformatics}, year={2019} } kalign-3.5.1/doc/paper/kalign3.org000066400000000000000000000213541515023132300167020ustar00rootroot00000000000000#+Options: toc:nil ^:nil title:nil author:nil #+BIND: org-latex-title-command "" #+LATEX_CMD: pdflatex #+Latex_Class: bioinfo #+LaTeX_CLASS_OPTIONS: [nocrop] # Nice code-blocks #+BEGIN_SRC elisp :noweb no-export :exports none :results none (setq org-ref-default-citation-link "citep") (setq org-latex-hyperref-template "") (setq org-latex-minted-options '(("bgcolor" "mintedbg") ("frame" "single") ("framesep" "6pt") ("mathescape" "true") ("fontsize" "\\footnotesize"))) #+END_SRC #+BEGIN_SRC latex \abstract{\textbf{Motivation:} Kalign is an efficient multiple sequence alignment (MSA) program capable of aligning thousands of protein or nucleotide sequences. However, current alignment problems involving large number of sequences are exceeding Kalign's original design specifications. Here we present a completely re-written and updated version to meet current and future alignment challenges.\\ \textbf{Results:} Kalign now uses a SIMD accelerated version of the bit-parallel Gene Myers algorithm to estimate pariwise distances, adopts a sequence embedding strategy and the bi-sectiong K-means algorithm to rapidly construct guide trees for thousands of sequences. The new version maintains high alignment accuracy on both protein and nucleotide alignments and scales better than other MSA tools.\\ \textbf{Availability:} The source code of Kalign and code to reproduce the results are found here: https://github.com/timolassmann/kalign\\ \textbf{Contact:} \href{timolassmann@icloud.com}{timolassmann@icloud.com} %\textbf{Supplementary information:} Supplementary data are available at \textit{Bioinformatics}online. } #+END_SRC #+TOC: headlines 2 #+Latex: \subtitle{Sequence Analysis} #+Latex: \title[short Title]{Kalign 3: multiple sequence alignment of large data sets.} #+Latex: \author[Sample \textit{et~al}.]{Timo Lassmann$^{\text{\sfb 1,}*}$} #+Latex: \address{$^{\text{\sf 1}}$Telethon Kids Institute, University of Western Australia, Nedlands, WA, Australia.} #+Latex: \corresp{$^\ast$To whom correspondence should be addressed.} #+Latex: \history{Received on XXXXX; revised on XXXXX; accepted on XXXXX} #+Latex: \editor{Associate Editor: XXXXXXX} #+Latex: \firstpage{1} #+Latex: \maketitle * Introduction Multiple sequence alignment (MSA) remains an important task in biological sequence analysis. MSA programs can be divided into consistency and progressive methods. The latter estimate pairwise sequence distances, construct a guide tree and align sequences following the order of the guide tree. Consistency based methods tend to be more accurate than compared to progressive methods but are orders of magnitude slower and therefore not practical when aligning thousands of sequences. Kalign citep:lassmann2005kalign,lassmann2008kalign2 is progressive alignment method striking a good balance between accuracy and speed compared to other alignment programs on a range of popular benchmark data sets (see for example cite:sievers2011fast). Despite having aged well Kalign was not designed to handle the tens of thousands of sequences frequently encountered today. In particular, the original Kalign program uses the unweighted pair group method with arithmetic mean (UPGMA) algorithm to construct a guide tree resulting in quadratic time complexity. More recent alignment programs have overcome this hurdle by implementing heuristics to construct guide trees citep:katoh2006parttree,blackshields2010sequence. Here we present a new version of Kalign, introducing a SIMD (single instruction, multiple data) accelerated version of Gene Myers bit-parallel algorithm to estimate pairwise sequence distances and adopting the sequence embedding strategy introduced by cite:blackshields2010sequence to speed up the construction of guide trees. * Methods We replaced the fast string matching algorithm used in Kalign2 citep:muth1996approximate with a new implementation of Gene Myers approximate string matching algorithm citep:myers1999fast. The algorithm calculates the exact edit distance between two strings using bit-parallel instructions. In the standard implementation the maximum length of a query is equivalent to the size of a computer word (64 characters on 64 bit architectures). However the algorithm lends itself to further parallelisation using SIMD (single instruction, multiple data) instructions including the AVX and AVX2 instructions available on all modern computers. Using these instructions it becomes possible to compare sequences of length 256. While the implementation of the Gene Myers algorithm is fairly straight forward using AVX instructions some operations are absent from the AVX instruction set and had to be implemented separately. A stand alone implementation of the algorithm is distributed together with Kalign to facilitate downstream adoption and development. To estimate pairwise sequences distances Kalign scans the first 256 characters of the shorter sequence across the longer sequence. The distance is defined as the number of edits required to turn one sequence into an exact match in the longer sequence. For distantly related protein sequences the sequence similarity is too low for the algorithm to detect meaningful distances. Therefore, following the method by cite:steinegger2018clustering, Kalign converts all protein sequences into a reduced alphabet by merging (L, M), (I, V), (K, R), (E, Q), (A, S, T), (N, D), and (F, Y) for the purpose of the distance calculation. #+BEGIN_SRC latex \begin{figure}[!tpb]%figure1 \centerline{\includegraphics[scale = 0.45]{Paper_figure.jpeg}} \caption{Benchmark results. a) Sum of pairs scores (SP) of all tested alignment programs on Balibase protein alignment data sets. b) SP scores of RNA bralibase alignments. c) Computational performance assessed on the BaliFam data set.}\label{fig:01} \end{figure} #+END_SRC Kalign 3 adopts the guide tree construction methods used in clustal omega citep:sievers2011fast. A number of seed sequences are selected and all sequences are compared against those forming for each sequence a vector of distances to all seeds. The bi-secting kmeans algorithm is used to cluster sequences based on the euclidean distance between these vectors until clusters containing fewer than 100 sequences are found. Here Kalign again uses AVX/ AVX2 instructions to accelerate the distance calculation. Finally, the UPGMA method is used to cluster the remaining sequences. Since the bi-section kmeans algorithm is not guaranteed to discover the optional split of sequence into two clusters Kalign runs the algorithm ten times using randomly selected sequences to seed the calculation. * Results We compared the performance of Kalign against two other popular progressive alignment methods muscle citep:edgar2004muscle and clustal omega citep:sievers2011fast. We used the Balibase citep:thompson1999balibase, Quantest2 citep:sievers2019quantest2, Bralibase citep:gardner2005benchmark and BaliFam benchmark data sets (Fig. \ref{fig:01}). Clustal omega and Muscle were run with parameters recommended for large alignments on the BaliFam data set (Clustal:--threads=8 --MAC-RAM=48000 --iterations=2; Muscle: -maxiters 2), but otherwise default parameters were used. Kalign's performance on all 6 Balibase categories is statistically indistinguishable from the other two programs (twosample t-test, corrected p < 0.05). Likewise there is no statistical difference in alignment accuracy on the Quantest2 benchmark data set (results not shown). Kalign's mean performance is significantly better compared to the other two programs in 2 out of the 6 Bralibase alignment categories. However, we note that the performance of all algorithms can vary dramatically depending on the specific alignment case (see Fig. \ref{fig:01} box plot error bars and outliers). Therefore, we do not assume that good performance on a MSA benchmark sets generalises and recommend users to manual inspect their alignments and compare the results of different alignment programs. Kalign compares favourably to the other two programs in terms of running times and scalability on the Balifam data set (Fig. \ref{fig:01}c). In all alignment cases Kalign is one to two orders of magnitude quicker and compared to clustal omega only uses a single CPU core. * Conclusion We present a new version of Kalign that outperforms other programs in terms of running times while sacrificing little in terms of accuracy. This combinations makes Kalign especially attractive in large alignment problems. * Acknowledgements :PROPERTIES: :UNNUMBERED: t :END: I would like to thank Max Burroughs for providing feedback on Kalign. * Funding :PROPERTIES: :UNNUMBERED: t :END: T.L. is supported by a fellowship from the Feilman Foundation. #+BEGIN_SRC latex \bibliographystyle{plainnat} \bibliography{kalign3} #+END_SRC kalign-3.5.1/doc/paper/revision/000077500000000000000000000000001515023132300164725ustar00rootroot00000000000000kalign-3.5.1/doc/paper/revision/main.tex000066400000000000000000000257771515023132300201620ustar00rootroot00000000000000\documentclass[nocrop]{bioinfo} \copyrightyear{2019} \pubyear{2019} \begin{document} \firstpage{1} \subtitle{Sequence Analysis} \title[short Title]{Kalign 3: multiple sequence alignment of large data sets.} \author[Sample \textit{et~al}.]{Timo Lassmann$^{\text{\sfb 1,}*}$} \corresp{$^\ast$To whom correspondence should be addressed.} \address{$^{\text{\sf 1}}$Telethon Kids Institute, University of Western Australia, Nedlands, WA, Australia.} \corresp{$^\ast$To whom correspondence should be addressed.} \history{Received on XXXXX; revised on XXXXX; accepted on XXXXX} \editor{Associate Editor: XXXXXXX} \abstract{\textbf{Motivation:} Kalign is an efficient multiple sequence alignment (MSA) program capable of aligning thousands of protein or nucleotide sequences. However, current alignment problems involving large number{\color{red} s} of sequences are exceeding Kalign's original design specifications. Here we present a completely re-written and updated version to meet current and future alignment challenges.\\ \textbf{Results:} Kalign now uses a SIMD accelerated version of the bit-parallel Gene Myers algorithm to estimate pariwise distances, adopts a sequence embedding strategy and the bi-secting K-means algorithm to rapidly construct guide trees for thousands of sequences. The new version maintains high alignment accuracy on both protein and nucleotide alignments and scales better than other MSA tools.\\ \textbf{Availability:} The source code of Kalign and code to reproduce the results are found here: https://github.com/timolassmann/kalign\\ \textbf{Contact:} \href{timolassmann@icloud.com}{timolassmann@icloud.com} %\textbf{Supplementary information:} Supplementary data are available at \textit{Bioinformatics}online. } \maketitle \section{Introduction} Multiple sequence alignment (MSA) remains an important task in biological sequence analysis. MSA programs can be divided into consistency and progressive methods. The latter estimate pairwise sequence distances, construct a guide tree and align sequences following the order of the guide tree. Consistency based methods tend to be more accurate than compared to progressive methods but are orders of magnitude slower and therefore not practical when aligning thousands of sequences. Kalign \citep{lassmann2008kalign2} is {\color{red} a} progressive alignment method striking a good balance between accuracy and speed compared to other alignment programs on a range of popular benchmark data sets (see for example \cite{sievers2011fast}). Despite having aged well Kalign was not designed to handle the tens of thousands of sequences frequently encountered today. In particular, the original Kalign program uses the unweighted pair group method with arithmetic mean (UPGMA) algorithm to construct a guide tree resulting in quadratic time complexity. More recent alignment programs have overcome this hurdle by implementing heuristics to construct guide trees \citep{katoh2006parttree,blackshields2010sequence}. Here we present a new version of Kalign, introducing a SIMD (single instruction, multiple data) accelerated version of Gene Myers{\color{red} '} bit-parallel algorithm \citep{myers1999fast} to estimate pairwise sequence distances and adopting the sequence embedding strategy introduced by \cite{blackshields2010sequence} to speed up the construction of guide trees. \section{Methods} We replaced the fast string matching algorithm used in Kalign2 \citep{muth1996approximate} with a new implementation of Gene Myers{\color{red} '} approximate string matching algorithm. The algorithm calculates the exact edit distance between two strings using bit-parallel instructions. In the standard implementation the maximum length of a query is equivalent to the size of a computer word (64 characters on 64 bit architectures). However the algorithm lends itself to further parallelisation using SIMD (single instruction, multiple data) instructions including the AVX and AVX2 instructions available on all modern computers. Using these instructions it becomes possible to compare sequences of length 256. While the implementation of the Gene Myers algorithm is fairly straight forward using AVX instructions some operations are absent from the AVX instruction set and had to be implemented separately. A stand alone implementation of the algorithm is distributed together with Kalign to facilitate downstream adoption and development. To estimate pairwise sequence distances Kalign scans the first 256 characters of the shorter sequence across the longer sequence. The distance is defined as the number of edits required to turn one sequence into an exact match in the longer sequence. For distantly related protein sequences the sequence similarity is too low for the algorithm to detect meaningful distances. Therefore, following the method by \cite{steinegger2018clustering}, Kalign converts all protein sequences into a reduced alphabet by merging (L, M), (I, V), (K, R), (E, Q), (A, S, T), (N, D), and (F, Y) for the purpose of the distance calculation. \begin{figure}[!tpb]%figure1 \centerline{\includegraphics[scale = 0.45]{Paper_figure.jpeg}} \caption{Benchmark results. a) Sum of pairs scores (SP) of all tested alignment programs on Balibase protein alignment data sets. b) SP scores of RNA bralibase alignments. c) Computational performance assessed on the {\color{red}Hom}Fam data set.}\label{fig:01} \end{figure} Kalign adopts the guide tree construction methods used in clustal omega \citep{sievers2011fast}. A number of seed sequences are selected and all sequences are compared against those forming for each sequence a vector of distances to all seeds. The bi-secting kmeans algorithm is used to cluster sequences based on the euclidean distance between these vectors until clusters containing fewer than 100 sequences are found. Here Kalign again uses AVX instructions to accelerate the distance calculation. Finally, the UPGMA method is used to cluster the remaining sequences. Since the bi-secting kmeans algorithm is not guaranteed to discover the opti{\color{red} mal} split of sequence{\color{red}s} into two clusters Kalign runs the algorithm {\color{red}fifty} times using randomly selected sequences to seed the calculation. \section{Results} We compared the performance of Kalign against two other popular progressive alignment methods muscle \citep{edgar2004muscle} and clustal omega \citep{sievers2011fast}. We used the Balibase \citep{thompson1999balibase}, Quantest2 \citep{sievers2019quantest2}, Bralibase \citep{gardner2005benchmark} and {\color{red}Hom}Fam benchmark data sets (Fig. \ref{fig:01}). Clustal omega and Muscle were run with parameters recommended for large alignments on the BaliFam data set (Clustal:--threads=8 --MAC-RAM=48000 --iterations=2; Muscle: -maxiters 2), but otherwise default parameters were used. Kalign's performance on all 6 Balibase categories is statistically indistinguishable from the other two programs (two sample t-test, corrected p < 0.05). Likewise there is no statistical difference in alignment accuracy on the Quantest2 benchmark data set (results not shown). Kalign's mean performance is significantly better compared to the other two programs in 2 out of the 6 Bralibase alignment categories. However, we note that the performance of all algorithms can vary dramatically depending on the specific alignment case (see Fig. \ref{fig:01} box plot error bars and outliers). Therefore, we do not assume that good performance on a{\color{red} n} MSA benchmark sets generalises and recommend users to manual{\color{red} ly} inspect their alignments and compare the results of different alignment programs. Kalign compares favourably to the other two programs in terms of running times and scalability on the Balifam data set (Fig. \ref{fig:01}c). In all alignment cases Kalign is one to two orders of magnitude quicker and compared to clustal omega only uses a single CPU core. \section{Conclusion} We present a new version of Kalign that outperforms other programs in terms of running times while sacrificing little in terms of accuracy. This combination makes Kalign especially attractive in large alignment problems. \section*{Acknowledgements} I would like to thank Max Burroughs for providing feedback on Kalign. \section*{Funding} T.L. is supported by a fellowship from the Feilman Foundation. %\bibliographystyle{natbib} %\bibliographystyle{achemnat} %\bibliographystyle{plainnat} %\bibliographystyle{abbrv} %\bibliographystyle{bioinformatics} % \bibliographystyle{plainnat} % %\bibliography{Document} \begin{thebibliography}{} \bibitem[Blackshields et~al.(2010)Blackshields, Sievers, Shi, Wilm, and Higgins]{blackshields2010sequence} Blackshields, G et al. (2010) Sequence embedding for fast construction of guide trees for multiple sequence alignment. {\it Algorithms for Molecular Biology}, {\bf 5}(1): 21. \bibitem[Edgar(2004)]{edgar2004muscle} Edgar, R.C. (2004) Muscle: multiple sequence alignment with high accuracy and high throughput. {\it Nucleic acids research}, }{\bf32} (5):1792--1797. \bibitem[Gardner et~al.(2005)Gardner, Wilm, and Washietl]{gardner2005benchmark} Gardner, P. et. al. (2005) A benchmark of multiple sequence alignment programs upon structural rnas. {\it Nucleic acids research}, {\bf 33} (8):2433--2439. \bibitem[Katoh and Toh(2006)]{katoh2006parttree} Katoh, K. and Toh, H (2006) Parttree: an algorithm to build an approximate tree from a large number of unaligned sequences. {\it Bioinformatics}, {\bf 23} (3):372--374. \bibitem[Lassmann et~al.(2008)Lassmann, Frings, and Sonnhammer]{lassmann2008kalign2} Lassmann, T. et al. (2008) Kalign2: high-performance multiple alignment of protein and nucleotide sequences allowing external features. {\it Nucleic acids research}, {\bf 37} (3):858--865. \bibitem[Muth and Manber(1996)]{muth1996approximate} Robert Muth, R. and Manber, U. (1996) Approximate multiple string search. {\it Annual Symposium on Combinatorial Pattern Matching}, 75--86. \bibitem[Myers(1999)]{myers1999fast} Myers, G. (1999) A fast bit-vector algorithm for approximate string matching based on dynamic programming. {\it Journal of the ACM (JACM)}, {\bf 46} (3): 395--415. \bibitem[Sievers and Higgins(2019)]{sievers2019quantest2} Sievers, F. and Higgins, D.G. (2019) Quantest2: Benchmarking multiple sequence alignments using secondary structure prediction. {\it Bioinformatics}. \bibitem[Sievers et~al.(2011)Sievers, Wilm, Dineen, Gibson, Karplus, Li, Lopz, McWilliam, Remmert, S{\"o}ding, et~al.]{sievers2011fast} Sievers, F. et al. (2011) Fast, scalable generation of high-quality protein multiple sequence alignments using clustal omega. {\it Molecular systems biology}, {\bf 7} (1). \bibitem[Steinegger and S{\"o}ding(2018)]{steinegger2018clustering} Steinegger, M. and S{\"o}ding, J. (2018) Clustering huge protein sequence sets in linear time. {\it Nature communications}, {\bf 9} (1):2542. \bibitem[Thompson et~al.(1999)Thompson, Plewniak, and Poch]{thompson1999balibase} Thompson, J.D. et al. (1999) Balibase: a benchmark alignment database for the evaluation of multiple alignment programs. {\it Bioinformatics}, {\bf 15} (1): 87--88. \end{thebibliography} \end{document} kalign-3.5.1/lib/000077500000000000000000000000001515023132300135265ustar00rootroot00000000000000kalign-3.5.1/lib/CMakeLists.txt000066400000000000000000000144711515023132300162750ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.18) if(NOT KALIGN_PACKAGE_NAME) set(KALIGN_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) endif() if(NOT KALIGN_PACKAGE_VERSION) set(KALIGN_PACKAGE_VERSION ${KALIGN_LIBRARY_VERSION_STRING}) endif() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" "version.h") set(source_files src/test.c src/tldevel.c src/tlmisc.c src/tlrng.c src/esl_stopwatch.c src/msa_alloc.c src/msa_op.c src/msa_io.c src/msa_misc.c src/msa_check.c src/msa_cmp.c src/msa_sort.c src/alphabet.c src/task.c src/bisectingKmeans.c src/sequence_distance.c src/bpm.c src/euclidean_dist.c src/pick_anchor.c src/aln_wrap.c src/aln_apair_dist.c src/aln_param.c src/aln_run.c src/aln_mem.c src/aln_setup.c src/aln_controller.c src/aln_seqseq.c src/aln_seqprofile.c src/aln_profileprofile.c src/aln_refine.c src/sp_score.c src/weave_alignment.c src/poar.c src/consensus_msa.c src/anchor_consistency.c src/ensemble.c # src/coretralign.c # src/test.h ) add_library(${PROJECT_NAME}_OBJ OBJECT ${source_files}) if(OpenMP_C_FOUND) target_link_libraries(${PROJECT_NAME}_OBJ PRIVATE OpenMP::OpenMP_C) endif(OpenMP_C_FOUND) target_include_directories(${PROJECT_NAME}_OBJ PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ) target_compile_definitions(${PROJECT_NAME}_OBJ PRIVATE KALIGN_PACKAGE_VERSION=\"${KALIGN_PACKAGE_VERSION}\") add_library(${PROJECT_NAME}) add_library(${NAMESPACE_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) generate_export_header(${PROJECT_NAME} EXPORT_FILE_NAME ${PROJECT_NAME}/${PROJECT_NAME}_export.h ) set(public_header_files ${CMAKE_SOURCE_DIR}/lib/include/kalign/kalign.h ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}_export.h #${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${PROJECT_NAME}_export.h ) set_target_properties(${PROJECT_NAME} PROPERTIES LANGUAGES C CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF VERSION ${KALIGN_LIBRARY_VERSION_STRING} SOVERSION ${KALIGN_LIBRARY_VERSION_MAJOR} PUBLIC_HEADER "${public_header_files}" CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1 ) target_sources(${PROJECT_NAME} PRIVATE ${public_header_files} $ ) # Define headers for this library. PUBLIC headers are used for # compiling the library, and will be added to consumers' build # paths. target_include_directories(${PROJECT_NAME} PUBLIC $ $ $ ) target_include_directories(${PROJECT_NAME} PRIVATE src ) target_link_libraries(${PROJECT_NAME} PRIVATE m) if(OpenMP_C_FOUND) target_link_libraries(${PROJECT_NAME} PRIVATE OpenMP::OpenMP_C) endif() add_library(${PROJECT_NAME}_static STATIC ${public_header_files} $ ) set_target_properties(${PROJECT_NAME}_static PROPERTIES LANGUAGES C CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF VERSION ${KALIGN_LIBRARY_VERSION_STRING} SOVERSION ${KALIGN_LIBRARY_VERSION_MAJOR} PUBLIC_HEADER "${public_header_files}" CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1 ) target_include_directories(${PROJECT_NAME}_static PUBLIC $ $ $ ) target_include_directories(${PROJECT_NAME}_static PRIVATE src ) target_link_libraries(${PROJECT_NAME}_static PRIVATE m) if(OpenMP_C_FOUND) target_link_libraries(${PROJECT_NAME}_static PRIVATE OpenMP::OpenMP_C) endif() add_library(tldevel STATIC src/tldevel.c src/tlmisc.c src/tlrng.c src/esl_stopwatch.c ) target_include_directories(tldevel PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_link_libraries(tldevel PRIVATE m) # adjust this path depending where cmake searches for the target files set(ConfigPackageLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") # 'make install' to the correct locations (provided by GNUInstallDirs). install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Library ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Library RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Library # This is for Windows PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} COMPONENT Development INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install(EXPORT ${PROJECT_NAME}Targets FILE ${PROJECT_NAME}Targets.cmake NAMESPACE ${NAMESPACE_NAME}:: DESTINATION ${ConfigPackageLocation} COMPONENT Development ) configure_package_config_file( ${PROJECT_NAME}Config.cmake.in ${PROJECT_NAME}Config.cmake INSTALL_DESTINATION "${ConfigPackageLocation}" PATH_VARS CMAKE_INSTALL_PREFIX ) write_basic_package_version_file( ${PROJECT_NAME}ConfigVersion.cmake VERSION ${KALIGN_LIBRARY_VERSION_STRING} COMPATIBILITY AnyNewerVersion ) install( FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION ${ConfigPackageLocation} COMPONENT Development ) add_executable(bpm_utest src/tldevel.c src/tlrng.c src/esl_stopwatch.c src/alphabet.c src/bpm.c src/bpm_test.c ) target_link_libraries(bpm_utest PRIVATE m) add_test( NAME bpm_utest COMMAND bpm_utest ) add_executable(alphabet_utest src/tldevel.c src/alphabet.c ) set_target_properties(alphabet_utest PROPERTIES COMPILE_FLAGS "-DUTEST_ALPHABET") add_test( NAME alphabet_utest COMMAND alphabet_utest ) add_executable(edist_utest src/tldevel.c src/tlrng.c src/esl_stopwatch.c src/euclidean_dist.c ) target_link_libraries(edist_utest PRIVATE m) set_target_properties(edist_utest PROPERTIES COMPILE_FLAGS "-DUTEST_EDIST") add_test( NAME edist_utest COMMAND edist_utest ) add_executable(task_utest src/tldevel.c src/tlrng.c src/task.c ) target_link_libraries(task_utest PRIVATE m) set_target_properties(task_utest PROPERTIES COMPILE_FLAGS "-DTASKWRITETEST") add_test( NAME task_utest COMMAND task_utest )kalign-3.5.1/lib/include/000077500000000000000000000000001515023132300151515ustar00rootroot00000000000000kalign-3.5.1/lib/include/kalign/000077500000000000000000000000001515023132300164165ustar00rootroot00000000000000kalign-3.5.1/lib/include/kalign/kalign.h000066400000000000000000000106271515023132300200420ustar00rootroot00000000000000#ifndef KALIGN_H #define KALIGN_H #include #ifdef KALIGN_IMPORT #define EXTERN #else #ifndef EXTERN #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #endif #define KALIGN_TYPE_DNA 0 #define KALIGN_TYPE_DNA_INTERNAL 1 #define KALIGN_TYPE_RNA 2 #define KALIGN_TYPE_PROTEIN 3 #define KALIGN_TYPE_PROTEIN_DIVERGENT 4 #define KALIGN_TYPE_PROTEIN_PFASUM43 5 #define KALIGN_TYPE_PROTEIN_PFASUM60 6 #define KALIGN_TYPE_PROTEIN_PFASUM_AUTO 7 #define KALIGN_TYPE_UNDEFINED 8 #define KALIGN_REFINE_NONE 0 #define KALIGN_REFINE_ALL 1 #define KALIGN_REFINE_CONFIDENT 2 #define KALIGN_REFINE_INLINE 3 struct msa; /* input output routines */ EXTERN int kalign_read_input(char* infile, struct msa** msa,int quiet); EXTERN int kalign_write_msa(struct msa *msa, char *outfile, char *format); /* EXTERN int kalign_msa_to_arr(struct msa *msa, char ***aligned, int *out_aln_len); */ /* Used to convert sequences read by non-kalign code into the msa struct.. */ /* EXTERN int kalign_arr_to_msa(char **input_sequences, int *len, int numseq, struct msa **multiple_aln); */ EXTERN int kalign(char **seq, int *len, int numseq, int n_threads, int type, float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len); EXTERN int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget); EXTERN int kalign_run_seeded(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, uint64_t tree_seed, float tree_noise, float dist_scale, float vsm_amax, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, float use_seq_weights); EXTERN int kalign_run_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights); EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, int n_runs, float gpo, float gpe, float tgpe, uint64_t seed, int min_support, const char* save_poar_path, int refine, float dist_scale, float vsm_amax, int realign, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); /* Memory */ EXTERN void kalign_free_msa(struct msa* msa); /* Auxillary... */ EXTERN int reformat_settings_msa(struct msa *msa, int rename, int unalign); EXTERN int kalign_check_msa(struct msa* msa, int exit_on_error); EXTERN int kalign_msa_compare(struct msa *r, struct msa *t, float *score); struct poar_score; EXTERN int kalign_msa_compare_detailed(struct msa *r, struct msa *t, float max_gap_frac, struct poar_score *out); EXTERN int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, int *scored_cols, int n_cols, struct poar_score *out); #undef KALIGN_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/kalignConfig.cmake.in000066400000000000000000000001761515023132300175340ustar00rootroot00000000000000@PACKAGE_INIT@ include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") check_required_components("@PROJECT_NAME@") kalign-3.5.1/lib/src/000077500000000000000000000000001515023132300143155ustar00rootroot00000000000000kalign-3.5.1/lib/src/aln_apair_dist.c000066400000000000000000000041331515023132300174330ustar00rootroot00000000000000#include "tldevel.h" #include "msa_struct.h" #define ALN_APAIR_DIST_IMPORT #include "aln_apair_dist.h" static float pairwise_identity_dist(const char* a, const char* b, int alnlen); int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr) { float** dm = NULL; int n; int i, j; ASSERT(msa != NULL, "No MSA"); ASSERT(msa->aligned == ALN_STATUS_FINAL, "MSA must be finalized"); n = msa->numseq; MMALLOC(dm, sizeof(float*) * n); for(i = 0; i < n; i++){ dm[i] = NULL; } for(i = 0; i < n; i++){ MMALLOC(dm[i], sizeof(float) * n); dm[i][i] = 0.0f; } for(i = 0; i < n - 1; i++){ const char* seq_i = msa->sequences[i]->seq; for(j = i + 1; j < n; j++){ float d = pairwise_identity_dist(seq_i, msa->sequences[j]->seq, msa->alnlen); dm[i][j] = d; dm[j][i] = d; } } *dm_ptr = dm; return OK; ERROR: if(dm){ for(i = 0; i < n; i++){ if(dm[i]) MFREE(dm[i]); } MFREE(dm); } return FAIL; } void free_aln_dm(float** dm, int n) { int i; if(dm == NULL) return; for(i = 0; i < n; i++){ if(dm[i]) MFREE(dm[i]); } MFREE(dm); } /* Distance = 1.0 - identity. Only counts columns where both sequences have a residue (no gap). */ float pairwise_identity_dist(const char* a, const char* b, int alnlen) { int matches = 0; int aligned = 0; int i; for(i = 0; i < alnlen; i++){ if(a[i] != '-' && b[i] != '-'){ aligned++; if(a[i] == b[i]){ matches++; } } } if(aligned == 0){ return 1.0f; } return 1.0f - (float)matches / (float)aligned; } kalign-3.5.1/lib/src/aln_apair_dist.h000066400000000000000000000012721515023132300174410ustar00rootroot00000000000000#ifndef ALN_APAIR_DIST_H #define ALN_APAIR_DIST_H #ifdef ALN_APAIR_DIST_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; /* Compute NxN pairwise identity distances from an aligned MSA. Distance = 1.0 - (matches / aligned_positions) for each pair. The MSA must be finalized (sequences contain gap characters). Caller must free the returned matrix with free_dm(). */ EXTERN int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr); /* Free an NxN distance matrix allocated by compute_aln_pairwise_dist. */ EXTERN void free_aln_dm(float** dm, int n); #undef ALN_APAIR_DIST_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_controller.c000066400000000000000000000312701515023132300175010ustar00rootroot00000000000000#include #include "tldevel.h" #include "aln_param.h" #include "aln_struct.h" /* The dynamic programming modules */ #include "aln_seqseq.h" #include "aln_seqprofile.h" #include "aln_profileprofile.h" #define ALN_CONTROLLER_IMPORT #include "aln_controller.h" static int aln_continue(struct aln_mem* m,float input_states[],int old_cor[],int meet,int transition, uint8_t serial); int aln_runner(struct aln_mem* m) { float input_states[6]; int old_cor[5]; float score; int mid; int meet; int transition; /* switch to serial if too little work. */ if(m->enda - m->starta < KALIGN_ALN_SERIAL_THRESHOLD){ return aln_runner_serial(m); } if(m->starta >= m->enda){ return OK;//hirsch_path; } if(m->startb >= m->endb){ return OK;///hirsch_path; } input_states[0] = m->f[0].a; input_states[1] = m->f[0].ga; input_states[2] = m->f[0].gb; input_states[3] = m->b[0].a; input_states[4] = m->b[0].ga; input_states[5] = m->b[0].gb; mid = ((m->enda - m->starta) / 2)+ m->starta; old_cor[0] = m->starta; old_cor[1] = m->enda; old_cor[2] = m->startb; old_cor[3] = m->endb; old_cor[4] = mid; /* fprintf(stderr,"Forward:%d-%d %d-%d\n",m->starta,m->enda,m->startb,m->endb); */ m->enda = mid; m->starta_2 = mid; m->enda_2 = old_cor[1]; /* fprintf(stderr,"Forward:%d-%d %d-%d\n",m->starta,m->enda,m->startb,m->endb); */ #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait { #endif if(m->seq1){ #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_seqseq_foward(m); #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_seqseq_backward(m); #ifdef HAVE_OPENMP #pragma omp taskwait #endif aln_seqseq_meetup(m,old_cor,&meet,&transition,&score); }else if(m->prof2){ #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_profileprofile_foward(m); #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_profileprofile_backward(m); #ifdef HAVE_OPENMP #pragma omp taskwait #endif aln_profileprofile_meetup(m,old_cor,&meet,&transition,&score); }else{ #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_seqprofile_foward(m); #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif aln_seqprofile_backward(m); #ifdef HAVE_OPENMP #pragma omp taskwait #endif aln_seqprofile_meetup(m,old_cor,&meet,&transition,&score); } #ifdef HAVE_OPENMP } #endif if(m->mode == ALN_MODE_SCORE_ONLY){ m->score = score; }else{ aln_continue(m, input_states,old_cor, meet, transition,0); } return OK; } int aln_runner_serial(struct aln_mem* m) { float input_states[6]; int old_cor[5]; float score; int mid; int meet; int transition; if(m->starta >= m->enda){ return OK;//hirsch_path; } if(m->startb >= m->endb){ return OK;///hirsch_path; } input_states[0] = m->f[0].a; input_states[1] = m->f[0].ga; input_states[2] = m->f[0].gb; input_states[3] = m->b[0].a; input_states[4] = m->b[0].ga; input_states[5] = m->b[0].gb; mid = ((m->enda - m->starta) / 2)+ m->starta; old_cor[0] = m->starta; old_cor[1] = m->enda; old_cor[2] = m->startb; old_cor[3] = m->endb; old_cor[4] = mid; /* fprintf(stderr,"Forward:%d-%d %d-%d\n",m->starta,m->enda,m->startb,m->endb); */ m->enda = mid; m->starta_2 = mid; m->enda_2 = old_cor[1]; if(m->seq1){ aln_seqseq_foward(m); aln_seqseq_backward(m); aln_seqseq_meetup(m,old_cor,&meet,&transition,&score); }else if(m->prof2){ aln_profileprofile_foward(m); aln_profileprofile_backward(m); aln_profileprofile_meetup(m,old_cor,&meet,&transition,&score); }else{ aln_seqprofile_foward(m); aln_seqprofile_backward(m); aln_seqprofile_meetup(m,old_cor,&meet,&transition,&score); } /* CRITICAL */ //m->starta = mid; //m->enda = old_cor[1]; //fprintf(stderr,"Backward:%d-%d %d-%d\n",m->starta,m->enda,m->startb,m->endb); /* if(m->seq1){ */ /* }else if(m->prof2){ */ /* }else{ */ /* } */ if(m->mode == ALN_MODE_SCORE_ONLY){ m->score = score; }else{ aln_continue(m, input_states,old_cor, meet, transition,1); } return OK; } int aln_continue(struct aln_mem* m,float input_states[],int old_cor[],int meet,int transition, uint8_t serial) { int* path = m->path; switch(transition){ case 1: //a -> a = 1 path[old_cor[4]] = meet; path[old_cor[4]+1] = meet+1; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; m->starta = old_cor[0]; m->enda = old_cor[4]-1; m->startb = old_cor[2]; m->endb = meet-1; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]+1; m->enda = old_cor[1]; m->startb = meet+1; m->endb = old_cor[3]; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; case 2:// a -> ga = 2 path[old_cor[4]] = meet; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; m->starta = old_cor[0]; m->enda = old_cor[4]-1; m->startb = old_cor[2]; m->endb = meet-1; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]; m->enda = old_cor[1]; m->startb = meet+1; m->endb = old_cor[3]; m->f[0].a = -FLT_MAX; m->f[0].ga = 0.0F; m->f[0].gb = -FLT_MAX; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; case 3:// a -> gb = 3 path[old_cor[4]] = meet; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; m->starta = old_cor[0]; m->enda = old_cor[4]-1; m->startb = old_cor[2]; m->endb = meet-1; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]+1; m->enda = old_cor[1]; m->startb = meet; m->endb = old_cor[3]; m->f[0].a = -FLT_MAX; m->f[0].ga = -FLT_MAX; m->f[0].gb = 0.0; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; case 5://ga -> a = 5 path[old_cor[4]+1] = meet+1; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = -FLT_MAX; m->b[0].ga = 0.0F; m->b[0].gb = -FLT_MAX; m->starta = old_cor[0]; m->enda = old_cor[4]; m->startb = old_cor[2]; m->endb = meet-1; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]+1; m->enda = old_cor[1]; m->startb = meet+1; m->endb = old_cor[3]; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; case 6://gb->gb = 6; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = -FLT_MAX; m->b[0].ga = -FLT_MAX; m->b[0].gb = 0.0F; m->starta = old_cor[0]; m->enda = old_cor[4]-1; m->startb = old_cor[2]; m->endb = meet; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]+1; m->enda = old_cor[1]; m->startb = meet; m->endb = old_cor[3]; m->f[0].a = -FLT_MAX; m->f[0].ga = -FLT_MAX; m->f[0].gb = 0.0F; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; case 7://gb->a = 7; path[old_cor[4]+1] = meet+1; //foward: m->f[0].a = input_states[0]; m->f[0].ga = input_states[1]; m->f[0].gb = input_states[2]; m->b[0].a = -FLT_MAX; m->b[0].ga = -FLT_MAX; m->b[0].gb = 0.0F; m->starta = old_cor[0]; m->enda = old_cor[4]-1; m->startb = old_cor[2]; m->endb = meet; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } //backward: m->starta = old_cor[4]+1; m->enda = old_cor[1]; m->startb = meet+1; m->endb = old_cor[3]; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = input_states[3]; m->b[0].ga = input_states[4]; m->b[0].gb = input_states[5]; if(serial){ aln_runner_serial(m); }else{ aln_runner(m); } break; default: break; } return OK; } kalign-3.5.1/lib/src/aln_controller.h000066400000000000000000000005601515023132300175040ustar00rootroot00000000000000#ifndef ALN_CONTROLLER_H #define ALN_CONTROLLER_H #include #ifdef ALN_CONTROLLER_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_mem; EXTERN int aln_runner(struct aln_mem* m); EXTERN int aln_runner_serial(struct aln_mem* m); #undef ALN_CONTROLLER_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_mem.c000066400000000000000000000056161515023132300161010ustar00rootroot00000000000000#include "tldevel.h" #include "aln_struct.h" #define ALN_MEM_IMPORT #include "aln_mem.h" int alloc_aln_mem(struct aln_mem** mem, int x) { struct aln_mem* m = NULL; ASSERT(x>=1, "Given size %d is too small",x); // a=((typeof(a))(((int)(((void *)malloc(c+15))+15))&-16)). MMALLOC(m,sizeof(struct aln_mem)); m->seq1 = NULL; m->seq2 = NULL; m->prof1 = NULL; m->prof2 = NULL; m->sip = 0; m->mode = ALN_MODE_FULL; m->score = 0.0F; m->margin_sum = 0.0F; m->margin_count = 0; m->flip_threshold = 0.0F; m->flip_trial = 0; m->flip_stride = 1; m->flip_counter = 0; m->flip_mask = 0; m->flip_margins = NULL; m->flip_margin_alloc = 0; m->flip_bit_map = NULL; m->flip_n_targets = 0; m->flip_n_uncertain = 0; m->ap = NULL; m->consistency = NULL; m->consistency_stride = 0; m->starta = 0; m->startb = 0; m->enda = 0; m->endb = 0; m->size = x; m->len_a = 0; m->len_b = 0; m->f = NULL; m->b = NULL; m->path = NULL; m->tmp_path = NULL; m->alloc_path_len = x; MMALLOC(m->f,sizeof(struct states)* m->size); MMALLOC(m->b,sizeof(struct states)* m->size); MMALLOC(m->path, sizeof(int) * m->alloc_path_len); MMALLOC(m->tmp_path, sizeof(int) * m->alloc_path_len); *mem = m; return OK; ERROR: free_aln_mem(m); return FAIL; } int resize_aln_mem(struct aln_mem* m) { int g; /* For dynamic programming I only need a slice of the dyn. prog. matrix. To be precise, a slide of the shorter sequence */ g = MACRO_MAX(m->len_a, m->len_b) + 2; if(g > m->size){ while(m->size < g){ m->size = m->size + m->size / 2; } MREALLOC(m->f,sizeof(struct states)* m->size); MREALLOC(m->b,sizeof(struct states)* m->size); } /* For the alignment path I need at most: */ g = m->len_a+ m->len_b + 2; /* memory */ if(g > m->alloc_path_len){ while(m->alloc_path_len < g){ m->alloc_path_len = m->alloc_path_len + m->alloc_path_len / 2; } MREALLOC(m->path, sizeof(int) * m->alloc_path_len); MREALLOC(m->tmp_path, sizeof(int) * m->alloc_path_len); } return OK; ERROR: free_aln_mem(m); return FAIL; } void free_aln_mem(struct aln_mem* m) { if(m){ if(m->flip_bit_map) MFREE(m->flip_bit_map); if(m->flip_margins) MFREE(m->flip_margins); MFREE(m->tmp_path); MFREE(m->path); MFREE(m->f); MFREE(m->b); MFREE(m); } } kalign-3.5.1/lib/src/aln_mem.h000066400000000000000000000005671515023132300161060ustar00rootroot00000000000000#ifndef ALN_MEM_H #define ALN_MEM_H #ifdef ALN_MEM_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_mem; EXTERN int alloc_aln_mem(struct aln_mem** mem, int x); EXTERN int resize_aln_mem(struct aln_mem* m); EXTERN void free_aln_mem(struct aln_mem* m); #undef ALN_MEM_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_param.c000066400000000000000000000461551515023132300164260ustar00rootroot00000000000000#include "tldevel.h" #include "kalign/kalign.h" #include "msa_struct.h" #define ALN_PARAM_IMPORT #include "aln_param.h" static int set_subm_gaps_CorBLOSUM66_13plus(struct aln_param *ap); static int set_subm_gaps_gon250(struct aln_param* ap); static int set_subm_gaps_PFASUM43(struct aln_param *ap); static int set_subm_gaps_PFASUM60(struct aln_param *ap); static int set_subm_gaps_DNA(struct aln_param *ap); static int set_subm_gaps_DNA_internal(struct aln_param *ap); static int set_subm_gaps_RNA(struct aln_param *ap); int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int type, float gpo, float gpe, float tgpe) { struct aln_param* ap = NULL; /* Allocate */ MMALLOC(ap, sizeof(struct aln_param)); ap->subm = NULL; ap->nthreads = n_threads; MMALLOC(ap->subm,sizeof (float*) * 23); for (int i = 23;i--;){ ap->subm[i] = NULL; MMALLOC(ap->subm[i],sizeof(float) * 23); for (int j = 23;j--;){ ap->subm[i][j] = 0.0f; } } if(biotype == ALN_BIOTYPE_DNA){ /* include/kalign/ */ switch (type) { case KALIGN_TYPE_DNA: set_subm_gaps_DNA(ap); break; case KALIGN_TYPE_DNA_INTERNAL: set_subm_gaps_DNA_internal(ap); break; case KALIGN_TYPE_RNA: set_subm_gaps_RNA(ap); break; case KALIGN_TYPE_PROTEIN: ERROR_MSG("Detected DNA sequences but --type protein option was selected."); break; default: set_subm_gaps_RNA(ap); break; } }else if(biotype == ALN_BIOTYPE_PROTEIN){ switch (type) { case KALIGN_TYPE_PROTEIN: set_subm_gaps_PFASUM43(ap); break; case KALIGN_TYPE_PROTEIN_DIVERGENT: set_subm_gaps_gon250(ap); break; case KALIGN_TYPE_PROTEIN_PFASUM43: set_subm_gaps_PFASUM43(ap); break; case KALIGN_TYPE_PROTEIN_PFASUM60: set_subm_gaps_PFASUM60(ap); break; case KALIGN_TYPE_DNA: ERROR_MSG("Detected protein sequences but --type dna option was selected."); break; case KALIGN_TYPE_DNA_INTERNAL: ERROR_MSG("Detected protein sequences but --type internal option was selected."); break; case KALIGN_TYPE_RNA: ERROR_MSG("Detected protein sequences but --type rna option was selected."); break; default: set_subm_gaps_PFASUM43(ap); break; } }else{ ERROR_MSG("Unable to determine what alphabet to use."); } if(gpo >= 0.0){ ap->gpo = gpo; } if(gpe >= 0.0){ ap->gpe = gpe; } if(tgpe >= 0.0){ ap->tgpe = tgpe; } ap->dist_scale = 0.0f; ap->vsm_amax = (biotype == ALN_BIOTYPE_PROTEIN) ? 2.0f : 0.0f; ap->subm_offset = 0.0f; ap->adaptive_budget = 0; ap->use_seq_weights = 0.0f; ap->consistency_anchors = 0; ap->consistency_weight = 2.0f; *aln_param = ap; return OK; ERROR: aln_param_free(ap); return FAIL; } int set_subm_gaps_gon250(struct aln_param* ap) { //A,R,N,D,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,B,Z,X, int gon250mt[23][23] = { // A, R, N, D, C, Q, E, G, H, I, L, K, M, F, P, S, T, W, Y, V, B, Z, X, { 24, -6, -3, -3, 5, -2, 0, 5, -8, -8, -12, -4, -7, -23, 3, 11, 6, -36, -22, 1, 0, 0, 0},// A { -6, 47, 3, -3, -22, 15, 4, -10, 6, -24, -22, 27, -17, -32, -9, -2, -2, -16, -18, -20, 0, 0, 0},// R { -3, 3, 38, 22, -18, 7, 9, 4, 12, -28, -30, 8, -22, -31, -9, 9, 5, -36, -14, -22, 0, 0, 0},// N { -3, -3, 22, 47, -32, 9, 27, 1, 4, -38, -40, 5, -30, -45, -7, 5, 0, -52, -28, -29, 0, 0, 0},// D { 5, -22, -18, -32, 115, -24, -30, -20, -13, -11, -15, -28, -9, -8, -31, 1, -5, -10, -5, 0, 0, 0, 0},// C { -2, 15, 7, 9, -24, 27, 17, -10, 12, -19, -16, 15, -10, -26, -2, 2, 0, -27, -17, -15, 0, 0, 0},// Q { 0, 4, 9, 27, -30, 17, 36, -8, 4, -27, -28, 12, -20, -39, -5, 2, -1, -43, -27, -19, 0, 0, 0},// E { 5, -10, 4, 1, -20, -10, -8, 66, -14, -45, -44, -11, -35, -52, -16, 4, -11, -40, -40, -33, 0, 0, 0},// G { -8, 6, 12, 4, -13, 12, 4, -14, 60, -22, -19, 6, -13, -1, -11, -2, -3, -8, 22, -20, 0, 0, 0},// H { -8, -24, -28, -38, -11, -19, -27, -45, -22, 40, 28, -21, 25, 10, -26, -18, -6, -18, -7, 31, 0, 0, 0},// I { -12, -22, -30, -40, -15, -16, -28, -44, -19, 28, 40, -21, 28, 20, -23, -21, -13, -7, 0, 18, 0, 0, 0},// L { -4, 27, 8, 5, -28, 15, 12, -11, 6, -21, -21, 32, -14, -33, -6, 1, 1, -35, -21, -17, 0, 0, 0},// K { -7, -17, -22, -30, -9, -10, -20, -35, -13, 25, 28, -14, 43, 16, -24, -14, -6, -10, -2, 16, 0, 0, 0},// M { -23, -32, -31, -45, -8, -26, -39, -52, -1, 10, 20, -33, 16, 70, -38, -28, -22, 36, 51, 1, 0, 0, 0},// F { 3, -9, -9, -7, -31, -2, -5, -16, -11, -26, -23, -6, -24, -38, 76, 4, 1, -50, -31, -18, 0, 0, 0},// P { 11, -2, 9, 5, 1, 2, 2, 4, -2, -18, -21, 1, -14, -28, 4, 22, 15, -33, -19, -10, 0, 0, 0},// S { 6, -2, 5, 0, -5, 0, -1, -11, -3, -6, -13, 1, -6, -22, 1, 15, 25, -35, -19, 0, 0, 0, 0},// T { -36, -16, -36, -52, -10, -27, -43, -40, -8, -18, -7, -35, -10, 36, -50, -33, -35, 142, 41, -26, 0, 0, 0},// W { -22, -18, -14, -28, -5, -17, -27, -40, 22, -7, 0, -21, -2, 51, -31, -19, -19, 41, 78, -11, 0, 0, 0},// Y { 1, -20, -22, -29, 0, -15, -19, -33, -20, 31, 18, -17, 16, 1, -18, -10, 0, -26, -11, 34, 0, 0, 0},// V { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},// B { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},// Z { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},// X }; for(int i = 0; i < 23;i++){ for(int j = 0; j < 23;j++){ ap->subm[i][j] = (float)(gon250mt[i][j]);// *2.0F; } } ap->gpo = 55; ap->gpe = 8; ap->tgpe = 4; return OK; } int set_subm_gaps_CorBLOSUM66_13plus(struct aln_param* ap) { int i; int j; //char aacode[20] = "ACDEFGHIKLMNPQRSTVWY"; //char aa_order[23] = "ARNDCQEGHILKMFPSTWYVBZX"; //int num_aa = 23; int CorBLOSUM66_13plus[23][23] = { /*A R N D C Q E G H I L K M F P S T W Y V B Z X*/ {5,-1,-1,-2,-2,-1,-1,0,-2,-1,-1,-1,0,-2,-1,1,0,-2,-2,0,-2,-1,0}, {-1,6,0,-1,-3,1,1,-2,0,-2,-2,3,-1,-3,-1,-1,-1,-1,-1,-2,0,1,-1}, {-1,0,6,2,-3,1,0,0,0,-3,-3,0,-2,-2,-1,1,0,-2,-1,-2,4,0,-1}, {-2,-1,2,7,-3,1,2,-1,-1,-3,-3,0,-3,-3,-1,0,-1,-3,-2,-3,5,2,-1}, {-2,-3,-3,-3,12,-3,-4,-3,-2,-2,-3,-3,-2,-1,-3,-2,-2,-3,-2,-2,-3,-3,-2}, {-1,1,1,1,-3,5,2,-2,0,-2,-2,1,0,-2,-1,0,0,-1,-1,-2,1,3,0}, {-1,1,0,2,-4,2,6,-2,-1,-3,-3,1,-2,-3,0,0,-1,-2,-2,-2,1,4,-1}, {0,-2,0,-1,-3,-2,-2,7,-2,-4,-4,-2,-3,-3,-2,0,-2,-3,-3,-3,-1,-2,-1}, {-2,0,0,-1,-2,0,-1,-2,10,-3,-3,0,-2,-2,-2,-1,-1,-2,1,-3,0,0,-1}, {-1,-2,-3,-3,-2,-2,-3,-4,-3,5,2,-2,2,0,-3,-2,-1,-1,-1,3,-3,-2,-1}, {-1,-2,-3,-3,-3,-2,-3,-4,-3,2,5,-2,3,1,-3,-3,-2,0,-1,1,-3,-2,-1}, {-1,3,0,0,-3,1,1,-2,0,-2,-2,5,-1,-3,-1,0,0,-2,-2,-2,0,1,-1}, {0,-1,-2,-3,-2,0,-2,-3,-2,2,3,-1,6,1,-2,-1,-1,0,-1,1,-2,-1,0}, {-2,-3,-2,-3,-1,-2,-3,-3,-2,0,1,-3,1,7,-3,-2,-2,2,3,0,-3,-3,-1}, {-1,-1,-1,-1,-3,-1,0,-2,-2,-3,-3,-1,-2,-3,9,0,-1,-2,-2,-2,-1,-1,-1}, {1,-1,1,0,-2,0,0,0,-1,-2,-3,0,-1,-2,0,4,2,-2,-2,-1,0,0,0}, {0,-1,0,-1,-2,0,-1,-2,-1,-1,-2,0,-1,-2,-1,2,5,-1,-1,0,0,0,0}, {-2,-1,-2,-3,-3,-1,-2,-3,-2,-1,0,-2,0,2,-2,-2,-1,13,3,-2,-2,-2,-1}, {-2,-1,-1,-2,-2,-1,-2,-3,1,-1,-1,-2,-1,3,-2,-2,-1,3,9,-1,-2,-2,-1}, {0,-2,-2,-3,-2,-2,-2,-3,-3,3,1,-2,1,0,-2,-1,0,-2,-1,4,-3,-2,-1}, {-2,0,4,5,-3,1,1,-1,0,-3,-3,0,-2,-3,-1,0,0,-2,-2,-3,4,1,-1}, {-1,1,0,2,-3,3,4,-2,0,-2,-2,1,-1,-3,-1,0,0,-2,-2,-2,1,4,-1}, {0,-1,-1,-1,-2,0,-1,-1,-1,-1,-1,-1,0,-1,-1,0,0,-1,-1,-1,-1,-1,-1}, }; for(i = 0; i < 23;i++){ for(j = 0; j < 23;j++){ ap->subm[i][j] = (float)(CorBLOSUM66_13plus[i][j]);// *2.0F; } } ap->gpo = 5.5F; ap->gpe = 2.0F; ap->tgpe = 1.0F; return OK; } /* PFASUM43 matrix (H = 0.3354 bit) from Keul, Hess, Goesele, Hamacher (2017) * BMC Bioinformatics 18:293. Derived from Pfam seed alignments (v29.0) with * 43% clustering threshold. 1/3 bit units — same scale as CorBLOSUM66. */ int set_subm_gaps_PFASUM43(struct aln_param *ap) { /* A,R,N,D,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,B,Z,X */ int PFASUM43[23][23] = { /* A R N D C Q E G H I L K M F P S T W Y V B Z X */ { 4, -1, -1, -1, 0, 0, -1, 0, -2, -1, -1, -1, 0, -2, -1, 1, 0, -2, -2, 0, 0, 0, 0}, /* A */ { -1, 6, 0, 0, -3, 2, 1, -2, 1, -3, -3, 3, -2, -3, -1, 0, 0, -2, -2, -3, 0, 0, 0}, /* R */ { -1, 0, 6, 2, -2, 1, 1, 0, 1, -4, -4, 1, -2, -3, -1, 1, 0, -3, -2, -3, 0, 0, 0}, /* N */ { -1, 0, 2, 6, -4, 1, 3, 0, 0, -5, -5, 0, -4, -5, 0, 0, 0, -4, -3, -4, 0, 0, 0}, /* D */ { 0, -3, -2, -4, 13, -3, -4, -2, -2, -1, -1, -4, 0, -1, -3, 0, -1, -2, -1, 0, 0, 0, 0}, /* C */ { 0, 2, 1, 1, -3, 5, 2, -1, 1, -3, -3, 2, -1, -3, -1, 0, 0, -3, -2, -2, 0, 0, 0}, /* Q */ { -1, 1, 1, 3, -4, 2, 5, -1, 0, -4, -4, 2, -3, -4, -1, 0, 0, -4, -3, -3, 0, 0, 0}, /* E */ { 0, -2, 0, 0, -2, -1, -1, 7, -2, -4, -4, -1, -3, -4, -1, 0, -1, -3, -3, -3, 0, 0, 0}, /* G */ { -2, 1, 1, 0, -2, 1, 0, -2, 9, -3, -3, 0, -2, -1, -1, 0, -1, -1, 2, -3, 0, 0, 0}, /* H */ { -1, -3, -4, -5, -1, -3, -4, -4, -3, 5, 2, -3, 2, 1, -3, -3, -1, -1, -1, 3, 0, 0, 0}, /* I */ { -1, -3, -4, -5, -1, -3, -4, -4, -3, 2, 4, -3, 2, 2, -3, -3, -2, 0, 0, 2, 0, 0, 0}, /* L */ { -1, 3, 1, 0, -4, 2, 2, -1, 0, -3, -3, 5, -2, -4, -1, 0, 0, -3, -2, -3, 0, 0, 0}, /* K */ { 0, -2, -2, -4, 0, -1, -3, -3, -2, 2, 2, -2, 6, 1, -3, -2, -1, 0, 0, 1, 0, 0, 0}, /* M */ { -2, -3, -3, -5, -1, -3, -4, -4, -1, 1, 2, -4, 1, 7, -3, -3, -2, 3, 4, 0, 0, 0, 0}, /* F */ { -1, -1, -1, 0, -3, -1, -1, -1, -1, -3, -3, -1, -3, -3, 9, 0, -1, -3, -3, -2, 0, 0, 0}, /* P */ { 1, 0, 1, 0, 0, 0, 0, 0, 0, -3, -3, 0, -2, -3, 0, 4, 2, -3, -2, -2, 0, 0, 0}, /* S */ { 0, 0, 0, 0, -1, 0, 0, -1, -1, -1, -2, 0, -1, -2, -1, 2, 4, -3, -2, 0, 0, 0, 0}, /* T */ { -2, -2, -3, -4, -2, -3, -4, -3, -1, -1, 0, -3, 0, 3, -3, -3, -3, 13, 3, -2, 0, 0, 0}, /* W */ { -2, -2, -2, -3, -1, -2, -3, -3, 2, -1, 0, -2, 0, 4, -3, -2, -2, 3, 8, -1, 0, 0, 0}, /* Y */ { 0, -3, -3, -4, 0, -2, -3, -3, -3, 3, 2, -3, 1, 0, -2, -2, 0, -2, -1, 4, 0, 0, 0}, /* V */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* B */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* Z */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* X */ }; for(int i = 0; i < 23; i++){ for(int j = 0; j < 23; j++){ ap->subm[i][j] = (float)(PFASUM43[i][j]); } } /* PFASUM43 is in 1/3 bit units (same scale as CorBLOSUM66). Jointly tuned with seq_weights=2.0 and vsm_amax=2.0 on BAliBASE to balance SP, TC, and F1. */ ap->gpo = 7.0F; ap->gpe = 1.25F; ap->tgpe = 1.0F; return OK; } /* PFASUM60 matrix (H = 0.4941 bit) from Keul, Hess, Goesele, Hamacher (2017) * BMC Bioinformatics 18:293. Derived from Pfam seed alignments (v29.0) with * 60% clustering threshold. 1/3 bit units — same scale as CorBLOSUM66. * Recommended as general-purpose matrix by the authors. */ int set_subm_gaps_PFASUM60(struct aln_param *ap) { /* A,R,N,D,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,B,Z,X */ int PFASUM60[23][23] = { /* A R N D C Q E G H I L K M F P S T W Y V B Z X */ { 5, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3, -3, 0, 0, 0, 0}, /* A */ { -1, 7, 0, -1, -4, 2, 0, -2, 1, -4, -3, 3, -2, -4, -2, -1, -1, -3, -2, -3, 0, 0, 0}, /* R */ { -2, 0, 7, 2, -3, 1, 0, 0, 1, -5, -4, 1, -3, -4, -1, 1, 0, -4, -2, -4, 0, 0, 0}, /* N */ { -2, -1, 2, 7, -5, 1, 3, -1, 0, -6, -6, 0, -4, -6, -1, 0, -1, -5, -4, -5, 0, 0, 0}, /* D */ { 0, -4, -3, -5, 14, -4, -5, -2, -2, -1, -1, -4, -1, -1, -4, 0, -1, -2, -1, 0, 0, 0, 0}, /* C */ { -1, 2, 1, 1, -4, 6, 2, -2, 1, -4, -3, 2, -1, -4, -1, 0, 0, -3, -2, -3, 0, 0, 0}, /* Q */ { -1, 0, 0, 3, -5, 2, 6, -2, 0, -5, -4, 1, -3, -5, -1, 0, -1, -5, -3, -4, 0, 0, 0}, /* E */ { 0, -2, 0, -1, -2, -2, -2, 8, -2, -5, -5, -2, -4, -5, -2, 0, -2, -4, -4, -4, 0, 0, 0}, /* G */ { -2, 1, 1, 0, -2, 1, 0, -2, 10, -4, -3, 0, -2, -1, -2, -1, -1, -1, 2, -3, 0, 0, 0}, /* H */ { -1, -4, -5, -6, -1, -4, -5, -5, -4, 6, 3, -4, 2, 1, -4, -3, -1, -2, -2, 4, 0, 0, 0}, /* I */ { -1, -3, -4, -6, -1, -3, -4, -5, -3, 3, 5, -4, 3, 2, -4, -4, -2, -1, -1, 1, 0, 0, 0}, /* L */ { -1, 3, 1, 0, -4, 2, 1, -2, 0, -4, -4, 6, -2, -5, -1, 0, 0, -4, -3, -3, 0, 0, 0}, /* K */ { -1, -2, -3, -4, -1, -1, -3, -4, -2, 2, 3, -2, 8, 1, -4, -2, -1, -1, -1, 1, 0, 0, 0}, /* M */ { -2, -4, -4, -6, -1, -4, -5, -5, -1, 1, 2, -5, 1, 8, -4, -3, -3, 3, 4, 0, 0, 0, 0}, /* F */ { -1, -2, -1, -1, -4, -1, -1, -2, -2, -4, -4, -1, -4, -4, 10, 0, -1, -4, -4, -3, 0, 0, 0}, /* P */ { 1, -1, 1, 0, 0, 0, 0, 0, -1, -3, -4, 0, -2, -3, 0, 5, 2, -4, -3, -2, 0, 0, 0}, /* S */ { 0, -1, 0, -1, -1, 0, -1, -2, -1, -1, -2, 0, -1, -3, -1, 2, 6, -3, -2, 0, 0, 0, 0}, /* T */ { -3, -3, -4, -5, -2, -3, -5, -4, -1, -2, -1, -4, -1, 3, -4, -4, -3, 14, 3, -2, 0, 0, 0}, /* W */ { -3, -2, -2, -4, -1, -2, -3, -4, 2, -2, -1, -3, -1, 4, -4, -3, -2, 3, 9, -2, 0, 0, 0}, /* Y */ { 0, -3, -4, -5, 0, -3, -4, -4, -3, 4, 1, -3, 1, 0, -3, -2, 0, -2, -2, 5, 0, 0, 0}, /* V */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* B */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* Z */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* X */ }; for(int i = 0; i < 23; i++){ for(int j = 0; j < 23; j++){ ap->subm[i][j] = (float)(PFASUM60[i][j]); } } /* PFASUM60 is in 1/3 bit units (same scale as CorBLOSUM66). Tuned with seq_weights=2.0 and vsm_amax=2.0 on BAliBASE. */ ap->gpo = 7.0F; ap->gpe = 1.25F; ap->tgpe = 1.0F; return OK; } int set_subm_gaps_DNA(struct aln_param *ap) { int i,j; for(i = 0; i < 5; i++){ for(j =0; j < 5;j++){ ap->subm[i][j] = -4; if(i == j){ ap->subm[i][j] = 5; } } } ap->gpo = 8; ap->gpe = 6; ap->tgpe = 0; return OK; } int set_subm_gaps_DNA_internal(struct aln_param *ap) { int i,j; for(i = 0; i < 5; i++){ for(j =0; j < 5;j++){ ap->subm[i][j] = -4; if(i == j){ ap->subm[i][j] = 5; } } } ap->gpo = 8; ap->gpe = 6; ap->tgpe = 8; return OK; } int set_subm_gaps_RNA(struct aln_param* ap) { int i,j; for(i = 0; i < 5; i++){ for(j =0; j < 5;j++){ ap->subm[i][j] = 283.0; } } // A 91 -114 -31 -123 0 -43 ap->subm[0][0] += 91.0; ap->subm[0][1] += -114.0; ap->subm[0][2] += -31.0; ap->subm[0][3] += -123.0; // C -114 100 -125 -31 0 -43 ap->subm[1][0] += -114.0; ap->subm[1][1] += 100.0; ap->subm[1][2] += -125.0; ap->subm[1][3] += -31.0; // G -31 -125 100 -114 0 -43 ap->subm[2][0] += -31.0; ap->subm[2][1] += -125.0; ap->subm[2][2] += 100.0; ap->subm[2][3] += -114.0; // T -123 -31 -114 91 0 -43 ap->subm[3][0] += -123.0; ap->subm[3][1] += -31.0; ap->subm[3][2] += -114.0; ap->subm[3][3] += 91.0; ap->gpo = 217.0; ap->gpe = 39.4; ap->tgpe = 292.6; return OK; } void aln_param_free(struct aln_param *ap) { if(ap){ if(ap->subm){ for (int i = 23;i--;){ MFREE(ap->subm[i]); } MFREE(ap->subm); } MFREE(ap); } } kalign-3.5.1/lib/src/aln_param.h000066400000000000000000000025721515023132300164260ustar00rootroot00000000000000#ifndef ALN_PARAM_H #define ALN_PARAM_H #ifdef ALN_PARAM_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif /* #define KALIGN_DNA 0 */ /* #define KALIGN_DNA_INTERNAL 1 */ /* #define KALIGN_RNA 2 */ /* #define KALIGN_PROTEIN 3 */ struct aln_param{ int nthreads; /* actual parameters */ float** subm; float gpo; float gpe; float tgpe; float score; float dist_scale; /* distance-dependent gap scaling: 0=off, >0 scales gap penalties down for divergent pairs */ float vsm_amax; /* variable scoring matrix: 0=off, >0 subtracts a(d)=max(0,amax-d) from subm scores */ float subm_offset; /* computed per alignment step: amount to subtract from substitution scores */ int adaptive_budget; /* 0=off, 1=scale trial count by uncertainty */ float use_seq_weights; /* 0=off, >0=pseudocount for profile rebalancing */ int consistency_anchors; /* 0=off, >0=number of anchor sequences K for consistency */ float consistency_weight; /* bonus scale for consistency (default: 2.0) */ }; EXTERN int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int type, float gpo, float gpe, float tgpe); EXTERN void aln_param_free(struct aln_param* ap); #undef ALN_PARAM_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_profileprofile.c000066400000000000000000000366711515023132300203510ustar00rootroot00000000000000#include #include #include "tldevel.h" #include "aln_param.h" #include "aln_struct.h" #define ALN_PROFILEPROFILE_IMPORT #include "aln_profileprofile.h" #define MAX(a, b) (a > b ? a : b) #define MAX3(a,b,c) MAX(MAX(a,b),c) int aln_profileprofile_foward(struct aln_mem* m) { unsigned int freq[24]; const float* restrict prof1 = m->prof1; const float* restrict prof2 = m->prof2; struct states* restrict s = m->f; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; register int c = 0; register int f = 0; prof1 += (m->starta) << 6; prof2 += (m->startb) << 6; s[m->startb].a = s[0].a; s[m->startb].ga = s[0].ga; s[m->startb].gb = s[0].gb; if(m->startb){ for (j = m->startb+1; j < m->endb;j++){ prof2+=64; s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga+prof2[28],s[j-1].a+prof2[27]); s[j].gb = -FLT_MAX; } prof2+=64; }else{ for (j = m->startb+1; j < m->endb;j++){ prof2+=64; s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga,s[j-1].a)+prof2[29]; s[j].gb = -FLT_MAX; } prof2+=64; } prof2 -= (m->endb-m->startb) << 6; s[m->endb].a = -FLT_MAX; s[m->endb].ga = -FLT_MAX; s[m->endb].gb = -FLT_MAX; for (i = m->starta;i < m->enda;i++){ prof1 += 64; //c = 1; f = 0; for (j = 0;j < 23; j++){ if(prof1[j]){ freq[f] = j; f++; } } f--; //freq[0] = c; pa = s[m->startb].a; pga = s[m->startb].ga; pgb = s[m->startb].gb; s[m->startb].a = -FLT_MAX; s[m->startb].ga = -FLT_MAX; xa = s[m->startb].a; xga = s[m->startb].ga; if(m->startb){ s[m->startb].gb = MAX(pgb+prof1[28],pa+prof1[27]); }else{ s[m->startb].gb = MAX(pgb,pa)+ prof1[29]; } for (j = m->startb+1; j < m->endb;j++){ prof2 += 64; ca = s[j].a; pa = MAX3(pa,pga + prof2[-37],pgb + prof1[-37]); prof2 += 32; for (c = f;c >= 0;c--){ //for (c = 0;c < f;c++){ //for (c = 1;c < freq[0];c++){ pa += prof1[freq[c]]*prof2[freq[c]]; } prof2 -= 32; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j-1].ga+prof2[28],s[j-1].a+prof2[27]); s[j].ga = MAX(xga+prof2[28],xa+prof2[27]); pgb = s[j].gb; s[j].gb = MAX(pgb+prof1[28] ,ca+prof1[27]); pa = ca; xa = s[j].a; xga = s[j].ga; } prof2 += 64; ca = s[j].a; pa = MAX3(pa,pga + prof2[-37],pgb + prof1[-37]); prof2 += 32; for (c = f;c >= 0;c--){ pa += prof1[freq[c]]*prof2[freq[c]]; } prof2 -= 32; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; s[j].ga = -FLT_MAX; if (m->endb != m->len_b){ s[j].gb = MAX(s[j].gb+prof1[28] ,ca+prof1[27]); }else{ s[j].gb = MAX(s[j].gb,ca)+ prof1[29]; } prof2 -= (m->endb-m->startb) << 6; } //prof1 -= (m->enda) << 6; return OK; } int aln_profileprofile_backward(struct aln_mem* m) { unsigned int freq[24]; struct states* restrict s = m->b; const float* restrict prof1 = m->prof1; const float* restrict prof2 = m->prof2; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; register int c = 0; register int f = 0; prof1 += (m->enda_2 +1) << 6; prof2 += (m->endb+1) << 6; s[m->endb].a = s[0].a; s[m->endb].ga = s[0].ga; s[m->endb].gb = s[0].gb; if(m->endb != m->len_b){ for(j = m->endb-1;j > m->startb;j--){ prof2 -= 64; s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga+prof2[28],s[j+1].a+prof2[27]); s[j].gb = -FLT_MAX; } prof2 -= 64; }else{ for(j = m->endb-1;j > m->startb;j--){ prof2 -= 64; s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga,s[j+1].a)+prof2[29]; s[j].gb = -FLT_MAX; } prof2 -= 64; } s[m->startb].a = -FLT_MAX; s[m->startb].ga = -FLT_MAX; s[m->startb].gb = -FLT_MAX; i = m->enda_2-m->starta_2; while(i--){ prof1 -= 64; f = 0; for (j = 0;j < 23; j++){ if(prof1[j]){ freq[f] = j; f++; } } f--; //freq[0] = c; pa = s[m->endb].a; pga = s[m->endb].ga; pgb = s[m->endb].gb; s[m->endb].a = -FLT_MAX; s[m->endb].ga = -FLT_MAX; xa = s[m->endb].a; xga = s[m->endb].ga; if(m->endb != m->len_b){ s[m->endb].gb = MAX(pgb+prof1[28] ,pa+prof1[27]); }else{ s[m->endb].gb = MAX(pgb,pa)+prof1[29]; } prof2 += (m->endb-m->startb) << 6; for(j = m->endb-1;j > m->startb;j--){ prof2 -= 64; ca = s[j].a; pa = MAX3(pa,pga + prof2[91],pgb + prof1[91]); prof2 += 32; for (c = f;c >= 0;c--){ //for (c = 0;c < f;c++){ //for (c = 1;c < freq[0];c++){ pa += prof1[freq[c]]*prof2[freq[c]]; } prof2 -= 32; if(m->consistency){ pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j+1].ga+prof2[28], s[j+1].a+prof2[27]); s[j].ga = MAX(xga+prof2[28], xa+prof2[27]); pgb = s[j].gb; s[j].gb = MAX(pgb+prof1[28], ca+prof1[27]); pa = ca; xa = s[j].a; xga = s[j].ga; } prof2 -= 64; ca = s[j].a; pa = MAX3(pa,pga + prof2[91],pgb + prof1[91]); prof2 += 32; for (c = f;c >= 0;c--){ //for (c = 0;c < f;c++){ pa += prof1[freq[c]]*prof2[freq[c]]; } prof2 -= 32; if(m->consistency){ pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; } s[j].a = pa; //pga = s[j].ga; s[j].ga = -FLT_MAX;//MAX(s[j+1].ga+prof2[28], s[j+1].a+prof2[27]); //pgb = s[j].gb; if(m->startb){ s[j].gb = MAX(s[j].gb+prof1[28], ca+prof1[27]); }else{ s[j].gb = MAX(s[j].gb,ca)+prof1[29]; } //pa = ca; } return OK; } int aln_profileprofile_meetup(struct aln_mem* m,int old_cor[], int* meet,int* t,float* score) { struct states* f = m->f; struct states* b = m->b; int i; int c; int c2 = -1; int transition = -1; int transition2 = -1; float s_tmp; const float* prof1 = m->prof1; const float* prof2 = m->prof2; //code: // a -> a = 1 // a -> ga = 2 // a -> gb = 3 // ga ->ga = 4 // ga -> a = 5 //gb->gb = 6; //gb->a = 7; //int max = -FLT_MAX; float max = -FLT_MAX; float max2 = -FLT_MAX; //float middle = (m->endb - m->startb)/2 + m->startb; float middle = (float) (old_cor[3] - old_cor[2])/2.0F + (float) old_cor[2]; float sub = 0.0F; prof1+= ((old_cor[4]+1) << 6); //prof2 += 64 * (m->startb); //i = m->startb; prof2 += old_cor[2] << 6; c = -1; //for(i = m->startb; i < m->endb;i++){ for(i = old_cor[2]; i < old_cor[3];i++){ sub = fabsf(middle - (float)i); sub /= 1000.0F; prof2 += 64; s_tmp = f[i].a+b[i].a-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 1; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 1; } s_tmp = f[i].a+b[i].ga+prof2[27]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 2; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 2; } s_tmp = f[i].a+b[i].gb+prof1[27]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } s_tmp = f[i].ga+b[i].a+prof2[-37]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 5; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 5; } if(m->startb == 0){ s_tmp = f[i].gb+b[i].gb+prof1[29]-sub; }else{ s_tmp = f[i].gb+b[i].gb+prof1[28]-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } s_tmp = f[i].gb+b[i].a+prof1[-37]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 7; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 7; } } //i = m->endb; i = old_cor[3]; sub = fabsf(middle - (float)i); sub /= 1000.0F; s_tmp = f[i].a+b[i].gb+prof1[27]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } if(m->endb == m->len_b){ s_tmp = f[i].gb+b[i].gb+prof1[29]-sub; }else{ s_tmp = f[i].gb+b[i].gb+prof1[28]-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } /* Accumulate confidence margin and record per-meetup margins */ if(max2 > -FLT_MAX){ float _margin = max - max2; if(m->flip_margins != NULL && m->margin_count < m->flip_margin_alloc){ m->flip_margins[m->margin_count] = _margin; } m->margin_sum += _margin; m->margin_count++; } /* Perturbation: flip uncertain midpoints to second-best choice. Three modes: individual targeting (MCTS), stride bitmask, or round-robin. */ if(m->flip_threshold > 0.0F && c2 >= 0 && max2 > -FLT_MAX){ float margin = max - max2; if(margin < m->flip_threshold){ if(m->flip_bit_map != NULL){ /* Individual midpoint targeting (MCTS) */ if(m->flip_counter < m->flip_n_uncertain){ int bit = m->flip_bit_map[m->flip_counter]; if(bit >= 0 && ((1U << bit) & m->flip_mask)){ c = c2; transition = transition2; } } }else if(m->flip_mask != 0){ /* Stride-based bitmask mode */ if((1U << (m->flip_counter % m->flip_stride)) & m->flip_mask){ c = c2; transition = transition2; } }else if(m->flip_trial > 0){ /* Round-robin mode */ if(m->flip_counter % m->flip_stride == m->flip_trial - 1){ c = c2; transition = transition2; } } m->flip_counter++; } } *meet = c; *t = transition; *score = max; return OK; } kalign-3.5.1/lib/src/aln_profileprofile.h000066400000000000000000000007531515023132300203460ustar00rootroot00000000000000#ifndef ALN_PROFILEPROFILE_H #define ALN_PROFILEPROFILE_H #ifdef ALN_PROFILEPROFILE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_mem; EXTERN int aln_profileprofile_foward(struct aln_mem* m); EXTERN int aln_profileprofile_backward(struct aln_mem* m); EXTERN int aln_profileprofile_meetup(struct aln_mem* m,int old_cor[], int* meet,int* t,float* score); #undef ALN_PROFILEPROFILE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_refine.c000066400000000000000000000641521515023132300165730ustar00rootroot00000000000000#include "tldevel.h" #include #include "msa_struct.h" #include "task.h" #include "aln_param.h" #include "aln_struct.h" #include "aln_mem.h" #include "aln_setup.h" #include "aln_controller.h" #include "weave_alignment.h" #include "sp_score.h" #include "aln_run.h" #include "anchor_consistency.h" #define ALN_REFINE_IMPORT #include "aln_refine.h" #define REFINE_N_TRIALS 5 static int refine_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int task_id); static int replay_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int task_id); static int dispatch_alignment(struct msa* msa, struct aln_mem* ml, float* prof_a, float* prof_b, int a, int b, int len_a, int len_b); static int convert_raw_path(struct aln_mem* m); static int compute_confidence_threshold(struct aln_tasks* t, float* threshold); /* Leaf weight for make_profile_n — always 1.0 since sequence weighting is handled via balanced freq-count merging in update_n. */ static float leaf_weight(struct msa* msa, int node) { (void)msa; (void)node; return 1.0f; } int refine_alignment(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int refine_mode) { int i; float threshold = 0.0F; if(refine_mode == KALIGN_REFINE_NONE){ return OK; } RUN(sort_tasks(t, TASK_ORDER_TREE)); if(refine_mode == KALIGN_REFINE_CONFIDENT){ RUN(compute_confidence_threshold(t, &threshold)); } /* Reset alignment state: zero all gaps and restore leaf sip/nsip */ RUN(clean_aln(msa)); /* Clear stale profiles from the initial alignment */ for(i = 0; i < msa->num_profiles; i++){ if(t->profile[i]){ MFREE(t->profile[i]); t->profile[i] = NULL; } } /* Process all edges bottom-up using progressive profile building. Each edge builds profiles from its children (identical to original progressive alignment), then refined edges run multi-trial DP. */ for(i = 0; i < t->n_tasks; i++){ int should_refine = 0; if(refine_mode == KALIGN_REFINE_ALL){ should_refine = 1; }else if(refine_mode == KALIGN_REFINE_CONFIDENT){ should_refine = (t->list[i]->confidence <= threshold); } if(should_refine){ RUN(refine_edge(msa, ap, t, i)); }else{ RUN(replay_edge(msa, ap, t, i)); } } return OK; ERROR: return FAIL; } /* Process an edge with multi-trial DP: builds progressive profiles (identical to do_align), runs K alignment trials (one deterministic baseline plus threshold-based alternatives), keeps the best by SP score. */ int refine_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int task_id) { struct aln_mem* ml = NULL; float* tmp = NULL; int* best_path = NULL; int a, b, c; int len_a, len_b; int k, j, g; float best_sp = -FLT_MAX; float best_margin_sum = 0.0F; int best_margin_count = 0; float avg_margin = 0.0F; int n_trials = REFINE_N_TRIALS; a = t->list[task_id]->a; b = t->list[task_id]->b; c = t->list[task_id]->c; /* Distance-dependent parameter scaling (must match do_align) */ struct aln_param* orig_ap = ap; struct aln_param scaled_ap; float gap_scale = compute_gap_scale(msa, ap, a, b); float subm_off = compute_subm_offset(msa, ap, a, b); if(gap_scale < 1.0f || subm_off > 0.0f){ scaled_ap = *ap; scaled_ap.gpo *= gap_scale; scaled_ap.gpe *= gap_scale; scaled_ap.tgpe *= gap_scale; scaled_ap.subm_offset = subm_off; ap = &scaled_ap; } /* Build profiles progressively (same as do_align) */ if(msa->nsip[a] == 1){ len_a = msa->sequences[a]->len; RUN(make_profile_n(ap, msa->sequences[a]->s, len_a, leaf_weight(msa,a), &t->profile[a])); }else{ len_a = msa->plen[a]; RUN(set_gap_penalties_n(t->profile[a], len_a, msa->nsip[b])); } if(msa->nsip[b] == 1){ len_b = msa->sequences[b]->len; RUN(make_profile_n(ap, msa->sequences[b]->s, len_b, leaf_weight(msa,b), &t->profile[b])); }else{ len_b = msa->plen[b]; RUN(set_gap_penalties_n(t->profile[b], len_b, msa->nsip[a])); } /* Allocate alignment memory */ RUN(alloc_aln_mem(&ml, 256)); ml->ap = ap; ml->mode = ALN_MODE_FULL; ml->len_a = len_a; ml->len_b = len_b; ml->margin_sum = 0.0F; ml->margin_count = 0; RUN(init_alnmem(ml)); /* Allocate best_path buffer */ MMALLOC(best_path, sizeof(int) * ml->alloc_path_len); /* Compute consistency bonus for all merge types */ ml->consistency = NULL; ml->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ int dp_row_node, dp_col_node, dp_rows, dp_cols; if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } }else if(msa->nsip[a] == 1){ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; }else if(msa->nsip[b] == 1){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } } RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &ml->consistency)); ml->consistency_stride = dp_cols; } } /* If adaptive budget, allocate flip_margins for baseline trial to record per-meetup margins for uncertainty analysis */ if(ap->adaptive_budget){ int est = MACRO_MIN(len_a, len_b) + 1; if(est < 64) est = 64; MMALLOC(ml->flip_margins, sizeof(float) * est); ml->flip_margin_alloc = est; } /* Multi-trial alignment: trial 0 is deterministic (baseline), trials 1..n_trials-1 use increasing flip thresholds. */ for(k = 0; k < n_trials; k++){ float sp = 0.0F; /* Re-initialize DP state before each trial. The Hirschberg recursion only writes matched positions to the path array; unmatched positions must be -1. After convert_raw_path swaps path/tmp_path, the buffer contains stale 0/1/2 values. */ { int _g = MACRO_MAX(len_a, len_b) + 2; int _i; for(_i = 0; _i < _g; _i++){ ml->path[_i] = -1; } } ml->starta = 0; ml->startb = 0; ml->enda = len_a; ml->endb = len_b; ml->len_a = len_a; ml->len_b = len_b; ml->f[0].a = 0.0F; ml->f[0].ga = -FLT_MAX; ml->f[0].gb = -FLT_MAX; ml->b[0].a = 0.0F; ml->b[0].ga = -FLT_MAX; ml->b[0].gb = -FLT_MAX; ml->margin_sum = 0.0F; ml->margin_count = 0; if(k == 0){ ml->flip_threshold = 0.0F; ml->flip_trial = 0; }else{ ml->flip_threshold = avg_margin; ml->flip_trial = k; ml->flip_stride = n_trials - 1; ml->flip_counter = 0; } RUN(dispatch_alignment(msa, ml, t->profile[a], t->profile[b], a, b, len_a, len_b)); /* Convert raw DP path to 0/1/2 format with gap info */ RUN(convert_raw_path(ml)); /* Score this candidate with sum-of-pairs */ RUN(compute_sp_score(msa, ap, ml->path, msa->sip[a], msa->nsip[a], msa->sip[b], msa->nsip[b], &sp)); if(sp > best_sp){ best_sp = sp; best_margin_sum = ml->margin_sum; best_margin_count = ml->margin_count; memcpy(best_path, ml->path, sizeof(int) * (ml->path[0] + 2)); } /* After baseline trial, compute avg_margin and adaptive budget */ if(k == 0){ if(ml->margin_count > 0){ avg_margin = ml->margin_sum / (float)ml->margin_count; } /* Adaptive budget: count very uncertain meetups */ if(ap->adaptive_budget && ml->flip_margins && ml->margin_count > 0){ int n_very_uncertain = 0; int m_i; float vu_threshold = avg_margin * 0.25F; for(m_i = 0; m_i < ml->margin_count; m_i++){ if(ml->flip_margins[m_i] < vu_threshold){ n_very_uncertain++; } } { float frac = (float)n_very_uncertain / (float)ml->margin_count; n_trials = 1 + (int)(7.0F * frac + 0.5F); } } /* Free flip_margins after extracting stats */ if(ml->flip_margins){ MFREE(ml->flip_margins); ml->flip_margins = NULL; } } } /* Restore best path into ml */ memcpy(ml->path, best_path, sizeof(int) * (best_path[0] + 2)); /* Free consistency bonus */ if(ml->consistency){ MFREE(ml->consistency); ml->consistency = NULL; ml->consistency_stride = 0; } /* Update confidence from best trial */ if(best_margin_count > 0){ t->list[task_id]->confidence = best_margin_sum / (float)best_margin_count; }else{ t->list[task_id]->confidence = 0.0F; } /* Restore original aln_param for profile update (unscaled base penalties) */ ap = orig_ap; /* Merge profiles for downstream edges */ MMALLOC(tmp, sizeof(float) * 64 * (ml->path[0] + 2)); if(task_id != t->n_tasks - 1){ update_n(t->profile[a], t->profile[b], tmp, ap, ml->path, msa->nsip[a], msa->nsip[b]); } t->profile[c] = tmp; /* Update gap arrays for all member sequences */ RUN(make_seq(msa, a, b, ml->path)); msa->plen[c] = ml->path[0]; msa->nsip[c] = msa->nsip[a] + msa->nsip[b]; MREALLOC(msa->sip[c], sizeof(int) * (msa->nsip[a] + msa->nsip[b])); g = 0; for(j = msa->nsip[a]; j--;){ msa->sip[c][g] = msa->sip[a][j]; g++; } for(j = msa->nsip[b]; j--;){ msa->sip[c][g] = msa->sip[b][j]; g++; } /* Free child profiles */ MFREE(t->profile[a]); t->profile[a] = NULL; MFREE(t->profile[b]); t->profile[b] = NULL; /* Cleanup */ MFREE(best_path); free_aln_mem(ml); return OK; ERROR: if(best_path){ MFREE(best_path); } free_aln_mem(ml); return FAIL; } /* Process an edge using standard progressive profiles (identical to the original do_align in aln_run.c). Used for high-confidence edges in REFINE_CONFIDENT mode and as the default replay path. */ int replay_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int task_id) { struct aln_mem* ml = NULL; float* tmp = NULL; int a, b, c; int len_a, len_b; int j, g; a = t->list[task_id]->a; b = t->list[task_id]->b; c = t->list[task_id]->c; /* Distance-dependent parameter scaling (must match do_align) */ struct aln_param* orig_ap = ap; struct aln_param scaled_ap; float gap_scale = compute_gap_scale(msa, ap, a, b); float subm_off = compute_subm_offset(msa, ap, a, b); if(gap_scale < 1.0f || subm_off > 0.0f){ scaled_ap = *ap; scaled_ap.gpo *= gap_scale; scaled_ap.gpe *= gap_scale; scaled_ap.tgpe *= gap_scale; scaled_ap.subm_offset = subm_off; ap = &scaled_ap; } /* Build profiles (standard progressive approach) */ if(msa->nsip[a] == 1){ len_a = msa->sequences[a]->len; RUN(make_profile_n(ap, msa->sequences[a]->s, len_a, leaf_weight(msa,a), &t->profile[a])); }else{ len_a = msa->plen[a]; RUN(set_gap_penalties_n(t->profile[a], len_a, msa->nsip[b])); } if(msa->nsip[b] == 1){ len_b = msa->sequences[b]->len; RUN(make_profile_n(ap, msa->sequences[b]->s, len_b, leaf_weight(msa,b), &t->profile[b])); }else{ len_b = msa->plen[b]; RUN(set_gap_penalties_n(t->profile[b], len_b, msa->nsip[a])); } /* Allocate alignment memory and run alignment */ RUN(alloc_aln_mem(&ml, 256)); ml->ap = ap; ml->mode = ALN_MODE_FULL; ml->len_a = len_a; ml->len_b = len_b; ml->margin_sum = 0.0F; ml->margin_count = 0; RUN(init_alnmem(ml)); /* Compute consistency bonus for all merge types */ ml->consistency = NULL; ml->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ int dp_row_node, dp_col_node, dp_rows, dp_cols; if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } }else if(msa->nsip[a] == 1){ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; }else if(msa->nsip[b] == 1){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } } RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &ml->consistency)); ml->consistency_stride = dp_cols; } } RUN(dispatch_alignment(msa, ml, t->profile[a], t->profile[b], a, b, len_a, len_b)); /* Free consistency bonus */ if(ml->consistency){ MFREE(ml->consistency); ml->consistency = NULL; ml->consistency_stride = 0; } /* Store alignment confidence */ if(ml->margin_count > 0){ t->list[task_id]->confidence = ml->margin_sum / (float)ml->margin_count; }else{ t->list[task_id]->confidence = 0.0F; } RUN(convert_raw_path(ml)); /* Restore original aln_param for profile update (unscaled base penalties) */ ap = orig_ap; /* Merge profiles for downstream edges */ MMALLOC(tmp, sizeof(float) * 64 * (ml->path[0] + 2)); if(task_id != t->n_tasks - 1){ update_n(t->profile[a], t->profile[b], tmp, ap, ml->path, msa->nsip[a], msa->nsip[b]); } t->profile[c] = tmp; /* Update gap arrays for all member sequences */ RUN(make_seq(msa, a, b, ml->path)); msa->plen[c] = ml->path[0]; msa->nsip[c] = msa->nsip[a] + msa->nsip[b]; MREALLOC(msa->sip[c], sizeof(int) * (msa->nsip[a] + msa->nsip[b])); g = 0; for(j = msa->nsip[a]; j--;){ msa->sip[c][g] = msa->sip[a][j]; g++; } for(j = msa->nsip[b]; j--;){ msa->sip[c][g] = msa->sip[b][j]; g++; } /* Free consumed profiles */ MFREE(t->profile[a]); t->profile[a] = NULL; MFREE(t->profile[b]); t->profile[b] = NULL; free_aln_mem(ml); return OK; ERROR: free_aln_mem(ml); return FAIL; } /* Dispatch alignment based on group types (seq-seq, seq-profile, profile-profile). Handles the shorter-first convention and path mirroring. */ int dispatch_alignment(struct msa* msa, struct aln_mem* ml, float* prof_a, float* prof_b, int a, int b, int len_a, int len_b) { if(msa->nsip[a] == 1){ if(msa->nsip[b] == 1){ /* seq vs seq */ if(len_a < len_b){ ml->seq1 = msa->sequences[a]->s; ml->seq2 = msa->sequences[b]->s; ml->prof1 = NULL; ml->prof2 = NULL; aln_runner(ml); }else{ ml->enda = len_b; ml->endb = len_a; ml->len_a = len_b; ml->len_b = len_a; ml->seq1 = msa->sequences[b]->s; ml->seq2 = msa->sequences[a]->s; ml->prof1 = NULL; ml->prof2 = NULL; aln_runner(ml); RUN(mirror_path_n(ml, len_a, len_b)); ml->len_a = len_a; ml->len_b = len_b; } }else{ /* seq a vs profile b: profile must be prof1 */ ml->enda = len_b; ml->endb = len_a; ml->len_a = len_b; ml->len_b = len_a; ml->seq1 = NULL; ml->seq2 = msa->sequences[a]->s; ml->prof1 = prof_b; ml->prof2 = NULL; ml->sip = msa->nsip[b]; aln_runner(ml); RUN(mirror_path_n(ml, len_a, len_b)); ml->len_a = len_a; ml->len_b = len_b; } }else{ if(msa->nsip[b] == 1){ /* profile a vs seq b */ ml->seq1 = NULL; ml->seq2 = msa->sequences[b]->s; ml->prof1 = prof_a; ml->prof2 = NULL; ml->sip = msa->nsip[a]; aln_runner(ml); }else{ /* profile vs profile */ if(len_a < len_b){ ml->seq1 = NULL; ml->seq2 = NULL; ml->prof1 = prof_a; ml->prof2 = prof_b; aln_runner(ml); }else{ ml->enda = len_b; ml->endb = len_a; ml->len_a = len_b; ml->len_b = len_a; ml->seq1 = NULL; ml->seq2 = NULL; ml->prof1 = prof_b; ml->prof2 = prof_a; aln_runner(ml); RUN(mirror_path_n(ml, len_a, len_b)); ml->len_a = len_a; ml->len_b = len_b; } } } return OK; ERROR: return FAIL; } /* Convert raw Hirschberg path (path[i] = B position or -1) to the 0/1/2 format with gap info bits that update_n and make_seq expect. This replaces add_gap_info_to_path_n for refinement trials. The original has a latent bug: it tracks the last B position via `b = path[i]`, which resets b to -1 on gap-in-B entries. When a later match skips B positions, the gap-in-A fill is skipped because `b == -1`. Stochastic Hirschberg sampling triggers this by producing interleaved gap patterns. This function tracks b_last correctly: only updated on actual matches (path[i] != -1). */ int convert_raw_path(struct aln_mem* m) { int* path = NULL; int* o_path = NULL; int* tmp_path = NULL; int i, j, a; int b_last; int len_a; int len_b; len_a = m->len_a; len_b = m->len_b; path = m->path; o_path = m->tmp_path; for(i = 0; i < len_a + len_b + 2; i++){ o_path[i] = 0; } j = 1; b_last = 0; /* last consumed B position (1-indexed), 0 = none */ for(i = 1; i <= len_a; i++){ if(path[i] == -1){ o_path[j] = 2; j++; }else{ /* Fill gap-in-A for B positions b_last+1..path[i]-1 */ for(a = b_last + 1; a < path[i]; a++){ o_path[j] = 1; j++; } o_path[j] = 0; j++; b_last = path[i]; } } /* Trailing gap-in-A for remaining B positions */ for(a = b_last + 1; a <= len_b; a++){ o_path[j] = 1; j++; } o_path[0] = j - 1; o_path[j] = 3; /* Add gap info bits */ i = 2; while(o_path[i] != 3){ if ((o_path[i-1] & 3) && !(o_path[i] & 3)){ if(o_path[i-1] & 8){ o_path[i-1] += 8; }else{ o_path[i-1] |= 16; } }else if (!(o_path[i-1] & 3) && (o_path[i] & 3)){ o_path[i] |= 4; }else if ((o_path[i-1] & 1) && (o_path[i] & 1)){ o_path[i] |= 8; }else if ((o_path[i-1] & 2) && (o_path[i] & 2)){ o_path[i] |= 8; } i++; } /* Add terminal gap flags */ i = 1; while(o_path[i] != 0){ o_path[i] |= 32; i++; } i = o_path[0]; while(o_path[i] != 0){ o_path[i] |= 32; i--; } tmp_path = m->path; m->path = m->tmp_path; m->tmp_path = tmp_path; return OK; } int compute_confidence_threshold(struct aln_tasks* t, float* threshold) { float* confidences = NULL; int n, i, j; float tmp; n = t->n_tasks; ASSERT(n > 0, "No tasks"); MMALLOC(confidences, sizeof(float) * n); for(i = 0; i < n; i++){ confidences[i] = t->list[i]->confidence; } /* Insertion sort for median */ for(i = 1; i < n; i++){ tmp = confidences[i]; j = i - 1; while(j >= 0 && confidences[j] > tmp){ confidences[j + 1] = confidences[j]; j--; } confidences[j + 1] = tmp; } if(n % 2 == 0){ *threshold = (confidences[n / 2 - 1] + confidences[n / 2]) / 2.0F; }else{ *threshold = confidences[n / 2]; } MFREE(confidences); return OK; ERROR: if(confidences){ MFREE(confidences); } return FAIL; } kalign-3.5.1/lib/src/aln_refine.h000066400000000000000000000007671515023132300166020ustar00rootroot00000000000000#ifndef ALN_REFINE_H #define ALN_REFINE_H #ifdef ALN_REFINE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #define KALIGN_REFINE_NONE 0 #define KALIGN_REFINE_ALL 1 #define KALIGN_REFINE_CONFIDENT 2 #define KALIGN_REFINE_INLINE 3 struct msa; struct aln_param; struct aln_tasks; EXTERN int refine_alignment(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int refine_mode); #undef ALN_REFINE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_run.c000066400000000000000000000703501515023132300161240ustar00rootroot00000000000000#include "tldevel.h" #include "tlrng.h" #include "msa_struct.h" #ifdef HAVE_OPENMP #include #endif #include "task.h" #include "aln_param.h" #include "aln_struct.h" #include "aln_mem.h" #include "aln_setup.h" #include "aln_controller.h" #include "weave_alignment.h" /* #include "weave_alignment.h" */ #include "sp_score.h" #include "anchor_consistency.h" #include #include #define ALN_RUN_IMPORT #include "aln_run.h" static void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, uint8_t* active, int c); /* static void recursive_aln_openMP(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, uint8_t* active, int c); */ /* static void recursive_aln_serial(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, uint8_t* active, int c); */ static int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id); static int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, struct aln_mem* m, int task_id, int n_trials); static void recursive_aln_inline(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, uint8_t* active, int c, int n_trials); /* static int do_align_serial(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id); */ /* static int do_score(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id); */ /* static int SampleWithoutReplacement(struct rng_state* rng, int N, int n,int* samples); */ /* static int int_cmp(const void *a, const void *b); */ int create_msa_tree(struct msa* msa, struct aln_param* ap,struct aln_tasks* t) { int i; uint8_t* active = NULL; RUN(sort_tasks(t, TASK_ORDER_TREE)); MMALLOC(active, sizeof(uint8_t)* msa->num_profiles); for(i = 0; i < msa->numseq;i++){ active[i] = 1; } for(i = msa->numseq; i < msa->num_profiles;i++){ active[i] = 0; } /* LOG_MSG("Setting threads to 1 for debugging!"); */ /* ap->nthreads = 1; */ msa->run_parallel = 1; if(ap->nthreads == 1){ msa->run_parallel = 0; } #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait #endif recursive_aln(msa, t, ap, active, t->n_tasks-1); MFREE(active); return OK; ERROR: if(active){ MFREE(active); } return FAIL; } void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, uint8_t* active, int c) { struct task* local_t = NULL; /* Follow left and right branch until I arrive at sequences / profiles ready to align. */ int a; int b; local_t = t->list[c]; a = local_t->a - msa->numseq; b = local_t->b - msa->numseq; if(!active[local_t->a] && local_t->a >= msa->numseq){ #ifdef HAVE_OPENMP #pragma omp task shared(msa,t,ap,active) firstprivate(a) #endif recursive_aln(msa, t, ap, active, a); } if(!active[local_t->b] && local_t->b >= msa->numseq){ #ifdef HAVE_OPENMP #pragma omp task shared(msa,t,ap,active) firstprivate(b) #endif recursive_aln(msa, t, ap, active, b); } #ifdef HAVE_OPENMP #pragma omp taskwait #endif struct aln_mem* ml = NULL; alloc_aln_mem(&ml, 256); ml->ap = ap; ml->mode = ALN_MODE_FULL; do_align(msa,t,ml,c); active[local_t->a] = 0; active[local_t->b] = 0; active[local_t->c] = 1; /* LOG_MSG("Local: %d %d %d p:%d", local_t->a, local_t->b, local_t->c, local_t->p); */ free_aln_mem(ml); } float compute_gap_scale(struct msa* msa, struct aln_param* ap, int a, int b) { float ds = ap->dist_scale; if(ds <= 0.0f || msa->seq_distances == NULL){ return 1.0f; } /* Compute mean normalized distance for all sequences in both clusters */ float sum = 0.0f; int count = 0; int i; for(i = 0; i < msa->nsip[a]; i++){ int si = msa->sip[a][i]; if(si < msa->numseq){ sum += msa->seq_distances[si]; count++; } } for(i = 0; i < msa->nsip[b]; i++){ int si = msa->sip[b][i]; if(si < msa->numseq){ sum += msa->seq_distances[si]; count++; } } if(count == 0){ return 1.0f; } float avg_div = sum / (float)count; /* Scale: 1.0 for similar sequences, decreasing for divergent. scale = max(0.3, 1.0 - dist_scale * avg_div) With dist_scale=0.5 and avg_div=1.0 (fully divergent): scale=0.5 With dist_scale=0.5 and avg_div=0.0 (identical): scale=1.0 */ float scale = 1.0f - ds * avg_div; if(scale < 0.3f) scale = 0.3f; if(scale > 1.0f) scale = 1.0f; return scale; } float compute_subm_offset(struct msa* msa, struct aln_param* ap, int a, int b) { float amax = ap->vsm_amax; if(amax <= 0.0f || msa->seq_distances == NULL){ return 0.0f; } /* Compute mean normalized distance for all sequences in both clusters */ float sum = 0.0f; int count = 0; int i; for(i = 0; i < msa->nsip[a]; i++){ int si = msa->sip[a][i]; if(si < msa->numseq){ sum += msa->seq_distances[si]; count++; } } for(i = 0; i < msa->nsip[b]; i++){ int si = msa->sip[b][i]; if(si < msa->numseq){ sum += msa->seq_distances[si]; count++; } } if(count == 0){ return 0.0f; } float avg_div = sum / (float)count; /* MAFFT VSM: a(d) = max(0, amax - d) Close sequences (small d) -> large offset -> more stringent scoring Distant sequences (large d) -> small/zero offset -> original scoring */ float offset = amax - avg_div; if(offset < 0.0f) offset = 0.0f; return offset; } /* Leaf weight for make_profile_n — always 1.0 since sequence weighting is handled via balanced freq-count merging in update_n. */ static float leaf_weight(struct msa* msa, int node) { (void)msa; (void)node; return 1.0f; } int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) { float* tmp = NULL; int a,b,c; int len_a; int len_b; int j,g; a = t->list[task_id]->a; b = t->list[task_id]->b; c = t->list[task_id]->c; /* Distance-dependent parameter scaling */ struct aln_param* orig_ap = m->ap; struct aln_param scaled_ap; float gap_scale = compute_gap_scale(msa, m->ap, a, b); float subm_off = compute_subm_offset(msa, m->ap, a, b); if(gap_scale < 1.0f || subm_off > 0.0f){ scaled_ap = *m->ap; /* shallow copy — shares subm pointer */ scaled_ap.gpo *= gap_scale; scaled_ap.gpe *= gap_scale; scaled_ap.tgpe *= gap_scale; scaled_ap.subm_offset = subm_off; m->ap = &scaled_ap; } if(msa->nsip[a] == 1){ m->len_a = msa->sequences[a]->len;// aln->sl[a]; RUN(make_profile_n(m->ap, msa->sequences[a]->s,m->len_a,leaf_weight(msa,a),&t->profile[a])); }else{ m->len_a = msa->plen[a]; RUN(set_gap_penalties_n(t->profile[a],m->len_a,msa->nsip[b])); } if(msa->nsip[b] == 1){ m->len_b = msa->sequences[b]->len;// aln->sl[b]; RUN(make_profile_n(m->ap, msa->sequences[b]->s,m->len_b,leaf_weight(msa,b),&t->profile[b])); }else{ m->len_b = msa->plen[b]; RUN(set_gap_penalties_n(t->profile[b],m->len_b,msa->nsip[a])); } RUN(init_alnmem(m)); m->margin_sum = 0.0F; m->margin_count = 0; m->consistency = NULL; m->consistency_stride = 0; /* Compute consistency bonus for all merge types */ { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ int dp_row_node, dp_col_node, dp_rows, dp_cols; if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(m->len_a < m->len_b){ dp_row_node = a; dp_rows = m->len_a; dp_col_node = b; dp_cols = m->len_b; }else{ dp_row_node = b; dp_rows = m->len_b; dp_col_node = a; dp_cols = m->len_a; } }else if(msa->nsip[a] == 1){ dp_row_node = b; dp_rows = m->len_b; dp_col_node = a; dp_cols = m->len_a; }else if(msa->nsip[b] == 1){ dp_row_node = a; dp_rows = m->len_a; dp_col_node = b; dp_cols = m->len_b; }else{ if(m->len_a < m->len_b){ dp_row_node = a; dp_rows = m->len_a; dp_col_node = b; dp_cols = m->len_b; }else{ dp_row_node = b; dp_rows = m->len_b; dp_col_node = a; dp_cols = m->len_a; } } RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &m->consistency)); m->consistency_stride = dp_cols; } } m->mode = ALN_MODE_FULL; if(msa->nsip[a] == 1){ if(msa->nsip[b] == 1){ if(m->len_a < m->len_b){ m->seq1 = msa->sequences[a]->s; m->seq2 = msa->sequences[b]->s; /* LOG_MSG("%d %d", m->len_a, m->len_b); */ m->prof1 = NULL; m->prof2 = NULL; aln_runner(m); }else{ len_b = m->len_b; len_a = m->len_a; m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = msa->sequences[b]->s; m->seq2 = msa->sequences[a]->s; m->prof1 = NULL; m->prof2 = NULL; aln_runner(m); RUN(mirror_path_n(m,len_a,len_b)); m->len_a = len_a; m->len_b = len_b; } /* m->seq1 = msa->sequences[a]->s; */ /* m->seq2 = msa->sequences[b]->s; */ /* m->prof1 = NULL; */ /* m->prof2 = NULL; */ /* aln_runner(m); */ }else{ len_b = m->len_b; len_a = m->len_a; m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = NULL; m->seq2 = msa->sequences[a]->s; m->prof1 = t->profile[b]; m->prof2 = NULL; m->sip = msa->nsip[b]; aln_runner(m); RUN(mirror_path_n(m, len_a,len_b)); m->len_a = len_a; m->len_b = len_b; } }else{ if(msa->nsip[b] == 1){ m->seq1 = NULL; m->seq2 = msa->sequences[b]->s; m->prof1 = t->profile[a]; m->prof2 = NULL; m->sip = msa->nsip[a]; aln_runner(m); }else{ if(m->len_a < m->len_b){ m->seq1 = NULL; m->seq2 = NULL; m->prof1 = t->profile[a]; m->prof2 = t->profile[b]; aln_runner(m); }else{ len_b = m->len_b; len_a = m->len_a; m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = NULL; m->seq2 = NULL; m->prof1 = t->profile[b]; m->prof2 = t->profile[a]; aln_runner(m); RUN(mirror_path_n(m,len_a,len_b)); m->len_a = len_a; m->len_b = len_b; } } } /* Store alignment confidence (average meetup margin) */ if(m->margin_count > 0){ t->list[task_id]->confidence = m->margin_sum / (float)m->margin_count; }else{ t->list[task_id]->confidence = 0.0F; } RUN(add_gap_info_to_path_n(m)) ; /* LOG_MSG("Aligned %d and %d (len %d %d) -> path is of length: %d",a,b, m->len_a,m->len_b, 64*(m->path[0]+2)); */ /* Free consistency bonus if allocated */ if(m->consistency){ MFREE(m->consistency); m->consistency = NULL; m->consistency_stride = 0; } /* Restore original aln_param for profile update (unscaled base penalties) */ m->ap = orig_ap; MMALLOC(tmp,sizeof(float)*64*(m->path[0]+2)); if(task_id != t->n_tasks-1){ update_n(t->profile[a],t->profile[b],tmp,m->ap,m->path,msa->nsip[a],msa->nsip[b]); } MFREE(t->profile[a]); MFREE(t->profile[b]); t->profile[c] = tmp; RUN(make_seq(msa,a,b,m->path)); msa->plen[c] = m->path[0]; msa->nsip[c] = msa->nsip[a] + msa->nsip[b]; MREALLOC(msa->sip[c],sizeof(int)*(msa->nsip[a] + msa->nsip[b])); g = 0; for (j = msa->nsip[a];j--;){ msa->sip[c][g] = msa->sip[a][j]; g++; } for (j = msa->nsip[b];j--;){ msa->sip[c][g] = msa->sip[b][j]; g++; } return OK; ERROR: return FAIL; } /* --------------------------------------------------------------------------- * Inline refinement: run N trials at each progressive merge, keep the best * by SP score. Single pass — no second replay pass needed. * ------------------------------------------------------------------------- */ int create_msa_tree_inline_refine(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int n_trials) { int i; uint8_t* active = NULL; RUN(sort_tasks(t, TASK_ORDER_TREE)); MMALLOC(active, sizeof(uint8_t) * msa->num_profiles); for(i = 0; i < msa->numseq; i++){ active[i] = 1; } for(i = msa->numseq; i < msa->num_profiles; i++){ active[i] = 0; } /* Inline refine is sequential — multi-trial per edge isn't thread-safe */ msa->run_parallel = 0; recursive_aln_inline(msa, t, ap, active, t->n_tasks - 1, n_trials); MFREE(active); return OK; ERROR: if(active){ MFREE(active); } return FAIL; } void recursive_aln_inline(struct msa* msa, struct aln_tasks* t, struct aln_param* ap, uint8_t* active, int c, int n_trials) { struct task* local_t = NULL; int a, b; local_t = t->list[c]; a = local_t->a - msa->numseq; b = local_t->b - msa->numseq; if(!active[local_t->a] && local_t->a >= msa->numseq){ recursive_aln_inline(msa, t, ap, active, a, n_trials); } if(!active[local_t->b] && local_t->b >= msa->numseq){ recursive_aln_inline(msa, t, ap, active, b, n_trials); } struct aln_mem* ml = NULL; alloc_aln_mem(&ml, 256); ml->ap = ap; ml->mode = ALN_MODE_FULL; do_align_inline_refine(msa, t, ml, c, n_trials); active[local_t->a] = 0; active[local_t->b] = 0; active[local_t->c] = 1; free_aln_mem(ml); } /* Run n_trials alignments for one edge, keep the best by SP score. Trial 0 is the deterministic baseline. Trials 1..n-1 use flip_threshold to explore alternative Hirschberg paths at uncertain meetup points. */ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, struct aln_mem* m, int task_id, int n_trials) { float* tmp = NULL; int* best_path = NULL; int a, b, c; int len_a, len_b; int j, g, k; float best_sp = -FLT_MAX; float avg_margin = 0.0F; a = t->list[task_id]->a; b = t->list[task_id]->b; c = t->list[task_id]->c; /* Distance-dependent parameter scaling */ struct aln_param* orig_ap = m->ap; struct aln_param scaled_ap; float gap_scale = compute_gap_scale(msa, m->ap, a, b); float subm_off = compute_subm_offset(msa, m->ap, a, b); if(gap_scale < 1.0f || subm_off > 0.0f){ scaled_ap = *m->ap; scaled_ap.gpo *= gap_scale; scaled_ap.gpe *= gap_scale; scaled_ap.tgpe *= gap_scale; scaled_ap.subm_offset = subm_off; m->ap = &scaled_ap; } /* Build profiles (same as do_align) */ if(msa->nsip[a] == 1){ m->len_a = msa->sequences[a]->len; RUN(make_profile_n(m->ap, msa->sequences[a]->s, m->len_a, leaf_weight(msa,a), &t->profile[a])); }else{ m->len_a = msa->plen[a]; RUN(set_gap_penalties_n(t->profile[a], m->len_a, msa->nsip[b])); } if(msa->nsip[b] == 1){ m->len_b = msa->sequences[b]->len; RUN(make_profile_n(m->ap, msa->sequences[b]->s, m->len_b, leaf_weight(msa,b), &t->profile[b])); }else{ m->len_b = msa->plen[b]; RUN(set_gap_penalties_n(t->profile[b], m->len_b, msa->nsip[a])); } len_a = m->len_a; len_b = m->len_b; RUN(init_alnmem(m)); /* Compute consistency bonus for all merge types */ m->consistency = NULL; m->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ int dp_row_node, dp_col_node, dp_rows, dp_cols; if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } }else if(msa->nsip[a] == 1){ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; }else if(msa->nsip[b] == 1){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; dp_col_node = b; dp_cols = len_b; }else{ dp_row_node = b; dp_rows = len_b; dp_col_node = a; dp_cols = len_a; } } RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &m->consistency)); m->consistency_stride = dp_cols; } } /* Allocate best_path buffer */ MMALLOC(best_path, sizeof(int) * m->alloc_path_len); /* Multi-trial alignment */ for(k = 0; k < n_trials; k++){ float sp = 0.0F; /* Re-initialize DP state */ { int _g = MACRO_MAX(len_a, len_b) + 2; int _i; for(_i = 0; _i < _g; _i++){ m->path[_i] = -1; } } m->starta = 0; m->startb = 0; m->enda = len_a; m->endb = len_b; m->len_a = len_a; m->len_b = len_b; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; m->margin_sum = 0.0F; m->margin_count = 0; if(k == 0){ m->flip_threshold = 0.0F; m->flip_trial = 0; }else{ m->flip_threshold = avg_margin; m->flip_trial = k; m->flip_stride = n_trials - 1; m->flip_counter = 0; } /* Dispatch alignment (handle seq/profile combinations + mirroring) */ if(msa->nsip[a] == 1){ if(msa->nsip[b] == 1){ if(len_a < len_b){ m->seq1 = msa->sequences[a]->s; m->seq2 = msa->sequences[b]->s; m->prof1 = NULL; m->prof2 = NULL; aln_runner(m); }else{ m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = msa->sequences[b]->s; m->seq2 = msa->sequences[a]->s; m->prof1 = NULL; m->prof2 = NULL; aln_runner(m); RUN(mirror_path_n(m, len_a, len_b)); m->len_a = len_a; m->len_b = len_b; } }else{ m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = NULL; m->seq2 = msa->sequences[a]->s; m->prof1 = t->profile[b]; m->prof2 = NULL; m->sip = msa->nsip[b]; aln_runner(m); RUN(mirror_path_n(m, len_a, len_b)); m->len_a = len_a; m->len_b = len_b; } }else{ if(msa->nsip[b] == 1){ m->seq1 = NULL; m->seq2 = msa->sequences[b]->s; m->prof1 = t->profile[a]; m->prof2 = NULL; m->sip = msa->nsip[a]; aln_runner(m); }else{ if(len_a < len_b){ m->seq1 = NULL; m->seq2 = NULL; m->prof1 = t->profile[a]; m->prof2 = t->profile[b]; aln_runner(m); }else{ m->enda = len_b; m->endb = len_a; m->len_a = len_b; m->len_b = len_a; m->seq1 = NULL; m->seq2 = NULL; m->prof1 = t->profile[b]; m->prof2 = t->profile[a]; aln_runner(m); RUN(mirror_path_n(m, len_a, len_b)); m->len_a = len_a; m->len_b = len_b; } } } /* Convert raw Hirschberg path to 0/1/2 format with gap info */ RUN(add_gap_info_to_path_n(m)); /* Score this trial */ RUN(compute_sp_score(msa, m->ap, m->path, msa->sip[a], msa->nsip[a], msa->sip[b], msa->nsip[b], &sp)); if(sp > best_sp){ best_sp = sp; memcpy(best_path, m->path, sizeof(int) * (m->path[0] + 2)); } /* After baseline, compute avg_margin for flip threshold */ if(k == 0 && m->margin_count > 0){ avg_margin = m->margin_sum / (float)m->margin_count; } } /* Install best path */ memcpy(m->path, best_path, sizeof(int) * (best_path[0] + 2)); /* Free consistency bonus if allocated */ if(m->consistency){ MFREE(m->consistency); m->consistency = NULL; m->consistency_stride = 0; } /* Store confidence */ t->list[task_id]->confidence = best_sp; /* Restore original aln_param for profile update */ m->ap = orig_ap; /* Merge profiles */ MMALLOC(tmp, sizeof(float) * 64 * (m->path[0] + 2)); if(task_id != t->n_tasks - 1){ update_n(t->profile[a], t->profile[b], tmp, m->ap, m->path, msa->nsip[a], msa->nsip[b]); } MFREE(t->profile[a]); MFREE(t->profile[b]); t->profile[a] = NULL; t->profile[b] = NULL; t->profile[c] = tmp; RUN(make_seq(msa, a, b, m->path)); msa->plen[c] = m->path[0]; msa->nsip[c] = msa->nsip[a] + msa->nsip[b]; MREALLOC(msa->sip[c], sizeof(int) * (msa->nsip[a] + msa->nsip[b])); g = 0; for(j = msa->nsip[a]; j--;){ msa->sip[c][g] = msa->sip[a][j]; g++; } for(j = msa->nsip[b]; j--;){ msa->sip[c][g] = msa->sip[b][j]; g++; } MFREE(best_path); return OK; ERROR: if(best_path){ MFREE(best_path); } return FAIL; } kalign-3.5.1/lib/src/aln_run.h000066400000000000000000000021701515023132300161240ustar00rootroot00000000000000#ifndef ALN_RUN_H #define ALN_RUN_H #ifdef ALN_RUN_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_param; struct aln_tasks; struct msa; /* EXTERN int create_msa(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); */ EXTERN int create_msa_tree(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); EXTERN int create_msa_tree_inline_refine(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int n_trials); EXTERN float compute_gap_scale(struct msa* msa, struct aln_param* ap, int a, int b); EXTERN float compute_subm_offset(struct msa* msa, struct aln_param* ap, int a, int b); EXTERN int create_chaos_msa_serial(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); EXTERN int create_msa_serial(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); /* EXTERN int create_msa_openMP(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); */ EXTERN int create_chaos_msa_openMP(struct msa* msa, struct aln_param* ap,struct aln_tasks* t); #undef ALN_RUN_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_seqprofile.c000066400000000000000000000323741515023132300174750ustar00rootroot00000000000000#include #include #include "tldevel.h" #include "aln_param.h" #include "aln_struct.h" #define ALN_SEQPROFILE_IMPORT #include "aln_seqprofile.h" #define MAX(a, b) (a > b ? a : b) #define MAX3(a,b,c) MAX(MAX(a,b),c) int aln_seqprofile_foward(struct aln_mem* m) { struct states* s = m->f; const float* prof1 = m->prof1; const uint8_t* seq2 = m->seq2; const int sip = m->sip; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; const float open = m->ap->gpo * sip; const float ext = m->ap->gpe * sip; const float text = m->ap->tgpe * sip; prof1 += (m->starta)<< 6; s[m->startb].a = s[0].a; s[m->startb].ga = s[0].ga; s[m->startb].gb = s[0].gb; if(m->startb){ for (j = m->startb +1; j < m->endb;j++){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga-ext,s[j-1].a-open); s[j].gb = -FLT_MAX; } }else{ for (j = m->startb+1; j < m->endb;j++){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga,s[j-1].a) - text; s[j].gb = -FLT_MAX; } } s[m->endb].a = -FLT_MAX; s[m->endb].ga = -FLT_MAX; s[m->endb].gb = -FLT_MAX; seq2--; for (i = m->starta;i < m->enda;i++){ prof1 += 64; pa = s[m->startb].a; pga = s[m->startb].ga; pgb = s[m->startb].gb; s[m->startb].a = -FLT_MAX; s[m->startb].ga = -FLT_MAX; xa = s[m->startb].a; xga = s[m->startb].ga; if(m->startb){ s[m->startb].gb = MAX(pgb+prof1[28],pa+prof1[27]); }else{ s[m->startb].gb = MAX(pgb,pa)+prof1[29]; } for (j = m->startb+1; j < m->endb;j++){ ca = s[j].a; pa = MAX3(pa,pga -open,pgb + prof1[-37]); pa += prof1[32 + seq2[j]]; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j-1].ga-ext,s[j-1].a-open); s[j].ga = MAX(xga-ext,xa-open); pgb = s[j].gb; s[j].gb = MAX(pgb+prof1[28],ca+prof1[27]); pa = ca; xa = s[j].a; xga = s[j].ga; } ca = s[j].a; pa = MAX3(pa,pga -open,pgb + prof1[-37]); pa += prof1[32 + seq2[j]]; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; s[j].ga = -FLT_MAX;//MAX(s[j-1].ga-ext,s[j-1].a-open); if (m->endb != m->len_b){ s[j].gb = MAX(s[j].gb+prof1[28] ,ca+prof1[27]); }else{ s[j].gb = MAX(s[j].gb,ca)+ prof1[29]; } } //prof1 -= m->enda << 6; return OK; } int aln_seqprofile_backward(struct aln_mem* m) { struct states* s = m->b; const float* prof1 = m->prof1; const uint8_t* seq2 = m->seq2; const int sip = m->sip; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; const float open = m->ap->gpo * sip; const float ext = m->ap->gpe * sip; const float text = m->ap->tgpe * sip; prof1 += (m->enda_2 +1) << 6; s[m->endb].a = s[0].a; s[m->endb].ga = s[0].ga; s[m->endb].gb = s[0].gb; if(m->endb != m->len_b){ for(j = m->endb-1;j > m->startb ;j--){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga-ext,s[j+1].a-open); s[j].gb = -FLT_MAX; } }else{ for(j = m->endb-1;j > m->startb;j--){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga,s[j+1].a)-text; s[j].gb = -FLT_MAX; } } s[m->startb].a = -FLT_MAX; s[m->startb].ga = -FLT_MAX; s[m->startb].gb = -FLT_MAX; i = m->enda_2 -m->starta_2; while(i--){ prof1 -= 64; pa = s[m->endb].a; pga = s[m->endb].ga; pgb = s[m->endb].gb; s[m->endb].a = -FLT_MAX; s[m->endb].ga = -FLT_MAX; xa = s[m->endb].a; xga = s[m->endb].ga; if(m->endb != m->len_b){ s[m->endb].gb = MAX(pgb+prof1[28],pa+prof1[27]); }else{ s[m->endb].gb = MAX(pgb,pa) +prof1[29]; } for(j = m->endb-1;j > m->startb;j--){ ca = s[j].a; pa = MAX3(pa,pga - open,pgb +prof1[91]); pa += prof1[32 + seq2[j]]; if(m->consistency){ pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j+1].ga-ext,s[j+1].a-open); s[j].ga = MAX(xga-ext,xa-open); pgb = s[j].gb; s[j].gb = MAX(pgb+prof1[28],ca+prof1[27]); pa = ca; xa = s[j].a; xga = s[j].ga; } ca = s[j].a; pa = MAX3(pa,pga - open,pgb +prof1[91]); pa += prof1[32 + seq2[j]]; if(m->consistency){ pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; } s[j].a = pa; s[j].ga = -FLT_MAX;//MAX(s[j+1].ga-ext,s[j+1].a-open); if(m->startb){ s[j].gb = MAX(s[j].gb+prof1[28], ca+prof1[27]); }else{ s[j].gb = MAX(s[j].gb,ca)+prof1[29]; } } return OK; } int aln_seqprofile_meetup(struct aln_mem* m,int old_cor[],int* meet,int* t,float* score) { struct states* f = m->f; struct states* b = m->b; const float* prof1 = m->prof1; float sip = m->sip; int i; int c; int c2 = -1; int transition = -1; int transition2 = -1; float s_tmp; const float open = m->ap->gpo * sip; //code: // a -> a = 1 // a -> ga = 2 // a -> gb = 3 // ga ->ga = 4 // ga -> a = 5 //gb->gb = 6; //gb->a = 7; //int max = -FLT_MAX; float max = -FLT_MAX; float max2 = -FLT_MAX; //float middle = (m->endb - m->startb)/2 + m->startb; float middle = (float)(old_cor[3] - old_cor[2])/2.0F + (float)old_cor[2]; float sub = 0.0F; prof1+= ((old_cor[4]+1)<<6); //i = m->startb; c = -1; //for(i = m->startb; i < m->endb;i++){ for(i = old_cor[2]; i < old_cor[3];i++){ sub = fabsf(middle - (float)i); sub /= 1000.0F; s_tmp = f[i].a+b[i].a-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 1; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 1; } s_tmp = f[i].a+b[i].ga-open-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 2; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 2; } s_tmp = f[i].a+b[i].gb+prof1[27]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } s_tmp = f[i].ga+b[i].a-open-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 5; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 5; } if(m->startb == 0){ s_tmp = f[i].gb+b[i].gb+prof1[29]-sub; }else{ s_tmp = f[i].gb+b[i].gb+prof1[28]-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } s_tmp = f[i].gb+b[i].a+prof1[-37]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 7; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 7; } } //i = m->endb; i = old_cor[3]; sub = fabsf(middle - (float)i); sub /= 1000.0F; s_tmp = f[i].a+b[i].gb+prof1[27]-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } if(m->endb == m->len_b){ s_tmp = f[i].gb+b[i].gb+prof1[29]-sub; }else{ s_tmp = f[i].gb+b[i].gb+prof1[28]-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } /* Accumulate confidence margin and record per-meetup margins */ if(max2 > -FLT_MAX){ float _margin = max - max2; if(m->flip_margins != NULL && m->margin_count < m->flip_margin_alloc){ m->flip_margins[m->margin_count] = _margin; } m->margin_sum += _margin; m->margin_count++; } /* Perturbation: flip uncertain midpoints to second-best choice. Three modes: individual targeting (MCTS), stride bitmask, or round-robin. */ if(m->flip_threshold > 0.0F && c2 >= 0 && max2 > -FLT_MAX){ float margin = max - max2; if(margin < m->flip_threshold){ if(m->flip_bit_map != NULL){ /* Individual midpoint targeting (MCTS) */ if(m->flip_counter < m->flip_n_uncertain){ int bit = m->flip_bit_map[m->flip_counter]; if(bit >= 0 && ((1U << bit) & m->flip_mask)){ c = c2; transition = transition2; } } }else if(m->flip_mask != 0){ /* Stride-based bitmask mode */ if((1U << (m->flip_counter % m->flip_stride)) & m->flip_mask){ c = c2; transition = transition2; } }else if(m->flip_trial > 0){ /* Round-robin mode */ if(m->flip_counter % m->flip_stride == m->flip_trial - 1){ c = c2; transition = transition2; } } m->flip_counter++; } } *meet = c; *t = transition; *score = max; return OK; } kalign-3.5.1/lib/src/aln_seqprofile.h000066400000000000000000000007141515023132300174730ustar00rootroot00000000000000#ifndef ALN_SEQPROF_H #define ALN_SEQPROF_H #ifdef ALN_SEQPROFILE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_mem; EXTERN int aln_seqprofile_foward(struct aln_mem* m); EXTERN int aln_seqprofile_backward(struct aln_mem* m); EXTERN int aln_seqprofile_meetup(struct aln_mem* m,int old_cor[],int* meet,int* t,float* score); #undef ALN_SEQPROFILE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_seqseq.c000066400000000000000000000323741515023132300166250ustar00rootroot00000000000000#include #include #include "tldevel.h" #include "aln_param.h" #include "aln_struct.h" #define ALN_SEQSEQ_IMPORT #include "aln_seqseq.h" #define MAX(a, b) (a > b ? a : b) #define MAX3(a,b,c) MAX(MAX(a,b),c) int aln_seqseq_foward(struct aln_mem* m) { struct states* s = m->f; const uint8_t* seq1 = m->seq1; const uint8_t* seq2 = m->seq2; float *subp = NULL; const int starta = m->starta; const int enda = m->enda; const int startb =m->startb; const int endb = m->endb; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; const float gpo = m->ap->gpo; const float gpe = m->ap->gpe; const float tgpe = m->ap->tgpe; float** subm = m->ap->subm; const float soff = m->ap->subm_offset; s[startb].a = s[0].a; s[startb].ga = s[0].ga; s[startb].gb = s[0].gb; if(startb){ for (j = startb+1; j < endb;j++){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga - gpe,s[j-1].a-gpo); s[j].gb = -FLT_MAX; } }else{ for (j = startb+1; j < endb;j++){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j-1].ga,s[j-1].a)-tgpe; s[j].gb = -FLT_MAX; } } s[endb].a = -FLT_MAX; s[endb].ga = -FLT_MAX; s[endb].gb = -FLT_MAX; seq2--; for (i = starta;i < enda;i++){ subp = subm[seq1[i]]; pa = s[startb].a; pga = s[startb].ga; pgb = s[startb].gb; s[startb].a = -FLT_MAX; s[startb].ga = -FLT_MAX; xa = s[startb].a; xga = s[startb].ga; if(startb){ s[startb].gb = MAX(pgb - gpe,pa - gpo); }else{ s[startb].gb = MAX(pgb,pa) - tgpe; } for (j = startb+1; j < endb;j++){ ca = s[j].a; pa = MAX3(pa,pga-gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j-1].ga-gpe,s[j-1].a-gpo); s[j].ga = MAX(xga-gpe,xa-gpo); pgb = s[j].gb; s[j].gb = MAX(pgb-gpe ,ca-gpo); pa = ca; xa = s[j].a; xga = s[j].ga; } ca = s[j].a; pa = MAX3(pa,pga-gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ pa += m->consistency[i * m->consistency_stride + j]; } s[j].a = pa; s[j].ga = -FLT_MAX;//MAX(s[j-1].ga-gpe,s[j-1].a-gpo); if (endb != m->len_b){ s[j].gb = MAX(s[j].gb-gpe ,ca-gpo); }else{ s[j].gb = MAX(s[j].gb,ca)-tgpe; } } return OK; } int aln_seqseq_backward(struct aln_mem* m) { struct states* s = m->b; const uint8_t* seq1 = m->seq1; const uint8_t* seq2 = m->seq2; float *subp = NULL; const int starta = m->starta_2; const int enda = m->enda_2; const int startb =m->startb; const int endb = m->endb; register float pa = 0; register float pga = 0; register float pgb = 0; register float ca = 0; register float xa = 0; register float xga = 0; register int i = 0; register int j = 0; const float gpo = m->ap->gpo; const float gpe = m->ap->gpe; const float tgpe = m->ap->tgpe; float** subm = m->ap->subm; const float soff = m->ap->subm_offset; s[endb].a = s[0].a ; s[endb].ga = s[0].ga; s[endb].gb = s[0].gb; //init of first row; //j = endb-startb; if(endb != m->len_b){ for(j = endb-1;j > startb;j--){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga-gpe,s[j+1].a-gpo); s[j].gb = -FLT_MAX; } }else{ for(j = endb-1;j > startb;j--){ s[j].a = -FLT_MAX; s[j].ga = MAX(s[j+1].ga,s[j+1].a)-tgpe; s[j].gb = -FLT_MAX; } } s[startb].a = -FLT_MAX; s[startb].ga = -FLT_MAX; s[startb].gb = -FLT_MAX; i = enda-starta; seq1+= starta; while(i--){ subp = subm[seq1[i]]; pa = s[endb].a; pga = s[endb].ga; pgb = s[endb].gb; s[endb].a = -FLT_MAX; s[endb].ga = -FLT_MAX; xa = s[endb].a; xga = s[endb].ga; if(endb != m->len_b){ s[endb].gb = MAX(pgb-gpe,pa-gpo); }else{ s[endb].gb = MAX(pgb,pa)-tgpe; } for(j = endb-1;j > startb;j--){ ca = s[j].a; pa = MAX3(pa,pga - gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ pa += m->consistency[(starta + i) * m->consistency_stride + j]; } s[j].a = pa; pga = s[j].ga; //s[j].ga = MAX(s[j+1].ga-gpe,s[j+1].a-gpo); s[j].ga = MAX(xga-gpe,xa-gpo); pgb = s[j].gb; s[j].gb = MAX(pgb-gpe,ca-gpo); pa = ca; xa = s[j].a; xga = s[j].ga; } ca = s[j].a; pa = MAX3(pa,pga - gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ pa += m->consistency[(starta + i) * m->consistency_stride + j]; } s[j].a = pa; s[j].ga = -FLT_MAX;//MAX(s[j+1].ga-gpe,s[j+1].a-gpo); if(startb){ s[j].gb = MAX(s[j].gb-gpe,ca-gpo); }else{ s[j].gb = MAX(s[j].gb,ca)-tgpe; } } return OK; } int aln_seqseq_meetup(struct aln_mem* m,int old_cor[],int* meet,int* t,float* score) { struct states* f = m->f; struct states* b = m->b; const float gpo = m->ap->gpo; const float gpe = m->ap->gpe; const float tgpe = m->ap->tgpe; int i; int c; int c2 = -1; int transition = -1; int transition2 = -1; float s_tmp; //code: // a -> a = 1 // a -> ga = 2 // a -> gb = 3 // ga ->ga = 4 // ga -> a = 5 //gb->gb = 6; //gb->a = 7; //int max = -FLT_MAX; float max = -FLT_MAX; float max2 = -FLT_MAX; //float middle = (hm->endb - hm->startb)/2 + hm->startb; float middle = (float)(old_cor[3] - old_cor[2])/2.0F + (float)old_cor[2]; float sub = 0.0F; //i = hm->startb; c = -1; //for(i = hm->startb; i < hm->endb;i++){ for(i = old_cor[2]; i < old_cor[3];i++){ sub = fabsf(middle - (float)i); sub /= 1000.0F; s_tmp = f[i].a+b[i].a-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 1; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 1; } s_tmp = f[i].a+b[i].ga-gpo-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 2; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 2; } s_tmp = f[i].a+b[i].gb-gpo-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } s_tmp = f[i].ga+b[i].a-gpo-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 5; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 5; } if(m->startb == 0){ s_tmp = f[i].gb+b[i].gb-tgpe-sub; }else{ s_tmp = f[i].gb+b[i].gb-gpe-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } s_tmp = f[i].gb+b[i].a-gpo-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 7; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 7; } } //i = hm->endb; i = old_cor[3]; sub = fabsf(middle - (float)i); sub /= 1000.0F; s_tmp = f[i].a+b[i].gb-gpo-sub; if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 3; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 3; } if(m->endb == m->len_b){ s_tmp = f[i].gb+b[i].gb-tgpe-sub; }else{ s_tmp = f[i].gb+b[i].gb-gpe-sub; } if(s_tmp > max){ max2 = max; c2 = c; transition2 = transition; max = s_tmp; transition = 6; c = i; }else if(s_tmp > max2){ max2 = s_tmp; c2 = i; transition2 = 6; } /* Accumulate confidence margin and record per-meetup margins */ if(max2 > -FLT_MAX){ float _margin = max - max2; if(m->flip_margins != NULL && m->margin_count < m->flip_margin_alloc){ m->flip_margins[m->margin_count] = _margin; } m->margin_sum += _margin; m->margin_count++; } /* Perturbation: flip uncertain midpoints to second-best choice. Three modes: individual targeting (MCTS), stride bitmask, or round-robin. */ if(m->flip_threshold > 0.0F && c2 >= 0 && max2 > -FLT_MAX){ float margin = max - max2; if(margin < m->flip_threshold){ if(m->flip_bit_map != NULL){ /* Individual midpoint targeting (MCTS) */ if(m->flip_counter < m->flip_n_uncertain){ int bit = m->flip_bit_map[m->flip_counter]; if(bit >= 0 && ((1U << bit) & m->flip_mask)){ c = c2; transition = transition2; } } }else if(m->flip_mask != 0){ /* Stride-based bitmask mode */ if((1U << (m->flip_counter % m->flip_stride)) & m->flip_mask){ c = c2; transition = transition2; } }else if(m->flip_trial > 0){ /* Round-robin mode */ if(m->flip_counter % m->flip_stride == m->flip_trial - 1){ c = c2; transition = transition2; } } m->flip_counter++; } } *meet = c; *t = transition; *score = max; return OK; } kalign-3.5.1/lib/src/aln_seqseq.h000066400000000000000000000006451515023132300166260ustar00rootroot00000000000000#ifndef ALN_SEQSEQ_H #define ALN_SEQSEQ_H #ifdef ALN_SEQSEQ_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif EXTERN int aln_seqseq_foward(struct aln_mem* m); EXTERN int aln_seqseq_backward(struct aln_mem* m); EXTERN int aln_seqseq_meetup(struct aln_mem* m,int old_cor[],int* meet,int* t,float* score); #undef ALN_SEQSEQ_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_setup.c000066400000000000000000000400331515023132300164530ustar00rootroot00000000000000#include "tldevel.h" #include #include "aln_struct.h" #include "aln_param.h" #include "aln_mem.h" #define ALN_SETUP_IMPORT #include "aln_setup.h" int init_alnmem(struct aln_mem* m) { int i; int g; m->starta = 0; m->startb = 0; m->enda = m->len_a; m->endb = m->len_b; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; RUN(resize_aln_mem(m)); g = MACRO_MAX(m->len_a, m->len_b) + 2; for(i = 0 ;i < g ;i++){ m->path[i] = -1; } return OK; ERROR: return FAIL; } int make_profile_n(struct aln_param* ap,const uint8_t* seq,const int len, float weight, float** p) { float** subm = NULL; float* prof = NULL; float gpo; float gpe; float tgpe; int i; int j; int c; gpo = ap->gpo; gpe = ap->gpe; tgpe = ap->tgpe; subm = ap->subm; float soff = ap->subm_offset; MMALLOC(prof,sizeof(float)*(len+2)*64); prof += (64 *(len+1)); for (i = 0;i < 64;i++){ prof[i] = 0; } prof[23+32] = -gpo; prof[24+32] = -gpe; prof[25+32] = -tgpe; i = len; while(i--){ prof -= 64; for (j = 0;j < 64;j++){ prof[j] = 0; } c = seq[i]; prof[c] += weight; prof += 32; for(j = 23;j--;){ prof[j] = subm[c][j] - soff; } prof[23] = -gpo; prof[24] = -gpe; prof[25] = -tgpe; prof -= 32; } prof -= 64; for (i = 0;i < 64;i++){ prof[i] = 0; } prof[23+32] = -gpo; prof[24+32] = -gpe; prof[25+32] = -tgpe; *p = prof; return OK; ERROR: return FAIL; } int set_gap_penalties_n(float* prof,int len,int nsip) { int i; prof += (64 *(len+1)); prof[27] = prof[55] * (float)nsip;//gap open or close 23 prof[28] = prof[56] * (float)nsip;//gap extention 24 prof[29] = prof[57] * (float)nsip;//gap open or close 25 i = len+1; while(i--){ prof -= 64; prof[27] = prof[55] * (float)nsip;//gap open or close prof[28] = prof[56] * (float)nsip;//gap extention prof[29] = prof[57] * (float)nsip;//gap open or close } return OK; } int add_gap_info_to_path_n(struct aln_mem* m) { int* path = NULL; int* o_path = NULL; int* tmp_path = NULL; int i,j; int a = 0; int b = 0; int len_a; int len_b; len_a = m->len_a; len_b = m->len_b; path = m->path; o_path = m->tmp_path; for(i = 0; i < len_a+len_b+2;i++){ o_path[i] = 0; } j = 1; b = -1; if(path[1] == -1){ o_path[j] = 2; j++; }else{ if(path[1] != 1){ for ( a = 0;a < path[1] -1;a++){ o_path[j] = 1; j++; } o_path[j] = 0; j++; }else{ o_path[j] = 0; j++; } } b = path[1]; for(i = 2; i <= len_a;i++){ if(path[i] == -1){ o_path[j] = 2; j++; }else{ if(path[i]-1 != b && b != -1){ for ( a = 0;a < path[i] - b-1;a++){ o_path[j] = 1; j++; } o_path[j] = 0; j++; }else{ o_path[j] = 0; j++; } } b = path[i]; } if(path[len_a] < len_b && path[len_a] != -1){ // fprintf(stderr,"WARNING:%d %d\n",path[len_a],len_b); for ( a = 0;a < len_b - path[len_a];a++){ o_path[j] = 1; j++; } } o_path[0] = j-1; o_path[j] = 3; //add gap info.. i = 2; while(o_path[j] != 3){ if ((o_path[i-1] &3) && !(o_path[i] & 3)){ if(o_path[i-1] & 8){ o_path[i-1] += 8; }else{ o_path[i-1] |= 16; } }else if (!(o_path[i-1] & 3) &&(o_path[i] &3)){ o_path[i] |= 4; }else if ((o_path[i-1] & 1) && (o_path[i] & 1)){ o_path[i] |= 8; }else if ((o_path[i-1] & 2) && (o_path[i] & 2)){ o_path[i] |= 8; } i++; } //add terminal gap... i = 1; while(o_path[i] != 0){ o_path[i] |= 32; i++; } /* j = i; */ i = o_path[0]; while(o_path[i] != 0){ o_path[i] |= 32; i--; } tmp_path = m->path; m->path = m->tmp_path; m->tmp_path = tmp_path; return OK; } int update_n(const float* profa, const float* profb,float* newp, struct aln_param*ap, int* path,int sipa,int sipb) { float gp; int i; int j; int c; /* Balanced profile merging: when use_seq_weights is on, rescale positions 0-22 (amino acid frequencies) at match columns so each side contributes 50%, keeping the total = sipa + sipb. Then recompute substitution scores (32-54) from the balanced freqs via a delta correction that preserves gap penalty adjustments already embedded in those positions from earlier merge steps. delta[j] = sum_aa((freqA*(scaleA-1) + freqB*(scaleB-1)) * subm[aa][j]) This gives: newp[32+j] = balanced_subst[j] - soff_sum + gap_adj, exactly rebalancing the substitution component while keeping gap penalties (which scale with nsip, not weights) intact. */ float scaleA = 1.0f; float scaleB = 1.0f; int do_rebalance = 0; if(ap->use_seq_weights > 0.0f && sipa > 0 && sipb > 0){ float pseudo = ap->use_seq_weights; float total = (float)(sipa + sipb); float denom = total + 2.0f * pseudo; scaleA = total * ((float)sipa + pseudo) / (denom * (float)sipa); scaleB = total * ((float)sipb + pseudo) / (denom * (float)sipb); do_rebalance = 1; } /* First boundary row (freq counts are zero — no rebalancing needed) */ if(do_rebalance){ for (i = 0; i < 23; i++){ newp[i] = profa[i] * scaleA + profb[i] * scaleB; } for (i = 23; i < 64; i++){ newp[i] = profa[i] + profb[i]; } }else{ for (i = 64; i--;){ newp[i] = profa[i] + profb[i]; } } profa += 64; profb += 64; newp += 64; c = 1; while(path[c] != 3){ if (!path[c]){ /* Match column */ if(do_rebalance){ for (i = 0; i < 23; i++){ newp[i] = profa[i] * scaleA + profb[i] * scaleB; } for (i = 23; i < 64; i++){ newp[i] = profa[i] + profb[i]; } /* Correct subst scores (32-54) for rebalanced freqs */ { float** subm = ap->subm; float dA = scaleA - 1.0f; float dB = scaleB - 1.0f; for(j = 0; j < 23; j++){ float delta = 0.0f; int aa; for(aa = 0; aa < 23; aa++){ delta += (profa[aa]*dA + profb[aa]*dB) * subm[aa][j]; } newp[32 + j] += delta; } } }else{ for (i = 64; i--;){ newp[i] = profa[i] + profb[i]; } } profa += 64; profb += 64; } if (path[c] & 1){ //fprintf(stderr,"Gap_A:%d\n",c); //printf("open:%d ext:%d %d %d\n",si->nsip[a] * gpo,si->nsip[a] * gpe,si->nsip[a] * profb[41],si->nsip[a] * profb[46]); for (i = 64; i--;){ newp[i] = profb[i]; } profb += 64; if(!(path[c] & 20)){ if(path[c] & 32){ newp[25] += (float)sipa;//1; gp = ap->tgpe*(float)sipa; }else{ newp[24] += (float)sipa;//1; gp = ap->gpe*(float)sipa; } for (j = 32; j < 55;j++){ newp[j] -= gp; } }else{ if (path[c] & 16){ // fprintf(stderr,"close_open"); if(path[c] & 32){ newp[25] += (float)sipa;//1; gp = ap->tgpe*(float)sipa; newp[23] += (float)sipa;//1; gp += ap->gpo*(float)sipa; }else{ newp[23] += (float)sipa;//1; gp = ap->gpo*(float)sipa; } for (j = 32; j < 55;j++){ newp[j] -= gp; } } if (path[c] & 4){ // fprintf(stderr,"Gap_open"); if(path[c] & 32){ newp[25] += (float)sipa;//1; gp = ap->tgpe*(float)sipa; newp[23] += (float)sipa;//1; gp += ap->gpo*(float)sipa; }else{ newp[23] += (float)sipa;//1; gp = ap->gpo*(float)sipa; } for (j = 32; j < 55;j++){ newp[j] -= gp; } } } } if (path[c] & 2){ //fprintf(stderr,"Gap_B:%d\n",c); //printf("open:%d ext:%d %d %d\n",si->nsip[b] * gpo,si->nsip[b] * gpe,profa[26],profa[27]); for (i = 64; i--;){ newp[i] = profa[i]; } profa+=64; if(!(path[c] & 20)){ if(path[c] & 32){ newp[25] += (float)sipb;//1; gp = ap->tgpe*(float)sipb; }else{ newp[24] += (float)sipb;//1; gp = ap->gpe*(float)sipb; } for (j = 32; j < 55;j++){ newp[j] -= gp; } }else{ if (path[c] & 16){ // fprintf(stderr,"close_open"); if(path[c] & 32){ newp[25] += (float)sipb;//1; gp = ap->tgpe*(float)sipb; newp[23] += (float)sipb;//1; gp += ap->gpo*(float)sipb; }else{ newp[23] += (float)sipb;//1; gp = ap->gpo*(float)sipb; } for (j = 32; j < 55;j++){ newp[j] -= gp; } } if (path[c] & 4){ // fprintf(stderr,"Gap_open"); if(path[c] & 32){ newp[25] += (float)sipb;//1; gp = ap->tgpe*(float)sipb; newp[23] += (float)sipb;//1; gp += ap->gpo*(float)sipb; }else{ newp[23] += (float)sipb;//1; gp = ap->gpo*(float)sipb; } for (j = 32; j < 55;j++){ newp[j] -= gp; } } } } newp += 64; c++; } /* Last boundary row (freq counts are zero — no subst correction needed) */ if(do_rebalance){ for (i = 0; i < 23; i++){ newp[i] = profa[i] * scaleA + profb[i] * scaleB; } for (i = 23; i < 64; i++){ newp[i] = profa[i] + profb[i]; } }else{ for (i = 64; i--;){ newp[i] = profa[i] + profb[i]; } } return OK; } int mirror_path_n(struct aln_mem* m,int len_a,int len_b) { int* apath = NULL; int* opath = NULL; int* tmppath = NULL; int i; apath = m->path; opath = m->tmp_path; for(i =0; i < len_a+2;i++){ opath[i] = -1; } for(i = 1; i <= len_b;i++){ if(apath[i] != -1){ opath[apath[i]] = i; } } tmppath = m->path; m->path = m->tmp_path; m->tmp_path = tmppath; return OK; } kalign-3.5.1/lib/src/aln_setup.h000066400000000000000000000013361515023132300164630ustar00rootroot00000000000000#ifndef ALN_SETUP_H #define ALN_SETUP_H #include #ifdef ALN_SETUP_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_mem; struct aln_param; EXTERN int init_alnmem(struct aln_mem* m); EXTERN int make_profile_n(struct aln_param* ap,const uint8_t* seq,const int len, float weight, float** p); EXTERN int set_gap_penalties_n(float* prof,int len,int nsip); EXTERN int add_gap_info_to_path_n(struct aln_mem* m); EXTERN int update_n(const float* profa, const float* profb,float* newp, struct aln_param*ap, int* path,int sipa,int sipb); EXTERN int mirror_path_n(struct aln_mem *m, int len_a, int len_b); #undef ALN_SETUP_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/aln_struct.h000066400000000000000000000035621515023132300166520ustar00rootroot00000000000000#ifndef ALN_STRUCT_H #define ALN_STRUCT_H #include #define ALN_MODE_SCORE_ONLY 2 #define ALN_MODE_FULL 1 struct states{ float a; float ga; float gb; // float x; }; struct aln_mem{ const float* prof1; const float* prof2; const uint8_t* seq1; const uint8_t* seq2; struct aln_param* ap; struct states* f; struct states* b; int* path; int* tmp_path; uint8_t run_parallel; int alloc_path_len; float score; float margin_sum; /* accumulated meetup margins */ int margin_count; /* number of meetup calls */ float flip_threshold; /* midpoints with margin < this are flip candidates; 0 = no flips */ int flip_trial; /* round-robin: current trial (1..K-1, 0 = baseline) */ int flip_stride; /* round-robin: number of flip slots */ int flip_counter; /* running count of flip candidates encountered */ uint32_t flip_mask; /* bitmask of which slots to flip (0 = no flips) */ float* flip_margins; /* per-meetup margins recorded during baseline */ int flip_margin_alloc; /* allocated size of flip_margins */ int* flip_bit_map; /* maps flip_counter → bit index (-1 = not targeted) */ int flip_n_targets; /* number of individually targeted midpoints */ int flip_n_uncertain; /* total uncertain midpoints (margin < threshold) */ int starta; int starta_2; int startb; int enda; int enda_2; int endb; int size; int len_a; int len_b; int sip; int mode; float* consistency; /* bonus matrix [i * consistency_stride + j], NULL if disabled */ int consistency_stride; /* = len_b (stride for j dimension) */ }; #endif kalign-3.5.1/lib/src/aln_wrap.c000066400000000000000000000553121515023132300162720ustar00rootroot00000000000000#include "tldevel.h" #include "tlmisc.h" #include "esl_stopwatch.h" #include "task.h" #include "msa_struct.h" #include "msa_op.h" #include "msa_alloc.h" #include "msa_check.h" #include "msa_sort.h" #include "msa_io.h" #include "alphabet.h" #include "bisectingKmeans.h" #include "aln_param.h" #include "aln_run.h" #include "aln_refine.h" #include "aln_apair_dist.h" #include "anchor_consistency.h" #include "kalign/kalign.h" #ifdef HAVE_OPENMP #include #endif #define ALN_WRAP_IMPORT #include "aln_wrap.h" /* Resolve PFASUM_AUTO: pick PFASUM43 for divergent, PFASUM60 for closer. Must be called after build_tree_kmeans which fills msa->seq_distances[]. */ static int resolve_pfasum_auto(struct msa *msa, int *type) { int i; int min_len, max_len; float len_ratio; if(*type != KALIGN_TYPE_PROTEIN_PFASUM_AUTO){ return OK; } if(msa->biotype != ALN_BIOTYPE_PROTEIN){ *type = KALIGN_TYPE_PROTEIN_PFASUM43; return OK; } /* Use sequence length ratio (max/min) to select matrix. Similar-length sequences (ratio < 1.5) prefer PFASUM43; high length variation (insertions/extensions) prefers PFASUM60. */ min_len = msa->sequences[0]->len; max_len = msa->sequences[0]->len; for(i = 1; i < msa->numseq; i++){ int l = msa->sequences[i]->len; if(l < min_len) min_len = l; if(l > max_len) max_len = l; } len_ratio = (min_len > 0) ? (float)max_len / (float)min_len : 1.0f; if(len_ratio < 1.5f){ *type = KALIGN_TYPE_PROTEIN_PFASUM43; }else{ *type = KALIGN_TYPE_PROTEIN_PFASUM60; } if(!msa->quiet){ LOG_MSG("Auto matrix: len_ratio=%.2f -> %s", len_ratio, *type == KALIGN_TYPE_PROTEIN_PFASUM60 ? "PFASUM60" : "PFASUM43"); } return OK; } static int compute_tree_weights(struct msa* msa, struct aln_tasks* tasks) { float* nw = NULL; int i; MMALLOC(nw, sizeof(float) * msa->num_profiles); for(i = 0; i < msa->num_profiles; i++){ nw[i] = 0.0f; } /* Root gets total weight = numseq */ nw[tasks->list[tasks->n_tasks - 1]->c] = (float)msa->numseq; /* Walk root → leaves: at each split, distribute parent's weight to children in proportion to the OTHER child's size */ for(i = tasks->n_tasks - 1; i >= 0; i--){ int a = tasks->list[i]->a; int b = tasks->list[i]->b; int c = tasks->list[i]->c; float total = (float)(msa->nsip[a] + msa->nsip[b]); nw[a] = nw[c] * (float)msa->nsip[b] / total; nw[b] = nw[c] * (float)msa->nsip[a] / total; } /* Copy leaf weights to seq_weights */ if(msa->seq_weights){ MFREE(msa->seq_weights); } MMALLOC(msa->seq_weights, sizeof(float) * msa->numseq); for(i = 0; i < msa->numseq; i++){ msa->seq_weights[i] = nw[i]; } MFREE(nw); return OK; ERROR: if(nw) MFREE(nw); return FAIL; } int kalign(char **seq, int *len, int numseq,int n_threads, int type, float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len) { struct msa *msa = NULL; RUN(kalign_arr_to_msa(seq, len,numseq, &msa)); msa->quiet = 1; if(n_threads < 1){ n_threads = 1; } RUN(kalign_run(msa,n_threads, type, gpo, gpe, tgpe, KALIGN_REFINE_NONE, 0)); RUN(kalign_msa_to_arr(msa, aligned, out_aln_len)); kalign_free_msa(msa); return OK; ERROR: if(msa){ kalign_free_msa(msa); } return FAIL; } int kalign_run_seeded(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, uint64_t tree_seed, float tree_noise, float dist_scale, float vsm_amax, float use_seq_weights, int consistency_anchors, float consistency_weight) { struct aln_tasks* tasks = NULL; struct aln_param* ap = NULL; /* This also adds the ranks of the sequences ! */ RUN(kalign_essential_input_check(msa, 0)); /* If already aligned unalign ! */ if(msa->aligned != ALN_STATUS_UNALIGNED){ RUN(dealign_msa(msa)); } /* Make sure sequences are in order */ RUN(msa_sort_len_name(msa)); /* Convert into internal representation */ if(msa->biotype == ALN_BIOTYPE_DNA){ msa->L = ALPHA_defDNA; RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ msa->L = ALPHA_redPROTEIN; RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); }else{ ERROR_MSG("Unable to determine what alphabet to use."); } RUN(alloc_tasks(&tasks, msa->numseq)); #ifdef HAVE_OPENMP omp_set_num_threads(n_threads); #endif /* Build guide tree - noisy variant if seed != 0 */ if(tree_seed != 0 && tree_noise > 0.0f){ RUN(build_tree_kmeans_noisy(msa, &tasks, tree_seed, tree_noise)); }else{ RUN(build_tree_kmeans(msa, &tasks)); } /* Convert to full alphabet after having converted to reduced alphabet for tree building above */ if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } /* Resolve auto matrix selection using BPM distances */ RUN(resolve_pfasum_auto(msa, &type)); /* align */ RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; } if(dist_scale > 0.0f){ ap->dist_scale = dist_scale; } if(vsm_amax >= 0.0f){ ap->vsm_amax = vsm_amax; } if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Build anchor consistency table if requested */ if(consistency_anchors > 0){ ap->consistency_anchors = consistency_anchors; ap->consistency_weight = consistency_weight; RUN(anchor_consistency_build(msa, ap, consistency_anchors, consistency_weight, (struct consistency_table**)&msa->consistency_table)); } DECLARE_TIMER(t1); if(!msa->quiet){ LOG_MSG("Aligning"); } START_TIMER(t1); if(refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; /* Optional iterative refinement (two-pass approach) */ if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ RUN(refine_alignment(msa, ap, tasks, refine)); } /* Free consistency table AFTER refinement */ if(msa->consistency_table){ anchor_consistency_free((struct consistency_table*)msa->consistency_table); msa->consistency_table = NULL; } RUN(finalise_alignment(msa)); RUN(msa_sort_rank(msa)); STOP_TIMER(t1); if(!msa->quiet){ GET_TIMING(t1); } DESTROY_TIMER(t1); aln_param_free(ap); free_tasks(tasks); return OK; ERROR: if(msa->consistency_table){ anchor_consistency_free((struct consistency_table*)msa->consistency_table); msa->consistency_table = NULL; } aln_param_free(ap); free_tasks(tasks); return FAIL; } int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget) { return kalign_run_seeded(msa, n_threads, type, gpo, gpe, tgpe, refine, adaptive_budget, 0, 0.0f, 0.0f, -1.0f, -1.0f, 0, 2.0f); } int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, float use_seq_weights) { struct aln_tasks* tasks = NULL; struct aln_param* ap = NULL; RUN(kalign_essential_input_check(msa, 0)); if(msa->aligned != ALN_STATUS_UNALIGNED){ RUN(dealign_msa(msa)); } RUN(msa_sort_len_name(msa)); if(msa->biotype == ALN_BIOTYPE_DNA){ msa->L = ALPHA_defDNA; RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ msa->L = ALPHA_redPROTEIN; RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); }else{ ERROR_MSG("Unable to determine what alphabet to use."); } RUN(alloc_tasks(&tasks, msa->numseq)); #ifdef HAVE_OPENMP omp_set_num_threads(n_threads); #endif RUN(build_tree_kmeans(msa, &tasks)); if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } RUN(resolve_pfasum_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; } ap->dist_scale = dist_scale; if(vsm_amax >= 0.0f){ ap->vsm_amax = vsm_amax; } if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } DECLARE_TIMER(t1); if(!msa->quiet){ LOG_MSG("Aligning (dist_scale=%.2f, vsm_amax=%.2f)", dist_scale, vsm_amax); } START_TIMER(t1); if(refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ RUN(refine_alignment(msa, ap, tasks, refine)); } RUN(finalise_alignment(msa)); RUN(msa_sort_rank(msa)); STOP_TIMER(t1); if(!msa->quiet){ GET_TIMING(t1); } DESTROY_TIMER(t1); aln_param_free(ap); free_tasks(tasks); return OK; ERROR: aln_param_free(ap); free_tasks(tasks); return FAIL; } int kalign_run_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights, int consistency_anchors, float consistency_weight) { struct aln_tasks* tasks = NULL; struct aln_param* ap = NULL; int iter; RUN(kalign_essential_input_check(msa, 0)); if(msa->aligned != ALN_STATUS_UNALIGNED){ RUN(dealign_msa(msa)); } RUN(msa_sort_len_name(msa)); if(msa->biotype == ALN_BIOTYPE_DNA){ msa->L = ALPHA_defDNA; RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ msa->L = ALPHA_redPROTEIN; RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); }else{ ERROR_MSG("Unable to determine what alphabet to use."); } RUN(alloc_tasks(&tasks, msa->numseq)); #ifdef HAVE_OPENMP omp_set_num_threads(n_threads); #endif /* Initial guide tree from BPM anchor distances */ RUN(build_tree_kmeans(msa, &tasks)); if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } RUN(resolve_pfasum_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; } ap->dist_scale = dist_scale; if(vsm_amax >= 0.0f){ ap->vsm_amax = vsm_amax; } if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Build anchor consistency table if requested */ if(consistency_anchors > 0){ ap->consistency_anchors = consistency_anchors; ap->consistency_weight = consistency_weight; RUN(anchor_consistency_build(msa, ap, consistency_anchors, consistency_weight, (struct consistency_table**)&msa->consistency_table)); } DECLARE_TIMER(t1); if(!msa->quiet){ LOG_MSG("Aligning (realign=%d, dist_scale=%.2f, vsm_amax=%.2f)", realign_iterations, dist_scale, vsm_amax); } START_TIMER(t1); /* First alignment with BPM-based guide tree */ if(refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; /* Iterative realignment: align -> compute distances -> new tree -> re-align */ for(iter = 0; iter < realign_iterations; iter++){ float** dm = NULL; int si; /* Finalize to get character sequences with gap characters */ RUN(finalise_alignment(msa)); /* Compute NxN pairwise identity distances from alignment */ RUN(compute_aln_pairwise_dist(msa, &dm)); /* Remove gaps, reset alignment status. dealign_msa zeroes the gaps[] array but does NOT strip '-' from seq->seq (which was linearized by finalise_alignment). We must rebuild seq->seq without gap characters. */ RUN(dealign_msa(msa)); for(si = 0; si < msa->numseq; si++){ struct msa_seq* seq = msa->sequences[si]; int r, w = 0; for(r = 0; seq->seq[r] != '\0'; r++){ if(seq->seq[r] != '-'){ seq->seq[w++] = seq->seq[r]; } } seq->seq[w] = '\0'; seq->len = w; } /* Re-encode internal representation for alignment */ if(msa->biotype == ALN_BIOTYPE_DNA){ RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } /* Reset profile tracking */ RUN(set_sip_nsip(msa)); /* Rebuild guide tree from alignment-derived distances */ free_tasks(tasks); tasks = NULL; RUN(alloc_tasks(&tasks, msa->numseq)); RUN(build_tree_from_pairwise(msa, &tasks, dm)); free_aln_dm(dm, msa->numseq); if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Re-align with new tree */ if(refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; } /* Refinement after all realign iterations (two-pass, skip for inline) */ if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ RUN(refine_alignment(msa, ap, tasks, refine)); } /* Free consistency table AFTER refinement */ if(msa->consistency_table){ anchor_consistency_free((struct consistency_table*)msa->consistency_table); msa->consistency_table = NULL; } RUN(finalise_alignment(msa)); RUN(msa_sort_rank(msa)); STOP_TIMER(t1); if(!msa->quiet){ GET_TIMING(t1); } DESTROY_TIMER(t1); aln_param_free(ap); free_tasks(tasks); return OK; ERROR: if(msa->consistency_table){ anchor_consistency_free((struct consistency_table*)msa->consistency_table); msa->consistency_table = NULL; } aln_param_free(ap); free_tasks(tasks); return FAIL; } int kalign_post_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights) { struct aln_tasks* tasks = NULL; struct aln_param* ap = NULL; int iter; ASSERT(msa != NULL, "No MSA"); ASSERT(realign_iterations > 0, "Need at least 1 realign iteration"); /* Detect biotype if not set */ if(msa->biotype == ALN_BIOTYPE_UNDEF){ RUN(detect_alphabet(msa)); } /* seq_distances available from prior alignment */ RUN(resolve_pfasum_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; } ap->dist_scale = dist_scale; if(vsm_amax >= 0.0f){ ap->vsm_amax = vsm_amax; } #ifdef HAVE_OPENMP omp_set_num_threads(n_threads); #endif DECLARE_TIMER(t1); if(!msa->quiet){ LOG_MSG("Post-realign (%d iterations, vsm_amax=%.2f)", realign_iterations, ap->vsm_amax); } START_TIMER(t1); for(iter = 0; iter < realign_iterations; iter++){ float** dm = NULL; int si; /* Finalize if not already (first iter may already be FINAL from ensemble) */ if(msa->aligned != ALN_STATUS_FINAL){ RUN(finalise_alignment(msa)); } /* Compute NxN pairwise identity distances from alignment */ RUN(compute_aln_pairwise_dist(msa, &dm)); /* Strip gap characters from seq->seq and fix seq->len. Consensus alignment may have set len to alignment length, so we recompute from the ungapped sequence. We also zero gaps[] and reset alignment status manually (rather than calling dealign_msa which uses the possibly wrong len to bound the gaps[] loop). */ for(si = 0; si < msa->numseq; si++){ struct msa_seq* seq = msa->sequences[si]; int r, w = 0; for(r = 0; seq->seq[r] != '\0'; r++){ if(seq->seq[r] != '-'){ seq->seq[w++] = seq->seq[r]; } } seq->seq[w] = '\0'; seq->len = w; /* Zero gaps array (len+1 entries) */ for(r = 0; r <= w; r++){ seq->gaps[r] = 0; } } msa->aligned = ALN_STATUS_UNALIGNED; /* Re-encode to internal representation */ if(msa->biotype == ALN_BIOTYPE_DNA){ RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } /* Reset profile tracking */ RUN(set_sip_nsip(msa)); /* Build UPGMA tree from alignment-derived distances */ if(tasks){ free_tasks(tasks); tasks = NULL; } RUN(alloc_tasks(&tasks, msa->numseq)); RUN(build_tree_from_pairwise(msa, &tasks, dm)); free_aln_dm(dm, msa->numseq); if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Re-align with new tree */ if(refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; } /* Refinement after all realign iterations (two-pass, skip for inline) */ if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ RUN(refine_alignment(msa, ap, tasks, refine)); } RUN(finalise_alignment(msa)); RUN(msa_sort_rank(msa)); STOP_TIMER(t1); if(!msa->quiet){ GET_TIMING(t1); } DESTROY_TIMER(t1); aln_param_free(ap); free_tasks(tasks); return OK; ERROR: aln_param_free(ap); if(tasks) free_tasks(tasks); return FAIL; } kalign-3.5.1/lib/src/aln_wrap.h000066400000000000000000000033141515023132300162720ustar00rootroot00000000000000#ifndef ALN_WRAP_H #define ALN_WRAP_H #include #ifdef ALN_WRAP_IMPORT #define EXTERN #else #ifndef EXTERN #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #endif struct msa; EXTERN int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget); EXTERN int kalign_run_seeded(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, uint64_t tree_seed, float tree_noise, float dist_scale, float vsm_amax, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_run_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, float dist_scale, float vsm_amax, int realign_iterations, float use_seq_weights); #undef ALN_WRAP_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/alphabet.c000066400000000000000000000242311515023132300162430ustar00rootroot00000000000000#include "tldevel.h" #include #define ALPHABET_IMPORT #include "alphabet.h" int create_default_protein(struct alphabet* a); int create_protein_BZX(struct alphabet* a); int create_default_DNA(struct alphabet* a); int create_reduced_protein(struct alphabet *a); int create_reduced_protein2(struct alphabet* a); int clean_and_set_to_extern(struct alphabet* a); static int merge_multiple(struct alphabet*a,char* p,int n); static int merge_codes(struct alphabet*a,const int X, const int Y); #ifdef UTEST_ALPHABET int print_alphabet(struct alphabet* a); int main(void) { struct alphabet* a = NULL; RUNP(a = create_alphabet(ALPHA_defPROTEIN)); print_alphabet(a); MFREE(a); a = NULL; RUNP(a = create_alphabet(ALPHA_redPROTEIN)); print_alphabet(a); MFREE(a); RUNP(a = create_alphabet(ALPHA_defDNA)); print_alphabet(a); MFREE(a); RUNP(a = create_alphabet(ALPHA_redPROTEIN2)); print_alphabet(a); MFREE(a); return EXIT_SUCCESS; ERROR: return EXIT_FAILURE; } int print_alphabet(struct alphabet* a) { fprintf(stdout,"LEN: %d\n",a->L); int i; for(i = 64;i < 96;i++){ fprintf(stdout,"%c\t%d\n", (char)i, a->to_internal[i]); } return OK; } #endif struct alphabet* create_alphabet(int type) { struct alphabet* a = NULL; int i; MMALLOC(a, sizeof(struct alphabet)); for(i = 0; i < 128;i++){ a->to_internal[i] = -1; } for(i = 0; i < 32;i++){ a->to_external[i] = -1; } switch (type) { case ALPHA_defPROTEIN : { create_default_protein(a); break; } case ALPHA_ambigiousPROTEIN :{ create_protein_BZX(a); break; } case ALPHA_defDNA : { create_default_DNA(a); break; } case ALPHA_redPROTEIN : { create_reduced_protein(a); break; } case ALPHA_redPROTEIN2 : { create_reduced_protein2(a); break; } default: break; } RUN(clean_and_set_to_extern(a)); return a; ERROR: if(a){ MFREE(a); } return NULL; } int switch_alphabet(struct alphabet* a, int type) { int i; for(i = 0; i < 128;i++){ a->to_internal[i] = -1; } for(i = 0; i < 32;i++){ a->to_external[i] = -1; } switch (type) { case ALPHA_defPROTEIN : { create_default_protein(a); break; } case ALPHA_redPROTEIN : { create_reduced_protein(a); break; } default: break; } RUN(clean_and_set_to_extern(a)); return OK; ERROR: return FAIL; } int create_default_protein(struct alphabet* a) { char aacode[20] = "ACDEFGHIKLMNPQRSTVWY"; /* char aacode[20] = "ARNDCQEGHILKMFPSTWYV";//BZX"; */ int code; int i; code = 0; for(i = 0; i < 20;i++){ //fprintf(stdout,"%c %d CODE: %d\n", aacode[i], (int) aacode[i], code); a->to_internal[(int) aacode[i]] = code; code++; } /* ambiguity codes */ /* BZX */ a->to_internal[(int) 'B'] = code; a->to_internal[(int) 'Z'] = code; a->to_internal[(int) 'X'] = code; /* Some protein sequences contain 'U' - a non-IUPAC code I will treat these as an ambiguous aa e.g: >Q74EN2_GEOSL/108-206 TRELEALVAKGTEEGGYLLIDSRPAGKYNEAHIPTAVSIPFAELEKNPALLTASKDRLLVFYCGGVTUVLSPKSAGLAKKSGYEKVRVYLDGEPEWKKA */ a->to_internal[(int) 'U'] = code; code++; return OK; } int create_protein_BZX(struct alphabet* a) { char aacode[23] = "ARNDCQEGHILKMFPSTWYVBZX"; int code; int i; code = 0; for(i = 0; i < 23;i++){ //fprintf(stdout,"%c %d CODE: %d\n", aacode[i], (int) aacode[i], code); a->to_internal[(int) aacode[i]] = code; code++; } /* ambiguity codes */ /* Some protein sequences contain 'U' - a non-IUPAC code I will treat these as an ambiguous aa e.g: >Q74EN2_GEOSL/108-206 TRELEALVAKGTEEGGYLLIDSRPAGKYNEAHIPTAVSIPFAELEKNPALLTASKDRLLVFYCGGVTUVLSPKSAGLAKKSGYEKVRVYLDGEPEWKKA */ a->to_internal[(int) 'U'] = code-1; //code++; return OK; } int create_default_DNA(struct alphabet* a) { char dnacode[16] = "ACGTUNRYSWKMBDHV"; int code; int i; code = 0; for(i = 0; i < 16;i++){ //fprintf(stdout,"%c %d CODE: %d\n", aacode[i], (int) aacode[i], code); a->to_internal[(int) dnacode[i]] = code; code++; } merge_codes(a,'U','T'); /* R.................A or G */ /* Y.................C or T */ /* S.................G or C */ /* W.................A or T */ /* K.................G or T */ /* M.................A or C */ /* B.................C or G or T */ /* D.................A or G or T */ /* H.................A or C or T */ /* V.................A or C or G */ merge_codes(a,'N','R'); merge_codes(a,'N','Y'); merge_codes(a,'N','S'); merge_codes(a,'N','W'); merge_codes(a,'N','K'); merge_codes(a,'N','M'); merge_codes(a,'N','B'); merge_codes(a,'N','D'); merge_codes(a,'N','H'); merge_codes(a,'N','V'); return OK; } int create_reduced_protein(struct alphabet* a) { char aacode[20] = "ACDEFGHIKLMNPQRSTVWY"; int code; int i; code = 0; for(i = 0; i < 20;i++){ a->to_internal[(int) aacode[i]] = code; code++; } /* ambiguity codes */ /* BZX */ a->to_internal[(int) 'B'] = code; code++; a->to_internal[(int) 'Z'] = code; code++; a->to_internal[(int) 'X'] = code; code++; /* From Clustering huge protein sequence sets in linear time Martin Steinegger 1, 2, 3 and Johannes Söding 1 */ /* The default alphabet with A = 13 merges (L,M), (I,V), (K,R), (E, Q), (A,S,T), (N, D) and (F,Y).*/ /* reduced codes */ merge_codes(a,'L','M'); merge_codes(a,'I','V'); merge_codes(a,'K','R'); merge_codes(a,'E','Q'); merge_codes(a,'A','S'); merge_codes(a,'A','T'); merge_codes(a,'S','T'); merge_codes(a,'N','D'); merge_codes(a,'F','Y'); /* merge ambiguity codes */ merge_codes(a,'B','N'); merge_codes(a,'B','D'); merge_codes(a,'Z','E'); merge_codes(a,'Z','Q'); /* Selenocysteine (U) - map to cysteine (C) as it is structurally nearly identical (Se replaces S) */ a->to_internal[(int) 'U'] = a->to_internal[(int) 'C']; return OK; } int create_reduced_protein2(struct alphabet* a) { char aacode[20] = "ACDEFGHIKLMNPQRSTVWY"; int code; int i; code = 0; for(i = 0; i < 20;i++){ a->to_internal[(int) aacode[i]] = code; code++; } /* ambiguity codes */ /* BZX */ a->to_internal[(int) 'B'] = code; code++; a->to_internal[(int) 'Z'] = code; code++; a->to_internal[(int) 'X'] = code; code++; /* From Clustering huge protein sequence sets in linear time Martin Steinegger 1, 2, 3 and Johannes Söding 1 */ /* The default alphabet with A = 13 merges (L,M), (I,V), (K,R), (E, Q), (A,S,T), (N, D) and (F,Y).*/ /* AM DEKNQRT CFIV GHTS WLY */ /* reduced codes */ merge_codes(a,'A','M'); /* DEKNQRP */ merge_multiple(a,"DEKNQRP",7); /*CFIV*/ merge_multiple(a,"CFIV",4); /*GHTS*/ merge_multiple(a,"GHTS",4); /* WLY */ merge_multiple(a,"WLY",3); merge_multiple(a,"BZX",3); /* Selenocysteine (U) - map to cysteine (C) as it is structurally nearly identical (Se replaces S) */ a->to_internal[(int) 'U'] = a->to_internal[(int) 'C']; return OK; } int merge_multiple(struct alphabet*a,char* p,int n) { // Declaring pointer to the // argument list int min = INT32_MAX; for(int i = 0; i < n;i++){ min = MACRO_MIN(min,a->to_internal[(int)p[i]]); } for(int i = 0; i < n;i++){ a->to_internal[(int)p[i]] = min; } return OK; } int merge_codes(struct alphabet*a,const int X, const int Y) { int min; min = MACRO_MIN(a->to_internal[X],a->to_internal[Y]); ASSERT(min != -1, "code not set!"); a->to_internal[X] = min; a->to_internal[Y] = min; return OK; ERROR: return FAIL; } int clean_and_set_to_extern(struct alphabet* a) { int i; int code = 0; int8_t trans[32]; for(i = 0; i < 32;i++){ trans[i] = -1; } for(i = 64; i < 96;i++){ if(a->to_internal[i] != -1){ trans[a->to_internal[i]] = 1; } } code = 0; for(i = 0; i < 32;i++){ if(trans[i] == 1){ trans[i] = code; code++; } } a->L = code; for(i = 64; i < 96;i++){ if(a->to_internal[i] != -1){ a->to_internal[i] = trans[a->to_internal[i]];//a->to_internal[i]]; a->to_internal[i+32] = a->to_internal[i]; } } for(i = 64;i < 96;i++){ if(a->to_internal[i] != -1){ a->to_external[a->to_internal[i]] = i; } } return OK; } kalign-3.5.1/lib/src/alphabet.h000066400000000000000000000013341515023132300162470ustar00rootroot00000000000000#ifndef ALPHABET_H #define ALPHABET_H #include #ifdef ALPHABET_IMPORT #define EXTERN #else #ifndef EXTERN #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #endif #define ALPHA_defPROTEIN 21 #define ALPHA_ambigiousPROTEIN 23 #define ALPHA_redPROTEIN 13 #define ALPHA_redPROTEIN2 8 #define ALPHA_defDNA 5 #define ALPHA_UNKNOWN 255 #define ALPHA_UNDEFINED -1 struct alphabet{ int8_t to_internal[128]; int8_t to_external[32]; int type; int L; }; EXTERN struct alphabet* create_alphabet(int type); EXTERN int switch_alphabet(struct alphabet* a, int type); #undef ALPHABET_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/anchor_consistency.c000066400000000000000000000503601515023132300203600ustar00rootroot00000000000000#include #include #include "tldevel.h" #include "msa_struct.h" #include "aln_param.h" #include "aln_struct.h" #include "aln_mem.h" #include "aln_setup.h" #include "aln_controller.h" #define ANCHOR_CONSISTENCY_IMPORT #include "anchor_consistency.h" /* Run a pairwise seq-seq alignment and extract position map. Returns map[0..len_i-1] where map[p] = position in seq_j that aligns to position p of seq_i, or -1 if gapped. */ static int pairwise_align_map(struct aln_param* ap, const uint8_t* s_i, int len_i, const uint8_t* s_j, int len_j, int** map_out) { struct aln_mem* m = NULL; int* posmap = NULL; int c, pos_a, pos_b; int swapped = 0; MMALLOC(posmap, sizeof(int) * len_i); for(c = 0; c < len_i; c++){ posmap[c] = -1; } RUN(alloc_aln_mem(&m, 256)); m->ap = ap; m->mode = ALN_MODE_FULL; m->run_parallel = 0; m->flip_threshold = 0.0F; m->flip_trial = 0; m->flip_stride = 1; m->flip_counter = 0; m->flip_mask = 0; m->margin_sum = 0.0F; m->margin_count = 0; /* Kalign requires seq1 = shorter, seq2 = longer */ if(len_i <= len_j){ m->len_a = len_i; m->len_b = len_j; m->enda = len_i; m->endb = len_j; m->seq1 = s_i; m->seq2 = s_j; swapped = 0; }else{ m->len_a = len_j; m->len_b = len_i; m->enda = len_j; m->endb = len_i; m->seq1 = s_j; m->seq2 = s_i; swapped = 1; } m->prof1 = NULL; m->prof2 = NULL; m->f[0].a = 0.0F; m->f[0].ga = -FLT_MAX; m->f[0].gb = -FLT_MAX; m->b[0].a = 0.0F; m->b[0].ga = -FLT_MAX; m->b[0].gb = -FLT_MAX; RUN(init_alnmem(m)); aln_runner(m); if(swapped){ RUN(mirror_path_n(m, len_i, len_j)); m->len_a = len_i; m->len_b = len_j; } RUN(add_gap_info_to_path_n(m)); /* Extract position map from alignment path. After add_gap_info_to_path_n: path[0] = alignment length path[c]: 0=match (both advance), 1=gap in a (b advances), 2=gap in b (a advances), 3=end */ pos_a = 0; pos_b = 0; c = 1; while(m->path[c] != 3){ if(m->path[c] == 0){ /* match: a[pos_a] aligns to b[pos_b] */ if(pos_a < len_i){ posmap[pos_a] = pos_b; } pos_a++; pos_b++; }else if(m->path[c] & 1){ /* gap in a: only b advances */ pos_b++; }else if(m->path[c] & 2){ /* gap in b: only a advances (no partner) */ if(pos_a < len_i){ posmap[pos_a] = -1; } pos_a++; } c++; } free_aln_mem(m); *map_out = posmap; return OK; ERROR: if(posmap) MFREE(posmap); if(m) free_aln_mem(m); return FAIL; } /* Select K diverse anchor sequences using farthest-first traversal on msa->seq_distances[] (per-sequence mean distance). */ static int select_anchors(struct msa* msa, int K, int* anchor_ids) { int i, k; float* min_dist = NULL; /* min distance to any selected anchor */ int N = msa->numseq; if(K > N) K = N; MMALLOC(min_dist, sizeof(float) * N); /* Pick first anchor: sequence with median seq_distances[] value. Use a simple approach: pick the one closest to the mean. */ { float sum = 0.0f; float best_diff = FLT_MAX; int best_idx = 0; for(i = 0; i < N; i++){ sum += msa->seq_distances[i]; } float mean = sum / (float)N; for(i = 0; i < N; i++){ float diff = msa->seq_distances[i] - mean; if(diff < 0) diff = -diff; if(diff < best_diff){ best_diff = diff; best_idx = i; } } anchor_ids[0] = best_idx; } /* Initialize min_dist: distance from each seq to first anchor. We use |seq_distances[i] - seq_distances[anchor]| as a proxy since we only have per-sequence mean distances, not pairwise. */ for(i = 0; i < N; i++){ float d = msa->seq_distances[i] - msa->seq_distances[anchor_ids[0]]; if(d < 0) d = -d; min_dist[i] = d; } /* Farthest-first: pick remaining K-1 anchors */ for(k = 1; k < K; k++){ float best_min = -1.0f; int best_idx = 0; for(i = 0; i < N; i++){ /* Skip already-selected anchors */ int skip = 0; for(int j = 0; j < k; j++){ if(anchor_ids[j] == i){ skip = 1; break; } } if(skip) continue; if(min_dist[i] > best_min){ best_min = min_dist[i]; best_idx = i; } } anchor_ids[k] = best_idx; /* Update min_dist with new anchor */ for(i = 0; i < N; i++){ float d = msa->seq_distances[i] - msa->seq_distances[best_idx]; if(d < 0) d = -d; if(d < min_dist[i]){ min_dist[i] = d; } } } MFREE(min_dist); return OK; ERROR: if(min_dist) MFREE(min_dist); return FAIL; } int anchor_consistency_build(struct msa* msa, struct aln_param* ap, int n_anchors, float weight, struct consistency_table** ct_out) { struct consistency_table* ct = NULL; int N = msa->numseq; int K = n_anchors; int i, k; if(K <= 0 || N < 3){ *ct_out = NULL; return OK; } if(K > N) K = N; /* Check that seq_distances are available */ if(msa->seq_distances == NULL){ *ct_out = NULL; return OK; } MMALLOC(ct, sizeof(struct consistency_table)); ct->pos_maps = NULL; ct->map_lengths = NULL; ct->anchor_ids = NULL; ct->n_anchors = K; ct->numseq = N; ct->weight = weight; MMALLOC(ct->anchor_ids, sizeof(int) * K); MMALLOC(ct->pos_maps, sizeof(int*) * N * K); MMALLOC(ct->map_lengths, sizeof(int) * N * K); for(i = 0; i < N * K; i++){ ct->pos_maps[i] = NULL; ct->map_lengths[i] = 0; } /* Select diverse anchors */ RUN(select_anchors(msa, K, ct->anchor_ids)); if(!msa->quiet){ LOG_MSG("Anchor consistency: K=%d, weight=%.1f", K, weight); } /* Build position maps: for each sequence i, for each anchor k */ for(i = 0; i < N; i++){ int len_i = msa->sequences[i]->len; for(k = 0; k < K; k++){ int ak = ct->anchor_ids[k]; ct->map_lengths[i * K + k] = len_i; if(i == ak){ /* Identity map for anchor itself */ int p; MMALLOC(ct->pos_maps[i * K + k], sizeof(int) * len_i); for(p = 0; p < len_i; p++){ ct->pos_maps[i * K + k][p] = p; } }else{ /* Pairwise alignment: seq_i vs anchor_k */ RUN(pairwise_align_map(ap, msa->sequences[i]->s, len_i, msa->sequences[ak]->s, msa->sequences[ak]->len, &ct->pos_maps[i * K + k])); } } } *ct_out = ct; return OK; ERROR: anchor_consistency_free(ct); *ct_out = NULL; return FAIL; } int anchor_consistency_get_bonus(struct consistency_table* ct, int seq_a, int len_a, int seq_b, int len_b, float** bonus_out) { float* bonus = NULL; int* inv_b = NULL; int K = ct->n_anchors; int k, i; float per_anchor_weight = ct->weight / (float)K; MMALLOC(bonus, sizeof(float) * len_a * len_b); memset(bonus, 0, sizeof(float) * len_a * len_b); /* For each anchor, build inverse map for seq_b (anchor_pos → seq_b_pos), then scan seq_a's map to accumulate bonus. O(K * (La + Lb + max_anchor_len)) */ for(k = 0; k < K; k++){ int* map_a = ct->pos_maps[seq_a * K + k]; int* map_b = ct->pos_maps[seq_b * K + k]; int anchor_len = 0; int j; if(map_a == NULL || map_b == NULL) continue; /* Determine anchor length for inverse map */ /* Find max mapped position to size the inverse map */ anchor_len = 0; for(i = 0; i < len_a; i++){ if(map_a[i] >= anchor_len) anchor_len = map_a[i] + 1; } for(j = 0; j < len_b; j++){ if(map_b[j] >= anchor_len) anchor_len = map_b[j] + 1; } if(anchor_len == 0) continue; /* Build inverse map: anchor_pos → seq_b_pos */ MMALLOC(inv_b, sizeof(int) * anchor_len); for(j = 0; j < anchor_len; j++){ inv_b[j] = -1; } for(j = 0; j < len_b; j++){ if(map_b[j] >= 0 && map_b[j] < anchor_len){ inv_b[map_b[j]] = j; } } /* Accumulate bonus */ for(i = 0; i < len_a; i++){ int ak_pos = map_a[i]; if(ak_pos >= 0 && ak_pos < anchor_len){ int bj = inv_b[ak_pos]; if(bj >= 0){ bonus[i * len_b + bj] += per_anchor_weight; } } } MFREE(inv_b); inv_b = NULL; } *bonus_out = bonus; return OK; ERROR: if(bonus) MFREE(bonus); if(inv_b) MFREE(inv_b); *bonus_out = NULL; return FAIL; } /* Compute consensus anchor positions for a profile node. For a leaf (nsip==1), positions come directly from pos_maps. For a profile (nsip>1), positions are derived by majority vote across member sequences, using gaps[] to map profile columns to ungapped positions. */ static int get_node_anchor_positions(struct consistency_table* ct, struct msa* msa, int node, int dp_len, int anchor_k, int* positions, float* confidence) { int K = ct->n_anchors; int i, c, p; if(msa->nsip[node] == 1){ /* Leaf: direct lookup from pos_maps */ int* map = ct->pos_maps[node * K + anchor_k]; int seq_len = msa->sequences[node]->len; if(map == NULL){ for(i = 0; i < dp_len; i++){ positions[i] = -1; confidence[i] = 0.0f; } return OK; } for(i = 0; i < dp_len && i < seq_len; i++){ positions[i] = map[i]; confidence[i] = (map[i] >= 0) ? 1.0f : 0.0f; } for(; i < dp_len; i++){ positions[i] = -1; confidence[i] = 0.0f; } }else{ /* Profile: consensus from member sequences */ int n_members = msa->nsip[node]; int* members = msa->sip[node]; int* col_to_ungapped = NULL; int* best_pos = NULL; int* agree_count = NULL; int* total_count = NULL; int mi, col; MMALLOC(col_to_ungapped, sizeof(int) * (dp_len + 1)); MMALLOC(best_pos, sizeof(int) * dp_len); MMALLOC(agree_count, sizeof(int) * dp_len); MMALLOC(total_count, sizeof(int) * dp_len); for(c = 0; c < dp_len; c++){ best_pos[c] = -1; agree_count[c] = 0; total_count[c] = 0; } for(mi = 0; mi < n_members; mi++){ int si = members[mi]; int* map; int seq_len; int* gaps; int apos; if(si >= ct->numseq) continue; map = ct->pos_maps[si * K + anchor_k]; if(map == NULL) continue; seq_len = msa->sequences[si]->len; gaps = msa->sequences[si]->gaps; /* Build col_to_ungapped from gaps[] */ col = 0; for(p = 0; p <= seq_len && col < dp_len; p++){ int g; for(g = 0; g < gaps[p] && col < dp_len; g++){ col_to_ungapped[col] = -1; col++; } if(p < seq_len && col < dp_len){ col_to_ungapped[col] = p; col++; } } while(col < dp_len){ col_to_ungapped[col] = -1; col++; } /* Accumulate votes for each profile column */ for(c = 0; c < dp_len; c++){ int ugp = col_to_ungapped[c]; if(ugp < 0 || ugp >= seq_len) continue; apos = map[ugp]; if(apos < 0) continue; total_count[c]++; if(best_pos[c] < 0){ best_pos[c] = apos; agree_count[c] = 1; }else if(apos == best_pos[c]){ agree_count[c]++; } } } for(c = 0; c < dp_len; c++){ if(total_count[c] > 0 && agree_count[c] > 0){ positions[c] = best_pos[c]; confidence[c] = (float)agree_count[c] / (float)total_count[c]; }else{ positions[c] = -1; confidence[c] = 0.0f; } } MFREE(col_to_ungapped); MFREE(best_pos); MFREE(agree_count); MFREE(total_count); } return OK; ERROR: return FAIL; } int anchor_consistency_get_bonus_profile(struct consistency_table* ct, struct msa* msa, int node_a, int len_a, int node_b, int len_b, float** bonus_out) { float* bonus = NULL; int* apos_a = NULL; float* conf_a = NULL; int* apos_b = NULL; float* conf_b = NULL; int* inv_b = NULL; float* inv_conf_b = NULL; int K = ct->n_anchors; int k, i, j; float per_anchor_weight = ct->weight / (float)K; MMALLOC(bonus, sizeof(float) * len_a * len_b); memset(bonus, 0, sizeof(float) * len_a * len_b); MMALLOC(apos_a, sizeof(int) * len_a); MMALLOC(conf_a, sizeof(float) * len_a); MMALLOC(apos_b, sizeof(int) * len_b); MMALLOC(conf_b, sizeof(float) * len_b); for(k = 0; k < K; k++){ int anchor_len; RUN(get_node_anchor_positions(ct, msa, node_a, len_a, k, apos_a, conf_a)); RUN(get_node_anchor_positions(ct, msa, node_b, len_b, k, apos_b, conf_b)); /* Find max anchor position to size the inverse map */ anchor_len = 0; for(i = 0; i < len_a; i++){ if(apos_a[i] >= anchor_len) anchor_len = apos_a[i] + 1; } for(j = 0; j < len_b; j++){ if(apos_b[j] >= anchor_len) anchor_len = apos_b[j] + 1; } if(anchor_len == 0) continue; /* Build inverse map: anchor_pos -> node_b DP col */ MMALLOC(inv_b, sizeof(int) * anchor_len); MMALLOC(inv_conf_b, sizeof(float) * anchor_len); for(j = 0; j < anchor_len; j++){ inv_b[j] = -1; inv_conf_b[j] = 0.0f; } for(j = 0; j < len_b; j++){ if(apos_b[j] >= 0 && apos_b[j] < anchor_len){ inv_b[apos_b[j]] = j; inv_conf_b[apos_b[j]] = conf_b[j]; } } /* Accumulate bonus */ for(i = 0; i < len_a; i++){ int ak_pos = apos_a[i]; if(ak_pos >= 0 && ak_pos < anchor_len){ int bj = inv_b[ak_pos]; if(bj >= 0){ bonus[i * len_b + bj] += per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; } } } MFREE(inv_b); inv_b = NULL; MFREE(inv_conf_b); inv_conf_b = NULL; } MFREE(apos_a); MFREE(conf_a); MFREE(apos_b); MFREE(conf_b); *bonus_out = bonus; return OK; ERROR: if(bonus) MFREE(bonus); if(apos_a) MFREE(apos_a); if(conf_a) MFREE(conf_a); if(apos_b) MFREE(apos_b); if(conf_b) MFREE(conf_b); if(inv_b) MFREE(inv_b); if(inv_conf_b) MFREE(inv_conf_b); *bonus_out = NULL; return FAIL; } void anchor_consistency_free(struct consistency_table* ct) { if(ct){ if(ct->pos_maps){ int total = ct->numseq * ct->n_anchors; for(int i = 0; i < total; i++){ if(ct->pos_maps[i]){ MFREE(ct->pos_maps[i]); } } MFREE(ct->pos_maps); } if(ct->map_lengths) MFREE(ct->map_lengths); if(ct->anchor_ids) MFREE(ct->anchor_ids); MFREE(ct); } } kalign-3.5.1/lib/src/anchor_consistency.h000066400000000000000000000030541515023132300203630ustar00rootroot00000000000000#ifndef ANCHOR_CONSISTENCY_H #define ANCHOR_CONSISTENCY_H #ifdef ANCHOR_CONSISTENCY_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; struct aln_param; struct consistency_table { int** pos_maps; /* [seq_idx * K + anchor_slot][pos] = anchor_pos or -1 */ int* map_lengths; /* [seq_idx * K + anchor_slot] = length of seq_idx */ int* anchor_ids; /* [0..K-1] = sequence indices of anchors */ int n_anchors; /* K */ int numseq; /* N */ float weight; /* scaling factor for bonus */ }; EXTERN int anchor_consistency_build(struct msa* msa, struct aln_param* ap, int n_anchors, float weight, struct consistency_table** ct_out); EXTERN int anchor_consistency_get_bonus(struct consistency_table* ct, int seq_a, int len_a, int seq_b, int len_b, float** bonus_out); EXTERN int anchor_consistency_get_bonus_profile(struct consistency_table* ct, struct msa* msa, int node_a, int len_a, int node_b, int len_b, float** bonus_out); EXTERN void anchor_consistency_free(struct consistency_table* ct); #undef ANCHOR_CONSISTENCY_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/bisectingKmeans.c000066400000000000000000001057301515023132300175750ustar00rootroot00000000000000#include "tldevel.h" #ifdef HAVE_OPENMP #include #endif #ifdef HAVE_AVX2 #include #include #endif #include "tlrng.h" #include "bisectingKmeans.h" #include "msa_struct.h" /* #include "global.h" */ #include "task.h" #include "sequence_distance.h" #include "euclidean_dist.h" /* #include "alignment.h" */ #include "pick_anchor.h" #include "esl_stopwatch.h" struct node{ struct node* left; struct node* right; int id; }; struct kmeans_result{ int* sl; int* sr; int nl; int nr; float score; }; static struct kmeans_result* alloc_kmeans_result(int num_samples); static void free_kmeans_results(struct kmeans_result* k); struct node* upgma(float **dm,int* samples, int numseq); static struct node* alloc_node(void); static int label_internal(struct node *n, int label); static void create_tasks(struct node*n, struct aln_tasks* t); /* static void create_tasks(struct node*n, struct aln_tasks* t); */ /* static int bisecting_kmeans_serial(struct msa *msa, struct node **ret_n, float **dm, int *samples, int num_samples); */ static int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * dm, int* samples, int num_samples); /* static int bisecting_kmeans_parallel(struct msa* msa, struct node** ret_n, float** dm,int* samples, int num_samples); */ static int split(const float * const * dm, int *samples, int num_anchors, int num_samples, int seed_pick, struct kmeans_result **ret); static int split2(const float * const * dm,const int* samples, const int num_anchors,const int num_samples,const int seed_pick,struct kmeans_result** ret); static inline int cmp_floats(const float a, const float b); inline int cmp_floats(const float a, const float b) { const float epsilon = 1e-6; // Set a small epsilon value for tolerance if (fabsf(a - b) < epsilon) { return 0; // Numbers are equal } else if (a > b) { return 1; // First number is bigger } else { return -1; // Second number is bigger } } int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, uint64_t seed, float noise_sigma) { struct aln_tasks* t = NULL; struct node* root = NULL; float** dm = NULL; int* samples = NULL; int* anchors = NULL; int num_anchors; int numseq; int i; ASSERT(msa != NULL, "No alignment."); t = *tasks; if(!t){ RUN(alloc_tasks(&t, msa->numseq)); } numseq = msa->numseq; DECLARE_TIMER(timer); if(!msa->quiet){ LOG_MSG("Calculating pairwise distances (noisy, seed=%lu)", (unsigned long)seed); } START_TIMER(timer); RUNP(anchors = pick_anchor(msa, &num_anchors)); RUNP(dm = d_estimation(msa, anchors, num_anchors, 0)); /* Add multiplicative Gaussian noise to distance matrix */ if(seed != 0 && noise_sigma > 0.0f){ struct rng_state* rng = NULL; rng = init_rng(seed); for(i = 0; i < numseq; i++){ for(int j = 0; j < num_anchors; j++){ double noise = tl_random_gaussian(rng, 1.0, (double)noise_sigma); if(noise < 0.1) noise = 0.1; dm[i][j] *= (float)noise; } } free_rng(rng); } STOP_TIMER(timer); if(!msa->quiet){ GET_TIMING(timer); } MFREE(anchors); MMALLOC(samples, sizeof(int) * numseq); for(i = 0; i < numseq; i++){ samples[i] = i; } START_TIMER(timer); if(!msa->quiet){ LOG_MSG("Building guide tree."); } #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait #endif bisecting_kmeans(msa, &root, (const float * const *)dm, samples, numseq); STOP_TIMER(timer); if(!msa->quiet){ GET_TIMING(timer); } label_internal(root, numseq); create_tasks(root, t); /* Compute per-sequence normalized mean distance (noisy variant). */ if(msa->seq_distances == NULL){ MMALLOC(msa->seq_distances, sizeof(float) * numseq); } for(i = 0; i < numseq; i++){ float sum = 0.0f; int j; for(j = 0; j < num_anchors; j++){ sum += dm[i][j]; } float mean_dist = sum / (float)num_anchors; float seq_len = (float)msa->sequences[i]->len; msa->seq_distances[i] = (seq_len > 0.0f) ? mean_dist / seq_len : 0.0f; } MFREE(root); for(i = 0; i < numseq; i++){ #ifdef HAVE_AVX2 _mm_free(dm[i]); #else MFREE(dm[i]); #endif } MFREE(dm); DESTROY_TIMER(timer); return OK; ERROR: return FAIL; } int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks) { struct aln_tasks* t = NULL; struct node* root = NULL; float** dm = NULL; int* samples = NULL; int* anchors = NULL; int num_anchors; int numseq; int i; ASSERT(msa != NULL, "No alignment."); t = *tasks; if(!t){ RUN(alloc_tasks(&t, msa->numseq)); } numseq = msa->numseq; DECLARE_TIMER(timer); /* pick anchors . */ if(!msa->quiet){ LOG_MSG("Calculating pairwise distances"); } START_TIMER(timer); RUNP(anchors = pick_anchor(msa, &num_anchors)); RUNP(dm = d_estimation(msa, anchors, num_anchors,0));//les,int pair) STOP_TIMER(timer); if(!msa->quiet){ GET_TIMING(timer); } MFREE(anchors); MMALLOC(samples, sizeof(int)* numseq); for(i = 0; i < numseq;i++){ samples[i] = i; } START_TIMER(timer); if(!msa->quiet){ LOG_MSG("Building guide tree."); } /* if(n_threads == 1){ */ /* RUN(bisecting_kmeans_serial(msa,&root, dm, samples, numseq)); */ /* }else{ */ #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait #endif bisecting_kmeans(msa,&root, (const float * const *)dm, samples, numseq); /* } */ STOP_TIMER(timer); if(!msa->quiet){ GET_TIMING(timer); } label_internal(root, numseq); create_tasks(root, t); /* Compute per-sequence normalized mean distance to anchors. Used for distance-dependent gap penalty scaling. */ if(msa->seq_distances == NULL){ MMALLOC(msa->seq_distances, sizeof(float) * numseq); } for(i = 0; i < numseq; i++){ float sum = 0.0f; int j; for(j = 0; j < num_anchors; j++){ sum += dm[i][j]; } float mean_dist = sum / (float)num_anchors; float seq_len = (float)msa->sequences[i]->len; msa->seq_distances[i] = (seq_len > 0.0f) ? mean_dist / seq_len : 0.0f; } MFREE(root); for(i = 0; i < numseq; i++){ #ifdef HAVE_AVX2 _mm_free(dm[i]); #else MFREE(dm[i]); #endif } MFREE(dm); DESTROY_TIMER(timer); return OK; ERROR: return FAIL; } int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * dm,int* samples, int num_samples) { struct kmeans_result* res_tmp = NULL; struct kmeans_result* best = NULL; /* struct kmeans_result** res = NULL; */ struct node* n = NULL; int num_anchors = 0; int i,j; int tries = 40; /* int t_iter; */ /* int r; */ int* sl = NULL; int* sr = NULL; int num_l,num_r; /* LOG_MSG("num_samples: %d", num_samples); */ num_anchors = MACRO_MIN(32, msa->numseq); if(num_samples < KALIGN_KMEANS_UPGMA_THRESHOLD){ float** dm = NULL; RUNP(dm = d_estimation(msa, samples, num_samples,1));// anchors, num_anchors,1)); n = upgma(dm,samples, num_samples); *ret_n = n; gfree(dm); MFREE(samples); return OK; //return n; } /* else if(num_samples < 1000){ */ /* RUN(bisecting_kmeans_serial(msa, &n, dm, samples, num_samples)); */ /* *ret_n = n; */ /* return OK; */ /* } */ best = NULL; res_tmp = NULL; struct kmeans_result* res[4]; /* MMALLOC(res, sizeof(struct kmeans_result*) * 4); */ for(i = 0; i < 4;i++){ res[i] = NULL; } tries = MACRO_MIN(tries, num_samples); int step = num_samples / tries; int change = 0; for(i = 0;i < tries;i += 4){ change = 0; #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif split2(dm,samples,num_anchors, num_samples, (i)*step, &res[0]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif split2(dm,samples,num_anchors, num_samples, (i+ 1)*step, &res[1]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif split2(dm,samples,num_anchors, num_samples, (i+ 2)*step, &res[2]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif split2(dm,samples,num_anchors, num_samples, (i+ 3)*step, &res[3]); #ifdef HAVE_OPENMP #pragma omp taskwait #endif for(j = 0; j < 4;j++){ if(!best){ change++; best = res[j]; res[j] = NULL; }else{ if(best->score > res[j]->score){ res_tmp = best; best = res[j]; res[j] = res_tmp; /* LOG_MSG("Better!!! %f %f", res_tmp->score,best->score); */ change++; } } } if(!change){ break; } } sl = best->sl; sr = best->sr; num_l = best->nl; num_r = best->nr; /* free_kmeans_results(res[0]); */ /* free_kmeans_results(res[1]); */ /* free_kmeans_results(res[2]); */ /* free_kmeans_results(res[3]); */ for(i = 0; i < 4;i++){ free_kmeans_results(res[i]); } /* MFREE(res); */ MFREE(best); MFREE(samples); n = alloc_node(); /* #ifdef HAVE_OPENMP */ /* #pragma omp parallel //num_threads(2) */ /* #pragma omp single nowait */ #ifdef HAVE_OPENMP #pragma omp task shared(msa,n,dm) #endif bisecting_kmeans(msa,&n->left, dm, sl, num_l); #ifdef HAVE_OPENMP #pragma omp task shared(msa,n,dm,num_anchors) #endif bisecting_kmeans(msa,&n->right, dm, sr, num_r); #ifdef HAVE_OPENMP #pragma omp taskwait #endif *ret_n =n; return OK; ERROR: return FAIL; } /* int bisecting_kmeans_serial(struct msa* msa, struct node** ret_n, float** dm,int* samples, int num_samples) */ /* { */ /* struct kmeans_result* res_tmp = NULL; */ /* struct kmeans_result* best = NULL; */ /* struct kmeans_result** res_ptr = NULL; */ /* int num_anchors = 0; */ /* struct node* n = NULL; */ /* int i,j; */ /* int tries = 40; */ /* /\* int t_iter; *\/ */ /* /\* int r; *\/ */ /* int* sl = NULL; */ /* int* sr = NULL; */ /* int num_l,num_r; */ /* num_anchors = MACRO_MIN(32, msa->numseq); */ /* if(num_samples < KALIGN_KMEANS_UPGMA_THRESHOLD){ */ /* float** dm = NULL; */ /* RUNP(dm = d_estimation(msa, samples, num_samples,1));// anchors, num_anchors,1)); */ /* n = upgma(dm,samples, num_samples); */ /* *ret_n = n; */ /* gfree(dm); */ /* MFREE(samples); */ /* return OK; */ /* } */ /* best = NULL; */ /* res_tmp = NULL; */ /* MMALLOC(res_ptr, sizeof(struct kmeans_result*) * 4); */ /* for(i = 0; i < 4;i++){ */ /* res_ptr[i] = NULL; */ /* } */ /* tries = MACRO_MIN(tries, num_samples); */ /* int step = num_samples / tries; */ /* int change = 0; */ /* for(i = 0;i < tries;i += 4){ */ /* change = 0; */ /* for(j = 0; j < 4;j++){ */ /* split(dm,samples,num_anchors, num_samples, (i+ j)*step, &res_ptr[j]); */ /* } */ /* for(j = 0; j < 4;j++){ */ /* if(!best){ */ /* change++; */ /* best = res_ptr[j]; */ /* res_ptr[j] = NULL; */ /* }else{ */ /* if(best->score > res_ptr[j]->score){ */ /* res_tmp = best; */ /* best = res_ptr[j]; */ /* res_ptr[j] = res_tmp; */ /* /\* LOG_MSG("Better!!! %f %f", res_tmp->score,best->score); *\/ */ /* change++; */ /* } */ /* } */ /* } */ /* if(!change){ */ /* break; */ /* } */ /* } */ /* sl = best->sl; */ /* sr = best->sr; */ /* num_l = best->nl; */ /* num_r = best->nr; */ /* for(i = 0; i < 4;i++){ */ /* free_kmeans_results(res_ptr[i]); */ /* } */ /* MFREE(res_ptr); */ /* MFREE(best); */ /* MFREE(samples); */ /* n = alloc_node(); */ /* bisecting_kmeans_serial(msa,&n->left , dm, sl, num_l); */ /* bisecting_kmeans_serial(msa,&n->right, dm, sr, num_r); */ /* *ret_n = n; */ /* return OK; */ /* ERROR: */ /* return FAIL; */ /* } */ int split(const float * const * dm,int* samples, int num_anchors,int num_samples,int seed_pick,struct kmeans_result** ret) { struct kmeans_result* res = NULL; int* sl = NULL; int* sr = NULL; int num_l,num_r; float* w = NULL; float* wl = NULL; float* wr = NULL; float* cl = NULL; float* cr = NULL; float dl = 0.0F; float dr = 0.0F; float score; int num_var; int i; int s; int j; int stop = 0; num_var = num_anchors / 8; if( num_anchors%8){ num_var++; } num_var = num_var << 3; #ifdef HAVE_AVX2 wr = _mm_malloc(sizeof(float) * num_var,32); wl = _mm_malloc(sizeof(float) * num_var,32); cr = _mm_malloc(sizeof(float) * num_var,32); cl = _mm_malloc(sizeof(float) * num_var,32); w = _mm_malloc(sizeof(float) * num_var,32); #else MMALLOC(wr,sizeof(float) * num_var); MMALLOC(wl,sizeof(float) * num_var); MMALLOC(cr,sizeof(float) * num_var); MMALLOC(cl,sizeof(float) * num_var); MMALLOC(w,sizeof(float) * num_var); #endif if(*ret){ res = *ret; }else{ RUNP(res = alloc_kmeans_result(num_samples)); } res->score = FLT_MAX; sl = res->sl; sr = res->sr; for(i = 0; i < num_var;i++){ w[i] = 0.0F; wr[i] = 0.0F; wl[i] = 0.0F; cr[i] = 0.0F; cl[i] = 0.0F; } for(i = 0; i < num_samples;i++){ s = samples[i]; for(j = 0; j < num_anchors;j++){ w[j] += dm[s][j]; } } for(j = 0; j < num_anchors;j++){ w[j] /= (float)num_samples; } //r = tl_random_int(rng , num_samples); //r = sel[t_iter]; s = samples[seed_pick]; /* LOG_MSG("Selected %d\n",s); */ for(j = 0; j < num_anchors;j++){ cl[j] = dm[s][j]; } for(j = 0; j < num_anchors;j++){ cr[j] = w[j] - (cl[j] - w[j]); fprintf(stdout,"BEGIN: %e %e diff::: %f %f\n", cl[j],cr[j], cl[j]-cr[j],w[j]); } #ifdef HAVE_AVX2 _mm_free(w); #else MFREE(w); #endif /* check if cr == cl - we have identical sequences */ s = 0; for(j = 0; j < num_anchors;j++){ int res = cmp_floats(cl[j],cr[j]); /* if(fabsf(cl[j]-cr[j]) > 1.0E-6){ */ /* s = 1; */ /* break; */ /* } */ if(res != 0){ s++; } } fprintf(stdout,"S: %d\n",s); s = 0; for(j = 0; j < num_anchors;j++){ /* int res = cmp_floats(dr,dl); */ if(fabsf(cl[j]-cr[j]) > 1.0E-6){ s++; //break; } /* if(res != 0){ */ /* s++; */ /* } */ } fprintf(stdout,"S: %d\n",s); #ifdef HAVE_AVX2 edist_256(cl,cr, num_anchors, &dr); #else edist_serial(cl, cr, num_anchors, &dr); #endif fprintf(stdout,"R/L Dist: %e %e %d\n",dr,1e-6, dr < 1e-6); cmp_floats(cl[j],cr[j]); if(!s){ score = 0.0F; num_l = 0; num_r = 0; /* The code below caused sequence sets of size 1 to be passed to clustering... */ /* sl[num_l] = samples[0]; */ /* num_l++; */ /* for(i =1 ; i nl = num_l; res->nr = num_r; res->score = score; *ret = res; return OK; ERROR: return FAIL; } int split2(const float * const * dm,const int* samples, const int num_anchors,const int num_samples,const int seed_pick,struct kmeans_result** ret) { struct kmeans_result* res = NULL; int* sl = NULL; int* sr = NULL; int num_l,num_r; float* w = NULL; float* wl = NULL; float* wr = NULL; float* cl = NULL; float* cr = NULL; float dl = 0.0F; float dr = 0.0F; float score; int num_var; int i; int s; int j; num_var = num_anchors / 8; if( num_anchors%8){ num_var++; } num_var = num_var << 3; #ifdef HAVE_AVX2 wr = _mm_malloc(sizeof(float) * num_var,32); wl = _mm_malloc(sizeof(float) * num_var,32); cr = _mm_malloc(sizeof(float) * num_var,32); cl = _mm_malloc(sizeof(float) * num_var,32); w = _mm_malloc(sizeof(float) * num_var,32); #else MMALLOC(wr,sizeof(float) * num_var); MMALLOC(wl,sizeof(float) * num_var); MMALLOC(cr,sizeof(float) * num_var); MMALLOC(cl,sizeof(float) * num_var); MMALLOC(w,sizeof(float) * num_var); #endif if(*ret){ res = *ret; }else{ RUNP(res = alloc_kmeans_result(num_samples)); } res->score = FLT_MAX; sl = res->sl; sr = res->sr; for(i = 0; i < num_var;i++){ w[i] = 0.0F; wr[i] = 0.0F; wl[i] = 0.0F; cr[i] = 0.0F; cl[i] = 0.0F; } for(i = 0; i < num_samples;i++){ s = samples[i]; for(j = 0; j < num_anchors;j++){ w[j] += dm[s][j]; } } for(j = 0; j < num_anchors;j++){ w[j] /= (float)num_samples; } //r = tl_random_int(rng , num_samples); //r = sel[t_iter]; s = samples[seed_pick]; /* LOG_MSG("Selected %d\n",s); */ for(j = 0; j < num_anchors;j++){ cl[j] = dm[s][j]; } for(j = 0; j < num_anchors;j++){ cr[j] = w[j] - (cl[j] - w[j]); } #ifdef HAVE_AVX2 _mm_free(w); #else MFREE(w); #endif w = NULL; for(int stop = 0; stop < 500; stop++){ num_l = 0; num_r = 0; for(i = 0; i < num_anchors;i++){ wr[i] = 0.0F; wl[i] = 0.0F; } score = 0.0f; for(i = 0; i < num_samples;i++){ s = samples[i]; #ifdef HAVE_AVX2 edist_256(dm[s], cl, num_anchors, &dl); edist_256(dm[s], cr, num_anchors, &dr); #else edist_serial(dm[s], cl, num_anchors, &dl); edist_serial(dm[s], cr, num_anchors, &dr); #endif score += MACRO_MIN(dl,dr); int res = cmp_floats(dr,dl); if(res == -1){ w = wr; sr[num_r] = s; num_r++; }else if (res == 1){ w = wl; sl[num_l] = s; num_l++; }else{ if(i & 1){ w = wr; sr[num_r] = s; num_r++; }else{ w = wl; sl[num_l] = s; num_l++; } } for(j = 0; j < num_anchors;j++){ w[j] += dm[s][j]; } } if(num_l == 0 || num_r == 0){ score = 0.0F; num_l = 0; num_r = 0; for(i = 0; i < num_samples/2;i++){ sl[num_l] = samples[i]; num_l++; } for(i = num_samples/2; i < num_samples;i++){ sr[num_r] = samples[i]; num_r++; } break; } for(j = 0; j < num_anchors;j++){ wl[j] /= (float)num_l; wr[j] /= (float)num_r; } s = 0; for(j = 0; j < num_anchors;j++){ int res = cmp_floats(wl[j],cl[j]); if(res != 0){ s = 1; break; } res = cmp_floats(wr[j],cr[j]); if(res != 0){ s = 1; break; } } if(s){ w = cl; cl = wl; wl = w; w = cr; cr = wr; wr = w; }else{ break; } } #ifdef HAVE_AVX2 _mm_free(wr); _mm_free(wl); _mm_free(cr); _mm_free(cl); #else MFREE(wr); MFREE(wl); MFREE(cr); MFREE(cl); #endif res->nl = num_l; res->nr = num_r; res->score = score; *ret = res; return OK; ERROR: return FAIL; } struct node* upgma(float **dm,int* samples, int numseq) { struct node** tree = NULL; struct node* tmp = NULL; int i,j; int *as = NULL; float max; int node_a = 0; int node_b = 0; int cnode = numseq; int numprofiles; numprofiles = (numseq << 1) - 1; MMALLOC(as,sizeof(int)*numseq); for (i = numseq; i--;){ as[i] = i+1; } MMALLOC(tree,sizeof(struct node*)*numseq); for (i = 0;i < numseq;i++){ tree[i] = NULL; tree[i] = alloc_node(); tree[i]->id = samples[i]; } while (cnode != numprofiles){ max = FLT_MAX; for (i = 0;i < numseq-1; i++){ if (as[i]){ for ( j = i + 1;j < numseq;j++){ if (as[j]){ if (dm[i][j] < max){ max = dm[i][j]; node_a = i; node_b = j; } } } } } tmp = NULL; tmp = alloc_node(); tmp->left = tree[node_a]; tmp->right = tree[node_b]; tree[node_a] = tmp; tree[node_b] = NULL; /*deactivate sequences to be joined*/ as[node_a] = cnode+1; as[node_b] = 0; cnode++; /*calculate new distances*/ for (j = numseq;j--;){ if (j != node_b){ dm[node_a][j] = (dm[node_a][j] + dm[node_b][j])*0.5F + 0.001F; } //fprintf(stdout,"\n"); } dm[node_a][node_a] = 0.0F; for (j = numseq;j--;){ dm[j][node_a] = dm[node_a][j]; } } tmp = tree[node_a]; MFREE(tree); MFREE(as); return tmp; ERROR: return NULL; } struct node* alloc_node(void) { struct node* n = NULL; MMALLOC(n, sizeof(struct node)); n->left = NULL; n->right = NULL; n->id = -1; return n; ERROR: return NULL; } int label_internal(struct node*n, int label) { //n->d = d; if(n->left){ label = label_internal(n->left, label); } if(n->right){ label = label_internal(n->right, label); } if(n->id == -1){ n->id = label; label++; } return label; } void create_tasks(struct node*n, struct aln_tasks* t) { if(n->left && n->right){ struct task* task; task = t->list[t->n_tasks]; task->a = n->left->id; task->b = n->right->id; task->c = n->id; /* task->p = depth; */ /* task->p = n->d; */ /* task->n = n->n; */ /* fprintf(stdout,"Node %d depends on %d %d\n", n->id , n->left->id, n->right->id); */ t->n_tasks++; } if(n->left){ create_tasks(n->left,t); } if(n->right){ create_tasks(n->right,t); } if(n->left){ if(n->right){ MFREE(n->left); MFREE(n->right); } } } struct kmeans_result* alloc_kmeans_result(int num_samples) { struct kmeans_result* k = NULL; ASSERT(num_samples != 0, "No samples???"); MMALLOC(k, sizeof(struct kmeans_result)); k->nl = 0; k->nr = 0; k->sl = NULL; k->sr = NULL; MMALLOC(k->sl, sizeof(int) * num_samples); MMALLOC(k->sr, sizeof(int) * num_samples); k->score = FLT_MAX; return k; ERROR: free_kmeans_results(k); return NULL; } void free_kmeans_results(struct kmeans_result* k) { if(k){ if(k->sl){ MFREE(k->sl); } if(k->sr){ MFREE(k->sr); } MFREE(k); } } int build_tree_from_pairwise(struct msa* msa, struct aln_tasks** tasks, float** dm) { struct aln_tasks* t = NULL; struct node* root = NULL; int* samples = NULL; int numseq; int i, j; ASSERT(msa != NULL, "No alignment."); ASSERT(dm != NULL, "No distance matrix."); t = *tasks; if(!t){ RUN(alloc_tasks(&t, msa->numseq)); } numseq = msa->numseq; /* Compute per-sequence mean pairwise distance (for gap/VSM scaling). Must be done BEFORE upgma() which modifies dm in place. */ if(msa->seq_distances == NULL){ MMALLOC(msa->seq_distances, sizeof(float) * numseq); } for(i = 0; i < numseq; i++){ float sum = 0.0f; for(j = 0; j < numseq; j++){ if(j != i) sum += dm[i][j]; } msa->seq_distances[i] = (numseq > 1) ? sum / (float)(numseq - 1) : 0.0f; } MMALLOC(samples, sizeof(int) * numseq); for(i = 0; i < numseq; i++){ samples[i] = i; } /* Build UPGMA tree from pairwise distances. Note: upgma() modifies dm in-place. */ root = upgma(dm, samples, numseq); ASSERT(root != NULL, "UPGMA tree construction failed."); label_internal(root, numseq); create_tasks(root, t); MFREE(samples); MFREE(root); *tasks = t; return OK; ERROR: if(samples) MFREE(samples); return FAIL; } kalign-3.5.1/lib/src/bisectingKmeans.h000066400000000000000000000013541515023132300175770ustar00rootroot00000000000000#ifndef BISECTINGKMEANS_H #define BISECTINGKMEANS_H #include #ifdef BISECTINGKMEANS_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct aln_tasks; struct msa; /* int build_tree_kmeans(struct msa* msa, struct aln_param* ap,struct aln_tasks** task_list); */ EXTERN int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks); EXTERN int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, uint64_t seed, float noise_sigma); EXTERN int build_tree_from_pairwise(struct msa* msa, struct aln_tasks** tasks, float** dm); #undef BISECTINGKMEANS_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/bpm.c000066400000000000000000000464611515023132300152520ustar00rootroot00000000000000#include #include #include #include "tldevel.h" #define BPM_IMPORT #include "bpm.h" #include #include #define SIGMA 13 #define DIV_CEIL(a,b) (a == 0 ? 1 : a/b+(a%b == 0 ? 0 : 1)) #ifdef HAVE_AVX2 #include __m256i BROADCAST_MASK[16]; void bitShiftLeft256ymm (__m256i *data, int count); __m256i bitShiftRight256ymm (__m256i *data, int count); /* taken from Alexander Yee: http://www.numberworld.org/y-cruncher/internals/addition.html#ks_add */ __m256i add256(uint32_t carry, __m256i A, __m256i B); #endif uint8_t dyn_256(const uint8_t* t,const uint8_t* p,int n,int m) { uint8_t* prev = NULL; uint8_t* cur = NULL; uint8_t* tmp = NULL; int i,j,c; if(m > 255){ m = 255; } MMALLOC(prev, sizeof(uint8_t)* 257); MMALLOC(cur, sizeof(uint8_t)* 257); cur[0] = 0; //fprintf(stdout,"%d ", cur[0]); for(j = 1; j <= m;j++){ cur[j] = cur[j-1] +1; //fprintf(stdout,"%d ", cur[j]); } //fprintf(stdout,"\n"); tmp = cur; cur = prev; prev = tmp; for(i = 1; i <= n;i++){ cur[0] = prev[0] ; //fprintf(stdout,"%d ", cur[0]); for(j = 1; j < m;j++){ c = 1; if(t[i-1] == p[j-1]){ c = 0; } cur[j] = prev[j-1] +c ; cur[j] = MACRO_MIN(cur[j], prev[j]+1); cur[j] = MACRO_MIN(cur[j], cur[j-1]+1); } j = m; c = 1; if(t[i-1] == p[j-1]){ c = 0; } cur[j] = prev[j-1] +c ; cur[j] = MACRO_MIN(cur[j], prev[j]); cur[j] = MACRO_MIN(cur[j], cur[j-1]+1); //fprintf(stdout,"\n"); tmp = cur; cur = prev; prev = tmp; } c = prev[m]; MFREE(prev); MFREE(cur); return c; ERROR: return 255; } uint8_t bpm(const uint8_t* t,const uint8_t* p,int n,int m) { register uint64_t VP,VN,D0,HN,HP,X; register uint64_t i;//,c; uint64_t MASK = 0; int64_t diff; uint64_t B[13]; int8_t k; if(m > 63){ m = 63; } diff = m; k = m; for(i = 0; i < 13;i++){ B[i] = 0; } for(i = 0; i < (uint64_t)m;i++){ B[p[i]] |= (1ul << i); } VP = (1ul << (m))-1 ; VN = 0ul; m--; MASK = 1ul << (m); //fprintf(stdout,"BEGINNING\t%lu %lu\n",VN,VP); for(i = 0; i < (uint64_t) n;i++){ // fprintf(stdout,"%lu:\t",i); X = (B[t[i]] | VN); //fprintf(stdout,"%lu ", X); D0 = ((VP+(X&VP)) ^ VP) | X ; //fprintf(stdout,"%lu ", D0); HN = VP & D0; //fprintf(stdout,"%lu ", HN); HP = VN | (~(VP | D0)); //fprintf(stdout,"%lu ", HP); X = HP << 1ul; //fprintf(stdout,"%lu ", X); VN = X & D0; //fprintf(stdout,"%lu ", VN); VP = (HN << 1ul) | (~(X | D0)); //fprintf(stdout,"%lu ", VP); diff += (HP & MASK)? 1 : 0;// >> m; diff -= (HN & MASK)? 1 : 0;// >> m; if(diff < k){ k = diff; } //fprintf(stdout,"%lu ", diff); //fprintf(stdout,"\n"); } return k; } #ifdef HAVE_AVX2 uint8_t bpm_256(const uint8_t* t,const uint8_t* p,int n,int m) { __m256i VP,VN,D0,HN,HP,X,NOTONE; __m256i xmm1,xmm2; __m256i MASK; __m256i B[13]; int i,j, k,diff; alignas(32) uint32_t f[13][8]; //int ALIGNED_(64) f[8]; if(m > 255){ m = 255; } for(i = 0; i < 13;i++){ for(j = 0;j < 8;j++){ f[i][j] =0u; } } for(i = 0; i < m;i++){ f[p[i]][i/32] |= (1 << (i % 32)); } /* for(i = 0; i < 13;i++){ */ /* B[i] = _mm256_load_si256((__m256i const*) &f[i]); */ /* } */ B[0] = _mm256_load_si256((__m256i const*) &f[0]); B[1] = _mm256_load_si256((__m256i const*) &f[1]); B[2] = _mm256_load_si256((__m256i const*) &f[2]); B[3] = _mm256_load_si256((__m256i const*) &f[3]); B[4] = _mm256_load_si256((__m256i const*) &f[4]); B[5] = _mm256_load_si256((__m256i const*) &f[5]); B[6] = _mm256_load_si256((__m256i const*) &f[6]); B[7] = _mm256_load_si256((__m256i const*) &f[7]); B[8] = _mm256_load_si256((__m256i const*) &f[8]); B[9] = _mm256_load_si256((__m256i const*) &f[9]); B[10] = _mm256_load_si256((__m256i const*) &f[10]); B[11] = _mm256_load_si256((__m256i const*) &f[11]); B[12] = _mm256_load_si256((__m256i const*) &f[12]); diff = m; k = m; VP = _mm256_set1_epi64x(0xFFFFFFFFFFFFFFFFul); VN = _mm256_setzero_si256(); NOTONE = _mm256_set1_epi64x(0xFFFFFFFFFFFFFFFFul); MASK = _mm256_set_epi64x (0ul,0ul,0ul,1); m--; i = m / 64; while(i){ bitShiftLeft256ymm(&MASK,64); i--; } bitShiftLeft256ymm(&MASK,m%64); for(i = 0; i < n ;i++){ //X = (B[(int) *t] | VN); X = _mm256_or_si256(B[t[i]], VN); //fprintf(stdout,"%lu ", X); //D0 = ((VP+(X&VP)) ^ VP) | X ; //print_256(X); xmm1 = _mm256_and_si256(X, VP); xmm2 = add256(0, VP, xmm1); //xmm2 = _mm256_add_epi64(VP, xmm1); xmm1 = _mm256_xor_si256(xmm2, VP); D0 = _mm256_or_si256(xmm1, X); //print_256(D0); //HN = VP & D0; HN =_mm256_and_si256(VP, D0); //print_256(HN); //HP = VN | ~(VP | D0); xmm1 = _mm256_or_si256(VP, D0); xmm2 = _mm256_andnot_si256(xmm1, NOTONE); HP = _mm256_or_si256(VN, xmm2); //print_256(HP); //X = HP << 1ul; X = HP; bitShiftLeft256ymm(&X, 1); //print_256(X); //VN = X & D0; VN= _mm256_and_si256(X, D0); //print_256(VN); //VP = (HN << 1ul) | ~(X | D0); xmm1 = HN; bitShiftLeft256ymm(&xmm1, 1); xmm2 = _mm256_or_si256(X, D0); xmm2 = _mm256_andnot_si256(xmm2, NOTONE); //xmm2 = _mm_andnot_si128 (xmm2,NOTONE); VP = _mm256_or_si256(xmm1, xmm2); //print_256(VP); //diff += (HP & MASK) >> m; diff += 1- _mm256_testz_si256(HP, MASK); ///diff -= (HN & MASK) >> m; diff -= 1- _mm256_testz_si256(HN,MASK); //fprintf(stdout,"%d ",diff); //xmm1 = _mm256_cmpgt_epi64(K, diff); k = MACRO_MIN(k, diff); } return k; } /* Must be called before BPM_256 is!!! */ void set_broadcast_mask(void) { BROADCAST_MASK[0] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000000, 0x8000000000000000, 0x8000000000000000); BROADCAST_MASK[1] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000000, 0x8000000000000000, 0x8000000000000001); BROADCAST_MASK[2] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000000, 0x8000000000000001, 0x8000000000000000); BROADCAST_MASK[3] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000000, 0x8000000000000001, 0x8000000000000001); BROADCAST_MASK[4] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000001, 0x8000000000000000, 0x8000000000000000); BROADCAST_MASK[5] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000001, 0x8000000000000000, 0x8000000000000001); BROADCAST_MASK[6] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000001, 0x8000000000000001, 0x8000000000000000); BROADCAST_MASK[7] = _mm256_set_epi64x(0x8000000000000000, 0x8000000000000001, 0x8000000000000001, 0x8000000000000001); BROADCAST_MASK[8] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000000, 0x8000000000000000, 0x8000000000000000); BROADCAST_MASK[9] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000000, 0x8000000000000000, 0x8000000000000001); BROADCAST_MASK[10] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000000, 0x8000000000000001, 0x8000000000000000); BROADCAST_MASK[11] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000000, 0x8000000000000001, 0x8000000000000001); BROADCAST_MASK[12] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000001, 0x8000000000000000, 0x8000000000000000); BROADCAST_MASK[13] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000001, 0x8000000000000000, 0x8000000000000001); BROADCAST_MASK[14] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000001, 0x8000000000000001, 0x8000000000000000); BROADCAST_MASK[15] = _mm256_set_epi64x(0x8000000000000001, 0x8000000000000001, 0x8000000000000001, 0x8000000000000001); } __m256i add256(uint32_t carry, __m256i A, __m256i B) { A = _mm256_xor_si256(A, _mm256_set1_epi64x(0x8000000000000000)); __m256i s = _mm256_add_epi64(A, B); __m256i cv = _mm256_cmpgt_epi64(A, s); __m256i mv = _mm256_cmpeq_epi64(s, _mm256_set1_epi64x(0x7fffffffffffffff)); uint32_t c = _mm256_movemask_pd(_mm256_castsi256_pd(cv)); uint32_t m = _mm256_movemask_pd(_mm256_castsi256_pd(mv)); { c = m + 2*c; // lea carry += c; m ^= carry; carry >>= 4; m &= 0x0f; } return _mm256_add_epi64(s, BROADCAST_MASK[m]); } //---------------------------------------------------------------------------- // bit shift left a 256-bit value using ymm registers // __m256i *data - data to shift // int count - number of bits to shift // return: __m256i - carry out bit(s) void bitShiftLeft256ymm (__m256i *data, int count) { __m256i innerCarry, rotate; innerCarry = _mm256_srli_epi64 (*data, 64 - count); // carry outs in bit 0 of each qword rotate = _mm256_permute4x64_epi64 (innerCarry, 0x93); // rotate ymm left 64 bits innerCarry = _mm256_blend_epi32 (_mm256_setzero_si256 (), rotate, 0xFC); // clear lower qword *data = _mm256_slli_epi64 (*data, count); // shift all qwords left *data = _mm256_or_si256 (*data, innerCarry); // propagate carrys from low qwords //carryOut = _mm256_xor_si256 (innerCarry, rotate); // clear all except lower qword //return carryOut; } __m256i bitShiftRight256ymm (__m256i *data, int count) { __m256i innerCarry, carryOut, rotate; innerCarry = _mm256_slli_epi64(*data, 64 - count); rotate = _mm256_permute4x64_epi64 (innerCarry, 0x39); innerCarry = _mm256_blend_epi32 (_mm256_setzero_si256 (), rotate, 0x3F); *data = _mm256_srli_epi64(*data, count); *data = _mm256_or_si256(*data, innerCarry); carryOut = _mm256_xor_si256 (innerCarry, rotate); //FIXME: not sure if this is correct!!! return carryOut; } #endif int bpm_block(const uint8_t *t, const uint8_t *p, int n, int m) { int32_t w; int32_t w_bytes; int32_t b_max; int32_t maxd; int32_t k; int32_t W; uint64_t HIGH_BIT; uint64_t ONE = 1; if(m > 1024){ m = 1024; } w_bytes = sizeof(uint64_t); w = w_bytes * 8; b_max = DIV_CEIL(m,w); HIGH_BIT = ONE << (w - 1); W = w * b_max - m; k = m; maxd = m; /* Precompute */ uint64_t Peq[13][16] = { {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; uint64_t P[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; uint64_t M[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; int32_t score[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; /* m = 1024; *- > 16 */ /* w_bytes = sizeof(uint64_t); */ /* w = w_bytes * 8; */ /* b_max = DIV_CEIL(m,w); */ /* LOG_MSG("%d -> %d",m,b_max); */ /* W = w * b_max - m; */ HIGH_BIT = ONE << (w - 1); /* MMALLOC(score,b_max * sizeof(int32_t)); */ /* MMALLOC(P ,b_max * w_bytes); */ /* MMALLOC(M ,b_max * w_bytes); */ /* MMALLOC(Peq,SIGMA * sizeof(uint64_t *)); */ for (int c = 0; c < SIGMA; c++) { /* Peq[c] = NULL; */ /* MMALLOC(Peq[c], b_max*w_bytes); */ /* Peq[c] = (uint64_t *) malloc(b_max*w_bytes); */ /* memset(Peq[c], 0, b_max*w_bytes); */ for (int32_t block = 0; block < b_max; block++) { uint64_t bitPos = (uint64_t) 1; for (int32_t i = block * w; i < (block + 1) * w; ++i) { // fill the remainder after the last block with 1 (>m matches anything) if (i >= m || p[i] == c) { Peq[c][block] |= bitPos; } /* if (i >= m ){ */ /* LOG_MSG("Am filling %d",i); */ /* } */ bitPos <<= 1; } } } int y = DIV_CEIL(maxd, w) - 1; /* 256 is max edit distance */ /* init blocks */ for (int b = 0; b <= y; b++) { /* bpm_init_block(s,b); */ P[b] = (uint64_t) -1;//Ones M[b] = 0; score[b] = (uint32_t)(b + 1) * w; } for (int i = 0; i < n+W; i++) { /* LOG_MSG(" (%d)",i); */ uint8_t c = 0; if(i >= n){ /* seq padding */ c = 0; }else{ c = (uint8_t)t[i]; } /* LOG_MSG(" c: %d (%d)", c,i); */ int carry = 0; for (int b = 0; b <= y; b++) { /* LOG_MSG("y: %d c: %d b: %d n: %d m:%d",y, c,b,n,m); */ uint64_t Pv = P[b]; uint64_t Mv = M[b]; uint64_t Eq = Peq[c][b]; uint64_t Xv, Xh; uint64_t Ph, Mh; int hIn = carry; int h_out = 0; Xv = Eq | Mv; if (hIn < 0) { Eq |= ONE; } Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq; Ph = Mv | (~(Xh | Pv)); Mh = Pv & Xh; if (Ph & HIGH_BIT) { h_out += 1; } if (Mh & HIGH_BIT) { h_out -= 1; } Ph <<= 1; Mh <<= 1; if (hIn < 0) { Mh |= ONE; } else if (hIn > 0) { Ph |= ONE; } Pv = Mh | (~(Xv | Ph)); Mv = Ph & Xv; P[b] = Pv; M[b] = Mv; carry = h_out; score[b] += carry; } if ((score[y] - carry <= maxd) && (y < (b_max - 1)) && ((Peq[c][y + 1] & ONE) || (carry < 0))) { y += 1; /* bpm_init_block(s,y); */ P[y] = (uint64_t) -1;//Ones M[y] = 0; int b = y; uint64_t Pv = P[b]; uint64_t Mv = M[b]; uint64_t Eq = Peq[c][b]; uint64_t Xv, Xh; uint64_t Ph, Mh; int hIn = carry; int h_out = 0; Xv = Eq | Mv; if (hIn < 0) { Eq |= ONE; } Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq; Ph = Mv | (~(Xh | Pv)); Mh = Pv & Xh; if (Ph & HIGH_BIT) { h_out += 1; } if (Mh & HIGH_BIT) { h_out -= 1; } Ph <<= 1; Mh <<= 1; if (hIn < 0) { Mh |= ONE; } else if (hIn > 0) { Ph |= ONE; } Pv = Mh | (~(Xv | Ph)); Mv = Ph & Xv; P[b] = Pv; M[b] = Mv; /* carry = h_out; */ score[y] = score[y - 1] + w - carry + h_out;//advanceBlock(s,y, c, carry); } else { while (score[y] >= (maxd + w)) { if (y == 0) break; y -= 1; /* LOG_MSG("y in loop: %d", y); */ } } if(score[y] < k){ /* LOG_MSG("%d", s->score[y]); */ k = score[y]; } /* if (y == (b_max - 1) && s->score[y] <= maxd) { */ /* assert(i - s->W >= 0); */ /* /\* result->push_front(SearchResult(score[y], (uint32_t)(i - W))); *\/ */ /* } */ } return k; } kalign-3.5.1/lib/src/bpm.h000066400000000000000000000015641515023132300152520ustar00rootroot00000000000000#ifndef BPM_H #define BPM_H #include #ifdef BPM_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif /* #ifdef HAVE_AVX2 */ /* #define BPM(a,b,len_a,len_b) bpm_256(a,b,len_a,len_b) */ /* #else */ #define BPM(a,b,len_a,len_b) bpm_block(a,b,len_a,len_b) /* #endif */ /* #define LOG_MSG(...) do { \ */ /* log_message( __VA_ARGS__ ); \ */ /* }while (0) */ /* Must be called before bpm_256!!!! */ EXTERN void set_broadcast_mask(void); EXTERN uint8_t bpm_256(const uint8_t* t,const uint8_t* p,int n,int m); EXTERN uint8_t bpm(const uint8_t* t,const uint8_t* p,int n,int m); EXTERN int bpm_block(const uint8_t *t, const uint8_t *p, int n, int m); EXTERN uint8_t dyn_256(const uint8_t* t,const uint8_t* p,int n,int m); #undef BPM_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/bpm_test.c000066400000000000000000000417021515023132300163020ustar00rootroot00000000000000#include "tldevel.h" #include "tlrng.h" #define BPM_IMPORT #include "bpm.h" #include #include #include #include "alphabet.h" #include "esl_stopwatch.h" /* Functions needed for the unit test*/ uint8_t dyn_256_print(const uint8_t* t,const uint8_t* p,int n,int m); int mutate_seq(uint8_t* s, int len,int k,int L, struct rng_state* rng); #ifdef HAVE_AVX2 #include /* For debugging */ void print_256(__m256i X); void print_256_all(__m256i X); #endif #define SIGMA 13 #define DIV_CEIL(a,b) (a == 0 ? 1 : a/b+(a%b == 0 ? 0 : 1)) struct bpm_struct { uint64_t** Peq; uint64_t* P; uint64_t* M; uint64_t HIGH_BIT; uint64_t ONE; int32_t W; int32_t* score; int k; }; static int bpm_block2(const uint8_t *t, const uint8_t *p, int n, int m, int maxd); static int bpm_precompute(const uint8_t *p, int m, struct bpm_struct **out); static int bpm_init_block(struct bpm_struct *s, int b); static inline int advanceBlock(struct bpm_struct *s, int b, uint8_t c, int hIn); int bpm_block2(const uint8_t *t, const uint8_t *p, int n, int m, int maxd) { struct bpm_struct* s = NULL; int w; int32_t w_bytes; int32_t b_max; uint64_t ONE; w_bytes = sizeof(uint64_t); w = w_bytes * 8; b_max = DIV_CEIL(m,w); bpm_precompute(p, m, &s); ONE = s->ONE; w_bytes = sizeof(uint64_t); w = w_bytes * 8; int y = DIV_CEIL(maxd, w) - 1; /* 256 is max edit distance */ for (int b = 0; b <= y; b++) { bpm_init_block(s,b); s->score[b] = (uint32_t)(b + 1) * w; } for (int i = 0; i < n+s->W; i++) { uint8_t c = (uint8_t)t[i]; int carry = 0; for (int b = 0; b <= y; b++) { carry = advanceBlock(s,b, c, carry); s->score[b] += carry; } if ((s->score[y] - carry <= maxd) && (y < (b_max - 1)) && ((s->Peq[c][y + 1] & ONE) || (carry < 0))) { y += 1; bpm_init_block(s,y); s->score[y] = s->score[y - 1] + w - carry + advanceBlock(s,y, c, carry); } else { while (s->score[y] >= (maxd + w)) { if (y == 0) break; y -= 1; } } if(s->score[y] < s->k){ /* LOG_MSG("%d", s->score[y]); */ s->k = s->score[y]; } /* if (y == (b_max - 1) && s->score[y] <= maxd) { */ /* assert(i - s->W >= 0); */ /* /\* result->push_front(SearchResult(score[y], (uint32_t)(i - W))); *\/ */ /* } */ } int k = s->k; for (int c = 0; c < SIGMA; c++) { MFREE(s->Peq[c]); } MFREE(s->Peq); MFREE(s->score); MFREE(s->P); MFREE(s->M); MFREE(s); return k; } inline int advanceBlock(struct bpm_struct *s, int b, uint8_t c, int hIn) { uint64_t Pv = s->P[b]; uint64_t Mv = s->M[b]; uint64_t Eq = s->Peq[c][b]; uint64_t ONE = s->ONE; uint64_t HIGH_BIT = s->HIGH_BIT; uint64_t Xv, Xh; uint64_t Ph, Mh; int h_out = 0; Xv = Eq | Mv; if (hIn < 0) { Eq |= ONE; } Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq; Ph = Mv | (~(Xh | Pv)); Mh = Pv & Xh; if (Ph & HIGH_BIT) { h_out += 1; } if (Mh & HIGH_BIT) { h_out -= 1; } Ph <<= 1; Mh <<= 1; if (hIn < 0) { Mh |= ONE; } else if (hIn > 0) { Ph |= ONE; } Pv = Mh | (~(Xv | Ph)); Mv = Ph & Xv; s->P[b] = Pv; s->M[b] = Mv; return h_out; } int bpm_init_block(struct bpm_struct *s, int b) { int w; int32_t w_bytes; w_bytes = sizeof(uint64_t); w = w_bytes * 8; s->P[b] = (uint64_t) -1;//Ones s->M[b] = 0; s->score[b] = (uint32_t)(b + 1) * w; return OK; } int bpm_precompute(const uint8_t *p, int m, struct bpm_struct **out) { struct bpm_struct* s = NULL; uint64_t** Peq = NULL; uint64_t* P = NULL; uint64_t* M = NULL; int32_t *score = NULL; int32_t w_bytes; int32_t b_max; uint64_t W; int w; uint64_t HIGH_BIT; uint64_t ONE = 1; /* m = 1024; *- > 16 */ w_bytes = sizeof(uint64_t); w = w_bytes * 8; b_max = DIV_CEIL(m,w); /* LOG_MSG("%d -> %d",m,b_max); */ W = w * b_max - m; HIGH_BIT = ONE << (w - 1); MMALLOC(score,b_max * sizeof(int32_t)); MMALLOC(P ,b_max * w_bytes); MMALLOC(M ,b_max * w_bytes); MMALLOC(Peq,SIGMA * sizeof(uint64_t *)); for (int c = 0; c < SIGMA; c++) { Peq[c] = NULL; MMALLOC(Peq[c], b_max*w_bytes); /* Peq[c] = (uint64_t *) malloc(b_max*w_bytes); */ memset(Peq[c], 0, b_max*w_bytes); for (int32_t block = 0; block < b_max; block++) { uint64_t bitPos = (uint64_t) 1; for (int32_t i = block * w; i < (block + 1) * w; ++i) { // fill the remainder after the last block with 1 (>m matches anything) if (i >= m || p[i] == c) { Peq[c][block] |= bitPos; } bitPos <<= 1; } } } MMALLOC(s ,sizeof(struct bpm_struct)); s->M = M; s->P = P; s->Peq = Peq; s->W = W; s->ONE = ONE; s->HIGH_BIT = HIGH_BIT; s->score = score; s->k = m; *out = s; return OK; ERROR: return FAIL; } /* The actual test. */ int bpm_test(void); int main(void) { /* Important set_broadcast_mask has to be called before using bpm_256!!! */ #ifdef HAVE_AVX2 set_broadcast_mask(); #endif RUN(bpm_test()); return EXIT_SUCCESS; ERROR: return EXIT_FAILURE; } int bpm_test(void) { /* idea: start with identical sequences add errors run bpm */ struct alphabet* alphabet = NULL; struct rng_state* rng; //long int r; int len = 0; int i,j,c; uint8_t* a = NULL; uint8_t* b = NULL; int test_iter = 100; int dyn_score; int bpm_score; int bb_score; int bb_score2; int diff[4][4] = { {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }; RUNP(rng = init_rng(0)); RUNP(alphabet = create_alphabet(ALPHA_redPROTEIN)); char seq[] = "MLLRNSFISQDENDDQLSPTQLGRAAIASSLPPEASLAIFEDLNSASRAIALDTELHMLYLVYFYKNSRAQIIQKIFKIYSIFILKKFKNLEPKFKKKISENITVHITNSIRKKQHFWHVTPINVSVWQECDWHHLFSIFSKLPSDHKRIAKLVGVSEKFILDQLQGRRNDKLLQIHIRFFSALALFDLISEMSIYEVSHKYRIPRGCLQTLQSQSATYAAMIVAFCLRLGWTYLKALLDGFATRLLFGVRSELSELVAIEGIDGQRARILHERGVTCLSHLSACDSSKLAHFLTLAVPYSSSNSNDGLGEWLFGEPRMRVDVAARTLKERARKVLIRRVQELGISVELPKFEENEENIQESCDSGLPDSCEGMEDELEEKENIVKMEEMTKSVTEMSLTDNTISFKSEDDLFKKEIKVEEDEVFIKKEIDEDEEEIVEETVIECLETSLLKLKASTDEVFLRRLSQTFSPIGRSRSILNNSLLEDSFDRPVPRSSIPILNFITPKRESPTPYFEDSFDRPIPGSLPISSSRRKSVLTNIANLDSSRRESINSNASDNNSFDVFVTPPTKSAKEEKRRIAVKHPRVGNIIYSPLTSSPVIKHPKLEINHFYLKDVCHDHNAWNLWTKSSTSTSSCSIRVSDDYTGIAIRTDAGNTFIPLLETFGGENSVPQFFKNISNFYVLKMPLKIFWKQPSPASKYFESFSKCIIPLNTRLEFLKTLAVTVEMYISSMEDAFLIFEKFGIKIFRLKVVRIAAYLNNVIDVEQEENSNFLPILMDRYSILDPEIRKTCSSSLHKAAVEVYSLKPIFEKMCCSGASLQLEMESCQTVLNIFYSGIVFDQALCNSFIYKIRKQIENLEENIWRLAYGKFNIHSSNEVANVLFYRLGLIYPETSGCKPKLRHLPTNKLILEQMNTQHPIVGKILEYRQIQHTLTQCLMPLAKFIGRIHCWFEMCTSTGRILTSVPNLQNVPKRISSDGMSARQLFIANSENLLIGADYKQLELRVLAHLSNDSNLVNLITSDRDLFEELSIQWNFPRDAVKQLCYGLIYGMGAKSLSELTRMSIEDAEKMLKAFFAMFPGVRSYINETKEKVCKEEPISTIIGRRTIIKASGIGEERARIERVAVNYTIQGSASEIFKTAIVDIESKIKEFGAQIVLTIHDEVLVECPEIHVAAASESIENCMQNALSHLLRVPMRVSMKTGRSWADLK"; len = strlen(seq); if(len > 256){ len = 256; } MMALLOC(a , sizeof(uint8_t) * len) ; MMALLOC(b , sizeof(uint8_t) * len) ; for(i = 0;i < len;i++){ a[i] = alphabet->to_internal[(int)seq[i]]; b[i] =a[i]; /* fprintf(stdout,"%d %d\n", a[i],b[i]); */ } fprintf(stdout,"LEN: %d\n", len); LOG_MSG("Testing correctness of serial bpm."); // len = 63; len = 63; //could fix; for(i = 0; i < len;i++){ for (j =0 ; j < test_iter; j++){ RUN(mutate_seq(b,len,i,alphabet->L,rng)); dyn_score = dyn_256(a,b,len,len); bpm_score = bpm(a,b,len,len); bb_score = bpm_block2(a,b,len,len, len); bb_score2 = bpm_block(a,b,len,len); /* if(j == 0){ */ /* LOG_MSG("k:%d %d %d %d %d",i,dyn_score,bpm_score, bb_score , bb_score2); */ /* } */ if(dyn_score != bpm_score){ diff[0][1]++; } if(dyn_score != bb_score){ diff[0][2]++; } if(dyn_score != bb_score2){ diff[0][3]++; } if(bpm_score != bb_score){ diff[1][2]++; } if(bpm_score != bb_score2){ diff[1][3]++; } if(bb_score != bb_score2){ diff[2][3]++; } /* restore sequence b */ for(c = 0;c < len;c++){ b[c] = a[c]; } } } for(int i = 0; i < 4;i++){ for(int j = 0; j < 4;j++){ fprintf(stdout,"%3d ",diff[i][j]); } fprintf(stdout, "\n"); } fprintf(stdout, "\n"); LOG_MSG("Testing correctness of AVX bpm."); len = strlen(seq); if(len > 255){ len = 255; } for(int i = 0; i < 4;i++){ for(int j = 0; j < 4;j++){ diff[i][j] = 0; } } for(i = 0; i < len;i++){ for (j =0 ; j < test_iter; j++){ RUN(mutate_seq(b,len,i,alphabet->L,rng)); dyn_score = dyn_256(a,b,len,len); #ifdef HAVE_AVX2 bpm_score = bpm_256(a,b,len,len); #else bpm_score = dyn_score; #endif bb_score = bpm_block2(a,b,len,len, len); bb_score2 = bpm_block(a,b,len,len); /* if(j == 0){ */ /* LOG_MSG("k:%d %d %d %d %d",i,dyn_score,bpm_score, bb_score , bb_score2); */ /* } */ if(dyn_score != bpm_score){ diff[0][1]++; } if(dyn_score != bb_score){ diff[0][2]++; } if(dyn_score != bb_score2){ diff[0][3]++; } if(bpm_score != bb_score){ diff[1][2]++; } if(bpm_score != bb_score2){ diff[1][3]++; } if(bb_score != bb_score2){ diff[2][3]++; } /* restore sequence b */ for(c = 0;c < len;c++){ b[c] = a[c]; } } } for(int i = 0; i < 4;i++){ for(int j = 0; j < 4;j++){ fprintf(stdout,"%3d ",diff[i][j]); } fprintf(stdout, "\n"); } fprintf(stdout, "\n"); len = strlen(seq); if(len > 255){ len = 255; } double dyn_timing = 0.0; double bpm_timing = 0.0; double block_timing = 0.0; double block2_timing = 0.0; int timing_iter = 10000; fprintf(stdout,"Dyn\tAVX\tDYN/AVX\n"); LOG_MSG("Timing DYN 256"); DECLARE_TIMER(t); for(i = 0; i < 1;i++){ RUN(mutate_seq(b,len,i,alphabet->L,rng)); START_TIMER(t); for(j = 0; j < timing_iter;j++){ dyn_score = dyn_256(a,b,len,len); } STOP_TIMER(t); /* GET_TIMING(t); */ dyn_timing += GET_USERTIME(t); #ifdef HAVE_AVX2 START_TIMER(t); for(j = 0; j < timing_iter;j++){ bpm_score = bpm_256(a,b,len,len); } STOP_TIMER(t); /* GET_TIMING(t); */ bpm_timing +=GET_USERTIME(t); #endif START_TIMER(t); for(j = 0; j < timing_iter;j++){ bpm_score = bpm_block(a,b,len,len); } STOP_TIMER(t); /* GET_TIMING(t); */ block2_timing +=GET_USERTIME(t); START_TIMER(t); for(j = 0; j < timing_iter;j++){ bpm_score = bpm_block2(a,b,len,len,len); } STOP_TIMER(t); /* GET_TIMING(t); */ block_timing +=GET_USERTIME(t); for(j = 0;j < len;j++){ b[j] = a[j]; } //dyn_timing = GET_TIMING(t); } LOG_MSG("%f - dyn ", dyn_timing); LOG_MSG("%f - bpm ", bpm_timing); LOG_MSG("%f - bpm_block2 ", block2_timing); LOG_MSG("%f - bpm_block ", block_timing); DESTROY_TIMER(t); MFREE(alphabet); MFREE(a); MFREE(b); MFREE(rng); return OK; ERROR: return FAIL; } int mutate_seq(uint8_t* s, int len,int k,int L, struct rng_state* rng) { int i,j; int r; for(i = 0; i < k;i++){ r = tl_random_int(rng,len); j = r; r = tl_random_int(rng,L); s[j] = r; } return OK; } uint8_t dyn_256_print(const uint8_t* t,const uint8_t* p,int n,int m) { uint8_t* prev = NULL; uint8_t* cur = NULL; uint8_t* tmp = NULL; int i,j,c; MMALLOC(prev, sizeof(uint8_t)* 257); MMALLOC(cur, sizeof(uint8_t)* 257); cur[0] = 0; fprintf(stdout,"%d ", cur[0]); for(j = 1; j <= m;j++){ cur[j] = cur[j-1] +1; fprintf(stdout,"%d ", cur[j]); } fprintf(stdout,"\n"); tmp = cur; cur = prev; prev = tmp; for(i = 1; i <= n;i++){ cur[0] = prev[0]; fprintf(stdout,"%d ", cur[0]); for(j = 1; j < m;j++){ c = 1; if(t[i-1] == p[j-1]){ c = 0; } cur[j] = prev[j-1] +c ; cur[j] = MACRO_MIN(cur[j], prev[j]+1); cur[j] = MACRO_MIN(cur[j], cur[j-1]+1); fprintf(stdout,"%d ", cur[j]); } j =m; c = 1; if(t[i-1] == p[j-1]){ c = 0; } cur[j] = prev[j-1] +c ; cur[j] = MACRO_MIN(cur[j], prev[j]); cur[j] = MACRO_MIN(cur[j], cur[j-1]+1); fprintf(stdout,"%d ", cur[j]); fprintf(stdout,"\n"); tmp = cur; cur = prev; prev = tmp; } c = prev[m]; MFREE(prev); MFREE(cur); return c; ERROR: return 255; } #ifdef HAVE_AVX2 void print_256(__m256i X) { alignas(32) uint64_t debug[4]; _mm256_store_si256( (__m256i*)& debug,X); fprintf(stdout,"%lu ", debug[0]); } void print_256_all(__m256i X) { alignas(32) uint64_t debug[4]; _mm256_store_si256( (__m256i*)& debug,X); int i; for(i = 0; i < 4;i++){ fprintf(stdout,"%lu ", debug[i]); } fprintf(stdout,"\n"); } #endif kalign-3.5.1/lib/src/consensus_msa.c000066400000000000000000000714411515023132300173500ustar00rootroot00000000000000#include "tldevel.h" #include #include "msa_struct.h" #include "msa_alloc.h" #include "poar.h" #define CONSENSUS_MSA_IMPORT #include "consensus_msa.h" /* Union-find data structure with per-set sequence membership tracking and element linked lists for efficient set enumeration. */ struct uf_set { int* parent; int* rank; int* elem_seq; /* element -> sequence index */ uint64_t** seq_mask; /* per-root: bitmask of sequences in set */ int* set_head; /* root -> first element in set (-1 if none) */ int* next_in_set; /* element -> next element in same set (-1 if last) */ int n_elements; int numseq; int mask_words; /* number of uint64_t words per bitmask */ }; static int uf_alloc(struct uf_set** uf, int n, int* seq_offsets, int* seq_lengths, int numseq) { struct uf_set* u = NULL; int mw = (numseq + 63) / 64; MMALLOC(u, sizeof(struct uf_set)); u->parent = NULL; u->rank = NULL; u->elem_seq = NULL; u->seq_mask = NULL; u->set_head = NULL; u->next_in_set = NULL; u->n_elements = n; u->numseq = numseq; u->mask_words = mw; MMALLOC(u->parent, sizeof(int) * n); MMALLOC(u->rank, sizeof(int) * n); MMALLOC(u->elem_seq, sizeof(int) * n); MMALLOC(u->set_head, sizeof(int) * n); MMALLOC(u->next_in_set, sizeof(int) * n); MMALLOC(u->seq_mask, sizeof(uint64_t*) * n); for(int i = 0; i < n; i++){ u->parent[i] = i; u->rank[i] = 0; u->set_head[i] = i; u->next_in_set[i] = -1; u->seq_mask[i] = NULL; MMALLOC(u->seq_mask[i], sizeof(uint64_t) * mw); memset(u->seq_mask[i], 0, sizeof(uint64_t) * mw); } /* Fill elem_seq and initialize per-element bitmasks */ for(int s = 0; s < numseq; s++){ for(int p = 0; p < seq_lengths[s]; p++){ int elem = seq_offsets[s] + p; u->elem_seq[elem] = s; u->seq_mask[elem][s / 64] |= (1ULL << (s % 64)); } } *uf = u; return OK; ERROR: return FAIL; } static void uf_free(struct uf_set* u) { if(u){ if(u->seq_mask){ for(int i = 0; i < u->n_elements; i++){ if(u->seq_mask[i]) MFREE(u->seq_mask[i]); } MFREE(u->seq_mask); } if(u->next_in_set) MFREE(u->next_in_set); if(u->set_head) MFREE(u->set_head); if(u->elem_seq) MFREE(u->elem_seq); if(u->parent) MFREE(u->parent); if(u->rank) MFREE(u->rank); MFREE(u); } } static int uf_find(struct uf_set* u, int x) { while(u->parent[x] != x){ u->parent[x] = u->parent[u->parent[x]]; /* path halving */ x = u->parent[x]; } return x; } /* Check if root 'target' is reachable from root 'start' via the implicit column ordering DAG. A directed edge col_A -> col_B exists when some sequence has consecutive residues in cols A and B. If target is reachable from start, merging them would create a cycle. */ static int dag_reachable(struct uf_set* u, int start, int target, int* seq_offsets, int* seq_lengths, int* visited, int visit_id) { /* BFS using the element linked lists for efficient enumeration */ /* We use a simple queue built from a scratch array */ int queue[4096]; /* bounded queue; for very large problems this needs malloc */ int head = 0, tail = 0; if(start == target) return 1; queue[tail++] = start; visited[start] = visit_id; while(head < tail){ int cur = queue[head++]; /* Enumerate outgoing edges: for each element in cur's set, check its successor (same seq, pos+1) */ int elem = u->set_head[cur]; while(elem >= 0){ int s = u->elem_seq[elem]; int pos = elem - seq_offsets[s]; if(pos + 1 < seq_lengths[s]){ int succ_elem = seq_offsets[s] + pos + 1; int succ_root = uf_find(u, succ_elem); if(succ_root == target) return 1; if(succ_root != cur && visited[succ_root] != visit_id){ visited[succ_root] = visit_id; if(tail < 4096){ queue[tail++] = succ_root; } } } elem = u->next_in_set[elem]; } } return 0; } /* Merge two sets. Returns 1 if merge succeeded, 0 if blocked by: - same-sequence conflict (two residues from same seq in one column) - ordering cycle (merging would create a cycle in the column DAG) When seq_offsets/seq_lengths are NULL, skip cycle check. */ static int uf_union_safe(struct uf_set* u, int a, int b, int* seq_offsets, int* seq_lengths, int* visited, int* visit_counter) { int ra = uf_find(u, a); int rb = uf_find(u, b); if(ra == rb) return 1; /* Check for conflict: do the two sets share any sequence? */ for(int w = 0; w < u->mask_words; w++){ if(u->seq_mask[ra][w] & u->seq_mask[rb][w]){ return 0; /* conflict: same sequence in both sets */ } } /* Check for ordering cycle: would merging create a cycle? A cycle exists if ra can reach rb (meaning ra < rb in the partial order, but merging makes them equal). */ if(seq_offsets != NULL){ (*visit_counter)++; if(dag_reachable(u, ra, rb, seq_offsets, seq_lengths, visited, *visit_counter)){ return 0; /* would create cycle */ } (*visit_counter)++; if(dag_reachable(u, rb, ra, seq_offsets, seq_lengths, visited, *visit_counter)){ return 0; /* would create cycle */ } } /* Safe to merge */ int new_root; int old_root; if(u->rank[ra] < u->rank[rb]){ u->parent[ra] = rb; new_root = rb; old_root = ra; }else if(u->rank[ra] > u->rank[rb]){ u->parent[rb] = ra; new_root = ra; old_root = rb; }else{ u->parent[rb] = ra; u->rank[ra]++; new_root = ra; old_root = rb; } /* Merge sequence bitmasks into new root */ for(int w = 0; w < u->mask_words; w++){ u->seq_mask[new_root][w] |= u->seq_mask[old_root][w]; } /* Concatenate element linked lists: append old_root's list to new_root's */ if(u->set_head[old_root] >= 0){ /* Find tail of new_root's list */ int tail = u->set_head[new_root]; if(tail < 0){ u->set_head[new_root] = u->set_head[old_root]; }else{ while(u->next_in_set[tail] >= 0){ tail = u->next_in_set[tail]; } u->next_in_set[tail] = u->set_head[old_root]; } } u->set_head[old_root] = -1; return 1; } static inline int popcount32(uint32_t x) { #ifdef __GNUC__ return __builtin_popcount(x); #else x = x - ((x >> 1) & 0x55555555u); x = (x & 0x33333333u) + ((x >> 2) & 0x33333333u); return (int)(((x + (x >> 4)) & 0x0F0F0F0Fu) * 0x01010101u >> 24); #endif } static inline int pair_index(int i, int j, int numseq) { return i * numseq - (i * (i + 1)) / 2 + (j - i - 1); } struct merge_candidate { int elem_i; int elem_j; int support; }; /* DFS-based topological sort that gracefully handles cycles by skipping back edges. This produces a valid ordering that respects as many sequence constraints as possible. */ static int topo_sort(int* col_id, /* [total_residues]: element -> column id */ int* seq_offsets, /* [numseq]: start offset for each seq */ int* seq_lengths, /* [numseq] */ int numseq, int n_cols, int** sorted_cols, /* output: sorted column indices */ int* n_sorted) { int* out = NULL; int** adj_list = NULL; int* adj_count = NULL; int* adj_alloc = NULL; int* state = NULL; /* 0=unvisited, 1=in-progress, 2=done */ int* dfs_stack = NULL; /* pairs of (node, edge_index) */ int i, s, pos; int out_idx; int sp; MMALLOC(adj_count, sizeof(int) * n_cols); MMALLOC(adj_alloc, sizeof(int) * n_cols); MMALLOC(adj_list, sizeof(int*) * n_cols); MMALLOC(out, sizeof(int) * n_cols); MMALLOC(state, sizeof(int) * n_cols); MMALLOC(dfs_stack, sizeof(int) * n_cols * 2); for(i = 0; i < n_cols; i++){ adj_count[i] = 0; adj_alloc[i] = 4; adj_list[i] = NULL; MMALLOC(adj_list[i], sizeof(int) * adj_alloc[i]); state[i] = 0; } /* Build adjacency list (deduplicated) */ for(s = 0; s < numseq; s++){ for(pos = 0; pos < seq_lengths[s] - 1; pos++){ int elem_a = seq_offsets[s] + pos; int elem_b = seq_offsets[s] + pos + 1; int ca = col_id[elem_a]; int cb = col_id[elem_b]; if(ca != cb){ int dup = 0; for(int k = 0; k < adj_count[ca]; k++){ if(adj_list[ca][k] == cb){ dup = 1; break; } } if(!dup){ if(adj_count[ca] >= adj_alloc[ca]){ adj_alloc[ca] *= 2; MREALLOC(adj_list[ca], sizeof(int) * adj_alloc[ca]); } adj_list[ca][adj_count[ca]++] = cb; } } } } /* DFS topological sort — back edges (cycles) are silently skipped */ out_idx = n_cols - 1; for(int start = 0; start < n_cols; start++){ if(state[start] != 0) continue; sp = 0; dfs_stack[sp++] = start; dfs_stack[sp++] = 0; state[start] = 1; while(sp > 0){ int edge_idx = dfs_stack[--sp]; int node = dfs_stack[--sp]; int pushed = 0; for(int e = edge_idx; e < adj_count[node]; e++){ int next = adj_list[node][e]; if(state[next] == 0){ /* Push current with updated edge index */ dfs_stack[sp++] = node; dfs_stack[sp++] = e + 1; /* Push next */ dfs_stack[sp++] = next; dfs_stack[sp++] = 0; state[next] = 1; pushed = 1; break; } /* state[next]==1: back edge (cycle) — skip */ /* state[next]==2: cross/forward edge — skip */ } if(!pushed){ state[node] = 2; out[out_idx--] = node; } } } for(i = 0; i < n_cols; i++){ if(adj_list[i]) MFREE(adj_list[i]); } MFREE(adj_list); MFREE(adj_count); MFREE(adj_alloc); MFREE(state); MFREE(dfs_stack); *sorted_cols = out; *n_sorted = n_cols; return OK; ERROR: if(adj_list){ for(i = 0; i < n_cols; i++){ if(adj_list[i]) MFREE(adj_list[i]); } MFREE(adj_list); } if(adj_count) MFREE(adj_count); if(adj_alloc) MFREE(adj_alloc); if(state) MFREE(state); if(dfs_stack) MFREE(dfs_stack); if(out) MFREE(out); return FAIL; } int build_consensus(struct poar_table* table, int* seq_lengths, int numseq, int min_support, struct msa* out_msa) { struct uf_set* uf = NULL; int* seq_offsets = NULL; int* col_id = NULL; /* element -> column */ int* root_to_col = NULL; /* uf root -> column index */ int* sorted_cols = NULL; int* visited = NULL; /* for cycle detection BFS */ int visit_counter = 0; int n_sorted = 0; int total_residues = 0; int n_cols = 0; int i, j, s, pos; char** out_seqs = NULL; ASSERT(table != NULL, "No POAR table"); ASSERT(out_msa != NULL, "No output MSA"); /* Compute offsets */ MMALLOC(seq_offsets, sizeof(int) * numseq); for(s = 0; s < numseq; s++){ seq_offsets[s] = total_residues; total_residues += seq_lengths[s]; } /* Initialize union-find with sequence membership tracking */ RUN(uf_alloc(&uf, total_residues, seq_offsets, seq_lengths, numseq)); /* Allocate visited array for cycle detection (use visit_counter to avoid clearing between checks) */ MMALLOC(visited, sizeof(int) * total_residues); memset(visited, 0, sizeof(int) * total_residues); /* Collect all POAR entries above min_support, then process in descending support order so higher-confidence pairs merge first */ { int n_candidates = 0; int alloc_candidates = 1024; struct merge_candidate *candidates = NULL; MMALLOC(candidates, sizeof(*candidates) * alloc_candidates); for(i = 0; i < numseq - 1; i++){ for(j = i + 1; j < numseq; j++){ int pidx = pair_index(i, j, numseq); struct poar_pair* pp = table->pairs[pidx]; for(int e = 0; e < pp->n_entries; e++){ int support = popcount32(pp->entries[e].support); if(support >= min_support){ if(n_candidates >= alloc_candidates){ alloc_candidates *= 2; MREALLOC(candidates, sizeof(*candidates) * alloc_candidates); } uint32_t key = pp->entries[e].key; candidates[n_candidates].elem_i = seq_offsets[i] + (int)(key >> 20); candidates[n_candidates].elem_j = seq_offsets[j] + (int)(key & 0xFFFFF); candidates[n_candidates].support = support; n_candidates++; } } } } /* Counting sort by descending support (values bounded 1..32) */ { int counts[33] = {0}; for(int a = 0; a < n_candidates; a++){ counts[candidates[a].support]++; } int offsets[33]; offsets[32] = 0; for(int v = 31; v >= 0; v--){ offsets[v] = offsets[v+1] + counts[v+1]; } int tmp_alloc = n_candidates > 0 ? n_candidates : 1; struct merge_candidate *sorted = NULL; MMALLOC(sorted, sizeof(*sorted) * tmp_alloc); for(int a = 0; a < n_candidates; a++){ int s_val = candidates[a].support; sorted[offsets[s_val]++] = candidates[a]; } memcpy(candidates, sorted, sizeof(*candidates) * n_candidates); MFREE(sorted); } /* Merge in priority order, skipping conflicts AND cycles */ for(int c = 0; c < n_candidates; c++){ uf_union_safe(uf, candidates[c].elem_i, candidates[c].elem_j, seq_offsets, seq_lengths, visited, &visit_counter); } MFREE(candidates); } MFREE(visited); /* Map UF roots to column IDs */ MMALLOC(root_to_col, sizeof(int) * total_residues); MMALLOC(col_id, sizeof(int) * total_residues); for(i = 0; i < total_residues; i++){ root_to_col[i] = -1; } n_cols = 0; for(i = 0; i < total_residues; i++){ int root = uf_find(uf, i); if(root_to_col[root] == -1){ root_to_col[root] = n_cols++; } col_id[i] = root_to_col[root]; } MFREE(root_to_col); /* Topological sort (DFS-based, cycle-safe) */ RUN(topo_sort(col_id, seq_offsets, seq_lengths, numseq, n_cols, &sorted_cols, &n_sorted)); /* Build column order lookup: sorted position -> column */ int* col_order = NULL; /* column -> position in sorted output */ MMALLOC(col_order, sizeof(int) * n_cols); for(i = 0; i < n_sorted; i++){ col_order[sorted_cols[i]] = i; } /* Build output alignment */ MMALLOC(out_seqs, sizeof(char*) * numseq); for(s = 0; s < numseq; s++){ out_seqs[s] = NULL; MMALLOC(out_seqs[s], sizeof(char) * (n_sorted + 1)); memset(out_seqs[s], '-', n_sorted); out_seqs[s][n_sorted] = '\0'; } /* Place residues */ for(s = 0; s < numseq; s++){ for(pos = 0; pos < seq_lengths[s]; pos++){ int elem = seq_offsets[s] + pos; int col = col_id[elem]; int sorted_pos = col_order[col]; /* Use original residue character from the MSA */ out_seqs[s][sorted_pos] = out_msa->sequences[s]->seq[pos]; } } /* Write back into MSA: replace seq pointers */ for(s = 0; s < numseq; s++){ MFREE(out_msa->sequences[s]->seq); out_msa->sequences[s]->seq = out_seqs[s]; out_msa->sequences[s]->len = n_sorted; out_seqs[s] = NULL; } out_msa->alnlen = n_sorted; out_msa->aligned = ALN_STATUS_FINAL; MFREE(out_seqs); MFREE(col_order); MFREE(sorted_cols); MFREE(col_id); MFREE(seq_offsets); uf_free(uf); return OK; ERROR: if(out_seqs){ for(s = 0; s < numseq; s++){ if(out_seqs[s]) MFREE(out_seqs[s]); } MFREE(out_seqs); } if(col_order) MFREE(col_order); if(sorted_cols) MFREE(sorted_cols); if(col_id) MFREE(col_id); if(root_to_col) MFREE(root_to_col); if(visited) MFREE(visited); if(seq_offsets) MFREE(seq_offsets); uf_free(uf); return FAIL; } /* Compute per-residue and per-column confidence from POAR table. For each residue at alignment position pos in sequence i: - Count residue-residue pairs with other sequences j that also have a residue (not gap) at the same column. - For each such pair, look up POAR support (popcount). - confidence = sum(supports) / (n_residue_pairs * n_alignments) Gaps get confidence 0.0. Column confidence = mean of residue confidences in that column. */ int compute_residue_confidence(struct poar_table* table, struct msa* aligned_msa) { struct pos_matrix* pm = NULL; int numseq = aligned_msa->numseq; int alnlen = aligned_msa->alnlen; int n_alignments = table->n_alignments; int i, j, col; char** seqs = NULL; ASSERT(table != NULL, "No POAR table"); ASSERT(aligned_msa != NULL, "No aligned MSA"); ASSERT(alnlen > 0, "Alignment length is 0"); /* Build position matrix from aligned MSA */ MMALLOC(seqs, sizeof(char*) * numseq); for(i = 0; i < numseq; i++){ seqs[i] = aligned_msa->sequences[i]->seq; } RUN(pos_matrix_from_msa(&pm, seqs, numseq, alnlen)); /* Allocate per-residue confidence arrays */ for(i = 0; i < numseq; i++){ if(aligned_msa->sequences[i]->confidence){ MFREE(aligned_msa->sequences[i]->confidence); } aligned_msa->sequences[i]->confidence = NULL; MMALLOC(aligned_msa->sequences[i]->confidence, sizeof(float) * alnlen); for(col = 0; col < alnlen; col++){ aligned_msa->sequences[i]->confidence[col] = 0.0f; } } /* Allocate per-column confidence */ if(aligned_msa->col_confidence){ MFREE(aligned_msa->col_confidence); } aligned_msa->col_confidence = NULL; MMALLOC(aligned_msa->col_confidence, sizeof(float) * alnlen); /* Compute per-residue confidence */ for(i = 0; i < numseq; i++){ for(col = 0; col < alnlen; col++){ int ri = pm->col_to_res[i][col]; if(ri < 0){ /* gap position */ aligned_msa->sequences[i]->confidence[col] = 0.0f; continue; } double sum_support = 0.0; int n_pairs = 0; for(j = 0; j < numseq; j++){ if(j == i) continue; int rj = pm->col_to_res[j][col]; if(rj < 0) continue; /* skip gaps */ /* Look up POAR support for this pair */ int si = i < j ? i : j; int sj = i < j ? j : i; int pidx = pair_index(si, sj, numseq); struct poar_pair* pp = table->pairs[pidx]; int orig_i = i < j ? ri : rj; int orig_j = i < j ? rj : ri; uint32_t key = ((uint32_t)orig_i << 20) | (uint32_t)orig_j; /* Binary search */ int lo = 0; int hi = pp->n_entries; int support = 0; while(lo < hi){ int mid = lo + (hi - lo) / 2; if(pp->entries[mid].key < key){ lo = mid + 1; }else if(pp->entries[mid].key == key){ support = popcount32(pp->entries[mid].support); break; }else{ hi = mid; } } sum_support += (double)support; n_pairs++; } if(n_pairs > 0 && n_alignments > 0){ aligned_msa->sequences[i]->confidence[col] = (float)(sum_support / ((double)n_pairs * (double)n_alignments)); }else{ aligned_msa->sequences[i]->confidence[col] = 0.0f; } } } /* Compute per-column confidence: mean over non-gap residues */ for(col = 0; col < alnlen; col++){ double sum = 0.0; int count = 0; for(i = 0; i < numseq; i++){ if(pm->col_to_res[i][col] >= 0){ sum += aligned_msa->sequences[i]->confidence[col]; count++; } } if(count > 0){ aligned_msa->col_confidence[col] = (float)(sum / count); }else{ aligned_msa->col_confidence[col] = 0.0f; } } pos_matrix_free(pm); MFREE(seqs); return OK; ERROR: if(pm) pos_matrix_free(pm); if(seqs) MFREE(seqs); return FAIL; } /* Score one alignment against the POAR table. Returns expected number of correct pairs: for each aligned pair, (support - 1) / (m - 1) gives the fraction of OTHER alignments agreeing. Summing these gives expected correct pairs. This rewards both high recall (many pairs) and high precision (pairs with broad agreement). */ int score_alignment_poar(struct poar_table* table, struct pos_matrix* pm, int numseq, int n_alignments, double* out_score) { double total_score = 0.0; int i, j, col; int alnlen = pm->alnlen; double denom = (n_alignments > 1) ? (double)(n_alignments - 1) : 1.0; for(i = 0; i < numseq - 1; i++){ for(j = i + 1; j < numseq; j++){ int pidx = pair_index(i, j, numseq); struct poar_pair* pp = table->pairs[pidx]; for(col = 0; col < alnlen; col++){ int ri = pm->col_to_res[i][col]; int rj = pm->col_to_res[j][col]; if(ri >= 0 && rj >= 0){ uint32_t key = ((uint32_t)ri << 20) | (uint32_t)rj; /* Binary search in sorted entries */ int lo = 0; int hi = pp->n_entries; int support = 0; while(lo < hi){ int mid = lo + (hi - lo) / 2; if(pp->entries[mid].key < key){ lo = mid + 1; }else if(pp->entries[mid].key == key){ support = popcount32(pp->entries[mid].support); break; }else{ hi = mid; } } /* support includes self; subtract 1 for other-agreement */ total_score += (double)(support - 1) / denom; } } } } *out_score = total_score; return OK; } kalign-3.5.1/lib/src/consensus_msa.h000066400000000000000000000015601515023132300173500ustar00rootroot00000000000000#ifndef CONSENSUS_MSA_H #define CONSENSUS_MSA_H #ifdef CONSENSUS_MSA_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct poar_table; struct pos_matrix; struct msa; EXTERN int build_consensus(struct poar_table* table, int* seq_lengths, int numseq, int min_support, struct msa* out_msa); EXTERN int score_alignment_poar(struct poar_table* table, struct pos_matrix* pm, int numseq, int n_alignments, double* out_score); EXTERN int compute_residue_confidence(struct poar_table* table, struct msa* aligned_msa); #undef CONSENSUS_MSA_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/core.h000066400000000000000000000012321515023132300154140ustar00rootroot00000000000000#ifndef CORE_H #define CORE_H #ifdef CORE_IMPORT #define EXTERN #else #define EXTERN extern #endif /* idea: use a shared struct holding dm & msa & empty root Process: put root on task list if (node is ready to align - do so ) }else{ while # tries < x take task, run split , update L / R arrays if better than previous split then add left and right node contine. } if(nseq at node == 1){ send possible align signal up; if(align counter ==2 ){ add parent node to queue } } if (number of left == 1 && number of right ==1){ send possible align to } */ #undef CORE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/coretralign.c000066400000000000000000000126421515023132300167770ustar00rootroot00000000000000#include "tldevel.h" #define CORETRALIGN_IMPORT #include "coretralign.h" int aln_scheduler_get_tid(aln_scheduler *s) { int i; int64_t tid = (int64_t) pthread_self(); int ID = -1; aln_scheduler_lock(s); for(i = 0; i < s->thread_id_idx;i++){ if(s->thread_id_map[i] == tid){ ID = i; break; } } if(ID == -1){ ID = s->thread_id_idx; s->thread_id_map[s->thread_id_idx] = tid; s->thread_id_idx++; } aln_scheduler_unlock(s); return ID; } int aln_scheduler_lock(aln_scheduler* s) { ASSERT(s != NULL, "No thread controll"); /* pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); */ if(pthread_mutex_lock(&s->lock) != 0){ ERROR_MSG("Can't get lock"); } return OK; ERROR: return FAIL; } int aln_scheduler_trylock(aln_scheduler* s) { return pthread_mutex_trylock(&s->lock); } int aln_scheduler_unlock(aln_scheduler* s) { ASSERT(s != NULL, "No thread controll"); if(pthread_mutex_unlock(&s->lock) != 0){ ERROR_MSG("Can't get lock"); } /* pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); */ /* pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); */ /* pthread_testcancel(); */ return OK; ERROR: return FAIL; } int aln_scheduler_alloc(aln_scheduler **scheduler, int n_threads) { aln_scheduler* n = NULL; MMALLOC(n, sizeof(aln_scheduler)); n->task_alloc = NULL; n->task_queue = NULL; n->root = NULL; n->threads = NULL; n->thread_id_map = NULL; n->thread_id_idx = 0; pthread_mutex_init(&n->lock, NULL); n->n_threads = n_threads; MMALLOC(n->threads, sizeof(pthread_t) * n->n_threads); MMALLOC(n->thread_id_map, sizeof(int64_t) * n->n_threads); queue_alloc(&n->task_alloc, 1); queue_alloc(&n->task_queue, 0); *scheduler = n; return OK; ERROR: return FAIL; } void aln_scheduler_free(aln_scheduler *n) { if(n){ if(n->task_alloc){ queue_free(n->task_alloc); } if(n->task_queue){ queue_free(n->task_queue); } if(n->threads){ MFREE(n->threads); } if(n->thread_id_map){ MFREE(n->thread_id_map); } MFREE(n); } } static int aln_elem_alloc(aln_elem **node); static int aln_elem_free(aln_elem *n); int queue_push(aln_elem_queue *q, aln_elem *n) { if(q->head == NULL){ q->head = n; q->tail = q->head; q->head->p = NULL; }else{ n->p = q->head; q->head = n; } q->n++; return OK; } int queue_pop(aln_elem_queue *q, aln_elem **node) { aln_elem * tmp = NULL; if(q->head == NULL){ if(q->is_allocator){ /* RUN(eq_add_mem(q)); */ }else{ *node = NULL; return FAIL; } } *node = q->head; tmp = q->head; q->head = tmp->p; if(q->head == NULL){ q->tail = NULL; } q->n--; return OK; } int queue_append(aln_elem_queue *q, aln_elem* n) { if(q->head == NULL){ q->head = n; q->tail = q->head; }else{ q->tail->p = n; q->tail = q->tail->p; } q->tail->p = NULL; q->n++; return OK; } int queue_add_mem(aln_elem_queue *q) { ASSERT(q->alloc_block_size != 0,"This queue is not an allocator!"); for(int i = 0; i < q->alloc_block_size;i++){ /* Alloc e_node */ aln_elem* n = NULL; aln_elem_alloc(&n); /* Add to queue */ queue_append(q, n); } return OK; ERROR: return FAIL; } int queue_alloc(aln_elem_queue **queue, uint8_t is_allocator) { aln_elem_queue* l = NULL; MMALLOC(l, sizeof(aln_elem_queue)); l->head = NULL; l->tail = 0; l->n = 0; l->alloc_block_size = 0; l->is_allocator = is_allocator; if(is_allocator){ l->alloc_block_size = 4096; RUN(queue_add_mem(l)); /* RUN(eq_add_mem(l)); */ } *queue = l; return OK; ERROR: queue_free(l); return FAIL; } void queue_free(aln_elem_queue *l) { if(l){ aln_elem* n = NULL; while(l->n){ n = NULL; queue_pop(l, &n); aln_elem_free(n); } MFREE(l); } } int aln_elem_alloc(aln_elem **node) { aln_elem* n = NULL; MMALLOC(n, sizeof(aln_elem)); n->l = NULL; n->r = NULL; n->p = NULL; n->wait = 0; n->type = AE_TYPE_UNDEF; n->aln_mem = NULL; *node = n; return OK; ERROR: aln_elem_free(n); return FAIL; } int aln_elem_free(aln_elem *n) { if(n){ MFREE(n); } } kalign-3.5.1/lib/src/coretralign.h000066400000000000000000000033131515023132300167770ustar00rootroot00000000000000#ifndef CORETRALIGN_H #define CORETRALIGN_H #include #include #include "aln_mem.h" #ifdef CORETRALIGN_IMPORT #define EXTERN #else #define EXTERN extern #endif /* Idea: construct hirschberg alignment via trees -> only because this would make threading easier to control */ typedef enum { AE_TYPE_UNDEF, AE_TYPE_ALN_BACK, AE_TYPE_ALN_FORWARD, AE_TYPE_SPLIT /* tree stuff here */ }aln_elem_type; typedef struct aln_elem aln_elem; typedef struct aln_elem { aln_elem* l; /* left OR backward in dyn*/ aln_elem* r; /* right OR backward in dyn*/ aln_elem* p; /* parent */ uint8_t wait; aln_elem_type type; struct aln_mem* aln_mem; } aln_elem; typedef struct aln_elem_queue { aln_elem* head; aln_elem* tail; int alloc_block_size; int n; uint8_t is_allocator; } aln_elem_queue; typedef struct aln_scheduler { aln_elem_queue* task_alloc; aln_elem_queue* task_queue; aln_elem* root; pthread_t* threads; int64_t* thread_id_map; int thread_id_idx; pthread_mutex_t lock; int n_threads; double** dm; struct msa* msa; } aln_scheduler; EXTERN int aln_scheduler_lock(aln_scheduler* s); EXTERN int aln_scheduler_trylock(aln_scheduler* s); EXTERN int aln_scheduler_unlock(aln_scheduler* s); EXTERN int aln_scheduler_alloc(aln_scheduler **scheduler, int n_threads); EXTERN void aln_scheduler_free(aln_scheduler *n); EXTERN int queue_alloc(aln_elem_queue **queue, uint8_t is_allocator); EXTERN void queue_free(aln_elem_queue *l); #undef CORETRALIGN_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/ensemble.c000066400000000000000000000505521515023132300162620ustar00rootroot00000000000000#include "tldevel.h" #include "esl_stopwatch.h" #include "msa_struct.h" #include "msa_op.h" #include "msa_alloc.h" #include "msa_sort.h" #include "msa_check.h" #include "aln_param.h" #include "aln_wrap.h" #include "poar.h" #include "consensus_msa.h" #include "kalign/kalign.h" #define ENSEMBLE_IMPORT #include "ensemble.h" /* Each ensemble run varies gap-open and gap-extend independently, with optional iterative refinement on select runs. This explores "few long gaps" vs "many short gaps" rather than just uniformly scaling all penalties together. Entry 0 is unused (run 0 always uses defaults). */ struct ensemble_params { float gpo_scale; /* gap open multiplier */ float gpe_scale; /* gap extend multiplier */ float tgpe_scale; /* terminal gap extend multiplier */ float noise; /* tree noise sigma */ }; static struct ensemble_params run_params[] = { {1.0f, 1.0f, 1.0f, 0.0f}, /* 0: default (unused, handled specially) */ {0.5f, 1.5f, 0.8f, 0.20f}, /* 1: fewer gap-opens, longer extensions */ {1.5f, 0.5f, 1.2f, 0.20f}, /* 2: more gap-opens, shorter extensions */ {0.7f, 0.7f, 0.5f, 0.25f}, /* 3: globally relaxed gaps */ {1.4f, 1.4f, 1.5f, 0.25f}, /* 4: globally strict gaps */ {0.8f, 1.2f, 1.0f, 0.30f}, /* 5: slight open-relax, extend-strict */ {1.3f, 0.8f, 0.7f, 0.30f}, /* 6: strict open, relaxed extend+terminal */ {0.6f, 1.0f, 1.3f, 0.15f}, /* 7: relaxed open only */ {1.0f, 0.6f, 0.6f, 0.15f}, /* 8: relaxed extend only */ {1.8f, 1.0f, 1.0f, 0.35f}, /* 9: very strict open, big tree perturbation */ {1.0f, 1.8f, 1.8f, 0.35f}, /* 10: very strict extend */ {0.4f, 0.4f, 0.3f, 0.20f}, /* 11: very relaxed all */ }; #define N_RUN_PARAMS 12 /* --------------------------------------------------------------------------- * Helper: resolve_run_params * * Given base gap penalties and a run index, compute the run-specific * gap-open, gap-extend, terminal-gap-extend, seed, and noise values. * Run 0 always uses defaults (deterministic, no noise). * ------------------------------------------------------------------------- */ static void resolve_run_params(float base_gpo, float base_gpe, float base_tgpe, int k, uint64_t seed, float* out_gpo, float* out_gpe, float* out_tgpe, uint64_t* out_seed, float* out_noise) { if(k == 0){ /* Run 0: default params, deterministic */ *out_gpo = base_gpo; *out_gpe = base_gpe; *out_tgpe = base_tgpe; *out_seed = 0; *out_noise = 0.0f; }else{ /* Subsequent runs: independent per-penalty scaling + tree noise */ struct ensemble_params ep = run_params[k % N_RUN_PARAMS]; *out_gpo = base_gpo * ep.gpo_scale; *out_gpe = base_gpe * ep.gpe_scale; *out_tgpe = base_tgpe * ep.tgpe_scale; *out_seed = seed + (uint64_t)k; *out_noise = ep.noise; } } /* --------------------------------------------------------------------------- * Helper: score_alignments * * Score all N alignments against the POAR table. Returns the scores * array (caller must MFREE) and the index of the best run (best_k). * Run 0 is preferred unless another run exceeds its score by >5%. * ------------------------------------------------------------------------- */ static int score_alignments(struct msa** alignments, struct poar_table* poar, int numseq, int n_runs, int quiet, double** out_scores, int* out_best_k) { struct pos_matrix* pm = NULL; double* scores = NULL; int k; MMALLOC(scores, sizeof(double) * n_runs); for(k = 0; k < n_runs; k++){ char** aln_seqs = NULL; MMALLOC(aln_seqs, sizeof(char*) * numseq); for(int i = 0; i < numseq; i++){ aln_seqs[i] = alignments[k]->sequences[i]->seq; } RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, alignments[k]->alnlen)); RUN(score_alignment_poar(poar, pm, numseq, n_runs, &scores[k])); if(!quiet){ LOG_MSG(" Run %d score: %.1f", k + 1, scores[k]); } pos_matrix_free(pm); pm = NULL; MFREE(aln_seqs); } /* Select: prefer run 0 (default params) unless another run has a meaningfully higher score (>5% improvement). */ int best_k = 0; double baseline = scores[0]; for(k = 1; k < n_runs; k++){ if(scores[k] > scores[best_k] && scores[k] > baseline * 1.05){ best_k = k; } } *out_scores = scores; *out_best_k = best_k; return OK; ERROR: if(pm) pos_matrix_free(pm); if(scores) MFREE(scores); return FAIL; } /* --------------------------------------------------------------------------- * Helper: build_consensus_from_poar * * Build a consensus MSA from a POAR table at the given min_support * threshold. Allocates and returns consensus_msa via *out_consensus. * The source MSA is used as the template (deep-copied internally). * ------------------------------------------------------------------------- */ static int build_consensus_from_poar(struct poar_table* poar, struct msa* msa, int numseq, int min_support, struct msa** out_consensus) { struct msa* consensus_msa = NULL; int* seq_lens = NULL; RUN(msa_cpy(&consensus_msa, msa)); MMALLOC(seq_lens, sizeof(int) * numseq); for(int i = 0; i < numseq; i++){ seq_lens[i] = msa->sequences[i]->len; } RUN(build_consensus(poar, seq_lens, numseq, min_support, consensus_msa)); MFREE(seq_lens); *out_consensus = consensus_msa; return OK; ERROR: if(seq_lens) MFREE(seq_lens); if(consensus_msa) kalign_free_msa(consensus_msa); return FAIL; } /* --------------------------------------------------------------------------- * Helper: copy_alignment_to_msa * * Move alignment sequences from src into dst, transferring ownership. * After this call, src's seq pointers are replaced with empty strings * so it can be safely freed. * ------------------------------------------------------------------------- */ static int copy_alignment_to_msa(struct msa* dst, struct msa* src, int numseq) { for(int i = 0; i < numseq; i++){ MFREE(dst->sequences[i]->seq); dst->sequences[i]->seq = src->sequences[i]->seq; dst->sequences[i]->len = src->sequences[i]->len; src->sequences[i]->seq = NULL; MMALLOC(src->sequences[i]->seq, 1); src->sequences[i]->seq[0] = '\0'; } dst->alnlen = src->alnlen; dst->aligned = src->aligned; return OK; ERROR: return FAIL; } /* --------------------------------------------------------------------------- * Helper: score_single_msa * * Score a single MSA against a POAR table. Returns the score via * out_score. This avoids repeating the aln_seqs + pos_matrix pattern. * ------------------------------------------------------------------------- */ static int score_single_msa(struct msa* aln, struct poar_table* poar, int numseq, int n_runs, double* out_score) { struct pos_matrix* pm = NULL; char** aln_seqs = NULL; MMALLOC(aln_seqs, sizeof(char*) * numseq); for(int i = 0; i < numseq; i++){ aln_seqs[i] = aln->sequences[i]->seq; } RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, aln->alnlen)); RUN(score_alignment_poar(poar, pm, numseq, n_runs, out_score)); pos_matrix_free(pm); MFREE(aln_seqs); return OK; ERROR: if(pm) pos_matrix_free(pm); if(aln_seqs) MFREE(aln_seqs); return FAIL; } /* ======================================================================== */ int kalign_ensemble(struct msa* msa, int n_threads, int type, int n_runs, float gpo, float gpe, float tgpe, uint64_t seed, int min_support, const char* save_poar_path, int refine, float dist_scale, float vsm_amax, int realign, float use_seq_weights, int consistency_anchors, float consistency_weight) { struct msa* copy = NULL; struct msa* consensus_msa = NULL; struct msa** alignments = NULL; struct poar_table* poar = NULL; struct pos_matrix* pm = NULL; struct aln_param* ap = NULL; double* scores = NULL; int numseq; int k; int best_k = 0; int use_consensus = 0; float base_gpo, base_gpe, base_tgpe; ASSERT(msa != NULL, "No MSA"); ASSERT(n_runs >= 1, "n_runs must be >= 1"); /* Seq_weights hurts ensemble performance (POAR consensus already handles profile imbalance). Default to OFF in ensemble mode. */ if(use_seq_weights < 0.0f){ use_seq_weights = 0.0f; } /* Essential input check + detect alphabet */ RUN(kalign_essential_input_check(msa, 0)); numseq = msa->numseq; DECLARE_TIMER(t_ensemble); if(!msa->quiet){ LOG_MSG("Ensemble alignment with %d runs", n_runs); } START_TIMER(t_ensemble); /* Resolve default gap penalties using aln_param_init. We need to detect biotype first. */ if(msa->biotype == ALN_BIOTYPE_UNDEF){ RUN(detect_alphabet(msa)); } /* Use aln_param_init to resolve defaults */ RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); base_gpo = ap->gpo; base_gpe = ap->gpe; base_tgpe = ap->tgpe; aln_param_free(ap); ap = NULL; /* Allocate POAR table and array to store completed alignments */ RUN(poar_table_alloc(&poar, numseq)); MMALLOC(alignments, sizeof(struct msa*) * n_runs); for(k = 0; k < n_runs; k++){ alignments[k] = NULL; } /* Run N alignments, extract POARs, and keep each alignment */ for(k = 0; k < n_runs; k++){ float run_gpo, run_gpe, run_tgpe, run_noise; uint64_t run_seed; resolve_run_params(base_gpo, base_gpe, base_tgpe, k, seed, &run_gpo, &run_gpe, &run_tgpe, &run_seed, &run_noise); /* Deep-copy MSA */ copy = NULL; RUN(msa_cpy(©, msa)); copy->quiet = 1; if(!msa->quiet){ LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", k + 1, n_runs, run_gpo, run_gpe, run_tgpe, run_noise); } /* Run alignment */ if(realign > 0){ RUN(kalign_run_realign(copy, n_threads, type, run_gpo, run_gpe, run_tgpe, refine, 0, dist_scale, vsm_amax, realign, use_seq_weights, consistency_anchors, consistency_weight)); }else{ RUN(kalign_run_seeded(copy, n_threads, type, run_gpo, run_gpe, run_tgpe, refine, 0, run_seed, run_noise, dist_scale, vsm_amax, use_seq_weights, consistency_anchors, consistency_weight)); } /* Extract POARs from the finalized alignment */ char** aln_seqs = NULL; MMALLOC(aln_seqs, sizeof(char*) * numseq); for(int i = 0; i < numseq; i++){ aln_seqs[i] = copy->sequences[i]->seq; } RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); RUN(extract_poars(poar, pm, k)); pos_matrix_free(pm); pm = NULL; MFREE(aln_seqs); /* Keep this alignment for scoring later */ alignments[k] = copy; copy = NULL; } /* Score all alignments and select the best */ RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, &scores, &best_k)); if(!msa->quiet){ LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); } /* Save POAR table if requested */ if(save_poar_path != NULL){ RUN(poar_table_write(poar, save_poar_path)); if(!msa->quiet){ LOG_MSG(" Saved POAR table to %s", save_poar_path); } } /* When min_support > 0: explicit consensus threshold, skip selection. When min_support == 0: auto behavior (selection vs consensus). */ if(min_support > 0){ /* Explicit consensus: force consensus path */ RUN(build_consensus_from_poar(poar, msa, numseq, min_support, &consensus_msa)); use_consensus = 1; if(!msa->quiet){ LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); } }else{ /* Try consensus approach: build a new alignment from POAR table. This can combine correct pairs from multiple runs, potentially outperforming any single run. */ double consensus_score = 0.0; int min_sup = (n_runs + 2) / 3; if(min_sup < 2) min_sup = 2; RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, &consensus_msa)); /* Score consensus against POAR table */ RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, &consensus_score)); if(!msa->quiet){ LOG_MSG(" Consensus score: %.1f (selection: %.1f)", consensus_score, scores[best_k]); } if(consensus_score > scores[best_k]){ use_consensus = 1; if(!msa->quiet){ LOG_MSG(" Using consensus alignment"); } }else{ kalign_free_msa(consensus_msa); consensus_msa = NULL; if(!msa->quiet){ LOG_MSG(" Keeping selection winner"); } } } /* Post-selection refinement: only when using selection (not consensus), re-run the winner with REFINE_CONFIDENT and keep if it scores higher. */ if(!use_consensus){ float ref_gpo, ref_gpe, ref_tgpe, ref_noise; uint64_t ref_seed; resolve_run_params(base_gpo, base_gpe, base_tgpe, best_k, seed, &ref_gpo, &ref_gpe, &ref_tgpe, &ref_seed, &ref_noise); copy = NULL; RUN(msa_cpy(©, msa)); copy->quiet = 1; if(!msa->quiet){ LOG_MSG(" Refining run %d...", best_k + 1); } RUN(kalign_run_seeded(copy, n_threads, type, ref_gpo, ref_gpe, ref_tgpe, KALIGN_REFINE_CONFIDENT, 0, ref_seed, ref_noise, dist_scale, vsm_amax, use_seq_weights, consistency_anchors, consistency_weight)); /* Score the refined alignment against the same POAR table */ double refined_score = 0.0; RUN(score_single_msa(copy, poar, numseq, n_runs, &refined_score)); if(!msa->quiet){ LOG_MSG(" Refined score: %.1f (was %.1f)", refined_score, scores[best_k]); } if(refined_score > scores[best_k]){ kalign_free_msa(alignments[best_k]); alignments[best_k] = copy; copy = NULL; if(!msa->quiet){ LOG_MSG(" Using refined alignment"); } }else{ kalign_free_msa(copy); copy = NULL; if(!msa->quiet){ LOG_MSG(" Keeping original alignment"); } } } MFREE(scores); scores = NULL; /* Copy the winning alignment back into the original MSA */ if(use_consensus){ RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); kalign_free_msa(consensus_msa); consensus_msa = NULL; }else{ RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); } /* Compute per-residue and per-column confidence from POAR table */ RUN(compute_residue_confidence(poar, msa)); /* Sort back to original rank order */ RUN(msa_sort_rank(msa)); STOP_TIMER(t_ensemble); if(!msa->quiet){ GET_TIMING(t_ensemble); } DESTROY_TIMER(t_ensemble); /* Free all alignments */ for(k = 0; k < n_runs; k++){ if(alignments[k]) kalign_free_msa(alignments[k]); } MFREE(alignments); poar_table_free(poar); return OK; ERROR: if(copy) kalign_free_msa(copy); if(consensus_msa) kalign_free_msa(consensus_msa); if(pm) pos_matrix_free(pm); if(alignments){ for(k = 0; k < n_runs; k++){ if(alignments[k]) kalign_free_msa(alignments[k]); } MFREE(alignments); } poar_table_free(poar); if(scores) MFREE(scores); if(ap) aln_param_free(ap); return FAIL; } int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support) { struct msa* consensus_msa = NULL; struct poar_table* poar = NULL; int numseq; ASSERT(msa != NULL, "No MSA"); ASSERT(poar_path != NULL, "No POAR file path"); ASSERT(min_support >= 1, "min_support must be >= 1"); RUN(kalign_essential_input_check(msa, 0)); numseq = msa->numseq; /* Read POAR table from file */ RUN(poar_table_read(&poar, poar_path)); if(poar->numseq != numseq){ ERROR_MSG("POAR file has %d sequences, input has %d", poar->numseq, numseq); } /* Build consensus at given min_support threshold */ RUN(build_consensus_from_poar(poar, msa, numseq, min_support, &consensus_msa)); /* Copy consensus alignment back into original MSA */ RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); kalign_free_msa(consensus_msa); consensus_msa = NULL; /* Compute per-residue and per-column confidence */ RUN(compute_residue_confidence(poar, msa)); RUN(msa_sort_rank(msa)); poar_table_free(poar); return OK; ERROR: if(consensus_msa) kalign_free_msa(consensus_msa); if(poar) poar_table_free(poar); return FAIL; } kalign-3.5.1/lib/src/ensemble.h000066400000000000000000000015771515023132300162720ustar00rootroot00000000000000#ifndef ENSEMBLE_H #define ENSEMBLE_H #include #ifdef ENSEMBLE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, int n_runs, float gpo, float gpe, float tgpe, uint64_t seed, int min_support, const char* save_poar_path, int refine, float dist_scale, float vsm_amax, int realign, float use_seq_weights, int consistency_anchors, float consistency_weight); EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); #undef ENSEMBLE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/esl_stopwatch.c000066400000000000000000000350501515023132300173430ustar00rootroot00000000000000/* Tracking cpu/system/elapsed time used by a process. * * Credits: * - Thanks to Warren Gish for assistance. * - Includes portable high-resolution timer code * (C) 2012 David Robert Nadeau, http://NadeauSoftware.com/ * Creative Commons Attribution 3.0 Unported License * http://creativecommons.org/licenses/by/3.0/deed.en_US */ //#include "esl_config.h" //#include "easel.h" #include "tldevel.h" #define ESL_STOPWATCH_IMPORT #include "esl_stopwatch.h" #define TRUE 1 double stopwatch_getRealTime(void); /***************************************************************** * ESL_STOPWATCH object maintenance *****************************************************************/ /* Function: esl_stopwatch_Create() * * Purpose: Creates a new stopwatch. * * Returns: ptr to a new object; caller is * responsible for free'ing it with * . * * Throws: NULL on allocation failure. */ ESL_STOPWATCH * esl_stopwatch_Create(void) { ESL_STOPWATCH *w = NULL; MMALLOC(w, sizeof(ESL_STOPWATCH)); //ESL_ALLOC(w, sizeof(ESL_STOPWATCH)); w->elapsed = 0.; w->user = 0.; w->sys = 0.; return w; ERROR: esl_stopwatch_Destroy(w); return NULL; } /* Function: esl_stopwatch_Destroy() * * Purpose: Frees an . */ void esl_stopwatch_Destroy(ESL_STOPWATCH *w) { if (w){ free(w); } } /* Function: esl_stopwatch_Start() * * Purpose: Start a stopwatch. This sets the base * for elapsed, cpu, and system time difference * calculations by subsequent calls to * . * * Returns: on success. */ int esl_stopwatch_Start(ESL_STOPWATCH *w) { #if defined HAVE_TIMES && defined eslSTOPWATCH_HIGHRES /* System-dependent highest resolution */ times(&(w->cpu0)); w->t0 = stopwatch_getRealTime(); #elif HAVE_TIMES /* ... else fall back to POSIX... */ w->t0 = times(&(w->cpu0)); #else /* ... else fallback to ANSI C */ w->t0 = time(NULL); w->cpu0 = clock(); #endif w->elapsed = 0.; w->user = 0.; w->sys = 0.; return OK; } /* Function: esl_stopwatch_Stop() * * Purpose: Stop a stopwatch. Record and store elapsed, * cpu, and system time difference relative to the * last call to . * * Returns: on success. */ int esl_stopwatch_Stop(ESL_STOPWATCH *w) { #if defined eslSTOPWATCH_HIGHRES && defined HAVE_TIMES double t1; struct tms cpu1; double clk_tck; #elif defined HAVE_TIMES clock_t t1; struct tms cpu1; double clk_tck; #else time_t t1; clock_t cpu1; #endif #if defined eslSTOPWATCH_HIGHRES && defined HAVE_TIMES t1 = stopwatch_getRealTime(); w->elapsed = t1 - w->t0; clk_tck = (double) sysconf(_SC_CLK_TCK); times(&cpu1); w->user = (double) (cpu1.tms_utime + cpu1.tms_cutime - w->cpu0.tms_utime - w->cpu0.tms_cutime) / clk_tck; w->sys = (double) (cpu1.tms_stime + cpu1.tms_cstime - w->cpu0.tms_stime - w->cpu0.tms_cstime) / clk_tck; #elif defined HAVE_TIMES /* POSIX */ t1 = times(&cpu1); clk_tck = (double) sysconf(_SC_CLK_TCK); w->elapsed = (double) (t1 - w->t0) / clk_tck; w->user = (double) (cpu1.tms_utime + cpu1.tms_cutime - w->cpu0.tms_utime - w->cpu0.tms_cutime) / clk_tck; w->sys = (double) (cpu1.tms_stime + cpu1.tms_cstime - w->cpu0.tms_stime - w->cpu0.tms_cstime) / clk_tck; #else /* fallback to ANSI C */ t1 = time(NULL); cpu1 = clock(); w->elapsed = difftime(t1, w->t0); w->user = (double) (cpu1- w->cpu0) / (double) CLOCKS_PER_SEC; w->sys = 0.; /* no way to portably get system time in ANSI C */ #endif return OK; } /* format_time_string() * Date: SRE, Fri Nov 26 15:06:28 1999 [St. Louis] * * Purpose: Given a number of seconds, format into * hh:mm:ss.xx in a provided buffer. * * Args: buf - allocated space (128 is plenty!) * sec - number of seconds * do_frac - TRUE (1) to include hundredths of a sec */ static void format_time_string(char *buf, double sec, int do_frac) { int h, m, s, hs; h = (int) (sec / 3600.); m = (int) (sec / 60.) - h * 60; s = (int) (sec) - h * 3600 - m * 60; if (do_frac) { hs = (int) (sec * 100.) - h * 360000 - m * 6000 - s * 100; sprintf(buf, "%02d:%02d:%02d.%02d", h,m,s,hs); } else { sprintf(buf, "%02d:%02d:%02d", h,m,s); } } /* Function: esl_stopwatch_Display() * * Purpose: Output a usage summary line from a stopped * stopwatch, showing elapsed, cpu, and system time * between the last calls to * and . * * The string will be prepended to the output * line. Use <""> to prepend nothing. If is NULL, * a default <"CPU Time: "> prefix is used. * * For = <"CPU Time: "> an example output line is:\\ * * * Args: fp - output stream * w - stopped stopwatch * prefix - output line prefix ("" for nothing) * * Returns: on success. * * Throws: on any system write error, such as filled disk. */ int esl_stopwatch_Display(FILE *fp, ESL_STOPWATCH *w, char *prefix) { char buf[128]; /* (safely holds up to 10^14 years; I'll be dead by then) */ if (prefix == NULL) { if (fputs("CPU Time: ", fp) < 0){ ERROR_MSG( "stopwatch display write failed"); } }else{ if (fputs(prefix, fp) < 0){ ERROR_MSG( "stopwatch display write failed"); } } format_time_string(buf, w->user+w->sys, TRUE); #ifdef HAVE_TIMES if (fprintf(fp, "%.2fu %.2fs %s ", w->user, w->sys, buf) < 0){ ERROR_MSG( "stopwatch display write failed"); } #else if (fprintf(fp, "%.2fu %s ", w->user, buf) < 0){ ERROR_MSG( "stopwatch display write failed"); } #endif format_time_string(buf, w->elapsed, TRUE); if (fprintf(fp, "Elapsed: %s\n", buf) < 0){ ERROR_MSG( "stopwatch display write failed"); } return OK; ERROR: return FAIL; } int tl_stopwatch_Display(ESL_STOPWATCH *w) { char buf[128]; /* (safely holds up to 10^14 years; even I'll be dead by then) */ char buf2[128]; /* (safely holds up to 10^14 years; even I'll be dead by then) */ format_time_string(buf, w->user+w->sys, TRUE); format_time_string(buf2, w->elapsed, TRUE); #ifdef HAVE_TIMES LOG_MSG("CPU Time: %.2fu %.2fs %s Elapsed: %s", w->user, w->sys, buf,buf2); #else LOG_MSG("CPU Time: %.2fu %s Elapsed: %s", w->user, buf,buf2); #endif return OK; } /* Function: esl_stopwatch_GetElapsed() * Synopsis: Return the elapsed time in seconds * Incept: SRE, Fri Jan 8 10:10:37 2016 * * Purpose: After watch is Stop()'ed, calling * returns the elapsed time * in seconds. * * The resolution is system-dependent. */ double esl_stopwatch_GetElapsed(ESL_STOPWATCH *w) { return w->elapsed; } double tl_stopwatch_utime(ESL_STOPWATCH *w) { return w->user; } /* Function: esl_stopwatch_Include() * * Purpose: Merge the cpu and system times from a slave into * a master stopwatch. Both watches must be * stopped, and should not be stopped again unless * You Know What You're Doing. * * Elapsed time is not merged. Master is assumed * to be keeping track of the wall clock (real) time, * and the slave/worker watch is ignored. * * Useful in at least two cases. One is in * PVM, where we merge in the stopwatch(es) from separate * process(es) in a cluster. A second is in * threads, for broken pthreads/times() implementations * that lose track of cpu times used by spawned * threads. * * Args: master - stopwatch that's aggregating times * w - watch to add to the master. * * Returns: on success. */ int esl_stopwatch_Include(ESL_STOPWATCH *master, ESL_STOPWATCH *w) { master->user += w->user; master->sys += w->sys; return OK; } /***************************************************************** * Portable high resolution timing *****************************************************************/ #ifdef eslSTOPWATCH_HIGHRES /* The following code is * (C) 2012 David Robert Nadeau, http://NadeauSoftware.com * Creative Commons Attribution 3.0 Unported License * http://creativecommons.org/licenses/by/3.0/deed.en_US * * Reference: http://nadeausoftware.com/articles/2012/04/c_c_tip_how_measure_elapsed_real_time_benchmarking * * On resolution: * I believe that on Mac OS/X, the high performance timer has a resolution in units * of nanoseconds (at least on some platforms, including my laptop). However, calling * the esl_stopwatch_* functions themselves have overhead. The example driver is * a reasonable test of the minimal resolution, including call overhead; that gives * me about 0.1 microseconds (12 Jan 2016). */ #if defined(_WIN32) #include #elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__)) #include /* POSIX flags */ #include /* clock_gettime(), time() */ #include /* gethrtime(), gettimeofday() */ #if defined(__MACH__) && defined(__APPLE__) #include #include #endif #else #error "Unable to define getRealTime( ) for an unknown OS." #endif /** * Returns the real time, in seconds, or -1.0 if an error occurred. * * Time is measured since an arbitrary and OS-dependent start time. * The returned real time is only useful for computing an elapsed time * between two calls to this function. */ static double stopwatch_getRealTime(void) { #if defined(_WIN32) FILETIME tm; ULONGLONG t; #if defined(NTDDI_WIN8) && NTDDI_VERSION >= NTDDI_WIN8 /* Windows 8, Windows Server 2012 and later. ---------------- */ GetSystemTimePreciseAsFileTime( &tm ); #else /* Windows 2000 and later. ---------------------------------- */ GetSystemTimeAsFileTime( &tm ); #endif t = ((ULONGLONG)tm.dwHighDateTime << 32) | (ULONGLONG)tm.dwLowDateTime; return (double)t / 10000000.0; #elif (defined(__hpux) || defined(hpux)) || ((defined(__sun__) || defined(__sun) || defined(sun)) && (defined(__SVR4) || defined(__svr4__))) /* HP-UX, Solaris. ------------------------------------------ */ return (double)gethrtime( ) / 1000000000.0; #elif defined(__MACH__) && defined(__APPLE__) /* OSX. ----------------------------------------------------- */ static double timeConvert = 0.0; if ( timeConvert == 0.0 ) { mach_timebase_info_data_t timeBase; (void)mach_timebase_info( &timeBase ); timeConvert = (double)timeBase.numer / (double)timeBase.denom / 1000000000.0; } return (double)mach_absolute_time( ) * timeConvert; #elif defined(_POSIX_VERSION) /* POSIX. --------------------------------------------------- */ #if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0) { struct timespec ts; #if defined(CLOCK_MONOTONIC_PRECISE) /* BSD. --------------------------------------------- */ const clockid_t id = CLOCK_MONOTONIC_PRECISE; #elif defined(CLOCK_MONOTONIC_RAW) /* Linux. ------------------------------------------- */ const clockid_t id = CLOCK_MONOTONIC_RAW; #elif defined(CLOCK_HIGHRES) /* Solaris. ----------------------------------------- */ const clockid_t id = CLOCK_HIGHRES; #elif defined(CLOCK_MONOTONIC) /* AIX, BSD, Linux, POSIX, Solaris. ----------------- */ const clockid_t id = CLOCK_MONOTONIC; #elif defined(CLOCK_REALTIME) /* AIX, BSD, HP-UX, Linux, POSIX. ------------------- */ const clockid_t id = CLOCK_REALTIME; #else const clockid_t id = (clockid_t)-1; /* Unknown. */ #endif /* CLOCK_* */ if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 ) return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; /* Fall thru. */ } #endif /* _POSIX_TIMERS */ /* AIX, BSD, Cygwin, HP-UX, Linux, OSX, POSIX, Solaris. ----- */ struct timeval tm; gettimeofday( &tm, NULL ); return (double)tm.tv_sec + (double)tm.tv_usec / 1000000.0; #else return -1.0; /* Failed. */ #endif } #endif /*eslSTOPWATCH_HIGHRES*/ /***************************************************************** * Example of using the stopwatch module *****************************************************************/ #ifdef eslSTOPWATCH_EXAMPLE /*::cexcerpt::stopwatch_example::begin::*/ /* compile: gcc -g -Wall -I. -o example -DeslSTOPWATCH_EXAMPLE esl_stopwatch.c easel.c -lm * run: ./example */ #include "easel.h" #include "esl_stopwatch.h" int main(void) { ESL_STOPWATCH *w; double t = 0.; w = esl_stopwatch_Create(); /* This tests the minimum *practical* resolution of the clock, * inclusive of overhead of calling the stopwatch functions. * It gives me ~0.1 usec (12 Jan 2016). */ esl_stopwatch_Start(w); while (t == 0.) { esl_stopwatch_Stop(w); t = esl_stopwatch_GetElapsed(w); } printf("Elapsed time clock has practical resolution of around: %g sec\n", t); esl_stopwatch_Display(stdout, w, "CPU Time: "); esl_stopwatch_Destroy(w); return 0; } /*::cexcerpt::stopwatch_example::end::*/ #endif /*ESL_STOPWATCH_EXAMPLE*/ kalign-3.5.1/lib/src/esl_stopwatch.h000066400000000000000000000047371515023132300173600ustar00rootroot00000000000000/* Tracking cpu/system/elapsed time used by a process. * * SRE, Wed Feb 22 19:30:36 2006 [St. Louis] [moved to Easel] * SRE, Thu Aug 3 08:00:35 2000 [St. Louis] [moved to SQUID] * SRE, Fri Nov 26 14:54:21 1999 [St. Louis] [HMMER] */ #ifndef eslSTOPWATCH_INCLUDED #define eslSTOPWATCH_INCLUDED #include #ifdef ESL_STOPWATCH_IMPORT #define EXTERN #else #ifndef EXTERN #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #endif #include #ifdef HAVE_TIMES #include #endif #ifdef HAVE_UNISTD_H #include /* need for sysconf() */ #endif typedef struct { #ifdef eslSTOPWATCH_HIGHRES double t0; /* baseline wall time from Nadeau routine */ #elif HAVE_TIMES clock_t t0; /* baseline wall time, POSIX times() */ #else time_t t0; /* baseline wall time from ANSI time() */ #endif #ifdef HAVE_TIMES struct tms cpu0; /* baseline CPU/system time, POSIX times() */ #else clock_t cpu0; /* baseline CPU time, fallback to ANSI clock() */ #endif /* elapsed/user/sys are t-t0 results for the last time the * watch was Stop()'ed. */ double elapsed; /* elapsed wall time, seconds */ double user; /* CPU time, seconds */ double sys; /* system time, seconds */ } ESL_STOPWATCH; EXTERN ESL_STOPWATCH *esl_stopwatch_Create(void); EXTERN void esl_stopwatch_Destroy(ESL_STOPWATCH *w); EXTERN int esl_stopwatch_Start(ESL_STOPWATCH *w); EXTERN int esl_stopwatch_Stop(ESL_STOPWATCH *w); EXTERN int esl_stopwatch_Display(FILE *fp, ESL_STOPWATCH *w, char *prefix); EXTERN int tl_stopwatch_Display(ESL_STOPWATCH *w); EXTERN double esl_stopwatch_GetElapsed(ESL_STOPWATCH *w); EXTERN double tl_stopwatch_utime(ESL_STOPWATCH *w); EXTERN int esl_stopwatch_Include(ESL_STOPWATCH *master, ESL_STOPWATCH *w); #define DECLARE_TIMER(n) ESL_STOPWATCH* timer_##n = esl_stopwatch_Create(); #define START_TIMER(n) esl_stopwatch_Start(timer_##n); #define STOP_TIMER(n) esl_stopwatch_Stop(timer_##n); #define GET_TIMING(n) tl_stopwatch_Display(timer_##n); #define GET_ELAPSED(n) esl_stopwatch_GetElapsed(timer_##n); #define GET_USERTIME(n) tl_stopwatch_utime(timer_##n); #define DESTROY_TIMER(n) esl_stopwatch_Destroy(timer_##n); #undef ESL_STOPWATCH_IMPORT #undef EXTERN #endif /*eslSTOPWATCH_INCLUDED*/ kalign-3.5.1/lib/src/euclidean_dist.c000066400000000000000000000115231515023132300174370ustar00rootroot00000000000000#ifdef HAVE_AVX2 #include #include #include #endif #include "float.h" #include "tldevel.h" #include "tlrng.h" #include "esl_stopwatch.h" #define EUCLIDEAN_DIST_IMPORT #include "euclidean_dist.h" /* These functions were taken from: */ /* https://stackoverflow.com/questions/6996764/fastest-way-to-do-horizontal-float-vector-sum-on-x86 */ #ifdef HAVE_AVX2 float hsum256_ps_avx(__m256 v); float hsum_ps_sse3(__m128 v); #endif #ifdef UTEST_EDIST int main(void) { struct rng_state* rng; float** mat = NULL; double r; float d1; float d2; int i,j,c; int max_iter = 10; int num_element = 128; // mat = galloc(mat,1000,8,0.0); //void* _mm_malloc (size_t size, size_t align) MMALLOC(mat, sizeof(float*)* 100); for(i = 0; i < 100;i++){ mat[i] = NULL; #ifdef HAVE_AVX2 mat[i] = _mm_malloc(sizeof(float)*num_element, 32); #else MMALLOC(mat[i],sizeof(float)*num_element); #endif } RUNP( rng =init_rng(0)); //srand48_r(time(NULL), &randBuffer); for(i =0; i < 100;i++){ for(j = 0; j 10e-6){ ERROR_MSG("DIFFER: %d\t%d\t%f\t%f (%e %e)\n", i,j,d1,d2, fabsf(d1-d2), FLT_EPSILON); } } } #endif DECLARE_TIMER(t); LOG_MSG("Timing serial"); START_TIMER(t); for(c = 0; c < max_iter;c++){ for(i = 0; i < 100;i++){ for(j = 0; j <= i;j++){ edist_serial(mat[i], mat[j], num_element, &d1); } } } STOP_TIMER(t); GET_TIMING(t); //LOG_MSG("%f\tsec.",GET_TIMING(t)); #ifdef HAVE_AVX2 LOG_MSG("Timing AVX"); START_TIMER(t); for(c = 0; c < max_iter; c++){ for(i = 0; i < 100;i++){ for(j = 0; j <= i;j++){ edist_256(mat[i], mat[j], num_element, &d2); } } } STOP_TIMER(t); GET_TIMING(t); //LOG_MSG("%f\tsec.",GET_TIMING(t)); #endif for(i = 0; i < 100;i++){ #ifdef HAVE_AVX2 _mm_free(mat[i]); #else MFREE(mat[i]); #endif } MFREE(mat); MFREE(rng); DESTROY_TIMER(t); return EXIT_SUCCESS; ERROR: return EXIT_FAILURE; } #endif int edist_serial(const float* a,const float* b,const int len, float* ret) { int i; float d = 0.0f; float t; for(i = 0; i < len;i++){ t = (a[i] - b[i]); d += t *t; } *ret = sqrtf(d); return OK; } int edist_serial_d(const double* a,const double* b,const int len, double* ret) { int i; double d = 0.0f; double t; for(i = 0; i < len;i++){ t = (a[i] - b[i]); d += t *t; } *ret = sqrt(d); return OK; } #ifdef HAVE_AVX2 int edist_256(const float* a,const float* b, const int len, float* ret) { float d = 0.0f; register int i; __m256 xmm1;// = _mm256_load_ps(a); __m256 xmm2;// = _mm256_load_ps(b); __m256 r = _mm256_set1_ps(0.0f); for(i = 0;i < len;i+=8){ xmm1 = _mm256_load_ps(a); xmm2 = _mm256_load_ps(b); xmm1 = _mm256_sub_ps(xmm1, xmm2); xmm1 = _mm256_mul_ps(xmm1, xmm1); r = _mm256_add_ps(r, xmm1); a+=8; b+=8; } d = hsum256_ps_avx(r); *ret = sqrtf(d); return OK; } float hsum256_ps_avx(__m256 v) { __m128 vlow = _mm256_castps256_ps128(v); __m128 vhigh = _mm256_extractf128_ps(v, 1); // high 128 vlow = _mm_add_ps(vlow, vhigh); // add the low 128 return hsum_ps_sse3(vlow); // and inline the sse3 version, which is optimal for AVX // (no wasted instructions, and all of them are the 4B minimum) } float hsum_ps_sse3(__m128 v) { __m128 shuf = _mm_movehdup_ps(v); // broadcast elements 3,1 to 2,0 __m128 sums = _mm_add_ps(v, shuf); shuf = _mm_movehl_ps(shuf, sums); // high half -> low half sums = _mm_add_ss(sums, shuf); return _mm_cvtss_f32(sums); } #endif kalign-3.5.1/lib/src/euclidean_dist.h000066400000000000000000000007531515023132300174470ustar00rootroot00000000000000#ifndef EUCLIDIAN_DIST_H #define EUCLIDIAN_DIST_H #ifdef EUCLIDEAN_DIST_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif EXTERN int edist_256(const float* a,const float* b, const int len, float* ret); EXTERN int edist_serial(const float* a,const float* b,const int len, float* ret); EXTERN int edist_serial_d(const double* a,const double* b,const int len, double* ret); #undef EUCLIDEAN_DIST_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/io.h000066400000000000000000000002131515023132300150710ustar00rootroot00000000000000#ifndef IO_H #define IO_H #ifdef IO_IMPORT #define EXTERN #else #define EXTERN extern #endif #undef IO_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/mod_tldevel.h000066400000000000000000000002151515023132300167620ustar00rootroot00000000000000#ifndef MOD_TLDEVEL_H #define MOD_TLDEVEL_H #include "tldevel.h" #include "tlrng.h" #include "tlmisc.h" #include "esl_stopwatch.h" #endif kalign-3.5.1/lib/src/msa_alloc.c000066400000000000000000000103741515023132300164200ustar00rootroot00000000000000#include "tldevel.h" #include "msa_struct.h" #include "alphabet.h" #define MSA_ALLOC_IMPORT #include "msa_alloc.h" int alloc_msa(struct msa** msa, int numseq) { struct msa* m = NULL; int i; MMALLOC(m, sizeof(struct msa)); m->sequences = NULL; m->alloc_numseq = numseq; m->numseq = 0; m->num_profiles = 0; m->L = ALPHA_UNDEFINED; m->biotype = ALN_BIOTYPE_UNDEF; m->aligned = 0; m->alnlen = 0; m->quiet = 0; m->seq_distances = NULL; m->col_confidence = NULL; m->seq_weights = NULL; m->plen = NULL; m->sip = NULL; m->nsip = NULL; m->consistency_table = NULL; MMALLOC(m->sequences, sizeof(struct msa_seq*) * m->alloc_numseq); for(i = 0; i < m->alloc_numseq;i++){ m->sequences[i] = NULL; RUN(alloc_msa_seq(&m->sequences[i])); } for(i = 0; i < 128; i++){ m->letter_freq[i] = 0; } *msa = m; return OK; ERROR: kalign_free_msa(m); return FAIL; } int resize_msa(struct msa* msa) { int i; int old_size; old_size = msa->alloc_numseq; msa->alloc_numseq = msa->alloc_numseq + 512; MREALLOC(msa->sequences, sizeof(struct msa_seq*) * msa->alloc_numseq); for(i = old_size; i < msa->alloc_numseq;i++){ msa->sequences[i] = NULL; RUN(alloc_msa_seq(&msa->sequences[i])); } return OK; ERROR: return FAIL; } void kalign_free_msa(struct msa* msa) { int i; if(msa){ for(i = 0; i < msa->alloc_numseq;i++){ if(msa->sequences[i]){ free_msa_seq(msa->sequences[i]); } } if(msa->seq_distances){ MFREE(msa->seq_distances); } if(msa->col_confidence){ MFREE(msa->col_confidence); } if(msa->seq_weights){ MFREE(msa->seq_weights); } for (i = msa->num_profiles;i--;){ if(msa->sip[i]){ MFREE(msa->sip[i]); } } if(msa->plen){ MFREE(msa->plen); } if(msa->sip){ MFREE(msa->sip); } if(msa->nsip){ MFREE(msa->nsip); } MFREE(msa->sequences); MFREE(msa); } } int alloc_msa_seq(struct msa_seq** s) { struct msa_seq* seq = NULL; int i; MMALLOC(seq, sizeof(struct msa_seq)); seq->name = NULL; seq->seq = NULL; seq->s = NULL; seq->gaps = NULL; seq->confidence = NULL; seq->len = 0; seq->rank = 0; /* seq->name_len = 128; */ seq->alloc_len = 512; MMALLOC(seq->name, sizeof(char)* MSA_NAME_LEN); MMALLOC(seq->seq, sizeof(char) * seq->alloc_len); MMALLOC(seq->s, sizeof(uint8_t) * seq->alloc_len); MMALLOC(seq->gaps, sizeof(int) * (seq->alloc_len+1)); for(i =0;i < seq->alloc_len+1;i++){ seq->gaps[i] = 0; } *s = seq; return OK; ERROR: free_msa_seq(seq); return FAIL; } int resize_msa_seq(struct msa_seq* seq) { int old_len; int i; old_len = seq->alloc_len; seq->alloc_len = seq->alloc_len + 512; MREALLOC(seq->seq, sizeof(char) * seq->alloc_len); MREALLOC(seq->s, sizeof(uint8_t) * seq->alloc_len); MREALLOC(seq->gaps, sizeof(int) * (seq->alloc_len+1)); for(i = old_len+1;i < seq->alloc_len+1;i++){ seq->gaps[i] = 0; } return OK; ERROR: return FAIL; } void free_msa_seq(struct msa_seq* seq) { if(seq){ MFREE(seq->name); MFREE(seq->seq); MFREE(seq->s); MFREE(seq->gaps); if(seq->confidence){ MFREE(seq->confidence); } MFREE(seq); } } kalign-3.5.1/lib/src/msa_alloc.h000066400000000000000000000007331515023132300164230ustar00rootroot00000000000000#ifndef MSA_ALLOC_H #define MSA_ALLOC_H #ifdef MSA_ALLOC_IMPORT #define EXTERN #else #define EXTERN extern #endif struct msa; struct msa_seq; EXTERN int alloc_msa(struct msa** msa, int numseq); EXTERN int resize_msa(struct msa* msa); EXTERN void kalign_free_msa(struct msa* msa); EXTERN int alloc_msa_seq(struct msa_seq** s); EXTERN int resize_msa_seq(struct msa_seq* seq); EXTERN void free_msa_seq(struct msa_seq* seq); #undef MSA_ALLOC_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_check.c000066400000000000000000000250311515023132300163770ustar00rootroot00000000000000#include "tldevel.h" #include #include /* #include "strnlen_compat.h" */ #include "msa_struct.h" #define MSA_CHECK_IMPORT #include "msa_check.h" struct sort_struct_name_chksum{ struct msa_seq* seq; char** name; int chksum; int action; }; static int GCGchecksum(char *seq, int len); static int sort_by_name(const void *a, const void *b); static int sort_by_chksum(const void *a, const void *b); static int sort_by_both(const void *a, const void *b); int sort_seq_by_len(const void *a, const void *b); int kalign_sort_msa(struct msa *msa) { struct sort_struct_name_chksum** a = NULL; MMALLOC(a, sizeof(struct sort_struct_name_chksum *) * msa->numseq); for(int i = 0; i < msa->numseq;i++){ a[i] = NULL; MMALLOC(a[i], sizeof(struct sort_struct_name_chksum)); a[i]->seq = msa->sequences[i]; a[i]->name = &msa->sequences[i]->name; a[i]->chksum = GCGchecksum(msa->sequences[i]->seq, msa->sequences[i]->len); a[i]->action = 0; } qsort(a, msa->numseq, sizeof(struct sort_struct*),sort_by_both); for(int i = 0; i < msa->numseq;i++){ msa->sequences[i] = a[i]->seq; } for(int i = 0; i < msa->numseq;i++){ MFREE(a[i]); } MFREE(a); return OK; ERROR: if(a){ for(int i = 0; i < msa->numseq;i++){ MFREE(a[i]); } MFREE(a); } return FAIL; } int kalign_essential_input_check(struct msa *msa, int exit_on_error) { int problem_len0 = 0; ASSERT(msa != NULL, "No alignment"); ASSERT(msa->numseq > 1,"only %d sequences found.", msa->numseq); for(int i = 0; i < msa->numseq;i++){ if(msa->sequences[i]->len == 0){ if(!msa->quiet){ WARNING_MSG("No sequence found for sequence %s ",msa->sequences[i]->name); } problem_len0++; } msa->sequences[i]->rank = i; } if(!exit_on_error){ /* Here we attempt to fix the zero length problem */ if(problem_len0){ if(problem_len0 == 1){ if(!msa->quiet){ LOG_MSG("Removing %d sequence with a length of 0.", problem_len0); } }else{ if(!msa->quiet){ LOG_MSG("Removing %d sequences with a length of 0.",problem_len0); } } struct msa_seq** tmp = NULL; MMALLOC(tmp, sizeof(struct msa_seq* ) * msa->alloc_numseq); int c = 0; int e = msa->numseq-1; for(int i = 0 ; i < msa->numseq;i++){ if(msa->sequences[i]->len){ tmp[c] = msa->sequences[i]; c++; }else{ tmp[e] = msa->sequences[i]; e--; } } for(int i = msa->numseq; i < msa->alloc_numseq;i++){ tmp[i] = NULL; } MFREE(msa->sequences); msa->sequences = tmp; /* for(int i = msa->numseq-500; i < msa->numseq;i++){ */ /* LOG_MSG("%d\t%s", msa->sequences[i]->len,msa->sequences[i]->name); */ /* } */ /* LOG_MSG("%d %d %d ", msa->numseq, msa->numseq -c , problem_len0); */ /* qsort(msa->sequences, msa->numseq, sizeof(struct msa_seq*),sort_seq_by_len); */ /* int c = 0; */ /* for(int i = msa->numseq-1;i >= 0;i--){ */ /* if(msa->sequences[i]->len != 0){ */ /* c = i; */ /* break; */ /* } */ /* } */ /* c++; */ msa->numseq = c; ASSERT(msa->numseq > 1,"only %d sequences found.", msa->numseq); /* exit(0); */ } }else{ ERROR_MSG("%d sequences found with length 0.", problem_len0); } return OK; ERROR: return FAIL; } int kalign_check_msa(struct msa* msa, int exit_on_error) { char* tmp_name = NULL; /* char* tmp_ptr; */ struct sort_struct_name_chksum** a = NULL; int i; /* int j; */ int c; int l; MMALLOC(a, sizeof(struct sort_struct_name_chksum *) * msa->numseq); for(i = 0; i < msa->numseq;i++){ a[i] = NULL; MMALLOC(a[i], sizeof(struct sort_struct_name_chksum)); a[i]->seq = msa->sequences[i]; a[i]->name = &msa->sequences[i]->name; a[i]->chksum = GCGchecksum(msa->sequences[i]->seq, msa->sequences[i]->len); a[i]->action = 0; } qsort(a, msa->numseq, sizeof(struct sort_struct*),sort_by_name); for(i = 1; i < msa->numseq;i++){ if(strncmp(*a[i-1]->name, *a[i]->name,MSA_NAME_LEN) == 0){ /* WARNING_MSG("Name: %s is duplicated", a[i]->name); */ if(a[i-1]->chksum == a[i]->chksum){ if(!msa->quiet){ LOG_MSG("Found duplicated sequence:\n%s checksum: %d\n%s checksum: %d\n", *a[i-1]->name, a[i-1]->chksum, *a[i]->name, a[i]->chksum); } a[i-1]->action = 1; a[i]->action = 1; if(exit_on_error){ ERROR_MSG("Same seq with same name!"); } }else{ if(!msa->quiet){ WARNING_MSG("Found sequence pair with same name but different sequence:\n%s checksum: %d\n%s checksum: %d\n", *a[i-1]->name, a[i-1]->chksum, *a[i]->name, a[i]->chksum); } a[i-1]->action = 1; a[i]->action = 1; if(exit_on_error){ ERROR_MSG("This is a problem if we want to compare alignments down the track. Will append \"_X\" to the sequence name."); }else{ WARNING_MSG("This is a problem if we want to compare alignments down the track. Will append \"_X\" to the sequence name."); } } } } c = 1; for(i = 0; i < msa->numseq;i++){ if(a[i]->action){ tmp_name = NULL; l = strnlen(*a[i]->name, MSA_NAME_LEN)+ 16; MMALLOC(tmp_name, sizeof(char) * l); snprintf(tmp_name, l, "%s_%d",*a[i]->name,c); MFREE(*a[i]->name); *a[i]->name = tmp_name; c++; } } qsort(a, msa->numseq, sizeof(struct sort_struct*),sort_by_chksum); for(i = 1; i < msa->numseq;i++){ if(a[i-1]->chksum == a[i]->chksum){ if(!msa->quiet){ WARNING_MSG("Found identical sequences:\n%s checksum: %d\n%s checksum: %d\n", *a[i-1]->name, a[i-1]->chksum, *a[i]->name, a[i]->chksum); } } } for(i = 0; i < msa->numseq;i++){ MFREE(a[i]); } MFREE(a); return OK; ERROR: if(a){ for(i = 0; i < msa->numseq;i++){ MFREE(a[i]); } MFREE(a); } return FAIL; } int sort_by_both(const void *a, const void *b) { struct sort_struct_name_chksum* const *one = a; struct sort_struct_name_chksum* const *two = b; if(strncmp(*(*one)->name, *(*two)->name,MSA_NAME_LEN) < 0){ return -1; }else if(strncmp(*(*one)->name, *(*two)->name,MSA_NAME_LEN) == 0 ){ if((*one)->chksum > (*two)->chksum){ return -1; }else{ return 1; } }else{ return 1; } } int sort_by_name(const void *a, const void *b) { struct sort_struct_name_chksum* const *one = a; struct sort_struct_name_chksum* const *two = b; if(strncmp(*(*one)->name, *(*two)->name,MSA_NAME_LEN) < 0){ return -1; }else{ return 1; } } int sort_by_chksum(const void *a, const void *b) { struct sort_struct_name_chksum* const *one = a; struct sort_struct_name_chksum* const *two = b; if((*one)->chksum > (*two)->chksum){ return -1; }else{ return 1; } } int sort_seq_by_len(const void *a, const void *b) { struct msa_seq* const *one = a; struct msa_seq* const *two = b; if((*one)->len > (*two)->len){ return -1; }else{ return 1; } } /* Taken from squid library by Sean Eddy */ int GCGchecksum(char *seq, int len) { int i; /* position in sequence */ int chk = 0; /* calculated checksum */ for (i = 0; i < len; i++){ chk = (chk + (i % 57 + 1) * (toupper((int) seq[i]))) % 10000; } return chk; } kalign-3.5.1/lib/src/msa_check.h000066400000000000000000000006451515023132300164100ustar00rootroot00000000000000#ifndef MSA_CHECK_H #define MSA_CHECK_H #ifdef MSA_CHECK_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; EXTERN int kalign_essential_input_check(struct msa *msa, int exit_on_error); EXTERN int kalign_check_msa(struct msa* msa, int exit_on_error); EXTERN int kalign_sort_msa(struct msa *msa); #undef MSA_CHECK_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_cmp.c000066400000000000000000000506211515023132300161040ustar00rootroot00000000000000#include "tldevel.h" #include #include "msa_struct.h" #include "msa_check.h" #include "msa_op.h" #define MSA_CMP_IMPORT #include "msa_cmp.h" struct cmp_stats { uint64_t ref_total_aligned_pairs; uint64_t ref_total_gap_pairs; uint64_t identical_aligned; uint64_t identical_gaps; uint64_t test_total_aligned_pairs; uint64_t test_total_gap_pairs; }; struct detailed_pair_stats { int64_t ref_scored_pairs; int64_t test_pairs; int64_t common_scored; /* matches in scored columns → for recall */ int64_t common_all; /* all matches → for precision */ }; static int compare_pair(char *seq1A, char *seq2A, char *seq1B, char *seq2B, int len_a, int len_b, struct cmp_stats *stat); static int compare_pair_detailed(char *seq1A, char *seq2A, char *seq1B, char *seq2B, int len_a, int len_b, int *scored_cols, struct detailed_pair_stats *stat); int kalign_msa_compare(struct msa *r, struct msa *t, float *score) { struct cmp_stats* stat = NULL; ASSERT(r != NULL, "No reference alignment"); ASSERT(t != NULL, "No test alignment"); if(r->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(r); } if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } RUN(kalign_check_msa(r,1)); RUN(kalign_check_msa(t,1)); kalign_sort_msa(r); kalign_sort_msa(t); MMALLOC(stat, sizeof(struct cmp_stats)); stat->identical_gaps = 0; stat->identical_aligned = 0; stat->ref_total_gap_pairs = 0; stat->ref_total_aligned_pairs = 0; stat->test_total_gap_pairs = 0; stat->test_total_aligned_pairs = 0; /* float s = 0.0; */ /* float c = 0.0; */ for(int i = 0; i < r->numseq;i++){ for(int j = i + 1; j < r->numseq;j++){ compare_pair(r->sequences[i]->seq, r->sequences[j]->seq, t->sequences[i]->seq, t->sequences[j]->seq, r->alnlen, t->alnlen, stat ); } } /* LOG_MSG("%ld %ld %ld %ld %ld %ld", */ /* stat->ref_total_aligned_pairs, */ /* stat->ref_total_gap_pairs, */ /* stat->test_total_aligned_pairs, */ /* stat->test_total_gap_pairs, */ /* stat->identical_aligned, */ /* stat->identical_gaps); */ double a; double b; /* Pairs of aligned (and unaligned) residues in common with reference divided by number of pairs in reference */ a = (double) (stat->identical_aligned+ stat->identical_gaps ); b = (double) (stat->ref_total_aligned_pairs + stat->ref_total_gap_pairs); /* LOG_MSG("Score: %f (%f / %f)", 100.0 * a / b, a,b); */ /* Pairs of aligned residues in common with reference divided by number of pairs in reference */ a = (double) (stat->identical_aligned); b = (double) (stat->ref_total_aligned_pairs); /* LOG_MSG("Score: %f (%f / %f)", 100.0 * a / b, a,b); */ /* Pairs of aligned (and unaligned) residues in common with reference , divided by average number if pairs */ a = (double) (stat->identical_aligned+ stat->identical_gaps ); b = (double)(stat->ref_total_aligned_pairs + stat->ref_total_gap_pairs + stat->test_total_aligned_pairs + stat->test_total_gap_pairs)/ 2.0; /* LOG_MSG("Score: %f (%f / %f)", 100.0 * a / b, a,b); */ /* Pairs of aligned residues in common with reference , divided by average number if pairs */ a = (double) (stat->identical_aligned ); b = (double)(stat->ref_total_aligned_pairs + stat->test_total_aligned_pairs) / 2.0; /* LOG_MSG("Score: %f (%f / %f)", 100.0 * a / b, a,b); */ /* LOG_MSG("ORG Score: %f", s); */ a = (double) (stat->identical_aligned+ stat->identical_gaps ); b = (double) (stat->ref_total_aligned_pairs + stat->ref_total_gap_pairs); *score = 100.0 * a / b; /* exit(0); */ MFREE(stat); return OK; ERROR: return FAIL; } int compare_pair(char* seq1A, char* seq2A, char* seq1B, char* seq2B, int len_a, int len_b,struct cmp_stats* stat) { int* codes1_A = NULL; int* codes2_A = NULL; int* codes1_B = NULL; int* codes2_B = NULL; int p1; int p2; int g1; int g2; if(len_a == 0 || len_b == 0){ return OK; } MMALLOC(codes1_A, sizeof(int) * len_a); MMALLOC(codes2_A, sizeof(int) * len_a); MMALLOC(codes1_B, sizeof(int) * len_b); MMALLOC(codes2_B, sizeof(int) * len_b); /* process aln A */ p1 = -1; p2 = -1; for(int i = 0; i < len_a;i++){ g1 = 0; g2 = 0; if(isalpha((int) seq1A[i])){ p1++; }else{ g1 = 1; } if(isalpha((int) seq2A[i])){ p2++; }else{ g2 = 1; } if(!g1 && ! g2){ stat->ref_total_aligned_pairs++; codes1_A[p1] = p2; stat->ref_total_aligned_pairs++; codes2_A[p2] = p1; }else if(!g1 && g2){ stat->ref_total_gap_pairs++; codes1_A[p1] = -1; }else if(g1 && !g2){ stat->ref_total_gap_pairs++; codes2_A[p2] = -1; } } /* process aln B */ p1 = -1; p2 = -1; for(int i = 0; i < len_b;i++){ g1 = 0; g2 = 0; if(isalpha((int) seq1B[i])){ p1++; }else{ g1 = 1; } if(isalpha((int) seq2B[i])){ p2++; }else{ g2 = 1; } if(!g1 && ! g2){ stat->test_total_aligned_pairs++; codes1_B[p1] = p2; stat->test_total_aligned_pairs++; codes2_B[p2] = p1; }else if(!g1 && g2){ stat->test_total_gap_pairs++; codes1_B[p1] = -1; }else if(g1 && !g2){ stat->test_total_gap_pairs++; codes2_B[p2] = -1; } } /* LOG_MSG("P1: %d P2: %d len b: %d", p1, p2,len_b); */ /* fprintf(stdout,"%s\n%s\n",seq1A,seq2A); */ /* fprintf(stdout,"%s\n%s\n",seq1B, seq2B); */ /* fprintf(stdout,"\n"); */ for(int i = 0; i <= p1;i++){ if(codes1_A[i] != -1){ if(codes1_A[i] == codes1_B[i]){ stat->identical_aligned++; } }else{ if(codes1_A[i] == codes1_B[i]){ stat->identical_gaps++; } } /* if(codes1_A[i] != codes1_B[i]){ */ /* d += 1.0; */ /* /\* LOG_MSG("%d %d %d", i, codes1_A[i], codes1_B[i]); *\/ */ /* } */ } for(int i = 0; i <= p2;i++){ if(codes2_A[i] != -1){ if(codes2_A[i] == codes2_B[i]){ stat->identical_aligned++; } }else{ if(codes2_A[i] == codes2_B[i]){ stat->identical_gaps++; } } /* if(codes2_A[i] != codes2_B[i]){ */ /* d += 1.0; */ /* /\* LOG_MSG("%d %d %d (second seq)", i, codes1_A[i], codes1_B[i]); *\/ */ /* } */ } /* s = d /(float)( p1 + p2); */ /* LOG_MSG("score: %f", s); */ /* *score = s; */ MFREE(codes1_A); MFREE(codes1_B); MFREE(codes2_A); MFREE(codes2_B); return OK; ERROR: MFREE(codes1_A); MFREE(codes1_B); MFREE(codes2_A); MFREE(codes2_B); return FAIL; } /* Shared helper: compute POAR + TC scores given a pre-built column mask. */ static int compare_with_mask_helper(struct msa *r, struct msa *t, int *scored_cols, struct poar_score *out) { struct detailed_pair_stats dstat; int tc_correct = 0; int tc_total = 0; /* Accumulate POAR stats over all sequence pairs */ dstat.ref_scored_pairs = 0; dstat.test_pairs = 0; dstat.common_scored = 0; dstat.common_all = 0; for(int i = 0; i < r->numseq; i++){ for(int j = i + 1; j < r->numseq; j++){ compare_pair_detailed( r->sequences[i]->seq, r->sequences[j]->seq, t->sequences[i]->seq, t->sequences[j]->seq, r->alnlen, t->alnlen, scored_cols, &dstat ); } } /* TC score: for each scored column in reference, check if all non-gap residues map to the same column in the test alignment. Build residue-to-test-column map for each sequence. */ { int** res_to_tcol = NULL; MMALLOC(res_to_tcol, sizeof(int*) * t->numseq); for(int s = 0; s < t->numseq; s++){ res_to_tcol[s] = NULL; MMALLOC(res_to_tcol[s], sizeof(int) * (t->sequences[s]->len + 1)); } /* Fill residue→test_column map */ for(int s = 0; s < t->numseq; s++){ int p = 0; for(int c = 0; c < t->alnlen; c++){ if(isalpha((int)t->sequences[s]->seq[c])){ res_to_tcol[s][p] = c; p++; } } } /* Check each scored reference column */ for(int c = 0; c < r->alnlen; c++){ if(!scored_cols[c]){ continue; } int first_tcol = -1; int all_same = 1; int nres = 0; /* Count non-gap residues at this column */ for(int s = 0; s < r->numseq; s++){ if(isalpha((int)r->sequences[s]->seq[c])){ nres++; } } if(nres < 2){ /* Need at least 2 residues for a "pair" column */ continue; } tc_total++; /* Check if all residues map to same test column */ { int pos[r->numseq]; for(int s = 0; s < r->numseq; s++){ pos[s] = 0; } for(int cc = 0; cc < c; cc++){ for(int s = 0; s < r->numseq; s++){ if(isalpha((int)r->sequences[s]->seq[cc])){ pos[s]++; } } } for(int s = 0; s < r->numseq; s++){ if(isalpha((int)r->sequences[s]->seq[c])){ int tcol = res_to_tcol[s][pos[s]]; if(first_tcol < 0){ first_tcol = tcol; }else if(tcol != first_tcol){ all_same = 0; break; } } } } if(all_same){ tc_correct++; } } for(int s = 0; s < t->numseq; s++){ MFREE(res_to_tcol[s]); } MFREE(res_to_tcol); } /* Fill output */ out->ref_pairs = dstat.ref_scored_pairs; out->test_pairs = dstat.test_pairs; out->common = dstat.common_scored; if(dstat.ref_scored_pairs > 0){ out->recall = (double)dstat.common_scored / (double)dstat.ref_scored_pairs; }else{ out->recall = 0.0; } if(dstat.test_pairs > 0){ out->precision = (double)dstat.common_all / (double)dstat.test_pairs; }else{ out->precision = 0.0; } if(out->recall + out->precision > 0.0){ out->f1 = 2.0 * out->recall * out->precision / (out->recall + out->precision); }else{ out->f1 = 0.0; } if(tc_total > 0){ out->tc = (double)tc_correct / (double)tc_total; }else{ out->tc = 0.0; } return OK; ERROR: return FAIL; } int kalign_msa_compare_detailed(struct msa *r, struct msa *t, float max_gap_frac, struct poar_score *out) { int* scored_cols = NULL; ASSERT(r != NULL, "No reference alignment"); ASSERT(t != NULL, "No test alignment"); ASSERT(out != NULL, "No output struct"); if(r->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(r); } if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } RUN(kalign_check_msa(r, 1)); RUN(kalign_check_msa(t, 1)); kalign_sort_msa(r); kalign_sort_msa(t); /* Build scored column mask from reference alignment */ MMALLOC(scored_cols, sizeof(int) * r->alnlen); for(int c = 0; c < r->alnlen; c++){ if(max_gap_frac < 0.0f){ scored_cols[c] = 1; }else{ int ngaps = 0; for(int s = 0; s < r->numseq; s++){ if(!isalpha((int)r->sequences[s]->seq[c])){ ngaps++; } } float gf = (float)ngaps / (float)r->numseq; scored_cols[c] = (gf <= max_gap_frac) ? 1 : 0; } } RUN(compare_with_mask_helper(r, t, scored_cols, out)); MFREE(scored_cols); return OK; ERROR: MFREE(scored_cols); return FAIL; } int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, int *scored_cols, int n_cols, struct poar_score *out) { ASSERT(r != NULL, "No reference alignment"); ASSERT(t != NULL, "No test alignment"); ASSERT(scored_cols != NULL, "No column mask"); ASSERT(out != NULL, "No output struct"); if(r->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(r); } if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } RUN(kalign_check_msa(r, 1)); RUN(kalign_check_msa(t, 1)); kalign_sort_msa(r); kalign_sort_msa(t); ASSERT(n_cols == r->alnlen, "Mask length (%d) != reference alignment length (%d)", n_cols, r->alnlen); RUN(compare_with_mask_helper(r, t, scored_cols, out)); return OK; ERROR: return FAIL; } int compare_pair_detailed(char* seq1A, char* seq2A, char* seq1B, char* seq2B, int len_a, int len_b, int* scored_cols, struct detailed_pair_stats* stat) { int* codes1_A = NULL; int* codes2_A = NULL; int* codes1_B = NULL; int* codes2_B = NULL; int* in_scored1 = NULL; int* in_scored2 = NULL; int p1, p2, g1, g2; if(len_a == 0 || len_b == 0){ return OK; } MMALLOC(codes1_A, sizeof(int) * len_a); MMALLOC(codes2_A, sizeof(int) * len_a); MMALLOC(codes1_B, sizeof(int) * len_b); MMALLOC(codes2_B, sizeof(int) * len_b); MMALLOC(in_scored1, sizeof(int) * len_a); MMALLOC(in_scored2, sizeof(int) * len_a); /* Process reference alignment */ p1 = -1; p2 = -1; for(int i = 0; i < len_a; i++){ g1 = 0; g2 = 0; if(isalpha((int)seq1A[i])){ p1++; in_scored1[p1] = 0; }else{ g1 = 1; } if(isalpha((int)seq2A[i])){ p2++; in_scored2[p2] = 0; }else{ g2 = 1; } if(!g1 && !g2){ /* Both residues aligned */ codes1_A[p1] = p2; codes2_A[p2] = p1; if(scored_cols[i]){ stat->ref_scored_pairs += 2; /* both directions */ in_scored1[p1] = 1; in_scored2[p2] = 1; } }else if(!g1 && g2){ codes1_A[p1] = -1; }else if(g1 && !g2){ codes2_A[p2] = -1; } } /* Process test alignment */ p1 = -1; p2 = -1; for(int i = 0; i < len_b; i++){ g1 = 0; g2 = 0; if(isalpha((int)seq1B[i])){ p1++; }else{ g1 = 1; } if(isalpha((int)seq2B[i])){ p2++; }else{ g2 = 1; } if(!g1 && !g2){ stat->test_pairs += 2; /* both directions */ codes1_B[p1] = p2; codes2_B[p2] = p1; }else if(!g1 && g2){ codes1_B[p1] = -1; }else if(g1 && !g2){ codes2_B[p2] = -1; } } /* Compare: count scored matches (for recall) and all matches (for precision) */ for(int i = 0; i <= p1; i++){ if(codes1_A[i] >= 0 && codes1_A[i] == codes1_B[i]){ stat->common_all++; if(in_scored1[i]){ stat->common_scored++; } } } for(int i = 0; i <= p2; i++){ if(codes2_A[i] >= 0 && codes2_A[i] == codes2_B[i]){ stat->common_all++; if(in_scored2[i]){ stat->common_scored++; } } } MFREE(codes1_A); MFREE(codes1_B); MFREE(codes2_A); MFREE(codes2_B); MFREE(in_scored1); MFREE(in_scored2); return OK; ERROR: MFREE(codes1_A); MFREE(codes1_B); MFREE(codes2_A); MFREE(codes2_B); MFREE(in_scored1); MFREE(in_scored2); return FAIL; } kalign-3.5.1/lib/src/msa_cmp.h000066400000000000000000000023751515023132300161140ustar00rootroot00000000000000#ifndef MSA_CMP_H #define MSA_CMP_H #include #ifdef MSA_CMP_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; struct poar_score { double recall; /* |common| / |ref_scored_pairs| — bali_score SP equivalent */ double precision; /* |common| / |test_pairs| — fraction of test POARs correct */ double f1; /* 2*recall*precision / (recall+precision) */ double tc; /* fraction of scored columns that are completely correct */ int64_t ref_pairs; /* total scored reference POARs */ int64_t test_pairs; /* total test POARs */ int64_t common; /* POARs in both ref and test */ }; EXTERN int kalign_msa_compare(struct msa *r, struct msa *t, float *score); EXTERN int kalign_msa_compare_detailed(struct msa *r, struct msa *t, float max_gap_frac, struct poar_score *out); EXTERN int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, int *scored_cols, int n_cols, struct poar_score *out); #undef MSA_CMP_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_io.c000066400000000000000000001217261515023132300157410ustar00rootroot00000000000000 #include #include #include #include /* #include "strnlen_compat.h" */ #include "tldevel.h" #include "tlmisc.h" #include "esl_stopwatch.h" /* #include "version.h" */ #include "msa_struct.h" #include "msa_alloc.h" #include "msa_misc.h" /* NEED TO REMOVE */ #include "msa_op.h" #include "alphabet.h" #define MSA_IO_IMPORT #include "msa_io.h" struct in_line{ char* line; int len; }; struct in_buffer{ struct in_line** l; int n_lines; int alloc_lines; }; /* only local; */ struct line_buffer{ struct out_line** lines; int max_line_len; int alloc_num_lines; int num_line; }; struct out_line{ char* line; int block; int seq_id; }; static int check_for_sequences(struct msa* msa); static int read_fasta(struct in_buffer* b, struct msa** msa); static int read_msf(struct in_buffer* b, struct msa** msa); static int read_clu(struct in_buffer* b, struct msa** msa); static int write_msa_fasta(struct msa* msa,char* outfile); static int write_msa_clu(struct msa* msa,char* outfile); static int write_msa_msf(struct msa* msa,char* outfile); static struct line_buffer* alloc_line_buffer(int max_line_len); static int resize_line_buffer(struct line_buffer* lb); static void free_line_buffer(struct line_buffer* lb); static int read_file_stdin(struct in_buffer** buffer,char* infile); static int alloc_in_buffer(struct in_buffer** buffer, int n); static int resize_in_buffer(struct in_buffer* b); static void free_in_buffer(struct in_buffer* b); static int detect_alignment_format(struct in_buffer* b,int* type); static int null_terminate_sequences(struct msa* msa); static int sort_out_lines(const void *a, const void *b); static int parse_format_argument(char* format, int* type); int kalign_read_input(char* infile, struct msa** msa, int quiet) { struct in_buffer* b = NULL; struct msa* m = NULL; int type; int i,j; //ASSERT(infile != NULL,"No input file"); /* sanity checks */ if(infile){ if(!my_file_exists(infile)){ ERROR_MSG("File: %s does not exist.",infile); } } DECLARE_TIMER(timer); START_TIMER(timer); /* read everything into a in_buffer */ RUN(read_file_stdin(&b,infile)); /* Check if any input was read */ /* Logic: sum length of lines 1.. up to 5 */ //LOG_MSG("%s lines: %d", infile, b->n_lines); j = 0; for(i = 0; i < MACRO_MIN(1, b->n_lines);i++){ j += b->l[i]->len -1; /* exclude '0' at the end of strings */ //fprintf(stdout,"%d %s\n", i, b->l[i]->line); } //LOG_MSG("Len: %d",j); if(j == 0){ DESTROY_TIMER(timer); free_in_buffer(b); *msa = NULL; return OK; } RUN(detect_alignment_format(b, &type)); //LOG_MSG("FORMAT: %d", type); if(type == FORMAT_FA){ //RUNP(msa = read_msf(infile,msa)); //RUNP(msa = read_clu(infile,msa)); /* LOG_MSG("reading fasta"); */ RUN(read_fasta(b,&m)); }else if(type == FORMAT_MSF){ RUN(read_msf(b,&m)); }else if(type == FORMAT_CLU){ RUN(read_clu(b,&m)); }else if(type == FORMAT_DETECT_FAIL){ if(infile){ WARNING_MSG("Could not detect input in file: %s", infile); }else{ WARNING_MSG("Could not detect input in standard input"); } /* clean up allocated structures */ free_in_buffer(b); DESTROY_TIMER(timer); *msa = NULL; return FAIL; /* Return FAIL instead of OK when no valid format detected */ } m->quiet = quiet; RUN(detect_alphabet(m)); RUN(detect_aligned(m)); RUN(set_sip_nsip(m)); free_in_buffer(b); STOP_TIMER(timer); if(!quiet){ if(infile){ LOG_MSG("Read %d sequences from %s.", m->numseq, infile); }else{ LOG_MSG("Read %d sequences from standard input.",m->numseq); } GET_TIMING(timer); } DESTROY_TIMER(timer); if(*msa != NULL){ RUN(merge_msa(msa, m)); kalign_free_msa(m); }else{ *msa = m; } /* LOG_MSG("%d " , (*msa)->aligned); */ RUN(check_for_sequences(*msa)); return OK; ERROR: if(m){ kalign_free_msa(m); } return FAIL; } int check_for_sequences(struct msa* msa) { if(!msa){ ERROR_MSG("No sequences were found in the input files or standard input."); } if(msa->numseq < 2){ if(msa->numseq == 0){ ERROR_MSG("No sequences were found in the input files or standard input."); }else if (msa->numseq == 1){ ERROR_MSG("Only 1 sequence was found in the input files or standard input"); } } return OK; ERROR: return FAIL; } int kalign_write_msa(struct msa* msa, char* outfile, char* format) { int type = FORMAT_FA; ASSERT(msa!= NULL, "No alignment"); if(msa->aligned != ALN_STATUS_FINAL){ ERROR_MSG("Cannot produce msa output: Sequences are not aligned."); } RUN(parse_format_argument(format, &type)); /* LOG_MSG("Aligned? %d", msa->aligned); */ if(type == FORMAT_FA){ RUN(write_msa_fasta(msa, outfile)); }else if(type == FORMAT_MSF){ /* if(msa->aligned == ALN_STATUS_UNALIGNED || msa->aligned == ALN_STATUS_UNKNOWN){ */ /* ERROR_MSG("Cannot produce msa output: Sequences are not aligned."); */ /* } */ RUN(write_msa_msf(msa, outfile)); }else if(type == FORMAT_CLU){ /* if(msa->aligned == ALN_STATUS_UNALIGNED || msa->aligned == ALN_STATUS_UNKNOWN){ */ /* ERROR_MSG("Cannot produce clu output: Sequences are not aligned."); */ /* } */ RUN(write_msa_clu(msa, outfile)); }else{ ERROR_MSG("Output format not recognized."); } return OK; ERROR: return FAIL; } int parse_format_argument(char* format, int* type) { int out_format = -1; if(format){ if(strstr(format,"msf")){ out_format = FORMAT_MSF; }else if(strstr(format,"clu")){ out_format = FORMAT_CLU; }else if(strstr(format,"fasta")){ out_format = FORMAT_FA; }else if(strstr(format,"fa")){ out_format = FORMAT_FA; }else{ ERROR_MSG("Format %s not recognized.",format); } }else{ out_format = FORMAT_FA; } *type = out_format; return OK; ERROR: return FAIL; } /* detect alignment format */ int detect_alignment_format(struct in_buffer*b,int* type) { //FILE* f_ptr = NULL; char* line = NULL; //size_t b_len = 0; //ssize_t nread; //char line[BUFFER_LEN]; int hints[3]; int line_len; int line_number; int set; int i; ASSERT(b != NULL,"No input file"); /* sanity checks */ //if(!my_file_exists(infile)){ //ERROR_MSG("File: %s does not exist.",infile); //} line_number = 0; for(i = 0; i < 3; i++){ hints[i] =0; } for(i = 0; i < MACRO_MIN(b->n_lines, 100);i++){ line = b->l[i]->line; line_len = b->l[i]->len; //} //RUNP(f_ptr = fopen(infile, "r")); /* scan through first line header */ //while ((nread = getline(&line, &b_len, f_ptr)) != -1){ //while(fgets(line, BUFFER_LEN, f_ptr)){ //line_len = nread;//strnlen(line, BUFFER_LEN); //line[line_len-1] = 0; line_len--; if(line[0] == '>'){ hints[0]++; /* fasta */ } if(strstr(line, "multiple sequence alignment")){ hints[2]++; /* clustal format */ } if(strstr(line, "CLUSTAL W")){ hints[2]++; /* clustal format */ } if(strstr(line, "CLUSTAL O")){ hints[2]++; /* clustal format */ } if(strstr(line, "!!AA_MULTIPLE_ALIGNMENT")){ hints[1]++; } if(strstr(line, "!!NA_MULTIPLE_ALIGNMENT")){ hints[1]++; } if(strstr(line, "MSF:")){ hints[1]++; } line_number++; if(line_number == 100){ break; } } //fclose(f_ptr); //MFREE(line); set = 0; for(i = 0; i < 3;i++){ if(hints[i]){ set++; } } if(set == 0){ *type = FORMAT_DETECT_FAIL; //ERROR_MSG("Input alignment format could not be detected."); } if(set > 1){ *type = FORMAT_DETECT_FAIL; //ERROR_MSG("Input format could not be unambiguously detected"); } if(hints[0]){ *type = FORMAT_FA; } if(hints[1]){ *type = FORMAT_MSF; } if(hints[2]){ *type = FORMAT_CLU; } //fprintf(stdout,"fa: %d msf:%d clu:%d", hints[0],hints[1],hints[2]); return OK; ERROR: return FAIL; } int read_file_stdin(struct in_buffer** buffer,char* infile) { struct in_buffer* b = NULL; FILE* f_ptr = NULL; char* line = NULL; char* tmp = NULL; size_t b_len = 0; size_t nread; int i; //char line[BUFFER_LEN]; int line_len; b = *buffer; if(!b){ RUN(alloc_in_buffer(&b, 1024)); } if(infile){ if(!my_file_exists(infile)){ ERROR_MSG("File: %s does not exist.",infile); } RUNP(f_ptr = fopen(infile, "r")); }else{ f_ptr = stdin; } b->n_lines = 0; while ((nread = getline(&line, &b_len, f_ptr)) != -1){ //LOG_MSG("Read %d ", nread); //while(fgets(line, BUFFER_LEN, f_ptr)){ line_len = nread; //tmp = b->l[b->n_lines]->line; tmp= NULL; MMALLOC(tmp, sizeof(char) * (line_len+1)); for(i = 0; i < line_len;i++){ if(iscntrl((int) line[i])){ break; } tmp[i] = line[i]; } tmp[i] = 0; //memcpy(tmp, line, line_len); //tmp[line_len-1] = 0; b->l[b->n_lines]->line = tmp; b->l[b->n_lines]->len = i; b->n_lines++; if(b->n_lines == b->alloc_lines){ RUN(resize_in_buffer(b)); } } if(f_ptr != stdin){ fclose(f_ptr); } MFREE(line); *buffer = b; return OK; ERROR: return FAIL; } int read_fasta( struct in_buffer* b,struct msa** m) { struct msa* msa = NULL; struct msa_seq* seq_ptr = NULL; char* line = NULL; int line_len; int i; int nl; if(msa == NULL){ RUN(alloc_msa(&msa,512)); /* msa = alloc_msa(); */ } for(nl = 0; nl < b->n_lines;nl++){ line = b->l[nl]->line; line_len = b->l[nl]->len; if(line[0] == '>'){ /* alloc seq if buffer is full */ if(msa->alloc_numseq == msa->numseq){ RUN(resize_msa(msa)); } /* line[line_len-1] = 0; */ /*for(i = 0; i < line_len;i++){ if(isspace(line[i])){ line[i] = 0; } }*/ seq_ptr = msa->sequences[msa->numseq]; MFREE(seq_ptr->name); MMALLOC(seq_ptr->name, sizeof(char) * line_len); memcpy(seq_ptr->name, line+1, line_len); /* seq_ptr->name_len = line_len; */ /* snprintf(seq_ptr->name ,MSA_NAME_LEN ,"%s",line+1); */ //LOG_MSG("Name %d: len %d name : %s ",msa->numseq, line_len, seq_ptr->name); //fprintf(stdout,"%s\n%s\n", line, seq_ptr->name); msa->numseq++; }else{ for(i = 0;i < line_len;i++){ msa->letter_freq[(int)line[i]]++; if(isalpha((int)line[i])){ if(!seq_ptr){ ERROR_MSG("Encountered a sequence before encountering it's name"); } seq_ptr->seq[seq_ptr->len] = line[i]; seq_ptr->len++; if(seq_ptr->alloc_len == seq_ptr->len){ resize_msa_seq(seq_ptr); } }else if(ispunct((int)line[i])){ seq_ptr->gaps[seq_ptr->len]++; } } } } RUN(null_terminate_sequences(msa)); *m = msa; return OK; ERROR: kalign_free_msa(msa); return FAIL; } int read_clu(struct in_buffer* b , struct msa** m) { struct msa* msa = NULL; struct msa_seq* seq_ptr = NULL; char* line = NULL; int i,j; char* p; int active_seq = 0; int line_len; int nl,ni; if(msa == NULL){ RUN(alloc_msa(&msa,512)); /* msa = alloc_msa(); */ } ni =0; for(nl = 0; nl < b->n_lines;nl++){ line = b->l[nl]->line; line_len = b->l[nl]->len; ni++; break; } active_seq =0; for(nl = ni; nl < b->n_lines;nl++){ line = b->l[nl]->line; line_len = b->l[nl]->len; if(!line_len){ active_seq = 0; }else{ if(!isspace(line[0])){ if(msa->alloc_numseq == active_seq){ RUN(resize_msa(msa)); } seq_ptr = msa->sequences[active_seq]; p = line; j = 0; for(i = 0;i < line_len;i++){ if(i == MSA_NAME_LEN-1){ seq_ptr->name[i] = 0; j = i; break; } if(isspace((int)p[i])){ j = i; break; } seq_ptr->name[i] = p[i]; } seq_ptr->name[j] = 0; for(i = j;i < line_len;i++){ msa->letter_freq[(int)p[i]]++; if(isalpha((int)p[i])){ seq_ptr->seq[seq_ptr->len] = p[i]; seq_ptr->len++; if(seq_ptr->alloc_len == seq_ptr->len){ resize_msa_seq(seq_ptr); } }else if(ispunct((int)p[i])){ seq_ptr->gaps[seq_ptr->len]++; } } active_seq++; msa->numseq = MACRO_MAX(msa->numseq, active_seq); } } } RUN(null_terminate_sequences(msa)); *m = msa; return OK; ERROR: kalign_free_msa(msa); return FAIL; } int read_msf(struct in_buffer* b,struct msa** m) { struct msa* msa = NULL; struct msa_seq* seq_ptr = NULL; char* line = NULL; int line_len; int i,j,nl,li; char* p; int active_seq = 0; if(msa == NULL){ RUN(alloc_msa(&msa,512)); /* msa = alloc_msa(); */ } li = 0; for(nl = 0; nl < b->n_lines;nl++){ line = b->l[nl]->line; line_len = b->l[nl]->len; li++; /* line_len--; /\* last character is newline *\/ */ //fprintf(stdout,"%d \"%s\"\n",line_len,line); if(strstr(line, "//")){ // LOG_MSG("Header done"); break; } /* look for name */ p = strstr(line,"Name:"); if(p && strstr(line,"Len:")){ if(msa->alloc_numseq == msa->numseq){ RUN(resize_msa(msa)); } p += 5; /* length of name: */ while( isspace((int)*p)){ p++; } /* LOG_MSG("Found name: %s len %d", p, strlen(p)); */ seq_ptr = msa->sequences[active_seq]; for(i = 0;i < line_len;i++){ if(i == MSA_NAME_LEN-1){ seq_ptr->name[i] = 0; break; } if(isspace((int)p[i])){ seq_ptr->name[i] = 0; break; } seq_ptr->name[i] = p[i]; } seq_ptr->seq[0] = 0; msa->numseq++; active_seq++; //LOG_MSG("Got a name %s",p); } } active_seq = 0; for(nl = li; nl < b->n_lines;nl++){ line = b->l[nl]->line; line_len = b->l[nl]->len; /* line_len--; /\* last character is newline *\/ */ if(!line_len){ active_seq = 0; }else{ if(!isspace(line[0])){ seq_ptr = msa->sequences[active_seq]; //p = strstr(line,seq_ptr->name); //if(p){ //LOG_MSG("Found bitsof seq %s", seq_ptr->name); p = line; j = strnlen(seq_ptr->name, MSA_NAME_LEN); p += j; for(i = 0;i < line_len-j;i++){ msa->letter_freq[(int)p[i]]++; if(isalpha((int)p[i])){ seq_ptr->seq[seq_ptr->len] = p[i]; seq_ptr->len++; if(seq_ptr->alloc_len == seq_ptr->len){ resize_msa_seq(seq_ptr); } }else if(ispunct((int)p[i])){ seq_ptr->gaps[seq_ptr->len]++; } } //} active_seq++; } } //fprintf(stdout,"%d \"%s\"\n",line_len,line); } RUN(null_terminate_sequences(msa)); *m = msa; //fclose(f_ptr); //MFREE(line); return OK; ERROR: kalign_free_msa(msa); return FAIL; } int write_msa_fasta(struct msa* msa,char* outfile) { FILE* f_ptr = NULL; int i,j,f; if(!outfile){ f_ptr = stdout; }else{ RUNP(f_ptr = fopen(outfile, "w")); } for(i = 0; i < msa->numseq;i++){ fprintf(f_ptr,">%s\n", msa->sequences[i]->name); f = 0; for(j = 0;j < msa->alnlen;j++){ /*for(c = 0;c < msa->sequences[i]->gaps[j];c++){ fprintf(f_ptr,"-"); f++; if(f == 60){ fprintf(f_ptr, "\n"); f = 0; } }*/ fprintf(f_ptr,"%c", msa->sequences[i]->seq[j]); f++; if(f == 60){ fprintf(f_ptr, "\n"); f = 0; } } /* for(c = 0;c < msa->sequences[i]->gaps[ msa->sequences[i]->len];c++){ */ /* fprintf(f_ptr,"-"); */ /* f++; */ /* if(f == 60){ */ /* fprintf(f_ptr, "\n"); */ /* f = 0; */ /* } */ /* } */ if(f){ fprintf(f_ptr,"\n"); } } if(outfile){ fclose(f_ptr); } return OK; ERROR: return FAIL; } int write_msa_clu(struct msa* msa,char* outfile) { struct line_buffer* lb = NULL; struct out_line* ol= NULL; struct msa_seq* seq = NULL; char* linear_seq = NULL; char* ptr; FILE* f_ptr = NULL; int aln_len; int i,j,c,f; int block; int max_name_len = 0; int line_length; if(!msa->aligned){ ERROR_MSG("Sequences appear to be unaligned!"); } if(!outfile){ f_ptr = stdout; }else{ RUNP(f_ptr = fopen(outfile, "w")); } for(i = 0; i < msa->numseq;i++){ max_name_len = MACRO_MAX(max_name_len, (int)strnlen( msa->sequences[i]->name,MSA_NAME_LEN)); } /* aln_len = 0; */ /* for (j = 0; j <= msa->sequences[0]->len;j++){ */ /* aln_len += msa->sequences[0]->gaps[j]; */ /* } */ /* aln_len += msa->sequences[0]->len; */ /* MMALLOC(linear_seq, sizeof(char)* (aln_len+1)); */ /* length of write line buffer should be: max_name_len + 5 + 60 (for sequences) + 1 for newline character */ line_length = max_name_len +5 + 60 + 2; line_length = MACRO_MAX(256, line_length ); RUNP(lb = alloc_line_buffer(line_length)); /* print header line here I will use seq index -1 to make sure this ends up on top. */ ol = lb->lines[lb->num_line]; snprintf(ol->line, line_length,"Kalign (%s) multiple sequence alignment", KALIGN_PACKAGE_VERSION); ol->block = -1; ol->seq_id = -2; lb->num_line++; ol = lb->lines[lb->num_line]; ol->line[0] = 0; ol->block = -1; ol->seq_id = -1; lb->num_line++; aln_len = msa->alnlen; /* now the actual sequence lines */ for(i = 0; i < msa->numseq;i++){ block = 0; seq = msa->sequences[i]; /* RUN(make_linear_sequence(seq,linear_seq)); */ linear_seq = seq->seq;// msa->sequences[i]->seq; /* aln_len = seq->len;//msa->sequences[i]->len; */ f = 0; while(1){ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; c = (int) strnlen(seq->name, MSA_NAME_LEN); for(j = 0;j < c;j++){ ol->line[j] = seq->name[j]; } for(j = c;j < max_name_len+5;j++){ ol->line[j] = ' '; } ptr = ol->line + max_name_len+5; for(j = 0;j < 60;j++){ if(f == aln_len){ ptr[j] = 0; break; } ptr[j] = linear_seq[f]; f++; } ptr[j] = 0; ol->block = block; ol->seq_id = i; lb->num_line++; if(i == 0){ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; ol->block = block; ol->seq_id = msa->numseq; ol->line[0] = '\n'; ol->line[1] = 0; lb->num_line++; } block++; if(f == aln_len){ break; } } } qsort(lb->lines , lb->num_line, sizeof(struct out_line *), sort_out_lines); for(i = 0; i < lb->num_line;i++){ ol = lb->lines[i]; fprintf(f_ptr, "%s\n", ol->line); //fprintf(stdout,"%d %d %s\n",ol->seq_id,ol->block,ol->line); } if(outfile){ fclose(f_ptr); } free_line_buffer(lb); /* MFREE(linear_seq); */ return OK; ERROR: return FAIL; } int write_msa_msf(struct msa* msa,char* outfile) { struct line_buffer* lb = NULL; struct out_line* ol= NULL; struct msa_seq* seq = NULL; time_t now; /* current time as a time_t */ struct tm local_time; char date[64]; /* today's date in GCG's format "October 3, 1996 15:57" */ char* linear_seq = NULL; char* ptr; char* basename = NULL; FILE* f_ptr = NULL; int aln_len; int i,j,c,f; int block; int max_name_len = 0; int line_length; int header_index; int written; if(!msa->aligned){ ERROR_MSG("Sequences appear to be unaligned!"); } if(!outfile){ f_ptr = stdout; }else{ RUNP(f_ptr = fopen(outfile, "w")); } for(i = 0; i < msa->numseq;i++){ max_name_len = MACRO_MAX(max_name_len, (int)strnlen( msa->sequences[i]->name,MSA_NAME_LEN)); } aln_len = msa->sequences[0]->len; /* for (j = 0; j <= msa->sequences[0]->len;j++){ */ /* aln_len+= msa->sequences[0]->gaps[j]; */ /* } */ /* aln_len += msa->sequences[0]->len; */ /* MMALLOC(linear_seq, sizeof(char)* (aln_len+1)); */ /* length of write line buffer should be: max_name_len + 5 + 60 (for sequences) + 1 for newline character + 6 for spaces every 10 letters */ /* BUG: the header lines might be longer!! */ line_length = max_name_len +5 + 60 + 2+6 ; line_length = MACRO_MAX(256, line_length); RUNP(lb = alloc_line_buffer(line_length)); /* print header line here I will use seq index -1 to make sure this ends up on top. */ /* !!AA_MULTIPLE_ALIGNMENT 1.0 stdout MSF: 131 Type: P 16/01/02 CompCheck: 3003 .. Name: IXI_234 Len: 131 Check: 6808 Weight: 1.00 Name: IXI_235 Len: 131 Check: 4032 Weight: 1.00 Name: IXI_236 Len: 131 Check: 2744 Weight: 1.00 Name: IXI_237 Len: 131 Check: 9419 Weight: 1.00 // */ header_index = -1 * (msa->numseq+10); ol = lb->lines[lb->num_line]; //LOG_MSG("Alphabet : %d", msa->L); if(msa->L == ALPHA_defPROTEIN){ snprintf(ol->line, line_length,"!!AA_MULTIPLE_ALIGNMENT 1.0"); }else if(msa->L == ALPHA_redPROTEIN){ snprintf(ol->line, line_length,"!!AA_MULTIPLE_ALIGNMENT 1.0"); }else if(msa->L == ALPHA_defDNA){ snprintf(ol->line, line_length,"!!NA_MULTIPLE_ALIGNMENT 1.0"); }else{ snprintf(ol->line, line_length,"!!NA_MULTIPLE_ALIGNMENT 1.0"); } ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; /* a space */ ol = lb->lines[lb->num_line]; ol->line[0] = 0; ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; /* The msf line*/ now = time(NULL); if((localtime_r(&now,&local_time)) == NULL){ ERROR_MSG("could not get local time"); } if (strftime(date, 64, "%B %d, %Y %H:%M", &local_time) == 0){ ERROR_MSG("time failed???"); } ol = lb->lines[lb->num_line]; if(outfile){ RUN(tlfilename(outfile, &basename)); } written = snprintf(ol->line, line_length," %s MSF: %d Type: %c %s Check: %d ..", outfile == NULL ? "stdout" : basename,aln_len, msa->L == ALPHA_defPROTEIN ? 'P' : 'N', date, GCGMultchecksum(msa)); if(written >= line_length){ MREALLOC(lb->lines[lb->num_line]->line,sizeof(char) * (written+1)); ol = lb->lines[lb->num_line]; written = snprintf(ol->line, written+1," %s MSF: %d Type: %c %s Check: %d ..", outfile == NULL ? "stdout" : basename,aln_len, msa->L == ALPHA_defPROTEIN ? 'P' : 'N', date, GCGMultchecksum(msa)); } if(basename){ MFREE(basename); } ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; /* another space */ ol = lb->lines[lb->num_line]; ol->line[0] = 0; ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; //Name: IXI_234 Len: 131 Check: 6808 Weight: 1.00 for(i = 0; i < msa->numseq;i++){ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; written = snprintf(ol->line, line_length," Name: %-*.*s Len: %5d Check: %4d Weight: %.2f", max_name_len,max_name_len, msa->sequences[i]->name , aln_len, GCGchecksum(msa->sequences[i]->seq, msa->sequences[i]->len), 1.0); if(written >= line_length){ MREALLOC(lb->lines[lb->num_line]->line,sizeof(char) * (written+1)); ol = lb->lines[lb->num_line]; written = snprintf(ol->line, written+1," Name: %-*.*s Len: %5d Check: %4d Weight: %.2f", max_name_len,max_name_len, msa->sequences[i]->name , aln_len, GCGchecksum(msa->sequences[i]->seq, msa->sequences[i]->len), 1.0); } ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; } /* another space */ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; ol->line[0] = 0; ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; /* header section finished */ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; snprintf(ol->line, line_length,"//"); ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; /* another space */ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; ol->line[0] = 0; ol->block = -1; ol->seq_id = header_index; header_index++; lb->num_line++; aln_len = msa->alnlen; /* now the actual sequence lines */ for(i = 0; i < msa->numseq;i++){ block = 0; seq = msa->sequences[i]; //RUN(make_linear_sequence(seq,linear_seq)); linear_seq = seq->seq; /* aln_len = seq->len; */ f = 0; while(1){ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; c = strnlen(seq->name, MSA_NAME_LEN); for(j = 0;j < c;j++){ ol->line[j] = seq->name[j]; } for(j = c;j < max_name_len+5;j++){ ol->line[j] = ' '; } ptr = ol->line + max_name_len+5; for(j = 0;j < 60;j++){ if(f == aln_len){ ptr[j] = 0; break; } ptr[j] = linear_seq[f]; f++; } ptr[j] = 0; ol->block = block; ol->seq_id = i; lb->num_line++; if(i == 0){ if(lb->alloc_num_lines == lb->num_line){ resize_line_buffer(lb); } ol = lb->lines[lb->num_line]; ol->block = block; ol->seq_id = msa->numseq; ol->line[0] = '\n'; ol->line[1] = 0; lb->num_line++; } block++; if(f == aln_len){ break; } } } qsort(lb->lines , lb->num_line, sizeof(struct out_line *), sort_out_lines); for(i = 0; i < lb->num_line;i++){ ol = lb->lines[i]; //fprintf(stdout,"%d %d %s\n",ol->seq_id,ol->block,ol->line); fprintf(f_ptr, "%s\n", ol->line); } if(outfile){ fclose(f_ptr); } free_line_buffer(lb); /* MFREE(linear_seq); */ return OK; ERROR: if(linear_seq){ MFREE(linear_seq); } if(basename){ MFREE(basename); } return FAIL; } int null_terminate_sequences(struct msa* msa) { int i; struct msa_seq* seq_ptr = NULL; /* 0 terminate sequences */ for(i = 0; i < msa->numseq;i++){ seq_ptr = msa->sequences[i]; if(seq_ptr->alloc_len == seq_ptr->len){ resize_msa_seq(seq_ptr); } seq_ptr->seq[seq_ptr->len] = 0; } return OK; } struct line_buffer* alloc_line_buffer(int max_line_len) { struct line_buffer* lb = NULL; int i; ASSERT(max_line_len > 60, "max_line_len:%d too small", max_line_len); MMALLOC(lb, sizeof(struct line_buffer)); lb->alloc_num_lines = 1024; lb->num_line = 0; lb->lines = NULL; lb->max_line_len = max_line_len; MMALLOC(lb->lines, sizeof(struct out_line*) * lb->alloc_num_lines); for(i = 0; i < lb->alloc_num_lines;i++){ lb->lines[i] = NULL; MMALLOC(lb->lines[i], sizeof(struct out_line)); lb->lines[i]->block = 0; lb->lines[i]->seq_id =0; lb->lines[i]->line = NULL; MMALLOC(lb->lines[i]->line,sizeof(char) * lb->max_line_len); } return lb; ERROR: return NULL; } int resize_line_buffer(struct line_buffer* lb) { int old_len = 0; int i; old_len = lb->alloc_num_lines; lb->alloc_num_lines = lb->alloc_num_lines + 1024; MREALLOC(lb->lines, sizeof(struct out_line*) * lb->alloc_num_lines); for(i = old_len; i < lb->alloc_num_lines;i++){ lb->lines[i] = NULL; MMALLOC(lb->lines[i], sizeof(struct out_line)); lb->lines[i]->block = 0; lb->lines[i]->seq_id =0; lb->lines[i]->line = NULL; MMALLOC(lb->lines[i]->line,sizeof(char) * lb->max_line_len); } return OK; ERROR: return FAIL; } void free_line_buffer(struct line_buffer* lb) { int i; if(lb){ for(i = 0; i < lb->alloc_num_lines;i++){ MFREE(lb->lines[i]->line); MFREE(lb->lines[i]); } MFREE(lb->lines); MFREE(lb); } } int alloc_in_buffer(struct in_buffer** buffer, int n) { struct in_buffer* b = NULL; int i; MMALLOC(b, sizeof(struct in_buffer)); b->alloc_lines = n; b->l = NULL; b->n_lines = 0; MMALLOC(b->l, sizeof(struct in_line*) * b->alloc_lines); for(i = 0; i < b->alloc_lines;i++){ b->l[i] = NULL; MMALLOC(b->l[i], sizeof(struct in_line)); b->l[i]->line = NULL; b->l[i]->len =0; } *buffer = b; return OK; ERROR: return FAIL; } int resize_in_buffer(struct in_buffer* b) { int i,o; o = b->alloc_lines; b->alloc_lines = b->alloc_lines + b->alloc_lines / 2; MREALLOC(b->l, sizeof(struct in_line*) * b->alloc_lines); for(i = o; i < b->alloc_lines;i++){ b->l[i] = NULL; MMALLOC(b->l[i], sizeof(struct in_line)); b->l[i]->line = NULL; b->l[i]->len =0; } return OK; ERROR: return FAIL; } void free_in_buffer(struct in_buffer* b) { int i; if(b){ for(i = 0; i < b->n_lines ;i++){ MFREE(b->l[i]->line); } for(i = 0; i < b->alloc_lines;i++){ MFREE(b->l[i]); } MFREE(b->l); MFREE(b); } } int sort_out_lines(const void *a, const void *b) { struct out_line* const *one = a; struct out_line* const *two = b; if((*one)->block > (*two)->block){ return 1; }else if((*one)->block == (*two)->block){ if((*one)->seq_id > (*two)->seq_id){ return 1; }else if((*one)->seq_id == (*two)->seq_id){ return 0; }else{ return -1; } }else{ return -1; } } kalign-3.5.1/lib/src/msa_io.h000066400000000000000000000006301515023132300157340ustar00rootroot00000000000000#ifndef MSA_IO_H #define MSA_IO_H #ifdef MSA_IO_IMPORT #define EXTERN #else #define EXTERN extern #endif #define FORMAT_DETECT_FAIL -1 #define FORMAT_FA 1 #define FORMAT_MSF 2 #define FORMAT_CLU 3 struct msa; EXTERN int kalign_read_input(char* infile, struct msa** msa,int quiet); EXTERN int kalign_write_msa(struct msa* msa, char* outfile, char* format); #undef MSA_IO_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_misc.c000066400000000000000000000013041515023132300162520ustar00rootroot00000000000000#include #include "msa_struct.h" #define MSA_MISC_IMPORT #include "msa_misc.h" /* alignment checksum */ int GCGMultchecksum(struct msa* msa) { int chk = 0; int idx; for (idx = 0; idx < msa->numseq; idx++){ chk = (chk + GCGchecksum(msa->sequences[idx]->seq, msa->sequences[idx]->len)) % 10000; } return chk; } /* Taken from squid library by Sean Eddy */ int GCGchecksum(char *seq, int len) { int i; /* position in sequence */ int chk = 0; /* calculated checksum */ for (i = 0; i < len; i++){ chk = (chk + (i % 57 + 1) * (toupper((int) seq[i]))) % 10000; } return chk; } kalign-3.5.1/lib/src/msa_misc.h000066400000000000000000000004051515023132300162600ustar00rootroot00000000000000#ifndef MSA_MISC_H #define MSA_MISC_H #ifdef MSA_MISC_IMPORT #define EXTERN #else #define EXTERN extern #endif struct msa; EXTERN int GCGMultchecksum(struct msa* msa); EXTERN int GCGchecksum(char *seq, int len); #undef MSA_MISC_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_op.c000066400000000000000000000447741515023132300157570ustar00rootroot00000000000000#include "tldevel.h" #include "msa_struct.h" #include "alphabet.h" #include #include "msa_alloc.h" #define MSA_OP_IMPORT #include "msa_op.h" static int aln_unknown_warning_message_gaps_but_len_diff(struct msa *msa); static int aln_unknown_warning_message_same_len_no_gaps(void); int msa_cpy(struct msa** dest, struct msa* src) { int i; struct msa* d = NULL; d = *dest; if(d == NULL){ RUN(alloc_msa(&d,src->alloc_numseq)); /* d = alloc_msa(); */ } if(d->biotype != ALN_BIOTYPE_UNDEF){ /* if(d->L != ALPHA_UNDEFINED){ */ if(d->biotype != src->biotype){ ERROR_MSG("Input alignments have different alphabets"); } } if(d->aligned != 0 && d->aligned != ALN_STATUS_UNKNOWN){ if(d->aligned != src->aligned){ d->aligned = ALN_STATUS_UNKNOWN; } } for(i = 0; i < 128;i++){ d->letter_freq[i] += src->letter_freq[i]; } d->numseq = 0; for(i = 0; i < src->numseq;i++){ msa_seq_cpy(d->sequences[i], src->sequences[i]); } d->numseq = src->numseq; d->quiet = src->quiet; RUN(detect_alphabet(d)); RUN(detect_aligned(d)); RUN(set_sip_nsip(d)); *dest = d; return OK; ERROR: return FAIL; } int msa_seq_cpy(struct msa_seq *d, struct msa_seq *src) { ASSERT(d != NULL,"No sequence"); ASSERT(src != NULL,"No sequence"); while(src->alloc_len > d->alloc_len){ resize_msa_seq(d); } snprintf(d->name, MSA_NAME_LEN, "%s", src->name); for(int j = 0; j < src->len;j++){ d->seq[j] = src->seq[j]; d->s[j] = src->s[j]; d->gaps[j] = src->gaps[j]; } d->gaps[src->alloc_len] = src->gaps[src->alloc_len]; d->seq[src->len] = 0; d->len = src->len; d->rank = src->rank; return OK; ERROR: return FAIL; } int merge_msa(struct msa** dest, struct msa* src) { int i; struct msa* d = NULL; d = *dest; if(d == NULL){ RUN(alloc_msa(&d,src->alloc_numseq)); /* d = alloc_msa(); */ } if(d->biotype != ALN_BIOTYPE_UNDEF){ /* if(d->L != ALPHA_UNDEFINED){ */ if(d->biotype != src->biotype){ ERROR_MSG("Input alignments have different alphabets"); } } if(d->aligned != 0 && d->aligned != ALN_STATUS_UNKNOWN){ if(d->aligned != src->aligned){ d->aligned = ALN_STATUS_UNKNOWN; } } for(i = 0; i < 128;i++){ d->letter_freq[i] += src->letter_freq[i]; } for(i = 0; i < src->numseq;i++){ free_msa_seq(d->sequences[d->numseq]); d->sequences[d->numseq] = src->sequences[i]; src->sequences[i] = NULL; d->numseq++; if(d->alloc_numseq == d->numseq){ RUN(resize_msa(d)); } } RUN(detect_alphabet(d)); RUN(detect_aligned(d)); RUN(set_sip_nsip(d)); *dest = d; return OK; ERROR: return FAIL; } int dealign_msa(struct msa* msa) { struct msa_seq* seq = NULL; int i; int j; for(i = 0; i < msa->numseq;i++){ seq = msa->sequences[i]; for(j = 0; j <= seq->len;j++){ seq->gaps[j] = 0; } } msa->aligned = ALN_STATUS_UNALIGNED; return OK; } int detect_alphabet(struct msa* msa) { int i; double DNA[128]; double protein[128]; char DNA_letters[12]= "acgtunACGTUN"; char protein_letters[40] = "acdefghiklmnpqrstvwyACDEFGHIKLMNPQRSTVWY"; double dna_prob; double prot_prob; ASSERT(msa != NULL, "No alignment"); for(i = 0; i < 128;i++){ DNA[i] = log(0.0001 * 1.0 / 116.0); protein[i] = log(0.0001 * 1.0 / 88.0); } for(i = 0 ; i < 12;i++){ DNA[(int) DNA_letters[i]] = log(0.9999 * 1.0 / 12.0); } for(i = 0 ; i < 40;i++){ protein[(int) protein_letters[i]] = log(0.9999 * 1.0 / 40.0); } /* dna_prob = 0.0; */ /* prot_prob = 0.0; */ /* for(i = 0; i <128;i++){ */ /* dna_prob += exp(DNA[i]); */ /* prot_prob += exp(protein[i]); */ /* } */ /* LOG_MSG("DNA: %f PROT: %f",dna_prob,prot_prob); */ dna_prob = 0.0; prot_prob = 0.0; for(i = 0; i < 128;i++){ if(msa->letter_freq[i]){ dna_prob += DNA[i] * (double) msa->letter_freq[i]; prot_prob += protein[i]* (double) msa->letter_freq[i]; } } /* LOG_MSG("DNA: %f PROT: %f",dna_prob,prot_prob); */ /* exit(0); */ if( dna_prob == prot_prob){ WARNING_MSG("Could not determine whether we have a DNA or Protein alignment"); msa->L = ALPHA_UNKNOWN; }else{ if(dna_prob > prot_prob){ if(!msa->quiet){ LOG_MSG("Detected DNA sequences."); } msa->biotype = ALN_BIOTYPE_DNA; /* msa->L = ALPHA_defDNA; */ /* RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); */ }else if(prot_prob > dna_prob){ if(!msa->quiet){ LOG_MSG("Detected protein sequences."); } msa->biotype = ALN_BIOTYPE_PROTEIN; /* msa->L = ALPHA_redPROTEIN; */ /* RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); */ }else{ ERROR_MSG("Alphabet not recognized."); } } return OK; ERROR: return FAIL; } int detect_aligned(struct msa* msa) { int min_len; int max_len; int l; int i; int j; int n; int gaps = 0; /* assume that sequences are not aligned */ msa->aligned = 0; /* Improved logic: Lets sum up the number of gaps plus sequence length of the first X sequences. if min == max it is aligned. */ min_len = INT32_MAX; max_len = 0; gaps = 0; /* n = MACRO_MIN(50, msa->numseq); */ n = msa->numseq; for(i = 0; i < n;i++){ l = 0; for (j = 0; j <= msa->sequences[i]->len;j++){ l += msa->sequences[i]->gaps[j]; } gaps += l; l += msa->sequences[i]->len; min_len = MACRO_MIN(min_len, l); max_len = MACRO_MAX(max_len, l); } /* LOG_MSG("%d %d", max_len, min_len); */ /* exit(0); */ if(gaps){ if(min_len == max_len){ /* sequences have gaps and total length is identical - clearly aligned */ msa->aligned = ALN_STATUS_ALIGNED; }else{ /* odd there are gaps but total length differs - unknown status */ if(!msa->quiet){ aln_unknown_warning_message_gaps_but_len_diff(msa); } msa->aligned = ALN_STATUS_UNKNOWN; } }else{ if(min_len == max_len){ /* no gaps and sequences have same length. Can' tell if they are aligned */ if(!msa->quiet){ aln_unknown_warning_message_same_len_no_gaps(); } msa->aligned = ALN_STATUS_UNKNOWN; }else{ /* No gaps and sequences have different lengths - unaligned */ msa->aligned = ALN_STATUS_UNALIGNED; } } /* LOG_MSG("Aligned: %d gaps: %d",msa->aligned,gaps); */ return OK; } int set_sip_nsip(struct msa* msa) { int i; ASSERT(msa!= NULL, "No msa"); if(msa->seq_weights){ MFREE(msa->seq_weights); msa->seq_weights = NULL; } if(msa->plen){ for (i = msa->num_profiles;i--;){ if(msa->sip[i]){ MFREE(msa->sip[i]); } } if(msa->plen){ MFREE(msa->plen); } if(msa->sip){ MFREE(msa->sip); } if(msa->nsip){ MFREE(msa->nsip); } msa->plen = NULL; msa->sip = NULL; msa->nsip = NULL; } msa->num_profiles = (msa->numseq << 1 )-1; MMALLOC(msa->sip,sizeof(int*)* msa->num_profiles); MMALLOC(msa->nsip,sizeof(int)* msa->num_profiles); MMALLOC(msa->plen,sizeof(int)* msa->num_profiles); for (i =0;i < msa->num_profiles;i++){ msa->sip[i] = NULL; msa->nsip[i] = 0; } for(i = 0;i < msa->numseq;i++){ MMALLOC(msa->sip[i],sizeof(int)); msa->nsip[i] = 1; msa->sip[i][0] = i; msa->plen[i] = 0; } return OK; ERROR: return FAIL; } int reformat_settings_msa(struct msa *msa, int rename, int unalign) { for (int i = 0 ;i < msa->numseq;i++){ msa->nsip[i] = i; } if(rename){ for (int i = 0 ;i < msa->numseq;i++){ snprintf(msa->sequences[i]->name, 128, "SEQ%d", i+1); } } if(unalign){ RUN(dealign_msa(msa)); } return OK; ERROR: return FAIL; } int convert_msa_to_internal(struct msa* msa, int type) { struct alphabet* a = NULL; struct msa_seq* seq = NULL; int8_t* t = NULL; int i,j; RUNP(a = create_alphabet(type)); t = a->to_internal; msa->L = a->L; for(i = 0; i < msa->numseq;i++){ seq = msa->sequences[i]; for(j =0 ; j < seq->len;j++){ if(t[(int) seq->seq[j]] == -1){ WARNING_MSG("there should be no character not matching the alphabet"); WARNING_MSG("offending character: >>>%c<<<", seq->seq[j]); seq->s[j] = 0; }else{ seq->s[j] = t[(int) seq->seq[j]]; } } } MFREE(a); return OK; ERROR: if(a){ MFREE(a); } return FAIL; } int kalign_msa_to_arr(struct msa* msa, char ***aligned, int *out_aln_len) { ASSERT(msa != NULL,"No MSA!"); ASSERT(msa->aligned == ALN_STATUS_FINAL,"Sequences are not finalized"); char** out = NULL; int numseq = msa->numseq; int aln_len = msa->alnlen; MMALLOC(out, sizeof(char*) * numseq); for(int i = 0 ; i < numseq;i++){ out[i] = NULL; MMALLOC(out[i], sizeof(char) * (aln_len +1)); } /* galloc(&out, numseq,aln_len+1); */ for(int i = 0 ; i < numseq;i++){ for(int j = 0; j < aln_len;j++){ out[i][j] = msa->sequences[i]->seq[j]; } out[i][aln_len] = 0; /* fprintf(stdout,"IN msa to ALIGNED: %s %d\n", out[i], msa->alnlen); */ } /* /\* msa->alnlen = aln_len; *\/ */ /* /\* msa->aligned = ALN_STATUS_FINAL; *\/ */ /* /\* int aln_len = 0; *\/ */ /* for (int j = 0; j <= msa->sequences[0]->len;j++){ */ /* aln_len += msa->sequences[0]->gaps[j]; */ /* } */ /* aln_len += msa->sequences[0]->len; */ /* aln_len += 1; */ /* MMALLOC(out, sizeof(char*) * numseq); */ /* for(int i = 0; i < numseq; i++){ */ /* out[i] = 0; */ /* MMALLOC(out[i], sizeof(char) * (uint64_t)aln_len); */ /* int pos = 0; */ /* for(int j = 0;j < msa->sequences[i]->len;j++){ */ /* for(int c = 0;c < msa->sequences[i]->gaps[j];c++){ */ /* out[i][pos] = '-'; */ /* pos++; */ /* } */ /* out[i][pos] = msa->sequences[i]->seq[j]; */ /* pos++; */ /* } */ /* for(int c = 0;c < msa->sequences[i]->gaps[ msa->sequences[i]->len];c++){ */ /* out[i][pos] = '-'; */ /* pos++; */ /* } */ /* out[i][pos] = 0; */ /* } */ *aligned = out; *out_aln_len = aln_len; return OK; ERROR: return FAIL; } int kalign_arr_to_msa(char** input_sequences, int* len, int numseq,struct msa** multiple_aln) { struct msa* msa = NULL; MMALLOC(msa, sizeof(struct msa)); msa->sequences = NULL; msa->alloc_numseq = numseq; msa->numseq = numseq; msa->num_profiles = 0; msa->L = ALPHA_UNDEFINED; msa->biotype = ALN_BIOTYPE_UNDEF; msa->aligned = 0; msa->alnlen = 0; msa->plen = NULL; msa->sip = NULL; msa->nsip = NULL; msa->seq_distances = NULL; msa->col_confidence = NULL; msa->seq_weights = NULL; msa->run_parallel = 0; msa->consistency_table = NULL; msa->quiet = 1; MMALLOC(msa->sequences, sizeof(struct msa_seq*) * msa->alloc_numseq); for(int i = 0; i < 128; i++){ msa->letter_freq[i] = 0; } for(int i = 0; i < msa->alloc_numseq;i++){ msa->sequences[i] = NULL; struct msa_seq* seq = NULL; MMALLOC(seq, sizeof(struct msa_seq)); seq->name = NULL; seq->seq = NULL; seq->s = NULL; seq->gaps = NULL; seq->confidence = NULL; seq->rank = 0; seq->len = len[i]; seq->alloc_len = len[i]+1; MMALLOC(seq->name, sizeof(char)* MSA_NAME_LEN); MMALLOC(seq->seq, sizeof(char) * seq->alloc_len); MMALLOC(seq->s, sizeof(uint8_t) * seq->alloc_len); MMALLOC(seq->gaps, sizeof(int) * (seq->alloc_len+1)); for(int j = 0;j < seq->alloc_len+1;j++){ seq->gaps[j] = 0; } for(int j = 0; j < len[i];j++){ msa->letter_freq[(int)input_sequences[i][j]]++; seq->seq[j] = input_sequences[i][j]; } seq->seq[len[i]] = 0; msa->sequences[i] = seq; /* LOG_MSG("%s",msa->sequences[i]->seq); */ } RUN(detect_alphabet(msa)); RUN(detect_aligned(msa)); RUN(set_sip_nsip(msa)); *multiple_aln = msa; return OK; ERROR: kalign_free_msa(msa); return FAIL; } static int aln_unknown_warning_message_gaps_but_len_diff(struct msa* msa) { int i; WARNING_MSG("--------------------------------------------"); WARNING_MSG("The input sequences contain gap characters: "); for(i = 0; i < 128;i++){ if(msa->letter_freq[i] && ispunct(i)){ WARNING_MSG("\"%c\" : %4d found ", (char)i,msa->letter_freq[i] ); } } WARNING_MSG("BUT the presumably aligned sequences do not "); WARNING_MSG("have the same length. "); WARNING_MSG(" "); WARNING_MSG("Kalign will remove the gap characters and "); WARNING_MSG("align the sequences. "); WARNING_MSG("--------------------------------------------"); return OK; } static int aln_unknown_warning_message_same_len_no_gaps(void) { /* int i; */ WARNING_MSG("--------------------------------------------"); WARNING_MSG("All input sequences have the same length. "); WARNING_MSG("BUT there are no gap characters. "); WARNING_MSG(" "); WARNING_MSG("Unable to determine whether the sequences "); WARNING_MSG("are already aligned. "); WARNING_MSG("Kalign will align the sequences. "); WARNING_MSG("--------------------------------------------"); return OK; } int finalise_alignment(struct msa* msa) { ASSERT(msa->aligned == ALN_STATUS_ALIGNED, "Sequences are not aligned"); struct msa_seq* seq = NULL; char* linear_seq = NULL; int aln_len = 0; for(int i = 0; i <= msa->sequences[0]->len;i++){ aln_len += msa->sequences[0]->gaps[i]; } aln_len += msa->sequences[0]->len; for(int i = 0; i < msa->numseq;i++){ MMALLOC(linear_seq, sizeof(char)* (aln_len+1)); seq = msa->sequences[i]; RUN(make_linear_sequence(seq,linear_seq)); MFREE(seq->seq); seq->seq = linear_seq; /* seq->len = aln_len; */ linear_seq = NULL; } msa->alnlen = aln_len; msa->aligned = ALN_STATUS_FINAL; return OK; ERROR: return FAIL; } int make_linear_sequence(struct msa_seq* seq, char* linear_seq) { int c,j,f; f = 0; for(j = 0;j < seq->len;j++){ //LOG_MSG("%d %d",j,seq->gaps[j]); for(c = 0;c < seq->gaps[j];c++){ linear_seq[f] = '-'; f++; } //LOG_MSG("%d %d %d",j,f,seq->gaps[j]); linear_seq[f] = seq->seq[j]; f++; } for(c = 0;c < seq->gaps[ seq->len];c++){ //LOG_MSG("%d %d",j,seq->gaps[seq->len]); linear_seq[f] = '-'; f++; } linear_seq[f] = 0; ///fprintf(stdout,"LINEAR:%s\n",linear_seq); return OK; } kalign-3.5.1/lib/src/msa_op.h000066400000000000000000000021321515023132300157420ustar00rootroot00000000000000#ifndef MSA_OP_H #define MSA_OP_H #ifdef MSA_OP_IMPORT #define EXTERN #else #define EXTERN extern #endif struct msa; struct msa_seq; EXTERN int msa_seq_cpy(struct msa_seq *d, struct msa_seq *src); EXTERN int msa_cpy(struct msa** dest, struct msa* src); EXTERN int merge_msa(struct msa** dest, struct msa* src); EXTERN int dealign_msa(struct msa *msa); EXTERN int detect_alphabet(struct msa *msa); EXTERN int detect_aligned(struct msa *msa); EXTERN int set_sip_nsip(struct msa *msa); EXTERN int reformat_settings_msa(struct msa *msa, int rename, int unalign); EXTERN int convert_msa_to_internal(struct msa* msa, int type); /* convert alinged msa sequences to character array */ EXTERN int kalign_msa_to_arr(struct msa *msa, char ***aligned, int *out_aln_len); /* Used to convert sequences read by non-kalign code into the msa struct.. */ EXTERN int kalign_arr_to_msa(char **input_sequences, int *len, int numseq, struct msa **multiple_aln); EXTERN int finalise_alignment(struct msa* msa); EXTERN int make_linear_sequence(struct msa_seq *seq, char *linear_seq); #undef MSA_OP_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_sort.c000066400000000000000000000045051515023132300163140ustar00rootroot00000000000000#include "tldevel.h" #include "tlrng.h" #include "msa_struct.h" #include #define MSA_SORT_IMPORT #include "msa_sort.h" static int sort_by_len_name(const void *a, const void *b); static int sort_by_rank(const void *a, const void *b); int msa_sort_len_name(struct msa *m) { ASSERT(m != NULL, "No alignment"); qsort(m->sequences, m->numseq, sizeof(struct msa_seq*),sort_by_len_name); return OK; ERROR: return FAIL; } int msa_sort_rank(struct msa *m) { ASSERT(m != NULL, "No alignment"); /* for(int i = 0; i < m->numseq;i++){ */ /* fprintf(stdout,"%d %s\n", */ /* m->sequences[i]->rank, */ /* m->sequences[i]->name */ /* ); */ /* } */ qsort(m->sequences, m->numseq, sizeof(struct msa_seq*),sort_by_rank); /* for(int i = 0; i < m->numseq;i++){ */ /* fprintf(stdout,"%d %s - sorted\n", */ /* m->sequences[i]->rank, */ /* m->sequences[i]->name */ /* ); */ /* } */ return OK; ERROR: return FAIL; } int msa_shuffle_seq(struct msa *m, struct rng_state* rng) { int r; int i,j; struct msa_seq* tmp; int n = m->numseq; for (i = 0; i < n - 1; i++) { r = tl_random_int(rng,n); j = i + r % (n-i); tmp = m->sequences[j]; m->sequences[j] = m->sequences[i]; m->sequences[i] = tmp; } return OK; } int sort_by_len_name(const void *a, const void *b) { struct msa_seq* const *one = a; struct msa_seq* const *two = b; if((*one)->len > (*two)->len){ return -1; }else if((*one)->len == (*two)->len){ int c = strncmp((*one)->name, (*two)->name, MSA_NAME_LEN); if(c < 0){ return -1; }else{ return 1; } }else{ return 1; } } int sort_by_rank(const void *a, const void *b) { struct msa_seq* const *one = a; struct msa_seq* const *two = b; if((*one)->rank > (*two)->rank){ return 1; }else{ return -1; } } kalign-3.5.1/lib/src/msa_sort.h000066400000000000000000000005261515023132300163200ustar00rootroot00000000000000#ifndef MSA_SORT_H #define MSA_SORT_H struct msa; struct rng_state; #ifdef MSA_SORT_IMPORT #define EXTERN #else #define EXTERN extern #endif EXTERN int msa_sort_len_name(struct msa *m); EXTERN int msa_sort_rank(struct msa *m); EXTERN int msa_shuffle_seq(struct msa *m, struct rng_state* rng); #undef MSA_SORT_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/msa_struct.h000066400000000000000000000031421515023132300166520ustar00rootroot00000000000000#ifndef MSA_STRUCT_H #define MSA_STRUCT_H #include #ifdef MSA_STRUCT_IMPORT #define EXTERN #else #define EXTERN extern #endif #define MSA_NAME_LEN 256 #define ALN_STATUS_UNALIGNED 1 /* no gaps sequences may or may not have equal lengths */ #define ALN_STATUS_ALIGNED 2 /* sequences have equal lengths and may or may not contain gaps*/ #define ALN_STATUS_FINAL 3 /* sequences have equal lengths and may or may not contain gaps*/ #define ALN_STATUS_UNKNOWN 3 /* sequences have un-equal length and contain gaps */ #define ALN_BIOTYPE_PROTEIN 0 #define ALN_BIOTYPE_DNA 1 #define ALN_BIOTYPE_UNDEF 2 struct msa_seq{ char* name; char* seq; uint8_t* s; int* gaps; float* confidence; /* per-position confidence [0..1], NULL if not computed */ int rank; int len; int alloc_len; }; struct msa{ struct msa_seq** sequences; float* seq_distances; /* per-sequence mean distance (normalized), set during tree building */ float* col_confidence; /* per-column confidence [0..1], NULL if not computed */ float* seq_weights; /* per-sequence tree-based weight, NULL when not computed */ int** sip; int* nsip; int* plen; uint8_t run_parallel; int numseq; int num_profiles; int alloc_numseq; int aligned; int alnlen; int letter_freq[128]; uint8_t L; uint8_t biotype; int quiet; void* consistency_table; /* struct consistency_table*, NULL when disabled */ }; #undef MSA_STRUCT_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/pick_anchor.c000066400000000000000000000044511515023132300167450ustar00rootroot00000000000000#include "tldevel.h" #include "msa_struct.h" #define PICK_ANCHOR_IMPORT #include "pick_anchor.h" struct sort_struct{ int len; int id; }; int sort_by_len(const void *a, const void *b); int* select_seqs(struct msa* msa, int num_anchor); int* pick_anchor(struct msa* msa, int* n) { int* anchors = NULL; int num_anchor = 0; ASSERT(msa != NULL, "No alignment."); /* num_anchor = MACRO_MAX(MACRO_MIN(32, msa->numseq), (int) pow(log2((double) msa->numseq), 2.0)); */ num_anchor = MACRO_MIN(32, msa->numseq); RUNP(anchors = select_seqs(msa, num_anchor)); *n = num_anchor; return anchors; ERROR: return NULL; } int* select_seqs(struct msa* msa, int num_anchor) { struct sort_struct** seq_sort = NULL; int* anchors = NULL; int i,stride; MMALLOC(seq_sort, sizeof(struct sort_struct*) * msa->numseq); for(i = 0; i < msa->numseq;i++){ seq_sort[i] = NULL; MMALLOC(seq_sort[i], sizeof(struct sort_struct)); seq_sort[i]->id = i; seq_sort[i]->len = msa->sequences[i]->len;// aln->sl[i]; } qsort(seq_sort, msa->numseq, sizeof(struct sort_struct*),sort_by_len); /* for(i = 0; i < msa->numseq;i++){ */ /* fprintf(stdout,"%d\t%d id: %d \n", seq_sort[i]->id,seq_sort[i]->len,seq_sort[i]->id); */ /* } */ //fprintf(stdout,"%d\t seeds\n", num_anchor); MMALLOC(anchors, sizeof(int) * num_anchor); stride = msa->numseq / num_anchor; // fprintf(stdout,"%d\tstride\n", stride); //c = 0; for(i = 0; i < num_anchor;i++){ anchors[i] = seq_sort[i*stride]->id; /* LOG_MSG("Anchor: %d",anchors[i] ); */ } ASSERT(i == num_anchor,"Cound not select all anchors\tnum_anchor:%d\t numseq:%d",num_anchor,msa->numseq); for(i = 0; i < msa->numseq;i++){ MFREE(seq_sort[i]); } MFREE(seq_sort); return anchors; ERROR: return NULL; } int sort_by_len(const void *a, const void *b) { struct sort_struct* const *one = a; struct sort_struct* const *two = b; if((*one)->len > (*two)->len){ return -1; }else{ return 1; } } kalign-3.5.1/lib/src/pick_anchor.h000066400000000000000000000004451515023132300167510ustar00rootroot00000000000000#ifndef PICK_ANCHOR_H #define PICK_ANCHOR_H #ifdef PICK_ANCHOR_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; EXTERN int* pick_anchor(struct msa* msa, int* n); #undef PICK_ANCHOR_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/poar.c000066400000000000000000000226531515023132300154320ustar00rootroot00000000000000#include "tldevel.h" #include #include #include #define POAR_IMPORT #include "poar.h" static inline int pair_index(int i, int j, int numseq) { /* Flat index for pair (i,j) where i < j */ return i * numseq - (i * (i + 1)) / 2 + (j - i - 1); } static inline uint32_t pack_key(int pos_i, int pos_j) { return ((uint32_t)pos_i << 20) | (uint32_t)pos_j; } static int poar_pair_alloc(struct poar_pair** pp) { struct poar_pair* p = NULL; MMALLOC(p, sizeof(struct poar_pair)); p->entries = NULL; p->n_entries = 0; p->alloc_entries = 64; MMALLOC(p->entries, sizeof(struct poar_entry) * p->alloc_entries); *pp = p; return OK; ERROR: return FAIL; } static void poar_pair_free(struct poar_pair* p) { if(p){ if(p->entries){ MFREE(p->entries); } MFREE(p); } } /* Insert key into sorted array, or update support if already present. */ static int poar_pair_insert(struct poar_pair* p, uint32_t key, int aln_idx) { /* Find insertion point */ int lo = 0; int hi = p->n_entries; while(lo < hi){ int mid = lo + (hi - lo) / 2; if(p->entries[mid].key < key){ lo = mid + 1; }else if(p->entries[mid].key == key){ /* Already exists, set bit */ p->entries[mid].support |= (1u << aln_idx); return OK; }else{ hi = mid; } } /* Need to insert at position lo */ if(p->n_entries >= p->alloc_entries){ p->alloc_entries *= 2; MREALLOC(p->entries, sizeof(struct poar_entry) * p->alloc_entries); } /* Shift entries right */ if(lo < p->n_entries){ memmove(&p->entries[lo + 1], &p->entries[lo], sizeof(struct poar_entry) * (p->n_entries - lo)); } p->entries[lo].key = key; p->entries[lo].support = (1u << aln_idx); p->n_entries++; return OK; ERROR: return FAIL; } int poar_table_alloc(struct poar_table** table, int numseq) { struct poar_table* t = NULL; int n_pairs; int i; MMALLOC(t, sizeof(struct poar_table)); t->pairs = NULL; t->numseq = numseq; t->n_alignments = 0; n_pairs = numseq * (numseq - 1) / 2; t->n_pairs = n_pairs; MMALLOC(t->pairs, sizeof(struct poar_pair*) * n_pairs); for(i = 0; i < n_pairs; i++){ t->pairs[i] = NULL; RUN(poar_pair_alloc(&t->pairs[i])); } *table = t; return OK; ERROR: poar_table_free(t); return FAIL; } void poar_table_free(struct poar_table* table) { if(table){ if(table->pairs){ for(int i = 0; i < table->n_pairs; i++){ poar_pair_free(table->pairs[i]); } MFREE(table->pairs); } MFREE(table); } } int pos_matrix_from_msa(struct pos_matrix** pm, char** seqs, int numseq, int alnlen) { struct pos_matrix* m = NULL; int i, j; MMALLOC(m, sizeof(struct pos_matrix)); m->col_to_res = NULL; m->numseq = numseq; m->alnlen = alnlen; MMALLOC(m->col_to_res, sizeof(int*) * numseq); for(i = 0; i < numseq; i++){ m->col_to_res[i] = NULL; MMALLOC(m->col_to_res[i], sizeof(int) * alnlen); int res_pos = -1; for(j = 0; j < alnlen; j++){ if(isalpha((int)seqs[i][j])){ res_pos++; m->col_to_res[i][j] = res_pos; }else{ m->col_to_res[i][j] = -1; } } } *pm = m; return OK; ERROR: pos_matrix_free(m); return FAIL; } void pos_matrix_free(struct pos_matrix* pm) { if(pm){ if(pm->col_to_res){ for(int i = 0; i < pm->numseq; i++){ if(pm->col_to_res[i]){ MFREE(pm->col_to_res[i]); } } MFREE(pm->col_to_res); } MFREE(pm); } } int extract_poars(struct poar_table* table, struct pos_matrix* pm, int aln_idx) { int i, j, col; int numseq = pm->numseq; int alnlen = pm->alnlen; ASSERT(aln_idx < 32, "Maximum 32 alignments supported in ensemble"); for(i = 0; i < numseq - 1; i++){ for(j = i + 1; j < numseq; j++){ int pidx = pair_index(i, j, numseq); struct poar_pair* pp = table->pairs[pidx]; for(col = 0; col < alnlen; col++){ int ri = pm->col_to_res[i][col]; int rj = pm->col_to_res[j][col]; if(ri >= 0 && rj >= 0){ uint32_t key = pack_key(ri, rj); RUN(poar_pair_insert(pp, key, aln_idx)); } } } } if(aln_idx >= table->n_alignments){ table->n_alignments = aln_idx + 1; } return OK; ERROR: return FAIL; } /* Binary POAR file format: 4 bytes: "POAR" magic 4 bytes: version (1) 4 bytes: numseq 4 bytes: n_alignments For each pair (n_pairs = numseq*(numseq-1)/2): 4 bytes: n_entries n_entries * 8 bytes: (key, support) pairs */ #define POAR_MAGIC 0x524F4150 /* "POAR" in little-endian */ #define POAR_VERSION 1 int poar_table_write(struct poar_table* table, const char* path) { FILE* fp = NULL; uint32_t magic = POAR_MAGIC; uint32_t version = POAR_VERSION; uint32_t numseq = (uint32_t)table->numseq; uint32_t n_alignments = (uint32_t)table->n_alignments; int i; ASSERT(table != NULL, "No POAR table"); ASSERT(path != NULL, "No output path"); fp = fopen(path, "wb"); if(!fp){ ERROR_MSG("Cannot open %s for writing", path); } fwrite(&magic, 4, 1, fp); fwrite(&version, 4, 1, fp); fwrite(&numseq, 4, 1, fp); fwrite(&n_alignments, 4, 1, fp); for(i = 0; i < table->n_pairs; i++){ struct poar_pair* pp = table->pairs[i]; uint32_t n_entries = (uint32_t)pp->n_entries; fwrite(&n_entries, 4, 1, fp); if(n_entries > 0){ fwrite(pp->entries, sizeof(struct poar_entry), n_entries, fp); } } fclose(fp); return OK; ERROR: if(fp) fclose(fp); return FAIL; } int poar_table_read(struct poar_table** out_table, const char* path) { FILE* fp = NULL; struct poar_table* t = NULL; uint32_t magic, version, numseq, n_alignments; int i; ASSERT(path != NULL, "No input path"); fp = fopen(path, "rb"); if(!fp){ ERROR_MSG("Cannot open %s for reading", path); } if(fread(&magic, 4, 1, fp) != 1 || magic != POAR_MAGIC){ ERROR_MSG("Invalid POAR file magic in %s", path); } if(fread(&version, 4, 1, fp) != 1 || version != POAR_VERSION){ ERROR_MSG("Unsupported POAR file version %u in %s", version, path); } if(fread(&numseq, 4, 1, fp) != 1){ ERROR_MSG("Failed to read numseq from %s", path); } if(fread(&n_alignments, 4, 1, fp) != 1){ ERROR_MSG("Failed to read n_alignments from %s", path); } MMALLOC(t, sizeof(struct poar_table)); t->pairs = NULL; t->numseq = (int)numseq; t->n_alignments = (int)n_alignments; t->n_pairs = (int)(numseq * (numseq - 1) / 2); MMALLOC(t->pairs, sizeof(struct poar_pair*) * t->n_pairs); for(i = 0; i < t->n_pairs; i++){ t->pairs[i] = NULL; } for(i = 0; i < t->n_pairs; i++){ uint32_t n_entries; struct poar_pair* pp = NULL; if(fread(&n_entries, 4, 1, fp) != 1){ ERROR_MSG("Failed to read pair %d entries count", i); } MMALLOC(pp, sizeof(struct poar_pair)); pp->entries = NULL; pp->n_entries = (int)n_entries; pp->alloc_entries = n_entries > 0 ? (int)n_entries : 1; MMALLOC(pp->entries, sizeof(struct poar_entry) * pp->alloc_entries); if(n_entries > 0){ if(fread(pp->entries, sizeof(struct poar_entry), n_entries, fp) != n_entries){ MFREE(pp->entries); MFREE(pp); ERROR_MSG("Failed to read pair %d entries", i); } } t->pairs[i] = pp; } fclose(fp); *out_table = t; return OK; ERROR: if(fp) fclose(fp); if(t) poar_table_free(t); return FAIL; } kalign-3.5.1/lib/src/poar.h000066400000000000000000000030001515023132300154200ustar00rootroot00000000000000#ifndef POAR_H #define POAR_H #ifdef POAR_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #include /* Position matrix: maps (seq, col) -> residue position or -1 (gap) */ struct pos_matrix { int** col_to_res; /* [seq][col] -> residue pos or -1 */ int numseq; int alnlen; }; /* POAR entry: packed (pos_i, pos_j) key + support bitmask */ struct poar_entry { uint32_t key; /* (pos_i << 20) | pos_j */ uint32_t support; /* bitmask: bit k set if alignment k has this POAR */ }; /* One sorted array per sequence pair */ struct poar_pair { struct poar_entry* entries; int n_entries; int alloc_entries; }; /* Table of all POAR pairs */ struct poar_table { struct poar_pair** pairs; /* indexed by flattened (i,j) pair */ int n_pairs; /* numseq*(numseq-1)/2 */ int numseq; int n_alignments; }; EXTERN int poar_table_alloc(struct poar_table** table, int numseq); EXTERN void poar_table_free(struct poar_table* table); EXTERN int pos_matrix_from_msa(struct pos_matrix** pm, char** seqs, int numseq, int alnlen); EXTERN void pos_matrix_free(struct pos_matrix* pm); EXTERN int extract_poars(struct poar_table* table, struct pos_matrix* pm, int aln_idx); EXTERN int poar_table_write(struct poar_table* table, const char* path); EXTERN int poar_table_read(struct poar_table** table, const char* path); #undef POAR_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/sequence_distance.c000066400000000000000000000315551515023132300201540ustar00rootroot00000000000000#include "tldevel.h" #include #ifdef HAVE_AVX2 #include #include #endif #include "msa_struct.h" #define SEQUENCE_DISTANCE_IMPORT #include "sequence_distance.h" /* #include "alphabet.h" */ /* #include "alignment.h" */ /* #include "align_io.h" */ /* #include "misc.h" */ #include "bpm.h" #define NODESIZE 16 /* small hash implementation */ struct bignode{ struct bignode *next; unsigned int pos[NODESIZE]; unsigned int num; }; struct bignode* big_insert_hash(struct bignode *n,const unsigned int pos); void big_remove_nodes(struct bignode *n); void big_print_nodes(struct bignode *n); float dna_distance_calculation(struct bignode* hash[],const uint8_t * p,const int seqlen,int diagonals,float mode); float protein_wu_distance_calculation(struct bignode* hash[],const uint8_t* seq,const int seqlen,const int diagonals,const float mode); float** d_estimation(struct msa* msa, int* samples, int num_samples,int pair) { float** dm = NULL; uint8_t* seq_a; uint8_t* seq_b; float dist; int len_a; int len_b; int i,j; #if HAVE_AVX2 set_broadcast_mask(); #endif if(pair){ RUN(galloc(&dm,num_samples,num_samples)); for(i = 0; i < num_samples;i++){ seq_a = msa->sequences[samples[i]]->s;// aln->s[samples[i]]; len_a = msa->sequences[samples[i]]->len;//aln->sl[samples[i]]; for(j = 0;j < num_samples;j++){ //fprintf(stdout, "Working on %d %d\n", i,j); seq_b = msa->sequences[samples[j]]->s; //aln->s[ samples[j]]; len_b = msa->sequences[samples[j]]->len;//aln->sl[selection[j]]; /*dm[i][j] = MACRO_MIN(len_a, len_b) - MACRO_MIN( bpm_256(seq_a, seq_b, len_a, len_b), bpm_256(seq_b, seq_a, len_b, len_a) );*/ dist = calc_distance(seq_a, seq_b, len_a, len_b); /* give shorter sequences a preference */ int s = (len_a + len_b) / 2; float add = MACRO_MIN(10000.0, s) / 10000.0; dist += add; /* fprintf(stdout,"%f\n", add * 1000.0); */ //dist = dist / (float) MACRO_MIN(len_a, len_b); dm[i][j] = dist;// + (float)( i * num_samples + j) / (float) ( num_samples * num_samples); dm[j][i] = dm[i][j]; } /* fprintf(stdout,"\n"); */ } /* fprintf(stdout,"\n"); */ }else{ int a; int numseq = msa->numseq; MMALLOC(dm, sizeof(float*)* numseq); //fprintf(stdout,"MASK: %lx\n", mask); a = num_samples / 8; if( num_samples%8){ a++; } a = a << 3; for(i = 0; i < numseq;i++){ dm[i] = NULL; #ifdef HAVE_AVX2 dm[i] = _mm_malloc(sizeof(float) * a,32); #else MMALLOC(dm[i], sizeof(float) *a); #endif for(j = 0; j < a;j++){ dm[i][j] = 0.0F; } } struct msa_seq** s = msa->sequences; #ifdef HAVE_OPENMP #pragma omp parallel for shared(dm, s) private(i, j) collapse(2) schedule(static) #endif for(i = 0; i < numseq;i++){ for(j = 0;j < num_samples;j++){ uint8_t* s1; uint8_t* s2; int l1; int l2; s1 = s[i]->s; l1 = s[i]->len; s2 = s[samples[j]]->s; l2 = s[samples[j]]->len; dm[i][j] = calc_distance(s1,s2,l1,l2); int s = (l1 + l2) / 2; float add = MACRO_MIN(10000.0, s) / 10000.0; dm[i][j] += add; /* fprintf(stdout,"%f ",dm[i][j]); */ /* dm[i][j] += (float)MACRO_MIN(l1, l2) / (float)MACRO_MAX(l1, l2); */ /* dm[i][j] = dm[i][j] / (float) MACRO_MIN(l1, l2); */ //dm[i][j] = dist; } /* fprintf(stdout,"\n"); */ } /* fprintf(stdout,"\n"); */ /* for(i = 0; i < numseq;i++){ */ /* seq_a = msa->sequences[i]->s;// aln->s[i]; */ /* len_a = msa->sequences[i]->len;// aln->sl[i]; */ /* for(j = 0;j < num_samples;j++){ */ /* seq_b = msa->sequences[samples[j]]->s;// aln->s[ seeds[j]]; */ /* len_b = msa->sequences[samples[j]]->len;// aln->sl[seeds[j]]; */ /* dist = calc_distance(seq_a, seq_b, len_a, len_b,msa->L); */ /* dm[i][j] = dist; */ /* } */ /* } */ } return dm; ERROR: return NULL; } float calc_distance(uint8_t* seq_a, uint8_t* seq_b, int len_a,int len_b) { uint32_t dist; if(len_a > len_b){ dist = BPM(seq_a, seq_b, len_a, len_b); }else{ dist = BPM(seq_b, seq_a, len_b, len_a); } return (float)dist; } float protein_wu_distance_calculation(struct bignode* hash[],const uint8_t* seq,const int seqlen,const int diagonals,const float mode) { struct bignode* node_p; unsigned int* d = NULL; unsigned int* tmp = NULL; float out = 0.0; register int i,j; register int c; register int num; register unsigned int hv; d = malloc(sizeof(unsigned int)*diagonals); if(d == NULL) return 0.0f; //for (i = diagonals;i--;){ for (i = 0;i < diagonals;i++){ d[i] = 0; } for (i = seqlen-2;i--;){ //for(i = 0; i < seqlen-2;i++){ /*hv = (seq[i+1] << 5) + seq[i+2]; node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; }*/ hv = (seq[i] << 5) + seq[i+1]; //printf("3:%d\n",hv); node_p = hash[hv]; while(node_p){ tmp = node_p->pos; num = node_p->num; for(j = 0;j < num;j++){ c = tmp[j]; d[c]++; c++; d[c]++; } node_p = node_p->next; } hv = (seq[i] << 5) + seq[i+2]; node_p = hash[hv]; while(node_p){ tmp = node_p->pos; num = node_p->num; for(j = 0;j < num;j++){ c = tmp[j]; d[c]++; } node_p = node_p->next; } d++; } //exit(0); d -= (seqlen-2); //unsigned int max = 0.0; for (i = diagonals;i--;){ // if(d[i] > max){ // max = d[i]; //} //d[i] /= minlen; //fprintf(stderr,"%d ",d[i]); if(d[i] > mode){ out += d[i]; // printf("%f %d\n",d[i]/ minlen,d[i]); } } free(d); //return out; return out; } float dna_distance_calculation(struct bignode* hash[],const uint8_t * p,const int seqlen,int diagonals,float mode) { struct bignode* node_p; float out = 0.0; unsigned int* tmp = NULL; unsigned int* d = NULL; int i,j; unsigned int hv; d = malloc(sizeof(unsigned int)*diagonals); if(d == NULL) return 0.0f; for (i = 0;i < diagonals;i++){ d[i] = 0; } for (i = seqlen-5;i--;){ hv = ((p[i]&3)<<8) + ((p[i+1]&3)<<6) + ((p[i+2]&3)<<4) + ((p[i+3]&3)<<2) + (p[i+4]&3);//ABCDE if (hash[hv]){ node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < (int) node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; } } hv = ((p[i]&3)<<8) + ((p[i+1]&3)<<6) + ((p[i+2]&3)<<4) + ((p[i+3]&3)<<2) + (p[i+5]&3);//ABCDF if (hash[hv]){ node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < (int)node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; } } hv = ((p[i]&3)<<8) + ((p[i+1]&3)<<6) + ((p[i+2]&3)<<4) + ((p[i+4]&3)<<2) + (p[i+5]&3);//ABCEF if (hash[hv]){ node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < (int)node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; } } hv = ((p[i]&3)<<8) + ((p[i+1]&3)<<6) + ((p[i+3]&3)<<4) + ((p[i+4]&3)<<2) + (p[i+5]&3);//ABDEF if (hash[hv]){ node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < (int)node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; } } hv = ((p[i]&3)<<8) + ((p[i+2]&3)<<6) + ((p[i+3]&3)<<4) + ((p[i+4]&3)<<2) + (p[i+5]&3);//ACDEF if (hash[hv]){ node_p = hash[hv]; while(node_p){ tmp = node_p->pos; for(j = 0;j < (int)node_p->num;j++){ d[tmp[j]]++; } node_p = node_p->next; } } d++; } //exit(0); d -= (seqlen-5); for (i = diagonals;i--;){ //d[i] /= minlen; //printf("%d ",d[i]); if(d[i] > mode){ //fprintf(stderr,"%f %d\n",d[i]/ minlen,d[i]); out += d[i]; } } free(d); return out; } struct bignode* big_insert_hash(struct bignode *n,const unsigned int pos) { struct bignode* p = NULL; if(n){ if(n->num < NODESIZE){ n->pos[n->num] = pos; n->num++; return n; }else{ MMALLOC(p, sizeof(struct bignode)); p->pos[0] = pos; p->num = 1; p->next = n; } }else{ MMALLOC(p, sizeof(struct bignode)); p->pos[0] = pos; p->num = 1; p->next = n; } return p; ERROR: return NULL; } void big_remove_nodes(struct bignode *n) { struct bignode* p = NULL; while(n){ p = n; n = n->next; MFREE(p); } } void big_print_nodes(struct bignode *n) { int i; while(n){ for (i = 0; i < (int)n->num;i++){ fprintf(stderr,"%d ",n->pos[i]); } n = n->next; } } kalign-3.5.1/lib/src/sequence_distance.h000066400000000000000000000010161515023132300201460ustar00rootroot00000000000000#ifndef SEQUENCE_DISTANCE_H #define SEQUENCE_DISTANCE_H #include #ifdef SEQUENCE_DISTANCE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; /* #include "alignment_parameters.h" */ EXTERN float** d_estimation(struct msa* msa, int* samples, int num_samples,int pair); EXTERN float calc_distance(uint8_t *seq_a, uint8_t *seq_b, int len_a, int len_b); #undef SEQUENCE_DISTANCE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/sp_score.c000066400000000000000000000170141515023132300163010ustar00rootroot00000000000000#include "tldevel.h" #include #include #include "msa_struct.h" #include "aln_param.h" #define SP_SCORE_IMPORT #include "sp_score.h" #define SP_ALPHA 23 /* Build a frequency profile from a group of sequences. For each profile column, counts residues (0..SP_ALPHA-1) and gaps. freq: pre-allocated array of prof_len * SP_ALPHA ints (zeroed) n_gap: pre-allocated array of prof_len ints (zeroed) Sequences are expanded one at a time via gap arrays to avoid O(N * L) simultaneous memory allocation. */ static int build_profile(struct msa* msa, int* sip, int nsip, int prof_len, int* freq, int* n_gap) { int i, j, k, pos, si; int8_t* cols = NULL; for(i = 0; i < nsip; i++){ si = sip[i]; /* Expand gap array into column array */ MMALLOC(cols, sizeof(int8_t) * prof_len); pos = 0; for(j = 0; j < msa->sequences[si]->len; j++){ for(k = 0; k < msa->sequences[si]->gaps[j]; k++){ cols[pos++] = -1; } cols[pos++] = (int8_t)msa->sequences[si]->s[j]; } for(k = 0; k < msa->sequences[si]->gaps[msa->sequences[si]->len]; k++){ cols[pos++] = -1; } /* Accumulate into frequency profile */ for(j = 0; j < prof_len; j++){ if(cols[j] >= 0 && cols[j] < SP_ALPHA){ freq[j * SP_ALPHA + (int)cols[j]]++; }else{ n_gap[j]++; } } MFREE(cols); cols = NULL; } return OK; ERROR: if(cols) MFREE(cols); return FAIL; } /* Compute profile-based SP score for cross-group sequence pairs. Instead of iterating all O(|A|*|B|) pairs, builds residue frequency profiles for each group and computes the expected score analytically: substitution: sum_r sum_s freq_a[r] * freq_b[s] * subm[r][s] gap penalty: gpo at path-level gap opens, gpe/tgpe per gap-residue pair The substitution component is exact (cross-group pairs are independent). Gap-open penalties are tracked at the path level (gap-in-A/B runs) which captures the dominant inter-group gap structure. Internal gap opens within profiles are not tracked (would require O(N^2) per-pair state). Complexity: O((|A|+|B|) * L) for profile construction + O(SP_ALPHA^2 * path_len) for scoring. */ int compute_sp_score(struct msa* msa, struct aln_param* ap, int* path, int* sip_a, int nsip_a, int* sip_b, int nsip_b, float* score) { int* freq_a = NULL; int* freq_b = NULL; int* gap_a = NULL; int* gap_b = NULL; int path_len; int i, j, c, si; int pos_a, pos_b; float total = 0.0F; const float gpo = ap->gpo; const float gpe = ap->gpe; const float tgpe = ap->tgpe; float** subm = ap->subm; path_len = path[0]; /* Compute profile lengths from first sequence in each group */ si = sip_a[0]; int prof_a_len = msa->sequences[si]->len; for(i = 0; i <= msa->sequences[si]->len; i++){ prof_a_len += msa->sequences[si]->gaps[i]; } si = sip_b[0]; int prof_b_len = msa->sequences[si]->len; for(i = 0; i <= msa->sequences[si]->len; i++){ prof_b_len += msa->sequences[si]->gaps[i]; } /* Allocate and build frequency profiles */ MMALLOC(freq_a, sizeof(int) * prof_a_len * SP_ALPHA); MMALLOC(gap_a, sizeof(int) * prof_a_len); memset(freq_a, 0, sizeof(int) * prof_a_len * SP_ALPHA); memset(gap_a, 0, sizeof(int) * prof_a_len); RUN(build_profile(msa, sip_a, nsip_a, prof_a_len, freq_a, gap_a)); MMALLOC(freq_b, sizeof(int) * prof_b_len * SP_ALPHA); MMALLOC(gap_b, sizeof(int) * prof_b_len); memset(freq_b, 0, sizeof(int) * prof_b_len * SP_ALPHA); memset(gap_b, 0, sizeof(int) * prof_b_len); RUN(build_profile(msa, sip_b, nsip_b, prof_b_len, freq_b, gap_b)); /* Walk the path and score using profiles. Track path-level gap runs for gap-open penalties. */ pos_a = 0; pos_b = 0; int in_a_gap = 0; /* currently in a gap-in-A run */ int in_b_gap = 0; /* currently in a gap-in-B run */ for(c = 1; c <= path_len; c++){ int step = path[c] & 3; int is_terminal = path[c] & 32; float pen = is_terminal ? tgpe : gpe; if(step == 0){ /* Match: both profiles advance */ int* fa = freq_a + pos_a * SP_ALPHA; int* fb = freq_b + pos_b * SP_ALPHA; /* Substitution: exact cross-group sum */ for(i = 0; i < SP_ALPHA; i++){ if(fa[i] == 0) continue; for(j = 0; j < SP_ALPHA; j++){ if(fb[j] == 0) continue; total += (float)(fa[i] * fb[j]) * subm[i][j]; } } /* Gap penalty: residue-gap cross pairs (gpe only, internal gap opens not tracked) */ int n_res_a = nsip_a - gap_a[pos_a]; int n_gap_b = gap_b[pos_b]; int n_gap_a = gap_a[pos_a]; int n_res_b = nsip_b - gap_b[pos_b]; total -= (float)(n_res_a * n_gap_b + n_gap_a * n_res_b) * pen; in_a_gap = 0; in_b_gap = 0; pos_a++; pos_b++; }else if(step == 1){ /* Gap in A: only B advances. All A sequences are gapped at the path level. Only B sequences with residues contribute penalty. */ int n_res_b = nsip_b - gap_b[pos_b]; int n_pairs = nsip_a * n_res_b; if(!in_a_gap){ total -= (float)n_pairs * gpo; } total -= (float)n_pairs * pen; in_a_gap = 1; in_b_gap = 0; pos_b++; }else if(step == 2){ /* Gap in B: only A advances. All B sequences are gapped at the path level. Only A sequences with residues contribute penalty. */ int n_res_a = nsip_a - gap_a[pos_a]; int n_pairs = n_res_a * nsip_b; if(!in_b_gap){ total -= (float)n_pairs * gpo; } total -= (float)n_pairs * pen; in_a_gap = 0; in_b_gap = 1; pos_a++; } } *score = total; MFREE(freq_a); MFREE(gap_a); MFREE(freq_b); MFREE(gap_b); return OK; ERROR: if(freq_a) MFREE(freq_a); if(gap_a) MFREE(gap_a); if(freq_b) MFREE(freq_b); if(gap_b) MFREE(gap_b); return FAIL; } kalign-3.5.1/lib/src/sp_score.h000066400000000000000000000006751515023132300163130ustar00rootroot00000000000000#ifndef SP_SCORE_H #define SP_SCORE_H #ifdef SP_SCORE_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; struct aln_param; EXTERN int compute_sp_score(struct msa* msa, struct aln_param* ap, int* path, int* sip_a, int nsip_a, int* sip_b, int nsip_b, float* score); #undef SP_SCORE_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/task.c000066400000000000000000000117471515023132300154350ustar00rootroot00000000000000#include "tldevel.h" #define ALN_TASK_IMPORT #include "task.h" static int sort_tasks_by_priority(const void *a, const void *b); static int sort_tasks_by_c(const void *a, const void *b); #ifdef TASKWRITETEST #include "tlrng.h" int main(void) { struct rng_state* rng = NULL; struct aln_tasks *t = NULL; rng = init_rng(0); int n_tasks = 54; alloc_tasks(&t, n_tasks); for(int i = 0; i < n_tasks;i++){ t->list[i]->score = 0.0; t->list[i]->a = tl_random_int(rng, 1000); t->list[i]->b = tl_random_int(rng, 1000); t->list[i]->c = tl_random_int(rng, 1000); t->list[i]->p = tl_random_int(rng, 1000); t->list[i]->n = tl_random_int(rng, 1000); t->n_tasks++; } RUN(write_tasks(t, "task_write_test.txt")); free_tasks(t); t = 0; RUN(read_tasks(&t, "task_write_test.txt" )); for(int i = 0; i < t->n_tasks;i++){ struct task* a = t->list[i]; fprintf(stdout,"%d %d %d %d %d\n",a->a,a->b,a->c,a->p,a->n); } free_tasks(t); free_rng(rng); return EXIT_SUCCESS; ERROR: if(t){ free_tasks(t); } if(rng){ free_rng(rng); } return EXIT_FAILURE; } #endif int write_tasks(struct aln_tasks *t, char *filename) { FILE* f_ptr = NULL; RUNP(f_ptr = fopen(filename, "w")); fprintf(f_ptr,"%d\n", t->n_tasks); for(int i = 0; i < t->n_tasks;i++){ struct task* a = t->list[i]; fprintf(f_ptr,"%d,%d,%d,%d,%d\n",a->a,a->b,a->c,a->p,a->n); } fclose(f_ptr); return OK; ERROR: if(f_ptr){ fclose(f_ptr); } return FAIL; } int read_tasks(struct aln_tasks **tasks, char *filename) { struct aln_tasks *t = NULL; FILE* f_ptr = NULL; RUNP(f_ptr = fopen(filename, "r")); int n_tasks = 0; fscanf( f_ptr, "%d", &n_tasks); RUN(alloc_tasks(&t, n_tasks)); for(int i = 0; i < n_tasks;i++){ struct task* a = t->list[i]; fscanf(f_ptr,"%d,%d,%d,%d,%d\n",&a->a,&a->b,&a->c,&a->p,&a->n); t->n_tasks++; } fclose(f_ptr); *tasks = t; return OK; ERROR: if(f_ptr){ fclose(f_ptr); } return FAIL; } int sort_tasks(struct aln_tasks* t , int order) { ASSERT(t != NULL, "No tasks"); ASSERT(t->n_tasks != 0, "No tasks"); switch (order) { case TASK_ORDER_PRIORITY: { qsort(t->list, t->n_tasks, sizeof(struct task*), sort_tasks_by_priority); break; } case TASK_ORDER_TREE: { qsort(t->list, t->n_tasks, sizeof(struct task*), sort_tasks_by_c); break; } default: ERROR_MSG("Task ordering %d not recognised.", order); break; } return OK; ERROR: return FAIL; } int sort_tasks_by_priority(const void *a, const void *b) { struct task* const *one = a; struct task* const *two = b; if((*one)->p >= (*two)->p){ return 1; }else{ return -1; } } int sort_tasks_by_c(const void *a, const void *b) { struct task* const *one = a; struct task* const *two = b; if((*one)->c >= (*two)->c){ return 1; }else{ return -1; } } int alloc_tasks(struct aln_tasks** tasks,int numseq) { struct aln_tasks* t = NULL; int np; int i; MMALLOC(t, sizeof(struct aln_tasks)); t->n_tasks = 0; t->n_alloc_tasks = numseq; t->list = NULL; t->profile = NULL; np = (numseq << 1) - 1; MMALLOC(t->profile,sizeof(float*)*np); for(i = 0; i < np;i++){ t->profile[i] = NULL; } MMALLOC(t->list, sizeof(struct task*) * t->n_alloc_tasks); for(i = 0; i < t->n_alloc_tasks;i++){ t->list[i] = NULL; MMALLOC(t->list[i], sizeof(struct task)); t->list[i]->confidence = 0.0F; } *tasks = t; return OK; ERROR: free_tasks(t); return FAIL; } void free_tasks(struct aln_tasks* t) { if(t){ for(int i = 0; i < t->n_alloc_tasks;i++){ MFREE(t->list[i]); } if(t->profile){ int np = t->n_alloc_tasks; np = (np << 1) - 1; for(int i = 0; i < np;i++){ if(t->profile[i]){ MFREE(t->profile[i]); } } MFREE(t->profile); } MFREE(t->list); MFREE(t); } } kalign-3.5.1/lib/src/task.h000066400000000000000000000023431515023132300154320ustar00rootroot00000000000000#ifndef ALN_TASK_H #define ALN_TASK_H #ifdef TASK_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct task{ float score; /* score of output alignment */ float confidence; /* average meetup margin (alignment confidence) */ int a; /* input 1 */ int b; /* input 2 */ int c; /* output */ int p; /* priority */ int n; /* amount of work */ }; struct aln_tasks{ struct task** list; /* list of pairwise alignments and their priority */ float** profile; /* buffer to hold output profiles */ /* int** map; /\* traceback paths *\/ */ int n_tasks; int n_alloc_tasks; }; #define TASK_ORDER_PRIORITY 1 #define TASK_ORDER_TREE 2 EXTERN int sort_tasks(struct aln_tasks* t , int order); EXTERN int alloc_tasks(struct aln_tasks** tasks,int numseq); EXTERN void free_tasks(struct aln_tasks* tasks); EXTERN int write_tasks(struct aln_tasks *t, char *filename); EXTERN int read_tasks(struct aln_tasks **tasks,char* filename); #undef TASK_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/test.c000066400000000000000000000003501515023132300154360ustar00rootroot00000000000000 #include "stdio.h" #include "test.h" /* #include "version.h" */ int add(int a, int b) { return sub(a,b);//a + b; } int sub(int a, int b) { fprintf(stdout," %s\n", KALIGN_PACKAGE_VERSION); return a -b; } kalign-3.5.1/lib/src/test.h000066400000000000000000000000751515023132300154470ustar00rootroot00000000000000#ifndef TEST_H #define TEST_H int sub(int a, int b); #endif kalign-3.5.1/lib/src/tldevel.c000066400000000000000000000324421515023132300161250ustar00rootroot00000000000000#include #include #include #define TLDEVEL_IMPORT #include "tldevel.h" #define TYPE_MARGIN 8 static void verror(FILE* f_ptr, const char *location, const char *format, va_list argp); static void vwarning(FILE* f_ptr,const char *location, const char *format, va_list argp); static void vlog(FILE* f_ptr,const char *format, va_list argp); static int get_time(char* time_ptr, int size); typedef struct { int dim1; int dim2; } mem_i; const char* tldevel_version(void) { return TLDEVEL_VERSION; } #define UNUSED(expr) do { (void)(expr); } while (0) int galloc_unknown_type_error (void* p, ...) { UNUSED(p); error(AT, "galloc was called with pointer of unknown type"); return FAIL; } int galloc_too_few_arg_error (void* p) { UNUSED(p); error(AT,"galloc was called with only one argument"); return FAIL; } #undef UNUSED /* g memory stuff */ int get_dim1(void* ptr,int* d) { if(ptr){ *d = ((mem_i*)((void*) ((char*)ptr - sizeof(mem_i))))->dim1; return OK; } return FAIL; } int get_dim2(void* ptr,int* d) { if(ptr){ *d = ((mem_i*)((void*) ((char*)ptr - sizeof(mem_i))))->dim2; return OK; } return FAIL; } #define ALLOC_1D_ARRAY(type) \ int alloc_1D_array_size_ ##type (type **array, int dim1) { \ mem_i* h = NULL; \ void* tmp = NULL; \ ASSERT(dim1 >= 1,"DIM1 is too small: %d",dim1); \ if(*array == NULL){ \ MMALLOC(tmp,(dim1 * sizeof **array + sizeof(mem_i))); \ }else{ \ tmp = *array; \ tmp = (void*) ((char*)tmp - sizeof(mem_i)); \ h = (mem_i*)(tmp); \ if(h->dim1 < dim1){ \ MREALLOC(tmp,(dim1 * sizeof **array + sizeof(mem_i))); \ }else{ \ *array = (type*) ((char*)tmp + sizeof(mem_i)); \ return OK; \ } \ } \ h = (mem_i*)(tmp); \ h->dim1 = dim1; \ h->dim2 = 0; \ *array= (type*) ((char*)tmp + sizeof(mem_i)); \ return OK; \ ERROR: \ gfree(*array); \ return FAIL; \ } ALLOC_1D_ARRAY(char) ALLOC_1D_ARRAY(int8_t) ALLOC_1D_ARRAY(uint8_t) ALLOC_1D_ARRAY(int16_t) ALLOC_1D_ARRAY(uint16_t) ALLOC_1D_ARRAY(int32_t) ALLOC_1D_ARRAY(uint32_t) ALLOC_1D_ARRAY(int64_t) ALLOC_1D_ARRAY(uint64_t) ALLOC_1D_ARRAY(float) ALLOC_1D_ARRAY(double) #undef ALLOC_1D_ARRAY #define ALLOC_2D_ARRAY(type) \ int alloc_2D_array_size_ ##type (type ***array, int dim1,int dim2) { \ int i,j,c; \ mem_i* h = NULL; \ type** ptr_t = NULL; \ type* ptr_tt = NULL; \ void* tmp = NULL; \ int max1, max2; \ int o1, o2; \ ASSERT(dim1 >= 1,"DIM1 is too small: %d",dim1); \ ASSERT(dim2 >= 1,"DIM1 is too small: %d",dim2); \ if(*array == NULL){ \ MMALLOC(tmp,(dim1 * sizeof **array+ sizeof(mem_i))); \ MMALLOC(ptr_tt,((dim1 * dim2) * sizeof ***array)); \ h = (mem_i*)tmp; \ h->dim1 = dim1; \ h->dim2 = dim2; \ max1 = dim1; \ max2 = dim2; \ ptr_t =(type**) ((char*)tmp + sizeof(mem_i)); \ for(i = 0;i< dim1;i++){ \ ptr_t[i] = ptr_tt + i * dim2; \ } \ *array = ptr_t; \ }else{ \ ptr_tt = *array[0]; \ tmp = (void*)( (char*)*array -sizeof(mem_i)); \ h = (mem_i*)tmp; \ o1 = h->dim1; \ o2 = h->dim2; \ max1 = MACRO_MAX(dim1,o1); \ max2 = MACRO_MAX(dim2,o2); \ if(dim1 > o1){ \ MREALLOC(tmp,(dim1 * sizeof **array+ sizeof(mem_i))); \ MREALLOC(ptr_tt,((dim1* max2) * sizeof ***array )); \ }else if(dim2 > o2){ \ MREALLOC(ptr_tt,((max1 * dim2) * sizeof ***array )); \ }else{ \ return OK; \ } \ if(dim2 > o2){ \ for(i = o1-1; i >= 0;i-- ){ \ c = i* max2; \ for(j = o2-1;j >=0;j--){ \ *(ptr_tt + c + j) =*(ptr_tt + i*o2 + j); \ } \ } \ } \ h = (mem_i*)tmp; \ h->dim1 = max1; \ h->dim2 = max2; \ ptr_t = (type**) ((char*)tmp + sizeof(mem_i)); \ for(i = 0; i < max1;i++){ \ ptr_t[i] = ptr_tt + i * max2; \ } \ *array = ptr_t; \ } \ return OK; \ ERROR: \ gfree(*array); \ return FAIL; \ } ALLOC_2D_ARRAY(char) ALLOC_2D_ARRAY(int8_t) ALLOC_2D_ARRAY(uint8_t) ALLOC_2D_ARRAY(int16_t) ALLOC_2D_ARRAY(uint16_t) ALLOC_2D_ARRAY(int32_t) ALLOC_2D_ARRAY(uint32_t) ALLOC_2D_ARRAY(int64_t) ALLOC_2D_ARRAY(uint64_t) ALLOC_2D_ARRAY(float) ALLOC_2D_ARRAY(double) #define FREE_VOID(type) \ void gfree_void_ ##type(type *a){\ error(AT, "free was called on wrong type (%p)",(void*)a); \ } FREE_VOID(char) FREE_VOID(int8_t) FREE_VOID(uint8_t) FREE_VOID(int16_t) FREE_VOID(uint16_t) FREE_VOID(int32_t) FREE_VOID(uint32_t) FREE_VOID(int64_t) FREE_VOID(uint64_t) FREE_VOID(float) FREE_VOID(double) #undef FREE_VOID #define FREE_1D_ARRAY(type) \ void free_1d_array_ ##type(type **array){ \ if(*array){ \ void* ptr = (void*)((char*)*array - sizeof(mem_i)); \ MFREE(ptr); \ *array = NULL; \ } \ } FREE_1D_ARRAY(char) FREE_1D_ARRAY(int8_t) FREE_1D_ARRAY(uint8_t) FREE_1D_ARRAY(int16_t) FREE_1D_ARRAY(uint16_t) FREE_1D_ARRAY(int32_t) FREE_1D_ARRAY(uint32_t) FREE_1D_ARRAY(int64_t) FREE_1D_ARRAY(uint64_t) FREE_1D_ARRAY(float) FREE_1D_ARRAY(double) #undef FREE_1D_ARRAY #define FREE_2D_ARRAY(type) \ void free_2d_array_ ##type(type ***array){ \ if(*array){ \ if(*array[0]){ \ MFREE(*array[0]); \ } \ void* ptr = (void*)((char*)*array- sizeof(mem_i)); \ MFREE(ptr); \ *array = NULL; \ } \ } FREE_2D_ARRAY(char) FREE_2D_ARRAY(int8_t) FREE_2D_ARRAY(uint8_t) FREE_2D_ARRAY(int16_t) FREE_2D_ARRAY(uint16_t) FREE_2D_ARRAY(int32_t) FREE_2D_ARRAY(uint32_t) FREE_2D_ARRAY(int64_t) FREE_2D_ARRAY(uint64_t) FREE_2D_ARRAY(float) FREE_2D_ARRAY(double) #undef FREE_2D_ARRAY void error(const char *location, const char *format, ...) { va_list argp; va_start(argp, format); verror(stderr,location,format,argp); va_end(argp); } void warning(const char *location, const char *format, ...) { va_list argp; va_start(argp, format); vwarning(stderr,location, format, argp); va_end(argp); } void log_message( const char *format, ...) { va_list argp; va_start(argp, format); vlog(stderr,format, argp); va_end(argp); } int get_time(char* time_ptr, int size) { //struct tm *ptr; time_t current = time(NULL); struct tm local_time; if((localtime_r(¤t,&local_time)) == NULL){ ERROR_MSG("could not get local time"); } if(!strftime(time_ptr, size, "[%F %H:%M:%S] ", &local_time))ERROR_MSG("write failed"); return OK; ERROR: return FAIL; } void verror(FILE* f_ptr, const char *location, const char *format, va_list argp) { char time_string[200]; if(get_time(time_string, 200) != OK){ fprintf(stderr,"notime"); } fprintf(f_ptr,"%*s: ",MESSAGE_MARGIN,time_string); fprintf(f_ptr,"%*s: ",TYPE_MARGIN,"ERROR "); vfprintf(f_ptr, format, argp); fprintf(f_ptr," (%s)\n",location); fflush(f_ptr); } void vwarning(FILE* f_ptr,const char *location, const char *format, va_list argp) { char time_string[200]; if(get_time(time_string, 200) != OK){ fprintf(stderr,"notime"); } fprintf(f_ptr,"%*s: ",MESSAGE_MARGIN,time_string); fprintf(f_ptr,"%*s: ",TYPE_MARGIN,"WARNING "); vfprintf(f_ptr, format, argp); fprintf(f_ptr," (%s)\n",location); fflush(f_ptr); } void vlog(FILE* f_ptr,const char *format, va_list argp) { char time_string[200]; if(get_time(time_string, 200) != OK){ fprintf(stderr,"notime"); } fprintf(f_ptr,"%*s: ",MESSAGE_MARGIN,time_string); fprintf(f_ptr,"%*s: ",TYPE_MARGIN,"LOG "); vfprintf(f_ptr, format, argp); fprintf(f_ptr,"\n"); fflush(f_ptr); } int nearly_equal_float(float a, float b) { float absa = fabsf(a); float absb = fabsf(b); float d = fabsf(a-b); if(a == b){ return 1; }else if (a == 0.0f || b == 0 || (absa + absb < FLT_MIN)){ return d < (FLT_EPSILON * FLT_MIN); }else{ return d / MACRO_MIN((absa+absb), FLT_MIN) < FLT_EPSILON; } } int nearly_equal_double(double a, double b) { double absa = fabs(a); double absb = fabs(b); double d = fabs(a-b); if(a == b){ return 1; }else if (a == 0.0f || b == 0 || (absa + absb < DBL_MIN)){ return d < (DBL_EPSILON * DBL_MIN); }else{ return d / MACRO_MIN((absa+absb), DBL_MIN) < DBL_EPSILON; } } kalign-3.5.1/lib/src/tldevel.h000066400000000000000000000255411515023132300161340ustar00rootroot00000000000000#ifndef TLDEVEL_H #define TLDEVEL_H #include #include #include #include #include #include #include #ifdef TLDEVEL_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #define TLDEVEL_VERSION "1.0" #define OK 0 #define FAIL 1 #define MESSAGE_MARGIN 22 #define MACRO_MIN(a,b) (((a)<(b))?(a):(b)) #define MACRO_MAX(a,b) (((a)>(b))?(a):(b)) #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) #define AT __FILE__ " line " TOSTRING(__LINE__) #define ERROR_MSG(...) do { \ error(AT, __VA_ARGS__ ); \ goto ERROR; \ }while (0) #define WARNING_MSG(...) do { \ warning(AT, __VA_ARGS__ ); \ }while (0) #define LOG_MSG(...) do { \ log_message( __VA_ARGS__ ); \ }while (0) #define ASSERT(TEST,...) if(!(TEST)) { \ error(AT,#TEST ); \ error(AT, __VA_ARGS__); \ goto ERROR; \ } #if (DEBUGLEVEL >= 1) #define DASSERT(TEST,...) if(!(TEST)) { \ error(AT,#TEST ); \ error(AT, __VA_ARGS__); \ goto ERROR; \ } #else #define DASSERT(TEST,...) #endif #define ADDFAILED(x) "Function \"" TOSTRING(x) "\" failed." #define RUN(EXP) do { \ if((EXP) != OK){ \ ERROR_MSG(ADDFAILED(EXP)); \ } \ }while (0) #define RUNP(EXP) do { \ if((EXP) == NULL){ \ ERROR_MSG(ADDFAILED(EXP)); \ } \ }while (0) EXTERN int nearly_equal_float(float a, float b); EXTERN int nearly_equal_double(double a, double b); #define TLSAFE_EQ(X,Y) _Generic((X), \ float: nearly_equal_float, \ double: nearly_equal_double \ )(X,Y) /* Functions to declare and use a timer */ /* #define DECLARE_TIMER(n) struct timespec ts1_##n; struct timespec ts2_##n; #define START_TIMER(n) clock_gettime(CLOCK_MONOTONIC_RAW, &ts1_##n); #define STOP_TIMER(n) clock_gettime(CLOCK_MONOTONIC_RAW, &ts2_##n); #define GET_TIMING(n) (double)(ts2_##n.tv_sec - ts1_##n.tv_sec) + ((double) ts2_##n.tv_nsec - ts1_##n.tv_nsec) / 1000000000.0 */ /* Memory functions */ #define MFREE(p) do { \ if(p){ \ free(p); \ p = NULL; \ }else{ \ WARNING_MSG("free on a null pointer"); \ } \ } while (0) #define MMALLOC(p,size) do { \ if (p != NULL){ \ ERROR_MSG( "malloc on a nun-null pointer"); \ goto ERROR; \ } \ if(size == 0){ \ ERROR_MSG("malloc of size %d failed", size); \ goto ERROR; \ } \ if (((p) = malloc(size)) == NULL) { \ ERROR_MSG("malloc of size %d failed", size); \ goto ERROR; \ } \ } while (0) #define MREALLOC(p, size) do { \ void *tmpp; \ if(size == 0){ \ ERROR_MSG("malloc of size %d failed", size); \ goto ERROR; \ } \ if ((p) == NULL) { \ tmpp = malloc(size); \ }else { \ tmpp = realloc((p), (size)); \ } \ if (tmpp != NULL){ \ p = tmpp; \ }else { \ ERROR_MSG("realloc for size %d failed", size); \ goto ERROR; \ }} while (0) /* g memory functions */ EXTERN int get_dim1(void* ptr, int* d); EXTERN int get_dim2(void* ptr, int* d); /* #define VA_NUM_ARGS(...) VA_NUM_ARGS_IMPL(__VA_ARGS__, 5,4,3,2,1) #define VA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,N,...) N #define macro_dispatcher(func, ...) macro_dispatcher_(func, VA_NUM_ARGS(__VA_ARGS__)) #define macro_dispatcher_(func, nargs) macro_dispatcher__(func, nargs) #define macro_dispatcher__(func, nargs) func ## nargs */ #define FUNC_DEF(type) \ EXTERN int alloc_1D_array_size_ ##type (type **array, int dim1); \ EXTERN int alloc_2D_array_size_ ##type (type ***array, int dim1,int dim2); \ EXTERN void gfree_void_ ##type(type *a); \ EXTERN void free_1d_array_ ##type(type **array); \ EXTERN void free_2d_array_ ##type(type ***array); FUNC_DEF(char) FUNC_DEF(int8_t) FUNC_DEF(uint8_t) FUNC_DEF(int16_t) FUNC_DEF(uint16_t) FUNC_DEF(int32_t) FUNC_DEF(uint32_t) FUNC_DEF(int64_t) FUNC_DEF(uint64_t) FUNC_DEF(float) FUNC_DEF(double) #undef FUNC_DEF EXTERN int galloc_unknown_type_error (void* p, ...); EXTERN int galloc_too_few_arg_error (void* p); #define p1(X) _Generic((X), \ default: galloc_too_few_arg_error \ )(X) #define p2(X,Y) _Generic((X), \ char**: alloc_1D_array_size_char, \ int8_t**: alloc_1D_array_size_int8_t, \ uint8_t**: alloc_1D_array_size_uint8_t, \ int16_t**: alloc_1D_array_size_int16_t, \ uint16_t**: alloc_1D_array_size_uint16_t, \ int32_t**: alloc_1D_array_size_int32_t, \ uint32_t**: alloc_1D_array_size_uint32_t, \ int64_t**: alloc_1D_array_size_int64_t, \ uint64_t**: alloc_1D_array_size_uint64_t, \ float**: alloc_1D_array_size_float, \ double**: alloc_1D_array_size_double, \ default: galloc_unknown_type_error \ )(X,Y) #define p3(X,Y,Z) _Generic((X), \ char***: alloc_2D_array_size_char, \ int8_t***: alloc_2D_array_size_int8_t, \ uint8_t***: alloc_2D_array_size_uint8_t, \ int16_t***: alloc_2D_array_size_int16_t, \ uint16_t***: alloc_2D_array_size_uint16_t, \ int32_t***: alloc_2D_array_size_int32_t, \ uint32_t***: alloc_2D_array_size_uint32_t, \ int64_t***: alloc_2D_array_size_int64_t, \ uint64_t***: alloc_2D_array_size_uint64_t, \ float***: alloc_2D_array_size_float, \ double***: alloc_2D_array_size_double, \ default: galloc_unknown_type_error \ )(X,Y,Z) #define _ARG3(_0, _1, _2, _3, ...) _3 #define NARG3(...) _ARG3(__VA_ARGS__,3, 2, 1, 0) #define _GALLOC_ARGS_1( a) p1(a) #define _GALLOC_ARGS_2( a, b) p2(a,b) #define _GALLOC_ARGS_3( a, b, c ) p3(a,b,c) #define __GALLOC_ARGS( N, ...) _GALLOC_ARGS_ ## N ( __VA_ARGS__) #define _GALLOC_ARGS( N, ...) __GALLOC_ARGS( N, __VA_ARGS__) #define GALLOC_ARGS( ...) _GALLOC_ARGS( NARG3(__VA_ARGS__), __VA_ARGS__) #define galloc(...) GALLOC_ARGS( __VA_ARGS__) #define gfree(X) _Generic((&X), \ char*: gfree_void_char,\ int8_t*: gfree_void_int8_t,\ uint8_t*: gfree_void_uint8_t,\ int16_t*: gfree_void_int16_t,\ uint16_t*: gfree_void_uint16_t,\ int32_t*: gfree_void_int32_t,\ uint32_t*: gfree_void_uint32_t,\ int64_t*: gfree_void_int64_t,\ uint64_t*: gfree_void_uint64_t,\ float*: gfree_void_float,\ double*: gfree_void_double,\ char**: free_1d_array_char,\ int8_t**: free_1d_array_int8_t,\ uint8_t**: free_1d_array_uint8_t,\ int16_t**: free_1d_array_int16_t, \ uint16_t**: free_1d_array_uint16_t, \ int32_t**: free_1d_array_int32_t,\ uint32_t**: free_1d_array_uint32_t,\ int64_t**: free_1d_array_int64_t,\ uint64_t**: free_1d_array_uint64_t,\ float**: free_1d_array_float,\ double**: free_1d_array_double, \ char***: free_2d_array_char, \ int8_t***: free_2d_array_int8_t,\ uint8_t***: free_2d_array_uint8_t,\ int16_t***: free_2d_array_int16_t, \ uint16_t***: free_2d_array_uint16_t, \ int32_t***: free_2d_array_int32_t,\ uint32_t***: free_2d_array_uint32_t,\ int64_t***: free_2d_array_int64_t,\ uint64_t***: free_2d_array_uint64_t,\ float***: free_2d_array_float,\ double***: free_2d_array_double \ )(&X) /* functions */ EXTERN void error(const char *location, const char *format, ...); EXTERN void warning(const char *location, const char *format, ...); EXTERN void log_message( const char *format, ...); EXTERN const char* tldevel_version(void); #undef TLDEVEL_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/tlmisc.c000066400000000000000000000123751515023132300157640ustar00rootroot00000000000000 #include #include #include /* #include "strnlen_compat.h" */ #define TLMISC_IMPORT #include "tlmisc.h" #include "tldevel.h" #define MAX_CMD_LEN 16384 int my_str_cpy(char* target, char* source, int t_size,int s_size) { if(s_size > t_size){ ERROR_MSG("Target size is < than source size"); } int s_len = strnlen(source, s_size); int t_len = strnlen(target, t_size); if(s_len > t_size){ ERROR_MSG("Target len is < than source size"); } for(int i = 0; i < t_len;i++){ target[i] = source[i]; } target[t_len] = 0; return OK; ERROR: return FAIL; } int my_str_append(char* target, char* source, int t_size,int s_size) { if(s_size > t_size){ ERROR_MSG("Target size is < than source size"); } int s_len = strnlen(source, s_size); int t_len = strnlen(target, t_size); if(s_len > t_size){ ERROR_MSG("Target len is < than source size"); } if(t_size - t_len < s_len){ ERROR_MSG("Target has insufficient space."); } int c = t_len; for(int i = 0; i < t_len;i++){ target[c] = source[i]; c++; } target[c] = 0; return OK; ERROR: return FAIL; } char* basename(const char* name) { int i= 0; int c = 0; while(1){ if(name[i] == '/'){ c = i+1; } if(!name[i]){ break; } i++; } return (char*)(name +c); } int my_file_exists(const char* name) { struct stat buf; int ret,local_ret; ret = 0; local_ret= stat ( name, &buf ); /* File found */ if ( local_ret == 0 ) { ret++; } return ret; } /* I don't like that both libgen and string have functions to work with directory / filenames. The functions below copy the input path and alloc a new character array to store the output. Needs to be MFREE'd... */ int tlfilename(char* path, char** out) { char* tmp = NULL; int len; int i; int c; len = 0; len = (int) strlen(path); MMALLOC(tmp, sizeof(char) * (len+1)); c = 0; for(i = 0;i < len;i++){ tmp[c] = path[i]; c++; if(path[i] == '/'){ c = 0; } } tmp[c] = 0; if(c == 0){ ERROR_MSG("No filename found in: %s", path); } *out = tmp; return OK; ERROR: if(tmp){ MFREE(tmp); } return FAIL; } int tldirname(char* path, char** out) { char* tmp = NULL; int len; int i; int c; int e; len = 0; len = (int) strlen(path); MMALLOC(tmp, sizeof(char) * (len+1)); c = 0; e = 0; for(i = 0;i < len;i++){ tmp[c] = path[i]; if(path[i] == '/'){ e = c; } c++; } tmp[e] = 0; if(e == 0){ ERROR_MSG("No dirname found in: %s", path); } *out = tmp; return OK; ERROR: if(tmp){ MFREE(tmp); } return FAIL; } int make_cmd_line(char** command, const int argc,char* const argv[]) { char* cmd = NULL; int i,j,c,g; int alloc_len = 16; int old_len; RUN(galloc(&cmd,alloc_len)); for(i =0 ; i < alloc_len;i++){ cmd[i] = 0; } c = 0; for(i =0 ; i < argc;i++){ //fprintf(stdout,"%s\n", argv[i]); for(j = 0; j < (int) strlen(argv[i]);j++){ if(c == 16384-1){ break; } cmd[c] = argv[i][j]; c++; if(c == alloc_len){ old_len = alloc_len; alloc_len = alloc_len + alloc_len /2; RUN(galloc(&cmd,alloc_len)); for(g = old_len; g < alloc_len;g++){ cmd[g] = 0; } } } if(c >= MAX_CMD_LEN){ ERROR_MSG("Command line too long! Allocated: %d", alloc_len); } if(c == 16384-1){ break; } cmd[c] = ' '; c++; if(c == alloc_len){ old_len = alloc_len; alloc_len = alloc_len + alloc_len /2; RUN(galloc(&cmd,alloc_len)); for(g = old_len; g < alloc_len;g++){ cmd[g] = 0; } } } cmd[c] = 0; *command = cmd; return OK; ERROR: if(cmd){ gfree(cmd); } return FAIL; } kalign-3.5.1/lib/src/tlmisc.h000066400000000000000000000011511515023132300157570ustar00rootroot00000000000000#ifndef TLMISC_H #define TLMISC_H #ifdef TLMISC_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif EXTERN char* basename(const char* name); EXTERN int my_str_cpy(char* target, char* source, int t_size,int s_size); EXTERN int my_str_append(char* target, char* source, int t_size,int s_size); EXTERN int my_file_exists(const char* name); EXTERN int make_cmd_line(char** command, const int argc,char* const argv[]); EXTERN int tlfilename(char* path, char** out); EXTERN int tldirname(char* path, char** out); #undef TLMISC_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/tlrng.c000066400000000000000000000316231515023132300156140ustar00rootroot00000000000000 #include #include #include /* #include */ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #define M_2PI 2.0*M_PI #include "tldevel.h" #define TLRNG_IMPORT #include "tlrng.h" /* code here was adopted from: */ /* Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) To the extent possible under law, the author has dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. See . */ /* http://vigna.di.unimi.it/xorshift/xoshiro256starstar.c */ /* http://xoshiro.di.unimi.it/splitmix64.c */ /* written by: */ /* Sebastiano Vigna (vigna@acm.org) */ /* David Blackman */ /* code for sampling from varius distributions was taken from */ /* Copyright 2005 Robert Kern (robert.kern@gmail.com) * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* And code bits from */ /* the easel library (by Sean Eddy) */ struct rng_state{ uint64_t s[4]; uint8_t gen; double z1; int has_gauss; /* !=0: gauss contains a gaussian deviate */ double gauss; }; static inline uint64_t rotl(const uint64_t x, int k); static uint64_t next(struct rng_state* s); static void jump(struct rng_state* s); void long_jump(struct rng_state* s); static uint64_t choose_arbitrary_seed(void); static uint32_t jenkins_mix3(uint32_t a, uint32_t b, uint32_t c); static double tl_standard_gamma(struct rng_state* rng, double shape); static double tl_standard_exponential(struct rng_state* rng); static double tl_gauss(struct rng_state* rng); double tl_random_double(struct rng_state* rng) { uint64_t x; double y; do{ x = next(rng); y = ((double) x / 18446744073709551616.0); }while (y == 0.0); return y; } int tl_random_int(struct rng_state* rng,int a) { return (int) (tl_random_double(rng) * a); } /* from: */ //https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform double tl_random_gaussian(struct rng_state* rng, double mu, double sigma) { rng->gen = !rng->gen; if (!rng->gen){ return rng->z1 * sigma + mu; } double u1, u2; do{ u1 = tl_random_double(rng);// rand() * (1.0 / RAND_MAX); u2 = tl_random_double(rng);//rand() * (1.0 / RAND_MAX); } while (u1 <= DBL_EPSILON); double z0; z0 = sqrt(-2.0 * log(u1)) * cos(M_2PI * u2); rng->z1 = sqrt(-2.0 * log(u1)) * sin(M_2PI * u2); return z0 * sigma + mu; } double tl_random_gamma(struct rng_state* rng, double shape, double scale) { return scale * tl_standard_gamma(rng, shape); } double tl_standard_gamma(struct rng_state* rng, double shape) { double b, c; double U, V, X, Y; if (shape == 1.0) { return tl_standard_exponential(rng); } else if (shape < 1.0) { for (;;) { U = tl_random_double(rng); V = tl_standard_exponential(rng); if (U <= 1.0 - shape) { X = pow(U, 1./shape); if (X <= V) { return X; } } else { Y = -log((1-U)/shape); X = pow(1.0 - shape + shape*Y, 1./shape); if (X <= (V + Y)) { return X; } } } } else { b = shape - 1./3.; c = 1./sqrt(9*b); for (;;) { do { X = tl_gauss(rng); V = 1.0 + c*X; } while (V <= 0.0); V = V*V*V; U = tl_random_double(rng); if (U < 1.0 - 0.0331*(X*X)*(X*X)) return (b*V); if (log(U) < 0.5*X*X + b*(1. - V + log(V))) return (b*V); } } } double tl_standard_exponential(struct rng_state* rng) { /* We use -log(1-U) since U is [0, 1) */ return -log(1.0 - tl_random_double(rng)); } double tl_gauss(struct rng_state* rng) { if (rng->has_gauss) { const double tmp = rng->gauss; rng->gauss = 0; rng->has_gauss = 0; return tmp; } else { double f, x1, x2, r2; do { x1 = 2.0*tl_random_double(rng) - 1.0; x2 = 2.0*tl_random_double(rng) - 1.0; r2 = x1*x1 + x2*x2; } while (r2 >= 1.0 || r2 == 0.0); /* Box-Muller transform */ f = sqrt(-2.0*log(r2)/r2); /* Keep for next call */ rng->gauss = f*x1; rng->has_gauss = 1; return f*x2; } } struct rng_state* init_rng(uint64_t seed) { struct rng_state* s = NULL; uint64_t z; uint64_t sanity; MMALLOC(s, sizeof(struct rng_state)); s->gen = 0; s->z1 = 0.0f; s->gauss = 0.0; s->has_gauss = 0; if(!seed){ seed = choose_arbitrary_seed(); } sanity = 0; while(!sanity){ sanity = 0; z = (seed += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; s->s[0] = z ^ (z >> 31); if(s->s[0]){ sanity++; } z = (seed += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; s->s[1] = z ^ (z >> 31); if(s->s[1]){ sanity++; } z = (seed += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; s->s[2] = z ^ (z >> 31); if(s->s[2]){ sanity++; } z = (seed += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; s->s[3] = z ^ (z >> 31); if(s->s[3]){ sanity++; } } return s; ERROR: return NULL; } struct rng_state* init_rng_from_rng(struct rng_state* rng) { struct rng_state* s = NULL; int i; MMALLOC(s, sizeof(struct rng_state)); for(i = 0; i < 4;i++){ s->s[i] = rng->s[i]; s->gen = 0; s->z1 = 0.0; s->gauss = rng->gauss; s->has_gauss = rng->has_gauss; } jump(rng); return s; ERROR: return NULL; } void free_rng(struct rng_state* rng) { if(rng){ MFREE(rng); } } /* Taken from easel library (by Sean Eddy) */ static uint64_t choose_arbitrary_seed(void) { uint32_t a = (uint32_t) time ((time_t *) NULL); uint32_t b = 87654321; // we'll use getpid() below, if we can uint32_t c = (uint32_t) clock(); // clock() gives time since process invocation, in msec at least, if not usec uint64_t seed; #ifdef HAVE_GETPID b = (uint32_t) getpid(); // preferable b choice, if we have POSIX getpid() #endif seed = jenkins_mix3(a,b,c); // try to decorrelate closely spaced choices of pid/times return (seed == 0) ? 42 : seed; /* 42 is entirely arbitrary, just to avoid seed==0. */ } /* jenkins_mix3() * * from Bob Jenkins: given a,b,c, generate a number that's distributed * reasonably uniformly on the interval 0..2^32-1 even for closely * spaced choices of a,b,c. */ static uint32_t jenkins_mix3(uint32_t a, uint32_t b, uint32_t c) { a -= b; a -= c; a ^= (c>>13); b -= c; b -= a; b ^= (a<<8); c -= a; c -= b; c ^= (b>>13); a -= b; a -= c; a ^= (c>>12); b -= c; b -= a; b ^= (a<<16); c -= a; c -= b; c ^= (b>>5); a -= b; a -= c; a ^= (c>>3); b -= c; b -= a; b ^= (a<<10); c -= a; c -= b; c ^= (b>>15); return c; } static inline uint64_t rotl(const uint64_t x, int k) { return (x << k) | (x >> (64 - k)); } /* This is xoshiro256** 1.0, one of our all-purpose, rock-solid generators. It has excellent (sub-ns) speed, a state (256 bits) that is large enough for any parallel application, and it passes all tests we are aware of. For generating just floating-point numbers, xoshiro256+ is even faster. The state must be seeded so that it is not everywhere zero. If you have a 64-bit seed, we suggest to seed a splitmix64 generator and use its output to fill s. */ uint64_t next(struct rng_state* s) { const uint64_t result_starstar = rotl(s->s[1] * 5, 7) * 9; const uint64_t t = s->s[1] << 17; s->s[2] ^= s->s[0]; s->s[3] ^= s->s[1]; s->s[1] ^= s->s[2]; s->s[0] ^= s->s[3]; s->s[2] ^= t; s->s[3] = rotl(s->s[3], 45); return result_starstar; } /* This is the jump function for the generator. It is equivalent to 2^128 calls to next(); it can be used to generate 2^128 non-overlapping subsequences for parallel computations. */ void jump(struct rng_state* s) { static const uint64_t JUMP[] = { 0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c }; uint64_t s0 = 0; uint64_t s1 = 0; uint64_t s2 = 0; uint64_t s3 = 0; for(uint64_t i = 0; i < sizeof JUMP / sizeof *JUMP; i++) for(int b = 0; b < 64; b++) { if (JUMP[i] & UINT64_C(1) << b) { s0 ^= s->s[0]; s1 ^= s->s[1]; s2 ^= s->s[2]; s3 ^= s->s[3]; } next(s); } s->s[0] = s0; s->s[1] = s1; s->s[2] = s2; s->s[3] = s3; } /* This is the long-jump function for the generator. It is equivalent to 2^192 calls to next(); it can be used to generate 2^64 starting points, from each of which jump() will generate 2^64 non-overlapping subsequences for parallel distributed computations. */ void long_jump(struct rng_state* s) { static const uint64_t LONG_JUMP[] = { 0x76e15d3efefdcbbf, 0xc5004e441c522fb3, 0x77710069854ee241, 0x39109bb02acbe635 }; uint64_t s0 = 0; uint64_t s1 = 0; uint64_t s2 = 0; uint64_t s3 = 0; for(uint64_t i = 0; i < sizeof LONG_JUMP / sizeof *LONG_JUMP; i++) for(int b = 0; b < 64; b++) { if (LONG_JUMP[i] & UINT64_C(1) << b) { s0 ^= s->s[0]; s1 ^= s->s[1]; s2 ^= s->s[2]; s3 ^= s->s[3]; } next(s); } s->s[0] = s0; s->s[1] = s1; s->s[2] = s2; s->s[3] = s3; } kalign-3.5.1/lib/src/tlrng.h000066400000000000000000000013001515023132300156060ustar00rootroot00000000000000#ifndef TLRNG_H #define TLRNG_H #include #ifdef TLRNG_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif typedef struct rng_state rng_state; EXTERN struct rng_state* init_rng(uint64_t seed); EXTERN struct rng_state* init_rng_from_rng(struct rng_state* rng); EXTERN void free_rng(struct rng_state* rng); EXTERN double tl_random_double(struct rng_state* rng); EXTERN double tl_random_gaussian(struct rng_state* rng, double mu, double sigma); EXTERN double tl_random_gamma(struct rng_state* rng, double shape, double scale); EXTERN int tl_random_int(struct rng_state* rng,int a); #undef TLRNG_IMPORT #undef EXTERN #endif kalign-3.5.1/lib/src/version.h.in000066400000000000000000000001761515023132300165640ustar00rootroot00000000000000#cmakedefine KALIGN_PACKAGE_NAME "@CMAKE_PROJECT_NAME@" #cmakedefine KALIGN_PACKAGE_VERSION "@KALIGN_LIBRARY_VERSION_STRING@" kalign-3.5.1/lib/src/weave_alignment.c000066400000000000000000000055071515023132300176350ustar00rootroot00000000000000#include "task.h" #include "tldevel.h" #include "msa_struct.h" #define WEAVE_ALIGNMENT_IMPORT #include "weave_alignment.h" int update_gaps(int old_len,int*gis,int *newgaps); int clean_aln(struct msa* msa) { int i,j; int* p = NULL; for (i = 0; i < msa->numseq;i++){ p = msa->sequences[i]->gaps;// aln->gaps[i]; for (j = 0; j <= msa->sequences[i]->len;j++){ p[j] = 0; } } for(i =0;i < msa->numseq;i++){ msa->nsip[i] = 1; msa->sip[i][0] = i; } for (i = msa->numseq;i < msa->num_profiles ;i++){ if(msa->sip[i]){ MFREE(msa->sip[i]); msa->sip[i] = NULL; } msa->nsip[i] =0; } return OK; } int make_seq(struct msa* msa,int a,int b,int* path) { int* gap_a = NULL; int* gap_b = NULL; int c; int i; int posa = 0; int posb = 0; MMALLOC(gap_a,(path[0]+1)*sizeof(int)); MMALLOC(gap_b,(path[0]+1)*sizeof(int)); for (i = path[0]+1;i--;){ gap_a[i] = 0; gap_b[i] = 0; } c = 1; while(path[c] != 3){ if (!path[c]){ posa++; posb++; }else if (path[c] & 1){ gap_a[posa] += 1; posb++; }else if (path[c] & 2){ gap_b[posb] += 1; posa++; } c++; } for (i = msa->nsip[a];i--;){ RUN(update_gaps(msa->sequences[msa->sip[a][i]]->len, msa->sequences[msa->sip[a][i]]->gaps,gap_a)); //RUN(update_gaps(aln->sl[aln->sip[a][i]],aln->gaps[aln->sip[a][i]],path[0],gap_a)); } for (i = msa->nsip[b];i--;){ RUN(update_gaps(msa->sequences[msa->sip[b][i]]->len,msa->sequences[msa->sip[b][i]]->gaps,gap_b)); //RUN(update_gaps(aln->sl[aln->sip[b][i]],aln->gaps[aln->sip[b][i]],path[0],gap_b)); } MFREE(gap_a); MFREE(gap_b); return OK; ERROR: if(gap_a){ MFREE(gap_a); } if(gap_b){ MFREE(gap_b); } return FAIL; } int update_gaps(int old_len,int*gis,int *newgaps) { int i,j; int add = 0; int rel_pos = 0; for (i = 0; i <= old_len;i++){ add = 0; for (j = rel_pos;j <= rel_pos + gis[i];j++){ if (newgaps[j] != 0){ add += newgaps[j]; } } rel_pos += gis[i]+1; gis[i] += add; } return OK; } kalign-3.5.1/lib/src/weave_alignment.h000066400000000000000000000005451515023132300176370ustar00rootroot00000000000000#ifndef WEAVE_ALIGNMENT_H #define WEAVE_ALIGNMENT_H #ifdef WEAVE_ALIGNMENT_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif struct msa; EXTERN int make_seq(struct msa* msa,int a,int b,int* path); EXTERN int clean_aln(struct msa* msa); #undef WEAVE_ALIGNMENT_IMPORT #undef EXTERN #endif kalign-3.5.1/pyproject.toml000066400000000000000000000132251515023132300156770ustar00rootroot00000000000000[build-system] requires = [ "scikit-build-core>=0.11.4", "pybind11>=2.12", "cmake>=3.18", ] build-backend = "scikit_build_core.build" [project] name = "kalign-python" version = "3.5.1" description = "Python wrapper for the Kalign multiple sequence alignment engine" readme = "README-python.md" license = "Apache-2.0" license-files = ["COPYING"] authors = [ {name = "Timo Lassmann", email = "timolassmann@icloud.com"} ] maintainers = [ {name = "Timo Lassmann", email = "timolassmann@icloud.com"} ] keywords = [ "bioinformatics", "sequence alignment", "multiple sequence alignment", "MSA", "computational biology", "genomics", "proteomics", "phylogenetic analysis", "evolutionary biology", "biopython", "scikit-bio", "DNA alignment", "protein alignment", "RNA alignment", "fast alignment", "parallel alignment", "SIMD optimization" ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: C++", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Software Development :: Libraries :: Python Modules" ] requires-python = ">=3.9" dependencies = [ "numpy>=1.19.0", ] [project.scripts] kalign-py = "kalign.cli:main" [project.optional-dependencies] # Bioinformatics ecosystem integrations biopython = ["biopython>=1.85"] skbio = ["scikit-bio>=0.6.3"] io = ["biopython>=1.85"] # For I/O helper functions analysis = ["pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] all = ["biopython>=1.85", "scikit-bio>=0.6.3", "pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] benchmark = ["dash>=2.14", "plotly>=5.18", "pandas>=2.0", "tqdm>=4.60"] # Development dependencies dev = [ "pytest>=6.0", "pytest-cov", "pytest-benchmark", "pytest-xdist", "rich", "black", "flake8", "mypy", "build", "twine", "biopython>=1.85", # For testing ecosystem features "scikit-bio>=0.6.3" ] test = [ "pytest>=6.0", "pytest-cov", "pytest-benchmark", "pytest-xdist", "rich" ] docs = [ "sphinx", "sphinx-rtd-theme", "myst-parser" ] [project.urls] Homepage = "https://github.com/TimoLassmann/kalign" Documentation = "https://github.com/TimoLassmann/kalign/blob/main/README.md" Repository = "https://github.com/TimoLassmann/kalign" "Bug Tracker" = "https://github.com/TimoLassmann/kalign/issues" Changelog = "https://github.com/TimoLassmann/kalign/blob/main/ChangeLog" [tool.scikit-build] # Wheel settings - removed py-api to build version-specific wheels # wheel.py-api = "cp39" # CMake settings cmake.build-type = "Release" cmake.source-dir = "." # CMakeLists.txt is now in the same directory as pyproject.toml cmake.args = [ "-DBUILD_PYTHON_MODULE=ON", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" ] build.targets = ["_core"] # Install settings install.components = ["python"] # Source distribution settings # Now that all files are in the same directory tree, sdist should work correctly sdist.exclude = [ "build*/", ".venv/", "__pycache__/", "**/*.pyc", "wheelhouse/", "dist/" ] # Explicitly include critical directories that might be excluded by default sdist.include = [ "lib/include/" ] # Logging logging.level = "DEBUG" [tool.scikit-build.cmake.define] BUILD_PYTHON_MODULE = "ON" [tool.cibuildwheel] # Build wheels for Python 3.9+ build = "cp39-* cp310-* cp311-* cp312-* cp313-*" # Skip 32-bit builds (PyPy wheels are not built by default) skip = "*-win32 *-manylinux_i686" # Test command test-command = "python -c 'import kalign; print(kalign.__version__)'" # Linux settings [tool.cibuildwheel.linux] before-all = [ "yum install -y cmake3 || apt-get update && apt-get install -y cmake", ] repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" # macOS settings [tool.cibuildwheel.macos] before-all = [ "brew install cmake || echo 'cmake already installed'", ] repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" # Windows settings [tool.cibuildwheel.windows] before-all = [ "choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' || echo 'cmake already installed'", ] # Test settings for all platforms [tool.cibuildwheel.environment] # Set OpenMP variables for better performance OMP_NUM_THREADS = "1" [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] python_functions = ["test_*"] addopts = [ "--strict-markers", "--strict-config", "--cov=kalign", "--cov-report=term-missing", "--cov-report=html" ] [tool.black] line-length = 88 target-version = ["py39"] include = '\.pyi?$' extend-exclude = ''' /( # directories \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | build | dist )/ ''' [tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_unreachable = true strict_equality = true [[tool.mypy.overrides]] module = "kalign._core" ignore_missing_imports = true [dependency-groups] dev = [ "build>=1.2.2.post1", "pytest>=8.4.1", "pytest-benchmark>=5.1.0", ] kalign-3.5.1/python-kalign/000077500000000000000000000000001515023132300155445ustar00rootroot00000000000000kalign-3.5.1/python-kalign/__init__.py000066400000000000000000001077651515023132300176750ustar00rootroot00000000000000""" Kalign - Fast multiple sequence alignment This package provides Python bindings for the Kalign multiple sequence alignment program. Kalign is a fast and accurate multiple sequence alignment tool for biological sequences. """ import os import threading from importlib import import_module from typing import Any, List, Literal, Optional, Union class AlignedSequences: """Result of aligning sequences from a file, preserving sequence names. Supports unpacking as ``names, sequences = result`` for backward compatibility, while also exposing optional confidence attributes. """ __slots__ = ("names", "sequences", "column_confidence", "residue_confidence") def __init__( self, names: List[str], sequences: List[str], column_confidence: Optional[List[float]] = None, residue_confidence: Optional[List[List[float]]] = None, ): self.names = names self.sequences = sequences self.column_confidence = column_confidence self.residue_confidence = residue_confidence # Backward-compatible 2-tuple unpacking: names, sequences = result def __iter__(self): return iter((self.names, self.sequences)) def __len__(self): return 2 def __getitem__(self, index): return (self.names, self.sequences)[index] def __repr__(self): return ( f"AlignedSequences(names={self.names!r}, sequences={self.sequences!r}, " f"column_confidence={'[...]' if self.column_confidence else None}, " f"residue_confidence={'[...]' if self.residue_confidence else None})" ) from importlib.metadata import version from . import _core, io, utils __version__ = version("kalign-python") __author__ = "Timo Lassmann" __email__ = "timolassmann@icloud.com" # Re-export constants for convenience DNA = _core.DNA DNA_INTERNAL = _core.DNA_INTERNAL RNA = _core.RNA PROTEIN = _core.PROTEIN PROTEIN_PFASUM43 = _core.PROTEIN_PFASUM43 PROTEIN_PFASUM60 = _core.PROTEIN_PFASUM60 PROTEIN_PFASUM_AUTO = _core.PROTEIN_PFASUM_AUTO PROTEIN_DIVERGENT = _core.PROTEIN_DIVERGENT AUTO = _core.AUTO # Refinement mode constants REFINE_NONE = _core.REFINE_NONE REFINE_ALL = _core.REFINE_ALL REFINE_CONFIDENT = _core.REFINE_CONFIDENT REFINE_INLINE = _core.REFINE_INLINE # Mode constants MODE_DEFAULT = "default" MODE_FAST = "fast" MODE_PRECISE = "precise" # Mode preset definitions _MODE_PRESETS = { "default": {"vsm_amax": -1.0, "consistency": 5, "consistency_weight": 2.0}, "fast": {"vsm_amax": -1.0, "consistency": 0, "consistency_weight": 2.0}, "precise": { "vsm_amax": -1.0, "ensemble": 3, "realign": 1, "consistency": 0, "consistency_weight": 2.0, }, } # Global thread control _thread_local = threading.local() _default_threads = 1 def _conf_to_pp(conf: float) -> str: """Convert a confidence value [0..1] to HMMER-style PP character.""" if conf >= 0.95: return "*" return str(int(conf * 10)) def _confidence_to_pp_string(seq: str, confidences: list) -> str: """Convert per-residue confidence array to PP string. Gap positions get '.', residues get HMMER-style PP characters. """ pp = [] for ch, conf in zip(seq, confidences): if ch == "-" or ch == ".": pp.append(".") else: pp.append(_conf_to_pp(conf)) return "".join(pp) def align( sequences: List[str], seq_type: Union[str, int] = "auto", gap_open: Optional[float] = None, gap_extend: Optional[float] = None, terminal_gap_extend: Optional[float] = None, n_threads: Optional[int] = None, refine: Union[str, int] = "none", ensemble: int = 0, min_support: int = 0, seq_weights: float = 0.0, consistency: int = 5, consistency_weight: float = 2.0, vsm_amax: float = -1.0, realign: int = 0, ensemble_seed: int = 42, mode: Optional[str] = None, fmt: Literal["plain", "biopython", "skbio"] = "plain", ids: Optional[List[str]] = None, ) -> Union[List[str], Any]: """ Multiple sequence alignment via Kalign. Parameters ---------- sequences : list of str List of sequences to align. Sequences should be provided as strings containing the sequence characters (e.g., 'ATCG' for DNA, 'ACGU' for RNA, or amino acid codes for proteins). seq_type : str or int, optional Sequence type specification. Can be: - "auto" or AUTO: Auto-detect sequence type (default) - "dna" or DNA: DNA sequences - "rna" or RNA: RNA sequences - "protein" or PROTEIN: Protein sequences - "divergent" or PROTEIN_DIVERGENT: Divergent protein sequences - "internal" or DNA_INTERNAL: DNA with internal gap preference gap_open : float, optional Gap opening penalty (positive value, e.g. 5.5). If None, uses Kalign defaults. gap_extend : float, optional Gap extension penalty (positive value, e.g. 2.0). If None, uses Kalign defaults. terminal_gap_extend : float, optional Terminal gap extension penalty (positive value, e.g. 1.0). If None, uses Kalign defaults. n_threads : int, optional Number of threads to use for alignment. If None, uses global default. refine : str or int, optional Refinement mode: "none", "all", "confident", or "inline" (default: "none"). ensemble : int, optional Number of ensemble runs (default: 0 = off). Set to e.g. 3 for higher accuracy at the cost of ~10x runtime. vsm_amax : float, optional Variable scoring matrix amplitude (default: -1.0 = use kalign defaults: 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 to disable. realign : int, optional Number of alignment-guided tree rebuild iterations (default: 0 = off). ensemble_seed : int, optional RNG seed for ensemble runs (default: 42). mode : str, optional Preset mode: "default" (consistency+VSM), "fast" (VSM only), "precise" (ensemble+VSM+realign). Explicit parameters override mode defaults. None is treated as "default". fmt : {'plain', 'biopython', 'skbio'}, default 'plain' Choose return-object flavour: - 'plain': list of aligned sequences (fastest) - 'biopython': Bio.Align.MultipleSeqAlignment object - 'skbio': skbio.TabularMSA object ids : list of str, optional Sequence IDs (used only for Biopython / scikit-bio objects). If None, generates 'seq0', 'seq1', etc. Returns ------- list of str | Bio.Align.MultipleSeqAlignment | skbio.TabularMSA Aligned sequences. Return type depends on `fmt` parameter. Raises ------ ValueError If input sequences are empty or invalid RuntimeError If alignment fails ImportError If Biopython or scikit-bio are requested but not installed Examples -------- >>> import kalign >>> sequences = ["ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG"] # 1) Plain list (default) >>> aligned = kalign.align(sequences) >>> print(aligned) ['ATCGATCGATCG', 'ATCG-TCGATCG', 'ATCGATC-ATCG'] # 2) Biopython object >>> aln_bp = kalign.align(sequences, fmt="biopython", ids=["s1","s2","s3"]) >>> print(type(aln_bp)) # 3) scikit-bio object >>> aln_sk = kalign.align(sequences, fmt="skbio") >>> print(type(aln_sk)) """ # Input validation - replicate C CLI robustness if not sequences: raise ValueError("No sequences were found in the input") if len(sequences) == 1: raise ValueError( "Only 1 sequence was found in the input - at least 2 sequences are required for alignment" ) if not all(isinstance(seq, str) for seq in sequences): raise ValueError("All sequences must be strings") # Check for empty or whitespace-only sequences empty_sequences = [] for i, seq in enumerate(sequences): if not seq or not seq.strip(): empty_sequences.append(i) if empty_sequences: if len(empty_sequences) == 1: raise ValueError( f"Sequence at index {empty_sequences[0]} is empty or contains only whitespace" ) else: raise ValueError( f"Sequences at indices {empty_sequences} are empty or contain only whitespace" ) # Check for valid sequence characters (basic validation) for i, seq in enumerate(sequences): # Remove common whitespace and check if anything remains cleaned_seq = "".join(seq.split()) if len(cleaned_seq) == 0: raise ValueError( f"Sequence at index {i} contains only whitespace characters" ) # Check for obviously invalid characters (control characters, etc) if any(ord(char) < 32 for char in cleaned_seq if char not in "\t\n\r"): raise ValueError( f"Sequence at index {i} contains invalid control characters" ) # Check for digits and other problematic characters that cause platform-specific segfaults invalid_chars = set(char for char in cleaned_seq if char.isdigit()) if invalid_chars: raise ValueError( f"Sequence at index {i} contains invalid characters: {sorted(invalid_chars)}. " f"Sequences should only contain valid biological sequence characters." ) # Warn about very short sequences (like C CLI warnings) very_short_sequences = [ i for i, seq in enumerate(sequences) if len(seq.strip()) < 3 ] if very_short_sequences and len(very_short_sequences) > len(sequences) * 0.5: import warnings warnings.warn( f"Many sequences are very short (< 3 characters). This may affect alignment quality.", UserWarning, stacklevel=2, ) # Convert string sequence types to integers seq_type_map = { "auto": AUTO, "dna": DNA, "rna": RNA, "protein": PROTEIN, "pfasum43": PROTEIN_PFASUM43, "pfasum60": PROTEIN_PFASUM60, "pfasum": PROTEIN_PFASUM_AUTO, "divergent": PROTEIN_DIVERGENT, "internal": DNA_INTERNAL, } if isinstance(seq_type, str): seq_type_lower = seq_type.lower() if seq_type_lower not in seq_type_map: raise ValueError( f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" ) seq_type_int = seq_type_map[seq_type_lower] else: seq_type_int = seq_type # Parameter validation and defaults # The C core expects positive penalty values (e.g., gpo=5.5, gpe=2.0). # A value of -1.0 signals "use defaults". if gap_open is None: gap_open = -1.0 elif not isinstance(gap_open, (int, float)): raise ValueError("gap_open must be a number") elif gap_open < 0: raise ValueError("gap_open must be a positive number (penalty value)") if gap_extend is None: gap_extend = -1.0 elif not isinstance(gap_extend, (int, float)): raise ValueError("gap_extend must be a number") elif gap_extend < 0: raise ValueError("gap_extend must be a positive number (penalty value)") if terminal_gap_extend is None: terminal_gap_extend = -1.0 elif not isinstance(terminal_gap_extend, (int, float)): raise ValueError("terminal_gap_extend must be a number") elif terminal_gap_extend < 0: raise ValueError( "terminal_gap_extend must be a positive number (penalty value)" ) # Handle thread count if n_threads is None: n_threads = get_num_threads() elif not isinstance(n_threads, int): raise ValueError("n_threads must be an integer") elif n_threads < 1: raise ValueError("n_threads must be at least 1") elif n_threads > 1024: # reasonable upper limit import warnings warnings.warn( f"Very high thread count ({n_threads}) may not improve performance and could cause issues.", UserWarning, stacklevel=2, ) # Resolve mode presets — collect explicitly-set params _explicit = {} # We detect "explicit" by checking if the value differs from the function signature default. # For mode-relevant params, the signature defaults match the "default" mode preset. if ensemble != 0: _explicit["ensemble"] = ensemble if realign != 0: _explicit["realign"] = realign if consistency != 5: _explicit["consistency"] = consistency if consistency_weight != 2.0: _explicit["consistency_weight"] = consistency_weight if vsm_amax != -1.0: _explicit["vsm_amax"] = vsm_amax resolved = _resolve_mode(mode, _explicit) ensemble = resolved.get("ensemble", ensemble) realign = resolved.get("realign", realign) consistency = resolved.get("consistency", consistency) consistency_weight = resolved.get("consistency_weight", consistency_weight) vsm_amax = resolved.get("vsm_amax", vsm_amax) # Convert refine mode refine_int = _parse_refine_mode(refine) # Validate ensemble if not isinstance(ensemble, int) or ensemble < 0: raise ValueError("ensemble must be a non-negative integer") # Call the C++ binding for core alignment confidence_data = None try: result = _core.align( sequences, seq_type_int, gap_open, gap_extend, terminal_gap_extend, n_threads, refine_int, ensemble, min_support, float(seq_weights), consistency, consistency_weight, vsm_amax, realign, ensemble_seed, ) # When ensemble is used, result is (sequences, confidence_dict) if isinstance(result, tuple): aligned_seqs = result[0] confidence_data = result[1] else: aligned_seqs = result except Exception as e: raise RuntimeError(f"Alignment failed: {str(e)}") # Validate IDs if provided (applies to all formats) if ids is not None and len(ids) != len(aligned_seqs): raise ValueError( f"Number of IDs ({len(ids)}) must match number of sequences ({len(aligned_seqs)})" ) # Handle different return formats if fmt == "plain": return aligned_seqs # Generate IDs if not provided (only for ecosystem formats) if ids is None: ids = [f"seq{i}" for i in range(len(aligned_seqs))] if fmt == "biopython": try: MultipleSeqAlignment = import_module("Bio.Align").MultipleSeqAlignment SeqRecord = import_module("Bio.SeqRecord").SeqRecord Seq = import_module("Bio.Seq").Seq except ModuleNotFoundError as e: raise ImportError( "Biopython not installed. Run: pip install kalign-python[biopython]" ) from e records = [] for idx, (s, i) in enumerate(zip(aligned_seqs, ids)): rec = SeqRecord(Seq(s), id=i) # Attach per-residue confidence as letter_annotations if available if confidence_data is not None: res_conf = confidence_data["residue_confidence"] if idx < len(res_conf) and len(res_conf[idx]) == len(s): pp_str = _confidence_to_pp_string(s, res_conf[idx]) rec.letter_annotations["posterior_probability"] = pp_str records.append(rec) return MultipleSeqAlignment(records) if fmt == "skbio": try: skbio_mod = import_module("skbio") skbio_seq = import_module("skbio.sequence") TabularMSA = skbio_mod.TabularMSA except ModuleNotFoundError as e: raise ImportError( "scikit-bio not installed. Run: pip install kalign-python[skbio]" ) from e # Select the appropriate skbio sequence type skbio_type_map = { DNA: skbio_seq.DNA, DNA_INTERNAL: skbio_seq.DNA, RNA: skbio_seq.RNA, PROTEIN: skbio_seq.Protein, PROTEIN_DIVERGENT: skbio_seq.Protein, } if seq_type_int in skbio_type_map: SeqClass = skbio_type_map[seq_type_int] else: # AUTO or unknown: infer from sequence content SeqClass = _infer_skbio_type(sequences, skbio_seq) return TabularMSA( [SeqClass(s, metadata={"id": i}) for s, i in zip(aligned_seqs, ids)] ) raise ValueError(f"Unknown fmt='{fmt}' (expected 'plain', 'biopython', 'skbio')") def _parse_refine_mode(refine): """Convert string or int refine mode to integer constant.""" if isinstance(refine, int): return refine refine_map = { "none": REFINE_NONE, "all": REFINE_ALL, "confident": REFINE_CONFIDENT, "inline": REFINE_INLINE, } refine_lower = refine.lower() if refine_lower not in refine_map: raise ValueError( f"Invalid refine mode: {refine}. Must be one of: {list(refine_map.keys())}" ) return refine_map[refine_lower] def _infer_skbio_type(sequences, skbio_seq): """Infer the appropriate skbio sequence class from raw sequence content.""" chars = set() for seq in sequences: chars.update(seq.upper().replace("-", "").replace(".", "")) dna_chars = set("ACGTNRYSWKMBDHV") rna_chars = set("ACGUNRYSWKMBDHV") if "U" in chars and "T" not in chars and chars <= rna_chars: return skbio_seq.RNA if chars <= dna_chars: return skbio_seq.DNA return skbio_seq.Protein def _resolve_mode(mode, explicit_kwargs): """Resolve mode presets, letting explicit parameters override. Parameters ---------- mode : str or None One of "default", "fast", "precise", or None (treated as "default"). explicit_kwargs : dict Only keys that the caller *explicitly* passed (not sentinel/default). Returns ------- dict Merged parameter values: mode defaults + explicit overrides. """ if mode is None: mode = "default" mode_lower = mode.lower() if mode_lower not in _MODE_PRESETS: raise ValueError( f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'precise'" ) result = dict(_MODE_PRESETS[mode_lower]) result.update(explicit_kwargs) return result def set_num_threads(n: int) -> None: """ Set the default number of threads for alignment operations. This affects all future calls to align() that don't explicitly specify n_threads. The setting is thread-local, so different threads can have different defaults. Parameters ---------- n : int Number of threads to use. Must be at least 1. Raises ------ ValueError If n is less than 1 Examples -------- >>> import kalign >>> kalign.set_num_threads(4) >>> aligned = kalign.align(sequences) # Uses 4 threads """ global _default_threads if n < 1: raise ValueError("Number of threads must be at least 1") # Use thread-local storage for thread safety _thread_local.num_threads = n # Also update global default for new threads _default_threads = n def get_num_threads() -> int: """ Get the current default number of threads for alignment operations. Returns ------- int Current default number of threads Examples -------- >>> import kalign >>> kalign.get_num_threads() 1 >>> kalign.set_num_threads(8) >>> kalign.get_num_threads() 8 """ return getattr(_thread_local, "num_threads", _default_threads) def align_from_file( input_file: str, seq_type: Union[str, int] = "auto", gap_open: Optional[float] = None, gap_extend: Optional[float] = None, terminal_gap_extend: Optional[float] = None, n_threads: Optional[int] = None, refine: Union[str, int] = "none", adaptive_budget: bool = False, ensemble: int = 0, ensemble_seed: int = 42, dist_scale: float = 0.0, vsm_amax: float = -1.0, min_support: int = 0, realign: int = 0, save_poar: str = "", load_poar: str = "", seq_weights: float = 0.0, consistency: int = 5, consistency_weight: float = 2.0, mode: Optional[str] = None, ) -> AlignedSequences: """ Align sequences from a file using Kalign. Parameters ---------- input_file : str Path to input file containing sequences. Supported formats: FASTA, MSF, Clustal, aligned FASTA. seq_type : str or int, optional Sequence type specification (same as align function) gap_open : float, optional Gap opening penalty gap_extend : float, optional Gap extension penalty terminal_gap_extend : float, optional Terminal gap extension penalty n_threads : int, optional Number of threads to use for alignment vsm_amax : float, optional Variable scoring matrix a_max parameter (default: -1.0 = use kalign defaults: 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 to disable. When > 0, subtracts max(0, amax - d) from all substitution scores, where d is the estimated pairwise distance. Returns ------- AlignedSequences Named tuple with ``names`` and ``sequences`` fields. Raises ------ FileNotFoundError If input file doesn't exist RuntimeError If alignment fails """ if not os.path.exists(input_file): raise FileNotFoundError(f"Input file not found: {input_file}") # Convert string sequence types to integers seq_type_map = { "auto": AUTO, "dna": DNA, "rna": RNA, "protein": PROTEIN, "pfasum43": PROTEIN_PFASUM43, "pfasum60": PROTEIN_PFASUM60, "pfasum": PROTEIN_PFASUM_AUTO, "divergent": PROTEIN_DIVERGENT, "internal": DNA_INTERNAL, } if isinstance(seq_type, str): seq_type_lower = seq_type.lower() if seq_type_lower not in seq_type_map: raise ValueError( f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" ) seq_type_int = seq_type_map[seq_type_lower] else: seq_type_int = seq_type # Use defaults if not specified if gap_open is None: gap_open = -1.0 if gap_extend is None: gap_extend = -1.0 if terminal_gap_extend is None: terminal_gap_extend = -1.0 # Handle thread count if n_threads is None: n_threads = get_num_threads() if n_threads < 1: raise ValueError("n_threads must be at least 1") # Resolve mode presets _explicit = {} if ensemble != 0: _explicit["ensemble"] = ensemble if realign != 0: _explicit["realign"] = realign if consistency != 5: _explicit["consistency"] = consistency if consistency_weight != 2.0: _explicit["consistency_weight"] = consistency_weight if vsm_amax != -1.0: _explicit["vsm_amax"] = vsm_amax resolved = _resolve_mode(mode, _explicit) ensemble = resolved.get("ensemble", ensemble) realign = resolved.get("realign", realign) consistency = resolved.get("consistency", consistency) consistency_weight = resolved.get("consistency_weight", consistency_weight) vsm_amax = resolved.get("vsm_amax", vsm_amax) # Convert refine mode refine_int = _parse_refine_mode(refine) # Call the C++ binding — returns (names, sequences) or (names, sequences, confidence) try: result = _core.align_from_file( input_file, seq_type_int, gap_open, gap_extend, terminal_gap_extend, n_threads, refine_int, int(adaptive_budget), ensemble, ensemble_seed, dist_scale, vsm_amax, min_support, realign, save_poar, load_poar, float(seq_weights), consistency, consistency_weight, ) if len(result) == 3: names, sequences, conf = result col_conf = list(conf["column_confidence"]) res_conf = [list(row) for row in conf["residue_confidence"]] return AlignedSequences( names=names, sequences=sequences, column_confidence=col_conf, residue_confidence=res_conf, ) else: names, sequences = result return AlignedSequences(names=names, sequences=sequences) except Exception as e: raise RuntimeError(f"Alignment failed: {str(e)}") def write_alignment( sequences: List[str], output_file: str, format: str = "fasta", ids: Optional[List[str]] = None, column_confidence: Optional[List[float]] = None, residue_confidence: Optional[List[List[float]]] = None, ) -> None: """ Write aligned sequences to a file. Parameters ---------- sequences : list of str List of aligned sequences output_file : str Path to output file format : str, optional Output format: "fasta", "clustal", "stockholm", "phylip" (default: "fasta") ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. Raises ------ ValueError If invalid format or empty sequence list ImportError If Biopython is not installed for non-FASTA formats Examples -------- >>> aligned = kalign.align(sequences) >>> kalign.write_alignment(aligned, "output.fasta") >>> kalign.write_alignment(aligned, "output.aln", format="clustal", ids=["seq1", "seq2"]) """ if not sequences: raise ValueError("Empty sequence list provided") format_lower = format.lower() # Map format aliases format_map = { "fasta": "fasta", "fa": "fasta", "clustal": "clustal", "aln": "clustal", "stockholm": "stockholm", "sto": "stockholm", "phylip": "phylip", "phy": "phylip", } if format_lower not in format_map: raise ValueError( f"Invalid format: {format}. Must be one of: fasta, clustal, stockholm, phylip" ) mapped_format = format_map[format_lower] # Use appropriate writer from io module (lazy import to avoid circular imports) from . import io if mapped_format == "fasta": io.write_fasta(sequences, output_file, ids=ids) elif mapped_format == "clustal": io.write_clustal(sequences, output_file, ids=ids) elif mapped_format == "stockholm": io.write_stockholm( sequences, output_file, ids=ids, column_confidence=column_confidence, residue_confidence=residue_confidence, ) elif mapped_format == "phylip": io.write_phylip(sequences, output_file, ids=ids) def generate_test_sequences( n_seq: int, n_obs: int, dna: bool, length: int, seed: int = 42 ) -> List[str]: """ Generate test sequences using DSSim HMM-based simulator. This function uses the DSSim (Dynamic Sequence Simulator) from the Kalign test suite to generate realistic evolutionary sequence data for testing and benchmarking purposes. Parameters ---------- n_seq : int Number of sequences to generate n_obs : int Number of observed sequences for training the HMM (typically 20-50) dna : bool True to generate DNA sequences, False to generate protein sequences length : int Target sequence length seed : int, optional Random seed for reproducible results (default: 42) Returns ------- list of str List of generated sequences Raises ------ RuntimeError If sequence generation fails Examples -------- >>> import kalign >>> # Generate 100 DNA sequences of length 200 >>> dna_seqs = kalign.generate_test_sequences(100, 20, True, 200) >>> len(dna_seqs) 100 >>> len(dna_seqs[0]) 200 >>> # Generate 50 protein sequences of length 150 >>> protein_seqs = kalign.generate_test_sequences(50, 30, False, 150) >>> len(protein_seqs) 50 """ if n_seq < 1: raise ValueError("n_seq must be at least 1") if n_obs < 1: raise ValueError("n_obs must be at least 1") if length < 1: raise ValueError("length must be at least 1") try: sequences = _core.generate_test_sequences(n_seq, n_obs, dna, length, seed) return sequences except Exception as e: raise RuntimeError(f"Test sequence generation failed: {str(e)}") def compare(reference_file: str, test_file: str) -> float: """ Compare two multiple sequence alignments and return SP score. Computes the sum-of-pairs (SP) score between a reference alignment and a test alignment. Sequences are matched by name, so both files must contain the same sequences. Parameters ---------- reference_file : str Path to reference alignment file (FASTA, MSF, or Clustal format) test_file : str Path to test alignment file Returns ------- float SP score (0-100), where 100 means identical alignments Raises ------ FileNotFoundError If either file doesn't exist RuntimeError If comparison fails """ if not os.path.exists(reference_file): raise FileNotFoundError(f"Reference file not found: {reference_file}") if not os.path.exists(test_file): raise FileNotFoundError(f"Test file not found: {test_file}") return _core.compare(reference_file, test_file) def compare_detailed( reference_file: str, test_file: str, max_gap_frac: float = 0.2, column_mask: Optional[List[int]] = None, ) -> dict: """ Compare two multiple sequence alignments returning detailed POAR scores. Computes BAliBASE-compatible recall (SP), precision, F1, and TC scores. Only "core" columns (gap fraction <= max_gap_frac in reference) are scored for recall/TC. Gap-gap matches are NOT counted. Parameters ---------- reference_file : str Path to reference alignment file (FASTA, MSF, or Clustal format) test_file : str Path to test alignment file max_gap_frac : float, optional Maximum gap fraction for a reference column to be scored (default: 0.2). Use -1.0 to score all columns regardless of gap content. Ignored when column_mask is provided. column_mask : list of int, optional Explicit binary mask (0/1) for each column in the reference alignment. When provided, only columns with mask=1 are scored (overrides max_gap_frac). Typically parsed from BAliBASE XML core block annotations. Returns ------- dict Keys: recall, precision, f1, tc, ref_pairs, test_pairs, common_pairs Raises ------ FileNotFoundError If either file doesn't exist RuntimeError If comparison fails """ if not os.path.exists(reference_file): raise FileNotFoundError(f"Reference file not found: {reference_file}") if not os.path.exists(test_file): raise FileNotFoundError(f"Test file not found: {test_file}") if column_mask is not None: return _core.compare_detailed_with_mask(reference_file, test_file, column_mask) return _core.compare_detailed(reference_file, test_file, max_gap_frac) def align_file_to_file( input_file: str, output_file: str, format: str = "fasta", seq_type: Union[str, int] = "auto", gap_open: Optional[float] = None, gap_extend: Optional[float] = None, terminal_gap_extend: Optional[float] = None, n_threads: Optional[int] = None, refine: Union[str, int] = "none", adaptive_budget: bool = False, ensemble: int = 0, ensemble_seed: int = 42, dist_scale: float = 0.0, vsm_amax: float = -1.0, min_support: int = 0, realign: int = 0, save_poar: str = "", load_poar: str = "", seq_weights: float = 0.0, consistency: int = 5, consistency_weight: float = 2.0, mode: Optional[str] = None, ) -> None: """ Align sequences from input file and write result to output file. Unlike align_from_file(), this preserves all sequence metadata (names, descriptions), which is required for MSA comparison with compare(). Parameters ---------- input_file : str Path to input file containing unaligned sequences output_file : str Path to output alignment file format : str, optional Output format: "fasta", "msf", "clu" (default: "fasta") seq_type : str or int, optional Sequence type (default: "auto") gap_open : float, optional Gap opening penalty gap_extend : float, optional Gap extension penalty terminal_gap_extend : float, optional Terminal gap extension penalty n_threads : int, optional Number of threads (default: uses global setting) vsm_amax : float, optional Variable scoring matrix a_max parameter (default: -1.0 = use kalign defaults: 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 to disable. Raises ------ FileNotFoundError If input file doesn't exist RuntimeError If alignment or writing fails """ if not os.path.exists(input_file): raise FileNotFoundError(f"Input file not found: {input_file}") seq_type_map = { "auto": AUTO, "dna": DNA, "rna": RNA, "protein": PROTEIN, "pfasum43": PROTEIN_PFASUM43, "pfasum60": PROTEIN_PFASUM60, "pfasum": PROTEIN_PFASUM_AUTO, "divergent": PROTEIN_DIVERGENT, "internal": DNA_INTERNAL, } if isinstance(seq_type, str): seq_type_lower = seq_type.lower() if seq_type_lower not in seq_type_map: raise ValueError( f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" ) seq_type_int = seq_type_map[seq_type_lower] else: seq_type_int = seq_type if gap_open is None: gap_open = -1.0 if gap_extend is None: gap_extend = -1.0 if terminal_gap_extend is None: terminal_gap_extend = -1.0 if n_threads is None: n_threads = get_num_threads() # Resolve mode presets _explicit = {} if ensemble != 0: _explicit["ensemble"] = ensemble if realign != 0: _explicit["realign"] = realign if consistency != 5: _explicit["consistency"] = consistency if consistency_weight != 2.0: _explicit["consistency_weight"] = consistency_weight if vsm_amax != -1.0: _explicit["vsm_amax"] = vsm_amax resolved = _resolve_mode(mode, _explicit) ensemble = resolved.get("ensemble", ensemble) realign = resolved.get("realign", realign) consistency = resolved.get("consistency", consistency) consistency_weight = resolved.get("consistency_weight", consistency_weight) vsm_amax = resolved.get("vsm_amax", vsm_amax) refine_int = _parse_refine_mode(refine) _core.align_file_to_file( input_file, output_file, format, seq_type_int, gap_open, gap_extend, terminal_gap_extend, n_threads, refine_int, int(adaptive_budget), ensemble, ensemble_seed, dist_scale, vsm_amax, min_support, realign, save_poar, load_poar, float(seq_weights), consistency, consistency_weight, ) # Convenience aliases kalign = align # For backward compatibility or alternative naming __all__ = [ "align", "align_from_file", "align_file_to_file", "compare", "compare_detailed", "write_alignment", "generate_test_sequences", "set_num_threads", "get_num_threads", "kalign", "AlignedSequences", "DNA", "DNA_INTERNAL", "RNA", "PROTEIN", "PROTEIN_PFASUM43", "PROTEIN_PFASUM60", "PROTEIN_PFASUM_AUTO", "PROTEIN_DIVERGENT", "AUTO", "REFINE_NONE", "REFINE_ALL", "REFINE_CONFIDENT", "REFINE_INLINE", "MODE_DEFAULT", "MODE_FAST", "MODE_PRECISE", "__version__", "__author__", "__email__", "io", "utils", ] kalign-3.5.1/python-kalign/_core.cpp000066400000000000000000000644661515023132300173570ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include // Include DSSim for sequence simulation and MSA structures extern "C" { #include "msa_struct.h" #include "msa_alloc.h" #include "msa_op.h" #include "msa_cmp.h" #include "dssim.h" } namespace py = pybind11; // Helper function to convert C strings to Python strings and free memory std::vector c_strings_to_python(char** c_strings, int count, int length) { std::vector result; result.reserve(count); for (int i = 0; i < count; ++i) { if (c_strings[i]) { result.emplace_back(c_strings[i], length); free(c_strings[i]); } } free(c_strings); return result; } // Helper to extract confidence data from MSA before freeing static py::object extract_confidence(struct msa* msa_data, int numseq) { if (!msa_data->col_confidence) { return py::none(); } int alnlen = msa_data->alnlen; // Per-column confidence py::list col_conf; for (int c = 0; c < alnlen; c++) { col_conf.append(msa_data->col_confidence[c]); } // Per-residue confidence py::list res_conf; for (int i = 0; i < numseq; i++) { py::list row; if (msa_data->sequences[i]->confidence) { for (int c = 0; c < alnlen; c++) { row.append(msa_data->sequences[i]->confidence[c]); } } res_conf.append(row); } py::dict result; result["column_confidence"] = col_conf; result["residue_confidence"] = res_conf; return result; } // Shared alignment routing helper — selects the appropriate kalign function // based on the combination of parameters provided. static int run_alignment(struct msa* msa_data, int n_threads, int seq_type, float gap_open, float gap_extend, float terminal_gap_extend, int refine, int adaptive_budget, int ensemble, uint64_t ensemble_seed, float dist_scale, float vsm_amax, int min_support, int realign, const std::string& save_poar, const std::string& load_poar, float use_seq_weights = -1.0f, int consistency_anchors = 0, float consistency_weight = 2.0f) { if (!load_poar.empty()) { return kalign_consensus_from_poar(msa_data, load_poar.c_str(), min_support > 0 ? min_support : 2); } else if (ensemble > 0) { const char* save_path = save_poar.empty() ? nullptr : save_poar.c_str(); return kalign_ensemble(msa_data, n_threads, seq_type, ensemble, gap_open, gap_extend, terminal_gap_extend, ensemble_seed, min_support, save_path, refine, dist_scale, vsm_amax, realign, use_seq_weights, consistency_anchors, consistency_weight); } else if (realign > 0) { return kalign_run_realign(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, dist_scale, vsm_amax, realign, use_seq_weights, consistency_anchors, consistency_weight); } else if (consistency_anchors > 0) { return kalign_run_seeded(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, 0, 0.0f, dist_scale, vsm_amax, use_seq_weights, consistency_anchors, consistency_weight); } else if (dist_scale > 0.0f || vsm_amax >= 0.0f || use_seq_weights >= 0.0f) { return kalign_run_dist_scale(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, dist_scale, vsm_amax, use_seq_weights); } else { return kalign_run(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget); } } // Main alignment function py::object align_sequences( const std::vector& sequences, int seq_type = KALIGN_TYPE_UNDEFINED, float gap_open = -1.0f, float gap_extend = -1.0f, float terminal_gap_extend = -1.0f, int n_threads = 1, int refine = KALIGN_REFINE_NONE, int ensemble = 0, int min_support = 0, float seq_weights = -1.0f, int consistency_anchors = 0, float consistency_weight = 2.0f, float vsm_amax = -1.0f, int realign = 0, uint64_t ensemble_seed = 42 ) { if (sequences.empty()) { throw std::invalid_argument("Empty sequence list provided"); } // Convert Python strings to C format std::vector seq_ptrs; std::vector seq_lengths; seq_ptrs.reserve(sequences.size()); seq_lengths.reserve(sequences.size()); for (const auto& seq : sequences) { seq_ptrs.push_back(const_cast(seq.c_str())); seq_lengths.push_back(static_cast(seq.length())); } if (n_threads < 1) { n_threads = 1; } // Build msa struct from input arrays struct msa* msa_data = nullptr; int result = kalign_arr_to_msa(seq_ptrs.data(), seq_lengths.data(), static_cast(sequences.size()), &msa_data); if (result != 0 || !msa_data) { throw std::runtime_error("Failed to create MSA from input sequences"); } msa_data->quiet = 1; // Route to appropriate alignment function result = run_alignment(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, 0, ensemble, ensemble_seed, 0.0f, vsm_amax, min_support, realign, "", "", seq_weights, consistency_anchors, consistency_weight); if (result != 0) { kalign_free_msa(msa_data); throw std::runtime_error("Kalign alignment failed with error code: " + std::to_string(result)); } // Extract confidence data before converting to arrays (which frees the MSA) py::object confidence = extract_confidence(msa_data, static_cast(sequences.size())); // Extract aligned sequences char** aligned_seqs = nullptr; int alignment_length = 0; result = kalign_msa_to_arr(msa_data, &aligned_seqs, &alignment_length); kalign_free_msa(msa_data); if (result != 0 || !aligned_seqs) { throw std::runtime_error("Failed to extract aligned sequences"); } // Convert results back to Python auto seqs = c_strings_to_python(aligned_seqs, static_cast(sequences.size()), alignment_length); // If ensemble was used and confidence data exists, return tuple if (ensemble > 0 && !confidence.is_none()) { return py::make_tuple(seqs, confidence); } // Otherwise return just sequences (backward compat) return py::cast(seqs); } // File-based alignment function — returns (names, sequences) or (names, sequences, confidence) py::object align_from_file( const std::string& input_file, int seq_type = KALIGN_TYPE_UNDEFINED, float gap_open = -1.0f, float gap_extend = -1.0f, float terminal_gap_extend = -1.0f, int n_threads = 1, int refine = KALIGN_REFINE_NONE, int adaptive_budget = 0, int ensemble = 0, uint64_t ensemble_seed = 42, float dist_scale = 0.0f, float vsm_amax = -1.0f, int min_support = 0, int realign = 0, const std::string& save_poar = "", const std::string& load_poar = "", float seq_weights = -1.0f, int consistency_anchors = 0, float consistency_weight = 2.0f ) { struct msa* msa_data = nullptr; // Read input file int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); if (result != 0) { throw std::runtime_error("Failed to read input file: " + input_file); } // Check if msa_data is NULL - this happens when the file format cannot be detected if (!msa_data) { throw std::runtime_error("Could not detect valid sequence format in file: " + input_file); } // Perform alignment result = run_alignment(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, ensemble, ensemble_seed, dist_scale, vsm_amax, min_support, realign, save_poar, load_poar, seq_weights, consistency_anchors, consistency_weight); if (result != 0) { kalign_free_msa(msa_data); throw std::runtime_error("Kalign alignment failed with error code: " + std::to_string(result)); } // Extract confidence data before writing (which doesn't preserve it) py::object confidence = extract_confidence(msa_data, msa_data->numseq); // Write to a temporary file so kalign_write_msa handles gap insertion const char* tmpdir = std::getenv("TMPDIR"); if (!tmpdir) tmpdir = std::getenv("TMP"); if (!tmpdir) tmpdir = std::getenv("TEMP"); if (!tmpdir) tmpdir = "/tmp"; std::string temp_file = std::string(tmpdir) + "/kalign_output.fa"; result = kalign_write_msa(msa_data, const_cast(temp_file.c_str()), const_cast("fasta")); kalign_free_msa(msa_data); if (result != 0) { throw std::runtime_error("Failed to write alignment results"); } // Parse the FASTA file, capturing both headers (names) and sequences std::ifstream file(temp_file); std::vector names; std::vector aligned_sequences; std::string line, current_name, current_seq; while (std::getline(file, line)) { if (line.empty()) continue; if (line[0] == '>') { if (!current_seq.empty()) { names.push_back(current_name); aligned_sequences.push_back(current_seq); current_seq.clear(); } // Strip the '>' prefix; take everything up to the first whitespace as the name current_name = line.substr(1); auto ws = current_name.find_first_of(" \t"); if (ws != std::string::npos) { current_name = current_name.substr(0, ws); } } else { current_seq += line; } } if (!current_seq.empty()) { names.push_back(current_name); aligned_sequences.push_back(current_seq); } // Clean up temp file std::remove(temp_file.c_str()); // If confidence data exists, return 3-tuple if (!confidence.is_none()) { return py::make_tuple(names, aligned_sequences, confidence); } return py::make_tuple(names, aligned_sequences); } // Generate test sequences using DSSim std::vector generate_test_sequences( int n_seq, int n_obs, bool dna, int length, int seed = 42 ) { struct msa* msa_data = nullptr; // Call DSSim to generate sequences int result = dssim_get_fasta(&msa_data, n_seq, n_obs, dna ? 1 : 0, length, seed); if (result != 0) { throw std::runtime_error("DSSim sequence generation failed with error code: " + std::to_string(result)); } if (!msa_data) { throw std::runtime_error("DSSim returned null MSA data"); } // Convert MSA sequences to Python strings std::vector sequences; sequences.reserve(msa_data->numseq); for (int i = 0; i < msa_data->numseq; ++i) { if (msa_data->sequences[i] && msa_data->sequences[i]->seq) { sequences.emplace_back(msa_data->sequences[i]->seq, msa_data->sequences[i]->len); } } // Clean up kalign_free_msa(msa_data); return sequences; } // Compare two MSA files (reference vs test), returning SP score float compare_msa_files(const std::string& reference_file, const std::string& test_file) { struct msa* ref = nullptr; struct msa* test = nullptr; float score = 0.0f; int result = kalign_read_input(const_cast(reference_file.c_str()), &ref, 1); if (result != 0 || !ref) { throw std::runtime_error("Failed to read reference file: " + reference_file); } result = kalign_read_input(const_cast(test_file.c_str()), &test, 1); if (result != 0 || !test) { kalign_free_msa(ref); throw std::runtime_error("Failed to read test file: " + test_file); } result = kalign_msa_compare(ref, test, &score); kalign_free_msa(ref); kalign_free_msa(test); if (result != 0) { throw std::runtime_error("MSA comparison failed"); } return score; } // Compare two MSA files returning detailed POAR scores py::dict compare_detailed_files(const std::string& reference_file, const std::string& test_file, float max_gap_frac = 0.2f) { struct msa* ref = nullptr; struct msa* test = nullptr; struct poar_score score; int result = kalign_read_input(const_cast(reference_file.c_str()), &ref, 1); if (result != 0 || !ref) { throw std::runtime_error("Failed to read reference file: " + reference_file); } result = kalign_read_input(const_cast(test_file.c_str()), &test, 1); if (result != 0 || !test) { kalign_free_msa(ref); throw std::runtime_error("Failed to read test file: " + test_file); } result = kalign_msa_compare_detailed(ref, test, max_gap_frac, &score); kalign_free_msa(ref); kalign_free_msa(test); if (result != 0) { throw std::runtime_error("Detailed MSA comparison failed"); } py::dict d; d["recall"] = score.recall; d["precision"] = score.precision; d["f1"] = score.f1; d["tc"] = score.tc; d["ref_pairs"] = score.ref_pairs; d["test_pairs"] = score.test_pairs; d["common_pairs"] = score.common; return d; } // Compare two MSA files with an explicit column mask py::dict compare_detailed_with_mask_files(const std::string& reference_file, const std::string& test_file, py::list column_mask) { struct msa* ref = nullptr; struct msa* test = nullptr; struct poar_score score; int result = kalign_read_input(const_cast(reference_file.c_str()), &ref, 1); if (result != 0 || !ref) { throw std::runtime_error("Failed to read reference file: " + reference_file); } result = kalign_read_input(const_cast(test_file.c_str()), &test, 1); if (result != 0 || !test) { kalign_free_msa(ref); throw std::runtime_error("Failed to read test file: " + test_file); } // Convert py::list to int array int n_cols = static_cast(column_mask.size()); std::vector mask(n_cols); for (int i = 0; i < n_cols; i++) { mask[i] = column_mask[i].cast(); } result = kalign_msa_compare_with_mask(ref, test, mask.data(), n_cols, &score); kalign_free_msa(ref); kalign_free_msa(test); if (result != 0) { throw std::runtime_error("Detailed MSA comparison with mask failed"); } py::dict d; d["recall"] = score.recall; d["precision"] = score.precision; d["f1"] = score.f1; d["tc"] = score.tc; d["ref_pairs"] = score.ref_pairs; d["test_pairs"] = score.test_pairs; d["common_pairs"] = score.common; return d; } // Align sequences from input file and write result to output file, preserving all metadata void align_file_to_file( const std::string& input_file, const std::string& output_file, const std::string& format = "fasta", int seq_type = KALIGN_TYPE_UNDEFINED, float gap_open = -1.0f, float gap_extend = -1.0f, float terminal_gap_extend = -1.0f, int n_threads = 1, int refine = KALIGN_REFINE_NONE, int adaptive_budget = 0, int ensemble = 0, uint64_t ensemble_seed = 42, float dist_scale = 0.0f, float vsm_amax = -1.0f, int min_support = 0, int realign = 0, const std::string& save_poar = "", const std::string& load_poar = "", float seq_weights = -1.0f, int consistency_anchors = 0, float consistency_weight = 2.0f ) { struct msa* msa_data = nullptr; int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); if (result != 0 || !msa_data) { throw std::runtime_error("Failed to read input file: " + input_file); } result = run_alignment(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, ensemble, ensemble_seed, dist_scale, vsm_amax, min_support, realign, save_poar, load_poar, seq_weights, consistency_anchors, consistency_weight); if (result != 0) { kalign_free_msa(msa_data); throw std::runtime_error("Alignment failed with error code: " + std::to_string(result)); } result = kalign_write_msa(msa_data, const_cast(output_file.c_str()), const_cast(format.c_str())); kalign_free_msa(msa_data); if (result != 0) { throw std::runtime_error("Failed to write output file: " + output_file); } } PYBIND11_MODULE(_core, m) { m.doc() = "Python bindings for Kalign multiple sequence alignment"; // Main alignment function m.def("align", &align_sequences, py::arg("sequences"), py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, py::arg("gap_open") = -1.0f, py::arg("gap_extend") = -1.0f, py::arg("terminal_gap_extend") = -1.0f, py::arg("n_threads") = 1, py::arg("refine") = KALIGN_REFINE_NONE, py::arg("ensemble") = 0, py::arg("min_support") = 0, py::arg("seq_weights") = -1.0f, py::arg("consistency_anchors") = 0, py::arg("consistency_weight") = 2.0f, py::arg("vsm_amax") = -1.0f, py::arg("realign") = 0, py::arg("ensemble_seed") = (uint64_t)42, R"pbdoc( Align a list of sequences using Kalign. Parameters ---------- sequences : list of str List of sequences to align seq_type : int, optional Sequence type (default: auto-detect) gap_open : float, optional Gap opening penalty (default: -1.0, uses Kalign defaults) gap_extend : float, optional Gap extension penalty (default: -1.0, uses Kalign defaults) terminal_gap_extend : float, optional Terminal gap extension penalty (default: -1.0, uses Kalign defaults) n_threads : int, optional Number of threads to use (default: 1) refine : int, optional Refinement mode (default: REFINE_NONE) ensemble : int, optional Number of ensemble runs (default: 0 = off) min_support : int, optional Explicit consensus threshold (default: 0 = auto) vsm_amax : float, optional Variable scoring matrix amplitude (default: -1.0, uses Kalign defaults) realign : int, optional Number of realignment iterations (default: 0 = off) ensemble_seed : int, optional RNG seed for ensemble (default: 42) Returns ------- list of str or tuple When ensemble > 0: (aligned_seqs, confidence_dict) Otherwise: aligned sequences )pbdoc"); // File-based alignment — returns (names, sequences) or (names, sequences, confidence) m.def("align_from_file", &align_from_file, py::arg("input_file"), py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, py::arg("gap_open") = -1.0f, py::arg("gap_extend") = -1.0f, py::arg("terminal_gap_extend") = -1.0f, py::arg("n_threads") = 1, py::arg("refine") = KALIGN_REFINE_NONE, py::arg("adaptive_budget") = 0, py::arg("ensemble") = 0, py::arg("ensemble_seed") = (uint64_t)42, py::arg("dist_scale") = 0.0f, py::arg("vsm_amax") = -1.0f, py::arg("min_support") = 0, py::arg("realign") = 0, py::arg("save_poar") = "", py::arg("load_poar") = "", py::arg("seq_weights") = -1.0f, py::arg("consistency_anchors") = 0, py::arg("consistency_weight") = 2.0f, "Align sequences from a file. Returns (names, sequences) or (names, sequences, confidence) tuple."); // Generate test sequences using DSSim m.def("generate_test_sequences", &generate_test_sequences, py::arg("n_seq"), py::arg("n_obs"), py::arg("dna"), py::arg("length"), py::arg("seed") = 42, R"pbdoc( Generate test sequences using DSSim HMM-based simulator. Parameters ---------- n_seq : int Number of sequences to generate n_obs : int Number of observed sequences for training the HMM dna : bool True for DNA sequences, False for protein sequences length : int Target sequence length seed : int, optional Random seed for reproducible results (default: 42) Returns ------- list of str Generated sequences )pbdoc"); // Compare two MSA files m.def("compare", &compare_msa_files, py::arg("reference_file"), py::arg("test_file"), R"pbdoc( Compare two multiple sequence alignments and return SP score. Parameters ---------- reference_file : str Path to reference alignment file test_file : str Path to test alignment file Returns ------- float SP score (0-100) )pbdoc"); // Detailed MSA comparison (POAR recall/precision/F1/TC) m.def("compare_detailed", &compare_detailed_files, py::arg("reference_file"), py::arg("test_file"), py::arg("max_gap_frac") = 0.2f, R"pbdoc( Compare two MSAs returning detailed POAR scores. Parameters ---------- reference_file : str Path to reference alignment file test_file : str Path to test alignment file max_gap_frac : float, optional Max gap fraction for scored columns (default: 0.2 for bali_score compat). Use -1.0 to score all columns. Returns ------- dict Keys: recall, precision, f1, tc, ref_pairs, test_pairs, common_pairs )pbdoc"); // Detailed MSA comparison with explicit column mask m.def("compare_detailed_with_mask", &compare_detailed_with_mask_files, py::arg("reference_file"), py::arg("test_file"), py::arg("column_mask"), R"pbdoc( Compare two MSAs with an explicit column mask. Parameters ---------- reference_file : str Path to reference alignment file test_file : str Path to test alignment file column_mask : list of int Binary mask (0/1) for each column in the reference alignment. Only columns with mask=1 are scored for recall/TC. Returns ------- dict Keys: recall, precision, f1, tc, ref_pairs, test_pairs, common_pairs )pbdoc"); // Align file to file (preserves sequence names/metadata) m.def("align_file_to_file", &align_file_to_file, py::arg("input_file"), py::arg("output_file"), py::arg("format") = "fasta", py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, py::arg("gap_open") = -1.0f, py::arg("gap_extend") = -1.0f, py::arg("terminal_gap_extend") = -1.0f, py::arg("n_threads") = 1, py::arg("refine") = KALIGN_REFINE_NONE, py::arg("adaptive_budget") = 0, py::arg("ensemble") = 0, py::arg("ensemble_seed") = (uint64_t)42, py::arg("dist_scale") = 0.0f, py::arg("vsm_amax") = -1.0f, py::arg("min_support") = 0, py::arg("realign") = 0, py::arg("save_poar") = "", py::arg("load_poar") = "", py::arg("seq_weights") = -1.0f, py::arg("consistency_anchors") = 0, py::arg("consistency_weight") = 2.0f, R"pbdoc( Align sequences from input file and write to output file. Unlike align_from_file, this preserves all sequence metadata (names, descriptions) which is required for MSA comparison. Parameters ---------- input_file : str Path to input sequence file output_file : str Path to output alignment file format : str, optional Output format: "fasta", "msf", "clu" (default: "fasta") seq_type : int, optional Sequence type (default: auto-detect) gap_open : float, optional Gap opening penalty gap_extend : float, optional Gap extension penalty terminal_gap_extend : float, optional Terminal gap extension penalty n_threads : int, optional Number of threads (default: 1) )pbdoc"); // Constants for sequence types m.attr("DNA") = KALIGN_TYPE_DNA; m.attr("DNA_INTERNAL") = KALIGN_TYPE_DNA_INTERNAL; m.attr("RNA") = KALIGN_TYPE_RNA; m.attr("PROTEIN") = KALIGN_TYPE_PROTEIN; m.attr("PROTEIN_PFASUM43") = KALIGN_TYPE_PROTEIN_PFASUM43; m.attr("PROTEIN_PFASUM60") = KALIGN_TYPE_PROTEIN_PFASUM60; m.attr("PROTEIN_PFASUM_AUTO") = KALIGN_TYPE_PROTEIN_PFASUM_AUTO; m.attr("PROTEIN_DIVERGENT") = KALIGN_TYPE_PROTEIN_DIVERGENT; m.attr("AUTO") = KALIGN_TYPE_UNDEFINED; // Constants for refinement modes m.attr("REFINE_NONE") = KALIGN_REFINE_NONE; m.attr("REFINE_ALL") = KALIGN_REFINE_ALL; m.attr("REFINE_CONFIDENT") = KALIGN_REFINE_CONFIDENT; m.attr("REFINE_INLINE") = KALIGN_REFINE_INLINE; }kalign-3.5.1/python-kalign/cli.py000066400000000000000000000170151515023132300166710ustar00rootroot00000000000000""" Command-line interface for the Python Kalign package. This CLI uses the compiled Python extension module shipped with the package instead of shelling out to a separately-installed `kalign` binary. Installed as ``kalign-py`` so it does not shadow the C binary on PATH. """ from __future__ import annotations import argparse import sys import tempfile from importlib.metadata import PackageNotFoundError from importlib.metadata import version as dist_version from pathlib import Path from typing import Optional def _resolve_version() -> str: # kalign-test is the name of the test distribution, which may be installed in test environments. If it's present, use its version; otherwise, fall back to the main kalign distribution. If neither is found, try to import __version__ from the package, and if that fails, return "unknown". for dist_name in ("kalign-python",): try: return dist_version(dist_name) except PackageNotFoundError: continue try: from . import __version__ return __version__ except Exception: return "unknown" def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="kalign-py", description="Multiple sequence alignment via the Kalign Python package", ) parser.add_argument( "-i", "--input", required=True, help="Input sequence file path, or '-' to read from stdin.", ) parser.add_argument( "-o", "--output", default="-", help="Output file path, or '-' to write to stdout (default).", ) parser.add_argument( "--format", default="fasta", help="Output format: fasta, clustal, stockholm, phylip (default: fasta).", ) parser.add_argument( "--type", default="auto", dest="seq_type", help="Sequence type: auto, dna, rna, internal, protein, divergent (default: auto).", ) parser.add_argument( "--gpo", type=float, default=None, help="Gap open penalty (default: Kalign internal defaults).", ) parser.add_argument( "--gpe", type=float, default=None, help="Gap extension penalty (default: Kalign internal defaults).", ) parser.add_argument( "--tgpe", type=float, default=None, help="Terminal gap extension penalty (default: Kalign internal defaults).", ) parser.add_argument( "-n", "--nthreads", type=int, default=1, help="Number of threads to use (default: 1).", ) parser.add_argument( "--refine", default="confident", help="Refinement mode: none, all, confident (default: confident).", ) parser.add_argument( "--adaptive-budget", action="store_true", default=False, help="Scale refinement trial count by uncertainty.", ) ens = parser.add_argument_group( "ensemble options", "These options only take effect when --ensemble is used.", ) ens.add_argument( "--ensemble", type=int, default=0, help="Number of ensemble runs (default: 0 = off). Try 3-5 for better accuracy.", ) ens.add_argument( "--ensemble-seed", type=int, default=42, help="RNG seed for ensemble (default: 42).", ) ens.add_argument( "--min-support", type=int, default=0, help="Explicit consensus threshold (default: 0 = auto).", ) ens.add_argument( "--save-poar", default=None, help="Save POAR consensus table to file for later re-thresholding.", ) ens.add_argument( "--load-poar", default=None, help="Load POAR consensus table from file (skip alignment, just re-threshold).", ) adv = parser.add_argument_group("advanced options") adv.add_argument( "--dist-scale", type=float, default=0.0, help="Distance scaling parameter (default: 0.0).", ) adv.add_argument( "--vsm-amax", type=float, default=None, help="Variable Scoring Matrix a_max (default: Kalign internal defaults).", ) adv.add_argument( "--realign", type=int, default=0, help="Realignment iterations (default: 0 = off).", ) parser.add_argument( "-V", "--version", action="version", version=f"%(prog)s {_resolve_version()}", help="Print version and exit", ) return parser def _write_stdout(names: list[str], sequences: list[str], fmt: str) -> None: import kalign fmt_lower = fmt.lower() if fmt_lower in {"fasta", "fa"}: kalign.io.write_fasta(sequences, sys.stdout, ids=names) return if fmt_lower in {"clustal", "aln"}: kalign.io.write_clustal(sequences, sys.stdout, ids=names) return if fmt_lower in {"stockholm", "sto"}: kalign.io.write_stockholm(sequences, sys.stdout, ids=names) return if fmt_lower in {"phylip", "phy"}: kalign.io.write_phylip(sequences, sys.stdout, ids=names) return raise ValueError( f"Invalid format: {fmt}. Must be one of: fasta, clustal, stockholm, phylip" ) def main(argv: Optional[list[str]] = None) -> int: parser = _build_parser() args = parser.parse_args(argv) import kalign input_path = args.input tmp_path: Optional[Path] = None if input_path == "-": with tempfile.NamedTemporaryFile( prefix="kalign-", suffix=".fa", delete=False ) as tmp: tmp.write(sys.stdin.buffer.read()) tmp_path = Path(tmp.name) input_path = str(tmp_path) try: kwargs = dict( seq_type=args.seq_type, gap_open=args.gpo, gap_extend=args.gpe, terminal_gap_extend=args.tgpe, n_threads=args.nthreads, refine=args.refine, adaptive_budget=args.adaptive_budget, ensemble=args.ensemble, ensemble_seed=args.ensemble_seed, min_support=args.min_support, realign=args.realign, dist_scale=args.dist_scale, ) if args.vsm_amax is not None: kwargs["vsm_amax"] = args.vsm_amax if args.save_poar is not None: kwargs["save_poar"] = args.save_poar if args.load_poar is not None: kwargs["load_poar"] = args.load_poar result = kalign.align_from_file(input_path, **kwargs) # Pass confidence data to Stockholm writer when available write_kwargs = dict( format=args.format, ids=result.names, ) if result.column_confidence is not None: write_kwargs["column_confidence"] = result.column_confidence if result.residue_confidence is not None: write_kwargs["residue_confidence"] = result.residue_confidence if args.output == "-": _write_stdout(result.names, result.sequences, args.format) else: kalign.write_alignment( result.sequences, args.output, **write_kwargs, ) return 0 except KeyboardInterrupt: return 130 except Exception as exc: print(f"kalign-py: error: {exc}", file=sys.stderr) return 2 finally: if tmp_path is not None: try: tmp_path.unlink() except OSError: pass if __name__ == "__main__": raise SystemExit(main()) kalign-3.5.1/python-kalign/io.py000066400000000000000000000301531515023132300165270ustar00rootroot00000000000000""" I/O helper functions for reading and writing sequence alignments. This module provides convenient functions for reading sequences from files and writing alignments in various formats, with optional Biopython integration. """ import os from pathlib import Path from typing import List, Optional, TextIO, Tuple, Union def read_fasta(path: Union[str, Path]) -> List[str]: """ Read FASTA file and return sequences as list of strings. Parameters ---------- path : str or Path Path to FASTA file Returns ------- list of str List of sequences (without headers) Raises ------ ImportError If Biopython is not installed FileNotFoundError If file doesn't exist Examples -------- >>> sequences = kalign.io.read_fasta("sequences.fasta") >>> aligned = kalign.align(sequences) """ try: from Bio import SeqIO except ImportError as e: raise ImportError( "Biopython required for FASTA I/O. Run: pip install kalign-python[io]" ) from e path = Path(path) if not path.exists(): raise FileNotFoundError(f"File not found: {path}") with open(path, "r") as handle: return [str(record.seq) for record in SeqIO.parse(handle, "fasta")] def read_sequences( path: Union[str, Path], format: str = "auto" ) -> Tuple[List[str], List[str]]: """ Read sequences from file and return sequences with their IDs. Parameters ---------- path : str or Path Path to sequence file format : str, optional File format. Options: "auto", "fasta", "genbank", "embl", "swiss". Default "auto" attempts to detect format from file extension. Returns ------- sequences : list of str List of sequences ids : list of str List of sequence IDs Raises ------ ImportError If Biopython is not installed FileNotFoundError If file doesn't exist ValueError If format is unsupported or auto-detection fails Examples -------- >>> sequences, ids = kalign.io.read_sequences("data.fasta") >>> aligned = kalign.align(sequences, fmt="biopython", ids=ids) """ try: from Bio import SeqIO except ImportError as e: raise ImportError( "Biopython required for sequence I/O. Run: pip install kalign-python[io]" ) from e path = Path(path) if not path.exists(): raise FileNotFoundError(f"File not found: {path}") # Auto-detect format from extension if format == "auto": ext = path.suffix.lower() format_map = { ".fasta": "fasta", ".fa": "fasta", ".fas": "fasta", ".fna": "fasta", ".ffn": "fasta", ".faa": "fasta", ".gb": "genbank", ".gbk": "genbank", ".embl": "embl", ".swiss": "swiss-prot", } format = format_map.get(ext, "fasta") # Default to FASTA try: with open(path, "r") as handle: records = list(SeqIO.parse(handle, format)) sequences = [str(record.seq) for record in records] ids = [record.id for record in records] return sequences, ids except Exception as e: raise ValueError(f"Failed to parse file as {format}: {e}") def write_fasta( alignment: List[str], path: Union[str, Path, TextIO], ids: Optional[List[str]] = None, line_length: int = 80, ) -> None: """ Write aligned sequences to FASTA format. This function doesn't require Biopython and is used as the default writer for FASTA format. Parameters ---------- alignment : list of str List of aligned sequences path : str, Path, or file-like object Output path or file handle ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. line_length : int, optional Maximum line length for sequence lines (default: 80) Examples -------- >>> aligned = kalign.align(sequences) >>> kalign.io.write_fasta(aligned, "output.fasta", ids=["seq1", "seq2"]) """ if not alignment: raise ValueError("Empty alignment provided") if ids is None: ids = [f"seq{i}" for i in range(len(alignment))] elif len(ids) != len(alignment): raise ValueError( f"Number of IDs ({len(ids)}) must match alignment length ({len(alignment)})" ) def write_to_handle(handle): for seq_id, seq in zip(ids, alignment): handle.write(f">{seq_id}\n") # Write sequence with line wrapping for i in range(0, len(seq), line_length): handle.write(seq[i : i + line_length] + "\n") if hasattr(path, "write"): # File-like object write_to_handle(path) else: # File path with open(path, "w") as handle: write_to_handle(handle) def write_clustal( alignment: List[str], path: Union[str, Path, TextIO], ids: Optional[List[str]] = None, ) -> None: """ Write aligned sequences to Clustal format. Parameters ---------- alignment : list of str List of aligned sequences path : str, Path, or file-like object Output path or file handle ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. Examples -------- >>> aligned = kalign.align(sequences) >>> kalign.io.write_clustal(aligned, "output.aln") """ try: from Bio import AlignIO from Bio.Align import MultipleSeqAlignment from Bio.Seq import Seq from Bio.SeqRecord import SeqRecord except ImportError as e: raise ImportError( "Biopython required for Clustal I/O. Run: pip install kalign-python[io]" ) from e if not alignment: raise ValueError("Empty alignment provided") if ids is None: ids = [f"seq{i}" for i in range(len(alignment))] elif len(ids) != len(alignment): raise ValueError( f"Number of IDs ({len(ids)}) must match alignment length ({len(alignment)})" ) # Create Biopython alignment object records = [SeqRecord(Seq(seq), id=seq_id) for seq, seq_id in zip(alignment, ids)] msa = MultipleSeqAlignment(records) if hasattr(path, "write"): # File-like object AlignIO.write(msa, path, "clustal") else: # File path with open(path, "w") as handle: AlignIO.write(msa, handle, "clustal") def _conf_to_pp_char(conf: float) -> str: """Convert a confidence value [0..1] to HMMER-style PP character.""" if conf >= 0.95: return "*" return str(int(conf * 10)) def write_stockholm( alignment: List[str], path: Union[str, Path, TextIO], ids: Optional[List[str]] = None, column_confidence: Optional[List[float]] = None, residue_confidence: Optional[List[List[float]]] = None, ) -> None: """ Write aligned sequences to Stockholm format. When confidence data is provided (from ensemble alignment), emits ``#=GR PP`` lines (per-residue) and ``#=GC PP_cons`` line (per-column) using HMMER-style PP encoding. Parameters ---------- alignment : list of str List of aligned sequences path : str, Path, or file-like object Output path or file handle ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. column_confidence : list of float, optional Per-column confidence values [0..1]. Emitted as ``#=GC PP_cons``. residue_confidence : list of list of float, optional Per-residue confidence values [0..1]. Emitted as ``#=GR PP``. Examples -------- >>> aligned = kalign.align(sequences) >>> kalign.io.write_stockholm(aligned, "output.sto") """ if not alignment: raise ValueError("Empty alignment provided") if ids is None: ids = [f"seq{i}" for i in range(len(alignment))] elif len(ids) != len(alignment): raise ValueError( f"Number of IDs ({len(ids)}) must match alignment length ({len(alignment)})" ) has_confidence = residue_confidence is not None or column_confidence is not None if has_confidence: # Write Stockholm manually to include PP annotations def write_to_handle(handle): handle.write("# STOCKHOLM 1.0\n") # Find max ID length for padding max_id = max(len(i) for i in ids) pp_label_len = max(max_id, len("PP_cons")) for idx, (seq_id, seq) in enumerate(zip(ids, alignment)): handle.write(f"{seq_id:<{max_id}} {seq}\n") # Per-residue confidence if residue_confidence is not None and idx < len(residue_confidence): rc = residue_confidence[idx] pp = [] for ch, conf in zip(seq, rc): if ch == "-" or ch == ".": pp.append(".") else: pp.append(_conf_to_pp_char(conf)) pp_str = "".join(pp) handle.write(f"#=GR {seq_id:<{max_id}} PP {pp_str}\n") # Per-column confidence if column_confidence is not None: pp_cons = "".join(_conf_to_pp_char(c) for c in column_confidence) handle.write(f"#=GC {'PP_cons':<{pp_label_len}} {pp_cons}\n") handle.write("//\n") if hasattr(path, "write"): write_to_handle(path) else: with open(path, "w") as handle: write_to_handle(handle) else: # No confidence data: use Biopython for standard Stockholm output try: from Bio import AlignIO from Bio.Align import MultipleSeqAlignment from Bio.Seq import Seq from Bio.SeqRecord import SeqRecord except ImportError as e: raise ImportError( "Biopython required for Stockholm I/O. Run: pip install kalign-python[io]" ) from e records = [ SeqRecord(Seq(seq), id=seq_id) for seq, seq_id in zip(alignment, ids) ] msa = MultipleSeqAlignment(records) if hasattr(path, "write"): AlignIO.write(msa, path, "stockholm") else: with open(path, "w") as handle: AlignIO.write(msa, handle, "stockholm") def write_phylip( alignment: List[str], path: Union[str, Path, TextIO], ids: Optional[List[str]] = None, interleaved: bool = False, ) -> None: """ Write aligned sequences to PHYLIP format. Parameters ---------- alignment : list of str List of aligned sequences path : str, Path, or file-like object Output path or file handle ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. interleaved : bool, optional Whether to use interleaved format (default: False, sequential) Examples -------- >>> aligned = kalign.align(sequences) >>> kalign.io.write_phylip(aligned, "output.phy") """ try: from Bio import AlignIO from Bio.Align import MultipleSeqAlignment from Bio.Seq import Seq from Bio.SeqRecord import SeqRecord except ImportError as e: raise ImportError( "Biopython required for PHYLIP I/O. Run: pip install kalign-python[io]" ) from e if not alignment: raise ValueError("Empty alignment provided") if ids is None: ids = [f"seq{i}" for i in range(len(alignment))] elif len(ids) != len(alignment): raise ValueError( f"Number of IDs ({len(ids)}) must match alignment length ({len(alignment)})" ) # Create Biopython alignment object records = [SeqRecord(Seq(seq), id=seq_id) for seq, seq_id in zip(alignment, ids)] msa = MultipleSeqAlignment(records) format_name = "phylip" if interleaved else "phylip-sequential" if hasattr(path, "write"): # File-like object AlignIO.write(msa, path, format_name) else: # File path with open(path, "w") as handle: AlignIO.write(msa, handle, format_name) kalign-3.5.1/python-kalign/py.typed000066400000000000000000000000001515023132300172310ustar00rootroot00000000000000kalign-3.5.1/python-kalign/utils.py000066400000000000000000000220631515023132300172610ustar00rootroot00000000000000""" Utility functions for working with multiple sequence alignments. This module provides helpful utilities for analyzing and manipulating alignments produced by Kalign. """ from collections import Counter from typing import Dict, List, Optional, Tuple import numpy as np def to_array(alignment: List[str]) -> np.ndarray: """ Convert alignment to NumPy character array. Parameters ---------- alignment : list of str List of aligned sequences Returns ------- numpy.ndarray 2D character array with shape (n_sequences, alignment_length) Examples -------- >>> import kalign >>> aligned = kalign.align(["ATCG", "ATCG", "ATGG"]) >>> arr = kalign.utils.to_array(aligned) >>> print(arr.shape) (3, 4) """ if not alignment: raise ValueError("Empty alignment provided") # Verify all sequences have same length lengths = [len(seq) for seq in alignment] if len(set(lengths)) > 1: raise ValueError("All sequences in alignment must have the same length") # Convert to numpy array return np.array([list(seq) for seq in alignment], dtype="U1") def alignment_stats(alignment: List[str]) -> Dict[str, float]: """ Calculate basic statistics for a multiple sequence alignment. Parameters ---------- alignment : list of str List of aligned sequences Returns ------- dict Dictionary containing alignment statistics: - length: alignment length - n_sequences: number of sequences - gap_fraction: fraction of positions that are gaps - conservation: fraction of positions that are fully conserved - identity: average pairwise identity Examples -------- >>> import kalign >>> aligned = kalign.align(["ATCG", "ATCG", "ATGG"]) >>> stats = kalign.utils.alignment_stats(aligned) >>> print(f"Conservation: {stats['conservation']:.2f}") Conservation: 0.75 """ if not alignment: raise ValueError("Empty alignment provided") # Convert to array for easier analysis arr = to_array(alignment) n_sequences, length = arr.shape # Calculate gap fraction n_gaps = np.sum(arr == "-") gap_fraction = n_gaps / (n_sequences * length) # Calculate conservation (fully conserved positions) conserved_positions = 0 for col in range(length): column = arr[:, col] # Remove gaps for conservation calculation non_gap = column[column != "-"] if len(non_gap) > 0 and len(set(non_gap)) == 1: conserved_positions += 1 conservation = conserved_positions / length # Calculate average pairwise identity total_comparisons = 0 total_matches = 0 for i in range(n_sequences): for j in range(i + 1, n_sequences): seq1, seq2 = arr[i], arr[j] # Only compare non-gap positions valid_positions = (seq1 != "-") & (seq2 != "-") if np.sum(valid_positions) > 0: matches = np.sum(seq1[valid_positions] == seq2[valid_positions]) total_matches += matches total_comparisons += np.sum(valid_positions) identity = total_matches / total_comparisons if total_comparisons > 0 else 0.0 return { "length": length, "n_sequences": n_sequences, "gap_fraction": gap_fraction, "conservation": conservation, "identity": identity, } def consensus_sequence(alignment: List[str], threshold: float = 0.5) -> str: """ Generate consensus sequence from alignment. Parameters ---------- alignment : list of str List of aligned sequences threshold : float, optional Minimum fraction of sequences that must have the same character for it to be included in consensus (default: 0.5) Returns ------- str Consensus sequence. Positions where no character meets the threshold are represented as 'N' (nucleotides) or 'X' (amino acids). Examples -------- >>> import kalign >>> aligned = ["ATCG", "ATCG", "ATGG"] >>> consensus = kalign.utils.consensus_sequence(aligned) >>> print(consensus) ATCG """ if not alignment: raise ValueError("Empty alignment provided") if not 0 <= threshold <= 1: raise ValueError("Threshold must be between 0 and 1") arr = to_array(alignment) n_sequences, length = arr.shape consensus = [] # Detect sequence type (crude heuristic) all_chars = set("".join(alignment).upper().replace("-", "")) is_nucleotide = all_chars.issubset(set("ATCGUN")) ambiguous_char = "N" if is_nucleotide else "X" for col in range(length): column = arr[:, col] # Remove gaps for consensus calculation non_gap = column[column != "-"] if len(non_gap) == 0: consensus.append("-") continue # Count character frequencies char_counts = Counter(non_gap) most_common_char, count = char_counts.most_common(1)[0] # Check if most common character meets threshold if count / len(non_gap) >= threshold: consensus.append(most_common_char) else: consensus.append(ambiguous_char) return "".join(consensus) def remove_gap_columns(alignment: List[str], threshold: float = 1.0) -> List[str]: """ Remove columns with gaps from alignment. Parameters ---------- alignment : list of str List of aligned sequences threshold : float, optional Remove columns where fraction of gaps >= threshold (default: 1.0) Default removes only all-gap columns. Returns ------- list of str Alignment with gap columns removed Examples -------- >>> aligned = ["AT-G", "AT-G", "AT-G"] >>> nogaps = kalign.utils.remove_gap_columns(aligned) >>> print(nogaps) ['ATG', 'ATG', 'ATG'] """ if not alignment: raise ValueError("Empty alignment provided") if not 0 <= threshold <= 1: raise ValueError("Threshold must be between 0 and 1") arr = to_array(alignment) n_sequences, length = arr.shape # Identify columns to keep keep_columns = [] for col in range(length): column = arr[:, col] gap_fraction = np.sum(column == "-") / n_sequences if gap_fraction < threshold: keep_columns.append(col) # Extract kept columns if not keep_columns: return [""] * n_sequences # All columns removed filtered_arr = arr[:, keep_columns] return ["".join(seq) for seq in filtered_arr] def pairwise_identity_matrix(alignment: List[str]) -> np.ndarray: """ Calculate pairwise identity matrix for alignment. Parameters ---------- alignment : list of str List of aligned sequences Returns ------- numpy.ndarray Symmetric matrix of pairwise identities (shape: n_sequences x n_sequences) Examples -------- >>> aligned = kalign.align(["ATCG", "ATCG", "ATGG"]) >>> matrix = kalign.utils.pairwise_identity_matrix(aligned) >>> print(matrix[0, 1]) # Identity between first two sequences 1.0 """ if not alignment: raise ValueError("Empty alignment provided") arr = to_array(alignment) n_sequences = arr.shape[0] identity_matrix = np.zeros((n_sequences, n_sequences)) for i in range(n_sequences): identity_matrix[i, i] = 1.0 # Self-identity is always 1 for j in range(i + 1, n_sequences): seq1, seq2 = arr[i], arr[j] # Only compare non-gap positions in both sequences valid_positions = (seq1 != "-") & (seq2 != "-") if np.sum(valid_positions) > 0: matches = np.sum(seq1[valid_positions] == seq2[valid_positions]) identity = matches / np.sum(valid_positions) else: identity = 0.0 identity_matrix[i, j] = identity identity_matrix[j, i] = identity # Symmetric return identity_matrix def trim_alignment( alignment: List[str], start: Optional[int] = None, end: Optional[int] = None ) -> List[str]: """ Trim alignment to specified positions. Parameters ---------- alignment : list of str List of aligned sequences start : int, optional Start position (0-based, inclusive). If None, starts from beginning. end : int, optional End position (0-based, exclusive). If None, goes to end. Returns ------- list of str Trimmed alignment Examples -------- >>> aligned = ["ATCGATCG", "ATCGATCG"] >>> trimmed = kalign.utils.trim_alignment(aligned, start=2, end=6) >>> print(trimmed) ['CGAT', 'CGAT'] """ if not alignment: raise ValueError("Empty alignment provided") length = len(alignment[0]) if start is None: start = 0 if end is None: end = length if start < 0: start = max(0, length + start) if end < 0: end = max(0, length + end) if start >= end: raise ValueError("Start position must be less than end position") return [seq[start:end] for seq in alignment] kalign-3.5.1/scripts/000077500000000000000000000000001515023132300144475ustar00rootroot00000000000000kalign-3.5.1/scripts/balibase_test.org000066400000000000000000001366411515023132300177740ustar00rootroot00000000000000#+TITLE: Test kalign on BAliBASE #+AUTHOR: Timo Lassmann #+EMAIL: timolassmann@icloud.com #+DATE: 2019-04-23 #+LATEX_CLASS: report #+OPTIONS: toc:nil #+OPTIONS: H:4 #+LATEX_CMD: pdflatex * Introduction Let's run kalign on the core BAliBASE test alignments. * Method ** Step 1: install balibase #+BEGIN_SRC sh cd mkdir -p data cd data if [ ! -f BAliBASE_R1-5.tar.gz ]; then wget https://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz fi tar -zxvf BAliBASE_R1-5.tar.gz #+END_SRC #+RESULTS: BAliBASE comes with a C program to score alignments. Unfortunately, something is wrong with the bundled expat library. To fix this install expat system and remove the links to the bundled library by editing the makefile: #+BEGIN_EXAMPLE makefile CC = cc CFLAGS = -c -O #-I$(EXPAT_INC) LFLAGS = -O -lm -lexpat #-L$(EXPAT_LIB) -lexpat EXPAT_LIB = expat-1.95.2/lib EXPAT_INC = expat-1.95.2/include #+END_EXAMPLE Also, by default bali_score will use all columns for the TC score. In the original clustal omega paper only core blocks are used to calculate the TC scores supercite:sievers-2014-fast-scalab. To change this modify =bali_score.c= line 104 from: #+BEGIN_EXAMPLE C method='C' #+END_EXAMPLE to: #+BEGIN_EXAMPLE C method='B' #+END_EXAMPLE Then compile: #+BEGIN_SRC sh cd ~/data/bb3_release/bali_score_src make #+END_SRC #+RESULTS: | cc | -c | -O | readxml.c | | | | | | | | cc | -c | -O | init.c | | | | | | | | cc | -c | -O | util.c | | | | | | | | cc | -c | -O | bali_score.c | | | | | | | | cc | -o | bali_score | readxml.o | init.o | util.o | bali_score.o | -O | -lm | -lexpat | ** Step 2: run tests #+BEGIN_SRC sh :session onesh cd ~/data/bb3_release cd .. mkdir -p bb3_release_tmp_aln cd bb3_release_tmp_aln KALIGNOUTDIR=$PWD echo $KALIGNOUTDIR #+END_SRC #+RESULTS: | | | sh-4.4$ sh-4.4$ sh-4.4$ sh-4.4$ sh-4.4$ /home/user/data/bb3_release_tmp_aln | #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_25.txt %s -o %s/%s_%skalign25.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_kalign.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_50.txt %s -o %s/%s_%skalign50.msf\n", $1,outdir,a[n-2],a[n-1] }' >> run_kalign.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_75.txt %s -o %s/%s_%skalign75.msf\n", $1,outdir,a[n-2],a[n-1] }' >> run_kalign.sh #+END_SRC #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp testparam %s -o %s/%s_%skalign.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_kalign.sh find ~/data/bb3_release -name "*.tfa" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo --dealign -i %s --outfmt=msf -o %s/%s_%sclustalo.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_clustalo.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign %s -set 0 -o %s/%s_%skalign_1.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 1 -o %s/%s_%skalign_2.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 2 -o %s/%s_%skalign_3.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 3 -o %s/%s_%skalign_4.msf\n", $1,outdir,a[n-2],a[n-1] ; }' > run_kalign_cmsa.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "cmsa -a %s/%s_%skalign_*.msf -out %s/%s_%skalign_cmsa.msf -f msf \n",outdir,a[n-2],a[n-1],outdir,a[n-2],a[n-1] ; }' > run_cmsa.sh chmod 755 run_kalign_cmsa.sh chmod 755 run_kalign.sh chmod 755 run_clustalo.sh #+END_SRC #+RESULTS: #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_1.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set1.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_2.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set2.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_3.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set3.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_4.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set4.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_5.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set5.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_cmsa.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_cmsa.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%sclustalo.msf\n", $1,outdir,a[n-2],a[n-1] }' >> alignment_scores.sh chmod 755 alignment_scores.sh #+END_SRC #+RESULTS: run the tests #+BEGIN_SRC sh :session onesh parallel --jobs 5 < ./run_kalign.sh parallel --jobs 5 < ./run_clustalo.sh ./alignment_scores.sh | grep auto > scores2.csv #+END_SRC ** Step 3: plot scores *** Check for dependencies Here I define the list of libraries I'll be using. #+NAME: liblist #+BEGIN_SRC R -n :exports code :results none libraries <- c("tidyverse","ggplot2","stringi","cowplot") #+END_SRC Script to test if libraries are present. #+BEGIN_SRC R -n :tangle test_for_libraries.R :shebang #!/usr/bin/env Rscript :noweb yes :exports code :results none <> Sys.info()["nodename"] for(library in libraries) { f = is.element(library, installed.packages()[,1]) print(paste("Library",library, "is installed?", f)) if(!f) { message("Missing library:",library ) quit(status=1) } } quit(status=0) #+END_SRC #+BEGIN_SRC sh -n :results output :exports both ./test_for_libraries.R #+END_SRC #+RESULTS: : nodename : "work" : [1] "Library tidyverse is installed? TRUE" : [1] "Library ggplot2 is installed? TRUE" : [1] "Library stringi is installed? TRUE" : [1] "Library cowplot is installed? TRUE" install.packages("tidyverse") Code block to load the libraries in R code. #+NAME: Rlibraries #+BEGIN_SRC R -n :exports code :results none :noweb yes <> lapply(libraries, FUN = function(X) { do.call("library", list(X)) }) #+END_SRC *** Plotting functions #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file BalibaseSP_scores.jpeg :exports both :width 160 :height 80 :noweb yes mat <- readBaliscores("scores_kalign_old.csv","kalign old"); mat <- rbind(mat,readBaliscores("scores_kalign_new.csv","kalign new")); mat <- rbind(mat,readBaliscores("scores_kalign_pbil2.csv","kalign pbil 2")); mat <- rbind(mat,readBaliscores("scores_kalign_16seed.csv","kalign 16 seed")); mat <- rbind(mat,readBaliscores("scores_kalign_test.csv","rel")) mat <- rbind(mat,readBaliscores("scores_kalign_optimized.csv","Opt")) ## mat <- rbind(mat,readBaliscores("scores_kalign_3.csv","kalign 3")); ## mata ## <- rbind(mat,readBaliscores("scores_kalign_bibpm.csv","bibpm")); ## mat ## <- rbind(mat,readBaliscores("scores_kalign_bibpm_zero.csv","bibpm_zero")); p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) means <- aggregate(SP ~ Type, mat, median) means$SP <- round(means$SP,digits = 4) p2 <- ggplot(mat, aes(Type, SP)) p2 <- p2 + geom_boxplot(aes(colour = Type)) p2 <- p2 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p2 <- p2 + geom_text(data = means, aes(label = SP, y = SP + 0.08)) p3 <- ggplot(mat, aes(Group, TC)) p3 <- p3 + geom_boxplot(aes(colour = Type)) means <- aggregate(TC ~ Type, mat, median) means$TC <- round(means$TC,digits = 4) p4 <- ggplot(mat, aes(Type, TC)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p4 <- p4 + geom_text(data = means, aes(label = TC, y = TnC + 0.08)) p = plot_grid(p1,p2,p3,p4, labels=c("SP", "TC"), ncol = 2, nrow= 2) #+END_SRC #+RESULTS: [[file:BalibaseSP_scores.jpeg]] * Benchmarking Programs to test: #+NAME: Benchprograms | Name | Download | |--------------+------------------------------------------------------------------------------| | Muscle | https://www.drive5.com/muscle/downloads3.8.31/muscle3.8.31_i86linux32.tar.gz | | Clustalomega | http://clustal.org/omega/clustalo-1.2.4-Ubuntu-x86_64 | | q-score | https://www.drive5.com/qscore/qscore_src.tar.gz | | Kalign2 | http://msa.sbc.su.se/downloads/kalign/current.tar.gz | Since the clustal papers provide a great overview of alignment programs I think these two should suffice. Note I am not comparing kalign against consistency based methods as these are a) all more accurate and b) slower. Kalign is targeted at the accurate and fast niche. Benchmarks: #+NAME: Benchmarkdata | Name | Download | |---------------+-------------------------------------------------------------------| | Balibase1-5 | http://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz | | Balifam | http://clustal.org/omega/bali3fam-26.tar.gz | | Bralibaseset1 | http://projects.binf.ku.dk/pgardner/bralibase/data-set1.tar.gz | | Bralibaseset2 | http://projects.binf.ku.dk/pgardner/bralibase/data-set2.tar.gz | | HomFam | http://www.clustal.org/omega/homfam-20110613-25.tar.gz | | QuanTest2 | http://bioinf.ucd.ie/quantest2.tar | ** Step one: create test directory and download all data #+BEGIN_SRC bash :exports both :results none cd mkdir -p kalignbenchmark cd kalignbenchmark mkdir -p data mkdir -p programs mkdir -p scratch #+END_SRC *** Download benchmark datasets #+BEGIN_SRC bash -n :results raw :exports both :var tbl=Benchmarkdata :colnames yes cd cd ~/kalignbenchmark/data for idx in ${!tbl[*]}; do #echo ${tbl[$idx]} if [[ ${tbl[$idx]} =~ ^http* ]]; then echo "wget ${tbl[$idx]}" wget ${tbl[$idx]} fi done #+END_SRC inter all data sets #+BEGIN_SRC bash :results none :exports code cd cd ~/kalignbenchmark/data mkdir -p homfam mv homfam-20110613-25.tar.gz homfam cd homfam tar -zxvf homfam-20110613-25.tar.gz cd ~/kalignbenchmark/data for filename in *.tar.gz; do tar -zxvf $filename done tar -xvf quantest2.tar #+END_SRC *** Compile baliscore program and place in program directory #+BEGIN_SRC bash cd ~/kalignbenchmark/data/bb3_release/bali_score_src gcc *.c -lm -lexpat -o bali_score cp bali_score ~/kalignbenchmark/programs #+END_SRC #+RESULTS: *** Download programs #+BEGIN_SRC bash -n :results none :exports none :var tbl=Benchprograms :colnames yes cd cd ~/kalignbenchmark/programs for idx in ${!tbl[*]}; do #echo ${tbl[$idx]} if [[ ${tbl[$idx]} =~ ^http* ]]; then echo "wget ${tbl[$idx]}" wget ${tbl[$idx]} fi done chmod 755 clustalo-1.2.4-Ubuntu-x86_64 tar -zxvf muscle3.8.31_i86linux32.tar.gz tar -zxvf qscore_src.tar.gz mkdir -p kalign2_src cd kalign2_src mv ../current.tar.gz . tar -zxvf current.tar.gz ./configure make cp kalign ../kalign2 #+END_SRC *** Compile q-score NOTE: this is manual! Add: #+BEGIN_EXAMPLE C #include #+END_EXAMPLE to qscore header: =qscore.h= then: #+BEGIN_EXAMPLE bash make #+END_EXAMPLE ** Step 2: test if all programs run Let's make sure individual programs run on all the test sets. small code chunk to set path: #+NAME: path #+BEGIN_SRC bash :results none :exports code :noweb yes export PATH=~/kalignbenchmark/programs:$PATH #+END_SRC *** muscle Can I run muscle? #+BEGIN_SRC bash :results value :exports code :noweb yes <> muscle3.8.31_i86linux32 -version #+END_SRC #+RESULTS: : MUSCLE v3.8.31 by Robert C. Edgar Let's try on a balibase case: #+BEGIN_SRC bash :results raw :exports code :noweb yes <> IN=~/kalignbenchmark/data/bb3_release/RV11/BB11002.tfa REF=~/kalignbenchmark/data/bb3_release/RV11/BB11002.msf OUT=~/kalignbenchmark/scratch/BB11002_muscle.msf muscle3.8.31_i86linux32 -msf -in $IN -out $OUT bali_score $REF $OUT #+END_SRC Let's try on a balifam #+BEGIN_SRC bash :results raw :exports both :noweb yes <> IN=~/kalignbenchmark/data/BB20021-package.vie ADD=~/kalignbenchmark/data/bb3_release/RV20/BB20021.tfa REF=~/kalignbenchmark/data/bb3_release/RV20/BB20021.msf OUT=~/kalignbenchmark/scratch/BB20021_muscle.fa cd ~/kalignbenchmark/scratch cat $IN $ADD > test.fa time muscle3.8.31_i86linux32 -maxiters 2 -in test.fa -out $OUT kalign $REF -r -f fasta -o ref.fa qscore -test $OUT -ref ref.fa #+END_SRC #+RESULTS: [2019-06-11 20:55:42] : LOG : kalign -r -f fasta -o ref.fa /home/user/kalignbenchmark/data/bb3_release/RV20/BB20021.msf [2019-06-11 20:55:42] : LOG : Detected protein sequences. [2019-06-11 20:55:42] : LOG : Detected: 53 sequences. [2019-06-11 20:55:42] : LOG : Output in fasta format *** clustalo Can I run muscle? #+BEGIN_SRC bash :results value :exports code :noweb yes <> clustalo-1.2.4-Ubuntu-x86_64 --version #+END_SRC Let's try on a balibase case: #+BEGIN_SRC bash :results raw :exports code :noweb yes <> IN=~/kalignbenchmark/data/bb3_release/RV11/BB11002.tfa REF=~/kalignbenchmark/data/bb3_release/RV11/BB11002.msf OUT=~/kalignbenchmark/scratch/BB11002_clustalo.msf clustalo-1.2.4-Ubuntu-x86_64 --outfmt=msf --in $IN --out $OUT bali_score $REF $OUT #+END_SRC #+RESULTS: Using GCG format reference alignment Comparing test alignment in /home/user/kalignbenchmark/scratch/BB11002_clustalo.msf with reference alignment in /home/user/kalignbenchmark/data/bb3_release/RV11/BB11002.msf SP score= 0.483 TC score= 0.000 auto /home/user/kalignbenchmark/scratch/BB11002_clustalo.msf 0.483 0.000 ** Step 3: scripts to run benchmarks #+NAME: scratchdir #+BEGIN_SRC bash :results none :exports code :noweb yes SCRATCHDIR="~/kalignbenchmark/scratch" #+END_SRC *** Balibase #+BEGIN_SRC bash -n :tangle gen_run_balibase.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_run_balibase.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the balibase MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done printf "Generating kalign run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.tfa" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -i %s -f msf -o %s/%s_%s_kalign.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_kalign.sh chmod 755 run_kalign.sh printf "Generating muscle run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.tfa" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "muscle3.8.31_i86linux32 -msf -in %s -out %s/%s_%s_muscle.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_muscle.sh chmod 755 run_muscle.sh printf "Generating clustalo run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.tfa" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo-1.2.4-Ubuntu-x86_64 --outfmt=msf --in %s --out %s/%s_%s_clustalo.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_clustalo.sh chmod 755 run_clustalo.sh printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_kalign.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_kalign.sh chmod 755 score_bb3_kalign.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_muscle.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_muscle.sh chmod 755 score_bb3_muscle.sh printf "Generating clustalo scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_clustalo.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_clustalo.sh chmod 755 score_bb3_clustalo.sh #+END_SRC **** Kick off jobs #+BEGIN_SRC bash -n :tangle run_balibase.sh :noweb yes :exports code :results none :noweb yes :shebang #!/usr/bin/env bash <> ./run_muscle.sh ./run_kalign.sh ./run_clustalo.sh #+END_SRC **** score alignments #+BEGIN_SRC bash ./score_bb3_kalign.sh > scores_kalign.csv ./score_bb3_muscle.sh > scores_muscle.csv ./score_bb3_clustalo.sh > scores_clustalo.csv #+END_SRC #+RESULTS: **** Plot #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file Balibase_scores.jpeg :exports both :noweb yes mat <- readBaliscores("scores_kalign.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo.csv","clustalo")); ## mat <- rbind(mat,readBaliscores("scores_kalign_3.csv","kalign 3")); ## mata ## <- rbind(mat,readBaliscores("scores_kalign_bibpm.csv","bibpm")); ## mat ## <- rbind(mat,readBaliscores("scores_kalign_bibpm_zero.csv","bibpm_zero")); p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) means <- aggregate(SP ~ Type, mat, median) means$SP <- round(means$SP,digits = 4) p2 <- ggplot(mat, aes(Type, SP)) p2 <- p2 + geom_boxplot(aes(colour = Type)) p2 <- p2 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p2 <- p2 + geom_text(data = means, aes(label = SP, y = SP + 0.08)) p3 <- ggplot(mat, aes(Group, TC)) p3 <- p3 + geom_boxplot(aes(colour = Type)) means <- aggregate(TC ~ Type, mat, median) means$TC <- round(means$TC,digits = 4) p4 <- ggplot(mat, aes(Type, TC)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p4 <- p4 + geom_text(data = means, aes(label = TC, y = TC + 0.08)) p1 <- p1 + theme(legend.position="none") p2 <- p2 + theme(legend.position="none") p3 <- p3 + theme(legend.position="none") p4 <- p4 + theme(legend.position="none") p = plot_grid(p1,p2,p3,p4, labels=c("SP", "TC"), ncol = 2, nrow= 2) ggsave("Balibase_scores.jpeg", p,width = 16, height = 12, dpi = 300, units = c( "cm")) p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) p1 <- p1 + scale_color_discrete(name = "Program") p1 <- p1 + theme(legend.position = c(0.25, 0.15),legend.text=element_text(size=16), legend.direction = "horizontal") p1 <- p1 + xlab(label="") p1 <- p1 + theme(plot.margin = margin(t = 0, b = -0.4, unit = "cm"))# unit(c(1,2,3,4), "cm")) # t = 0, r = 0, b = 0, l = 0 ("left", "right", "bottom", "top") ggsave("Figure1.jpeg",p1,width = 16, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:Balibase_scores.jpeg]] *** Bralibase #+BEGIN_SRC bash -n :tangle gen_run_bralibase.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_run_bralibase.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the balibase MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" -exec rm -rf {} \; printf "Generate commands to reformat the reference alignments\n\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.fa" -o -name '*.fasta' | grep structural | awk -v outdir="$SCRATCHDIR" '{printf "kalign %s --rename -reformat -f msf -o %s.msf\n", $1,$1 }' > run_kalign_reformat.sh find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.fa" -o -name '*.fasta' | grep unaligned | awk -v outdir="$SCRATCHDIR" '{printf "kalign %s --rename -reformat -f fasta -o %s_test.fa\n", $1,$1 }' >> run_kalign_reformat.sh chmod 755 run_kalign_reformat.sh ./run_kalign_reformat.sh printf "Generating kalign run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" | grep unaligned | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign %s -f msf -o %s/%s_%s_%s_kalign.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2] }' > run_kalign_bralibase.sh chmod 755 run_kalign_bralibase.sh printf "Generating muscle run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" -o -name '*.fasta' | grep unaligned | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "muscle3.8.31_i86linux32 -msf -in %s -out %s/%s_%s_%s_muscle.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2] }' > run_muscle_bralibase.sh chmod 755 run_muscle_bralibase.sh printf "Generating clustalo run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" -o -name '*.fasta' | grep unaligned | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo-1.2.4-Ubuntu-x86_64 --outfmt=msf --in %s --force --out %s/%s_%s_%s_clustalo.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2] }' > run_clustalo_bralibase.sh chmod 755 run_clustalo_bralibase.sh printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_%s_kalign.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_kalign_bralibase.sh chmod 755 score_kalign_bralibase.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%s_%s_muscle.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_muscle_bralibase.sh chmod 755 score_muscle_bralibase.sh printf "Generating clustalo scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%s_%s_clustalo.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_clustalo_bralibase.sh chmod 755 score_clustalo_bralibase.sh #+END_SRC **** Kick off jobs #+BEGIN_SRC bash -n :tangle run_bralibase.sh :noweb yes :exports code :results none :noweb yes :shebang #!/usr/bin/env bash <> ./run_kalign_bralibase.sh ./run_muscle_bralibase.sh ./run_clustalo_bralibase.sh #+END_SRC **** score alignments #+BEGIN_SRC bash ./score_kalign_bralibase.sh > scores_kalign_bralibase.csv ./score_muscle_bralibase.sh > scores_muscle_bralibase.csv ./score_clustalo_bralibase.sh > scores_clustalo_bralibase.csv #+END_SRC #+RESULTS: **** Plot #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file Bralibase_scores.jpeg :exports both :noweb yes mat <- readBaliscores("scores_kalign_bralibase.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle_bralibase.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo_bralibase.csv","clustalo")); ## mat <- rbind(mat,readBaliscores("scores_kalign_3.csv","kalign 3")); ## mata ## <- rbind(mat,readBaliscores("scores_kalign_bibpm.csv","bibpm")); ## mat ## <- rbind(mat,readBaliscores("scores_kalign_bibpm_zero.csv","bibpm_zero")); p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) means <- aggregate(SP ~ Type, mat, median) means$SP <- round(means$SP,digits = 4) p2 <- ggplot(mat, aes(Type, SP)) p2 <- p2 + geom_boxplot(aes(colour = Type)) p2 <- p2 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p2 <- p2 + geom_text(data = means, aes(label = SP, y = SP + 0.08)) p3 <- ggplot(mat, aes(Group, TC)) p3 <- p3 + geom_boxplot(aes(colour = Type)) means <- aggregate(TC ~ Type, mat, median) means$TC <- round(means$TC,digits = 4) p4 <- ggplot(mat, aes(Type, TC)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p4 <- p4 + geom_text(data = means, aes(label = TC, y = TC + 0.08)) p1 <- p1 + theme(legend.position="none") p2 <- p2 + theme(legend.position="none") p3 <- p3 + theme(legend.position="none") p4 <- p4 + theme(legend.position="none") p = plot_grid(p1,p2,p3,p4, labels=c("SP", "TC"), ncol = 2, nrow= 2) ggsave("Bralibase_scores.jpeg", p,width = 16, height = 12, dpi = 300, units = c( "cm")) p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) p1 <- p1 + scale_color_discrete(name = "Program") p1 <- p1 + theme(legend.position = c(0.25, 0.15),legend.text=element_text(size=16), legend.direction = "horizontal") p1 <- p1 + xlab(label="") p1 <- p1 + theme(plot.margin = margin(t = 0, b = -0.4, unit = "cm"))# unit(c(1,2,3,4), "cm")) # t = 0, r = 0, b = 0, l = 0 ("left", "right", "bottom", "top") ggsave("Figure2.jpeg",p1,width = 16, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:Bralibase_scores.jpeg]] *** Homfam **** Create test alignments create script to merge =*_ref.vie= and =*_test-only.vie= alignments. #+BEGIN_SRC bash -n :tangle create_homfam_tests.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle create_homfam_tests.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will merge *_ref.vie and *_test-only.vie alignments and convert them into msf format.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done find ~/kalignbenchmark/data/homfam -name "*_test-only.vie" | awk '{test_aln=$1;ref=$1 ;gsub("_test-only.vie", "_ref.vie", ref);gsub("_test-only.vie", "_testaln.fa", test_aln); printf "kalign %s %s --reformat --rename -f fasta -o %s\n",ref, $1, test_aln }' > homfam_reformat.sh find ~/kalignbenchmark/data/homfam -name "*_ref.vie" | awk '{ref=$1;gsub("_ref.vie", "_ref.msf", ref);printf "kalign %s --reformat --rename -f msf -o %s\n", $1,ref}' >> homfam_reformat.sh chmod 755 homfam_reformat.sh #+END_SRC **** Run alignment programs on Homfam #+BEGIN_SRC bash -n :tangle gen_run_homfam.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_run_homfam.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the balibase MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done printf "Generating kalign run script\n" printf "export PATH=~/kalignbenchmark/programs:$PATH\n\n" > run_homfam.sh find ~/kalignbenchmark/data/homfam -name "*_testaln.fa" | awk -v outdir="$SCRATCHDIR" '{ ref_aln=$1;gsub("_testaln.fa", "_ref.msf",ref_aln); printf "timescorealn -test %s -ref %s -program kalign --scratch ~/kalignbenchmark/scratch -out scores_homfam.csv \n", $1,ref_aln; printf "timescorealn -test %s -ref %s -program muscle --scratch ~/kalignbenchmark/scratch -out scores_homfam.csv\n", $1,ref_aln; printf "timescorealn -test %s -ref %s -program clustalo --scratch ~/kalignbenchmark/scratch -out scores_homfam.csv\n", $1,ref_aln; }' >> run_homfam.sh chmod 755 run_homfam.sh #+END_SRC #+BEGIN_EXAMPLE bash -n :tangle run_homfam.sh :noweb yes :exports code :results none :noweb yes :shebang #!/usr/bin/env bash <> ./run_homfam.sh #+END_EXAMPLE /home/user/kalignbenchmark/data/homfam printf "Generating clustalo run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" -o -name '*.fasta' | grep unaligned | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo-1.2.4-Ubuntu-x86_64 --outfmt=msf --in %s --force --out %s/%s_%s_%s_clustalo.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2] }' > run_clustalo_bralibase.sh chmod 755 run_clustalo_bralibase.sh **** plot scores #+BEGIN_SRC R :session one :results output graphics :file homfam_runtime.jpeg :exports both :noweb yes <> mat <- read.csv("scores_homfam.csv"); p <- ggplot(mat, aes(NUMSEQ,Time)) p <- p + geom_point(aes(colour = Program,size = AVGLEN)) p <- p + scale_x_continuous(trans = 'log10', breaks=c(100,3000,10000,100000) ,labels = scales::comma) p <- p + scale_y_continuous(trans = 'log10',breaks=c(0.01, 0.1, 1 , 10,100,1000,10000), labels=scales::comma ) ggsave("homfam_runtime.jpeg",p,width = 18, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:homfam_runtime.jpeg]] q[[file:homfam_runtime.jpeg]] #+BEGIN_SRC R :session one :results output graphics :file homfam_scores.jpeg :exports both :noweb yes p <- ggplot(mat, aes(Program, SP)) p <- p + geom_boxplot(aes(colour = Program)) ggsave("homfam_scores.jpeg",p,width = 16, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:homfam_scores.jpeg]] *** QuantTest 2 **** script to generate run and score commands #+BEGIN_SRC bash -n :tangle gen_run_quanttest2.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_run_quanttest2.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the quanttest2 MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done printf "Generating kalign run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -i %s -o %s/%s_kalign.afa\n", $1,outdir,a[n-1] }' > run_kalign_quanttest.sh chmod 755 run_kalign_quanttest.sh printf "Generating muscle run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "muscle3.8.31_i86linux32 -in %s -out %s/%s_muscle.afa\n", $1,outdir,a[n-1] }' > run_muscle_quanttest.sh chmod 755 run_muscle_quanttest.sh printf "Generating clustalo run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo-1.2.4-Ubuntu-x86_64 --threads 8 --outfmt=a2m --in %s --out %s/%s_clustalo.afa\n", $1,outdir,a[n-1] }' > run_clustalo_quanttest.sh chmod 755 run_clustalo_quanttest.sh printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_kalign.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_kalign_quanttest.sh chmod 755 score_kalign_quanttest.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_muscle.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_muscle_quanttest.sh chmod 755 score_muscle_quanttest.sh printf "Generating clustalo scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_clustalo.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_clustalo_quanttest.sh chmod 755 score_clustalo_quanttest.sh #+END_SRC **** Run jobs and score alignments #+BEGIN_SRC bash -n :tangle run_quanttest.sh :noweb yes :exports code :results none :noweb yes :shebang #!/usr/bin/env bash <> ./run_muscle_quanttest.sh ./run_kalign_quanttest.sh ./run_clustalo_quanttest.sh ./score_kalign_quanttest.sh > scores_quanttest_kalign.csv ./score_muscle_quanttest.sh > scores_quanttest_muscle.csv ./score_clustalo_quanttest.sh > scores_quanttest_clustalo.csv #+END_SRC **** Plot #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readquanttestscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Name","Score") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[_]{1}[A-Z]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as_tibble(mat) return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file Quantest.jpeg :exports both :noweb yes mat <- readquanttestscores("scores_quanttest_clustalo.csv","clustalo") mat <- rbind(mat, readquanttestscores("scores_quanttest_kalign.csv","kalign")); mat <- rbind(mat, readquanttestscores("scores_quanttest_muscle.csv","muscle")); #mat <- mat %>% mutate(Score = as.dbl(Score)) p4 <- ggplot(mat, aes(Type, Score)) p4 <- p4 + geom_boxplot(aes(colour = Type)) means <- aggregate(Score ~ Type, mat, median) means$Score <- round(means$Score,digits = 4) p4 <- p4 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p4 <- p4 + geom_text(data = means, aes(label = Score, y = Score + 0.08)) ggsave("Quantest.jpeg",p4,width = 16, height = 12, dpi = 300, units = c( "cm")) t = t.test(filter(mat,Type == "kalign")$Score,filter(mat,Type == "muscle")$Score, paired = FALSE, alternative = "greater") t = t.test(filter(mat,Type == "kalign")$Score,filter(mat,Type == "clustalo")$Score, paired = FALSE, alternative = "greater") #+END_SRC #+RESULTS: [[file:Quantest.jpeg]] *** Rose :PROPERTIES: :ORDERED: t :END: #+BEGIN_SRC bash -n :tangle create_rose_tests.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will merge *_ref.vie and *_test-only.vie alignments and convert them into msf format.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done numseq=(100 1000 10000 100000 1000000 10000000) for item in ${numseq[*]} do printf "# kalignbenchmark sample 1\n" > rose.in; printf "\n" >> rose.in; printf "%%include protein-defaults\n" >> rose.in; printf "\n" >> rose.in; printf "StdOut= False\n" >> rose.in printf "OutputFilebase = \"rosetest%d\"\n" $item >> rose.in; printf "AlignmentFormat= \"FASTA\"\n" >> rose.in; printf "SequenceLen = 500\n" >> rose.in; printf "SequenceNum = %d\n" $item >> rose.in; printf "ChooseFromLeaves = False\n" >> rose.in; printf "TreeWithSequences = True\n" >> rose.in; rose rose.in done #+END_SRC # rose sample 1 %include protein-defaults OutputFilename = "TIMO" OutputFilebase = "timotest" AlignmentFormat= "FASTA" SequenceLen = 500 SequenceNum = 1000 ChooseFromLeaves = False TreeWithSequences = True * Paper figure #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file Paper_figure.jpeg :exports both :noweb yes library(ggplot2) library(ggsignif) my_comparisons = list( c("clustalo", "kalign"), c("kalign", "muscle"), c("clustalo", "muscle") ) mat <- readBaliscores("scores_kalign.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo.csv","clustalo")); p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) p1 <- p1 + scale_color_discrete(name = "Program") p1 <- p1 + theme(legend.position = c(0.25, 0.15),legend.text=element_text(size=16), legend.direction = "horizontal") p1 <- p1 + xlab(label="") ## p1 <- p1 + geom_signif(comparisons = my_comparisons ,map_signif_level=TRUE) mat <- readBaliscores("scores_kalign_bralibase.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle_bralibase.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo_bralibase.csv","clustalo")); ## mat <- rbind(mat,readBaliscores("scores_kalign_3.csv","kalign 3")); ## mata ## <- rbind(mat,readBaliscores("scores_kalign_bibpm.csv","bibpm")); ## mat ## <- rbind(mat,readBaliscores("scores_kalign_bibpm_zero.csv","bibpm_zero")); p2 <- ggplot(mat, aes(Group, SP)) p2 <- p2 + geom_boxplot(aes(colour = Type)) p2 <- p2 + scale_color_discrete(name = "Program") p2 <- p2 + theme(legend.position = c(0.25, 0.15),legend.text=element_text(size=16), legend.direction = "horizontal") p2 <- p2 + xlab(label="") # p2 <- p2 + theme(plot.margin = margin(t = 0, b = -0.2, unit = "cm"))# unit(c(1,2,3,4), "cm")) # t = 0, r mat <- read.csv("scores_homfam.csv"); p3 <- ggplot(mat, aes(NUMSEQ,Time)) p3 <- p3 + geom_point(aes(colour = Program,size = AVGLEN)) p3 <- p3 + scale_x_continuous(trans = 'log10', breaks=c(100,3000,10000,100000) ,labels = scales::comma) p3 <- p3 + scale_y_continuous(trans = 'log10',breaks=c(0.01, 0.1, 1 , 10,100,1000,10000),labels=scales::comma ) p3 <- p3 + xlab("Number of sequences") p3 <- p3 + ylab("Time (s)") p = plot_grid(p1, p2, p3, labels = c('A','B','C'), ncol = 1, nrow = 3, rel_heights = c(0.7,0.7,1.0)) ggsave("Paper_figure.jpeg",p,width = 18, height = 24, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:Paper_figure.jpeg]] Test for statistical significant differences: #+BEGIN_SRC R :session one assess_diff_alignment_scores <-function(mat, groupname,tests){ f <- filter(mat, Group == groupname) %>% select(-TC,-Group) %>% spread(Type, SP) %>% column_to_rownames("Name") t = t.test(f$kalign,f$clustalo, paired = TRUE, alternative = "greater") if(t$p.value * tests <= 0.05){ message("sig diff: kalign, clustalo ", groupname," ", t$p.value); message("Clustalo ", mean(f$clustalo)); message("Kalign ", mean(f$kalign)); message("Muscle ", mean(f$muscle)); } t = t.test(f$kalign,f$muscle, paired = TRUE, alternative = "greater") if(t$p.value * tests <= 0.05){ message("sig diff: kalign, muscle ", groupname," ", t$p.value); message("Clustalo ", mean(f$clustalo)); message("Kalign ", mean(f$kalign)); message("Muscle ", mean(f$muscle)); } } mat <- readBaliscores("scores_kalign.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo.csv","clustalo")); g <- unique(mat$Group) tests = length(g) * length(unique(mat$Type)) for (i in g){ assess_diff_alignment_scores(mat,i,tests); } mat <- readBaliscores("scores_kalign_bralibase.csv","kalign"); mat <- rbind(mat,readBaliscores("scores_muscle_bralibase.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_clustalo_bralibase.csv","clustalo")); g <- unique(mat$Group) tests = length(g) * length(unique(mat$Type)) for (i in g){ assess_diff_alignment_scores(mat,i,tests); } #+END_SRC #+RESULTS: * Slurm scripts #+BEGIN_SRC bash -n :tangle srun_kalign.sh :shebang #!/usr/bin/env bash :exports code :results none :noweb yes #SBATCH -N 1 # number of nodes #SBATCH -n 1 # number of cores #SBATCH -t 0-10:00 # time (D-HH:MM) #SBATCH -o slurm.%N.%j.out # STDOUT #SBATCH -e slurm.%N.%j.err # STDERR DIR=`pwd` OUTPUT= INPUT= function usage() { printf "Slurm script to run kalign.\n\n" ; printf "usage: $0 -i input -o output\n\n" ; exit 1; } while getopts i:o: opt do case ${opt} in i) INPUT=${OPTARG};; o) OUTPUT=${OPTARG};; ,*) usage;; esac done if [ "${INPUT}" == "" ]; then usage; fi if [ "${OUTPUT}" == "" ]; then usage; fi kalign -i $INPUT -f msf -o $OUTPUT #+END_SRC kalign-3.5.1/scripts/benchmark.org000066400000000000000000000677121515023132300171270ustar00rootroot00000000000000#+TITLE: Benchmark of MSA algorithms #+AUTHOR: Timo Lassmann #+EMAIL: timolassmann@icloud.com #+DATE: 2019-09-27 #+LATEX_CLASS: report #+OPTIONS: toc:nil #+OPTIONS: H:4 #+LATEX_CMD: pdflatex * Introduction This org mode document contains the code and scripts to generate the results presented in: #+begin_quote Kalign 3: multiple sequence alignment of large data sets. #+end_quote * Benchmark ** MSA programs Programs to test: #+NAME: Benchprograms | Name | Download | |--------------+------------------------------------------------------------------------------| | Muscle | https://www.drive5.com/muscle/downloads3.8.31/muscle3.8.31_i86linux32.tar.gz | | Clustalomega | http://clustal.org/omega/clustalo-1.2.4-Ubuntu-x86_64 | | q-score | https://www.drive5.com/qscore/qscore_src.tar.gz | | Kalign2 | http://msa.sbc.su.se/downloads/kalign/current.tar.gz | Since the clustal papers provide a great overview of alignment programs I think these two should suffice. Note I am not comparing kalign against consistency based methods as these are a) all more accurate and b) slower. Kalign is targeted at the accurate and fast niche. ** Alignment benchmark datasets #+NAME: Benchmarkdata | Name | Download | |---------------+-------------------------------------------------------------------| | Balibase1-5 | http://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz | | Balifam | http://clustal.org/omega/bali3fam-26.tar.gz | | Bralibaseset1 | http://projects.binf.ku.dk/pgardner/bralibase/data-set1.tar.gz | | Bralibaseset2 | http://projects.binf.ku.dk/pgardner/bralibase/data-set2.tar.gz | | HomFam | http://www.clustal.org/omega/homfam-20110613-25.tar.gz | | QuanTest2 | http://bioinf.ucd.ie/quantest2.tar | ** Installation *** Step one: create directory and download all data #+BEGIN_SRC bash :exports both :results none cd mkdir -p kalignbenchmark cd kalignbenchmark mkdir -p data mkdir -p programs mkdir -p scratch #+END_SRC *** Download data sets #+BEGIN_SRC bash -n :results raw :exports both :var tbl=Benchmarkdata :colnames yes cd cd ~/kalignbenchmark/data for idx in ${!tbl[*]}; do #echo ${tbl[$idx]} if [[ ${tbl[$idx]} =~ ^http* ]]; then echo "wget ${tbl[$idx]}" wget ${tbl[$idx]} fi done #+END_SRC #+RESULTS: wget http://www.clustal.org/omega/homfam-20110613-25.tar.gz wget http://clustal.org/omega/bali3fam-26.tar.gz wget http://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz wget http://projects.binf.ku.dk/pgardner/bralibase/data-set1.tar.gz wget http://projects.binf.ku.dk/pgardner/bralibase/data-set2.tar.gz wget http://bioinf.ucd.ie/quantest2.tar Unpack data sets: #+BEGIN_SRC bash :results none :exports code cd cd ~/kalignbenchmark/data mkdir -p homfam mv homfam-20110613-25.tar.gz homfam cd homfam tar -zxvf homfam-20110613-25.tar.gz cd ~/kalignbenchmark/data for filename in *.tar.gz; do tar -zxvf $filename done tar -xvf quantest2.tar #+END_SRC **** Compile baliscore program and place in program directory #+BEGIN_SRC bash cd ~/kalignbenchmark/data/bb3_release/bali_score_src gcc *.c -lm -lexpat -o bali_score cp bali_score ~/kalignbenchmark/programs #+END_SRC #+RESULTS: *** Download programs #+BEGIN_SRC bash -n :results none :exports none :var tbl=Benchprograms :colnames yes cd cd ~/kalignbenchmark/programs for idx in ${!tbl[*]}; do #echo ${tbl[$idx]} if [[ ${tbl[$idx]} =~ ^http* ]]; then echo "wget ${tbl[$idx]}" wget ${tbl[$idx]} fi done chmod 755 clustalo-1.2.4-Ubuntu-x86_64 tar -zxvf muscle3.8.31_i86linux32.tar.gz tar -zxvf qscore_src.tar.gz mkdir -p kalign2_src cd kalign2_src mv ../current.tar.gz . tar -zxvf current.tar.gz ./configure make cp kalign ../kalign2 #+END_SRC And kalign3! #+BEGIN_SRC bash -n :results none :exports none cd ~/kalignbenchmark/programs mkdir -p kalign3_src cd kalign3_src git clone https://github.com/TimoLassmann/kalign.git cd kalign ./autogen.sh ./configure --bindir=$HOME/kalignbenchmark/programs make make check make install #+END_SRC *** Compile q-score NOTE: this is manual! Add: #+BEGIN_EXAMPLE C #include #+END_EXAMPLE to qscore header: =qscore.h= then: #+BEGIN_EXAMPLE bash make #+END_EXAMPLE *** Reformat datasets #+BEGIN_SRC bash :exports both :results none export PATH=$HOME/kalignbenchmark/programs:$PATH find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" -exec rm -rf {} \; printf "Generate commands to reformat the reference alignments\n\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.fa" -o -name '*.fasta' | grep structural | awk -v outdir="$SCRATCHDIR" '{printf "kalign %s --changename -reformat msf -o %s.msf\n", $1,$1 }' > run_reformat.sh find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.fa" -o -name '*.fasta' | grep unaligned | awk -v outdir="$SCRATCHDIR" '{printf "kalign %s --changename -reformat fasta -o %s_test.fa\n", $1,$1 }' >> run_reformat.sh chmod 755 run_reformat.sh ./run_reformat.sh #+END_SRC *** Reformat homfam create script to merge =*_ref.vie= and =*_test-only.vie= alignments. #+BEGIN_SRC bash -n :tangle create_homfam_tests.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle create_homfam_tests.sh :noweb yes :exports code :results none :noweb yes export PATH=$HOME/kalignbenchmark/programs:$PATH DIR=`pwd` function usage() { printf "This script will merge *_ref.vie and *_test-only.vie alignments and convert them into msf format.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done find ~/kalignbenchmark/data/homfam -name "*_test-only.vie" | awk '{\ test_aln=$1;ref=$1 ;\ gsub("_test-only.vie", "_ref.vie", ref);\ gsub("_test-only.vie", "_testaln.fa", test_aln);\ printf "cat %s %s > tmp.vie\n" ,ref, $1;\ printf "kalign tmp.vie --reformat --rename -f fasta -o %s\n", test_aln }' > homfam_reformat.sh find ~/kalignbenchmark/data/homfam -name "*_ref.vie" | awk '{ref=$1;gsub("_ref.vie", "_ref.msf", ref);printf "kalign %s --reformat --rename -f msf -o %s\n", $1,ref}' >> homfam_reformat.sh chmod 755 homfam_reformat.sh ./homfam_reformat.sh #+END_SRC ** Scripts to run programs Alignment programs #+BEGIN_SRC sh :exports both :results none :noweb yes :tangle run_aln.sh :shebang #!/usr/bin/env bash export PATH=$HOME/kalignbenchmark/programs:$PATH CPU=1 MEM=8 FMT= IN= OUT= PROG= function usage() { printf "usage: $0 -p -i -o \n\n" ; printf "Options:\n-f \n-t \n-m \n\n"; printf "Valid options for include:\n kalign\n kalign2\n muscle\n clustal\n\n"; exit 1; } while getopts t:m:p:i:o:f: opt do case ${opt} in t) CPU=${OPTARG};; m) MEM=${OPTARG};; p) PROG=${OPTARG};; i) IN=${OPTARG};; o) OUT=${OPTARG};; f) FMT=${OPTARG};; ,*) usage;; esac done if [ "${PROG}" == "" ]; then usage; fi if [ "${IN}" == "" ]; then usage; fi if [ "${OUT}" == "" ]; then usage; fi SLURMMEM=$MEM"G" CMD= if [ "${PROG}" == "kalign2" ]; then if [ "${FMT}" == "msf" ]; then CMD="kalign2 -i $IN -f msf -o $OUT" else CMD="kalign2 -i $IN -o $OUT" fi fi if [ "${PROG}" == "kalign" ]; then if [ "${FMT}" == "msf" ]; then CMD="kalign -i $IN -f msf -o $OUT" else CMD="kalign -i $IN -o $OUT" fi fi if [ "${PROG}" == "muscle" ]; then if [ "${FMT}" == "msf" ]; then CMD="muscle3.8.31_i86linux32 -msf -in $IN -out $OUT" else CMD="muscle3.8.31_i86linux32 -in $IN -out $OUT" fi fi if [ "${PROG}" == "clustal" ]; then if [ "${FMT}" == "msf" ]; then CMD="clustalo-1.2.4-Ubuntu-x86_64 --outfmt=msf --in $IN --out $OUT" else CMD="clustalo-1.2.4-Ubuntu-x86_64 --outfmt=a2m --in $IN --out $OUT" fi fi HAS_SLURM=0 # printf "Running Sanity checks:\n"; if which sbatch >/dev/null; then HAS_SLURM=1 fi # echo $HAS_SLURM if [ $HAS_SLURM = 1 ]; then echo "YES" sbatch < -i -r \n\n" ; printf "Options:\n-f \n-t \n-m \n\n"; printf "Valid options for include:\n kalign\n kalign2\n muscle\n clustal\n\n"; exit 1; } while getopts t:m:p:i:r: opt do case ${opt} in t) CPU=${OPTARG};; m) MEM=${OPTARG};; p) PROG=${OPTARG};; i) IN=${OPTARG};; r) OUT=${OPTARG};; ,*) usage;; esac done if [ "${PROG}" == "" ]; then usage; fi if [ "${IN}" == "" ]; then usage; fi if [ "${OUT}" == "" ]; then usage; fi SLURMMEM=$MEM"G" CMD= CMD="timescorealn -test $IN -ref $OUT -program $PROG --scratch $HOME/kalignbenchmark/scratch -out scores_homfam.csv" HAS_SLURM=0 # printf "Running Sanity checks:\n"; if which sbatch >/dev/null; then HAS_SLURM=1 fi # echo $HAS_SLURM if [ $HAS_SLURM = 1 ]; then echo "YES" sbatch <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_cmd.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the balibase MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done printf "Generating balibase run commands\n"; find ~/kalignbenchmark/data/bb3_release -name "*.tfa" |\ awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/);\ printf "run_aln.sh -p kalign -i %s -f msf -o %s/%s_%s_kalign.msf\n", $1,outdir,a[n-2],a[n-1];\ printf "run_aln.sh -p kalign2 -i %s -f msf -o %s/%s_%s_kalign2.msf\n", $1,outdir,a[n-2],a[n-1];\ printf "run_aln.sh -p muscle -i %s -f msf -o %s/%s_%s_muscle.msf\n", $1,outdir,a[n-2],a[n-1];\ printf "run_aln.sh -p clustal -i %s -f msf -o %s/%s_%s_clustal.msf\n", $1,outdir,a[n-2],a[n-1];\ }' > run_benchmark.sh printf "Generating bralibase run commands\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*_test.fa" |\ grep unaligned |\ awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/);\ printf "run_aln.sh -p kalign -i %s -f msf -o %s/%s_%s_%s_kalign.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2];\ printf "run_aln.sh -p kalign2 -i %s -f msf -o %s/%s_%s_%s_kalign2.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2];\ printf "run_aln.sh -p muscle -i %s -f msf -o %s/%s_%s_%s_muscle.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2];\ printf "run_aln.sh -p clustal -i %s -f msf -o %s/%s_%s_%s_clustal.msf\n", $1,outdir,a[n-4],a[n-3],a[n-2];\ }' >> run_benchmark.sh printf "Generating Quantest2 run commands\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" |\ awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/);\ printf "run_aln.sh -p kalign -i %s -o %s/%s_kalign.afa\n", $1,outdir,a[n-1];\ printf "run_aln.sh -p kalign2 -i %s -o %s/%s_kalign2.afa\n", $1,outdir,a[n-1];\ printf "run_aln.sh -p muscle -i %s -o %s/%s_muscle.afa\n", $1,outdir,a[n-1];\ printf "run_aln.sh -p clustal -i %s -o %s/%s_clustal.afa\n", $1,outdir,a[n-1];\ }' >> run_benchmark.sh chmod 755 run_benchmark.sh #+END_SRC Here is the script to run the homfam test: #+BEGIN_SRC bash -n :tangle gen_timecmd.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle gen_timecmd.sh :noweb yes :exports code :results none :noweb yes DIR=`pwd` function usage() { printf "This script will generate scripts to run kalign, clustal omega and muscle on the balibase MSA benchmark data set.\n\n" ; printf "usage: $0\n\n" ; exit 1; } while getopts h opt do case ${opt} in h) usage;; ,*) usage;; esac done printf "export PATH=~/kalignbenchmark/programs:$PATH\n\n" > run_homfam.sh find ~/kalignbenchmark/data/homfam -name "*_testaln.fa" | awk -v outdir="$SCRATCHDIR" '{ ref_aln=$1;gsub("_testaln.fa", "_ref.msf",ref_aln); printf "run_timealn.sh -p kalign -i %s -r %s\n", $1,ref_aln; printf "run_timealn.sh -p muscle -i %s -r %s\n", $1,ref_aln; printf "run_timealn.sh -p clustal -i %s -r %s\n", $1,ref_aln; }' >> run_homfam.sh #+END_SRC * Score alignments ** Balibase #+BEGIN_SRC bash -n :tangle score_balibase.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle score_balibase.sh :noweb yes :exports code :results none :noweb yes printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_kalign.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_kalign.sh chmod 755 score_bb3_kalign.sh find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_kalign2.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_kalign2.sh chmod 755 score_bb3_kalign2.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_muscle.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_muscle.sh chmod 755 score_bb3_muscle.sh printf "Generating clustal scoring run script\n"; find ~/kalignbenchmark/data/bb3_release -name "*.xml" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_clustal.msf | grep auto\n", $1,outdir,a[n-2],a[n-1] }' > score_bb3_clustal.sh chmod 755 score_bb3_clustal.sh ./score_bb3_kalign.sh > scores_balibase_kalign.csv ./score_bb3_kalign2.sh > scores_balibase_kalign2.csv ./score_bb3_muscle.sh > scores_balibase_muscle.csv ./score_bb3_clustal.sh > scores_balibase_clustal.csv #+END_SRC To plot the scores: #+NAME: Rplotheader #+BEGIN_SRC R :session one :results none :export none :noweb yes <> readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC The actual plotting: #+BEGIN_SRC R :session one :results output graphics :file Balibase_scores.jpeg :exports both :noweb yes library(ggplot2) mat <- readBaliscores("scores_balibase_kalign.csv","kalign3"); mat <- rbind(mat,readBaliscores("scores_balibase_kalign2.csv","kalign2")); mat <- rbind(mat,readBaliscores("scores_balibase_muscle.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_balibase_clustal.csv","clustalo")); p <- ggplot(mat, aes(Group, SP)) p <- p + geom_boxplot(aes(colour = Type)) p <- p + scale_color_discrete(name = "Program") p <- p + theme(legend.position = c(0.6, 0.15),legend.text=element_text(size=14),legend.direction = "horizontal") p <- p + xlab(label="") p <- p + ggtitle("Balibase benchmark") p # ggsave("Balibase_scores.jpeg",p,width = 18, height = 12, dpi = 300, units = c( "cm")) ## p1 <- p1 + geom_signif(comparisons = my_comparisons ,map_signif_level=TRUE) #+END_SRC #+RESULTS: ** Bralibase #+BEGIN_SRC bash -n :tangle score_bralibase.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle score_bralibase.sh :noweb yes :exports code :results none :noweb yes printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_%s_kalign.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_bralibase_kalign.sh chmod 755 score_bralibase_kalign.sh printf "Generating kalign2 scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_%s_kalign2.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_bralibase_kalign2.sh chmod 755 score_bralibase_kalign2.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_%s_muscle.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_bralibase_muscle.sh chmod 755 score_bralibase_muscle.sh printf "Generating clustalo scoring run script\n"; find ~/kalignbenchmark/data/data-set1 ~/kalignbenchmark/data/data-set2 -name "*.msf" | grep structural | awk -v outdir="$SCRATCHDIR" '{name=$1; gsub(/structural/, "unaligned"); n=split ($1,a,/[\/,.]/); printf "~/kalignbenchmark/programs/bali_score %s %s/%s_%s_%s_clustal.msf | grep auto\n", name,outdir,a[n-4],a[n-3],a[n-2] }' > score_bralibase_clustal.sh chmod 755 score_bralibase_clustal.sh ./score_bralibase_kalign.sh > scores_bralibase_kalign.csv ./score_bralibase_kalign2.sh > scores_bralibase_kalign2.csv ./score_bralibase_muscle.sh > scores_bralibase_muscle.csv ./score_bralibase_clustal.sh > scores_bralibase_clustal.csv #+END_SRC Plot scores #+BEGIN_SRC R :session one :results output graphics :file Bralibase_scores.jpeg :exports both :noweb yes <> library(ggplot2) mat <- readBaliscores("scores_bralibase_kalign.csv","kalign3"); mat <- rbind(mat,readBaliscores("scores_bralibase_kalign2.csv","kalign2")); mat <- rbind(mat,readBaliscores("scores_bralibase_muscle.csv","muscle")); mat <- rbind(mat,readBaliscores("scores_bralibase_clustal.csv","clustalo")); p <- ggplot(mat, aes(Group, SP)) p <- p + geom_boxplot(aes(colour = Type)) p <- p + scale_color_discrete(name = "Program") p <- p + theme(legend.position = c(0.6, 0.15),legend.text=element_text(size=14),legend.direction = "horizontal") p <- p + xlab(label="") p <- p + ggtitle("Bralibase benchmark") ggsave("Bralibase_scores.jpeg",p,width = 18, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: q[[file:Bralibase_scores.jpeg]] ** Quantest2 #+BEGIN_SRC bash -n :tangle score_quantest2.sh :shebang #!/usr/bin/env bash :noweb yes :exports code :results none :noweb yes <> #+END_SRC #+BEGIN_SRC bash -n :tangle score_quantest2.sh :noweb yes :exports code :results none :noweb yes printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_kalign.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_quantest2_kalign.sh chmod 755 score_quantest2_kalign.sh printf "Generating kalign scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_kalign2.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_quantest2_kalign2.sh chmod 755 score_quantest2_kalign2.sh printf "Generating muscle scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_muscle.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_quantest2_muscle.sh chmod 755 score_quantest2_muscle.sh printf "Generating clustalo scoring run script\n"; find ~/kalignbenchmark/data/QuanTest2/Test -name "*.vie" | awk -v outdir="$SCRATCHDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/kalignbenchmark/data/QuanTest2/quantest2.py test@test %s/%s_clustal.afa ~/kalignbenchmark/data/QuanTest2/SS/%s.ss | grep kalignbenchmark \n", outdir,a[n-1],a[n-1] }' > score_quantest2_clustal.sh chmod 755 score_quantest2_clustal.sh ./score_quantest2_kalign.sh > scores_quantest2_kalign.csv ./score_quantest2_kalign2.sh > scores_quantest2_kalign2.csv ./score_quantest2_muscle.sh > scores_quantest2_muscle.csv ./score_quantest2_clustal.sh > scores_quantest2_clustal.csv #+END_SRC *** Plot #+BEGIN_SRC R :session one :results output graphics :file Quantest2_scores.jpeg :exports both :noweb yes <> readquanttestscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Name","Score") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[_]{1}[A-Z]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as_tibble(mat) return(mat) } library(ggplot2) mat <- readquanttestscores("scores_quanttest_clustalo.csv","clustalo") mat <- rbind(mat, readquanttestscores("scores_quanttest_kalign.csv","kalign3")); mat <- rbind(mat, readquanttestscores("scores_quantest2_kalign2.csv","kalign2")); mat <- rbind(mat, readquanttestscores("scores_quanttest_muscle.csv","muscle")); #mat <- mat %>% mutate(Score = as.dbl(Score)) p4 <- ggplot(mat, aes(Type, Score)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + ggtitle("Quantest2 benchmark") ggsave("Quantest2_scores.jpeg",p4,width = 16, height = 12, dpi = 300, units = c( "cm")) #+END_SRC #+RESULTS: [[file:Quantest2_scores.jpeg]] ** Homfam *** Plot: #+BEGIN_SRC R :session one :results output graphics :file Homfam_scores.jpeg :exports both :noweb yes <> library(ggplot2) mat <- read.csv("scores_homfam.csv"); p <- ggplot(mat, aes(NUMSEQ,Time)) p <- p + geom_point(aes(colour = Program,size = AVGLEN)) p <- p + scale_x_continuous(trans = 'log10', breaks=c(100,3000,10000,100000) ,labels = scales::comma) p <- p + scale_y_continuous(trans = 'log10',breaks=c(0.01, 0.1, 1 , 10,100,1000,10000),labels=scales::comma ) p <- p + xlab("Number of sequences") p <- p + ylab("Time (s)") p <- p + ggtitle("HomFam benchmark") p ggsave("Homfam_scores.jpeg",p,width = 18, height = 12, dpi = 300, units = c( "cm")) #+END_SRC *** Run tests * Check for dependencies #+NAME: liblist #+BEGIN_SRC R -n :exports code :results none libraries <- c("tidyverse","ggplot2","stringi","cowplot") #+END_SRC Script to test if libraries are present. #+BEGIN_SRC R -n :tangle test_for_libraries.R :shebang #!/usr/bin/env Rscript :noweb yes :exports code :results none <> Sys.info()["nodename"] for(library in libraries) { f = is.element(library, installed.packages()[,1]) print(paste("Library",library, "is installed?", f)) if(!f) { message("Missing library:",library ) quit(status=1) } } quit(status=0) #+END_SRC #+BEGIN_SRC sh -n :results output :exports both ./test_for_libraries.R #+END_SRC #+RESULTS: : nodename : "work" : [1] "Library tidyverse is installed? TRUE" : [1] "Library ggplot2 is installed? TRUE" : [1] "Library stringi is installed? TRUE" : [1] "Library cowplot is installed? TRUE" install.packages("tidyverse") Code block to load the libraries in R code. #+NAME: Rlibraries #+BEGIN_SRC R -n :exports code :results none :noweb yes <> lapply(libraries, FUN = function(X) { do.call("library", list(X)) }) #+END_SRC kalign-3.5.1/scripts/bralibase.org000066400000000000000000000214601515023132300171070ustar00rootroot00000000000000#+TITLE: Test kalign on BAliBASE #+AUTHOR: Timo Lassmann #+EMAIL: timolassmann@icloud.com #+DATE: 2019-04-23 #+LATEX_CLASS: report #+OPTIONS: toc:nil #+OPTIONS: H:4 #+LATEX_CMD: pdflatex * Introduction Let's run kalign on the Bralibase benchmark * Method ** script to run test :PROPERTIES: :ORDERED: t :END: The script has points to a bralibase directory containing =structural= and =unaligned= sub directory #+BEGIN_SRC bash -n :tangle run_bralibase_test.sh :shebang #!/usr/bin/env bash :noweb yes DIR=`pwd` INDIR= OUTPUT= function usage() { printf "This script runs kalign for a across the bralibase test sets.\n\n" ; printf "usage: $0 -i -o \n"; exit 1; } while getopts i:o: opt do case ${opt} in i) INDIR=${OPTARG};; o) OUTPUT=${OPTARG};; ,*) usage;; esac done if [ "${INDIR}" == "" ]; then usage; fi if [ "${OUTPUT}" == "" ]; then usage; fi if [ -f $OUTPUT ]; then printf "OUTPUT file exits - will overwrite.\n"; rm $OUTPUT fi function run_test() { dir=$1 name=$2 echo "$name" for file in "$dir"/unaligned/*.fa do ref="${file//unaligned/structural}" # printf "Working on %s\n" $file; kalign $file -o test.msf 2>&1 > /dev/null kalign $ref -r -o ref.msf 2>&1 > /dev/null bali_score ref.msf test.msf | grep auto | awk -v name="$name" '{printf "%s,%f,%f\n" , name, $3, $4}' >> $OUTPUT done } run_test $INDIR/data-set1/g2intron g2intron run_test $INDIR/data-set1/rRNA rRNA run_test $INDIR/data-set1/SRP SRP run_test $INDIR/data-set1/tRNA tRNA run_test $INDIR/data-set1/U5 U5 #+END_SRC #+BEGIN_SRC sh :session onesh cd ~/data/bb3_release cd .. mkdir -p bb3_release_tmp_aln cd bb3_release_tmp_aln KALIGNOUTDIR=$PWD echo $KALIGNOUTDIR #+END_SRC #+RESULTS: | | | sh-4.4$ sh-4.4$ sh-4.4$ sh-4.4$ sh-4.4$ /home/user/data/bb3_release_tmp_aln | #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_25.txt %s -o %s/%s_%skalign25.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_kalign.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_50.txt %s -o %s/%s_%skalign50.msf\n", $1,outdir,a[n-2],a[n-1] }' >> run_kalign.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign -alnp param_75.txt %s -o %s/%s_%skalign75.msf\n", $1,outdir,a[n-2],a[n-1] }' >> run_kalign.sh #+END_SRC #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign %s -o %s/%s_%skalign.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_kalign.sh find ~/data/bb3_release -name "*.tfa" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "clustalo --dealign -i %s --outfmt=msf -o %s/%s_%sclustalo.msf\n", $1,outdir,a[n-2],a[n-1] }' > run_clustalo.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "kalign %s -set 0 -o %s/%s_%skalign_1.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 1 -o %s/%s_%skalign_2.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 2 -o %s/%s_%skalign_3.msf\n", $1,outdir,a[n-2],a[n-1] ; printf "kalign %s -set 3 -o %s/%s_%skalign_4.msf\n", $1,outdir,a[n-2],a[n-1] ; }' > run_kalign_cmsa.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); printf "cmsa -a %s/%s_%skalign_*.msf -out %s/%s_%skalign_cmsa.msf -f msf \n",outdir,a[n-2],a[n-1],outdir,a[n-2],a[n-1] ; }' > run_cmsa.sh chmod 755 run_kalign_cmsa.sh chmod 755 run_kalign.sh chmod 755 run_clustalo.sh #+END_SRC #+RESULTS: #+BEGIN_SRC sh :session onesh :results raw find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_1.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set1.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_2.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set2.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_3.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set3.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_4.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set4.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_5.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_set5.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%skalign_cmsa.msf\n", $1,outdir,a[n-2],a[n-1] }' > alignment_scores_cmsa.sh find ~/data/bb3_release -name "*.xml" | awk -v outdir="$KALIGNOUTDIR" '{n=split ($1,a,/[\/,.]/); ;printf "~/data/bb3_release/bali_score_src/bali_score %s %s/%s_%sclustalo.msf\n", $1,outdir,a[n-2],a[n-1] }' >> alignment_scores.sh chmod 755 alignment_scores.sh #+END_SRC #+RESULTS: run the tests #+BEGIN_SRC sh :session onesh parallel --jobs 5 < ./run_kalign.sh parallel --jobs 5 < ./run_clustalo.sh ./alignment_scores.sh | grep auto > scores2.csv #+END_SRC ** Step 3: plot scores #+BEGIN_SRC R :session one :results none :export none library(tidyverse) library(ggplot2) library(stringi) library(cowplot) readBaliscores <-function(file,name){ mat <- read.table(file); colnames(mat) <- c("Type","Name","SP","TC") mat$Type <- name mat$Name <- sub(".*/" ,"", mat$Name) mat$Name <- sub("[A-Z,_]*[.]{1}[A-Z]*$" ,"", mat$Name,ignore.case = TRUE) mat <- as.tibble(mat) x = str_split(mat$Name, "_", n = Inf, simplify = TRUE) mat$Group <- x[,1] return(mat) } #+END_SRC #+BEGIN_SRC R :session one :results output graphics :file BalibaseSP_scores.jpeg :exports both :width 160 :height 80 mat <- readBaliscores("scores_kalign_old.csv","kalign old"); mat <- rbind(mat,readBaliscores("scores_kalign_newp.csv","kalign newp")); mat <- rbind(mat,readBaliscores("scores_kalign_digi.csv","kalign digi")); mat <- rbind(mat,readBaliscores("scores_kalign_digi2.csv","kalign digi 2")); mat <- rbind(mat,readBaliscores("scores_kalign_dist2.csv","kalign dist 2")); ## mat <- rbind(mat,readBaliscores("scores_kalign_3.csv","kalign 3")); ## mata ## <- rbind(mat,readBaliscores("scores_kalign_bibpm.csv","bibpm")); ## mat ## <- rbind(mat,readBaliscores("scores_kalign_bibpm_zero.csv","bibpm_zero")); p1 <- ggplot(mat, aes(Group, SP)) p1 <- p1 + geom_boxplot(aes(colour = Type)) means <- aggregate(SP ~ Type, mat, mean) means$SP <- round(means$SP,digits = 4) p2 <- ggplot(mat, aes(Type, SP)) p2 <- p2 + geom_boxplot(aes(colour = Type)) p2 <- p2 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p2 <- p2 + geom_text(data = means, aes(label = SP, y = SP + 0.08)) p3 <- ggplot(mat, aes(Group, TC)) p3 <- p3 + geom_boxplot(aes(colour = Type)) means <- aggregate(TC ~ Type, mat, mean) means$TC <- round(means$TC,digits = 4) p4 <- ggplot(mat, aes(Type, TC)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + geom_boxplot(aes(colour = Type)) p4 <- p4 + stat_summary(fun.y=mean, colour="darkred", geom="point", shape=18, size=3,show.legend = FALSE) p4 <- p4 + geom_text(data = means, aes(label = TC, y = TC + 0.08)) p = plot_grid(p1,p2,p3,p4, labels=c("SP", "TC"), ncol = 2, nrow= 2) #+END_SRC #+RESULTS: [[file:BalibaseSP_scores.jpeg]] kalign-3.5.1/src/000077500000000000000000000000001515023132300135475ustar00rootroot00000000000000kalign-3.5.1/src/CMakeLists.txt000066400000000000000000000024551515023132300163150ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.18) # Build library in sub-directories if (NOT TARGET kalign::kalign) add_subdirectory(${PROJECT_SOURCE_DIR}/lib build EXCLUDE_FROM_ALL) endif() # link to installed library # if (NOT TARGET kalign::kalign) # find_package(kalign) # endif() if(NOT KALIGN_PACKAGE_NAME) set(KALIGN_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) endif() if(NOT KALIGN_PACKAGE_VERSION) set(KALIGN_PACKAGE_VERSION ${KALIGN_LIBRARY_VERSION_STRING}) endif() #configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/version.h.in" #"version.h" ) add_executable(kalign-bin run_kalign.c parameters.c ) # Link with static library.... target_link_libraries(kalign-bin PRIVATE tldevel ${PROJECT_NAME}_static ) if(OpenMP_C_FOUND) target_link_libraries(kalign-bin PRIVATE OpenMP::OpenMP_C) endif(OpenMP_C_FOUND) target_compile_definitions(kalign-bin PRIVATE KALIGN_PACKAGE_NAME=\"${KALIGN_PACKAGE_NAME}\" KALIGN_PACKAGE_VERSION=\"${KALIGN_PACKAGE_VERSION}\") set_target_properties(kalign-bin PROPERTIES OUTPUT_NAME kalign) add_executable(kalignfmt run_reformat.c parameters.c ) if(OpenMP_C_FOUND) target_link_libraries(kalignfmt PRIVATE OpenMP::OpenMP_C) endif(OpenMP_C_FOUND) install(TARGETS kalign-bin DESTINATION bin) target_link_libraries(kalignfmt PRIVATE tldevel kalign::kalign) kalign-3.5.1/src/parameters.c000066400000000000000000000060121515023132300160550ustar00rootroot00000000000000#include "tldevel.h" #include "kalign/kalign.h" #include #ifdef _WIN32 #include #else #include #endif #ifdef HAVE_OPENMP #include #endif #define PARAMETERS_IMPORT #include "parameters.h" static int get_default_thread_count(void) { int cores = 1; #ifdef HAVE_OPENMP cores = omp_get_num_procs(); #elif defined(_WIN32) SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); cores = sysinfo.dwNumberOfProcessors; #elif defined(_SC_NPROCESSORS_ONLN) cores = sysconf(_SC_NPROCESSORS_ONLN); if (cores <= 0) cores = 1; #endif if (cores > 1) cores = cores - 1; if (cores > 16) cores = 16; if (cores < 1) cores = 1; return cores; } struct parameters*init_param(void) { struct parameters* param = NULL; MMALLOC(param, sizeof(struct parameters)); param->dist_method = KALIGNDIST_BPM; param->aln_param_file = NULL; param->param_set = -1; param->infile = NULL; param->num_infiles = 0; param->input = NULL; param->outfile = NULL; param->format = NULL; param->reformat = 0; param->rename = 0; param->help_flag = 0; param->dump_internal = 0; param->type = -1; param->gpo = -1.0; param->gpe = -1.0; param->tgpe = -1.0; param->matadd = 0.0F; param->chaos = 0; param->nthreads = get_default_thread_count(); param->clean = 0; param->unalign = 0; param->refine = KALIGN_REFINE_NONE; param->adaptive_budget = 0; param->ensemble = 0; param->ensemble_seed = 42; param->min_support = 0; param->save_poar = NULL; param->load_poar = NULL; param->consistency_anchors = 5; param->consistency_weight = 2.0f; param->realign = 0; param->vsm_amax = -1.0f; /* sentinel: use C defaults */ param->mode = 0; /* 0=default, 1=fast, 2=precise */ param->quiet = 0; return param; ERROR: free_parameters(param); return NULL; } void free_parameters(struct parameters* param) { if(param){ if(param->num_infiles){ MFREE(param->infile); } MFREE(param); } } int check_msa_format_string(char* format) { int ok = 0; if(format){ if(strstr(format,"msf")){ ok = 1; }else if(strstr(format,"clu")){ ok = 1; }else if(strstr(format,"fasta")){ ok = 1; }else if(strstr(format,"fa")){ ok = 1; }else{ ok = 0; ERROR_MSG("Format %s not recognized.",format); } if(!ok){ ERROR_MSG("Format %s not recognized.",format); } } return OK; ERROR: return FAIL; } kalign-3.5.1/src/parameters.h000066400000000000000000000026211515023132300160640ustar00rootroot00000000000000#ifndef PARAMETERS_H #define PARAMETERS_H #include #ifdef PARAMETERS_IMPORT #define EXTERN #else #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN extern #endif #endif #define KALIGNDIST_ALN 0 #define KALIGNDIST_BPM 1 #define KALIGNDIST_WU 2 struct parameters{ char **infile; char *input; char *outfile; char* format; char* aln_param_file; int type; float gpo; float gpe; float tgpe; float matadd; int chaos; int out_format; int param_set; int dist_method; int num_infiles; int reformat; int rename; /* rename sequences - to make bali_score swallow the alignments */ int dump_internal; int nthreads; int clean; int unalign; int refine; int adaptive_budget; int ensemble; uint64_t ensemble_seed; int min_support; char* save_poar; char* load_poar; int consistency_anchors; float consistency_weight; int realign; float vsm_amax; int mode; /* 0=default, 1=fast, 2=precise */ int help_flag; int quiet; }; EXTERN struct parameters* init_param(void); EXTERN void free_parameters(struct parameters *param); EXTERN int check_msa_format_string(char* format); #undef PARAMETERS_IMPORT #undef EXTERN #endif kalign-3.5.1/src/run_kalign.c000066400000000000000000000511571515023132300160550ustar00rootroot00000000000000#include "tldevel.h" #include "tlmisc.h" #include "kalign/kalign.h" /* #include "version.h" */ #include "parameters.h" #include #include #include #include #define OPT_SET 1 #define OPT_SHOWW 5 #define OPT_GPO 6 #define OPT_GPE 7 #define OPT_TGPE 8 #define OPT_NTHREADS 10 #define OPT_ALN_TYPE 13 #define OPT_REFINE 14 #define OPT_ADAPTIVE_BUDGET 15 #define OPT_ENSEMBLE 16 #define OPT_ENSEMBLE_SEED 17 #define OPT_MIN_SUPPORT 18 #define OPT_SAVE_POAR 19 #define OPT_LOAD_POAR 20 #define OPT_CONSISTENCY 21 #define OPT_FAST 22 #define OPT_PRECISE 23 #define OPT_REALIGN 24 #define OPT_VSM_AMAX 25 #define OPT_CONSISTENCY_WEIGHT 26 static int set_aln_type(char* in, int* type ); static int set_refine_mode(char* in, int* refine); static int run_kalign(struct parameters* param); static int print_kalign_header(void); static int print_kalign_help(char * argv[]); static int print_kalign_warranty(void); int print_kalign_help(char * argv[]) { const char usage[] = " -i -o "; char* basename = NULL; RUN(tlfilename(argv[0], &basename)); fprintf(stdout,"\nUsage: %s %s\n\n",basename ,usage); fprintf(stdout,"Modes:\n\n"); fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"(default)","Consistency anchors + VSM (best general-purpose)"); fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"--fast","VSM only, no consistency (fastest)"); fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"--precise","Ensemble(3) + VSM + realign (highest precision)"); fprintf(stdout,"\nOptions:\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--format","Output format." ,"[Fasta]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--type","Alignment type (rna, dna, internal)." ,"[rna]" ); fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,"","Options: protein, divergent (protein)" ,"" ); fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,""," rna, dna, internal (nuc)." ,"" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpo","Gap open penalty." ,"[]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpe","Gap extension penalty." ,"[]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--tgpe","Terminal gap extension penalty." ,"[]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--refine","Refinement mode." ,"[none]"); fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,"","Options: none, all, confident" ,"" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-n/--nthreads","Number of threads." ,"[auto: N-1, max 16]"); fprintf(stdout,"\nEnsemble options:\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--ensemble","Number of ensemble runs." ,"[off; 5 if no value given]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--ensemble-seed","RNG seed for ensemble." ,"[42]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--min-support","Explicit consensus threshold." ,"[auto]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--save-poar","Save POAR table to file." ,"[off]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--load-poar","Load POAR table for re-threshold." ,"[off]"); fprintf(stdout,"\nAdvanced (usually managed by modes):\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--consistency","Anchor consistency (K anchors)." ,"[5]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--consistency-weight","Consistency anchor weight." ,"[2.0]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--realign","Alignment-guided tree rebuild iters." ,"[0]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--vsm-amax","VSM amplitude (0 to disable)." ,"[auto]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--adaptive-budget","Scale refinement trials by uncertainty." ,"[off]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--version (-V/-v)","Prints version." ,"[NA]" ); fprintf(stdout,"\nExamples:\n\n"); fprintf(stdout,"Passing sequences via stdin:\n\n cat input.fa | kalign -i - -f fasta > out.afa\n\n"); fprintf(stdout,"Combining multiple input files:\n\n kalign seqsA.fa seqsB.fa seqsC.fa -f fasta > combined.afa\n\n"); if(basename){ MFREE(basename); } return OK; ERROR: if(basename){ MFREE(basename); } return FAIL; } int print_kalign_warranty(void) { fprintf(stdout,"Disclaimer of Warranty (Apache License, Version 2.0, Section 7):\n"); fprintf(stdout,"\n"); fprintf(stdout,"Unless required by applicable law or agreed to in writing, Licensor\n"); fprintf(stdout,"provides the Work (and each Contributor provides its Contributions)\n"); fprintf(stdout,"on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\n"); fprintf(stdout,"either express or implied, including, without limitation, any\n"); fprintf(stdout,"warranties or conditions of TITLE, NON-INFRINGEMENT,\n"); fprintf(stdout,"MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.\n"); fprintf(stdout,"\n"); fprintf(stdout,"See the COPYING file for the full Apache License, Version 2.0.\n"); return OK; } int print_kalign_header(void) { fprintf(stderr,"\n"); fprintf(stderr,"Kalign (%s)\n", KALIGN_PACKAGE_VERSION); fprintf(stderr,"\n"); fprintf(stderr,"Copyright (C) 2006-2026 Timo Lassmann\n"); fprintf(stderr,"\n"); fprintf(stderr,"Licensed under the Apache License, Version 2.0.\n"); fprintf(stderr,"See the COPYING file or http://www.apache.org/licenses/LICENSE-2.0\n"); fprintf(stderr,"\n"); fprintf(stderr,"Please cite:\n"); fprintf(stderr," Lassmann, Timo.\n"); fprintf(stderr," \"Kalign 3: multiple sequence alignment of large data sets.\"\n"); fprintf(stderr," Bioinformatics (2019) \n"); fprintf(stderr," https://doi.org/10.1093/bioinformatics/btz795\n"); fprintf(stderr,"\n"); return OK; } int main(int argc, char *argv[]) { int version = 0; int c; int showw = 0; struct parameters* param = NULL; char* in = NULL; char* in_type = NULL; char* in_refine = NULL; RUNP(param = init_param()); param->num_infiles = 0; while (1){ static struct option long_options[] ={ {"showw", 0,0,OPT_SHOWW }, {"set", required_argument,0,OPT_SET}, {"format", required_argument, 0, 'f'}, {"type", required_argument, 0, OPT_ALN_TYPE}, {"gpo", required_argument, 0, OPT_GPO}, {"gpe", required_argument, 0, OPT_GPE}, {"tgpe", required_argument, 0, OPT_TGPE}, {"refine", required_argument, 0, OPT_REFINE}, {"adaptive-budget", no_argument, 0, OPT_ADAPTIVE_BUDGET}, {"ensemble", optional_argument, 0, OPT_ENSEMBLE}, {"ensemble-seed", required_argument, 0, OPT_ENSEMBLE_SEED}, {"min-support", required_argument, 0, OPT_MIN_SUPPORT}, {"save-poar", required_argument, 0, OPT_SAVE_POAR}, {"load-poar", required_argument, 0, OPT_LOAD_POAR}, {"consistency", required_argument, 0, OPT_CONSISTENCY}, {"consistency-weight", required_argument, 0, OPT_CONSISTENCY_WEIGHT}, {"fast", no_argument, 0, OPT_FAST}, {"precise", no_argument, 0, OPT_PRECISE}, {"realign", required_argument, 0, OPT_REALIGN}, {"vsm-amax", required_argument, 0, OPT_VSM_AMAX}, {"nthreads", required_argument, 0, 'n'}, {"input", required_argument, 0, 'i'}, {"infile", required_argument, 0, 'i'}, {"in", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"outfile", required_argument, 0, 'o'}, {"out", required_argument, 0, 'o'}, {"help", no_argument,0,'h'}, {"version", no_argument,0,'v'}, {"quiet", 0, 0, 'q'}, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long_only (argc, argv,"i:o:f:n:hqvV",long_options, &option_index); /* Detect the end of the options. */ if (c == -1){ break; } switch(c) { case OPT_ALN_TYPE: in_type = optarg; break; case OPT_SHOWW: showw = 1; break; case OPT_SET: param->param_set = atoi(optarg); break; case 'f': param->format = optarg; break; case 'n': param->nthreads = atoi(optarg); break; case 'q': param->quiet = 1; break; case OPT_GPO: param->gpo = atof(optarg); break; case OPT_GPE: param->gpe = atof(optarg); break; case OPT_TGPE: param->tgpe = atof(optarg); break; case OPT_REFINE: in_refine = optarg; break; case OPT_ADAPTIVE_BUDGET: param->adaptive_budget = 1; break; case OPT_ENSEMBLE: if(optarg){ param->ensemble = atoi(optarg); }else if(optind < argc && argv[optind][0] != '-'){ param->ensemble = atoi(argv[optind]); optind++; }else{ param->ensemble = 5; } break; case OPT_ENSEMBLE_SEED: param->ensemble_seed = (uint64_t)strtoull(optarg, NULL, 10); break; case OPT_MIN_SUPPORT: param->min_support = atoi(optarg); break; case OPT_SAVE_POAR: param->save_poar = optarg; break; case OPT_LOAD_POAR: param->load_poar = optarg; break; case OPT_CONSISTENCY: param->consistency_anchors = atoi(optarg); break; case OPT_CONSISTENCY_WEIGHT: param->consistency_weight = atof(optarg); break; case OPT_FAST: param->mode = 1; break; case OPT_PRECISE: param->mode = 2; break; case OPT_REALIGN: param->realign = atoi(optarg); break; case OPT_VSM_AMAX: param->vsm_amax = atof(optarg); break; case 'h': param->help_flag = 1; break; case 'v': case 'V': version = 1; break; case 'i': in = optarg; break; case 'o': param->outfile = optarg; break; case '?': free_parameters(param); exit(1); break; default: abort (); } } if(version){ fprintf(stdout,"%s %s\n",KALIGN_PACKAGE_NAME, KALIGN_PACKAGE_VERSION); free_parameters(param); return EXIT_SUCCESS; } if(!param->dump_internal){ if(!param->quiet){ print_kalign_header(); } } if(showw){ print_kalign_warranty(); free_parameters(param); return EXIT_SUCCESS; } if(param->help_flag){ RUN(print_kalign_help(argv)); free_parameters(param); return EXIT_SUCCESS; } if(param->nthreads < 1){ RUN(print_kalign_help(argv)); LOG_MSG("Number of threads has to be >= 1."); free_parameters(param); return EXIT_FAILURE; } param->num_infiles = 0; /* Use "-" to indicate stdin (like samtools/bcftools). * NULL in the infile array signals read_file_stdin() to use stdin. */ if(in){ param->num_infiles++; } if (optind < argc){ param->num_infiles += argc-optind; } if(param->num_infiles == 0){ RUN(print_kalign_help(argv)); LOG_MSG("No input files"); free_parameters(param); return EXIT_SUCCESS; } MMALLOC(param->infile, sizeof(char*) * param->num_infiles); c = 0; if(in){ param->infile[c] = (strcmp(in, "-") == 0) ? NULL : in; c++; } if (optind < argc){ while (optind < argc){ if(strcmp(argv[optind], "-") == 0){ param->infile[c] = NULL; /* stdin */ }else{ param->infile[c] = argv[optind]; } c++; optind++; } } RUN(check_msa_format_string(param->format)); RUN(set_aln_type(in_type, ¶m->type)); RUN(set_refine_mode(in_refine, ¶m->refine)); /* Apply mode presets. Explicit params (already set above) will have * overridden their init_param() defaults, so we only fill in mode * values for fields that are still at their init_param() defaults. */ if(param->mode == 1){ /* fast: no consistency anchors (unless user explicitly set --consistency) */ if(param->consistency_anchors == 5){ /* still at default */ param->consistency_anchors = 0; } }else if(param->mode == 2){ /* precise: ensemble + realign */ if(param->ensemble == 0){ param->ensemble = 3; } if(param->realign == 0){ param->realign = 1; } } /* if(param->chaos){ */ /* if(param->chaos == 1){ */ /* ERROR_MSG("Param chaos need to be bigger than 1 (currently %d)", param->chaos); */ /* } */ /* if(param->chaos > 10){ */ /* ERROR_MSG("Param chaos bigger than 10 (currently %d)",param->chaos); */ /* } */ /* } */ RUN(run_kalign(param)); /* if(devtest){ */ /* for(c = 0; c < param->num_infiles;c++){ */ /* MFREE(param->infile[c]); */ /* } */ /* } */ free_parameters(param); return EXIT_SUCCESS; ERROR: free_parameters(param); return EXIT_FAILURE; } int run_kalign(struct parameters* param) { struct msa* msa = NULL; if(param->num_infiles == 1){ RUN(kalign_read_input(param->infile[0], &msa,param->quiet)); }else{ for(int i = 0; i < param->num_infiles;i++){ RUN(kalign_read_input(param->infile[i], &msa,param->quiet)); } } if(param->load_poar != NULL){ RUN(kalign_consensus_from_poar(msa, param->load_poar, param->min_support > 0 ? param->min_support : 2)); }else if(param->ensemble > 0){ RUN(kalign_ensemble(msa, param->nthreads, param->type, param->ensemble, param->gpo, param->gpe, param->tgpe, param->ensemble_seed, param->min_support, param->save_poar, param->refine, 0.0f, param->vsm_amax, param->realign, -1.0f, param->consistency_anchors, param->consistency_weight)); }else if(param->realign > 0){ RUN(kalign_run_realign(msa, param->nthreads, param->type, param->gpo, param->gpe, param->tgpe, param->refine, param->adaptive_budget, 0.0f, param->vsm_amax, param->realign, -1.0f, param->consistency_anchors, param->consistency_weight)); }else{ RUN(kalign_run_seeded(msa, param->nthreads, param->type, param->gpo, param->gpe, param->tgpe, param->refine, param->adaptive_budget, 0, 0.0f, 0.0f, param->vsm_amax, -1.0f, param->consistency_anchors, param->consistency_weight)); } RUN(kalign_write_msa(msa, param->outfile, param->format)); kalign_free_msa(msa); return OK; ERROR: kalign_free_msa(msa); return FAIL; } int set_aln_type(char* in, int* type ) { int t = 0; if(in){ if(strstr(in,"rna")){ t = KALIGN_TYPE_RNA; }else if(strstr(in,"dna")){ t = KALIGN_TYPE_DNA; }else if(strstr(in,"internal")){ t = KALIGN_TYPE_DNA_INTERNAL; }else if(strstr(in,"protein")){ t = KALIGN_TYPE_PROTEIN; }else if(strstr(in,"divergent")){ t = KALIGN_TYPE_PROTEIN_DIVERGENT; }else if(strstr(in,"pfasum43")){ t = KALIGN_TYPE_PROTEIN_PFASUM43; }else if(strstr(in,"pfasum60")){ t = KALIGN_TYPE_PROTEIN_PFASUM60; }else if(strstr(in,"pfasum")){ t = KALIGN_TYPE_PROTEIN_PFASUM_AUTO; }else{ ERROR_MSG("In %s not recognized.",in); } }else{ t = KALIGN_TYPE_UNDEFINED; } *type = t; return OK; ERROR: return FAIL; } int set_refine_mode(char* in, int* refine) { if(in){ if(strstr(in,"all")){ *refine = KALIGN_REFINE_ALL; }else if(strstr(in,"confident")){ *refine = KALIGN_REFINE_CONFIDENT; }else if(strstr(in,"none")){ *refine = KALIGN_REFINE_NONE; }else{ ERROR_MSG("Refine mode '%s' not recognized. Use: none, all, confident.", in); } } /* When in is NULL, keep the default from init_param() */ return OK; ERROR: return FAIL; } kalign-3.5.1/src/run_reformat.c000066400000000000000000000255631515023132300164310ustar00rootroot00000000000000#include "tldevel.h" #include "tlmisc.h" #include "kalign/kalign.h" #include "version.h" #include "parameters.h" #include #include #include #include #define OPT_RENAME 3 #define OPT_REFORMAT 4 #define OPT_SHOWW 5 #define OPT_CLEAN 15 #define OPT_UNALIGN 16 static int run_reformat(struct parameters* param); static int print_kalign_header(void); static int print_kalign_help(char * argv[]); static int print_kalign_warranty(void); int print_kalign_help(char * argv[]) { const char usage[] = " -i -o "; char* basename = NULL; RUN(tlfilename(argv[0], &basename)); fprintf(stdout,"\nUsage: %s %s\n\n",basename ,usage); fprintf(stdout,"Options:\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--format","Output format." ,"[Fasta]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--reformat","Reformat existing alignment." ,"[NA]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--changename","Change sequence names to 1 .. N." ,"[NA]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--unalign","Remove gaps and write to fasta." ,"[NA]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--clean","Perform additional checks on alignment." ,"[NA]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--version (-V/-v)","Prints version." ,"[NA]" ); fprintf(stdout,"\nExamples:\n\n"); fprintf(stdout,"Passing sequences via stdin:\n\n cat input.fa | kalign -f fasta > out.afa\n\n"); fprintf(stdout,"Combining multiple input files:\n\n kalign seqsA.fa seqsB.fa seqsC.fa -f fasta > combined.afa\n\n"); if(basename){ MFREE(basename); } return OK; ERROR: if(basename){ MFREE(basename); } return FAIL; } int print_kalign_warranty(void) { fprintf(stdout,"Disclaimer of Warranty (Apache License, Version 2.0, Section 7):\n"); fprintf(stdout,"\n"); fprintf(stdout,"Unless required by applicable law or agreed to in writing, Licensor\n"); fprintf(stdout,"provides the Work (and each Contributor provides its Contributions)\n"); fprintf(stdout,"on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\n"); fprintf(stdout,"either express or implied, including, without limitation, any\n"); fprintf(stdout,"warranties or conditions of TITLE, NON-INFRINGEMENT,\n"); fprintf(stdout,"MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.\n"); fprintf(stdout,"\n"); fprintf(stdout,"See the COPYING file for the full Apache License, Version 2.0.\n"); return OK; } int print_kalign_header(void) { fprintf(stdout,"\n"); fprintf(stdout,"Kalign (%s)\n", KALIGN_PACKAGE_VERSION); fprintf(stdout,"\n"); fprintf(stdout,"Copyright (C) 2006-2026 Timo Lassmann\n"); fprintf(stdout,"\n"); fprintf(stdout,"Licensed under the Apache License, Version 2.0.\n"); fprintf(stdout,"See the COPYING file or http://www.apache.org/licenses/LICENSE-2.0\n"); fprintf(stdout,"\n"); fprintf(stdout,"Please cite:\n"); /* fprintf(stdout," Kalign 3: multiple sequence alignment of large data sets Timo Lassmann Bioinformatics, btz795, https://doi.org/10.1093/bioinformatics/btz795 */ fprintf(stdout," Lassmann, Timo.\n"); fprintf(stdout," \"Kalign 3: multiple sequence alignment of large data sets.\"\n"); fprintf(stdout," Bioinformatics (2019) \n"); fprintf(stdout," https://doi.org/10.1093/bioinformatics/btz795\n"); fprintf(stdout,"\n"); /*fprintf(stdout," Lassmann, Timo, Oliver Frings, and Erik LL Sonnhammer.\n"); fprintf(stdout," \"Kalign2: high-performance multiple alignment of protein and\n"); fprintf(stdout," nucleotide sequences allowing external features.\"\n"); fprintf(stdout," Nucleic acids research 37.3 (2008): 858-865.\n"); fprintf(stdout,"\n"); fprintf(stdout," Lassmann, Timo, and Erik LL Sonnhammer. \"Kalign–an accurate and\n"); fprintf(stdout," fast multiple sequence alignment algorithm.\"\n BMC bioinformatics 6.1 (2005): 298.\n"); fprintf(stdout,"\n");*/ return OK; } int main(int argc, char *argv[]) { int version = 0; int c; int showw = 0; struct parameters* param = NULL; char* in = NULL; RUNP(param = init_param()); param->num_infiles = 0; while (1){ static struct option long_options[] ={ {"showw", 0,0,OPT_SHOWW }, {"format", required_argument, 0, 'f'}, {"reformat", required_argument, 0, OPT_REFORMAT}, {"changename", 0, 0, OPT_RENAME}, {"unalign", 0, 0, OPT_UNALIGN}, {"clean", 0, 0, OPT_CLEAN}, {"input", required_argument, 0, 'i'}, {"infile", required_argument, 0, 'i'}, {"in", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"outfile", required_argument, 0, 'o'}, {"out", required_argument, 0, 'o'}, {"help", no_argument,0,'h'}, {"version", no_argument,0,'v'}, {"quiet", 0, 0, 'q'}, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long_only (argc, argv,"i:o:f:n:hqvV",long_options, &option_index); /* Detect the end of the options. */ if (c == -1){ break; } switch(c) { case OPT_CLEAN: param->clean = 1; break; case OPT_UNALIGN: param->unalign = 1; break; case OPT_SHOWW: showw = 1; break; case OPT_RENAME: param->rename = 1; break; case 'f': param->format = optarg; break; case 'n': param->nthreads = atoi(optarg); break; case 'q': param->quiet = 1; break; case OPT_REFORMAT: param->format = optarg; param->reformat = 1; break; case 'h': param->help_flag = 1; break; case 'v': case 'V': version = 1; break; case 'i': in = optarg; break; case 'o': param->outfile = optarg; break; case '?': free_parameters(param); exit(1); break; default: abort (); } } if(version){ fprintf(stdout,"%s %s\n",KALIGN_PACKAGE_NAME, KALIGN_PACKAGE_VERSION); free_parameters(param); return EXIT_SUCCESS; } if(!param->dump_internal){ print_kalign_header(); } if(showw){ print_kalign_warranty(); free_parameters(param); return EXIT_SUCCESS; } if(param->help_flag){ RUN(print_kalign_help(argv)); free_parameters(param); return EXIT_SUCCESS; } param->num_infiles = 0; if (!isatty(fileno(stdin))){ param->num_infiles++; } if(in){ param->num_infiles++; } if (optind < argc){ param->num_infiles += argc-optind; } if(param->num_infiles == 0){ RUN(print_kalign_help(argv)); LOG_MSG("No input files"); free_parameters(param); return EXIT_SUCCESS; } //fprintf(stdout,"%d fgiles\n",param->num_infiles); MMALLOC(param->infile, sizeof(char*) * param->num_infiles); c = 0; if (!isatty(fileno(stdin))){ param->infile[c] = NULL; c++; } if(in){ param->infile[c] = in; c++; } if (optind < argc){ while (optind < argc){ param->infile[c] = argv[optind++]; c++; } } RUN(check_msa_format_string(param->format)); if(param->num_infiles == 0){ if (!isatty(fileno(stdin))){ LOG_MSG("Attempting stdin"); param->num_infiles =1; MMALLOC(param->infile, sizeof(char*)); param->infile[0] = NULL; }else{ LOG_MSG("No infiles"); free_parameters(param); return EXIT_SUCCESS; } } RUN(run_reformat(param)); free_parameters(param); return EXIT_SUCCESS; ERROR: free_parameters(param); return EXIT_FAILURE; } int run_reformat(struct parameters* param) { struct msa* msa = NULL; if(param->num_infiles == 1){ kalign_read_input(param->infile[0], &msa,1); }else{ for(int i = 0; i < param->num_infiles;i++){ kalign_read_input(param->infile[i], &msa,1); } } reformat_settings_msa(msa, param->rename, param->unalign); if(param->unalign){ param->format = NULL; } if(param->clean){ RUN(kalign_check_msa(msa,0)); } /* extra checks for input alignments */ /* if(param->clean){ */ /* RUN(run_extra_checks_on_msa(msa)); */ /* } */ /* if(!msa->quiet){ */ /* LOG_MSG("Detected: %d sequences.", msa->numseq); */ /* } */ /* If we just want to reformat end here */ /* if(param->out_format != FORMAT_FA && msa->aligned != ALN_STATUS_ALIGNED){ */ /* ERROR_MSG("Input sequences are not aligned - cannot write to MSA format: %s", param->format); */ /* } */ RUN(kalign_write_msa(msa, param->outfile, param->format)); kalign_free_msa(msa); return OK; ERROR: kalign_free_msa(msa); return FAIL; } kalign-3.5.1/src/version.h.in000066400000000000000000000001761515023132300160160ustar00rootroot00000000000000#cmakedefine KALIGN_PACKAGE_NAME "@CMAKE_PROJECT_NAME@" #cmakedefine KALIGN_PACKAGE_VERSION "@KALIGN_LIBRARY_VERSION_STRING@" kalign-3.5.1/tests/000077500000000000000000000000001515023132300141225ustar00rootroot00000000000000kalign-3.5.1/tests/CMakeLists.txt000066400000000000000000000056071515023132300166720ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.18) # Build library in sub-directories if (NOT TARGET kalign::kalign) add_subdirectory(${PROJECT_SOURCE_DIR}/lib build EXCLUDE_FROM_ALL) endif() # link to installed library if (NOT TARGET kalign::kalign) find_package(kalign) endif() add_executable(largebench large_benchmark.c ) target_link_libraries(largebench tldevel ${PROJECT_NAME}_static) add_executable(kalign_io_test kalign_io_test.c ) target_link_libraries(kalign_io_test kalign::kalign) add_executable(dssim dssim_test.c dssim.c ) target_link_libraries(dssim tldevel kalign::kalign) add_executable(kalign_lib_test kalign_lib_test.c ) target_link_libraries(kalign_lib_test kalign::kalign) add_executable(kalign_cmp_test kalign_cmp_test.c ) target_link_libraries(kalign_cmp_test kalign::kalign) add_executable(kaligncpp kalign_lib_testCXX.cpp) target_link_libraries(kaligncpp kalign::kalign) add_test( NAME C++Lib COMMAND kaligncpp WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB11001.tfa add_test( NAME kalign_itest_BB11001.tfa COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB11001.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB11001_EOF.msf add_test( NAME kalign_itest_BB11001_EOF.msf COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB11001_EOF.msf WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB12006.msf add_test( NAME kalign_itest_BB12006.msf COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB12006.msf WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB12006.tfa add_test( NAME kalign_itest_BB12006.tfa COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB12006.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB30014.msf add_test( NAME kalign_itest_BB30014.msf COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB30014.msf WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) #BB30014.tfa add_test( NAME kalign_itest_BB30014.tfa COMMAND kalign-bin ${CMAKE_CURRENT_SOURCE_DIR}/data/BB30014.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_executable(kalign_ensemble_test kalign_ensemble_test.c ) target_link_libraries(kalign_ensemble_test tldevel ${PROJECT_NAME}_static) add_test( NAME kalign_ensemble_test COMMAND kalign_ensemble_test ${CMAKE_CURRENT_SOURCE_DIR}/data/BB11001.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_executable(kalign_api_test kalign_api_test.c ) target_link_libraries(kalign_api_test tldevel ${PROJECT_NAME}_static) add_test( NAME kalign_api_test COMMAND kalign_api_test ${CMAKE_CURRENT_SOURCE_DIR}/data/BB11001.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_test( NAME kalign_api_test_large COMMAND kalign_api_test ${CMAKE_CURRENT_SOURCE_DIR}/data/BB30014.tfa WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_test( NAME DSSIM_takes_a_min COMMAND dssim WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) kalign-3.5.1/tests/data/000077500000000000000000000000001515023132300150335ustar00rootroot00000000000000kalign-3.5.1/tests/data/BB11001.msf000066400000000000000000000014601515023132300164110ustar00rootroot00000000000000PileUp MSF: 96 Type: P Check: 7038 .. Name: 1aab_ oo Len: 96 Check: 4681 Weight: 10.0 Name: 1j46_A oo Len: 96 Check: 1914 Weight: 10.0 Name: 1k99_A oo Len: 96 Check: 8221 Weight: 10.0 Name: 2lef_A oo Len: 96 Check: 2222 Weight: 10.0 // 1aab_ ...GKGDPKK PRGKMSSYAF FVQTSREEHK KKHPDASVNF SEFSKKCSER 1j46_A ......MQDR VKRPMNAFIV WSRDQRRKMA LENP..RMRN SEISKQLGYQ 1k99_A MKKLKKHPDF PKKPLTPYFR FFMEKRAKYA KLHP..EMSN LDLTKILSKK 2lef_A ........MH IKKPLNAFML YMKEMRANVV AEST..LKES AAINQILGRR 1aab_ WKTMSAKEKG KFEDMAKADK ARYEREMKTY IPPKGE.... ...... 1j46_A WKMLTEAEKW PFFQEAQKLQ AMHREKYPNY KYRPRRKAKM LPK... 1k99_A YKELPEKKKM KYIQDFQREK QEFERNLARF REDHPDLIQN AKK... 2lef_A WHALSREEQA KYYELARKER QLHMQLYPGW SARDNYGKKK KRKREK kalign-3.5.1/tests/data/BB11001.tfa000066400000000000000000000006001515023132300163710ustar00rootroot00000000000000>1aab_ GKGDPKKPRGKMSSYAFFVQTSREEHKKKHPDASVNFSEFSKKCSERWKT MSAKEKGKFEDMAKADKARYEREMKTYIPPKGE >1j46_A MQDRVKRPMNAFIVWSRDQRRKMALENPRMRNSEISKQLGYQWKMLTEAE KWPFFQEAQKLQAMHREKYPNYKYRPRRKAKMLPK >1k99_A MKKLKKHPDFPKKPLTPYFRFFMEKRAKYAKLHPEMSNLDLTKILSKKYK ELPEKKKMKYIQDFQREKQEFERNLARFREDHPDLIQNAKK >2lef_A MHIKKPLNAFMLYMKEMRANVVAESTLKESAAINQILGRRWHALSREEQA KYYELARKERQLHMQLYPGWSARDNYGKKKKRKREK kalign-3.5.1/tests/data/BB11001_EOF.msf000066400000000000000000000014511515023132300171020ustar00rootroot00000000000000PileUp MSF: 96 Type: P Check: 7038 .. Name: 1aab_ oo Len: 96 Check: 4681 Weight: 10.0 Name: 1j46_A oo Len: 96 Check: 1914 Weight: 10.0 Name: 1k99_A oo Len: 96 Check: 8221 Weight: 10.0 Name: 2lef_A oo Len: 96 Check: 2222 Weight: 10.0 // 1aab_ ...GKGDPKK PRGKMSSYAF FVQTSREEHK KKHPDASVNF SEFSKKCSER 1j46_A ......MQDR VKRPMNAFIV WSRDQRRKMA LENP..RMRN SEISKQLGYQ 1k99_A MKKLKKHPDF PKKPLTPYFR FFMEKRAKYA KLHP..EMSN LDLTKILSKK 2lef_A ........MH IKKPLNAFML YMKEMRANVV AEST..LKES AAINQILGRR 1aab_ WKTMSAKEKG KFEDMAKADK ARYEREMKTY IPPKGE.... ...... 1j46_A WKMLTEAEKW PFFQEAQKLQ AMHREKYPNY KYRPRRKAKM LPK... 1k99_A YKELPEKKKM KYIQDFQREK QEFERNLARF REDHPDLIQN AKK... 2lef_A WHALSREEQA KYYELARKER QLHMQLYPGW SARDNYGKKK KRKREKkalign-3.5.1/tests/data/BB12006.msf000066400000000000000000000031731515023132300164220ustar00rootroot00000000000000PileUp MSF: 245 Type: P Check: 59 .. Name: 1al2_ad oo Len: 245 Check: 2091 Weight: 10.0 Name: 1bbt_ac oo Len: 245 Check: 8510 Weight: 10.0 Name: 1bev_ac oo Len: 245 Check: 6172 Weight: 10.0 Name: 1mec_ac oo Len: 245 Check: 3286 Weight: 10.0 // 1al2_ad .GLPVMNTPG SNQYLTADNF QSPCALPEFD VTPPIDIPGE VKNMMELAEI 1bbt_ac GIFPVACSDG YGGLVTTDPK TADPVYGKVF NPPRNQLPGR FTNLLDVAEA 1bev_ac .GLPTKPGPG SYQFMTTDED CSPCILPDFQ PTPEIFIPGK VNNLLEIAQV 1mec_ac SPIPVTIREH AGTWYSTLPD STVPIYGKTP VAPANYMVGE YKDFLEIAQI 1al2_ad D.TMIPFDLS ATKKNTMEMY RVRLSDKPHT DDPILCLSLS PASDPRLSHT 1bbt_ac CPTFLR.... ......FEGG VPYVTTKTDS DRVLAQFDMS LAA.KHMSNT 1bev_ac E.SILEANN. REGVEGVERY VIPVSVQDAL DAQIYALRLE LGGSGPLSSS 1mec_ac P.TFIG.... .NKMPNAVPY IEASNTAVK. TQPLAVYQVT LSC.SCLANT 1al2_ad MLGEILNYYT HWAGSLKFTF LFCGSMMATG KLLVSYAPPG ADPPKKRKEA 1bbt_ac FLAGLAQYYT QYSGTINLHF MFTGPTDAKA RYMVAYAPPG MEPPKTPEAA 1bev_ac LLGTLAKHYT QWSGSVEITC MFTGTFMTTG KVLLAYTPPG GDMPRNREEA 1mec_ac FLAALSRNFA QYRGSLVYTF VFTGTAMMKG KFLIAYTPPG AGKPTSRDQA 1al2_ad MLGTHVIWDI GLQSSCTMVV PWISNTTYRQ TIDD.....S FTEGGYISVF 1bbt_ac AHCIHAEWDT GLNSKFTFSI PYLSAADYTY TASDVAE..T TNVQGWVCLF 1bev_ac MLGTHVIWDF GLQSSITLVI PWISASHFRG VSNDDVLNYQ YYAAGHVTIW 1mec_ac MQATYAIWDL GLNSSYSFTV PFISPTHFRM VGTDQAN..I TNVDGWVTVW 1al2_ad YQTRIVVPLS TPREMDILGF VSACNDFSVR LLRDTTHIEQ KA... 1bbt_ac QITHGK.... .ADGDALVVL ASAGKDFELR LPVDARAE.. ..... 1bev_ac YQTNMVIPPG FPNTAGIIMM IAAQPNFSFR IQKDREDMTQ TAILQ 1mec_ac QLTPLTYPPG CPTSAKILTM VSAGKDFSLK MPISPAPWSP Q.... kalign-3.5.1/tests/data/BB12006.tfa000066400000000000000000000017301515023132300164040ustar00rootroot00000000000000>1al2_ad GLPVMNTPGSNQYLTADNFQSPCALPEFDVTPPIDIPGEVKNMMELAEID TMIPFDLSATKKNTMEMYRVRLSDKPHTDDPILCLSLSPASDPRLSHTML GEILNYYTHWAGSLKFTFLFCGSMMATGKLLVSYAPPGADPPKKRKEAML GTHVIWDIGLQSSCTMVVPWISNTTYRQTIDDSFTEGGYISVFYQTRIVV PLSTPREMDILGFVSACNDFSVRLLRDTTHIEQKA >1bbt_ac GIFPVACSDGYGGLVTTDPKTADPVYGKVFNPPRNQLPGRFTNLLDVAEA CPTFLRFEGGVPYVTTKTDSDRVLAQFDMSLAAKHMSNTFLAGLAQYYTQ YSGTINLHFMFTGPTDAKARYMVAYAPPGMEPPKTPEAAAHCIHAEWDTG LNSKFTFSIPYLSAADYTYTASDVAETTNVQGWVCLFQITHGKADGDALV VLASAGKDFELRLPVDARAE >1bev_ac GLPTKPGPGSYQFMTTDEDCSPCILPDFQPTPEIFIPGKVNNLLEIAQVE SILEANNREGVEGVERYVIPVSVQDALDAQIYALRLELGGSGPLSSSLLG TLAKHYTQWSGSVEITCMFTGTFMTTGKVLLAYTPPGGDMPRNREEAMLG THVIWDFGLQSSITLVIPWISASHFRGVSNDDVLNYQYYAAGHVTIWYQT NMVIPPGFPNTAGIIMMIAAQPNFSFRIQKDREDMTQTAILQ >1mec_ac SPIPVTIREHAGTWYSTLPDSTVPIYGKTPVAPANYMVGEYKDFLEIAQI PTFIGNKMPNAVPYIEASNTAVKTQPLAVYQVTLSCSCLANTFLAALSRN FAQYRGSLVYTFVFTGTAMMKGKFLIAYTPPGAGKPTSRDQAMQATYAIW DLGLNSSYSFTVPFISPTHFRMVGTDQANITNVDGWVTVWQLTPLTYPPG CPTSAKILTMVSAGKDFSLKMPISPAPWSPQ kalign-3.5.1/tests/data/BB30014.msf000066400000000000000000000544151515023132300164260ustar00rootroot00000000000000PileUp MSF: 307 Type: P Check: 2883 .. Name: 1ags_A oo Len: 307 Check: 2542 Weight: 10.0 Name: GTA1_BOVIN oo Len: 307 Check: 1045 Weight: 10.0 Name: GTA1_MOUSE oo Len: 307 Check: 2301 Weight: 10.0 Name: GTC1_RAT oo Len: 307 Check: 6389 Weight: 10.0 Name: GTC_RABIT oo Len: 307 Check: 2138 Weight: 10.0 Name: GTA1_RABIT oo Len: 307 Check: 6143 Weight: 10.0 Name: GTA1_CAVPO oo Len: 307 Check: 2879 Weight: 10.0 Name: GTA3_RAT oo Len: 307 Check: 3064 Weight: 10.0 Name: GTA3_CHICK oo Len: 307 Check: 6404 Weight: 10.0 Name: GTA2_CHICK oo Len: 307 Check: 3444 Weight: 10.0 Name: GTA1_CHICK oo Len: 307 Check: 4246 Weight: 10.0 Name: GTA1_ANTST oo Len: 307 Check: 2040 Weight: 10.0 Name: GTA4_HUMAN oo Len: 307 Check: 7381 Weight: 10.0 Name: 1oe7_A oo Len: 307 Check: 3128 Weight: 10.0 Name: GTP1_BUFBU oo Len: 307 Check: 7779 Weight: 10.0 Name: GT28_SCHJA oo Len: 307 Check: 7618 Weight: 10.0 Name: GTS1_ASCSU oo Len: 307 Check: 6708 Weight: 10.0 Name: GTS9_CAEEL oo Len: 307 Check: 3425 Weight: 10.0 Name: GTS3_CAEEL oo Len: 307 Check: 9066 Weight: 10.0 Name: GTS5_CAEEL oo Len: 307 Check: 8451 Weight: 10.0 Name: GTS6_CAEEL oo Len: 307 Check: 245 Weight: 10.0 Name: GTS4_CAEEL oo Len: 307 Check: 1081 Weight: 10.0 Name: GTS2_CAEEL oo Len: 307 Check: 4110 Weight: 10.0 Name: GTS1_CAEEL oo Len: 307 Check: 1820 Weight: 10.0 Name: GTS_ANOGA oo Len: 307 Check: 660 Weight: 10.0 Name: GTS2_MANSE oo Len: 307 Check: 5117 Weight: 10.0 Name: GT1_ONCVO oo Len: 307 Check: 728 Weight: 10.0 Name: SC11_OMMSL oo Len: 307 Check: 7004 Weight: 10.0 Name: SC2_OCTDO oo Len: 307 Check: 9708 Weight: 10.0 Name: 1aw9_ oo Len: 307 Check: 4565 Weight: 10.0 Name: GTH3_ARATH oo Len: 307 Check: 2928 Weight: 10.0 Name: GTH7_ARATH oo Len: 307 Check: 2013 Weight: 10.0 Name: GTH6_ARATH oo Len: 307 Check: 8296 Weight: 10.0 Name: GTH_SILCU oo Len: 307 Check: 9759 Weight: 10.0 Name: GTH1_ARATH oo Len: 307 Check: 9980 Weight: 10.0 Name: GTH4_ARATH oo Len: 307 Check: 9350 Weight: 10.0 Name: GTH4_MAIZE oo Len: 307 Check: 3503 Weight: 10.0 Name: GTH1_MAIZE oo Len: 307 Check: 2572 Weight: 10.0 Name: GTH1_WHEAT oo Len: 307 Check: 9368 Weight: 10.0 Name: GTH5_ARATH oo Len: 307 Check: 9671 Weight: 10.0 Name: 1eem_A oo Len: 307 Check: 6005 Weight: 10.0 Name: GTO1_MOUSE oo Len: 307 Check: 911 Weight: 10.0 Name: GTO2_HUMAN oo Len: 307 Check: 9751 Weight: 10.0 Name: YKJ3_CAEEL oo Len: 307 Check: 7547 Weight: 10.0 // 1ags_A .......... .......... .......... ....AEKPKL H.YFNARGRM GTA1_BOVIN .......... .......... .......... ....AGKPTL H.YFNGRGRM GTA1_MOUSE .......... .......... .......... ....AGKPVL H.YFNARGRM GTC1_RAT .......... .......... .......... ....PGKPVL H.YFDGRGRM GTC_RABIT .......... .......... .......... ....AGKPKL H.YFNARGRM GTA1_RABIT .......... .......... .......... ...MARKPLL H.YFNGRGRM GTA1_CAVPO .......... .......... .......... ....SGKPVL H.YFNVQGRM GTA3_RAT .......... .......... .......... ...MEVKPKL Y.YFQGRGRM GTA3_CHICK .......... .......... .......... ....AAKPVL Y.YFNGRGKM GTA2_CHICK .......... .......... .......... ...MAGKPKL H.YTRGRGKM GTA1_CHICK .......... .......... .......... ...MSGKPVL H.YANTRGRM GTA1_ANTST .......... .......... .......... ...MAGEQNI K.YFNIKGRM GTA4_HUMAN .......... .......... .......... ...MAARPKL H.YPNGRGRM 1oe7_A .......... .......... .......... .....DHIKV I.YFNGRGRA GTP1_BUFBU .......... .......... .......... .....PEYTI I.YFNARGRC GT28_SCHJA .......... .......... .......... .......VKL I.YFNGRGRA GTS1_ASCSU .......... .......... .......... .....PQYKL T.YFDIRGLG GTS9_CAEEL .......... .......... .......... ....MVSYKL I.YFQSRGNG GTS3_CAEEL .......... .......... .......... ....MVHYKL T.YFNARGLA GTS5_CAEEL .......... .......... .......... ....MVSYKL T.YFNGRGAG GTS6_CAEEL .......... .......... .......... ....MVHYKL V.YFPLRARA GTS4_CAEEL .......... .......... .......... ....MPNYKL L.YFDARALA GTS2_CAEEL .......... .......... .......... ....MVHYKL M.CFDVRGLG GTS1_CAEEL .......... .......... .......... ....MPHFKF Y.YFDVRGRG GTS_ANOGA .......... .........L SSSSISRSSL KCNIMPDYKV Y.YFNVKALG GTS2_MANSE .......... .......... .......... ....MPKVVF H.YFGAKGWA GT1_ONCVO FITMNFVVEA ASSNANQAIT SENSIKPKGK LQPQMEKYTL T.YFNGRGRA SC11_OMMSL .......... .......... .......... ....MPSYTL Y.YFNGRGRA SC2_OCTDO .......... .......... .......... ....MPSYTL N.YFNHRGRA 1aw9_ .......... .......... .......... .....APLKL Y.GMPLSPNV GTH3_ARATH .......... .......... .......... ......VLTI Y..APLFASS GTH7_ARATH .......... .......... .......... .....MVVKV Y.GQIKAANP GTH6_ARATH .......... .......... .......... ....MASIKV H.GVPMSTAT GTH_SILCU .......... .......... .......... ......TIKV H.GNPRSTAT GTH1_ARATH .......... .......... .......... ....MAGIKV F.GHPASTAT GTH4_ARATH .......... .......... .......... .....AGIKV F.GHPASIAT GTH4_MAIZE .......... .......... .......... ...ATPAVKV Y.GWAISPFV GTH1_MAIZE .......... .......... .......... .....APMKL Y.GAVMSWNL GTH1_WHEAT .......... .......... .......... ....MSPVKV F.GHPMLTNV GTH5_ARATH .......... .......... .......... ....MVTVKL Y.GMAYSTCT 1eem_A .......... ........SA RSLGKGSAPP GPVPEGSIRI Y.SMRFCPFA GTO1_MOUSE .......... ....MSGESS RSLGKGSAPP GPVPEGQIRV Y.SMRFCPFA GTO2_HUMAN .......... ....MSGDAT RTLGKGSQPP GPVPEGLIRI Y.SMRFCPYS YKJ3_CAEEL .......... .......... MTVLAGVNSK IVKNGCWNHS YLQHAILPVA 1ags_A ESTRWLLAAA GVEFEEKFIK SAEDL...DK LRNDGYLMFQ QVPMVEIDG. GTA1_BOVIN ECIRWLLAAA GVEFEEKFIE KPEDL...DK LKNDGSLMFQ QVPMVEIDG. GTA1_MOUSE ECIRWLLAAA GVEFEEKFIQ SPEDL...EK LKKDGNLMFD QVPMVEIDG. GTC1_RAT EPIRWLLAAA GVEFEEQFLK TRDDL...AR LRNDGSLMFQ QVPMVEIDG. GTC_RABIT ESIRWLLTAA GVEFEEKCMK TREDL...EK LRKDGVLMFQ QVPMVEIDG. GTA1_RABIT ESIRWLLAAA GEEFDEKFME TAEDL...DK LRNDGSLMYQ QVPMVEIDG. GTA1_CAVPO ESIRWLLAAA GVEFEEKLIM CQEDL...DK LKNDGLLMFQ QVPMVEMDG. GTA3_RAT ESIRWLLATA GVEFEEEFLE TREQY...EK LQKDGCLLFG QVPLVEIDG. GTA3_CHICK ESIRWLLAAA GVEFEEVFLE TREQY...EK LLQSGILMFQ QVPMVEIDG. GTA2_CHICK ESIRWLLAAA GVEFEEEFIE KKEDL...EK LRNDGSLLFQ QVPMVEIDG. GTA1_CHICK ESVRWLLAAA GVEFEEKFLE KKEDL...QK LKSDGSLLFQ QVPMVEIDG. GTA1_ANTST EAIRWLLAVA GVEFEEKFFE TKEQL...QK LKET.VLLFQ QVPMVEIDG. GTA4_HUMAN ESVRWVLAAA GVEFDEEFLE TKEQL...YK LQDGNHLLFQ QVPMVEIDG. 1oe7_A ESIRMTLVAA GVNYEDERIS FQDW....PK IKP..TIPGG RLPAVKITDN GTP1_BUFBU EAMRMLMADQ GAQWKEEVVT SDDWQ..KGD LKK..AAVYG QLPGFKDGD. GT28_SCHJA EPIRMILVAA GVEFEDERIE FQDW....PK IKP..TIPGG RLPIVKITDK GTS1_ASCSU EGARLIFHQA GVKFEDNRLK REDW....PA LKP..KTPFG QLPLLEVDG. GTS9_CAEEL EIARQVFAFA GQEFIDERIS KEQW....AE IKN..MTPFG QVPVLEVDG. GTS3_CAEEL EISRQLFHMA GVEFEDERIN EEKF....SQ LKP..TFPSG QVPILCIDG. GTS5_CAEEL EVSRQIFAYA GQQYEDNRVT QEQW....PA LKETCAAPFG QLPFLEVDG. GTS6_CAEEL EIARQIFAYA GQDYSEENLS FEQW....PA RKN..NTPFG QLPILEVDG. GTS4_CAEEL EPIRIMFAML NVPYEDYRVS VEEW....SK LKP..TTPFG QLPILQVDG. GTS2_CAEEL EVIRQLFYLG DVSFEDFRVS REEF....KS LKS..NLPSG QLPVLEIDG. GTS1_CAEEL EAIRLLFHLA DEKFDDERFG MEQW....GV LKS..EMPLG QVPVLEIDG. GTS_ANOGA EPLRFLLSYG NLPFDDVRIT REEW....PA LKP..TMPMR QMPVLEVDG. GTS2_MANSE RP.TMLLAYG GQEFEDHRVE YEQW....PE FKP..NTPFG QMPVLEIDG. GT1_ONCVO EVIRLLFALA NVSYEDNRIT RDEW....KY LKP..RTPFG HVPMLNVSG. SC11_OMMSL EICRMLFAVA SVQYQDKRIE LAEW....TQ FKT..KMPCH MLPILEIDT. SC2_OCTDO EICRMLFAAA GVQYNDRRIE TSEW....SN MRS..KMPCS MMPMLDIDN. 1aw9_ VRVATVLNEK GLDFEIVPVD LTTGAHKQPD FLA..LNPFG QIPALVDGD. GTH3_ARATH KRAVVTLVEK GVSFETVNVD LMKGEQRQPE YLA..IQPFG KIPVLVDGD. GTH7_ARATH QRVLLCFLEK DIEFEVIHVD LDKLEQKKPQ HLL..RQPFG QVPAIEDGY. GTH6_ARATH MRVLATLYEK DLQFELIPVD MRAGAHKQEA HLA..LNPFG QIPALEDGD. GTH_SILCU QRVLVALYEK HLEFEFVPID MGAGGHKQPS YLA..LNPFG QVPALEDGE. GTH1_ARATH RRVLIALHEK NVDFEFVHVE LKDGEHKKEP FIL..RNPFG KVPAFEDGD. GTH4_ARATH RRVLIALHEK NLDFELVHVE LKDGEHKKEP FLS..RNPFG QVPAFEDGD. GTH4_MAIZE SRALLALEEA GVDYELVPMS RQDGDHRRPE HLA..RNPFG KVPVLEDGD. GTH1_MAIZE TRCATALEEA GSDYEIVPIN FATAEHKSPE HLV..RNPFG QVPALQDGD. GTH1_WHEAT ARVLLFLEEV GAEYELVPMD FVAGEHKRPQ HVQ..LNPFA KMPGFQDGD. GTH5_ARATH KRVYTTAKEI GVDVKIVPVD LMKGEHKEPA YLDN.YHPFG VIPVLEDEDG 1eem_A ERTRLVLKAK GIRHEVININ LKNK...PEW FFK..KNPFG LVPVLENSQG GTO1_MOUSE QRTLMVLKAK GIRHEVININ LKNK...PEW FFE..KNPLG LVPVLENSQG GTO2_HUMAN HRTRLVLKAK DIRHEVVNIN LRNK...PEW YYT..KHPFG HIPVLETSQC YKJ3_CAEEL QRALIYASVK NIPSDVINVH LQEKP...DW YFS..KHYKG QVPTLEHDEG 1ags_A ....MKLVQT RAILNYIASK YN.......L YRKDI...KE KALIDMYIEG GTA1_BOVIN ....MKLVQT RAILNYIATK YN.......L YGKDM...KE RALIDMYSEG GTA1_MOUSE ....MKLAQT RAILNYIATK YD.......L YGKDM...KE RALIDMYSEG GTC1_RAT ....MKLVQT RAILNYIATK YN.......L YGKDM...KE RALIDMYAEG GTC_RABIT ....MKLVQT RAIFNYIADK HN.......L YGKDI...KE RALIDMYTEG GTA1_RABIT ....MKLVQT RAILNYVANK HN.......L YGKDM...KE RALIDMYTEG GTA1_CAVPO ....MKMVQS RAILNYIATK YN.......L YGKDT...KE RLLIDMYTEG GTA3_RAT ....MLLTQT RAILSYLAAK YN.......L YGKDL...KE RVRIDMYADG GTA3_CHICK ....MKLVQT RAILNYIAGK YN.......L YGKDL...KE RALIDMYVGG GTA2_CHICK ....MKMVQS RAILCYIAGK YN.......L YGKDL...KE RAWIDMYVEG GTA1_CHICK ....MKMVQT RAILNYIAGK YN.......L YGKDL...KE RALIDMYVEG GTA1_ANTST ....MKLVQT RAILHYIAEK YN.......L LGKDM...KE HAQIIMYSEG GTA4_HUMAN ....MKLVQT RSILHYIADK HN.......L FGKNL...KE RTLIDMYVEG 1oe7_A HGHVKWMVES LAIARYMAKK HH.......M MGGTE...EE YYNVEKLIGQ GTP1_BUFBU ....FTLYQS NAMLRLLARN HD.......L YGKNP...RE ASLIDMVNDG GT28_SCHJA RGDVKTMSES LAIARFIARK HN.......M MGDTD...DE YYIIEKMIGQ GTS1_ASCSU ....EVLAQS AAIYRYLGRQ FG.......L AGKTP...ME EAQVDSIFDQ GTS9_CAEEL ....RQLAQS ITIVRYLSKQ FG.......I SGKSS...WE EAQVDALGDQ GTS3_CAEEL ....AQFSQS TAIARYLARK FG.......F VGQTA...EE ELQADEVVDT GTS5_CAEEL ....KKLAQS HAIARFLARE FK.......L NGKTA...WE EAQVNSLADQ GTS6_CAEEL ....KPLGQS YAIARYLARE FG.......I AGQND...TE AAEVDAIADQ GTS4_CAEEL ....EQFGQS MSITRYLARK FG.......L AGKTA...EE EAYADSIVDQ GTS2_CAEEL ....VMISQS ASIGRFLARQ YG.......Y SGKTP...TE EMQVDSIIDL GTS1_CAEEL ....VKISQT TAIARYLGHQ FH.......R AGTNA...VD CARLDMIAEV GTS_ANOGA ....KRVHQS LAMCRYVAKQ IN.......L AGDNP...LE ALQIDAIVDT GTS2_MANSE ....KKYAQS LAISRYLGRK YG.......L AGNDI...EE DFEIDQIVDF GT1_ONCVO ....NVLGES HAIELLLGGR FG.......L LGTND...WE EAKIMAVVLN SC11_OMMSL ...ETQVPQS MAISRYLARE FG.......F YGKNN...MD MFKVDCLCDS SC2_OCTDO ...RHQIPQT MAIARYLARE FG.......F HGKNN...ME MARVEYISDC 1aw9_ ....EVLFES RAINRYIASK YASEG..TDL LPATA...SA AKL.EVWLEV GTH3_ARATH ....YKIFES RAIMRYIAEK YRSQG..PDL LGKTI...EE RGQVEQWLDV GTH7_ARATH ....LKLFES RAIARYYATK YADQG..TDL LGKTL...EG RAIVDQWVEV GTH6_ARATH ....LTLFES RAITQYLAEE YSEKG..EKL ISQDCK..KV KATTNVWLQV GTH_SILCU ....IKLFES RAITKYLAYT HDHQNEGTSL IHKEK...HE MAAQLVWEEV GTH1_ARATH ....FKIFES RAITQYIAHE FSDKG..NNL LSTG....KD MAIIAMGIEI GTH4_ARATH ....LKLFES RAITQYIAHR YENQGT..NL LQTDSKNISQ YAIMAIGMQV GTH4_MAIZE ....LTLFES RAIARHVLRK HKP.....EL LGGGRL..EQ TAMVDVWLEV GTH1_MAIZE ....LYLFES RAICKYAARK NKP.....EL LREGNL..EE AAMVDVWIEV GTH1_WHEAT ....LVLFES RAIAKYILRK YGGTAG.LDL LGENSGI.EE LAMVDVWTEV GTH5_ARATH ....TKIYES RAISRYLVAK YGKGS...SL LPSPSDP.KA YGLFEQAASV 1eem_A ....QLIYES AITCEYLDEA YPGKK....L LPDDP...YE KACQKMILEL GTO1_MOUSE ....HLVTES VITCEYLDEA YPEK....KL FPDDP...YK KARQKMTLES GTO2_HUMAN ....QLIYES VIACEYLDDA YPGR....KL FPYDP...YE RARQKMLLEL YKJ3_CAEEL K...KHVIES AVIPEYLDDI YPET....RI LPTDP...YE KVQQKLLLDR 1ags_A .IADLGEMIL LLPFTQ.... ......PEEQ DAKLALIKEK IKNRYFPAFE GTA1_BOVIN .VADLGEMIM HFPLCP.... ......PAEK DAKLTLIREK TTNRYLPAFE GTA1_MOUSE .ILDLTEMIG QLVLCP.... ......PDQR EAKTALAKDR TKNRYLPAFE GTC1_RAT .VADLDEIVL HYPYIP.... ......PGEK EASLAKIKDK ARNRYFPAFE GTC_RABIT .IVDLNELIL TRPFLP.... ......PEEQ EAKLAQIKDK AKNRYFPAFE GTA1_RABIT .VADLYELVL LLPLCP.... ......PEQK DAKVDFIKEK IRTRYFPAFE GTA1_CAVPO .MTDLYELFF KVILAP.... ......PEEK DAAKSLIKDR AKNRFLPAFE GTA3_RAT .TQDLMMMII GAPFKA.... ......PQEK EESLALAVKR AKNRYFPVFE GTA3_CHICK .TDDLMGFLL SFPFLS.... ......AEDK VKQCAFVVEK ATSRYFPAYE GTA2_CHICK .TTDLMGMIM ALPFQA.... ......ADVK EKNIALITER ATTRYFPVYE GTA1_CHICK .LADLYELIM MNVVQP.... ......ADKK EEHLANALDK AANRYFPVFE GTA1_ANTST .TMDLMELIM IYPFLK.... ......GEEK KQRLVEIANK AKGRYFPAFE GTA4_HUMAN .TLDLLELLI MHPFLK.... ......PDDQ QKEVVNMAQK AIIRYFPVFE 1oe7_A .AEDLEHEYY KTLMKP.... ......EEEK QKIIKEILNG KVPVLLDIIC GTP1_BUFBU .VEDLRLKYL KMIYQN.... .......... YENGKDDYVK ALPTNLGHFE GT28_SCHJA .VEDVESEYH KTLIKP.... ......PEEK EKISKEILNG KVPILLQAIC GTS1_ASCSU .FKDFMAELR PCFRVLAGFE ......EGDK EKVLKEVAVP ARDKHLPLLE GTS9_CAEEL .FKDYRVEAR PFFRAKMGFS ......DGDV DQLYKDLFVP AFNKMYSIFT GTS3_CAEEL .FKDFIESFR KFVIAVLSGE S.....EEIL KNIREEVIKP AVKTYTAYLK GTS5_CAEEL .YKDYSSEAR PYFYAVMGFG ......PGDV ETLKKDIFLP AFEKFYGFLV GTS6_CAEEL .FKDYLNDVS PYLTVLAGFK ......PGDK DQLRTDVFVP AFKKNFEFFE GTS4_CAEEL .YRDFIFFFR QFTSSVFYGS D.....ADHI NKVRFEVVEP ARDDFLAIIN GTS2_CAEEL .FKDFMLTFR QFFFAVIHGY P.....EYEK ERMKRDIVKP AIKNYFIALN GTS1_CAEEL .IQEFMSSSG MGKFSRVLLG M....IQANK EQFFKENVLP DVEKYAPIVE GTS_ANOGA .INDFRLKIA IVAYEPDD.. .......MVK EKKMVTLNNE VIPFYLTKLN GTS2_MANSE .VNDIRASAA SVEYEQDAAN .........K EVKHEENMKN KYPFQLNKLS GT1_ONCVO .IDELFQKLI PWTHEKNTTK .........K AELFRNLSES DVMPFLGRYE SC11_OMMSL .LFELFNDYM AVYNEKDAA. .........K KTELQKRFQN TCLRVLPYME SC2_OCTDO .FYDILDDYL RMYQDDNCRM MFQRSGDRNG SSEKRTRYQE TLRRILPFME 1aw9_ ESHHFYPNAS PLVFQLLVRP LLG....GAP DAAVVDKHAE QLAKVLDVYE GTH3_ARATH EATSYHPPLL ALTLNIVFAP LMG....FPA DEKVIKESEE KLAEVLDVYE GTH7_ARATH ENNYFYAVAL PLVMNVVFKP KSG....KPC DVALVEELKV KFDKVLDVYE GTH6_ARATH EGQQFDPNAS KLAFERVFKG MFG....MTT DPAAVQELEG KLQKVLDVYE GTH_SILCU EAHQFDPVAS KLAWELVFKG IFG....MQT DTTVVEENEA KLAKVLDVYE GTH1_ARATH ESHEFDPVGS KLVWEQVLKP LYG....MTT DKTVVEEEEA KLAKVLDVYE GTH4_ARATH EDHQFDPVAS KLAFEQIFKS IYG....LTT DEAVVAEEEA KLAKVLDVYE GTH4_MAIZE EAHQLSPPAI AIVVECVFAP FLG....RER NQAVVDENVE KLKKVLEVYE GTH1_MAIZE EANQYTAALN PILFQVLISP MLG....GTT DQKVVDENLE KLKKVLEVYE GTH1_WHEAT EAQQYYPAIS PVVFECIIIP FIIPGGGAAP NQTVVDESLE RLRGVLGIYE GTH5_ARATH EYSSFDPPAS SLAYERVFAG MRG....LKT NEELAKKYVD TLNAKMDGYE 1eem_A F.SKVPSLVG SFIRS..... .........Q NKEDYAGLKE EFRKEFTKLE GTO1_MOUSE F.SKVPPLIA SFVRS..... .........K RKEDSPNLRE ALENEFKKLE GTO2_HUMAN F.CKVPHLTK ECLVA..... ........LR CGRECTNLKA ALRQEFSNLE YKJ3_CAEEL ISGQVSPAFY GVVQA..... ........VK NPDLREEKFA DIKKAYDNAE 1ags_A KVLKSHG..Q DYLVGNKL.S RADIHLVELL YYVEELDS.. .........S GTA1_BOVIN NVLKSHG..Q DYLVGNKL.S RADIHLVELL YYVEELDP.. .........S GTA1_MOUSE KVLKSHG..Q DYLVGNRL.T RVDIHLLEVL LYVEEFDA.. .........S GTC1_RAT KVLKSHG..Q DYLVGNRL.S RADVYLVQVL YHVEELDP.. .........S GTC_RABIT KVLKSHG..Q DYLVGNKL.S KADILLVELL YNVEELNP.. .........G GTA1_RABIT KVLKSHG..Q DYLVGNRL.S KADILLVELL YNVEELDP.. .........S GTA1_CAVPO KVLKSHG..Q GYLVGNKL.S KADILLTELL YMVEEFDA.. .........S GTA3_RAT KILKDHG..E AFLVGNQL.S WADIQLLEAI LMVEEVSA.. .........P GTA3_CHICK KVLKDHG..Q DFLVGNRL.S WADIHLLEAI LMVEEKKS.. .........D GTA2_CHICK KALKDHG..Q DYLVGNKL.S WADIHLLEAI LMTEELKS.. .........D GTA1_CHICK KVLKDHG..H DFLVGNKL.S RADVHLLETI LAVEESKP.. .........D GTA1_ANTST NVLKTHG..Q NFLVGNQL.S MADVQLFEAI LMVEEKVP.. .........D GTA4_HUMAN KILRGHG..Q SFLVGNQL.S LADVILLQTI LALEEKIP.. .........N 1oe7_A ESLKAST..G KLAVGDKV.T LADLVLIAVI DHVTDLDK.. ........EF GTP1_BUFBU RLLASNNEGK GFVVGAHI.S FADYNLVDLL HNHLVLAP.. .........D GT28_SCHJA ETLKEST..G NLTVGDKV.T LADVVLIASI DHITDLDK.. ........EF GTS1_ASCSU KFLAKSG..S EYMVGKSV.T WADLVITDSL ASWESLIP.. .........D GTS9_CAEEL ESLKSSG..S GFLVGDSL.T WMDLAIAQHS ADLLEADG.. .........K GTS3_CAEEL AILEKSS..S GYLVGNEL.T WADLVIADNL TTLINAEL.. .........L GTS5_CAEEL NFLKASG..S GFLVGDSL.T WIDLAIAQHS ADLIAKGG.. .......... GTS6_CAEEL NILASNHS.. GFFVGNSL.T WVDLLISQHV QDILDKDL.. .........A GTS4_CAEEL KFLAKSK..S GFLVGDSL.T WADIVIADNL TSLLKNGF.. .........L GTS2_CAEEL KILLRSK..S GFLVGDDL.T WADLQIADNL STLINIRL.. .......... GTS1_CAEEL KFLLENGN.N GLLLGDRE.T WVDVFAAESF SKLIDYGSP. .........D GTS_ANOGA VIAKENN... GHLVLGKP.T WADVYFAGIL DYLNYLTKT. .........N GTS2_MANSE EIITKNN... GFLALGRL.T WADFVFVGMF DYLKKMLR.. .......MPD GT1_ONCVO KFLKEST..T GHIVGNKV.S VADLTVFNML MTLDDEVK.. .......... SC11_OMMSL KTLEANKGGA GWFIGDQI.L LCDMMTHAAL ENPIQENAN. .......... SC2_OCTDO RTLEMYKSGG QFFMGDQM.T MADMMCYCAL ENPIMEES.. .........S 1aw9_ AHLARN.... KYLAGDEF.T LADANHASYL LYLSKTPK.. ........AG GTH3_ARATH AQLSKNE... .YLAGDFV.S LADLAHLPFT EYLVGPIG.. .......KAH GTH7_ARATH NRLATNR... .YLGGDEF.T LADLSHMPGM RYIMNETS.. .......LSG GTH6_ARATH ARLAKSE... .FLAGDSF.T LADLHHLPAI HYLLGTDS.. ........KV GTH_SILCU ARLTESEY.. .LGANDSF.T LVDLHHLPLL GYLMGTQV.. ........KK GTH1_ARATH HRLGESK... .YLASDHF.T LVDLHTIPVI QYLLGTPT.. ........KK GTH4_ARATH ARLKEFK... .YLAGETF.T LTDLHHIPAI QYLLGTPT.. ........KK GTH4_MAIZE ARLATCT... .YLAGDFL.S LADLSPFTIM HCLMATEY.. ........AA GTH1_MAIZE ARLTKCK... .YLAGDFL.S LADLNHVSVT LCLFATPY.. ........AS GTH1_WHEAT ARLEKSR... .YLAGDSI.T FADLNHIPFT FYFMTTPY.. ........AK GTH5_ARATH RILSKQK... .YLAGNDF.T LADLFHLPYG AMVAQLEP.. .........T 1eem_A EVLTNKK..T TFFGGNSI.S MIDYLIWPWF ERLEAMKLN. .........E GTO1_MOUSE EGMDNYK... SFLGGDSP.S MVDYLTWPWF QRLEALELK. .........E GTO2_HUMAN EILEYQN..T TFFGGTCI.S MIDYLLWPWF ERLDVYGIL. .........D YKJ3_CAEEL QLLTGD.... .FYSGTSKPG FVDYLLYPNI QRAYWAAHIV PDFPLEAESF 1ags_A LISSFPLLKA LKTRISN.LP TVKKFLQPGS PRKPPMDEKS LEEARKIFRF GTA1_BOVIN LLANFPLLKA LKARVSS.LP AVKKFLQPGS QRKPPTDEKK IEEARKVFKF GTA1_MOUSE LLTPFPLLKA FKSRISS.LP NVKKFLQPGS QRKPPMDAKQ IQEARKAFKI GTC1_RAT ALANFPLLKA LRTRVSN.LP TVKKFLQPGS QRKPLEDEKC VESAVKIFS. GTC_RABIT ATASFPLLQA LKTRISN.LP TVKKFLQPGS QRNPPDDEKC REEAKIIFH. GTA1_RABIT AIASFPLLKA LKTRISS.LP TVKKFLQPGS QRKPPMDEKN LEKAKKIFKI GTA1_CAVPO LLANFTLLQA LKTRVSN.LP NVKKFLQPGS QRKPFPTQEM FEEMRKF... GTA3_RAT VLSDFPLLQA FKTRISN.IP TIKKFLQPGS QRKPPPDGHY VDVVRTVLKF GTA3_CHICK ALSGFPLLQA FKKRISS.IP TIKKFLAPGS KRKPISDDKY VETVRRVLRM GTA2_CHICK ILSAFPLLQA FKGRMSN.VP TIKKFLQPGS QRKPPLDEKS IANVRKIFSF GTA1_CHICK ALAKFPLLQS FKARTSN.IP NIKKFLQPGS QRKPRLEEKD IPRLMAIFH. GTA1_ANTST ALSGFPLLQA FKTRISN.IP TVKTFLAPGS KRKPVPDAKY VEDIIKIFYF GTA4_HUMAN ILSAFPFLQE YTVKLSN.IP TIKRFLEPGS KKKPPPDEIY VRTVYNIFRP 1oe7_A LTGKYPEIHK HRENLLASSP RLAKYLSDRA .......... .......... GTP1_BUFBU CLSGFPLLCA YVKRISS.RP KLEAYLSSDA HKKRPINGNG KQQ....... GT28_SCHJA LTGKYPEIHK HRKHLLATSP KLAKYLSERH ATAF...... .......... GTS1_ASCSU FLSGHLQLKK YIEHVRE.LP NIKKWIAERP KTPY...... .......... GTS9_CAEEL ILDTFLEMKD HQKKIHS.IP NVKKWIEKRP VTSR...... .......... GTS3_CAEEL DIENDKLLKE FREKIIE.TP KLKEWLAKRP ETRF...... .......... GTS5_CAEEL DFSKFPELKA HAEKIQA.IP QIKKWIETRP VTPF...... .......... GTS6_CAEEL VVEEFKKVLA HRKKVQS.ID RIQKYIANRP DYPF...... .......... GTS4_CAEEL DFNKEKKLEE FYNKIHS.IP EIKNYVATRK DSIV...... .......... GTS2_CAEEL FAEKEPHLNV FIRKL..... .......... .......... .......... GTS1_CAEEL ALDAYPHILA LINRVFN.HP NIKKYVSQRK ATPA...... .......... GTS_ANOGA LLENFPNLQE VVQKVLD.NE NVKAYIAKRP ITEV...... .......... GTS2_MANSE LEEQYPIFKK PIETVLS.NP KLKAYLDSAP KKEF...... .......... GT1_ONCVO .LEEYPQLAS FVNKIGQ.MP GIKEWIKKRP KTYF...... .......... SC11_OMMSL LLKEYPKLAA LRTRVAA.HP KIAAYIKKRN NTAF...... .......... SC2_OCTDO LLNSYPKLQA LRTRVMS.HL KMSPYLKKRS STEF...... .......... 1aw9_ LVAARPHVKA WWEAIVA.RP AFQKTVAAIP LPPPP..... .......... GTH3_ARATH LIKDRKHVSA WWDKISS.RA AWKEVSAKYS LPV....... .......... GTH7_ARATH LVTSRENLNR WWNEISA.RP AWKKLMELAA Y......... .......... GTH6_ARATH LFDSRPKVSE WIKKISA.RP AWAKVIDLQK Q......... .......... GTH_SILCU LFEERAHVSA WCKKILA.RP SWEKTLALQK QA........ .......... GTH1_ARATH LFDERPHVSA WVADITS.RP SAQKVL.... .......... .......... GTH4_ARATH LFTERPRVNE WVAEITK.RP ASEKVQ.... .......... .......... GTH4_MAIZE LVHALPHVSA WWQGLAA.RP AANKVAQFMP VGAGAPKEQE .......... GTH1_MAIZE VLDAYPHVKA WWSGLME.RP SVQKVAALMK PSA....... .......... GTH1_WHEAT VFDDYPKVKA WWEMLMA.RP AVQRVCKHMP TEFKLGAQY. .......... GTH5_ARATH VLDSKPHVKA WWAASLRVIP GRLLRNSSKE FM........ .......... 1eem_A CVDHTPKLKL WMAAMKE.DP TVSALLTSEK DWQGFLELYL QNSPEACDYG GTO1_MOUSE CLAHTPKLKL WMAAMQQ.DP VASSHKIDAK TYREYLNLYL QDSPEACDYG GTO2_HUMAN CVSHTPALRL WISAMKW.DP TVCALLMDKS IFQGFLNLYF QNNPNAFDFG YKJ3_CAEEL PGPNYPRLSK WYKALESIPE VAAASQPTEN GVGFFKDYLG GSPNYDYGLT 1ags_A ....... GTA1_BOVIN ....... GTA1_MOUSE Q...... GTC1_RAT ....... GTC_RABIT ....... GTA1_RABIT P...... GTA1_CAVPO ....... GTA3_RAT ....... GTA3_CHICK YYDVKPH GTA2_CHICK ....... GTA1_CHICK ....... GTA1_ANTST ....... GTA4_HUMAN ....... 1oe7_A ....... GTP1_BUFBU ....... GT28_SCHJA ....... GTS1_ASCSU ....... GTS9_CAEEL ....... GTS3_CAEEL ....... GTS5_CAEEL ....... GTS6_CAEEL ....... GTS4_CAEEL ....... GTS2_CAEEL ....... GTS1_CAEEL ....... GTS_ANOGA ....... GTS2_MANSE ....... GT1_ONCVO ....... SC11_OMMSL ....... SC2_OCTDO ....... 1aw9_ ....... GTH3_ARATH ....... GTH7_ARATH ....... GTH6_ARATH ....... GTH_SILCU ....... GTH1_ARATH ....... GTH4_ARATH ....... GTH4_MAIZE ....... GTH1_MAIZE ....... GTH1_WHEAT ....... GTH5_ARATH ....... 1eem_A L...... GTO1_MOUSE L...... GTO2_HUMAN LC..... YKJ3_CAEEL KLSETI. kalign-3.5.1/tests/data/BB30014.tfa000066400000000000000000000240541515023132300164070ustar00rootroot00000000000000>1ags_A AEKPKLHYFNARGRMESTRWLLAAAGVEFEEKFIKSAEDLDKLRNDGYLM FQQVPMVEIDGMKLVQTRAILNYIASKYNLYRKDIKEKALIDMYIEGIAD LGEMILLLPFTQPEEQDAKLALIKEKIKNRYFPAFEKVLKSHGQDYLVGN KLSRADIHLVELLYYVEELDSSLISSFPLLKALKTRISNLPTVKKFLQPG SPRKPPMDEKSLEEARKIFRF >GTA1_BOVIN AGKPTLHYFNGRGRMECIRWLLAAAGVEFEEKFIEKPEDLDKLKNDGSLM FQQVPMVEIDGMKLVQTRAILNYIATKYNLYGKDMKERALIDMYSEGVAD LGEMIMHFPLCPPAEKDAKLTLIREKTTNRYLPAFENVLKSHGQDYLVGN KLSRADIHLVELLYYVEELDPSLLANFPLLKALKARVSSLPAVKKFLQPG SQRKPPTDEKKIEEARKVFKF >GTA1_MOUSE AGKPVLHYFNARGRMECIRWLLAAAGVEFEEKFIQSPEDLEKLKKDGNLM FDQVPMVEIDGMKLAQTRAILNYIATKYDLYGKDMKERALIDMYSEGILD LTEMIGQLVLCPPDQREAKTALAKDRTKNRYLPAFEKVLKSHGQDYLVGN RLTRVDIHLLEVLLYVEEFDASLLTPFPLLKAFKSRISSLPNVKKFLQPG SQRKPPMDAKQIQEARKAFKIQ >GTC1_RAT PGKPVLHYFDGRGRMEPIRWLLAAAGVEFEEQFLKTRDDLARLRNDGSLM FQQVPMVEIDGMKLVQTRAILNYIATKYNLYGKDMKERALIDMYAEGVAD LDEIVLHYPYIPPGEKEASLAKIKDKARNRYFPAFEKVLKSHGQDYLVGN RLSRADVYLVQVLYHVEELDPSALANFPLLKALRTRVSNLPTVKKFLQPG SQRKPLEDEKCVESAVKIFS >GTC_RABIT AGKPKLHYFNARGRMESIRWLLTAAGVEFEEKCMKTREDLEKLRKDGVLM FQQVPMVEIDGMKLVQTRAIFNYIADKHNLYGKDIKERALIDMYTEGIVD LNELILTRPFLPPEEQEAKLAQIKDKAKNRYFPAFEKVLKSHGQDYLVGN KLSKADILLVELLYNVEELNPGATASFPLLQALKTRISNLPTVKKFLQPG SQRNPPDDEKCREEAKIIFH >GTA1_RABIT MARKPLLHYFNGRGRMESIRWLLAAAGEEFDEKFMETAEDLDKLRNDGSL MYQQVPMVEIDGMKLVQTRAILNYVANKHNLYGKDMKERALIDMYTEGVA DLYELVLLLPLCPPEQKDAKVDFIKEKIRTRYFPAFEKVLKSHGQDYLVG NRLSKADILLVELLYNVEELDPSAIASFPLLKALKTRISSLPTVKKFLQP GSQRKPPMDEKNLEKAKKIFKIP >GTA1_CAVPO SGKPVLHYFNVQGRMESIRWLLAAAGVEFEEKLIMCQEDLDKLKNDGLLM FQQVPMVEMDGMKMVQSRAILNYIATKYNLYGKDTKERLLIDMYTEGMTD LYELFFKVILAPPEEKDAAKSLIKDRAKNRFLPAFEKVLKSHGQGYLVGN KLSKADILLTELLYMVEEFDASLLANFTLLQALKTRVSNLPNVKKFLQPG SQRKPFPTQEMFEEMRKF >GTA3_RAT MEVKPKLYYFQGRGRMESIRWLLATAGVEFEEEFLETREQYEKLQKDGCL LFGQVPLVEIDGMLLTQTRAILSYLAAKYNLYGKDLKERVRIDMYADGTQ DLMMMIIGAPFKAPQEKEESLALAVKRAKNRYFPVFEKILKDHGEAFLVG NQLSWADIQLLEAILMVEEVSAPVLSDFPLLQAFKTRISNIPTIKKFLQP GSQRKPPPDGHYVDVVRTVLKF >GTA3_CHICK AAKPVLYYFNGRGKMESIRWLLAAAGVEFEEVFLETREQYEKLLQSGILM FQQVPMVEIDGMKLVQTRAILNYIAGKYNLYGKDLKERALIDMYVGGTDD LMGFLLSFPFLSAEDKVKQCAFVVEKATSRYFPAYEKVLKDHGQDFLVGN RLSWADIHLLEAILMVEEKKSDALSGFPLLQAFKKRISSIPTIKKFLAPG SKRKPISDDKYVETVRRVLRMYYDVKPH >GTA2_CHICK MAGKPKLHYTRGRGKMESIRWLLAAAGVEFEEEFIEKKEDLEKLRNDGSL LFQQVPMVEIDGMKMVQSRAILCYIAGKYNLYGKDLKERAWIDMYVEGTT DLMGMIMALPFQAADVKEKNIALITERATTRYFPVYEKALKDHGQDYLVG NKLSWADIHLLEAILMTEELKSDILSAFPLLQAFKGRMSNVPTIKKFLQP GSQRKPPLDEKSIANVRKIFSF >GTA1_CHICK MSGKPVLHYANTRGRMESVRWLLAAAGVEFEEKFLEKKEDLQKLKSDGSL LFQQVPMVEIDGMKMVQTRAILNYIAGKYNLYGKDLKERALIDMYVEGLA DLYELIMMNVVQPADKKEEHLANALDKAANRYFPVFEKVLKDHGHDFLVG NKLSRADVHLLETILAVEESKPDALAKFPLLQSFKARTSNIPNIKKFLQP GSQRKPRLEEKDIPRLMAIFH >GTA1_ANTST MAGEQNIKYFNIKGRMEAIRWLLAVAGVEFEEKFFETKEQLQKLKETVLL FQQVPMVEIDGMKLVQTRAILHYIAEKYNLLGKDMKEHAQIIMYSEGTMD LMELIMIYPFLKGEEKKQRLVEIANKAKGRYFPAFENVLKTHGQNFLVGN QLSMADVQLFEAILMVEEKVPDALSGFPLLQAFKTRISNIPTVKTFLAPG SKRKPVPDAKYVEDIIKIFYF >GTA4_HUMAN MAARPKLHYPNGRGRMESVRWVLAAAGVEFDEEFLETKEQLYKLQDGNHL LFQQVPMVEIDGMKLVQTRSILHYIADKHNLFGKNLKERTLIDMYVEGTL DLLELLIMHPFLKPDDQQKEVVNMAQKAIIRYFPVFEKILRGHGQSFLVG NQLSLADVILLQTILALEEKIPNILSAFPFLQEYTVKLSNIPTIKRFLEP GSKKKPPPDEIYVRTVYNIFRP >1oe7_A DHIKVIYFNGRGRAESIRMTLVAAGVNYEDERISFQDWPKIKPTIPGGRL PAVKITDNHGHVKWMVESLAIARYMAKKHHMMGGTEEEYYNVEKLIGQAE DLEHEYYKTLMKPEEEKQKIIKEILNGKVPVLLDIICESLKASTGKLAVG DKVTLADLVLIAVIDHVTDLDKEFLTGKYPEIHKHRENLLASSPRLAKYL SDRA >GTP1_BUFBU PEYTIIYFNARGRCEAMRMLMADQGAQWKEEVVTSDDWQKGDLKKAAVYG QLPGFKDGDFTLYQSNAMLRLLARNHDLYGKNPREASLIDMVNDGVEDLR LKYLKMIYQNYENGKDDYVKALPTNLGHFERLLASNNEGKGFVVGAHISF ADYNLVDLLHNHLVLAPDCLSGFPLLCAYVKRISSRPKLEAYLSSDAHKK RPINGNGKQQ >GT28_SCHJA VKLIYFNGRGRAEPIRMILVAAGVEFEDERIEFQDWPKIKPTIPGGRLPI VKITDKRGDVKTMSESLAIARFIARKHNMMGDTDDEYYIIEKMIGQVEDV ESEYHKTLIKPPEEKEKISKEILNGKVPILLQAICETLKESTGNLTVGDK VTLADVVLIASIDHITDLDKEFLTGKYPEIHKHRKHLLATSPKLAKYLSE RHATAF >GTS1_ASCSU PQYKLTYFDIRGLGEGARLIFHQAGVKFEDNRLKREDWPALKPKTPFGQL PLLEVDGEVLAQSAAIYRYLGRQFGLAGKTPMEEAQVDSIFDQFKDFMAE LRPCFRVLAGFEEGDKEKVLKEVAVPARDKHLPLLEKFLAKSGSEYMVGK SVTWADLVITDSLASWESLIPDFLSGHLQLKKYIEHVRELPNIKKWIAER PKTPY >GTS9_CAEEL MVSYKLIYFQSRGNGEIARQVFAFAGQEFIDERISKEQWAEIKNMTPFGQ VPVLEVDGRQLAQSITIVRYLSKQFGISGKSSWEEAQVDALGDQFKDYRV EARPFFRAKMGFSDGDVDQLYKDLFVPAFNKMYSIFTESLKSSGSGFLVG DSLTWMDLAIAQHSADLLEADGKILDTFLEMKDHQKKIHSIPNVKKWIEK RPVTSR >GTS3_CAEEL MVHYKLTYFNARGLAEISRQLFHMAGVEFEDERINEEKFSQLKPTFPSGQ VPILCIDGAQFSQSTAIARYLARKFGFVGQTAEEELQADEVVDTFKDFIE SFRKFVIAVLSGESEEILKNIREEVIKPAVKTYTAYLKAILEKSSSGYLV GNELTWADLVIADNLTTLINAELLDIENDKLLKEFREKIIETPKLKEWLA KRPETRF >GTS5_CAEEL MVSYKLTYFNGRGAGEVSRQIFAYAGQQYEDNRVTQEQWPALKETCAAPF GQLPFLEVDGKKLAQSHAIARFLAREFKLNGKTAWEEAQVNSLADQYKDY SSEARPYFYAVMGFGPGDVETLKKDIFLPAFEKFYGFLVNFLKASGSGFL VGDSLTWIDLAIAQHSADLIAKGGDFSKFPELKAHAEKIQAIPQIKKWIE TRPVTPF >GTS6_CAEEL MVHYKLVYFPLRARAEIARQIFAYAGQDYSEENLSFEQWPARKNNTPFGQ LPILEVDGKPLGQSYAIARYLAREFGIAGQNDTEAAEVDAIADQFKDYLN DVSPYLTVLAGFKPGDKDQLRTDVFVPAFKKNFEFFENILASNHSGFFVG NSLTWVDLLISQHVQDILDKDLAVVEEFKKVLAHRKKVQSIDRIQKYIAN RPDYPF >GTS4_CAEEL MPNYKLLYFDARALAEPIRIMFAMLNVPYEDYRVSVEEWSKLKPTTPFGQ LPILQVDGEQFGQSMSITRYLARKFGLAGKTAEEEAYADSIVDQYRDFIF FFRQFTSSVFYGSDADHINKVRFEVVEPARDDFLAIINKFLAKSKSGFLV GDSLTWADIVIADNLTSLLKNGFLDFNKEKKLEEFYNKIHSIPEIKNYVA TRKDSIV >GTS2_CAEEL MVHYKLMCFDVRGLGEVIRQLFYLGDVSFEDFRVSREEFKSLKSNLPSGQ LPVLEIDGVMISQSASIGRFLARQYGYSGKTPTEEMQVDSIIDLFKDFML TFRQFFFAVIHGYPEYEKERMKRDIVKPAIKNYFIALNKILLRSKSGFLV GDDLTWADLQIADNLSTLINIRLFAEKEPHLNVFIRKL >GTS1_CAEEL MPHFKFYYFDVRGRGEAIRLLFHLADEKFDDERFGMEQWGVLKSEMPLGQ VPVLEIDGVKISQTTAIARYLGHQFHRAGTNAVDCARLDMIAEVIQEFMS SSGMGKFSRVLLGMIQANKEQFFKENVLPDVEKYAPIVEKFLLENGNNGL LLGDRETWVDVFAAESFSKLIDYGSPDALDAYPHILALINRVFNHPNIKK YVSQRKATPA >GTS_ANOGA LSSSSISRSSLKCNIMPDYKVYYFNVKALGEPLRFLLSYGNLPFDDVRIT REEWPALKPTMPMRQMPVLEVDGKRVHQSLAMCRYVAKQINLAGDNPLEA LQIDAIVDTINDFRLKIAIVAYEPDDMVKEKKMVTLNNEVIPFYLTKLNV IAKENNGHLVLGKPTWADVYFAGILDYLNYLTKTNLLENFPNLQEVVQKV LDNENVKAYIAKRPITEV >GTS2_MANSE MPKVVFHYFGAKGWARPTMLLAYGGQEFEDHRVEYEQWPEFKPNTPFGQM PVLEIDGKKYAQSLAISRYLGRKYGLAGNDIEEDFEIDQIVDFVNDIRAS AASVEYEQDAANKEVKHEENMKNKYPFQLNKLSEIITKNNGFLALGRLTW ADFVFVGMFDYLKKMLRMPDLEEQYPIFKKPIETVLSNPKLKAYLDSAPK KEF >GT1_ONCVO FITMNFVVEAASSNANQAITSENSIKPKGKLQPQMEKYTLTYFNGRGRAE VIRLLFALANVSYEDNRITRDEWKYLKPRTPFGHVPMLNVSGNVLGESHA IELLLGGRFGLLGTNDWEEAKIMAVVLNIDELFQKLIPWTHEKNTTKKAE LFRNLSESDVMPFLGRYEKFLKESTTGHIVGNKVSVADLTVFNMLMTLDD EVKLEEYPQLASFVNKIGQMPGIKEWIKKRPKTYF >SC11_OMMSL MPSYTLYYFNGRGRAEICRMLFAVASVQYQDKRIELAEWTQFKTKMPCHM LPILEIDTETQVPQSMAISRYLAREFGFYGKNNMDMFKVDCLCDSLFELF NDYMAVYNEKDAAKKTELQKRFQNTCLRVLPYMEKTLEANKGGAGWFIGD QILLCDMMTHAALENPIQENANLLKEYPKLAALRTRVAAHPKIAAYIKKR NNTAF >SC2_OCTDO MPSYTLNYFNHRGRAEICRMLFAAAGVQYNDRRIETSEWSNMRSKMPCSM MPMLDIDNRHQIPQTMAIARYLAREFGFHGKNNMEMARVEYISDCFYDIL DDYLRMYQDDNCRMMFQRSGDRNGSSEKRTRYQETLRRILPFMERTLEMY KSGGQFFMGDQMTMADMMCYCALENPIMEESSLLNSYPKLQALRTRVMSH LKMSPYLKKRSSTEF >1aw9_ APLKLYGMPLSPNVVRVATVLNEKGLDFEIVPVDLTTGAHKQPDFLALNP FGQIPALVDGDEVLFESRAINRYIASKYASEGTDLLPATASAAKLEVWLE VESHHFYPNASPLVFQLLVRPLLGGAPDAAVVDKHAEQLAKVLDVYEAHL ARNKYLAGDEFTLADANHASYLLYLSKTPKAGLVAARPHVKAWWEAIVAR PAFQKTVAAIPLPPPP >GTH3_ARATH VLTIYAPLFASSKRAVVTLVEKGVSFETVNVDLMKGEQRQPEYLAIQPFG KIPVLVDGDYKIFESRAIMRYIAEKYRSQGPDLLGKTIEERGQVEQWLDV EATSYHPPLLALTLNIVFAPLMGFPADEKVIKESEEKLAEVLDVYEAQLS KNEYLAGDFVSLADLAHLPFTEYLVGPIGKAHLIKDRKHVSAWWDKISSR AAWKEVSAKYSLPV >GTH7_ARATH MVVKVYGQIKAANPQRVLLCFLEKDIEFEVIHVDLDKLEQKKPQHLLRQP FGQVPAIEDGYLKLFESRAIARYYATKYADQGTDLLGKTLEGRAIVDQWV EVENNYFYAVALPLVMNVVFKPKSGKPCDVALVEELKVKFDKVLDVYENR LATNRYLGGDEFTLADLSHMPGMRYIMNETSLSGLVTSRENLNRWWNEIS ARPAWKKLMELAAY >GTH6_ARATH MASIKVHGVPMSTATMRVLATLYEKDLQFELIPVDMRAGAHKQEAHLALN PFGQIPALEDGDLTLFESRAITQYLAEEYSEKGEKLISQDCKKVKATTNV WLQVEGQQFDPNASKLAFERVFKGMFGMTTDPAAVQELEGKLQKVLDVYE ARLAKSEFLAGDSFTLADLHHLPAIHYLLGTDSKVLFDSRPKVSEWIKKI SARPAWAKVIDLQKQ >GTH_SILCU TIKVHGNPRSTATQRVLVALYEKHLEFEFVPIDMGAGGHKQPSYLALNPF GQVPALEDGEIKLFESRAITKYLAYTHDHQNEGTSLIHKEKHEMAAQLVW EEVEAHQFDPVASKLAWELVFKGIFGMQTDTTVVEENEAKLAKVLDVYEA RLTESEYLGANDSFTLVDLHHLPLLGYLMGTQVKKLFEERAHVSAWCKKI LARPSWEKTLALQKQA >GTH1_ARATH MAGIKVFGHPASTATRRVLIALHEKNVDFEFVHVELKDGEHKKEPFILRN PFGKVPAFEDGDFKIFESRAITQYIAHEFSDKGNNLLSTGKDMAIIAMGI EIESHEFDPVGSKLVWEQVLKPLYGMTTDKTVVEEEEAKLAKVLDVYEHR LGESKYLASDHFTLVDLHTIPVIQYLLGTPTKKLFDERPHVSAWVADITS RPSAQKVL >GTH4_ARATH AGIKVFGHPASIATRRVLIALHEKNLDFELVHVELKDGEHKKEPFLSRNP FGQVPAFEDGDLKLFESRAITQYIAHRYENQGTNLLQTDSKNISQYAIMA IGMQVEDHQFDPVASKLAFEQIFKSIYGLTTDEAVVAEEEAKLAKVLDVY EARLKEFKYLAGETFTLTDLHHIPAIQYLLGTPTKKLFTERPRVNEWVAE ITKRPASEKVQ >GTH4_MAIZE ATPAVKVYGWAISPFVSRALLALEEAGVDYELVPMSRQDGDHRRPEHLAR NPFGKVPVLEDGDLTLFESRAIARHVLRKHKPELLGGGRLEQTAMVDVWL EVEAHQLSPPAIAIVVECVFAPFLGRERNQAVVDENVEKLKKVLEVYEAR LATCTYLAGDFLSLADLSPFTIMHCLMATEYAALVHALPHVSAWWQGLAA RPAANKVAQFMPVGAGAPKEQE >GTH1_MAIZE APMKLYGAVMSWNLTRCATALEEAGSDYEIVPINFATAEHKSPEHLVRNP FGQVPALQDGDLYLFESRAICKYAARKNKPELLREGNLEEAAMVDVWIEV EANQYTAALNPILFQVLISPMLGGTTDQKVVDENLEKLKKVLEVYEARLT KCKYLAGDFLSLADLNHVSVTLCLFATPYASVLDAYPHVKAWWSGLMERP SVQKVAALMKPSA >GTH1_WHEAT MSPVKVFGHPMLTNVARVLLFLEEVGAEYELVPMDFVAGEHKRPQHVQLN PFAKMPGFQDGDLVLFESRAIAKYILRKYGGTAGLDLLGENSGIEELAMV DVWTEVEAQQYYPAISPVVFECIIIPFIIPGGGAAPNQTVVDESLERLRG VLGIYEARLEKSRYLAGDSITFADLNHIPFTFYFMTTPYAKVFDDYPKVK AWWEMLMARPAVQRVCKHMPTEFKLGAQY >GTH5_ARATH MVTVKLYGMAYSTCTKRVYTTAKEIGVDVKIVPVDLMKGEHKEPAYLDNY HPFGVIPVLEDEDGTKIYESRAISRYLVAKYGKGSSLLPSPSDPKAYGLF EQAASVEYSSFDPPASSLAYERVFAGMRGLKTNEELAKKYVDTLNAKMDG YERILSKQKYLAGNDFTLADLFHLPYGAMVAQLEPTVLDSKPHVKAWWAA SLRVIPGRLLRNSSKEFM >1eem_A SARSLGKGSAPPGPVPEGSIRIYSMRFCPFAERTRLVLKAKGIRHEVINI NLKNKPEWFFKKNPFGLVPVLENSQGQLIYESAITCEYLDEAYPGKKLLP DDPYEKACQKMILELFSKVPSLVGSFIRSQNKEDYAGLKEEFRKEFTKLE EVLTNKKTTFFGGNSISMIDYLIWPWFERLEAMKLNECVDHTPKLKLWMA AMKEDPTVSALLTSEKDWQGFLELYLQNSPEACDYGL >GTO1_MOUSE MSGESSRSLGKGSAPPGPVPEGQIRVYSMRFCPFAQRTLMVLKAKGIRHE VININLKNKPEWFFEKNPLGLVPVLENSQGHLVTESVITCEYLDEAYPEK KLFPDDPYKKARQKMTLESFSKVPPLIASFVRSKRKEDSPNLREALENEF KKLEEGMDNYKSFLGGDSPSMVDYLTWPWFQRLEALELKECLAHTPKLKL WMAAMQQDPVASSHKIDAKTYREYLNLYLQDSPEACDYGL >GTO2_HUMAN MSGDATRTLGKGSQPPGPVPEGLIRIYSMRFCPYSHRTRLVLKAKDIRHE VVNINLRNKPEWYYTKHPFGHIPVLETSQCQLIYESVIACEYLDDAYPGR KLFPYDPYERARQKMLLELFCKVPHLTKECLVALRCGRECTNLKAALRQE FSNLEEILEYQNTTFFGGTCISMIDYLLWPWFERLDVYGILDCVSHTPAL RLWISAMKWDPTVCALLMDKSIFQGFLNLYFQNNPNAFDFGLC >YKJ3_CAEEL MTVLAGVNSKIVKNGCWNHSYLQHAILPVAQRALIYASVKNIPSDVINVH LQEKPDWYFSKHYKGQVPTLEHDEGKKHVIESAVIPEYLDDIYPETRILP TDPYEKVQQKLLLDRISGQVSPAFYGVVQAVKNPDLREEKFADIKKAYDN AEQLLTGDFYSGTSKPGFVDYLLYPNIQRAYWAAHIVPDFPLEAESFPGP NYPRLSKWYKALESIPEVAAASQPTENGVGFFKDYLGGSPNYDYGLTKLS ETI kalign-3.5.1/tests/data/README000066400000000000000000000003121515023132300157070ustar00rootroot00000000000000This directory contains test alignments: - distributed together with Sean Eddy's easel library: https://github.com/EddyRivasLab/easel - from the balibase MSA benchmark: https://www.lbgi.fr/balibase/ kalign-3.5.1/tests/data/a2m.good.1000066400000000000000000000012521515023132300165230ustar00rootroot00000000000000>MYG_PHYCA VLSEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEKFDRFKHLKTEAEMKASED LKKHGVTVLTALGAILKKKGHHEAELKPLAQSHATKHKIPIKYLEFISEAIIHVLHSRHP GDFGADAQGAMNKALELFRKDIAAKYKELGYQG >GLB5_PETMA pivdtgsvApLSAAEKTKIRSAWAPVYSTYETSGVDILVKFFTSTPAAQEFFPKFKGLTT ADQLKKSADVRWHAERIINAVNDAVASMDDtekMSMKLRDLSGKHAKSFQVDPQYFKVLA AVI---------ADTVAAGDAGFEKLMSMICILLRSAY------- >HBB_HUMAN VhLTPEEKSAVTALWGKV--NVDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNP KVKAHGKKVLGAFSDGLAHLDNLKGTFATLSELHCDKLHVDPENFRLLGNVLVCVLAHHF GKEFTPPVQAAYQKVVAGVANALAHKYH------ >HBA_HUMAN VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHF------DLSHGSAQ VKGHGKKVADALTNAVAHVDDMPNALSALSDLHAHKLRVDPVNFKLLSHCLLVTLAAHLP AEFTPAVHASLDKFLASVSTVLTSKYR------ kalign-3.5.1/tests/data/a2m.good.2000066400000000000000000000006311515023132300165240ustar00rootroot00000000000000>tRNA2 UCCGAUAUAGUGUAACGGCUAUCACAUCACGCUUUCACCGUGGAGACCGGGGUUCGACUC CCCGUAUCGGAG >tRNA3 UCCGUGAUAGUUUAAUGGUCAGAAUGG-GCGCUUGUCGCGUGCcAGAUCGGGGUUCAAUU CCCCGUCGCGGAG >tRNA5 GGGCACAUGGCGCAGUUGGUAGCGCGCUUCCCUUGCAAGGAAGaGGUCAUCGGUUCGAUU CCGGUUGCGUCCA >tRNA1 GCGGAUUUAGCUCAGUUGGGAGAGCGCCAGACUGAAGAUCUGGaGGUCCUGUGUUCGAUC CACAGAAUUCGCA >tRNA4 GCUCGUAUGGCGCAGUGG-UAGCGCAGCAGAUUGCAAAUCUGUuGGUCCUUAGUUCGAUC CUGAGUGCGAGCU kalign-3.5.1/tests/data/afa.good.1000066400000000000000000000013151515023132300165730ustar00rootroot00000000000000>MYG_PHYCA --------V-LSEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEKFDRFKHLKT EAEMKASEDLKKHGVTVLTALGAILKKKGH---HEAELKPLAQSHATKHKIPIKYLEFIS EAIIHVLHSRHPGDFGADAQGAMNKALELFRKDIAAKYKELGYQG >GLB5_PETMA PIVDTGSVAPLSAAEKTKIRSAWAPVYSTYETSGVDILVKFFTSTPAAQEFFPKFKGLTT ADQLKKSADVRWHAERIINAVNDAVASMDDTEKMSMKLRDLSGKHAKSFQVDPQYFKVLA AVI---------ADTVAAGDAGFEKLMSMICILLRSAY------- >HBB_HUMAN --------VHLTPEEKSAVTALWGKV--NVDEVGGEALGRLLVVYPWTQRFFESFGDLST PDAVMGNPKVKAHGKKVLGAFSDGLAHLDN---LKGTFATLSELHCDKLHVDPENFRLLG NVLVCVLAHHFGKEFTPPVQAAYQKVVAGVANALAHKYH------ >HBA_HUMAN --------V-LSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHF----- -DLSHGSAQVKGHGKKVADALTNAVAHVDD---MPNALSALSDLHAHKLRVDPVNFKLLS HCLLVTLAAHLPAEFTPAVHASLDKFLASVSTVLTSKYR------ kalign-3.5.1/tests/data/afa.good.2000066400000000000000000000006321515023132300165750ustar00rootroot00000000000000>tRNA2 UCCGAUAUAGUGUAACGGCUAUCACAUCACGCUUUCACCGUGG-AGACCGGGGUUCGACU CCCCGUAUCGGAG >tRNA3 UCCGUGAUAGUUUAAUGGUCAGAAUGG-GCGCUUGUCGCGUGCCAGAUCGGGGUUCAAUU CCCCGUCGCGGAG >tRNA5 GGGCACAUGGCGCAGUUGGUAGCGCGCUUCCCUUGCAAGGAAGAGGUCAUCGGUUCGAUU CCGGUUGCGUCCA >tRNA1 GCGGAUUUAGCUCAGUUGGGAGAGCGCCAGACUGAAGAUCUGGAGGUCCUGUGUUCGAUC CACAGAAUUCGCA >tRNA4 GCUCGUAUGGCGCAGUGG-UAGCGCAGCAGAUUGCAAAUCUGUUGGUCCUUAGUUCGAUC CUGAGUGCGAGCU kalign-3.5.1/tests/data/afa.good.3000066400000000000000000000016151515023132300166000ustar00rootroot00000000000000>7295730___KOG0003 mqifvktltgktitlevepsdtienvkakiqdkegippdqqrlifagkql edgrtlsdyniqkestlhlvlrlrggiiepslrilaqkyncdkmicrkcy arlhpratncrkkkcghtnnlrpkkklk >Hs4507761___KOG0003 mqifvktltgktitlevepsdtienvkakiqdkegippdqqrlifagkql edgrtlsdyniqkestlhlvlrlrggiiepslrqlaqkyncdkmicrkcy arlhpravncrkkkcghtnnlrpkkkvk >At3g52590___KOG0003 mqifvktltgktitlevessdtidnvkakiqdkegippdqqrlifagkql edgrtladyniqkestlhlvlrlrggiiepslmmlarkynqdkmicrkcy arlhpravncrkkkcghsnqlrpkkkik >CE15495___KOG0003 mqifvktltgktitleveasdtienvkakiqdkegippdqqrlifagkql edgrtlsdyniqkestlhlvlrlrggiiepslrqlaqkyncdkqicrkcy arlpprasncrkkkcghsselrikkklk >SPAC1805.12c___KOG0003 mqifvktltgktitlevessdtidnvkskiqdkegippdqqrlifagkql edgrtlsdyniqkestlhlvlrlrggiiepslkalaskyncekqicrkcy arlppratncrkkkcghtnqlrpkkklk >YIL148w___KOG0003 mqifvktltgktitlevessdtidnvkskiqdkegippdqqrlifagkql edgrtlsdyniqkestlhlvlrlrggiiepslkalaskyncdksvcrkcy arlppratncrkrkcghtnqlrpkkklk kalign-3.5.1/tests/data/clustal.good.1000066400000000000000000000021451515023132300175150ustar00rootroot00000000000000MUSCLE (3.7) multiple sequence alignment MYG_PHYCA --------V-LSEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEKFDRFKHLKT GLB5_PETMA PIVDTGSVAPLSAAEKTKIRSAWAPVYSTYETSGVDILVKFFTSTPAAQEFFPKFKGLTT HBB_HUMAN --------VHLTPEEKSAVTALWGKV--NVDEVGGEALGRLLVVYPWTQRFFESFGDLST HBA_HUMAN --------V-LSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHF----- . *: :. : *. * * : * .:: * : * * MYG_PHYCA EAEMKASEDLKKHGVTVLTALGAILKKKGH---HEAELKPLAQSHATKHKIPIKYLEFIS GLB5_PETMA ADQLKKSADVRWHAERIINAVNDAVASMDDTEKMSMKLRDLSGKHAKSFQVDPQYFKVLA HBB_HUMAN PDAVMGNPKVKAHGKKVLGAFSDGLAHLDN---LKGTFATLSELHCDKLHVDPENFRLLG HBA_HUMAN -DLSHGSAQVKGHGKKVADALTNAVAHVDD---MPNALSALSDLHAHKLRVDPVNFKLLS . .:. *. : *. : . : *: *. . .: : .:. MYG_PHYCA EAIIHVLHSRHPGDFGADAQGAMNKALELFRKDIAAKYKELGYQG GLB5_PETMA AVI---------ADTVAAGDAGFEKLMSMICILLRSAY------- HBB_HUMAN NVLVCVLAHHFGKEFTPPVQAAYQKVVAGVANALAHKYH------ HBA_HUMAN HCLLVTLAAHLPAEFTPAVHASLDKFLASVSTVLTSKYR------ : : . .. :* : . : * kalign-3.5.1/tests/data/clustal.good.2000066400000000000000000000012611515023132300175140ustar00rootroot00000000000000CLUSTAL W (1.81) multiple sequence alignment tRNA2 UCCGAUAUAGUGUAACGGCUAUCACAUCACGCUUUCACCGUGG-AGACCGGGGUUCGACU tRNA3 UCCGUGAUAGUUUAAUGGUCAGAAUGG-GCGCUUGUCGCGUGCCAGAUCGGGGUUCAAUU tRNA5 GGGCACAUGGCGCAGUUGGUAGCGCGCUUCCCUUGCAAGGAAGAGGUCAUCGGUUCGAUU tRNA1 GCGGAUUUAGCUCAGUUGGGAGAGCGCCAGACUGAAGAUCUGGAGGUCCUGUGUUCGAUC tRNA4 GCUCGUAUGGCGCAGUGG-UAGCGCAGCAGAUUGCAAAUCUGUUGGUCCUUAGUUCGAUC * * * * * * * **** * tRNA2 CCCCGUAUCGGAG tRNA3 CCCCGUCGCGGAG tRNA5 CCGGUUGCGUCCA tRNA1 CACAGAAUUCGCA tRNA4 CUGAGUGCGAGCU * kalign-3.5.1/tests/data/small.fa000066400000000000000000000003561515023132300164570ustar00rootroot00000000000000>1 GGGGGAAGGGGG >2 GGGGAAGGGGG >3 GGGGAAGGGGG >4 GGGGAAGGGG >5 GGGGGAAGGGGGG >6 GGGGGAAGGGG >7 GGGGGAAGGGGGA >8 GGGGGAAGGGGGG >9 GGGGGAAGGGGGG >10 GGGGAAGTTG >11 A >12 GGGGGGAAGGGGG >13 GGGGGAAGGGGGG >14 GGGAGT >15 GGGGAAGGGGG >16 G >17 Tkalign-3.5.1/tests/data/tiny.fa000066400000000000000000000000261515023132300163240ustar00rootroot00000000000000>2 A >1 GGGGGAAGGGGGG kalign-3.5.1/tests/data/tiny_internal.fa000066400000000000000000000000231515023132300202150ustar00rootroot00000000000000>1 GATTACA >2 GATTAkalign-3.5.1/tests/dssim.c000066400000000000000000000427461515023132300154220ustar00rootroot00000000000000#include "tldevel.h" #include "tlrng.h" #include "msa_struct.h" #include "msa_alloc.h" #include "msa_op.h" #include "alphabet.h" #define DSSIM_IMPORT #include "dssim.h" typedef enum { TMM = 0, TMI = 1, TMD = 2, TII = 3, TIM = 4, TDD = 5, TDM = 6 } trans_type; typedef enum { HMM_MATCH, HMM_INSERT, HMM_DELETE } state_type; struct hmm_param { double match_err_p; double insert_err_p; double indel_p; int n_observed_seq; int n_sim_seq; int len; int seed; int dna; }; struct hmm { double** match_emit; double** insert_emit; double** transition; struct rng_state* rng; int L; int len; }; int hmm_emit_simple(struct hmm *hmm, char **out,int *len); int sample_pick(double *p, int len, struct rng_state* rng, int *pick); char emit_letter(int c); char emit_letter_dna(int c); /* int hmm_init(struct hmm **hmm, int len, int seed); */ int hmm_init(struct hmm **hmm, struct hmm_param *p); int get_prior_emit(double **prior); int get_prior_emit_dna(double** prior); int get_prior_trans(double** prior); int pick(double *p, int len, struct rng_state *rng, int *pick); int hmm_print(struct hmm *m); int hmm_alloc(struct hmm **hmm, int len, int seed, int dna); void hmm_free(struct hmm *n); int hmm_param_init(struct hmm_param **param); void hmm_param_free(struct hmm_param *p); int dssim_get_fasta(struct msa **msa, int n_seq, int n_obs, int dna,int len, int seed) { struct msa* m = NULL; struct hmm* hmm = NULL; struct hmm_param* p = NULL; char* tmp_seq = NULL; int r_len; /* RUN(alloc_msa(&m)); */ hmm_param_init(&p); p->n_sim_seq = n_seq; p->n_observed_seq = n_obs; p->len = len; p->seed = seed; p->dna = dna; if(n_seq > 100){ p->indel_p = 0.02; }else{ p->indel_p = 0.04; } RUN(hmm_init(&hmm, p)); /* hmm_print(hmm); */ MMALLOC(m, sizeof(struct msa)); m->sequences = NULL; m->alloc_numseq = n_seq; m->numseq = n_seq; m->num_profiles = 0; m->L = ALPHA_UNDEFINED; m->biotype = ALN_BIOTYPE_UNDEF; m->aligned = 0; m->alnlen = 0; m->plen = NULL; m->sip = NULL; m->nsip = NULL; m->seq_distances = NULL; m->col_confidence = NULL; m->seq_weights = NULL; m->run_parallel = 0; m->consistency_table = NULL; m->quiet = 1; MMALLOC(m->sequences, sizeof(struct msa_seq*) * m->alloc_numseq); for(int i = 0; i < 128; i++){ m->letter_freq[i] = 0; } m->numseq = 0; for(int i = 0; i < p->n_sim_seq;i++){ hmm_emit_simple(hmm, &tmp_seq, &r_len); /* LOG_MSG("%s", tmp_seq); */ m->sequences[i] = NULL; struct msa_seq* seq = NULL; MMALLOC(seq, sizeof(struct msa_seq)); seq->name = NULL; seq->seq = NULL; seq->s = NULL; seq->gaps = NULL; seq->confidence = NULL; seq->len = r_len; seq->alloc_len = r_len+1; seq->rank = i; MMALLOC(seq->name, sizeof(char)* MSA_NAME_LEN); snprintf(seq->name, MSA_NAME_LEN,"%d",i+1); MMALLOC(seq->seq, sizeof(char) * seq->alloc_len); MMALLOC(seq->s, sizeof(uint8_t) * seq->alloc_len); MMALLOC(seq->gaps, sizeof(int) * (seq->alloc_len+1)); for(int j = 0;j < seq->alloc_len+1;j++){ seq->gaps[j] = 0; } for(int j = 0; j < r_len;j++){ m->letter_freq[(int)tmp_seq[j]]++; seq->seq[j] = tmp_seq[j]; } MFREE(tmp_seq); seq->seq[r_len] = 0; m->sequences[i] = seq; m->numseq++; } /* tl_random_double(struct rng_state *rng) */ hmm_param_free(p); hmm_free(hmm); RUN(detect_alphabet(m)); /* LOG_MSG("%d biotype",m->biotype); */ RUN(detect_aligned(m)); /* LOG_MSG("%d aligned ",m->aligned); */ RUN(set_sip_nsip(m)); /* LOG_MSG("%d aligned ",m->aligned); */ *msa = m; return OK; ERROR: return FAIL; } /* int main(int argc, char *argv[]) */ /* { */ /* struct msa* m = NULL; */ /* dssim_get_fasta(&m, 20, 10, 250, 1); */ /* for(int i= 0; i < m->numseq;i++){ */ /* fprintf(stdout,">Seq_%d\n%s\n",i, m->sequences[i]->seq); */ /* } */ /* kalign_free_msa(m); */ /* return EXIT_SUCCESS; */ /* ERROR: */ /* kalign_free_msa(m); */ /* return EXIT_FAILURE; */ /* } */ int hmm_emit_simple(struct hmm *hmm, char **out,int *len) { char* seq = NULL; int n_alloc = 256; int n = 0; int pos = 0; int pick; state_type state = HMM_INSERT; double r; double sum; state = tl_random_int(hmm->rng, 3); MMALLOC(seq, sizeof(char) * n_alloc); seq[0] = 0; while(pos+1 < hmm->len){ /* transiation; */ switch (state) { case HMM_MATCH: r = tl_random_double(hmm->rng); sum = hmm->transition[pos][TMM]; if(r < sum){ state = HMM_MATCH; }else{ sum += hmm->transition[pos][TMI]; if(r < sum){ state = HMM_INSERT; }else{ sum += hmm->transition[pos][TMD]; if(r < sum){ state = HMM_DELETE; } } } pos++; break; case HMM_INSERT: r = tl_random_double(hmm->rng); sum = hmm->transition[pos][TII]; if(r < sum){ state = HMM_INSERT; }else{ state = HMM_MATCH; pos++; } break; case HMM_DELETE: r = tl_random_double(hmm->rng); sum = hmm->transition[pos][TDD]; if(r < sum){ state = HMM_DELETE; pos++; }else{ state = HMM_MATCH; pos++; } break; } /* Emit */ switch (state) { case HMM_MATCH: sample_pick(hmm->match_emit[pos], 20, hmm->rng,&pick); if(hmm->L == 20){ seq[n] = emit_letter(pick); }else{ seq[n] = emit_letter_dna(pick); } n++; if(n == n_alloc){ n_alloc = n_alloc + n_alloc / 2; MREALLOC(seq, sizeof(char) * n_alloc); } break; case HMM_INSERT: sample_pick(hmm->insert_emit[pos], 20, hmm->rng,&pick); if(hmm->L == 20){ seq[n] = emit_letter(pick); }else{ seq[n] = emit_letter_dna(pick); } n++; if(n == n_alloc){ n_alloc = n_alloc + n_alloc / 2; MREALLOC(seq, sizeof(char) * n_alloc); } break; case HMM_DELETE: break; } seq[n] = 0; /* n++; */ if(n == n_alloc){ n_alloc = n_alloc + n_alloc / 2; MREALLOC(seq, sizeof(char) * n_alloc); } /* LOG_MSG("%d %s n:%d state:%d",pos,seq,n ,state); */ } seq[n] = 0; *len = n; *out = seq; return OK; ERROR: return FAIL; } int sample_pick(double *p, int len, struct rng_state* rng, int *pick) { double sum = 0.0; double r = 0.0; int sel = -1; r =tl_random_double(rng); sum = 0.0; for(int i = 0; i < len;i++){ sum += p[i]; if(r < sum){ sel = i; break; } } /* LOG_MSG("Selected: %d (%d) r:%f %f", sel, len, r, sum); */ /* if(sel == -1){ */ /* } */ *pick = sel; return OK; } char emit_letter(int c) { char alphabet[20] = "ACDEFGHIKLMNPQRSTVWY"; return alphabet[c]; } char emit_letter_dna(int c) { char alphabet[4] = "ACGT"; return alphabet[c]; } int hmm_init(struct hmm **hmm, struct hmm_param* p) { struct hmm* n = NULL; double* prior_e = NULL; double* prior_t = NULL; double sum = 0.0; double r = 0.0; int pick; if(p->dna == 0){ RUN(get_prior_emit(&prior_e)); RUN(get_prior_trans(&prior_t)); }else{ RUN(get_prior_emit_dna(&prior_e)); RUN(get_prior_trans(&prior_t)); } prior_t[TMM] = 1.0 - p->indel_p; prior_t[TMI] = p->indel_p / 2.0; prior_t[TMD] = p->indel_p / 2.0; prior_t[TMM] = prior_t[TMM] / (prior_t[TMM] + prior_t[TMI] + prior_t[TMD]); prior_t[TMI] = prior_t[TMI] / (prior_t[TMM] + prior_t[TMI] + prior_t[TMD]); prior_t[TMD] = prior_t[TMD] / (prior_t[TMM] + prior_t[TMI] + prior_t[TMD]); RUN(hmm_alloc(&n, p->len,p->seed,p->dna)); /* Start state... */ /* n->transition[0][TMM] = 0.5; */ /* n->transition[0][TMI] = 0.5; */ /* n->transition[0][TMD] = 0.0; */ for(int i = 0; i < p->len;i++){ for(int j = 0; j < n->L;j++){ n->match_emit[i][j] = 0.0; n->insert_emit[i][j] = 0.0; } /* match */ /* pick =tl_random_int(n->rng, 20); */ sample_pick(prior_e, n->L, n->rng, &pick); for(int j = 0; j < p->n_observed_seq;j++){ r = tl_random_double(n->rng); if(r < p->match_err_p){ int c = tl_random_int(n->rng, n->L); n->match_emit[i][c] += 1.0; }else{ n->match_emit[i][pick] += 1.0; } } /* insert */ /* pick =tl_random_int(n->rng, 20); */ sample_pick(prior_e, n->L, n->rng, &pick); for(int j = 0; j < p->n_observed_seq;j++){ r = tl_random_double(n->rng); if(r < p->insert_err_p){ int c = tl_random_int(n->rng, n->L); n->insert_emit[i][c] += 1.0; }else{ n->insert_emit[i][pick] += 1.0; } } for(int j = 0; j < n->L;j++){ n->match_emit[i][j] += prior_e[j]; n->insert_emit[i][j] += prior_e[j]; } sum = 0.0; for(int j = 0; j < n->L;j++){ sum += n->match_emit[i][j]; } for(int j = 0; j < n->L;j++){ n->match_emit[i][j]/=sum; } sum = 0.0; for(int j = 0; j < n->L;j++){ sum += n->insert_emit[i][j]; } for(int j = 0; j < n->L;j++){ n->insert_emit[i][j]/=sum; } /* transitions */ for(int j = 0; j < 7;j++){ n->transition[i][j] = prior_t[j]; } } gfree(prior_e); gfree(prior_t); *hmm = n; return OK; ERROR: return FAIL; } int get_prior_emit(double** prior) { double* p = NULL; double sum = 0.0; RUN(galloc(&p , 20)); p[0] = 0.075520; /* A */ p[1] = 0.016973; /* C */ p[2] = 0.053029; /* D */ p[3] = 0.063204; /* E */ p[4] = 0.040762; /* F */ p[5] = 0.068448; /* G */ p[6] = 0.022406; /* H */ p[7] = 0.057284; /* I */ p[8] = 0.059398; /* K */ p[9] = 0.093399; /* L */ p[10] = 0.023569; /* M */ p[11] = 0.045293; /* N */ p[12] = 0.049262; /* P */ p[13] = 0.040231; /* Q */ p[14] = 0.051573; /* R */ p[15] = 0.072214; /* S */ p[16] = 0.057454; /* T */ p[17] = 0.065252; /* V */ p[18] = 0.012513; /* W */ p[19] = 0.031985; /* Y */ sum = 0.0; for(int i = 0; i < 20;i++){ sum += p[i]; } for(int i = 0; i < 20;i++){ p[i]/= sum; } *prior = p; return OK; ERROR: return FAIL; } int get_prior_emit_dna(double** prior) { double* p = NULL; double sum = 0.0; RUN(galloc(&p , 4)); p[0] = 0.2; /* A */ p[1] = 0.3; /* C */ p[2] = 0.3; /* G */ p[3] = 0.2; /* T */ sum = 0.0; for(int i = 0; i < 4;i++){ sum += p[i]; } for(int i = 0; i < 4;i++){ p[i]/= sum; } *prior = p; return OK; ERROR: return FAIL; } int get_prior_trans(double** prior) { double* p = NULL; RUN(galloc(&p , 7)); p[TMM] = 0.96; p[TMI] = 0.02; p[TMD] = 0.02; p[TII] = 0.50; p[TIM] = 0.50; p[TDD] = 0.50; p[TDM] = 0.50; *prior = p; return OK; ERROR: return FAIL; } int hmm_print(struct hmm *m) { ASSERT(m == NULL,"No hmm"); LOG_MSG("Emission: MATCH "); for(int i = 0; i < m->len;i++){ fprintf(stdout," %4d ", i); for(int j = 0; j < m->L;j++){ fprintf(stdout,"%0.2f ", m->match_emit[i][j]); } fprintf(stdout,"\n"); } fprintf(stdout,"\n"); LOG_MSG("Emission: Insert "); for(int i = 0; i < m->len;i++){ fprintf(stdout," %4d ", i); for(int j = 0; j < m->L;j++){ fprintf(stdout,"%0.2f ", m->insert_emit[i][j]); } fprintf(stdout,"\n"); } fprintf(stdout,"\n"); LOG_MSG("Transition:"); for(int i = 0; i < m->len;i++){ fprintf(stdout," %4d ", i); for(int j = 0; j < 7;j++){ fprintf(stdout,"%0.2f ", m->transition[i][j]); } fprintf(stdout,"\n"); } fprintf(stdout,"\n"); return OK; ERROR: return FAIL; } int hmm_alloc(struct hmm **hmm, int len, int seed, int dna) { struct hmm* n = NULL; ASSERT(len >0 ,"len has to be > 0"); MMALLOC(n, sizeof(struct hmm)); n->match_emit = NULL; n->insert_emit = NULL; n->transition = NULL; n->rng = init_rng(seed); if(!dna){ n->L = 20; }else{ n->L = 4; } n->len = len; galloc(&n->match_emit,n->len, n->L); galloc(&n->insert_emit,n->len, n->L); galloc(&n->transition, n->len, 7); *hmm = n; return OK; ERROR: return FAIL; } void hmm_free(struct hmm *n) { if(n){ gfree(n->match_emit); gfree(n->insert_emit); gfree(n->transition); free_rng(n->rng); MFREE(n); } } int hmm_param_init(struct hmm_param **param) { struct hmm_param* p = NULL; MMALLOC(p, sizeof(struct hmm_param)); p->insert_err_p = 0.25; p->match_err_p = 0.05; p->indel_p = 0.04; p->n_sim_seq = 20; p->n_observed_seq = 10; p->len = 250; p->seed = 42; p->dna = 0; *param = p; return OK; ERROR: return FAIL; } void hmm_param_free(struct hmm_param *p) { if(p){ MFREE(p); } } kalign-3.5.1/tests/dssim.h000066400000000000000000000005521515023132300154140ustar00rootroot00000000000000#ifndef DSSIM_H #define DSSIM_H #include "kalign/kalign.h" #ifdef DSSIM_IMPORT #define EXTERN #else #define EXTERN extern #endif EXTERN int dssim_get_fasta(struct msa **msa, int n_seq, int n_obs, int dna,int len, int seed); /* EXTERN int dssim_get_fasta(struct msa** msa, int n_seq, int n_obs,int len,int seed); */ #undef DSSIM_IMPORT #undef EXTERN #endif kalign-3.5.1/tests/dssim_test.c000066400000000000000000000075311515023132300164520ustar00rootroot00000000000000#include "tldevel.h" #include "tlrng.h" #include "kalign/kalign.h" #include "msa_struct.h" #include "msa_sort.h" #include "msa_op.h" #include "dssim.h" int msa_restore_ord_seq_order(struct msa* m); int msa_remove_gaps(struct msa *m); /* int test_consistency(int num_tests, int numseq, int seed); */ int test_consistency(int num_tests, int numseq,int dna,int seed); void progress_bar(int c, int t, int w); int main(void) { LOG_MSG("DSSim - a simplistic sequence simulator."); LOG_MSG("Protein alignments - 20 seq"); RUN(test_consistency(1000,20,0,42)); LOG_MSG("Protein alignments - 1000 seq"); RUN(test_consistency(10,1000,0,42)); LOG_MSG("Protein alignments - 10000 seq"); RUN(test_consistency(2,2000,0,42)); LOG_MSG("DNA alignments - 20 seq"); RUN(test_consistency(1000,20,1,42)); LOG_MSG("DNA alignments - 1000 seq"); RUN(test_consistency(10,1000,1,42)); LOG_MSG("DNA alignments - 10000 seq"); RUN(test_consistency(2,2000,1,42)); return EXIT_SUCCESS; ERROR: return EXIT_FAILURE; } int test_consistency(int num_tests, int numseq,int dna,int seed) { struct msa* m = NULL; struct msa* m2 = NULL; struct rng_state* rng = NULL; RUNP(rng = init_rng(seed)); for(int i = 0; i < num_tests;i++){ int local_seed = tl_random_int(rng, 1000000); int t1 = tl_random_int(rng, 8) + 1; int t2 = tl_random_int(rng, 8) + 1; while(t1 == t2){ t2 = tl_random_int(rng, 8) + 1; } float score = 0.0; dssim_get_fasta(&m, numseq, 10,dna, 45, local_seed); m->quiet = 1; msa_cpy(&m2, m); msa_shuffle_seq(m, rng); msa_shuffle_seq(m2, rng); kalign_run(m, t1, KALIGN_TYPE_UNDEFINED,0.0,0.0,0.0, 0, 0); kalign_run(m2, t2, KALIGN_TYPE_UNDEFINED,0.0,0.0,0.0, 0, 0); kalign_msa_compare(m, m2, &score); if(score != 100.0f){ LOG_MSG("Testing %d : %d %d %f", i , t1 ,t2,score); kalign_write_msa(m, NULL, "msf"); } kalign_free_msa(m); kalign_free_msa(m2); m = NULL; m2 = NULL; progress_bar(i, num_tests, 50); } progress_bar(num_tests, num_tests, 50); fprintf(stdout,"\n"); free_rng(rng); return OK; ERROR: return FAIL; } int msa_remove_gaps(struct msa *m) { int aln_len = m->alnlen; for(int i = 0; i < m->numseq;i++){ struct msa_seq* s = m->sequences[i]; int c = 0; for(int j = 0; j < aln_len;j++){ if(s->seq[j] != '-'){ s->seq[c] = s->seq[j]; c++; } } s->len = c; m->sequences[i]->rank = atoi(m->sequences[i]->name); } dealign_msa(m); return OK; } int msa_restore_ord_seq_order(struct msa* m) { for(int i = 0; i < m->numseq;i++){ m->sequences[i]->rank = atoi(m->sequences[i]->name); } RUN(msa_sort_rank(m)); return OK; ERROR: return FAIL; } void progress_bar(int c, int t, int w) { double progress = (double) c / (double) t; int l = (int)round(progress * (double) w); fprintf(stdout,"\r["); for(int i = 0; i < w;i++){ if(i < l){ fprintf(stdout,"="); }else{ fprintf(stdout," "); } } fprintf(stdout,"] %3d%%", (int)round(progress * 100.0)); fflush(stdout); } kalign-3.5.1/tests/kalign_api_test.c000066400000000000000000000552431515023132300174340ustar00rootroot00000000000000/* * kalign_api_test.c — comprehensive tests for the kalign public C API. * * Covers the functions not exercised by the existing test suite: * - kalign_run_seeded() (VSM, consistency, tree seed/noise) * - kalign_run_dist_scale() (VSM, seq_weights) * - kalign_run_realign() (realign iterations) * - kalign_post_realign() (post-align realign) * - kalign_run() with refine modes * - kalign_msa_compare_detailed() * - kalign_msa_compare_with_mask() * - kalign_check_msa() * - reformat_settings_msa() * - kalign_write_msa() round-trip fasta * * Each test: * 1. Reads input from the file passed as argv[1] * 2. Calls the API function * 3. Asserts structural correctness (alnlen>0, all seqs same length, * non-gap chars preserved, scores in range) * 4. Returns 0 on success, -1 on failure * * Exit code: EXIT_SUCCESS if all pass, EXIT_FAILURE otherwise. */ #include #include #include #include #include "kalign/kalign.h" #include "msa_struct.h" #include "msa_cmp.h" /* ------------------------------------------------------------------ */ /* helpers */ /* ------------------------------------------------------------------ */ /* Count non-gap characters in an aligned sequence string. */ static int count_residues(const char *seq) { int n = 0; for (int i = 0; seq[i]; i++) { if (seq[i] != '-') n++; } return n; } /* Record the ungapped lengths of all sequences before alignment. */ static int *snapshot_lengths(struct msa *m) { int *lens = malloc(sizeof(int) * m->numseq); if (!lens) return NULL; for (int i = 0; i < m->numseq; i++) { lens[i] = m->sequences[i]->len; } return lens; } /* Verify basic alignment invariants: * - alnlen > 0 * - every seq string has length == alnlen * - every seq preserves its original residue count * Returns 0 on success, -1 on failure. */ static int verify_alignment(struct msa *m, int *orig_lens, const char *label) { if (m->alnlen <= 0) { fprintf(stderr, " [%s] FAIL: alnlen=%d\n", label, m->alnlen); return -1; } for (int i = 0; i < m->numseq; i++) { if (m->sequences[i]->seq == NULL) { fprintf(stderr, " [%s] FAIL: seq %d is NULL\n", label, i); return -1; } int slen = (int)strlen(m->sequences[i]->seq); if (slen != m->alnlen) { fprintf(stderr, " [%s] FAIL: seq %d strlen=%d != alnlen=%d\n", label, i, slen, m->alnlen); return -1; } if (orig_lens) { int res = count_residues(m->sequences[i]->seq); if (res != orig_lens[i]) { fprintf(stderr, " [%s] FAIL: seq %d residues=%d != orig=%d\n", label, i, res, orig_lens[i]); return -1; } } } return 0; } /* Read input, returning a fresh MSA. Caller must free with kalign_free_msa. */ static struct msa *read_input(const char *path) { struct msa *m = NULL; if (kalign_read_input((char *)path, &m, 1) != 0 || m == NULL) { fprintf(stderr, " ERROR: cannot read %s\n", path); return NULL; } return m; } /* ------------------------------------------------------------------ */ /* individual test functions */ /* ------------------------------------------------------------------ */ static int test_run_with_refine(const char *input) { /* Test KALIGN_REFINE_ALL and KALIGN_REFINE_CONFIDENT */ int modes[] = {KALIGN_REFINE_ALL, KALIGN_REFINE_CONFIDENT}; const char *names[] = {"REFINE_ALL", "REFINE_CONFIDENT"}; for (int m = 0; m < 2; m++) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); int rv = kalign_run(msa, 1, -1, -1, -1, -1, modes[m], 0); if (rv != 0) { fprintf(stderr, " kalign_run(%s) returned %d\n", names[m], rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, names[m]) != 0) { free(lens); kalign_free_msa(msa); return -1; } fprintf(stdout, " %s: OK (alnlen=%d)\n", names[m], msa->alnlen); free(lens); kalign_free_msa(msa); } return 0; } static int test_run_dist_scale(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); /* vsm_amax=2.0, seq_weights=1.0 */ int rv = kalign_run_dist_scale(msa, 1, -1, -1, -1, -1, 0, 0, 0.0f, 2.0f, 1.0f); if (rv != 0) { fprintf(stderr, " kalign_run_dist_scale returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, "dist_scale") != 0) { free(lens); kalign_free_msa(msa); return -1; } fprintf(stdout, " vsm_amax=2.0, seq_weights=1.0: OK (alnlen=%d)\n", msa->alnlen); free(lens); kalign_free_msa(msa); return 0; } static int test_run_seeded(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); /* tree_seed=42, tree_noise=0, vsm_amax=2.0, consistency_anchors=5 */ int rv = kalign_run_seeded(msa, 1, -1, -1, -1, -1, 0, 0, 42, 0.0f, 0.0f, 2.0f, 0.0f, 5, 2.0f); if (rv != 0) { fprintf(stderr, " kalign_run_seeded returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, "seeded") != 0) { free(lens); kalign_free_msa(msa); return -1; } fprintf(stdout, " seeded + consistency: OK (alnlen=%d)\n", msa->alnlen); free(lens); kalign_free_msa(msa); return 0; } static int test_run_realign(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); /* realign_iterations=1, vsm_amax=2.0 */ int rv = kalign_run_realign(msa, 1, -1, -1, -1, -1, 0, 0, 0.0f, 2.0f, 1, 0.0f, 0, 2.0f); if (rv != 0) { fprintf(stderr, " kalign_run_realign returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, "realign") != 0) { free(lens); kalign_free_msa(msa); return -1; } fprintf(stdout, " realign=1, vsm=2.0: OK (alnlen=%d)\n", msa->alnlen); free(lens); kalign_free_msa(msa); return 0; } static int test_post_realign(const char *input) { /* First do a normal alignment, then post-realign the result */ struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); if (rv != 0) { fprintf(stderr, " initial kalign_run returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } rv = kalign_post_realign(msa, 1, -1, -1, -1, -1, 0, 0, 0.0f, 0.0f, 1, 0.0f); if (rv != 0) { fprintf(stderr, " kalign_post_realign returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, "post_realign") != 0) { free(lens); kalign_free_msa(msa); return -1; } fprintf(stdout, " post_realign: OK (alnlen=%d)\n", msa->alnlen); free(lens); kalign_free_msa(msa); return 0; } static int test_ensemble_with_realign(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); /* ensemble=3, vsm=2.0, realign=1, refine=CONFIDENT */ int rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 0, NULL, KALIGN_REFINE_CONFIDENT, 0.0f, 2.0f, 1, 0.0f, 0, 2.0f); if (rv != 0) { fprintf(stderr, " kalign_ensemble+realign returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; } if (verify_alignment(msa, lens, "ens+realign") != 0) { free(lens); kalign_free_msa(msa); return -1; } /* Also check col_confidence */ if (msa->col_confidence == NULL) { fprintf(stderr, " [ens+realign] FAIL: col_confidence is NULL\n"); free(lens); kalign_free_msa(msa); return -1; } for (int i = 0; i < msa->alnlen; i++) { if (msa->col_confidence[i] < 0.0f || msa->col_confidence[i] > 1.0f) { fprintf(stderr, " [ens+realign] FAIL: col_confidence[%d]=%.3f\n", i, msa->col_confidence[i]); free(lens); kalign_free_msa(msa); return -1; } } fprintf(stdout, " ensemble+realign+refine: OK (alnlen=%d)\n", msa->alnlen); free(lens); kalign_free_msa(msa); return 0; } static int test_compare_detailed(const char *input) { /* Align, then compare to self — should get perfect scores */ struct msa *ref = read_input(input); struct msa *test_msa = read_input(input); if (!ref || !test_msa) return -1; kalign_run(ref, 1, -1, -1, -1, -1, 0, 0); kalign_run(test_msa, 1, -1, -1, -1, -1, 0, 0); struct poar_score out; memset(&out, 0, sizeof(out)); int rv = kalign_msa_compare_detailed(ref, test_msa, -1.0f, &out); if (rv != 0) { fprintf(stderr, " kalign_msa_compare_detailed returned %d\n", rv); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } /* Self-comparison: recall, precision, f1 should all be 1.0 */ if (fabs(out.recall - 1.0) > 0.001) { fprintf(stderr, " FAIL: recall=%.4f (expected 1.0)\n", out.recall); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } if (fabs(out.precision - 1.0) > 0.001) { fprintf(stderr, " FAIL: precision=%.4f (expected 1.0)\n", out.precision); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } if (fabs(out.f1 - 1.0) > 0.001) { fprintf(stderr, " FAIL: f1=%.4f (expected 1.0)\n", out.f1); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } if (fabs(out.tc - 1.0) > 0.001) { fprintf(stderr, " FAIL: tc=%.4f (expected 1.0)\n", out.tc); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } if (out.ref_pairs <= 0 || out.test_pairs <= 0 || out.common <= 0) { fprintf(stderr, " FAIL: pair counts ref=%lld test=%lld common=%lld\n", (long long)out.ref_pairs, (long long)out.test_pairs, (long long)out.common); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } if (out.ref_pairs != out.common || out.test_pairs != out.common) { fprintf(stderr, " FAIL: self-compare pairs mismatch ref=%lld test=%lld common=%lld\n", (long long)out.ref_pairs, (long long)out.test_pairs, (long long)out.common); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } fprintf(stdout, " compare_detailed self: recall=%.3f prec=%.3f f1=%.3f tc=%.3f pairs=%lld OK\n", out.recall, out.precision, out.f1, out.tc, (long long)out.common); kalign_free_msa(ref); kalign_free_msa(test_msa); return 0; } static int test_compare_with_mask(const char *input) { struct msa *ref = read_input(input); struct msa *test_msa = read_input(input); if (!ref || !test_msa) return -1; kalign_run(ref, 1, -1, -1, -1, -1, 0, 0); kalign_run(test_msa, 1, -1, -1, -1, -1, 0, 0); /* Create a mask that includes all columns */ int n_cols = ref->alnlen; int *mask = malloc(sizeof(int) * n_cols); if (!mask) { kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } for (int i = 0; i < n_cols; i++) mask[i] = 1; struct poar_score out; memset(&out, 0, sizeof(out)); int rv = kalign_msa_compare_with_mask(ref, test_msa, mask, n_cols, &out); if (rv != 0) { fprintf(stderr, " kalign_msa_compare_with_mask returned %d\n", rv); free(mask); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } /* All columns masked in → same as full comparison → perfect scores */ if (fabs(out.recall - 1.0) > 0.001 || fabs(out.precision - 1.0) > 0.001) { fprintf(stderr, " FAIL: mask-all recall=%.4f prec=%.4f\n", out.recall, out.precision); free(mask); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } /* Now test with partial mask (first half only) */ for (int i = n_cols / 2; i < n_cols; i++) mask[i] = 0; memset(&out, 0, sizeof(out)); rv = kalign_msa_compare_with_mask(ref, test_msa, mask, n_cols, &out); if (rv != 0) { fprintf(stderr, " kalign_msa_compare_with_mask (partial) returned %d\n", rv); free(mask); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } /* Partial mask self-compare should still give perfect scores */ if (fabs(out.recall - 1.0) > 0.001) { fprintf(stderr, " FAIL: partial mask recall=%.4f\n", out.recall); free(mask); kalign_free_msa(ref); kalign_free_msa(test_msa); return -1; } fprintf(stdout, " compare_with_mask: full-mask OK, partial-mask OK\n"); free(mask); kalign_free_msa(ref); kalign_free_msa(test_msa); return 0; } static int test_check_msa(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; /* kalign_check_msa checks for duplicate sequences. * Our test files shouldn't have exact duplicates. */ msa->quiet = 1; int rv = kalign_check_msa(msa, 0); if (rv != 0) { fprintf(stderr, " kalign_check_msa returned %d\n", rv); kalign_free_msa(msa); return -1; } fprintf(stdout, " check_msa: OK (%d sequences validated)\n", msa->numseq); kalign_free_msa(msa); return 0; } static int test_reformat_settings(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; /* First align so we have gaps */ int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); if (rv != 0) { fprintf(stderr, " initial align failed\n"); kalign_free_msa(msa); return -1; } /* Test rename */ rv = reformat_settings_msa(msa, 1, 0); if (rv != 0) { fprintf(stderr, " reformat_settings_msa(rename) returned %d\n", rv); kalign_free_msa(msa); return -1; } /* Verify names were changed to SEQ1, SEQ2, etc. */ for (int i = 0; i < msa->numseq; i++) { char expected[32]; snprintf(expected, sizeof(expected), "SEQ%d", i + 1); if (strncmp(msa->sequences[i]->name, expected, strlen(expected)) != 0) { fprintf(stderr, " FAIL: seq %d name='%s' expected='%s'\n", i, msa->sequences[i]->name, expected); kalign_free_msa(msa); return -1; } } fprintf(stdout, " reformat rename: OK\n"); /* Test unalign — zeroes the gaps[] array and sets status to unaligned. * Note: dealign_msa operates on the internal gaps[] representation, * not on seq->seq (which holds finalised text with '-' chars). */ rv = reformat_settings_msa(msa, 0, 1); if (rv != 0) { fprintf(stderr, " reformat_settings_msa(unalign) returned %d\n", rv); kalign_free_msa(msa); return -1; } /* After unalign, all gaps[] entries should be zero */ for (int i = 0; i < msa->numseq; i++) { for (int j = 0; j <= msa->sequences[i]->len; j++) { if (msa->sequences[i]->gaps[j] != 0) { fprintf(stderr, " FAIL: seq %d gaps[%d]=%d after unalign\n", i, j, msa->sequences[i]->gaps[j]); kalign_free_msa(msa); return -1; } } } fprintf(stdout, " reformat unalign: OK (gaps[] zeroed)\n"); kalign_free_msa(msa); return 0; } static int test_write_roundtrip(const char *input) { /* Align, write to fasta, read back, compare */ struct msa *msa = read_input(input); if (!msa) return -1; int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); if (rv != 0) { kalign_free_msa(msa); return -1; } const char *tmpfile = "test_roundtrip.fa"; rv = kalign_write_msa(msa, (char *)tmpfile, "fasta"); if (rv != 0) { fprintf(stderr, " kalign_write_msa(fasta) returned %d\n", rv); kalign_free_msa(msa); return -1; } /* Read it back */ struct msa *msa2 = NULL; rv = kalign_read_input((char *)tmpfile, &msa2, 1); if (rv != 0 || msa2 == NULL) { fprintf(stderr, " failed to read back written fasta\n"); kalign_free_msa(msa); remove(tmpfile); return -1; } /* Compare: same number of sequences, same alignment content */ if (msa->numseq != msa2->numseq) { fprintf(stderr, " FAIL: numseq %d vs %d\n", msa->numseq, msa2->numseq); kalign_free_msa(msa); kalign_free_msa(msa2); remove(tmpfile); return -1; } /* Use kalign_msa_compare for a real score check */ float score = 0; rv = kalign_msa_compare(msa, msa2, &score); if (rv != 0) { fprintf(stderr, " kalign_msa_compare on roundtrip returned %d\n", rv); kalign_free_msa(msa); kalign_free_msa(msa2); remove(tmpfile); return -1; } if (fabsf(score - 100.0f) > 0.01f) { fprintf(stderr, " FAIL: roundtrip score=%.2f (expected 100.0)\n", score); kalign_free_msa(msa); kalign_free_msa(msa2); remove(tmpfile); return -1; } fprintf(stdout, " write fasta roundtrip: score=%.1f OK\n", score); kalign_free_msa(msa); kalign_free_msa(msa2); remove(tmpfile); return 0; } /* ------------------------------------------------------------------ */ /* main: run all tests */ /* ------------------------------------------------------------------ */ int main(int argc, char *argv[]) { if (argc <= 1) { fprintf(stderr, "Usage: %s \n", argv[0]); return EXIT_FAILURE; } const char *input = argv[1]; int ret = EXIT_SUCCESS; int pass = 0, fail = 0; struct { const char *name; int (*fn)(const char *); } tests[] = { {"kalign_run + refine", test_run_with_refine}, {"kalign_run_dist_scale (VSM)", test_run_dist_scale}, {"kalign_run_seeded (consistency)", test_run_seeded}, {"kalign_run_realign", test_run_realign}, {"kalign_post_realign", test_post_realign}, {"kalign_ensemble + realign", test_ensemble_with_realign}, {"kalign_msa_compare_detailed", test_compare_detailed}, {"kalign_msa_compare_with_mask", test_compare_with_mask}, {"kalign_check_msa", test_check_msa}, {"reformat_settings_msa", test_reformat_settings}, {"write fasta roundtrip", test_write_roundtrip}, }; int n_tests = (int)(sizeof(tests) / sizeof(tests[0])); fprintf(stdout, "=== Kalign API Tests (%d tests) ===\n\n", n_tests); for (int i = 0; i < n_tests; i++) { fprintf(stdout, "Test %d/%d: %s\n", i + 1, n_tests, tests[i].name); if (tests[i].fn(input) != 0) { fprintf(stdout, " FAIL\n\n"); fail++; ret = EXIT_FAILURE; } else { fprintf(stdout, " PASS\n\n"); pass++; } } fprintf(stdout, "=== Results: %d passed, %d failed (of %d) ===\n", pass, fail, n_tests); return ret; } kalign-3.5.1/tests/kalign_cmp_test.c000066400000000000000000000011041515023132300174250ustar00rootroot00000000000000#include "stdio.h" #include "stdlib.h" #include "kalign/kalign.h" int main(int argc, char *argv[]) { struct msa* r = NULL; struct msa* t = NULL; fprintf(stdout,"inputs: %d\n", argc); if(argc == 3){ kalign_read_input(argv[1], &r,1); kalign_read_input(argv[2], &t,1); float score = 0.0; kalign_msa_compare(r, t, &score); kalign_free_msa(r); kalign_free_msa(t); fprintf(stdout,"Aln score : %f\n", score); } return 0; } kalign-3.5.1/tests/kalign_ensemble_test.c000066400000000000000000000254221515023132300204510ustar00rootroot00000000000000#include #include #include #include "kalign/kalign.h" #include "msa_struct.h" static int test_ensemble_confidence(const char *input_file); static int test_poar_round_trip(const char *input_file); static int test_min_support(const char *input_file); int main(int argc, char *argv[]) { int ret = EXIT_SUCCESS; if(argc <= 1){ fprintf(stderr, "Usage: %s \n", argv[0]); return EXIT_FAILURE; } const char *input_file = argv[1]; fprintf(stdout, "=== Kalign Ensemble Tests ===\n\n"); fprintf(stdout, "Test 1: Ensemble with confidence\n"); if(test_ensemble_confidence(input_file) != 0){ fprintf(stdout, " FAIL\n\n"); ret = EXIT_FAILURE; }else{ fprintf(stdout, " PASS\n\n"); } fprintf(stdout, "Test 2: POAR round-trip\n"); if(test_poar_round_trip(input_file) != 0){ fprintf(stdout, " FAIL\n\n"); ret = EXIT_FAILURE; }else{ fprintf(stdout, " PASS\n\n"); } fprintf(stdout, "Test 3: min_support parameter\n"); if(test_min_support(input_file) != 0){ fprintf(stdout, " FAIL\n\n"); ret = EXIT_FAILURE; }else{ fprintf(stdout, " PASS\n\n"); } if(ret == EXIT_SUCCESS){ fprintf(stdout, "All ensemble tests passed.\n"); }else{ fprintf(stderr, "Some ensemble tests FAILED.\n"); } return ret; } /* Test 1: Run ensemble alignment with n_runs=3 and verify that * col_confidence is populated with values in [0, 1]. */ static int test_ensemble_confidence(const char *input_file) { struct msa *msa = NULL; int rv; rv = kalign_read_input((char *)input_file, &msa, 1); if(rv != 0 || msa == NULL){ fprintf(stderr, " ERROR: failed to read input file: %s\n", input_file); return -1; } /* n_runs=3, default gap penalties, seed=42, min_support=0 (auto), no POAR save */ rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 0, NULL, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); if(rv != 0){ fprintf(stderr, " ERROR: kalign_ensemble returned %d\n", rv); kalign_free_msa(msa); return -1; } /* Check that col_confidence was allocated */ if(msa->col_confidence == NULL){ fprintf(stderr, " ERROR: col_confidence is NULL after ensemble\n"); kalign_free_msa(msa); return -1; } /* Verify alignment length is positive */ if(msa->alnlen <= 0){ fprintf(stderr, " ERROR: alnlen is %d (expected > 0)\n", msa->alnlen); kalign_free_msa(msa); return -1; } /* Check that all col_confidence values are in [0, 1] */ for(int i = 0; i < msa->alnlen; i++){ float c = msa->col_confidence[i]; if(c < 0.0f || c > 1.0f){ fprintf(stderr, " ERROR: col_confidence[%d] = %f out of range [0,1]\n", i, c); kalign_free_msa(msa); return -1; } } fprintf(stdout, " col_confidence: %d values, all in [0,1]\n", msa->alnlen); /* Also verify that sequences are aligned (have equal length = alnlen) */ for(int i = 0; i < msa->numseq; i++){ if(msa->sequences[i]->seq == NULL){ fprintf(stderr, " ERROR: sequence %d has NULL seq\n", i); kalign_free_msa(msa); return -1; } int slen = (int)strlen(msa->sequences[i]->seq); if(slen != msa->alnlen){ fprintf(stderr, " ERROR: sequence %d length %d != alnlen %d\n", i, slen, msa->alnlen); kalign_free_msa(msa); return -1; } } fprintf(stdout, " %d sequences aligned to length %d\n", msa->numseq, msa->alnlen); kalign_free_msa(msa); return 0; } /* Test 2: Run ensemble with save_poar, then load POAR with * kalign_consensus_from_poar and verify both produce aligned output. */ static int test_poar_round_trip(const char *input_file) { struct msa *msa1 = NULL; struct msa *msa2 = NULL; int rv; const char *poar_path = "test_ensemble_poar.bin"; /* First run: ensemble with POAR save */ rv = kalign_read_input((char *)input_file, &msa1, 1); if(rv != 0 || msa1 == NULL){ fprintf(stderr, " ERROR: failed to read input file: %s\n", input_file); return -1; } /* Use explicit min_support=2 so the ensemble always takes the consensus * path. kalign_consensus_from_poar() also requires min_support >= 1, * and both must use the same threshold for the output to match. */ rv = kalign_ensemble(msa1, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 2, poar_path, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); if(rv != 0){ fprintf(stderr, " ERROR: kalign_ensemble (save) returned %d\n", rv); kalign_free_msa(msa1); return -1; } /* Verify the POAR file was created */ FILE *fp = fopen(poar_path, "rb"); if(fp == NULL){ fprintf(stderr, " ERROR: POAR file was not created: %s\n", poar_path); kalign_free_msa(msa1); return -1; } fclose(fp); fprintf(stdout, " POAR saved to %s\n", poar_path); /* Second run: load POAR and derive consensus */ rv = kalign_read_input((char *)input_file, &msa2, 1); if(rv != 0 || msa2 == NULL){ fprintf(stderr, " ERROR: failed to read input for POAR load\n"); kalign_free_msa(msa1); return -1; } rv = kalign_consensus_from_poar(msa2, poar_path, 2); if(rv != 0){ fprintf(stderr, " ERROR: kalign_consensus_from_poar returned %d\n", rv); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } /* Verify both MSAs have valid aligned sequences */ if(msa1->alnlen <= 0){ fprintf(stderr, " ERROR: msa1 alnlen = %d\n", msa1->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } if(msa2->alnlen <= 0){ fprintf(stderr, " ERROR: msa2 alnlen = %d\n", msa2->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } /* Both should have the same number of sequences */ if(msa1->numseq != msa2->numseq){ fprintf(stderr, " ERROR: numseq mismatch: %d vs %d\n", msa1->numseq, msa2->numseq); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } /* Both should have the same alignment length (same POAR, same consensus) */ if(msa1->alnlen != msa2->alnlen){ fprintf(stderr, " ERROR: alnlen mismatch: %d vs %d\n", msa1->alnlen, msa2->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } /* Verify aligned sequences match between direct ensemble and POAR-loaded consensus */ for(int i = 0; i < msa1->numseq; i++){ if(msa1->sequences[i]->seq == NULL || msa2->sequences[i]->seq == NULL){ fprintf(stderr, " ERROR: NULL seq at index %d\n", i); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } if(strcmp(msa1->sequences[i]->seq, msa2->sequences[i]->seq) != 0){ fprintf(stderr, " ERROR: sequence %d mismatch between ensemble and POAR load\n", i); fprintf(stderr, " direct: %.60s...\n", msa1->sequences[i]->seq); fprintf(stderr, " loaded: %.60s...\n", msa2->sequences[i]->seq); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } } fprintf(stdout, " Round-trip: %d sequences, alnlen=%d, consensus matches\n", msa1->numseq, msa1->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); /* Clean up temp file */ remove(poar_path); return 0; } /* Test 3: Run ensemble with explicit min_support=2 and verify it completes. */ static int test_min_support(const char *input_file) { struct msa *msa = NULL; int rv; rv = kalign_read_input((char *)input_file, &msa, 1); if(rv != 0 || msa == NULL){ fprintf(stderr, " ERROR: failed to read input file: %s\n", input_file); return -1; } /* min_support=2 (explicit), n_runs=3 */ rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 2, NULL, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); if(rv != 0){ fprintf(stderr, " ERROR: kalign_ensemble with min_support=2 returned %d\n", rv); kalign_free_msa(msa); return -1; } /* Verify alignment was produced */ if(msa->alnlen <= 0){ fprintf(stderr, " ERROR: alnlen is %d (expected > 0)\n", msa->alnlen); kalign_free_msa(msa); return -1; } /* Verify sequences are aligned */ for(int i = 0; i < msa->numseq; i++){ if(msa->sequences[i]->seq == NULL){ fprintf(stderr, " ERROR: sequence %d has NULL seq\n", i); kalign_free_msa(msa); return -1; } int slen = (int)strlen(msa->sequences[i]->seq); if(slen != msa->alnlen){ fprintf(stderr, " ERROR: sequence %d length %d != alnlen %d\n", i, slen, msa->alnlen); kalign_free_msa(msa); return -1; } } /* Verify col_confidence is populated */ if(msa->col_confidence == NULL){ fprintf(stderr, " ERROR: col_confidence is NULL with min_support=2\n"); kalign_free_msa(msa); return -1; } fprintf(stdout, " min_support=2: %d sequences aligned to length %d\n", msa->numseq, msa->alnlen); kalign_free_msa(msa); return 0; } kalign-3.5.1/tests/kalign_io_test.c000066400000000000000000000007361515023132300172670ustar00rootroot00000000000000#include "stdio.h" #include "kalign/kalign.h" int main(int argc, char *argv[]) { struct msa* msa = NULL; fprintf(stdout,"inputs: %d\n", argc); for(int i = 1; i < argc;i++){ fprintf(stdout,"reading from %s\n", argv[i]); kalign_read_input(argv[i], &msa,1); } kalign_write_msa(msa, "test.clu", "clu"); kalign_write_msa(msa, "test.fa", "fasta"); kalign_free_msa(msa); return 0; } kalign-3.5.1/tests/kalign_lib_test.c000066400000000000000000000044651515023132300174310ustar00rootroot00000000000000#include #include #include "kalign/kalign.h" int test_2(void); int main(int argc, char *argv[]) { struct msa* msa = NULL; if(argc <= 1){ fprintf(stdout,"no inputs\n"); return EXIT_SUCCESS; } for(int i = 1; i < argc;i++){ fprintf(stdout,"reading from %s\n", argv[i]); kalign_read_input(argv[i], &msa,1); } /* Align seqences */ kalign_run(msa,1 , -1, -1, -1 , -1, 0, 0); /* write alignment in clustal format */ kalign_write_msa(msa, "test.clu", "clu"); /* write alignment in aligned fasta format */ kalign_write_msa(msa, "test.fa", "fasta"); /* cleaning up */ kalign_free_msa(msa); test_2(); return EXIT_SUCCESS; } int test_2(void){ // Initialize array char* inseq[] = { "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTTGCATGCATGCATGCATGCATGCATGCA", "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTTGCATGCATGCATGCATGCATGCATGCA", "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTTGCATGCATGCATGCATGCATGCATGCA", "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT" }; int numseq = 4; int L[] = {76, 76, 76, 48}; char** aln = NULL; int aln_len = 0; kalign(inseq, L, numseq, 4 , KALIGN_TYPE_DNA_INTERNAL, -1, -1 , -1, &aln, &aln_len); printf("Aligned:\n"); for(int i = 0; i < numseq; i++){ printf("%s\n", aln[i]); /* printf("%s\n", inseq[i]); */ } for (int i = 0; i < numseq; ++i) { int num_aligned = 0; for (int j = 0; j < aln_len; ++j) { if (aln[i][j] != '-' && aln[i][j] != '\0') { num_aligned++; } } if (num_aligned != L[i]) { fprintf(stderr, "error: sequence %d is not fully aligned\n", i); fprintf(stderr, "%s\n", inseq[i]); fprintf(stderr, "%s\n", aln[i]); } } /* Free alignment */ for(int i = 0; i < numseq;i++){ free(aln[i]); } free(aln); return 0; } kalign-3.5.1/tests/kalign_lib_testCXX.cpp000066400000000000000000000023301515023132300203410ustar00rootroot00000000000000 #include #include #include #include "kalign/kalign.h" int main() { // Initialize array char * inseq[95] = { "GKGDPKKPRGKMSSYAFFVQTSREEHKKKHPDASVNFSEFSKKCSERWKTMSAKEKGKFEDMAKADKARYEREMKTYIPPKGE", "MQDRVKRPMNAFIVWSRDQRRKMALENPRMRNSEISKQLGYQWKMLTEAEKWPFFQEAQKLQAMHREKYPNYKYRPRRKAKMLPK", "MKKLKKHPDFPKKPLTPYFRFFMEKRAKYAKLHPEMSNLDLTKILSKKYKELPEKKKMKYIQDFQREKQEFERNLARFREDHPDLIQNAKK", "MHIKKPLNAFMLYMKEMRANVVAESTLKESAAINQILGRRWHALSREEQAKYYELARKERQLHMQLYPGWSARDNYGKKKKRKREK", }; int numseq = 4; int* L = new int[numseq]; L[0] = 83; L[1] = 85; L[2] = 91; L[3] = 86; char** aln = NULL; int aln_len = 0; kalign(inseq,L, numseq,4 , KALIGN_TYPE_PROTEIN, -1, -1 , -1, &aln, &aln_len); std::cout << "Aligned:\n"; for(int i = 0; i < numseq;i++){ std::cout << aln[i] << "\n"; } /* Free alignment */ for(int i = 0; i < numseq;i++){ // delete[] aln[i]; free(aln[i]); } free(aln); delete[] L; } kalign-3.5.1/tests/large_benchmark.c000066400000000000000000000311541515023132300173760ustar00rootroot00000000000000#include "tldevel.h" #include "tlmisc.h" #include #include #include #include #include #include #include #include #include "kalign/kalign.h" #define MAX_PATH_LEN 4096 struct parameters { char* target_dir; int verbose; int dryrun; int help_flag; }; struct aln_case { char* dir; char* name; float score; int alloc_len; int valid; }; struct bench_collection { struct aln_case** l; int n; int n_alloc; }; static int run_test_aln(struct aln_case *tcase); static int bench_collection_add_case(struct bench_collection* b, char* dir, char* name); static int bench_collection_alloc(struct bench_collection **bench_collection, int n); static int bench_collection_resize(struct bench_collection *b); static void bench_collection_free(struct bench_collection *b); static int aln_case_alloc(struct aln_case **aln_case); static void aln_case_free(struct aln_case *a); static int run_bench(struct parameters *p); static int process_dir(struct parameters *p,char* dir,struct bench_collection* b); static int detect_likely_alignment(char *name, int *is_aln); static int param_sanity_check(struct parameters *p); static int print_help(char * argv[]); static int param_print(struct parameters *p); static int param_init(struct parameters **param); static void param_free(struct parameters *p); static int local_dir_exists(const char* name); #define OPT_VERBOSE 1 #define OPT_DRY 2 int main(int argc, char *argv[]) { struct parameters* param = NULL; int c; RUN(param_init(¶m)); while (1){ static struct option long_options[] ={ {"dir", required_argument, 0, 'd'}, {"verbose", no_argument,0, OPT_VERBOSE}, {"dry-run", no_argument,0,OPT_DRY}, {"help", no_argument,0,'h'}, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long_only (argc, argv,"d:h",long_options, &option_index); /* Detect the end of the options. */ if (c == -1){ break; } switch(c) { case OPT_DRY: param->dryrun = 1; break; case OPT_VERBOSE: param->verbose = 1; break; case 'd': param->target_dir = optarg; break; case 'h': param->help_flag = 1; break; default: abort (); } } if(param->help_flag || argc < 2){ RUN(print_help(argv)); param_free(param); return EXIT_SUCCESS; } RUN(param_sanity_check(param)); RUN(param_print(param)); RUN(run_bench(param)); param_free(param); return EXIT_SUCCESS; ERROR: param_free(param); return EXIT_FAILURE; } int run_bench(struct parameters *p) { struct bench_collection* b = NULL; ASSERT(p != NULL,"No parameters"); RUN(bench_collection_alloc(&b, 1024)); RUN(process_dir(p, p->target_dir,b)); for(int i = 0; i < b->n;i++){ struct aln_case *c = NULL; c = b->l[i]; if(p->dryrun){ LOG_MSG("DryRun: would run kalign on: %s",c->name ); }else{ RUN(run_test_aln(c)); if(p->verbose){ LOG_MSG("Case %d:\t%-*s\t%d\t%f ",i, 20,c->name,c->valid, c->score); } } } fprintf(stdout,"Alignment,Score\n"); for(int i = 0; i < b->n;i++){ struct aln_case *c = NULL; c = b->l[i]; if(c->valid){ fprintf(stdout,"%s,%f\n",c->name,c->score); } } bench_collection_free(b); return OK; ERROR: bench_collection_free(b); return FAIL; } int run_test_aln(struct aln_case *tcase) { char path[MAX_PATH_LEN]; struct msa* r = NULL; struct msa* t = NULL; snprintf(path, MAX_PATH_LEN, "%s/%s",tcase->dir,tcase->name); RUN(kalign_read_input(path, &t,1)); if(!t){ kalign_free_msa(t); tcase->valid = 0; return OK; } /* reference */ RUN(kalign_read_input(path, &r,1)); kalign_run(t,16 , -1, -1, -1 , -1, 0, 0); kalign_msa_compare(r, t, &tcase->score); /* if(tcase->score >= 0.5){ */ /* LOG_MSG("Ref:"); */ /* kalign_write_msa(r, NULL , "clu"); */ /* LOG_MSG("test:"); */ /* kalign_write_msa(t, NULL , "clu"); */ /* } */ kalign_free_msa(r); kalign_free_msa(t); return OK; ERROR: kalign_free_msa(r); kalign_free_msa(t); return FAIL; } int process_dir(struct parameters *p,char* dir,struct bench_collection* b) { struct dirent *dp; DIR *d_ptr; if ((d_ptr = opendir(dir)) == NULL){ ERROR_MSG("Can't open %s", dir); } while ((dp = readdir(d_ptr)) != NULL){ /* struct stat stbuf ; */ int is_aln = 0; if(dp->d_type == DT_DIR){ if(dp->d_name[0] != '.'){ char* tmp = NULL; MMALLOC(tmp, sizeof(char) * MAX_PATH_LEN); snprintf(tmp,MAX_PATH_LEN,"%s/%s", dir, dp->d_name); RUN(process_dir(p, tmp,b)); MFREE(tmp); } }else if(dp->d_type == DT_REG){ /* LOG_MSG("File: %s %s",dir, dp->d_name); */ char* tmp = NULL; MMALLOC(tmp, sizeof(char) * MAX_PATH_LEN); snprintf(tmp,MAX_PATH_LEN,"%s/%s", dir, dp->d_name); detect_likely_alignment(tmp, &is_aln); if(is_aln){ RUN(bench_collection_add_case(b, dir, dp->d_name)); } MFREE(tmp); }else if(dp->d_type == DT_LNK){ /* LOG_MSG("LINK: %s %s",dir, dp->d_name); */ char* tmp = NULL; char* rp = NULL; MMALLOC(tmp, sizeof(char) * MAX_PATH_LEN); snprintf(tmp,MAX_PATH_LEN,"%s/%s", dir, dp->d_name); rp = realpath(tmp, NULL); if(rp!= NULL){ /* LOG_MSG("resolved: %s", rp); */ detect_likely_alignment(rp, &is_aln); if(is_aln){ RUN(bench_collection_add_case(b, dir, dp->d_name)); } free(rp); } MFREE(tmp); } } closedir(d_ptr); return OK; ERROR: if(d_ptr){ closedir(d_ptr); } return FAIL; } int detect_likely_alignment(char *name, int *is_aln) { int len = strnlen(name,MAX_PATH_LEN); int ret = 0; if(strncmp(name + len - 4, ".msf", 4) == 0){ /* LOG_MSG("%s is aln", name); */ ret = 1; }else if(strncmp(name + len - 4, ".vie", 4) == 0){ /* LOG_MSG("%s is aln", name); */ ret = 1; } *is_aln = ret; return OK; } int print_help(char * argv[]) { const char usage[] = " -d "; char* basename = NULL; RUN(tlfilename(argv[0], &basename)); fprintf(stdout,"\nUsage: %s %s\n\n",basename ,usage); fprintf(stdout,"Options:\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-d","Target directory" ,"[]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-verbose","Target directory" ,"[off]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-dry-run","Target directory" ,"[off]" ); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-h/--help","Print help." ,"[off]" ); fprintf(stdout,"\n\n"); if(basename){ MFREE(basename); } return OK; ERROR: if(basename){ MFREE(basename); } return FAIL; } int param_print(struct parameters *p) { fprintf(stderr,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"-d", p->target_dir); fprintf(stderr,"%*s%-*s: %d\n",3,"",MESSAGE_MARGIN-3,"-verbose", p->verbose); fprintf(stderr,"%*s%-*s: %d\n",3,"",MESSAGE_MARGIN-3,"-dry-run", p->dryrun); return OK; } int param_sanity_check(struct parameters *p) { ASSERT(p->target_dir != NULL, " No target dir use -d .... "); RUN(local_dir_exists(p->target_dir)); return OK; ERROR: return FAIL; } int local_dir_exists(const char* name) { ASSERT(name != NULL," No directory name!"); DIR* dir = opendir(name); if (dir) { /* Directory exists. */ closedir(dir); } else if (ENOENT == errno) { ERROR_MSG("Directory %s does not exist!", name); /* Directory does not exist. */ } else { ERROR_MSG("Opening directory %s failed!", name); /* opendir() failed for some other reason. */ } return OK; ERROR: return FAIL; } int param_init(struct parameters **param) { struct parameters* p = NULL; MMALLOC(p, sizeof(struct parameters)); p->target_dir = NULL; p->verbose = 0; p->dryrun = 0; p->help_flag = 0; *param = p; return OK; ERROR: param_free(p); return FAIL; } void param_free(struct parameters *p) { if(p){ MFREE(p); } } int bench_collection_add_case(struct bench_collection* b, char* dir, char* name) { struct aln_case* a = NULL; int idx = 0; idx = b->n; a = b->l[idx]; strncpy(a->name, name, a->alloc_len); strncpy(a->dir, dir, a->alloc_len); b->n++; if(b->n == b->n_alloc){ RUN(bench_collection_resize(b)); } return OK; ERROR: return FAIL; } int bench_collection_alloc(struct bench_collection **bench_collection, int n) { struct bench_collection* b = NULL; MMALLOC(b, sizeof(struct bench_collection)); b->n = 0; b->n_alloc = n; b->l = NULL; MMALLOC(b->l, sizeof(struct aln_cases*) * b->n_alloc); for(int i = 0; i < b->n_alloc;i++){ b->l[i] = NULL; RUN(aln_case_alloc(&b->l[i])); } *bench_collection = b; return OK; ERROR: return FAIL; } int bench_collection_resize(struct bench_collection *b) { int old_size = b->n_alloc; int new_size = b->n_alloc + b->n_alloc / 2; MREALLOC(b->l, sizeof(struct aln_cases*) * new_size); for(int i = old_size; i < new_size;i++){ b->l[i] = NULL; RUN(aln_case_alloc(&b->l[i])); } b->n_alloc = new_size; return OK; ERROR: return FAIL; } void bench_collection_free(struct bench_collection *b) { if(b){ for(int i = 0; i < b->n_alloc;i++){ aln_case_free(b->l[i]); } MFREE(b->l); MFREE(b); } } int aln_case_alloc(struct aln_case **aln_case) { struct aln_case* a = NULL; MMALLOC(a, sizeof(struct aln_case)); a->name = NULL; a->dir = NULL; a->alloc_len = MAX_PATH_LEN; a->valid = 1; MMALLOC(a->dir, sizeof(char) * a->alloc_len); MMALLOC(a->name, sizeof(char) * a->alloc_len); a->score = -1.0; *aln_case = a; return OK; ERROR: aln_case_free(a); return FAIL; } void aln_case_free(struct aln_case *a) { if(a){ if(a->dir){ MFREE(a->dir); } if(a->name){ MFREE(a->name); } MFREE(a); } } kalign-3.5.1/tests/python/000077500000000000000000000000001515023132300154435ustar00rootroot00000000000000kalign-3.5.1/tests/python/conftest.py000066400000000000000000000211461515023132300176460ustar00rootroot00000000000000""" Shared pytest configuration and fixtures for Kalign Python tests. """ import pytest import tempfile import os from pathlib import Path from typing import List, Dict, Any # Rich table support for benchmarks try: from rich.console import Console from rich.table import Table from rich.text import Text RICH_AVAILABLE = True except ImportError: RICH_AVAILABLE = False # Test data constants DNA_SEQUENCES_SIMPLE = ["ATCGATCG", "ATCGTCG", "ATCGATCG"] DNA_SEQUENCES_WITH_GAPS = [ "ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG", "ATCGATCGAGATCG", ] RNA_SEQUENCES_SIMPLE = ["AUCGAUCG", "AUCGUCG", "AUCGAUCG"] PROTEIN_SEQUENCES_SIMPLE = ["MKTAYIAK", "MKTAYK", "MKTAYIAK"] PROTEIN_SEQUENCES_COMPLEX = [ "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRLSPLHSVYVDQWDWERVMGDGERQFSTLKSTVEAIWAGIKATEAAVSEEFGLAPFLPDQIHFVHSQELLSRYPDLDAKGRERAIAKDLGAVFLVGIGGKLSDGHRHDVRAPDYDDWUAIFRRVVSAEFQRQPVHQSYLNTVLGSQGKL", "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRLSPLHSVYVDQWDWERVMGDGERQFSTLKSTVEAIWAGIKATEAAVSEEFGLAPFLPDQIHFVHSQELLSRYPDLDAKGRERAIAKDLGAVFLVGIGGKLSDGHRHDVRAPDYDDWUAIFRRVVSAEFQRQPVHQSYLNTVLGSQGKL", "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRLSPLHSVYVDQWDWERVMGDGERQFSTLKSTVEAIWAGIKATEAAVSEEFGLAPFLPDQIHFVHSQELLSRYPDLDAKGRERAIAKDLGAVFLVGIGGKLSDGHRHDVRAPDYDDWUAIFRRVVSAEFQRQPVHQSYLNTVLGSQGKL", ] INVALID_DNA_SEQUENCES = [ "ATCG123", # Contains numbers "ATCGXYZ", # Invalid characters "ATCG@#$", # Special characters ] MIXED_CASE_SEQUENCES = ["AtCgAtCg", "atcgatcg", "ATCGATCG"] @pytest.fixture def dna_simple(): """Simple DNA sequences for basic testing.""" return DNA_SEQUENCES_SIMPLE.copy() @pytest.fixture def dna_with_gaps(): """DNA sequences that will likely produce gaps.""" return DNA_SEQUENCES_WITH_GAPS.copy() @pytest.fixture def rna_simple(): """Simple RNA sequences for basic testing.""" return RNA_SEQUENCES_SIMPLE.copy() @pytest.fixture def protein_simple(): """Simple protein sequences for basic testing.""" return PROTEIN_SEQUENCES_SIMPLE.copy() @pytest.fixture def protein_complex(): """Complex protein sequences for advanced testing.""" return PROTEIN_SEQUENCES_COMPLEX.copy() @pytest.fixture def invalid_dna(): """Invalid DNA sequences for error testing.""" return INVALID_DNA_SEQUENCES.copy() @pytest.fixture def mixed_case(): """Mixed case sequences for case handling testing.""" return MIXED_CASE_SEQUENCES.copy() @pytest.fixture def temp_dir(): """Create a temporary directory for file operations.""" with tempfile.TemporaryDirectory() as tmpdir: yield Path(tmpdir) @pytest.fixture def sample_fasta_file(temp_dir): """Create a sample FASTA file for testing.""" fasta_content = """>seq1 ATCGATCGATCG >seq2 ATCGTCGATCG >seq3 ATCGATCATCG """ fasta_file = temp_dir / "sample.fasta" fasta_file.write_text(fasta_content) return str(fasta_file) @pytest.fixture def sample_protein_fasta_file(temp_dir): """Create a sample protein FASTA file for testing.""" fasta_content = """>protein1 MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRLSPLHSVY >protein2 MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRLSPLH >protein3 MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQTLGQHDFSAGEGLYTHMKALRPDEDRL """ fasta_file = temp_dir / "proteins.fasta" fasta_file.write_text(fasta_content) return str(fasta_file) @pytest.fixture def invalid_fasta_file(temp_dir): """Create an invalid FASTA file for error testing.""" invalid_content = """This is not a valid FASTA file It has no proper headers And random content """ invalid_file = temp_dir / "invalid.fasta" invalid_file.write_text(invalid_content) return str(invalid_file) @pytest.fixture def nonexistent_file(): """Return path to a file that doesn't exist.""" return "/path/that/does/not/exist.fasta" @pytest.fixture def test_data_dir(): """Get the test data directory path.""" return Path(__file__).parent / "test_data" # Parametrize fixtures for common test scenarios @pytest.fixture( params=[ ("dna", DNA_SEQUENCES_SIMPLE), ("rna", RNA_SEQUENCES_SIMPLE), ("protein", PROTEIN_SEQUENCES_SIMPLE), ] ) def sequence_type_data(request): """Parametrized fixture providing different sequence types.""" seq_type, sequences = request.param return seq_type, sequences.copy() @pytest.fixture(params=[1, 2, 4]) def thread_count(request): """Parametrized fixture for testing different thread counts.""" return request.param @pytest.fixture( params=[ ("auto", None), ("dna", "dna"), ("rna", "rna"), ("protein", "protein"), ("divergent", "divergent"), ("internal", "internal"), ] ) def sequence_type_spec(request): """Parametrized fixture for testing sequence type specifications.""" return request.param # Pytest configuration def pytest_configure(config): """Configure pytest with custom markers.""" config.addinivalue_line("markers", "slow: mark test as slow running") config.addinivalue_line("markers", "integration: mark test as integration test") config.addinivalue_line("markers", "performance: mark test as performance test") config.addinivalue_line("markers", "file_io: mark test as requiring file I/O") def pytest_collection_modifyitems(config, items): """Add markers to tests based on their names.""" for item in items: # Mark slow tests if "slow" in item.nodeid or "large" in item.nodeid: item.add_marker(pytest.mark.slow) # Mark integration tests if "integration" in item.nodeid: item.add_marker(pytest.mark.integration) # Mark performance tests if "performance" in item.nodeid or "benchmark" in item.nodeid: item.add_marker(pytest.mark.performance) # Mark file I/O tests if "file" in item.nodeid: item.add_marker(pytest.mark.file_io) # Helper functions for tests def assert_valid_alignment(sequences: List[str], aligned: List[str]) -> None: """Assert that an alignment result is valid.""" # Same number of sequences assert len(aligned) == len(sequences), "Number of sequences changed" # All aligned sequences have same length if aligned: expected_length = len(aligned[0]) for i, seq in enumerate(aligned): assert len(seq) == expected_length, f"Sequence {i} has different length" # No empty sequences (unless input was empty) for seq in aligned: if any( original for original in sequences if original ): # If any input was non-empty assert seq, "Aligned sequence is empty" def assert_alignment_preserves_characters( original: List[str], aligned: List[str] ) -> None: """Assert that alignment preserves the original characters (just adds gaps).""" for orig, align in zip(original, aligned): # Remove gaps from aligned sequence degapped = align.replace("-", "") assert degapped == orig, f"Original sequence {orig} not preserved in {align}" def calculate_identity(seq1: str, seq2: str) -> float: """Calculate sequence identity percentage between two sequences.""" if len(seq1) != len(seq2): raise ValueError("Sequences must have same length") matches = sum(1 for a, b in zip(seq1, seq2) if a == b and a != "-" and b != "-") valid_positions = sum(1 for a, b in zip(seq1, seq2) if a != "-" and b != "-") return (matches / valid_positions * 100) if valid_positions > 0 else 0.0 # Expected results for regression testing EXPECTED_ALIGNMENTS = { "dna_simple": { "sequences": DNA_SEQUENCES_SIMPLE, "expected_length": 8, # This might need adjustment after testing "min_identity": 75.0, # Minimum expected identity percentage }, "protein_simple": { "sequences": PROTEIN_SEQUENCES_SIMPLE, "expected_length": 8, # This might need adjustment after testing "min_identity": 85.0, # Minimum expected identity percentage }, } @pytest.fixture def expected_results(): """Expected alignment results for regression testing.""" return EXPECTED_ALIGNMENTS.copy() # Note: pytest_benchmark_update_json hook removed to avoid plugin validation errors # when pytest-benchmark is not installed. If you need benchmark display functionality, # install pytest-benchmark: pip install pytest-benchmark kalign-3.5.1/tests/python/pytest.ini000066400000000000000000000010561515023132300174760ustar00rootroot00000000000000[tool:pytest] testpaths = tests python_files = test_*.py python_functions = test_* addopts = --strict-markers --strict-config -v --benchmark-columns=min,max,mean,median,ops,rounds --benchmark-sort=name --benchmark-max-time=2.0 markers = slow: marks tests as slow (deselect with '-m "not slow"') integration: marks tests as integration tests performance: marks tests as performance tests file_io: marks tests as requiring file I/O filterwarnings = ignore::DeprecationWarning ignore::PendingDeprecationWarningkalign-3.5.1/tests/python/test_basic_alignment.py000066400000000000000000000050101515023132300221670ustar00rootroot00000000000000""" Basic alignment functionality tests for Kalign Python package. """ import pytest import sys import kalign from conftest import assert_valid_alignment, assert_alignment_preserves_characters class TestBasicAlignment: """Test core alignment functionality.""" def test_simple_dna_alignment(self, dna_simple): """Test basic DNA sequence alignment.""" aligned = kalign.align(dna_simple, seq_type="dna") assert_valid_alignment(dna_simple, aligned) assert_alignment_preserves_characters(dna_simple, aligned) # Check that alignment adds gaps where needed assert len(aligned[0]) >= max(len(seq) for seq in dna_simple) def test_simple_protein_alignment(self, protein_simple): """Test basic protein sequence alignment.""" aligned = kalign.align(protein_simple, seq_type="protein") assert_valid_alignment(protein_simple, aligned) assert_alignment_preserves_characters(protein_simple, aligned) def test_simple_rna_alignment(self, rna_simple): """Test basic RNA sequence alignment.""" aligned = kalign.align(rna_simple, seq_type="rna") assert_valid_alignment(rna_simple, aligned) assert_alignment_preserves_characters(rna_simple, aligned) def test_auto_detection(self, dna_simple): """Test automatic sequence type detection.""" aligned = kalign.align(dna_simple) # No seq_type specified assert_valid_alignment(dna_simple, aligned) assert len(aligned) == len(dna_simple) def test_alignment_length_consistency(self, sequence_type_data): """Test that all aligned sequences have same length.""" seq_type, sequences = sequence_type_data aligned = kalign.align(sequences, seq_type=seq_type) if aligned: expected_length = len(aligned[0]) for seq in aligned: assert len(seq) == expected_length def test_empty_sequence_list(self): """Test behavior with empty sequence list.""" with pytest.raises(ValueError, match="No sequences were found in the input"): kalign.align([]) def test_single_sequence(self): """Test alignment with single sequence should raise error.""" single_seq = ["ATCGATCG"] # Kalign requires at least 2 sequences for alignment with pytest.raises( ValueError, match="Only 1 sequence was found in the input - at least 2 sequences are required for alignment", ): kalign.align(single_seq, seq_type="dna") kalign-3.5.1/tests/python/test_cli.py000066400000000000000000000024071515023132300176260ustar00rootroot00000000000000import subprocess import sys from pathlib import Path def test_cli_version() -> None: result = subprocess.run( [sys.executable, "-m", "kalign.cli", "--version"], check=True, capture_output=True, text=True, ) assert result.stdout.strip() def test_cli_align_fasta_to_stdout() -> None: repo_root = Path(__file__).resolve().parents[2] input_fasta = repo_root / "tests" / "data" / "tiny.fa" result = subprocess.run( [ sys.executable, "-m", "kalign.cli", "-i", str(input_fasta), "-o", "-", "--format", "fasta", "--type", "dna", "-n", "1", ], check=True, capture_output=True, text=True, ) # FASTA output should start with a header line. assert result.stdout.lstrip().startswith(">") def test_cli_entry_point() -> None: """Verify kalign-py entry point is installed and runs.""" result = subprocess.run( ["kalign-py", "--version"], capture_output=True, text=True, ) # Entry point should exist and return version info assert result.returncode == 0 assert result.stdout.strip() kalign-3.5.1/tests/python/test_compare.py000066400000000000000000000060751515023132300205120ustar00rootroot00000000000000"""Tests for kalign.compare() and kalign.align_file_to_file().""" import os import tempfile import pytest import kalign DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") def _data(name): return os.path.join(DATA_DIR, name) class TestCompare: def test_identical_alignment_scores_100(self): ref = _data("BB11001.msf") score = kalign.compare(ref, ref) assert score == pytest.approx(100.0) def test_compare_different_alignments(self): ref = _data("BB11001.msf") with tempfile.TemporaryDirectory() as tmpdir: out = os.path.join(tmpdir, "test_aln.fasta") kalign.align_file_to_file(_data("BB11001.tfa"), out) score = kalign.compare(ref, out) assert 0.0 <= score <= 100.0 def test_compare_file_not_found(self): with pytest.raises(FileNotFoundError): kalign.compare("/nonexistent/ref.msf", "/nonexistent/test.msf") def test_compare_ref_not_found(self): ref = _data("BB11001.msf") with pytest.raises(FileNotFoundError): kalign.compare("/nonexistent/ref.msf", ref) def test_compare_test_not_found(self): ref = _data("BB11001.msf") with pytest.raises(FileNotFoundError): kalign.compare(ref, "/nonexistent/test.msf") class TestAlignFileToFile: def test_basic_align_fasta(self): with tempfile.TemporaryDirectory() as tmpdir: out = os.path.join(tmpdir, "out.fasta") kalign.align_file_to_file(_data("BB11001.tfa"), out) assert os.path.exists(out) assert os.path.getsize(out) > 0 def test_align_msf_format(self): with tempfile.TemporaryDirectory() as tmpdir: out = os.path.join(tmpdir, "out.msf") kalign.align_file_to_file(_data("BB11001.tfa"), out, format="msf") assert os.path.exists(out) assert os.path.getsize(out) > 0 def test_align_and_compare(self): """Align BB11001 and compare to reference - SP score should be reasonable.""" ref = _data("BB11001.msf") with tempfile.TemporaryDirectory() as tmpdir: out = os.path.join(tmpdir, "aln.msf") kalign.align_file_to_file(_data("BB11001.tfa"), out, format="msf") score = kalign.compare(ref, out) assert score > 0.0, "SP score should be positive" def test_input_not_found(self): with pytest.raises(FileNotFoundError): kalign.align_file_to_file("/nonexistent/input.fa", "/tmp/out.fa") def test_align_preserves_sequence_names(self): """Output should contain FASTA headers from input.""" with tempfile.TemporaryDirectory() as tmpdir: out = os.path.join(tmpdir, "out.fasta") kalign.align_file_to_file(_data("BB11001.tfa"), out) with open(out) as f: content = f.read() assert content.startswith(">"), "Output should be FASTA format" headers = [l for l in content.splitlines() if l.startswith(">")] assert len(headers) >= 2, "Should have multiple sequences" kalign-3.5.1/tests/python/test_data/000077500000000000000000000000001515023132300174135ustar00rootroot00000000000000kalign-3.5.1/tests/python/test_data/dna_sequences.fasta000066400000000000000000000000531515023132300232460ustar00rootroot00000000000000>seq1 ATCGATCG >seq2 ATCGTCG >seq3 ATCGATCGkalign-3.5.1/tests/python/test_data/invalid_sequences.fasta000066400000000000000000000000431515023132300241310ustar00rootroot00000000000000>invalid1 ATCG123 >invalid2 ATCGXYZkalign-3.5.1/tests/python/test_data/protein_sequences.fasta000066400000000000000000000000551515023132300241660ustar00rootroot00000000000000>prot1 MKTAYIAK >prot2 MKTAYK >prot3 MKTAYIAKkalign-3.5.1/tests/python/test_data/rna_sequences.fasta000066400000000000000000000000531515023132300232640ustar00rootroot00000000000000>rna1 AUCGAUCG >rna2 AUCGUCG >rna3 AUCGAUCGkalign-3.5.1/tests/python/test_downstream_foundation.py000066400000000000000000000446561515023132300235040ustar00rootroot00000000000000"""Tests for downstream benchmark foundation modules. Verifies provenance, utils, and simulation modules can be imported and their core functions work correctly without external tools. """ import json import os import tempfile from pathlib import Path import pytest pytest.importorskip("benchmarks", reason="benchmarks package not on sys.path (run from repo root)") # ====================================================================== # provenance.py # ====================================================================== class TestProvenance: """Tests for benchmarks.downstream.provenance.""" def test_import(self): from benchmarks.downstream import provenance assert hasattr(provenance, "Provenance") assert hasattr(provenance, "collect_provenance") assert hasattr(provenance, "collect_tool_versions") assert hasattr(provenance, "result_path") assert hasattr(provenance, "update_latest_symlink") assert hasattr(provenance, "load_latest_results") def test_provenance_dataclass_fields(self): from benchmarks.downstream.provenance import Provenance import dataclasses fields = {f.name for f in dataclasses.fields(Provenance)} expected = { "timestamp", "kalign_version", "kalign_commit", "container_image", "hostname", "cpu_model", "cpu_cores", "ram_gb", "os_version", "python_version", "tool_versions", "parameters", } assert expected == fields def test_collect_tool_versions_returns_dict(self): from benchmarks.downstream.provenance import collect_tool_versions versions = collect_tool_versions() assert isinstance(versions, dict) # Should have entries for all known tools for tool in ["kalign", "mafft", "muscle", "clustalo", "hmmer", "iqtree"]: assert tool in versions assert isinstance(versions[tool], str) def test_collect_provenance_structure(self): from benchmarks.downstream.provenance import Provenance, collect_provenance prov = collect_provenance({"test_param": "value"}) assert isinstance(prov, Provenance) assert prov.cpu_cores > 0 assert prov.ram_gb > 0 assert prov.python_version # non-empty assert prov.hostname # non-empty assert prov.parameters == {"test_param": "value"} def test_provenance_json_roundtrip(self): from dataclasses import asdict from benchmarks.downstream.provenance import collect_provenance prov = collect_provenance({"key": 42}) data = asdict(prov) # Must be JSON-serialisable text = json.dumps(data) loaded = json.loads(text) assert loaded["parameters"] == {"key": 42} assert loaded["cpu_cores"] > 0 def test_result_path_generation(self): from benchmarks.downstream.provenance import result_path with tempfile.TemporaryDirectory() as tmpdir: path = result_path(Path(tmpdir), "calibration") assert path.parent.name == "calibration" assert path.name.startswith("run_") assert path.name.endswith(".json") assert path.parent.exists() def test_symlink_management(self): from benchmarks.downstream.provenance import ( load_latest_results, update_latest_symlink, ) with tempfile.TemporaryDirectory() as tmpdir: # Create a fake result file result_file = Path(tmpdir) / "run_test.json" result_file.write_text(json.dumps({"score": 42})) # Create symlink update_latest_symlink(result_file) link = Path(tmpdir) / "latest.json" assert link.is_symlink() # Load through symlink data = load_latest_results(Path(tmpdir)) assert data == {"score": 42} def test_symlink_update_replaces(self): from benchmarks.downstream.provenance import update_latest_symlink with tempfile.TemporaryDirectory() as tmpdir: # First result f1 = Path(tmpdir) / "run_1.json" f1.write_text(json.dumps({"v": 1})) update_latest_symlink(f1) # Second result replaces f2 = Path(tmpdir) / "run_2.json" f2.write_text(json.dumps({"v": 2})) update_latest_symlink(f2) link = Path(tmpdir) / "latest.json" data = json.loads(link.read_text()) assert data == {"v": 2} # ====================================================================== # utils.py # ====================================================================== class TestUtils: """Tests for benchmarks.downstream.utils.""" def test_import(self): from benchmarks.downstream import utils assert hasattr(utils, "AlignResult") assert hasattr(utils, "parse_fasta") assert hasattr(utils, "write_fasta") assert hasattr(utils, "mask_alignment_by_confidence") assert hasattr(utils, "write_site_weights") assert hasattr(utils, "holm_bonferroni") assert hasattr(utils, "METHOD_COLORS") assert hasattr(utils, "METHODS") assert hasattr(utils, "run_method") def test_align_result_fields(self): from benchmarks.downstream.utils import AlignResult r = AlignResult( sequences=["ACGT", "AC-T"], names=["s1", "s2"], column_confidence=[0.9, 0.8, 0.7, 0.6], residue_confidence=[[0.9, 0.8, 0.7, 0.6], [0.8, 0.7, 0.0, 0.5]], wall_time=1.5, peak_memory_mb=100.0, ) assert len(r.sequences) == 2 assert r.wall_time == 1.5 def test_fasta_roundtrip(self): from benchmarks.downstream.utils import parse_fasta, write_fasta names = ["seq1", "seq2", "seq3"] seqs = ["ACGTACGT" * 20, "TGCATGCA" * 20, "AAAA"] with tempfile.TemporaryDirectory() as tmpdir: fasta_path = Path(tmpdir) / "test.fasta" write_fasta(names, seqs, fasta_path) loaded_names, loaded_seqs = parse_fasta(fasta_path) assert loaded_names == names assert loaded_seqs == seqs def test_mask_alignment_basic(self): from benchmarks.downstream.utils import mask_alignment_by_confidence seqs = ["ABCD", "EFGH"] conf = [0.9, 0.3, 0.8, 0.2] masked, n_kept = mask_alignment_by_confidence(seqs, conf, 0.5) assert n_kept == 2 assert masked == ["AC", "EG"] def test_mask_alignment_empty(self): from benchmarks.downstream.utils import mask_alignment_by_confidence masked, n_kept = mask_alignment_by_confidence([], [], 0.5) assert n_kept == 0 assert masked == [] def test_mask_all_filtered(self): from benchmarks.downstream.utils import mask_alignment_by_confidence seqs = ["AB", "CD"] conf = [0.1, 0.2] masked, n_kept = mask_alignment_by_confidence(seqs, conf, 0.5) assert n_kept == 0 assert masked == ["", ""] def test_write_site_weights(self): from benchmarks.downstream.utils import write_site_weights with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "weights.txt" conf = [0.95, 0.3, 1.5, -0.1] # includes out-of-range write_site_weights(conf, path) lines = path.read_text().strip().split("\n") assert len(lines) == 4 vals = [float(x) for x in lines] assert vals[0] == pytest.approx(0.95, abs=1e-5) assert vals[1] == pytest.approx(0.3, abs=1e-5) assert vals[2] == pytest.approx(1.0, abs=1e-5) # clamped assert vals[3] == pytest.approx(0.0, abs=1e-5) # clamped def test_holm_bonferroni_basic(self): from benchmarks.downstream.utils import holm_bonferroni # Simple case: 3 p-values adjusted = holm_bonferroni([0.01, 0.04, 0.03]) assert len(adjusted) == 3 # All adjusted values should be >= original for adj, orig in zip(adjusted, [0.01, 0.04, 0.03]): assert adj >= orig # All adjusted values should be <= 1.0 for adj in adjusted: assert adj <= 1.0 def test_holm_bonferroni_empty(self): from benchmarks.downstream.utils import holm_bonferroni assert holm_bonferroni([]) == [] def test_holm_bonferroni_single(self): from benchmarks.downstream.utils import holm_bonferroni result = holm_bonferroni([0.05]) assert len(result) == 1 assert result[0] == pytest.approx(0.05) def test_method_colors_has_entries(self): from benchmarks.downstream.utils import METHOD_COLORS assert "kalign" in METHOD_COLORS assert "mafft" in METHOD_COLORS assert "guidance2_mafft" in METHOD_COLORS def test_methods_registry_structure(self): from benchmarks.downstream.utils import METHODS assert "kalign" in METHODS assert "kalign_ens3" in METHODS assert "mafft" in METHODS assert "muscle" in METHODS assert "clustalo" in METHODS assert "guidance2_mafft" in METHODS # All entries have "fn" for name, cfg in METHODS.items(): assert "fn" in cfg, f"METHODS[{name!r}] missing 'fn'" assert callable(cfg["fn"]), f"METHODS[{name!r}]['fn'] not callable" def test_run_method_unknown_raises(self): from benchmarks.downstream.utils import run_method with pytest.raises(ValueError, match="Unknown method"): run_method("nonexistent_aligner", Path("/tmp/x.fa"), Path("/tmp")) # ====================================================================== # simulation.py # ====================================================================== class TestSimulation: """Tests for benchmarks.downstream.simulation.""" def test_import(self): from benchmarks.downstream import simulation assert hasattr(simulation, "SimulatedDataset") assert hasattr(simulation, "random_birth_death_tree") assert hasattr(simulation, "generate_indelible_dataset") assert hasattr(simulation, "CODON_GRID") assert hasattr(simulation, "PROTEIN_GRID") assert hasattr(simulation, "iter_simulation_params") assert hasattr(simulation, "strip_gaps") def test_simulated_dataset_fields(self): import dataclasses from benchmarks.downstream.simulation import SimulatedDataset fields = {f.name for f in dataclasses.fields(SimulatedDataset)} expected = {"true_alignment", "unaligned", "true_tree", "site_classes", "params"} assert expected == fields def test_strip_gaps(self): from benchmarks.downstream.simulation import strip_gaps assert strip_gaps(["A-C.G-T", "--ACGT", "ACGT"]) == [ "ACGT", "ACGT", "ACGT", ] def test_strip_gaps_empty(self): from benchmarks.downstream.simulation import strip_gaps assert strip_gaps([]) == [] assert strip_gaps(["---"]) == [""] def test_codon_grid_structure(self): from benchmarks.downstream.simulation import CODON_GRID assert len(CODON_GRID.n_taxa) == 3 assert len(CODON_GRID.tree_depths) == 3 assert len(CODON_GRID.indel_rates) == 3 assert CODON_GRID.replicates == 10 def test_protein_grid_structure(self): from benchmarks.downstream.simulation import PROTEIN_GRID assert len(PROTEIN_GRID.n_taxa) == 3 assert len(PROTEIN_GRID.tree_depths) == 4 assert len(PROTEIN_GRID.indel_rates) == 3 assert PROTEIN_GRID.replicates == 20 def test_iter_simulation_params_wag(self): from benchmarks.downstream.simulation import SimulationGrid, iter_simulation_params small_grid = SimulationGrid( n_taxa=[8], tree_depths=[0.5], indel_rates=[0.05], indel_length_means=[2.0], replicates=3, ) params = list(iter_simulation_params(small_grid, "WAG")) assert len(params) == 3 # 1x1x1x1x3 for p in params: assert "sim_id" in p assert p["model"] == "WAG" assert p["n_taxa"] == 8 assert "WAG" in p["sim_id"] def test_iter_simulation_params_m8(self): from benchmarks.downstream.simulation import SimulationGrid, iter_simulation_params small_grid = SimulationGrid( n_taxa=[16], tree_depths=[0.7], indel_rates=[0.05], psel_fractions=[0.0, 0.10], replicates=2, ) params = list(iter_simulation_params(small_grid, "M8")) assert len(params) == 4 # 1x1x1x2x2 for p in params: assert p["model"] == "M8" def test_iter_simulation_params_unknown_model(self): from benchmarks.downstream.simulation import SimulationGrid, iter_simulation_params small_grid = SimulationGrid(n_taxa=[8], tree_depths=[0.5], indel_rates=[0.05]) with pytest.raises(ValueError, match="Unknown model"): list(iter_simulation_params(small_grid, "JTT")) def test_indelible_control_file_codon(self): """Test that codon control file is written correctly.""" from benchmarks.downstream.simulation import _write_codon_control with tempfile.TemporaryDirectory() as tmpdir: tree = "((A:0.1,B:0.1):0.2,C:0.3);" path = _write_codon_control( Path(tmpdir), tree, n_codons=100, seed=123 ) assert path.exists() text = path.read_text() assert "[TYPE] CODON 1" in text assert "[randomseed] 123" in text assert "simmodel" in text assert "[EVOLVE]" in text def test_indelible_control_file_protein(self): """Test that protein control file is written correctly.""" from benchmarks.downstream.simulation import _write_protein_control with tempfile.TemporaryDirectory() as tmpdir: tree = "((A:0.1,B:0.1):0.2,C:0.3);" path = _write_protein_control( Path(tmpdir), tree, seq_length=200, seed=456 ) assert path.exists() text = path.read_text() assert "[TYPE] AMINOACID 1" in text assert "[randomseed] 456" in text assert "WAG" in text def test_parse_indelible_output_phylip(self): """Test PHYLIP parsing with synthetic data.""" from benchmarks.downstream.simulation import _parse_indelible_output with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) # Write synthetic PHYLIP true alignment phy_path = tmpdir / "output_TRUE_1.phy" phy_path.write_text( " 3 10\n" "T1 ACGTACGTAC\n" "T2 ACGT--GTAC\n" "T3 AC--ACGTAC\n" ) # Write synthetic FASTA unaligned fas_path = tmpdir / "output_1.fas" fas_path.write_text( ">T1\nACGTACGTAC\n>T2\nACGTGTAC\n>T3\nACACGTAC\n" ) true_names, true_seqs, unaln_names, unaln_seqs = ( _parse_indelible_output(tmpdir) ) assert true_names == ["T1", "T2", "T3"] assert true_seqs[0] == "ACGTACGTAC" assert true_seqs[1] == "ACGT--GTAC" assert unaln_names == ["T1", "T2", "T3"] assert unaln_seqs[1] == "ACGTGTAC" def test_parse_site_classes_empty(self): """No rates file → all zeros.""" from benchmarks.downstream.simulation import _parse_site_classes with tempfile.TemporaryDirectory() as tmpdir: classes = _parse_site_classes(Path(tmpdir), 10) assert classes == [0] * 10 def test_parse_site_classes_with_data(self): """Synthetic rates file parsing.""" from benchmarks.downstream.simulation import _parse_site_classes with tempfile.TemporaryDirectory() as tmpdir: rates = Path(tmpdir) / "output_1_RATES.txt" rates.write_text( "Site\tClass\tRate\n" "1\t0\t0.5\n" "2\t0\t0.3\n" "3\t1\t2.5\n" "4\t0\t0.1\n" "5\t1\t3.0\n" ) classes = _parse_site_classes(Path(tmpdir), 5) assert classes == [0, 0, 1, 0, 1] # ====================================================================== # Cross-module integration # ====================================================================== class TestCrossModule: """Tests that modules work together correctly.""" def test_provenance_in_result_json(self): """provenance + utils.write_fasta can produce a complete result.""" from dataclasses import asdict from benchmarks.downstream.provenance import collect_provenance, result_path from benchmarks.downstream.utils import write_fasta with tempfile.TemporaryDirectory() as tmpdir: # Generate result path path = result_path(Path(tmpdir), "test_pipeline") assert path.parent.exists() # Collect provenance prov = collect_provenance({"method": "kalign_ens3"}) prov_dict = asdict(prov) # Write a fake result result = { "provenance": prov_dict, "cases": [{"sp_score": 0.85, "tc_score": 0.72}], } path.write_text(json.dumps(result, indent=2)) assert path.exists() # Verify it can be read back loaded = json.loads(path.read_text()) assert "provenance" in loaded assert loaded["provenance"]["cpu_cores"] > 0 def test_utils_fasta_compatible_with_simulation_strip_gaps(self): """write_fasta → parse_fasta → strip_gaps chain works.""" from benchmarks.downstream.simulation import strip_gaps from benchmarks.downstream.utils import parse_fasta, write_fasta names = ["s1", "s2"] seqs = ["AC-GT-A", "A--GTCA"] with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "aligned.fasta" write_fasta(names, seqs, path) loaded_names, loaded_seqs = parse_fasta(path) assert loaded_names == names assert loaded_seqs == seqs # Strip gaps ungapped = strip_gaps(loaded_seqs) assert ungapped == ["ACGTA", "AGTCA"] kalign-3.5.1/tests/python/test_downstream_integration.py000066400000000000000000000363761515023132300236610ustar00rootroot00000000000000"""Integration tests for downstream benchmark package. Verifies that all modules import cleanly, dataclasses have correct fields, pipeline functions exist with correct signatures, the CLI works, and cross-module interactions are correct. """ from __future__ import annotations import json import subprocess import tempfile from pathlib import Path import pytest pytest.importorskip("benchmarks", reason="benchmarks package not on sys.path (run from repo root)") # ====================================================================== # Module import tests # ====================================================================== class TestModuleImports: """Verify every module in the downstream package imports without error.""" def test_import_package(self): from benchmarks.downstream import __init__ # noqa: F401 def test_import_provenance(self): from benchmarks.downstream import provenance # noqa: F401 def test_import_utils(self): from benchmarks.downstream import utils # noqa: F401 def test_import_simulation(self): from benchmarks.downstream import simulation # noqa: F401 def test_import_calibration(self): from benchmarks.downstream import calibration # noqa: F401 def test_import_positive_selection(self): from benchmarks.downstream import positive_selection # noqa: F401 def test_import_phylo_accuracy(self): from benchmarks.downstream import phylo_accuracy # noqa: F401 def test_import_hmmer_detection(self): from benchmarks.downstream import hmmer_detection # noqa: F401 def test_import_figures(self): from benchmarks.downstream import figures # noqa: F401 def test_import_main(self): from benchmarks.downstream import __main__ # noqa: F401 # ====================================================================== # Dataclass field tests # ====================================================================== class TestDataclassFields: """Verify all dataclasses have the expected fields.""" @staticmethod def _field_names(cls): import dataclasses return {f.name for f in dataclasses.fields(cls)} def test_provenance_fields(self): from benchmarks.downstream.provenance import Provenance fields = self._field_names(Provenance) for f in [ "timestamp", "kalign_version", "kalign_commit", "container_image", "hostname", "cpu_model", "cpu_cores", "ram_gb", "os_version", "python_version", "tool_versions", "parameters", ]: assert f in fields, f"Provenance missing field: {f}" def test_case_result_fields(self): from benchmarks.downstream.calibration import CaseResult fields = self._field_names(CaseResult) for f in [ "sim_id", "method", "predicted_confidence", "actual_correct", "sp_score", "tc_score", "wall_time", "peak_memory_mb", ]: assert f in fields, f"CaseResult missing field: {f}" def test_selection_case_result_fields(self): from benchmarks.downstream.positive_selection import SelectionCaseResult fields = self._field_names(SelectionCaseResult) for f in [ "sim_id", "method", "true_positives", "false_positives", "false_negatives", "true_negatives", "precision", "recall", "f1", "sp_score", ]: assert f in fields, f"SelectionCaseResult missing field: {f}" def test_phylo_case_result_fields(self): from benchmarks.downstream.phylo_accuracy import PhyloCaseResult fields = self._field_names(PhyloCaseResult) for f in [ "sim_id", "method", "nrf", "branch_score_dist", "sp_score", "wall_time_align", "wall_time_iqtree", ]: assert f in fields, f"PhyloCaseResult missing field: {f}" def test_hmmer_case_result_fields(self): from benchmarks.downstream.hmmer_detection import HmmerCaseResult fields = self._field_names(HmmerCaseResult) for f in [ "family_id", "method", "true_positives", "false_positives", "false_negatives", "sensitivity", ]: assert f in fields, f"HmmerCaseResult missing field: {f}" def test_simulated_dataset_fields(self): from benchmarks.downstream.simulation import SimulatedDataset fields = self._field_names(SimulatedDataset) for f in ["true_alignment", "unaligned", "true_tree", "site_classes", "params"]: assert f in fields, f"SimulatedDataset missing field: {f}" # ====================================================================== # Public API tests # ====================================================================== class TestPublicAPI: """Verify each module exposes the expected public functions.""" def test_provenance_api(self): from benchmarks.downstream import provenance assert callable(provenance.collect_provenance) assert callable(provenance.collect_tool_versions) assert callable(provenance.result_path) assert callable(provenance.update_latest_symlink) assert callable(provenance.load_latest_results) def test_utils_api(self): from benchmarks.downstream import utils assert callable(utils.parse_fasta) assert callable(utils.write_fasta) assert callable(utils.mask_alignment_by_confidence) assert callable(utils.write_site_weights) assert callable(utils.alignment_accuracy) assert callable(utils.compare_trees) assert callable(utils.bootstrap_ci) assert callable(utils.wilcoxon_paired) assert callable(utils.holm_bonferroni) assert callable(utils.run_method) assert isinstance(utils.METHODS, dict) assert isinstance(utils.METHOD_COLORS, dict) def test_simulation_api(self): from benchmarks.downstream import simulation assert callable(simulation.random_birth_death_tree) assert callable(simulation.generate_indelible_dataset) assert callable(simulation.iter_simulation_params) assert callable(simulation.strip_gaps) def test_calibration_api(self): from benchmarks.downstream import calibration assert callable(calibration.brier_score) assert callable(calibration.expected_calibration_error) assert callable(calibration.calibration_curve) assert callable(calibration.run_calibration_case) assert callable(calibration.run_pipeline) assert callable(calibration.load_results) def test_positive_selection_api(self): from benchmarks.downstream import positive_selection assert callable(positive_selection.run_selection_case) assert callable(positive_selection.run_pipeline) assert callable(positive_selection.load_results) def test_phylo_accuracy_api(self): from benchmarks.downstream import phylo_accuracy assert callable(phylo_accuracy.run_phylo_case) assert callable(phylo_accuracy.run_pipeline) assert callable(phylo_accuracy.load_results) def test_hmmer_detection_api(self): from benchmarks.downstream import hmmer_detection assert callable(hmmer_detection.run_hmmer_case) assert callable(hmmer_detection.run_pipeline) assert callable(hmmer_detection.load_results) assert isinstance(hmmer_detection.PFAM_FAMILIES, list) assert len(hmmer_detection.PFAM_FAMILIES) == 50 def test_figures_api(self): from benchmarks.downstream import figures assert callable(figures.figure_calibration) assert callable(figures.figure_positive_selection) assert callable(figures.figure_phylo_accuracy) assert callable(figures.figure_hmmer_detection) assert callable(figures.figure_speed_comparison) assert callable(figures.figure_summary_heatmap) assert callable(figures.generate_all_figures) # ====================================================================== # Calibration metric tests # ====================================================================== class TestCalibrationMetrics: """Test calibration scoring functions with known inputs.""" def test_brier_score_perfect(self): from benchmarks.downstream.calibration import brier_score # Perfect predictions: confidence matches outcome exactly assert brier_score([1.0, 0.0, 1.0], [1, 0, 1]) == pytest.approx(0.0) def test_brier_score_worst(self): from benchmarks.downstream.calibration import brier_score # Worst predictions: fully confident but wrong assert brier_score([1.0, 1.0], [0, 0]) == pytest.approx(1.0) def test_brier_score_uniform(self): from benchmarks.downstream.calibration import brier_score # Uniform 0.5 confidence, half correct score = brier_score([0.5, 0.5, 0.5, 0.5], [1, 0, 1, 0]) assert score == pytest.approx(0.25) def test_brier_score_empty(self): from benchmarks.downstream.calibration import brier_score assert brier_score([], []) == pytest.approx(0.0) def test_ece_perfect(self): from benchmarks.downstream.calibration import expected_calibration_error # Perfect calibration: 100% confidence, all correct assert expected_calibration_error([1.0, 1.0], [1, 1]) == pytest.approx( 0.0, abs=0.01 ) def test_calibration_curve_shape(self): from benchmarks.downstream.calibration import calibration_curve pred = [0.1, 0.2, 0.3, 0.8, 0.9, 0.95] actual = [0, 0, 1, 1, 1, 1] centers, fracs, counts = calibration_curve(pred, actual, n_bins=5) assert len(centers) == 5 assert len(fracs) == 5 assert len(counts) == 5 # ====================================================================== # Utils integration tests # ====================================================================== class TestUtilsIntegration: """Test utility functions with realistic data.""" def test_fasta_roundtrip_with_long_sequences(self): from benchmarks.downstream.utils import parse_fasta, write_fasta names = [f"seq{i}" for i in range(10)] seqs = ["ACDEFGHIKLMNPQRSTVWY" * 50 for _ in range(10)] # 1000 chars each with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "test.fasta" write_fasta(names, seqs, path) loaded_names, loaded_seqs = parse_fasta(path) assert loaded_names == names assert loaded_seqs == seqs def test_mask_then_write(self): from benchmarks.downstream.utils import ( mask_alignment_by_confidence, parse_fasta, write_fasta, ) names = ["s1", "s2"] seqs = ["ABCDEFGH", "IJKLMNOP"] conf = [0.9, 0.1, 0.8, 0.2, 0.7, 0.3, 0.6, 0.4] masked, n_kept = mask_alignment_by_confidence(seqs, conf, 0.5) assert n_kept == 4 with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "masked.fasta" write_fasta(names, masked, path) loaded_names, loaded_seqs = parse_fasta(path) assert loaded_names == names assert all(len(s) == 4 for s in loaded_seqs) def test_methods_registry_consistency(self): """Every method in METHODS should have a matching color.""" from benchmarks.downstream.utils import METHOD_COLORS, METHODS for name in METHODS: assert name in METHOD_COLORS, ( f"Method {name!r} in METHODS but not in METHOD_COLORS" ) # ====================================================================== # Simulation grid tests # ====================================================================== class TestSimulationGrids: """Test simulation parameter generation.""" def test_codon_grid_count(self): from benchmarks.downstream.simulation import CODON_GRID, iter_simulation_params params = list(iter_simulation_params(CODON_GRID, "M8")) # 3 taxa x 3 depths x 3 indel_rates x 3 psel_fracs x 3 reps = 243 assert len(params) == 243 def test_protein_grid_count(self): from benchmarks.downstream.simulation import ( PROTEIN_GRID, iter_simulation_params, ) params = list(iter_simulation_params(PROTEIN_GRID, "WAG")) # 2 taxa x 4 depths x 4 indel_rates x 2 indel_lengths x 3 reps = 192 assert len(params) == 192 def test_sim_ids_unique(self): from benchmarks.downstream.simulation import CODON_GRID, iter_simulation_params params = list(iter_simulation_params(CODON_GRID, "M8")) sim_ids = [p["sim_id"] for p in params] assert len(sim_ids) == len(set(sim_ids)), "sim_ids are not unique" # ====================================================================== # CLI tests # ====================================================================== class TestCLI: """Test the __main__.py CLI.""" def test_help_flag(self): result = subprocess.run( ["uv", "run", "python", "-m", "benchmarks.downstream", "--help"], capture_output=True, text=True, timeout=30, ) assert result.returncode == 0 assert "downstream" in result.stdout.lower() assert "--quick" in result.stdout assert "--all" in result.stdout assert "--figures" in result.stdout def test_no_args_exits_nonzero(self): result = subprocess.run( ["uv", "run", "python", "-m", "benchmarks.downstream"], capture_output=True, text=True, timeout=30, ) assert result.returncode != 0 # No action specified # ====================================================================== # Provenance integration # ====================================================================== class TestProvenanceIntegration: """Test provenance works end-to-end with result file management.""" def test_full_provenance_cycle(self): from dataclasses import asdict from benchmarks.downstream.provenance import ( collect_provenance, load_latest_results, result_path, update_latest_symlink, ) with tempfile.TemporaryDirectory() as tmpdir: # Collect provenance prov = collect_provenance({"pipeline": "test", "quick": True}) prov_dict = asdict(prov) # Generate result path path = result_path(Path(tmpdir), "test_pipeline") # Write result result_data = { "provenance": prov_dict, "cases": [{"method": "kalign", "sp_score": 0.85}], "summary": {"kalign": {"mean_sp": 0.85}}, } path.write_text(json.dumps(result_data, indent=2)) # Create symlink update_latest_symlink(path) # Load via symlink loaded = load_latest_results(path.parent) assert loaded["provenance"]["cpu_cores"] > 0 assert loaded["cases"][0]["sp_score"] == 0.85 def test_kalign_version_detected(self): from benchmarks.downstream.provenance import collect_provenance prov = collect_provenance({}) # kalign should be installed in the dev environment assert prov.kalign_version != "not installed" kalign-3.5.1/tests/python/test_ecosystem_integration.py000066400000000000000000000272561515023132300235060ustar00rootroot00000000000000""" Tests for bioinformatics ecosystem integration features. This module tests the Biopython and scikit-bio integration capabilities, as well as threading controls and I/O helpers. """ import pytest from unittest.mock import patch import kalign # Test data TEST_SEQUENCES = ["ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG"] TEST_IDS = ["seq1", "seq2", "seq3"] class TestReturnFormats: """Test different return format options.""" def test_plain_format_default(self): """Test default plain format returns list of strings.""" result = kalign.align(TEST_SEQUENCES) assert isinstance(result, list) assert all(isinstance(seq, str) for seq in result) assert len(result) == len(TEST_SEQUENCES) def test_plain_format_explicit(self): """Test explicit plain format specification.""" result = kalign.align(TEST_SEQUENCES, fmt="plain") assert isinstance(result, list) assert all(isinstance(seq, str) for seq in result) def test_biopython_format_with_biopython(self): """Test Biopython format when Biopython is available.""" try: from Bio.Align import MultipleSeqAlignment except ImportError: pytest.skip("Biopython not installed") result = kalign.align(TEST_SEQUENCES, fmt="biopython", ids=TEST_IDS) assert isinstance(result, MultipleSeqAlignment) assert len(result) == len(TEST_SEQUENCES) assert result[0].id == TEST_IDS[0] def test_biopython_format_without_biopython(self): """Test Biopython format fails gracefully without Biopython.""" with patch.dict( "sys.modules", {"Bio.Align": None, "Bio.SeqRecord": None, "Bio.Seq": None} ): with pytest.raises(ImportError, match="Biopython not installed"): kalign.align(TEST_SEQUENCES, fmt="biopython") def test_skbio_format_with_skbio(self): """Test scikit-bio format when scikit-bio is available.""" try: import skbio except ImportError: pytest.skip("scikit-bio not installed") result = kalign.align(TEST_SEQUENCES, fmt="skbio") assert isinstance(result, skbio.TabularMSA) assert len(result) == len(TEST_SEQUENCES) def test_skbio_format_without_skbio(self): """Test scikit-bio format fails gracefully without scikit-bio.""" with patch.dict("sys.modules", {"skbio": None, "skbio.sequence": None}): with pytest.raises(ImportError, match="scikit-bio not installed"): kalign.align(TEST_SEQUENCES, fmt="skbio") def test_invalid_format(self): """Test invalid format specification raises error.""" with pytest.raises(ValueError, match="Unknown fmt"): kalign.align(TEST_SEQUENCES, fmt="invalid") def test_ids_validation(self): """Test ID validation for ecosystem formats.""" # Too few IDs with pytest.raises(ValueError, match="Number of IDs"): kalign.align(TEST_SEQUENCES, fmt="plain", ids=["seq1"]) # Too many IDs with pytest.raises(ValueError, match="Number of IDs"): kalign.align( TEST_SEQUENCES, fmt="plain", ids=["seq1", "seq2", "seq3", "seq4"] ) def test_auto_generated_ids(self): """Test automatic ID generation for ecosystem formats.""" try: from Bio.Align import MultipleSeqAlignment except ImportError: pytest.skip("Biopython not installed") result = kalign.align(TEST_SEQUENCES, fmt="biopython") assert isinstance(result, MultipleSeqAlignment) for i, record in enumerate(result): assert record.id == f"seq{i}" class TestThreadingControl: """Test global threading control functions.""" def test_default_thread_count(self): """Test default thread count is 1.""" assert kalign.get_num_threads() == 1 def test_set_get_threads(self): """Test setting and getting thread count.""" original = kalign.get_num_threads() try: kalign.set_num_threads(4) assert kalign.get_num_threads() == 4 kalign.set_num_threads(8) assert kalign.get_num_threads() == 8 finally: kalign.set_num_threads(original) def test_invalid_thread_count(self): """Test invalid thread counts raise errors.""" with pytest.raises(ValueError, match="must be at least 1"): kalign.set_num_threads(0) with pytest.raises(ValueError, match="must be at least 1"): kalign.set_num_threads(-1) def test_thread_local_storage(self): """Test that thread settings are thread-local.""" import threading import time results = {} def worker(thread_id, num_threads): kalign.set_num_threads(num_threads) time.sleep(0.1) # Allow other threads to set different values results[thread_id] = kalign.get_num_threads() # Start multiple threads with different thread counts threads = [] for i in range(3): t = threading.Thread(target=worker, args=(i, (i + 1) * 2)) threads.append(t) t.start() # Wait for all threads to complete for t in threads: t.join() # Each thread should have maintained its own setting assert results[0] == 2 assert results[1] == 4 assert results[2] == 6 def test_align_uses_default_threads(self): """Test that align() uses default thread count when n_threads=None.""" original = kalign.get_num_threads() try: kalign.set_num_threads(4) # This should use the default (4 threads) # We can't easily test this directly, but we can ensure it doesn't crash result = kalign.align(TEST_SEQUENCES, n_threads=None) assert len(result) == len(TEST_SEQUENCES) finally: kalign.set_num_threads(original) def test_align_override_threads(self): """Test that align() can override default thread count.""" original = kalign.get_num_threads() try: kalign.set_num_threads(4) # This should override the default result = kalign.align(TEST_SEQUENCES, n_threads=8) assert len(result) == len(TEST_SEQUENCES) finally: kalign.set_num_threads(original) class TestIOModule: """Test I/O helper functions.""" def test_io_module_imported(self): """Test that I/O module is properly imported.""" assert hasattr(kalign, "io") assert hasattr(kalign.io, "read_fasta") assert hasattr(kalign.io, "write_fasta") assert hasattr(kalign.io, "write_clustal") assert hasattr(kalign.io, "write_stockholm") assert hasattr(kalign.io, "write_phylip") def test_io_functions_require_biopython(self): """Test that I/O functions require Biopython and fail gracefully.""" with patch.dict("sys.modules", {"Bio": None, "Bio.SeqIO": None}): with pytest.raises(ImportError, match="Biopython required"): kalign.io.read_fasta("test.fasta") class TestUtilsModule: """Test utility functions.""" def test_utils_module_imported(self): """Test that utils module is properly imported.""" assert hasattr(kalign, "utils") assert hasattr(kalign.utils, "to_array") assert hasattr(kalign.utils, "alignment_stats") assert hasattr(kalign.utils, "consensus_sequence") def test_to_array(self): """Test conversion to NumPy array.""" aligned = kalign.align(TEST_SEQUENCES) arr = kalign.utils.to_array(aligned) assert arr.shape[0] == len(aligned) assert arr.shape[1] == len(aligned[0]) assert arr.dtype.kind == "U" # Unicode string def test_to_array_empty(self): """Test to_array with empty alignment.""" with pytest.raises(ValueError, match="Empty alignment"): kalign.utils.to_array([]) def test_alignment_stats(self): """Test alignment statistics calculation.""" aligned = kalign.align(TEST_SEQUENCES) stats = kalign.utils.alignment_stats(aligned) required_keys = { "length", "n_sequences", "gap_fraction", "conservation", "identity", } assert set(stats.keys()) == required_keys assert stats["n_sequences"] == len(aligned) assert stats["length"] == len(aligned[0]) assert 0 <= stats["gap_fraction"] <= 1 assert 0 <= stats["conservation"] <= 1 assert 0 <= stats["identity"] <= 1 def test_consensus_sequence(self): """Test consensus sequence generation.""" aligned = kalign.align(TEST_SEQUENCES) consensus = kalign.utils.consensus_sequence(aligned) assert isinstance(consensus, str) assert len(consensus) == len(aligned[0]) def test_consensus_sequence_empty(self): """Test consensus with empty alignment.""" with pytest.raises(ValueError, match="Empty alignment"): kalign.utils.consensus_sequence([]) def test_consensus_sequence_invalid_threshold(self): """Test consensus with invalid threshold.""" aligned = kalign.align(TEST_SEQUENCES) with pytest.raises(ValueError, match="Threshold must be between 0 and 1"): kalign.utils.consensus_sequence(aligned, threshold=-0.1) with pytest.raises(ValueError, match="Threshold must be between 0 and 1"): kalign.utils.consensus_sequence(aligned, threshold=1.1) class TestBackwardCompatibility: """Test that all changes maintain backward compatibility.""" def test_existing_api_unchanged(self): """Test that existing API still works exactly as before.""" # All these calls should work exactly as they did before result1 = kalign.align(TEST_SEQUENCES) result2 = kalign.align(TEST_SEQUENCES, seq_type="dna") result3 = kalign.align(TEST_SEQUENCES, seq_type="dna", n_threads=2) result4 = kalign.align(TEST_SEQUENCES, gap_open=5.0, gap_extend=1.0) # All should return lists of strings for result in [result1, result2, result3, result4]: assert isinstance(result, list) assert all(isinstance(seq, str) for seq in result) assert len(result) == len(TEST_SEQUENCES) def test_module_exports(self): """Test that all expected exports are available.""" expected_exports = { "align", "align_from_file", "align_file_to_file", "compare", "compare_detailed", "write_alignment", "generate_test_sequences", "set_num_threads", "get_num_threads", "kalign", "AlignedSequences", "io", "utils", "DNA", "DNA_INTERNAL", "RNA", "PROTEIN", "PROTEIN_PFASUM43", "PROTEIN_PFASUM60", "PROTEIN_PFASUM_AUTO", "PROTEIN_DIVERGENT", "AUTO", "REFINE_NONE", "REFINE_ALL", "REFINE_CONFIDENT", "REFINE_INLINE", "MODE_DEFAULT", "MODE_FAST", "MODE_PRECISE", "__version__", "__author__", "__email__", } actual_exports = set(kalign.__all__) assert expected_exports == actual_exports def test_convenience_alias(self): """Test that the kalign convenience alias still works.""" result1 = kalign.align(TEST_SEQUENCES) result2 = kalign.kalign(TEST_SEQUENCES) # Alias # Results should be identical assert result1 == result2 kalign-3.5.1/tests/python/test_ecosystem_real.py000066400000000000000000000214621515023132300220770ustar00rootroot00000000000000""" Integration tests that exercise real ecosystem packages. These tests require the actual packages installed (biopython, scikit-bio). They are skipped when the packages are not available, but exercised in CI via the test_ecosystem job which installs kalign[all]. Biopython and scikit-bio test groups are independently skippable. """ import os import pytest import kalign # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _skip_no_biopython(): try: import Bio # noqa: F401 return False except ImportError: return True def _skip_no_skbio(): try: import skbio # noqa: F401 return False except ImportError: return True def _assert_alignment_valid(aligned, input_seqs): """Verify aligned sequences are a plausible alignment of the inputs.""" assert len(aligned) == len(input_seqs), ( f"Aligned count ({len(aligned)}) != input count ({len(input_seqs)})" ) # All aligned sequences must be the same length lengths = {len(s) for s in aligned} assert len(lengths) == 1, f"Inconsistent alignment lengths: {lengths}" aln_len = lengths.pop() # Alignment must be at least as long as the longest input max_input = max(len(s) for s in input_seqs) assert aln_len >= max_input # If inputs differ in length, at least one aligned seq must contain gaps if len({len(s) for s in input_seqs}) > 1: assert any("-" in s for s in aligned), "Inputs differ in length but no gaps in alignment" # Ungapped content must match the original sequences for orig, aln in zip(input_seqs, aligned): assert aln.replace("-", "") == orig # --------------------------------------------------------------------------- # Test data # --------------------------------------------------------------------------- DNA_SEQS = ["ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG"] DNA_IDS = ["seq1", "seq2", "seq3"] RNA_SEQS = ["AUCGAUCGAUCG", "AUCGUCGAUCG", "AUCGAUCAUCG"] PROTEIN_SEQS = [ "MKTAYIAKQRQISFVK", "MKTAYIAKQRQ", "MKTAYIAK", ] # Path to test FASTA data shipped with the project TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") TEST_FASTA = os.path.join(TEST_DATA_DIR, "BB11001.tfa") # --------------------------------------------------------------------------- # Biopython tests (skipped independently if Bio is not installed) # --------------------------------------------------------------------------- @pytest.mark.skipif(_skip_no_biopython(), reason="Biopython not installed") class TestBiopythonFormat: """Test fmt='biopython' with real Biopython.""" def test_align_biopython_format_dna(self): from Bio.Align import MultipleSeqAlignment result = kalign.align(DNA_SEQS, fmt="biopython", ids=DNA_IDS) assert isinstance(result, MultipleSeqAlignment) assert len(result) == len(DNA_SEQS) assert result[0].id == "seq1" assert result[1].id == "seq2" assert result[2].id == "seq3" # Verify actual alignment content aligned_strs = [str(r.seq) for r in result] _assert_alignment_valid(aligned_strs, DNA_SEQS) def test_align_biopython_format_protein(self): from Bio.Align import MultipleSeqAlignment result = kalign.align( PROTEIN_SEQS, seq_type="protein", fmt="biopython", ids=["p1", "p2", "p3"], ) assert isinstance(result, MultipleSeqAlignment) assert len(result) == len(PROTEIN_SEQS) aligned_strs = [str(r.seq) for r in result] _assert_alignment_valid(aligned_strs, PROTEIN_SEQS) def test_align_biopython_auto_ids(self): from Bio.Align import MultipleSeqAlignment result = kalign.align(DNA_SEQS, fmt="biopython") assert isinstance(result, MultipleSeqAlignment) assert result[0].id == "seq0" @pytest.mark.skipif(_skip_no_biopython(), reason="Biopython not installed") class TestIORead: """Test kalign.io read functions with real Biopython.""" def test_read_fasta(self): sequences = kalign.io.read_fasta(TEST_FASTA) assert isinstance(sequences, list) assert len(sequences) > 0 assert all(isinstance(s, str) for s in sequences) assert all(len(s) > 0 for s in sequences) def test_read_sequences(self): sequences, ids = kalign.io.read_sequences(TEST_FASTA) assert len(sequences) == len(ids) assert len(sequences) > 0 assert all(len(s) > 0 for s in sequences) assert all(len(i) > 0 for i in ids) @pytest.mark.skipif(_skip_no_biopython(), reason="Biopython not installed") class TestIOWrite: """Test kalign.io write functions with real Biopython.""" @pytest.fixture def aligned(self): return kalign.align(DNA_SEQS) def test_write_fasta_roundtrip(self, aligned, tmp_path): out = tmp_path / "out.fasta" kalign.io.write_fasta(aligned, str(out), ids=DNA_IDS) read_back = kalign.io.read_fasta(str(out)) assert read_back == aligned def test_write_clustal(self, aligned, tmp_path): from Bio import AlignIO out = tmp_path / "out.aln" kalign.io.write_clustal(aligned, str(out), ids=DNA_IDS) aln = AlignIO.read(str(out), "clustal") assert len(aln) == len(aligned) assert aln.get_alignment_length() == len(aligned[0]) # Verify content matches for orig, record in zip(aligned, aln): assert str(record.seq) == orig def test_write_stockholm(self, aligned, tmp_path): from Bio import AlignIO out = tmp_path / "out.sto" kalign.io.write_stockholm(aligned, str(out), ids=DNA_IDS) aln = AlignIO.read(str(out), "stockholm") assert len(aln) == len(aligned) assert aln.get_alignment_length() == len(aligned[0]) for orig, record in zip(aligned, aln): assert str(record.seq) == orig def test_write_phylip(self, aligned, tmp_path): from Bio import AlignIO out = tmp_path / "out.phy" kalign.io.write_phylip(aligned, str(out), ids=DNA_IDS) aln = AlignIO.read(str(out), "phylip-sequential") assert len(aln) == len(aligned) for orig, record in zip(aligned, aln): assert str(record.seq) == orig # --------------------------------------------------------------------------- # scikit-bio tests (skipped independently if skbio is not installed) # --------------------------------------------------------------------------- @pytest.mark.skipif(_skip_no_skbio(), reason="scikit-bio not installed") class TestSkbioFormat: """Test fmt='skbio' with real scikit-bio.""" def test_align_skbio_format_dna(self): import skbio result = kalign.align(DNA_SEQS, seq_type="dna", fmt="skbio", ids=DNA_IDS) assert isinstance(result, skbio.TabularMSA) assert len(result) == len(DNA_SEQS) assert all(isinstance(seq, skbio.DNA) for seq in result) # Verify alignment content aligned_strs = [str(seq) for seq in result] _assert_alignment_valid(aligned_strs, DNA_SEQS) def test_align_skbio_format_rna(self): import skbio result = kalign.align(RNA_SEQS, seq_type="rna", fmt="skbio") assert isinstance(result, skbio.TabularMSA) assert len(result) == len(RNA_SEQS) assert all(isinstance(seq, skbio.RNA) for seq in result) aligned_strs = [str(seq) for seq in result] _assert_alignment_valid(aligned_strs, RNA_SEQS) def test_align_skbio_format_protein(self): import skbio result = kalign.align( PROTEIN_SEQS, seq_type="protein", fmt="skbio", ids=["p1", "p2", "p3"] ) assert isinstance(result, skbio.TabularMSA) assert len(result) == len(PROTEIN_SEQS) assert all(isinstance(seq, skbio.Protein) for seq in result) aligned_strs = [str(seq) for seq in result] _assert_alignment_valid(aligned_strs, PROTEIN_SEQS) def test_align_skbio_auto_detect_dna(self): """AUTO seq_type should infer DNA and produce skbio.DNA objects.""" import skbio result = kalign.align(DNA_SEQS, fmt="skbio") assert all(isinstance(seq, skbio.DNA) for seq in result) def test_align_skbio_auto_detect_protein(self): """AUTO seq_type should infer Protein and produce skbio.Protein objects.""" import skbio result = kalign.align(PROTEIN_SEQS, fmt="skbio") assert all(isinstance(seq, skbio.Protein) for seq in result) def test_align_skbio_metadata(self): import skbio result = kalign.align(DNA_SEQS, seq_type="dna", fmt="skbio", ids=DNA_IDS) assert result[0].metadata["id"] == "seq1" assert result[1].metadata["id"] == "seq2" assert result[2].metadata["id"] == "seq3" kalign-3.5.1/tests/python/test_edge_cases.py000066400000000000000000000056701515023132300211460ustar00rootroot00000000000000""" Edge case tests for Kalign Python package. """ import pytest import kalign class TestEdgeCases: """Test unusual inputs and boundary conditions.""" def test_identical_sequences(self): """Test alignment of identical sequences.""" identical = ["ATCGATCG"] * 3 aligned = kalign.align(identical, seq_type="dna") assert len(aligned) == 3 # All should be identical (no gaps needed) assert all(seq == aligned[0] for seq in aligned) def test_very_short_sequences(self): """Test very short sequences.""" short_seqs = ["A", "T", "G"] aligned = kalign.align(short_seqs, seq_type="dna") assert len(aligned) == 3 assert all(len(seq) >= 1 for seq in aligned) def test_very_different_lengths(self): """Test sequences with very different lengths.""" diff_lengths = ["A", "ATCGATCGATCGATCG", "ATCG"] aligned = kalign.align(diff_lengths, seq_type="dna") assert len(aligned) == 3 assert all(len(seq) == len(aligned[0]) for seq in aligned) def test_single_character_sequences(self): """Test single character sequences.""" single_chars = ["A", "T", "C", "G"] aligned = kalign.align(single_chars, seq_type="dna") assert len(aligned) == 4 def test_repetitive_sequences(self): """Test highly repetitive sequences.""" repetitive = ["AAAAAAA", "TTTTTTT", "AAAAAAA"] aligned = kalign.align(repetitive, seq_type="dna") assert len(aligned) == 3 def test_mixed_case_sequences(self, mixed_case): """Test mixed case handling.""" aligned = kalign.align(mixed_case, seq_type="dna") assert len(aligned) == len(mixed_case) def test_large_sequence_count(self): """Test with many sequences.""" # Use valid DNA characters only - create variation with different bases base_patterns = ["ATCG", "TACG", "GATC", "CGTA", "TGCA", "ACGT", "GCTA", "CTAG"] many_seqs = [] for i in range(20): pattern = base_patterns[i % len(base_patterns)] # Add length variation with valid DNA bases suffix = "A" * (i % 4) # Add 0-3 A's for length variation many_seqs.append(f"{pattern}{suffix}TG") aligned = kalign.align(many_seqs, seq_type="dna") assert len(aligned) == 20 assert all(len(seq) == len(aligned[0]) for seq in aligned) def test_ambiguous_nucleotides(self): """Test sequences with ambiguous nucleotides.""" ambiguous = ["ATCGN", "ATCGY", "ATCGR"] # Use auto-detection since ambiguous nucleotides might be detected as protein try: aligned = kalign.align(ambiguous) # Let kalign auto-detect the type assert len(aligned) == 3 # Should handle gracefully with auto-detection except (ValueError, RuntimeError): # Some ambiguous characters might still cause issues pass kalign-3.5.1/tests/python/test_error_handling.py000066400000000000000000000043351515023132300220560ustar00rootroot00000000000000""" Error handling and exception tests for Kalign Python package. """ import pytest import kalign class TestErrorHandling: """Test error conditions and exception handling.""" def test_invalid_sequence_type_raises_error(self, dna_simple): """Test that invalid sequence type raises ValueError.""" with pytest.raises(ValueError, match="Invalid seq_type"): kalign.align(dna_simple, seq_type="invalid_type") def test_zero_threads_raises_error(self, dna_simple): """Test that zero threads raises ValueError.""" with pytest.raises(ValueError, match="at least 1"): kalign.align(dna_simple, n_threads=0) def test_negative_threads_raises_error(self, dna_simple): """Test that negative threads raises ValueError.""" with pytest.raises(ValueError, match="at least 1"): kalign.align(dna_simple, n_threads=-1) def test_empty_sequence_in_list_raises_error(self): """Test that empty sequences raise ValueError.""" with pytest.raises( ValueError, match="Sequence at index 1 is empty or contains only whitespace" ): kalign.align(["ATCG", "", "GCTA"]) def test_non_string_sequence_raises_error(self): """Test that non-string sequences raise ValueError.""" with pytest.raises(ValueError, match="must be strings"): kalign.align(["ATCG", 123, "GCTA"]) def test_runtime_error_on_alignment_failure(self): """Test that alignment failures raise RuntimeError.""" # Test with sequences containing invalid characters that should cause failure with pytest.raises((RuntimeError, ValueError)): kalign.align(["ATCGXYZ123", "ATCGXYZ456"], seq_type="dna") def test_file_not_found_error(self): """Test file not found error handling.""" with pytest.raises(FileNotFoundError): kalign.align_from_file("/nonexistent/file.fasta") def test_invalid_file_format_error(self, temp_dir): """Test invalid file format handling.""" invalid_file = temp_dir / "invalid.txt" invalid_file.write_text("This is not a sequence file") with pytest.raises(RuntimeError): kalign.align_from_file(str(invalid_file)) kalign-3.5.1/tests/python/test_file_operations.py000066400000000000000000000054521515023132300222440ustar00rootroot00000000000000""" File I/O operation tests for Kalign Python package. """ import pytest import kalign from pathlib import Path class TestFileOperations: """Test file-based alignment operations.""" @pytest.mark.file_io def test_align_from_fasta_file(self, sample_fasta_file): """Test alignment from FASTA file.""" result = kalign.align_from_file(sample_fasta_file, seq_type="dna") assert len(result.sequences) == 3 assert len(result.names) == 3 assert all(len(seq) == len(result.sequences[0]) for seq in result.sequences) @pytest.mark.file_io def test_align_protein_fasta_file(self, sample_protein_fasta_file): """Test protein alignment from FASTA file.""" result = kalign.align_from_file(sample_protein_fasta_file, seq_type="protein") assert len(result.sequences) == 3 assert len(result.names) == 3 assert all(len(seq) == len(result.sequences[0]) for seq in result.sequences) @pytest.mark.file_io def test_auto_detect_from_file(self, sample_fasta_file): """Test auto-detection with file input.""" result = kalign.align_from_file(sample_fasta_file) assert len(result.sequences) == 3 assert len(result.names) == 3 @pytest.mark.file_io def test_file_not_found(self, nonexistent_file): """Test handling of non-existent files.""" with pytest.raises(FileNotFoundError): kalign.align_from_file(nonexistent_file) @pytest.mark.file_io def test_invalid_file_format(self, invalid_fasta_file): """Test handling of invalid file formats.""" with pytest.raises(RuntimeError): kalign.align_from_file(invalid_fasta_file) @pytest.mark.file_io def test_file_with_threading(self, sample_fasta_file): """Test file operations with multiple threads.""" result = kalign.align_from_file(sample_fasta_file, n_threads=2) assert len(result.sequences) == 3 @pytest.mark.file_io def test_relative_file_path(self, test_data_dir): """Test relative file paths.""" dna_file = test_data_dir / "dna_sequences.fasta" if dna_file.exists(): result = kalign.align_from_file(str(dna_file)) assert len(result.sequences) >= 1 @pytest.mark.file_io def test_names_preserved(self, sample_fasta_file): """Test that sequence names from the file are preserved.""" result = kalign.align_from_file(sample_fasta_file, seq_type="dna") assert result.names == ["seq1", "seq2", "seq3"] @pytest.mark.file_io def test_named_tuple_unpacking(self, sample_fasta_file): """Test that result can be unpacked as a tuple.""" names, sequences = kalign.align_from_file(sample_fasta_file, seq_type="dna") assert len(names) == 3 assert len(sequences) == 3 kalign-3.5.1/tests/python/test_input_validation.py000066400000000000000000000051101515023132300224220ustar00rootroot00000000000000""" Input validation tests for Kalign Python package. """ import pytest import kalign class TestInputValidation: """Test input validation and error handling.""" def test_empty_sequence_list(self): """Test empty sequence list raises ValueError.""" with pytest.raises(ValueError, match="No sequences were found in the input"): kalign.align([]) def test_empty_strings_in_list(self): """Test empty strings in sequence list.""" with pytest.raises( ValueError, match="Sequence at index 1 is empty or contains only whitespace" ): kalign.align(["ATCG", "", "GCTA"]) def test_whitespace_only_sequences(self): """Test sequences with only whitespace.""" with pytest.raises( ValueError, match="Sequence at index 1 is empty or contains only whitespace" ): kalign.align(["ATCG", " ", "GCTA"]) def test_non_string_sequences(self): """Test non-string elements in sequence list.""" with pytest.raises(ValueError, match="must be strings"): kalign.align(["ATCG", 123, "GCTA"]) def test_none_in_sequence_list(self): """Test None values in sequence list.""" with pytest.raises(ValueError, match="must be strings"): kalign.align(["ATCG", None, "GCTA"]) def test_invalid_sequence_type_string(self): """Test invalid sequence type string.""" with pytest.raises(ValueError, match="Invalid seq_type"): kalign.align(["ATCG", "GCTA"], seq_type="invalid") def test_invalid_thread_count_zero(self): """Test zero thread count.""" with pytest.raises(ValueError, match="at least 1"): kalign.align(["ATCG", "GCTA"], n_threads=0) def test_invalid_thread_count_negative(self): """Test negative thread count.""" with pytest.raises(ValueError, match="at least 1"): kalign.align(["ATCG", "GCTA"], n_threads=-1) def test_valid_thread_counts(self): """Test valid thread counts work.""" sequences = ["ATCG", "GCTA"] # These should not raise errors for n_threads in [1, 2, 4]: aligned = kalign.align(sequences, n_threads=n_threads) assert len(aligned) == 2 def test_case_insensitive_sequence_types(self): """Test case insensitive sequence type specification.""" sequences = ["ATCG", "GCTA"] # These should all work for seq_type in ["DNA", "dna", "Dna", "DNA"]: aligned = kalign.align(sequences, seq_type=seq_type) assert len(aligned) == 2 kalign-3.5.1/tests/python/test_integration.py000066400000000000000000000070451515023132300214050ustar00rootroot00000000000000""" Integration tests for Kalign Python package. """ import pytest import kalign from pathlib import Path class TestIntegration: """Test end-to-end workflows and integration scenarios.""" @pytest.mark.integration def test_full_dna_workflow(self, temp_dir): """Test complete DNA alignment workflow.""" # Create test sequences sequences = ["ATCGATCGATCGATCG", "ATCGATCGTCGATCG", "ATCGATCGATCATCG"] # Align sequences aligned = kalign.align(sequences, seq_type="dna", n_threads=2) # Validate results assert len(aligned) == 3 assert all(len(seq) == len(aligned[0]) for seq in aligned) # Check that original sequences are preserved (without gaps) for orig, align in zip(sequences, aligned): degapped = align.replace("-", "") assert degapped == orig @pytest.mark.integration def test_full_protein_workflow(self, sample_protein_fasta_file): """Test complete protein alignment workflow.""" # Read and align from file result = kalign.align_from_file( sample_protein_fasta_file, seq_type="protein", gap_open=10.0, gap_extend=1.0, ) assert len(result.sequences) == 3 assert all(isinstance(seq, str) for seq in result.sequences) @pytest.mark.integration def test_mixed_parameter_workflow(self, dna_simple): """Test workflow with various parameter combinations.""" results = [] # Test different parameter combinations param_sets = [ {"seq_type": "dna", "n_threads": 1}, {"seq_type": "dna", "n_threads": 2, "gap_open": 8.0}, {"seq_type": "auto", "gap_extend": 2.0}, ] for params in param_sets: aligned = kalign.align(dna_simple, **params) results.append(aligned) assert len(aligned) == len(dna_simple) # All results should be valid (though may differ) for result in results: assert all(len(seq) == len(result[0]) for seq in result) @pytest.mark.integration def test_error_recovery(self, dna_simple): """Test that system recovers from errors properly.""" # First, cause an error try: kalign.align([], seq_type="dna") except ValueError: pass # Expected # Then verify normal operation still works aligned = kalign.align(dna_simple, seq_type="dna") assert len(aligned) == len(dna_simple) @pytest.mark.integration def test_consistency_across_calls(self, dna_simple): """Test that results are consistent across multiple calls.""" aligned1 = kalign.align(dna_simple, seq_type="dna", n_threads=1) aligned2 = kalign.align(dna_simple, seq_type="dna", n_threads=1) # Results should be identical for same input/parameters assert aligned1 == aligned2 @pytest.mark.integration def test_real_biological_sequences(self, test_data_dir): """Test with real biological sequences if available.""" # Use the test data files we created dna_file = test_data_dir / "dna_sequences.fasta" protein_file = test_data_dir / "protein_sequences.fasta" if dna_file.exists(): result = kalign.align_from_file(str(dna_file), seq_type="dna") assert len(result.sequences) >= 1 if protein_file.exists(): result = kalign.align_from_file( str(protein_file), seq_type="protein" ) assert len(result.sequences) >= 1 kalign-3.5.1/tests/python/test_modes.py000066400000000000000000000107171515023132300201710ustar00rootroot00000000000000"""Tests for the unified mode interface (default/fast/precise).""" import os import tempfile import pytest import kalign TEST_SEQUENCES = ["ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG"] PROTEIN_SEQUENCES = [ "MKFLILLFNILCLFPVLAADNHGVSLHCTTATAIP", "MKFLILLFNILCLFPVLAADNHGVSLHCTTATAIP", "MKFLILLFNILCLFPVLAADNHGVSLHCTTATAIP", ] TEST_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "BB11001.tfa") class TestModeConstants: """Test mode constant exports.""" def test_mode_constants_exist(self): assert kalign.MODE_DEFAULT == "default" assert kalign.MODE_FAST == "fast" assert kalign.MODE_PRECISE == "precise" class TestAlignModes: """Test mode parameter on align().""" def test_default_mode_no_kwarg(self): """Default mode (no mode kwarg) produces alignment.""" result = kalign.align(TEST_SEQUENCES) assert isinstance(result, list) assert len(result) == len(TEST_SEQUENCES) assert all(len(s) == len(result[0]) for s in result) def test_default_mode_explicit(self): """mode='default' produces alignment.""" result = kalign.align(TEST_SEQUENCES, mode="default") assert isinstance(result, list) assert len(result) == len(TEST_SEQUENCES) def test_fast_mode(self): """mode='fast' produces alignment.""" result = kalign.align(TEST_SEQUENCES, mode="fast") assert isinstance(result, list) assert len(result) == len(TEST_SEQUENCES) assert all(len(s) == len(result[0]) for s in result) def test_precise_mode(self): """mode='precise' produces alignment (ensemble).""" result = kalign.align(TEST_SEQUENCES, mode="precise") # precise uses ensemble, which returns (seqs, confidence) tuple if isinstance(result, tuple): seqs = result[0] else: seqs = result assert len(seqs) == len(TEST_SEQUENCES) assert all(len(s) == len(seqs[0]) for s in seqs) def test_invalid_mode(self): """Invalid mode raises ValueError.""" with pytest.raises(ValueError, match="Invalid mode"): kalign.align(TEST_SEQUENCES, mode="turbo") def test_explicit_param_overrides_mode(self): """Explicit consistency=10 overrides fast mode default (consistency=0).""" # This should not crash — fast base + explicit consistency result = kalign.align(TEST_SEQUENCES, mode="fast", consistency=10) assert isinstance(result, list) assert len(result) == len(TEST_SEQUENCES) def test_mode_case_insensitive(self): """Mode names are case-insensitive.""" result = kalign.align(TEST_SEQUENCES, mode="FAST") assert isinstance(result, list) assert len(result) == len(TEST_SEQUENCES) @pytest.mark.skipif(not os.path.exists(TEST_FILE), reason="Test data not found") class TestAlignFromFileModes: """Test mode parameter on align_from_file().""" def test_default_mode(self): result = kalign.align_from_file(TEST_FILE) names, sequences = result assert len(names) > 0 assert len(sequences) == len(names) def test_fast_mode(self): result = kalign.align_from_file(TEST_FILE, mode="fast") names, sequences = result assert len(names) > 0 def test_precise_mode(self): result = kalign.align_from_file(TEST_FILE, mode="precise") names, sequences = result assert len(names) > 0 @pytest.mark.skipif(not os.path.exists(TEST_FILE), reason="Test data not found") class TestAlignFileToFileModes: """Test mode parameter on align_file_to_file().""" def test_default_mode(self): with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as f: out = f.name try: kalign.align_file_to_file(TEST_FILE, out) assert os.path.getsize(out) > 0 finally: os.unlink(out) def test_fast_mode(self): with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as f: out = f.name try: kalign.align_file_to_file(TEST_FILE, out, mode="fast") assert os.path.getsize(out) > 0 finally: os.unlink(out) def test_precise_mode(self): with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as f: out = f.name try: kalign.align_file_to_file(TEST_FILE, out, mode="precise") assert os.path.getsize(out) > 0 finally: os.unlink(out) kalign-3.5.1/tests/python/test_parameters.py000066400000000000000000000030701515023132300212170ustar00rootroot00000000000000""" Parameter validation and behavior tests for Kalign Python package. """ import pytest import kalign class TestParameters: """Test parameter handling and validation.""" def test_default_parameters(self, dna_simple): """Test that default parameters work.""" aligned = kalign.align(dna_simple) assert len(aligned) == len(dna_simple) def test_custom_gap_penalties(self, dna_simple): """Test custom gap penalty parameters.""" aligned = kalign.align( dna_simple, seq_type="dna", gap_open=10.0, gap_extend=1.0, terminal_gap_extend=0.0, ) assert len(aligned) == len(dna_simple) def test_threading_parameters(self, dna_simple): """Test different thread counts.""" for n_threads in [1, 2, 4]: aligned = kalign.align(dna_simple, n_threads=n_threads) assert len(aligned) == len(dna_simple) def test_none_parameters_use_defaults(self, dna_simple): """Test that None parameters use Kalign defaults.""" aligned = kalign.align( dna_simple, gap_open=None, gap_extend=None, terminal_gap_extend=None ) assert len(aligned) == len(dna_simple) @pytest.mark.parametrize("gap_penalty", [0.0, 1.0, 5.0, 20.0]) def test_gap_penalty_range(self, dna_simple, gap_penalty): """Test various gap penalty values.""" aligned = kalign.align( dna_simple, gap_open=gap_penalty, gap_extend=gap_penalty / 2 ) assert len(aligned) == len(dna_simple) kalign-3.5.1/tests/python/test_performance.py000066400000000000000000000114551515023132300213630ustar00rootroot00000000000000""" Performance and benchmark tests for Kalign Python package. """ import pytest import time import kalign class TestPerformance: """Test performance characteristics and benchmarks.""" @pytest.mark.performance def test_basic_alignment_speed(self, dna_simple, benchmark): """Benchmark basic alignment speed.""" result = benchmark(kalign.align, dna_simple, seq_type="dna") assert len(result) == len(dna_simple) @pytest.mark.performance def test_threading_speedup(self, dna_with_gaps): """Test that multiple threads provide speedup.""" # Single thread start_time = time.time() aligned_single = kalign.align(dna_with_gaps, seq_type="dna", n_threads=1) single_time = time.time() - start_time # Multiple threads start_time = time.time() aligned_multi = kalign.align(dna_with_gaps, seq_type="dna", n_threads=2) multi_time = time.time() - start_time # Results should be identical assert aligned_single == aligned_multi # Just verify both completed successfully - timing comparisons are unreliable in CI print( f"Single-thread time: {single_time:.6f}s, Multi-thread time: {multi_time:.6f}s" ) @pytest.mark.performance @pytest.mark.slow def test_large_sequence_performance(self): """Test performance with larger sequences.""" large_seqs = ["ATCG" * 100] * 10 # 400bp sequences start_time = time.time() aligned = kalign.align(large_seqs, seq_type="dna") elapsed = time.time() - start_time assert len(aligned) == 10 assert elapsed < 10.0 # Should complete in reasonable time @pytest.mark.performance def test_memory_usage_reasonable(self, dna_simple): """Test that memory usage is reasonable.""" # This is a basic test - could be enhanced with memory profiling aligned = kalign.align(dna_simple * 10) # 30 sequences assert len(aligned) == 30 @pytest.mark.performance def test_dna_alignment_speed(self, dna_simple, benchmark): """Benchmark DNA alignment speed.""" result = benchmark.pedantic( kalign.align, args=[dna_simple], kwargs={"seq_type": "dna"}, rounds=3 ) assert len(result) == len(dna_simple) @pytest.mark.performance def test_protein_alignment_speed(self, protein_simple, benchmark): """Benchmark protein alignment speed.""" result = benchmark.pedantic( kalign.align, args=[protein_simple], kwargs={"seq_type": "protein"}, rounds=3, ) assert len(result) == len(protein_simple) @pytest.mark.performance @pytest.mark.parametrize("n_threads", [1, 2, 4, 8, 16]) def test_dna_threading_performance(self, n_threads, benchmark): """Test DNA alignment performance with different thread counts.""" # Generate a larger problem to really test threading sequences = kalign.generate_test_sequences( n_seq=200, # 200 sequences - much more demanding n_obs=30, # 30 observed sequences for HMM training dna=True, # DNA sequences length=1000, # 1000bp sequences - much longer seed=42, # Reproducible results ) result = benchmark.pedantic( kalign.align, args=[sequences], kwargs={"seq_type": "dna", "n_threads": n_threads}, rounds=1, # Reduced rounds due to longer runtime ) assert len(result) == len(sequences) # Verify alignment length consistency if result: expected_length = len(result[0]) for seq in result: assert len(seq) == expected_length @pytest.mark.performance @pytest.mark.parametrize("n_threads", [1, 2, 4, 8, 16]) def test_protein_threading_performance(self, n_threads, benchmark): """Test protein alignment performance with different thread counts.""" # Generate a larger problem to really test threading sequences = kalign.generate_test_sequences( n_seq=150, # 150 sequences - more demanding n_obs=25, # 25 observed sequences for HMM training dna=False, # Protein sequences length=320, # 320aa sequences - much longer seed=123, # Different seed for variety ) result = benchmark.pedantic( kalign.align, args=[sequences], kwargs={"seq_type": "protein", "n_threads": n_threads}, rounds=1, # Reduced rounds due to longer runtime ) assert len(result) == len(sequences) # Verify alignment length consistency if result: expected_length = len(result[0]) for seq in result: assert len(seq) == expected_length kalign-3.5.1/tests/python/test_sequence_types.py000066400000000000000000000065461515023132300221230ustar00rootroot00000000000000""" Sequence type specific tests for Kalign Python package. """ import pytest import kalign class TestSequenceTypes: """Test different sequence types and their handling.""" @pytest.mark.parametrize( "seq_type_str,seq_type_const", [ ("auto", kalign.AUTO), ("dna", kalign.DNA), ("internal", kalign.DNA_INTERNAL), ], ) def test_sequence_type_constants(self, seq_type_str, seq_type_const, dna_simple): """Test that string and constant sequence types give same results.""" aligned_str = kalign.align(dna_simple, seq_type=seq_type_str) aligned_const = kalign.align(dna_simple, seq_type=seq_type_const) assert aligned_str == aligned_const @pytest.mark.parametrize( "seq_type_str,seq_type_const", [ ("rna", kalign.RNA), ], ) def test_rna_sequence_type_constants( self, seq_type_str, seq_type_const, rna_simple ): """Test RNA type constants with RNA sequences.""" aligned_str = kalign.align(rna_simple, seq_type=seq_type_str) aligned_const = kalign.align(rna_simple, seq_type=seq_type_const) assert aligned_str == aligned_const @pytest.mark.parametrize( "seq_type_str,seq_type_const", [ ("protein", kalign.PROTEIN), ("divergent", kalign.PROTEIN_DIVERGENT), ], ) def test_protein_sequence_type_constants( self, seq_type_str, seq_type_const, protein_simple ): """Test protein type constants with protein sequences.""" aligned_str = kalign.align(protein_simple, seq_type=seq_type_str) aligned_const = kalign.align(protein_simple, seq_type=seq_type_const) assert aligned_str == aligned_const def test_dna_alignment(self, dna_simple): """Test DNA-specific alignment.""" aligned = kalign.align(dna_simple, seq_type="dna") assert len(aligned) == len(dna_simple) # Check that only valid DNA characters (including gaps) are present valid_chars = set("ATCG-") for seq in aligned: assert all(c.upper() in valid_chars for c in seq) def test_rna_alignment(self, rna_simple): """Test RNA-specific alignment.""" aligned = kalign.align(rna_simple, seq_type="rna") assert len(aligned) == len(rna_simple) # Check that RNA characters are preserved for orig, align in zip(rna_simple, aligned): degapped = align.replace("-", "") assert degapped == orig def test_protein_alignment(self, protein_simple): """Test protein-specific alignment.""" aligned = kalign.align(protein_simple, seq_type="protein") assert len(aligned) == len(protein_simple) # Check that amino acid characters are preserved for orig, align in zip(protein_simple, aligned): degapped = align.replace("-", "") assert degapped == orig def test_divergent_protein_type(self, protein_simple): """Test divergent protein sequence type.""" aligned = kalign.align(protein_simple, seq_type="divergent") assert len(aligned) == len(protein_simple) def test_internal_dna_type(self, dna_simple): """Test internal DNA sequence type.""" aligned = kalign.align(dna_simple, seq_type="internal") assert len(aligned) == len(dna_simple) kalign-3.5.1/tests/zig_test.c000066400000000000000000000007741515023132300161260ustar00rootroot00000000000000/* #include "tldevel.h" */ #include "stdio.h" #include "string.h" /* #include "tlrng.h" */ #include /* #include "strnlen_compat.h" */ int main(int argc, char *argv[]) { /* struct rng_state* rng = NULL; */ /* rng = init_rng(0); */ /* fprintf(stdout,"Kalign (%s)\n", KALIGN_PACKAGE_VERSION); */ /* fprintf(stdout,"HEllo random: %f\n", tl_random_double(rng)); */ int len = strnlen("FAFAF", 40); fprintf(stdout,"Len: %d\n",len); return 0; } kalign-3.5.1/uv.lock000066400000000000000000032441551515023132300143020ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] [[package]] name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "array-api-compat" version = "1.11.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/6c/1e/d04312a19a67744298b7546885149488b8afbb965dfe693aa4964bb60586/array_api_compat-1.11.2.tar.gz", hash = "sha256:a3b7f7b6af18f4c42e79423b1b2479798998b6a74355069d77a01a5282755b5d", size = 50776, upload-time = "2025-03-20T13:11:55.095Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d8/3388c7da49f522e51ab2f919797db28782216cadc9ecc9976160302cfcd6/array_api_compat-1.11.2-py3-none-any.whl", hash = "sha256:b1d0059714a4153b3ae37c989e47b07418f727be5b22908dd3cf9d19bdc2c547", size = 53149, upload-time = "2025-03-20T13:11:53.815Z" }, ] [[package]] name = "array-api-compat" version = "1.13.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/68/36/f799b36d7025a92a23819f9f06541babdb84b6fd0bd4253f8be2eca348a4/array_api_compat-1.13.0.tar.gz", hash = "sha256:8b83a56aa8b9477472fee37f7731968dd213e20c198a05ac49caeff9b03f48a6", size = 103065, upload-time = "2025-12-28T11:26:57.734Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/5d/493b1b5528ab5072feae30821ff3a07b7a0474213d548efb1fdf135f85c1/array_api_compat-1.13.0-py3-none-any.whl", hash = "sha256:c15026a0ddec42815383f07da285472e1b1ff2e632eb7afbcfe9b08fcbad9bf1", size = 58585, upload-time = "2025-12-28T11:26:56.081Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "biom-format" version = "2.1.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/63/da18b1cdb9de07f8ad07d71535eac0460d065c71312ad400f55f6c2865ae/biom_format-2.1.17.tar.gz", hash = "sha256:8e3fa07a432b3f6d5c3cad491ef1f27b18d10fc151ca2d223761be4f0b050479", size = 11721758, upload-time = "2025-08-26T15:19:19.35Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/93/663ba9aba08fdbca7a6a4231f89305dfc6e8534b8551fbd2060222bdf995/biom_format-2.1.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3ed04cc11abfd257e84fff693720a6ec1bc092dcb67094eea78bf9ff8ef4645", size = 11875588, upload-time = "2025-08-26T15:18:27.78Z" }, { url = "https://files.pythonhosted.org/packages/d9/fe/00070214677a1ba6ae6952be17c553c0a9efbb7ead7783db47d52ca05560/biom_format-2.1.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c04fd6485098a37fd26766fe7d260086eca9f1ccaab1f1ca2e1e52a11f72c1ac", size = 11873656, upload-time = "2025-08-26T15:18:30.107Z" }, { url = "https://files.pythonhosted.org/packages/79/a1/ebdc161b7798ec751a5ee34395aac49f6e971fc1002ac909a94f259ec35a/biom_format-2.1.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d882183957798acd9f8ca4fc99a5e0993c2ceaca0765c6148d7e57c7214f67f5", size = 12265786, upload-time = "2025-08-26T15:18:31.918Z" }, { url = "https://files.pythonhosted.org/packages/23/ff/92012af5e35d92ec34c4170331b772ee9bd04b8ec5d133e922d0b31f3be1/biom_format-2.1.17-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36098987ab5338e28fa6267206e222ce8f9e0dd12185dd203b50553b122c4f9f", size = 12268464, upload-time = "2025-08-26T15:18:33.718Z" }, { url = "https://files.pythonhosted.org/packages/20/5f/c66e15b4e405c6eef2265fd1afa31ab5bdf6f6edf267f0a6efa8fc392c1f/biom_format-2.1.17-cp310-cp310-win_amd64.whl", hash = "sha256:b79cf2b47f5f11b1edb134d905838b392af909fd22b7db9863fdc90da0e5535a", size = 11878451, upload-time = "2025-08-26T15:18:35.696Z" }, { url = "https://files.pythonhosted.org/packages/62/7c/c651602791af584a8f3951a61b5ee1b8a109d7de82c4fe555b401f521cd1/biom_format-2.1.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8763ea43b39d9b6721607a1d47a54fd12f424a67c4a26b9e763d370150ef964", size = 11875504, upload-time = "2025-08-26T15:18:37.47Z" }, { url = "https://files.pythonhosted.org/packages/8c/92/805ab970b67058f27047c612fa355cf685990e0c577b94a00447347ac433/biom_format-2.1.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84c65cb172efc90f7aa77269e23c9034d0de2f49ee094bcb8bdfbad459582fa6", size = 11873629, upload-time = "2025-08-26T15:18:39.829Z" }, { url = "https://files.pythonhosted.org/packages/a0/e9/566455f6e4c6202aae0c97d1fc51483949944e45d4dd5134bd071966cd7e/biom_format-2.1.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:270582123050a0114887e3c2565374726d17dc1bc5ae5574f11e2f1b55e50c38", size = 12289428, upload-time = "2025-08-26T15:18:42.118Z" }, { url = "https://files.pythonhosted.org/packages/cc/a3/45d7440f198afdc840154d4eddeafa0af35673a9995b1ef273d0cd908a07/biom_format-2.1.17-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06de7333dc371602c73d33011149a3ee5a8a9a2a628efbbbd6eab22903edf6d7", size = 12291218, upload-time = "2025-08-26T15:18:44.681Z" }, { url = "https://files.pythonhosted.org/packages/a9/78/d12960be5f80d2b8777a295d58c66b25f66e4ba28e92916167999001b5ef/biom_format-2.1.17-cp311-cp311-win_amd64.whl", hash = "sha256:7ac8acf2d9f1df370d50b688ca098d9a82790568eda50fa5aaa6ba787b0c4c06", size = 11878781, upload-time = "2025-08-26T15:18:46.959Z" }, { url = "https://files.pythonhosted.org/packages/e4/6b/3dfb3fe094d048115746289953c88086884c7dede917daa9de72e655a56c/biom_format-2.1.17-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a87f182f6cfa48d2d0cd39911834e89ec84e4f7a96fc01b4cb016a6124f6437a", size = 11875690, upload-time = "2025-08-26T15:18:48.877Z" }, { url = "https://files.pythonhosted.org/packages/a3/ee/2b7b210ded6e1967fa4050278fe20a405a0cd33d1831c934276173c59386/biom_format-2.1.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be2b62738a816cf99f3a9d69c5b4ca28882d5383d82a060bd04bb08bce661a60", size = 11874386, upload-time = "2025-08-26T15:18:50.821Z" }, { url = "https://files.pythonhosted.org/packages/c2/98/f51ce183bd3d3c2c38749d64df816b207c5e9f464c53cb5cf77e56874dfc/biom_format-2.1.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a571d8bc051147a7402229ea0c55942adbd2539f257beb4b7b2230a18efd977", size = 12311295, upload-time = "2025-08-26T15:18:53.065Z" }, { url = "https://files.pythonhosted.org/packages/03/5e/dc0bc86ddd030bc575305b0d6ee55c16e264588b273729a80215c7920f3c/biom_format-2.1.17-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:601a012503363cfa8d011d51f5d90a93dcca8feaa01cce38eacd5c26005bc3a0", size = 12318435, upload-time = "2025-08-26T15:18:54.899Z" }, { url = "https://files.pythonhosted.org/packages/92/86/5f3c19d24bf9061eaf1b53614cca20de379ee963d7f20a274f0a64192978/biom_format-2.1.17-cp312-cp312-win_amd64.whl", hash = "sha256:6b4b9c3d27ff7129d83293ee8cf26bfad2757438d21d147ae26f412ca7002dc5", size = 11879250, upload-time = "2025-08-26T15:18:57.171Z" }, { url = "https://files.pythonhosted.org/packages/28/8b/1529bd9a804b7a9ea331acebb3afb55f8692543aa5feb7bfd682ba7fc92a/biom_format-2.1.17-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8de0eb6e61557f2ea2e174a6040bee62065dd62f7e7bb754be07bcd8dd8d4cfd", size = 11874427, upload-time = "2025-08-26T15:18:59.714Z" }, { url = "https://files.pythonhosted.org/packages/de/62/1b337636d5a88816df2fc60e3733c1f7790cc131c6e58f69e7c96c6a69d6/biom_format-2.1.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5917af9bd459b645e835ee2b9392da3fcbccbdde05a76ccd639a86f2d299cfa0", size = 11873338, upload-time = "2025-08-26T15:19:02.01Z" }, { url = "https://files.pythonhosted.org/packages/18/ab/90a5c2d51fc3e43f5634de070c002be4b34255e00ab9d7fc33eda48ef48a/biom_format-2.1.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e7061fd7f68a85b3b14f03696dac0fd9c18dc93821d367da1c50db39e3220d6", size = 12291356, upload-time = "2025-08-26T15:19:03.949Z" }, { url = "https://files.pythonhosted.org/packages/7c/71/f7167a0926994a4fd4740befcf379ded751efe7b53c8b790fd581df0c1a2/biom_format-2.1.17-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d17ff0d09c2d58d111756b7fcf05f6709ccc9db511b534d1a636bda58caa39ed", size = 12296610, upload-time = "2025-08-26T15:19:05.797Z" }, { url = "https://files.pythonhosted.org/packages/6b/06/dbc9e5ee8a413b07cddde794a34ae57b3edd7469934d77fe79c05abf446e/biom_format-2.1.17-cp313-cp313-win_amd64.whl", hash = "sha256:e70c4939545ca65ed0a665ee4b33ded6c1ffa933a9ed1a01990496179f0a29cd", size = 11878888, upload-time = "2025-08-26T15:19:07.523Z" }, { url = "https://files.pythonhosted.org/packages/39/8a/fe6e95757fb736e8145bedd33c312b8557e72f47ca471ab0323007c12dec/biom_format-2.1.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fbf60d88248402b3593f19e8e296bf9888ae9e3933aabbdaacee3ad4f5f1a516", size = 11875563, upload-time = "2025-08-26T15:19:09.436Z" }, { url = "https://files.pythonhosted.org/packages/1f/8d/29b43cc22a4ec31d0ca56af33c22366137d33ce8ba1212bb01f3ced0cd2e/biom_format-2.1.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f4dfb1e4b27dfd09b63c9adbb5e84b8507c38b596c9fa6db11301d9c081e1c95", size = 11873697, upload-time = "2025-08-26T15:19:11.747Z" }, { url = "https://files.pythonhosted.org/packages/ff/25/eea9d1f0227c3c5725a6de34e8d5510e3f0bdc3b2407921268e4c0849f3b/biom_format-2.1.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d6d6a41ecba94f2fd9f63257856eaf05eb29baa768194089ecbda8347410b34", size = 12263554, upload-time = "2025-08-26T15:19:13.676Z" }, { url = "https://files.pythonhosted.org/packages/97/0d/10eb061ea10a9c43e8be99c4b0cd00db26c3dcc9cb25ada4b78d4c998c94/biom_format-2.1.17-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05cf52851552f0ae6918e885b8f123414d9421579cad9396f3cf6adbccf2d043", size = 12265957, upload-time = "2025-08-26T15:19:15.638Z" }, { url = "https://files.pythonhosted.org/packages/0f/e3/be8d4d7e6128993c024d5d794aa912cffe251f53021daa3e938554ecffc7/biom_format-2.1.17-cp39-cp39-win_amd64.whl", hash = "sha256:5f1f0cadea17646817b41ae5ef87ea0fdf4de7b260f085f73e6a4531680e7044", size = 11878504, upload-time = "2025-08-26T15:19:17.455Z" }, ] [[package]] name = "biopython" version = "1.85" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/ca/1d5fab0fedaf5c2f376d9746d447cdce04241c433602c3861693361ce54c/biopython-1.85.tar.gz", hash = "sha256:5dafab74059de4e78f49f6b5684eddae6e7ce46f09cfa059c1d1339e8b1ea0a6", size = 19909902, upload-time = "2025-01-15T15:06:51.997Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/de/79824470fdc5c2aedeb1bab637d24eab654a7e9fbb7070f779fd944fd1ca/biopython-1.85-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6308053a61f3bdbb11504ece4cf24e264c6f1d6fad278f7e59e6b84b0d9a7b4", size = 2787511, upload-time = "2025-01-15T15:11:56.656Z" }, { url = "https://files.pythonhosted.org/packages/4b/d6/3c90df2ebc4f2a522235f4391392253e528f19f5fb6a52396a2316e17064/biopython-1.85-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:434dd23e972b0c89e128f2ebbd16b38075d609184f4f1fd16368035f923019c2", size = 2765456, upload-time = "2025-01-15T15:12:05.177Z" }, { url = "https://files.pythonhosted.org/packages/ff/d7/34233c4ee906b088b199eb2af9c4107384a547d44b5960257ef2bf2cc3c8/biopython-1.85-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a08d082e85778259a83501063871e00ba57336136403b75050eea14d523c00a", size = 3225137, upload-time = "2025-01-15T15:12:15.847Z" }, { url = "https://files.pythonhosted.org/packages/ae/6b/e7a5ae49ef7e8f81720593db22e884c1c8e3504e0e235da0395758a5c7d0/biopython-1.85-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93464e629950e4df87d810125dc4e904538a4344b924f340908ea5bc95db986", size = 3245258, upload-time = "2025-01-15T15:12:21.012Z" }, { url = "https://files.pythonhosted.org/packages/68/2c/0035656dbe51ac0bbe81321172b43c37b38408f8d136abaac443e9f44b62/biopython-1.85-cp310-cp310-win32.whl", hash = "sha256:f2f45ab3f1e43fdaa697fd753148999090298623278097c19c2c3c0ba134e57c", size = 2786505, upload-time = "2025-01-15T15:12:26.545Z" }, { url = "https://files.pythonhosted.org/packages/32/a4/f20d5830dbd8654c13e763daef429fa32ef0b2b09749ac427d045cc81976/biopython-1.85-cp310-cp310-win_amd64.whl", hash = "sha256:7c8326cd2825ba166abecaf72843b9b15823affd6cec04fde65f0d2526767da4", size = 2820961, upload-time = "2025-01-15T15:12:32.364Z" }, { url = "https://files.pythonhosted.org/packages/c3/73/c3a1323a3fe0d07212b09c04fb903e2cbb98aebfbb58e55e8717473e1bc0/biopython-1.85-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db8822adab0cd75a6e6ae845acf312addd8eab5f9b731c191454b961fc2c2cdc", size = 2787585, upload-time = "2025-01-15T15:12:36.927Z" }, { url = "https://files.pythonhosted.org/packages/ff/cf/299524e896fa49beb7588143e1509cce4848572215ebafb8eea83a861820/biopython-1.85-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e2bbe58cc1a592b239ef6d68396745d3fbfaafc668ce38283871d8ff070dbab", size = 2765608, upload-time = "2025-01-15T15:12:43.853Z" }, { url = "https://files.pythonhosted.org/packages/b1/dd/be3e95b72a35ee6d52c84412e15af828951e5c69175080d4619985fd54ce/biopython-1.85-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5916eb56df7ecd4a3babc07a48d4894c40cfb45dc18ccda1c148d0131017ce04", size = 3237552, upload-time = "2025-01-15T15:12:49.046Z" }, { url = "https://files.pythonhosted.org/packages/25/9c/612821b946930b6caa5d795cfe4169ed6a522562eced9776914be7efaf21/biopython-1.85-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cec8833bf3036049129944ee5382dd576dac9670c3814ff2314b52aa94f199", size = 3257287, upload-time = "2025-01-15T15:12:56.168Z" }, { url = "https://files.pythonhosted.org/packages/a1/77/316e51dd42fd8225429574a268bdc627ce4f42067a3976c4c8c457a42023/biopython-1.85-cp311-cp311-win32.whl", hash = "sha256:cf88a4c8d8af13138be115949639a5e4a201618185a72ff09adbe175b7946b28", size = 2786411, upload-time = "2025-01-15T15:13:02.754Z" }, { url = "https://files.pythonhosted.org/packages/f2/11/3c4e8c049b91998bbbd51ddebc6f790b1aa66211babfbf5ff008a72fb1f9/biopython-1.85-cp311-cp311-win_amd64.whl", hash = "sha256:d3c99db65d57ae4fc5034e42ac6cd8ddce069e664903f04c8a4f684d7609d6fa", size = 2820912, upload-time = "2025-01-15T15:13:10.499Z" }, { url = "https://files.pythonhosted.org/packages/a3/25/e46f05359df7f0049c3adc5eaeb9aee0f5fbde1d959d05c78eb1de8f4d12/biopython-1.85-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc5b981b9e3060db7c355b6145dfe3ce0b6572e1601b31211f6d742b10543874", size = 2789327, upload-time = "2025-01-15T15:13:17.086Z" }, { url = "https://files.pythonhosted.org/packages/54/5b/8b3b029c94c63ab4c1781d141615b4a837e658422381d460c5573d5d8262/biopython-1.85-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe47d704c2d3afac99aeb461219ec5f00273120d2d99835dc0a9a617f520141", size = 2765805, upload-time = "2025-01-15T15:13:26.92Z" }, { url = "https://files.pythonhosted.org/packages/69/0a/9a8a38eff03c4607b9cec8d0e08c76b346b1cee1f77bc6d00efebfc7ec83/biopython-1.85-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54e495239e623660ad367498c2f7a1a294b1997ba603f2ceafb36fd18f0eba6", size = 3249276, upload-time = "2025-01-15T15:13:36.639Z" }, { url = "https://files.pythonhosted.org/packages/b4/0d/b7a0f10f5100dcf51ae36ba31490169bfa45617323bd82af43e1fb0098fb/biopython-1.85-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d024ad48997ad53d53a77da24b072aaba8a550bd816af8f2e7e606a9918a3b43", size = 3268869, upload-time = "2025-01-15T15:13:44.009Z" }, { url = "https://files.pythonhosted.org/packages/07/51/646a4b7bdb4c1153786a70d33588ed09178bfcdda0542dfdc976294f4312/biopython-1.85-cp312-cp312-win32.whl", hash = "sha256:6985e17a2911defcbd30275a12f5ed5de2765e4bc91a60439740d572fdbfdf43", size = 2787011, upload-time = "2025-01-15T15:13:48.958Z" }, { url = "https://files.pythonhosted.org/packages/c1/84/c583fa2ac6e7d392d24ebdc5c99e95e517507de22cf143efb6cf1fc93ff5/biopython-1.85-cp312-cp312-win_amd64.whl", hash = "sha256:d6f8efb2db03f2ec115c5e8c764dbadf635e0c9ecd4c0e91fc8216c1b62f85f5", size = 2820972, upload-time = "2025-01-15T15:13:54.475Z" }, { url = "https://files.pythonhosted.org/packages/c3/3f/65814bf221f0bfdd2633830e573ac8594794686f54110571dce98cc32fd3/biopython-1.85-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae6f17262f20e9587d419ba5683e9dc93a31ee1858b98fa0cff203694d5b786", size = 2789844, upload-time = "2025-01-15T15:14:01.171Z" }, { url = "https://files.pythonhosted.org/packages/be/61/1443ce34226e261c20ae4a154b2bab72c109cf31415c92c54c1aada36b00/biopython-1.85-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1e4918e6399ab0183dd863527fec18b53b7c61b6f0ef95db84b4adfa430ce75", size = 2765767, upload-time = "2025-01-15T15:14:07.096Z" }, { url = "https://files.pythonhosted.org/packages/53/51/bec4c763c704e2715691bb087cfab5907804a1bbef5873588698cece1a30/biopython-1.85-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86e487b6fe0f20a2b0138cb53f3d4dc26a7e4060ac4cb6fb1d19e194d445ef46", size = 3248867, upload-time = "2025-01-15T15:14:12.822Z" }, { url = "https://files.pythonhosted.org/packages/9e/a4/552f20253a7c95988067c4955831bd17dd9b864fd5c215d15c2f63f2d415/biopython-1.85-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327184048b5a50634ae0970119bcb8a35b76d7cefb2658a01f772915f2fb7686", size = 3268479, upload-time = "2025-01-15T15:14:20.414Z" }, { url = "https://files.pythonhosted.org/packages/97/f4/6dfc6ef3e0997f792f93893551d4a9bf7f6052a70d34f4e1b1e5e4a359f6/biopython-1.85-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66b08905fb1b3f5194f908aaf04bbad474c4e3eaebad6d9f889a04e64dd1faf4", size = 3192338, upload-time = "2025-01-15T15:14:26.843Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/73da6588fab3a7d734118f21ac619c0a949f51d3cf7b1b8343d175c33460/biopython-1.85-cp313-cp313-win32.whl", hash = "sha256:5a236ab1e2797c7dcf1577d80fdaafabead2908bc338eaed0aa1509dab769fef", size = 2787017, upload-time = "2025-01-15T15:14:33.802Z" }, { url = "https://files.pythonhosted.org/packages/60/ff/fe8f03ac0ccc7219e0959010750c6ac1a5b1411c91c7f4ec3a37d8313673/biopython-1.85-cp313-cp313-win_amd64.whl", hash = "sha256:1b61593765e9ebdb71d73307d55fd4b46eb976608d329ae6803c084d90ed34c7", size = 2821012, upload-time = "2025-01-15T15:14:40.825Z" }, { url = "https://files.pythonhosted.org/packages/b3/04/9bdf0003913803d3d2239785f0f53875694dd441bd5e1a8c1581cab37747/biopython-1.85-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d76e44b46f555da2e72ac36e757efd327f7f5f690e9f00ede6f723b48538b6d5", size = 2787519, upload-time = "2025-01-15T15:14:45.867Z" }, { url = "https://files.pythonhosted.org/packages/a4/9b/bef7dd17980eb4625c3c69411d3273e9b45d83dc7e2f1c63719607fa1026/biopython-1.85-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8469095907a17f156c76b6644829227efdf4996164f7726e6f4ca15039329776", size = 2765442, upload-time = "2025-01-15T15:14:51.469Z" }, { url = "https://files.pythonhosted.org/packages/94/49/20f55dcfcc5487d84a6a4c84b59bcfb7ae2610f7370847b07e63e0f79cc6/biopython-1.85-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cffc15ac46688cd4cf662b24d03037234ce00b571df67be45a942264f101f990", size = 3223396, upload-time = "2025-01-15T15:14:56.519Z" }, { url = "https://files.pythonhosted.org/packages/80/5a/6ba0066b7f38b9e7a085f2fc4c171a25ebfa64202aab0965961621f561e1/biopython-1.85-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a630b3804f6c3fcae2f9b7485d7a05425e143fc570f25babbc5a4b3d3db00d7", size = 3243475, upload-time = "2025-01-15T15:15:01.798Z" }, { url = "https://files.pythonhosted.org/packages/90/1d/a38a9a61abd4a917fb9b95698971914d722e75027885bdb957b0fe757cd5/biopython-1.85-cp39-cp39-win32.whl", hash = "sha256:0ffb03cd982cb3a79326b84e789f2093880175c44eea10f3030c632f98de24f6", size = 2786482, upload-time = "2025-01-15T15:15:06.532Z" }, { url = "https://files.pythonhosted.org/packages/4a/46/9cb732167a3ce4fd40920d1bec444d1739aaf18b58190f2fab8692fcaba5/biopython-1.85-cp39-cp39-win_amd64.whl", hash = "sha256:8208bf2d87ade066fafe9a63a2eb77486c233bc1bdda2cbf721ebee54715f1bf", size = 2820941, upload-time = "2025-01-15T15:15:12.311Z" }, ] [[package]] name = "biopython" version = "1.86" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/61/c59a849bd457c8a1b408ae828dbcc15e674962b5a29705e869e15b32bf25/biopython-1.86.tar.gz", hash = "sha256:93a50b586a4d2cec68ab2f99d03ef583c5761d8fba5535cb8e81da781d0d92ff", size = 19835323, upload-time = "2025-10-28T21:18:31.041Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/9f/5c95732ad98a6d40f4be58978be801cc87b50c71d79a7aee46c4a085114d/biopython-1.86-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02aef2e31cc92544f574ff837cabaaaf53733f3a6b5a433f781c59e5424a7576", size = 2691935, upload-time = "2025-10-28T21:27:12.558Z" }, { url = "https://files.pythonhosted.org/packages/ca/15/9902cbc901073ba2de397f5c9c84e72147e02aaca1755fa650d26bb715a2/biopython-1.86-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e1b12819a78242b529f54e5d2d00ad90023710a5846ca0f2011ac989fd17d4b", size = 2669420, upload-time = "2025-10-28T21:26:40.048Z" }, { url = "https://files.pythonhosted.org/packages/9d/5d/9cb775106361f8ef7ab459b89ff6d725e81dae7abd382309d3bbf82ced6d/biopython-1.86-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e62504faac6e62fe26e40d6905a69519d8b7b5b0506a426d641b218fde788b5", size = 3183554, upload-time = "2025-10-29T00:35:24.074Z" }, { url = "https://files.pythonhosted.org/packages/08/47/87c8db55746099b38baf6c597688807bad2e59074cec37169168fe98e69e/biopython-1.86-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d4530060aadc6af060a9a049da91a582738837e187fcea80486c71eca74ae59", size = 3205260, upload-time = "2025-10-29T00:35:33.89Z" }, { url = "https://files.pythonhosted.org/packages/88/6c/7822f6b7521073ccc80eb18736b664efe1d59edeb6a3f72c362f542ec352/biopython-1.86-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186e2065c0d1a6c2afc85b9c21a2911a931949668bd73b4c03f429a31b3589f8", size = 3154783, upload-time = "2025-10-28T23:52:52.157Z" }, { url = "https://files.pythonhosted.org/packages/98/7a/6759f99b481432969a4979f03b4c4966716d4cffd2154749ae5af0d8904a/biopython-1.86-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915a09859159598b9421e7240561692e7bb4084e5340c4dbb2435c5c38805a2", size = 3173920, upload-time = "2025-10-28T23:52:57.611Z" }, { url = "https://files.pythonhosted.org/packages/17/c2/fe2a223b3fdd718099055679b68f9f15b2006d83a5a1c1593246d5e5fe81/biopython-1.86-cp310-cp310-win32.whl", hash = "sha256:be3d83152fe3232e2d197896a506902b84ad60d40b3f1d1fc934914d138c6dc1", size = 2697949, upload-time = "2025-10-28T21:32:07.305Z" }, { url = "https://files.pythonhosted.org/packages/1f/11/44fb3975df1070966ffc213f52e9fff63fb48b43bc3d403584ca3f4f9a42/biopython-1.86-cp310-cp310-win_amd64.whl", hash = "sha256:6fbbfe19e12170754adb9632155b7e3be0d4c247f0a2e09d3917bec859282de1", size = 2734235, upload-time = "2025-10-28T21:32:03.014Z" }, { url = "https://files.pythonhosted.org/packages/c3/f5/37d6bb3a1245ec5f5f1c66d5cd790b06cdb54a75b36849893405c17f3612/biopython-1.86-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba88b0754ad53c93eba11d910364cfc773686933c89a886522309ba903151e50", size = 2691944, upload-time = "2025-10-28T21:27:24.053Z" }, { url = "https://files.pythonhosted.org/packages/14/12/44d71f333b7302b30788df80705f2207c47b54c17d0935a378dfc709507d/biopython-1.86-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6cceb32b9036bbdc59962e31bd1605ece24edc226c0d50f99839948b5b5c9dda", size = 2669434, upload-time = "2025-10-28T21:26:49.145Z" }, { url = "https://files.pythonhosted.org/packages/a5/1b/731060090ed29b5ac2484865255f1f363a50afb7275717ceb2c6f20d3ea4/biopython-1.86-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0f040ff85bd7d0ee06574bc6d032bc666802f2fe781b0c316b936237eb3d17e", size = 3196718, upload-time = "2025-10-29T00:35:47.806Z" }, { url = "https://files.pythonhosted.org/packages/1c/8d/8409535c341061b9c78faf151e73b484b456b3c3bdf59b27cf3984f16fbc/biopython-1.86-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ac858fd71f1093380d8b0a16acf060e7c228ad65f9ecacdb9f5760cfb9f59b1", size = 3218383, upload-time = "2025-10-29T00:35:53.523Z" }, { url = "https://files.pythonhosted.org/packages/f4/bc/5e93a11f70732122679747a728509d03a6a066b178cc1d7ca30ed2f1ebee/biopython-1.86-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4bcf5a48ee647624e2d0bedac7fb1c24ef0facd514519cca074593b8a6a40e", size = 3168368, upload-time = "2025-10-28T23:53:16.425Z" }, { url = "https://files.pythonhosted.org/packages/b2/c6/e187940571a3a24d20f407f1d7514ab1fe0dc9fa49e01790c4bd56ced0bc/biopython-1.86-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d4dd9090caaf364a08ab54cd561f37c5f4ea5bcc8f0189d332dcd36d6df5767", size = 3186451, upload-time = "2025-10-28T23:53:22.463Z" }, { url = "https://files.pythonhosted.org/packages/2a/88/1e8ffb0db6a03888768613d682a79043e9975067b9095e644a6872905c88/biopython-1.86-cp311-cp311-win32.whl", hash = "sha256:90591f4554c09d311193e7774b5143442c67e178a5b7d929aaa2a054048b22a7", size = 2697756, upload-time = "2025-10-28T21:32:23.017Z" }, { url = "https://files.pythonhosted.org/packages/f0/b2/e34e45d6cb46c96486a2ed5f07874b6c9493dec68b9d6262ae05f4fe909b/biopython-1.86-cp311-cp311-win_amd64.whl", hash = "sha256:0a95321ca929c04c934e62252c9e2cc5c4fd13ce575798d98af2d79512334b9b", size = 2733781, upload-time = "2025-10-28T21:32:18.409Z" }, { url = "https://files.pythonhosted.org/packages/98/e2/199b8ccbd4b9bf234157db0668177b5b7784d62f29d9096fd0d3a70e3b86/biopython-1.86-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f8d372aae21d79b11613751c6ae23c88db0e94d25b7567b1f67aa0304fb61667", size = 2693171, upload-time = "2025-10-29T00:26:59.028Z" }, { url = "https://files.pythonhosted.org/packages/d8/2f/1a7da2a55212b3d0a03866d22213f91273fee3722b5364575419fbe574a5/biopython-1.86-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:baf19d9237aaaa387a68f8f055f978af5c80338d7e037ab028e8d768928f1250", size = 2692543, upload-time = "2025-10-28T21:27:31.855Z" }, { url = "https://files.pythonhosted.org/packages/5b/e9/4057d4c2aa22ca25c180ecbed2ce9e7d65bf787999778bc63b41df0d03b5/biopython-1.86-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f9abdf6cbf0087850de5f8148da0d420c4cb87905bf4de3145ad24a8d55dcd", size = 2669975, upload-time = "2025-10-28T21:26:54.181Z" }, { url = "https://files.pythonhosted.org/packages/a7/b2/3e6862720d7c51f0fbe7d6d25be72a95486779d9d98122283b4e8032fb40/biopython-1.86-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:187c3c24dd2255e7328f3e0523ab5d6350b73ff562517de0c1922385617101d2", size = 3209367, upload-time = "2025-10-29T00:36:06.522Z" }, { url = "https://files.pythonhosted.org/packages/d7/cb/61877367bf08670573d62513b239dc65cf2b7488dc74322cc6051da2e55e/biopython-1.86-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1859830b8262785c6b59dfe0c82cddb643974f63b9d2779bb9f3e2c47c0a95da", size = 3235466, upload-time = "2025-10-29T00:36:11.516Z" }, { url = "https://files.pythonhosted.org/packages/84/1a/3182a77776b76f3f5c64825ee1acf9355f665bed72ee9e8ff49e48f25d98/biopython-1.86-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfd906c47b6fb38e3abb9f52e0c06822e6e82a043d38c2000773692c29db1ed8", size = 3178776, upload-time = "2025-10-28T23:53:41.487Z" }, { url = "https://files.pythonhosted.org/packages/1a/22/828b08fac8dbc8c1dbc1ad03815137cebc9c78303ec7d21b568544028119/biopython-1.86-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6ab2c60742f1c8494cfbbe3b7a8b45f0400c8f2b36b686b895d5e4d625f04e", size = 3197586, upload-time = "2025-10-28T23:53:47.136Z" }, { url = "https://files.pythonhosted.org/packages/36/7a/122aea7653fa93d7eb72978928e80759082efffa70afe0c25a17e18521da/biopython-1.86-cp312-cp312-win32.whl", hash = "sha256:192c61bc3d782c171b7d50bb7d8189d84790d6e3c4b24fd41d1d7ffc7d303efe", size = 2698043, upload-time = "2025-10-28T21:32:39.452Z" }, { url = "https://files.pythonhosted.org/packages/a9/13/00db03b01e54070d5b0ec9c71eef86e61afa733d9af76e5b9b09f5dc9165/biopython-1.86-cp312-cp312-win_amd64.whl", hash = "sha256:35a6b9c5dcdfb5c2631a313a007f3f41a7d72573ba2b68c962e10ea92096ff3b", size = 2733610, upload-time = "2025-10-28T21:32:34.99Z" }, { url = "https://files.pythonhosted.org/packages/fd/6e/84d6c66ab93095aa7adb998a8eef045328470eafd36b9237c4db213e587c/biopython-1.86-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb3a11a98e49428720dca227e2a5bdd57c973ee7c4df3cf6734c0aa13fd134c7", size = 2693185, upload-time = "2025-10-28T21:27:39.709Z" }, { url = "https://files.pythonhosted.org/packages/12/75/60386f2640f13765b1651f2f26d8b4f893c46ee663df3ca76eda966d4f6a/biopython-1.86-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e161f3d3b6e65fbfd1ce22a01c3e9fa9da789adde4972fd0cc2370795ea5357b", size = 2669980, upload-time = "2025-10-28T21:26:58.839Z" }, { url = "https://files.pythonhosted.org/packages/dd/de/a39adb98a0552a257219503c236ef17f007598af55326c0d143db52e5a92/biopython-1.86-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aa8c9e92ee6fe59dfe0d2c2daf9a9eec6b812c78328caad038f79163c500218", size = 3209657, upload-time = "2025-10-29T00:36:28.842Z" }, { url = "https://files.pythonhosted.org/packages/0b/c7/b2e7aca3de8981f4ecb6ab1e0334c3c4a512e5e9898b57b3d8734b086da7/biopython-1.86-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593ec6a2a4fedec08ddcee1a8a0e0b0ed56835b2714904b352ec4a93d5b9d973", size = 3235774, upload-time = "2025-10-29T00:36:34.07Z" }, { url = "https://files.pythonhosted.org/packages/52/ed/e6647b0b9cf2bb67347612e8e443b84378c44768a8d8439276e4ba881178/biopython-1.86-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2f9ebf9b14d67ca92f48779c4f0ba404c35dba3e8b9d6c34d1a3591c3b746d", size = 3178415, upload-time = "2025-10-28T23:54:05.475Z" }, { url = "https://files.pythonhosted.org/packages/ff/37/f6a14b835842c66a52f212136a99416265f5ce76813d668ceac1cb306357/biopython-1.86-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:137fe9aafd93baa5127d17534b473f6646f92a883f52b34f7c306b800ac50038", size = 3197201, upload-time = "2025-10-28T23:54:10.462Z" }, { url = "https://files.pythonhosted.org/packages/f2/73/0eac930016c509763c174a0e25e92e6d7a711f6f5de1f7001e54fd5c49f7/biopython-1.86-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e784dc8382430c9893aa084ca18fe8a8815b5811f1c324492ef3f4b54e664fff", size = 3145106, upload-time = "2025-10-28T23:54:15.235Z" }, { url = "https://files.pythonhosted.org/packages/00/aa/26e836274d03402e8011b04a1714d4ac2f704add303a493e54d2d5646973/biopython-1.86-cp313-cp313-win32.whl", hash = "sha256:5329a777ba90ea624447173046e77c4df2862acc46eea4e94fe2211fe041750f", size = 2698051, upload-time = "2025-10-28T21:32:55.225Z" }, { url = "https://files.pythonhosted.org/packages/ae/27/fa1f8fa57f2ac8fdc41d14ab36001b8ba0fce5eac01585227b99a4da0e9d/biopython-1.86-cp313-cp313-win_amd64.whl", hash = "sha256:f6f2f1dc75423b15d8a22b8eceae32785736612b6740688526401b8c2d821270", size = 2733649, upload-time = "2025-10-28T21:32:51.052Z" }, { url = "https://files.pythonhosted.org/packages/a4/2d/5b87ab859d38f2c7d7d1f9df375b4734737c2ef62cf8506983e882419a30/biopython-1.86-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:236ca61aa996f12cbc65a8d6a15abfac70b9ee800656629b784c6a240e7d8dc0", size = 2694733, upload-time = "2025-10-29T00:27:49.142Z" }, { url = "https://files.pythonhosted.org/packages/24/7e/a80fad6dbfa1335c506b1565d2b3fdd78cda705408a839c5583a9cfca8b6/biopython-1.86-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f96b7441f456c7eecad5c6e61e75b0db1435c489be7cc5e4f97dd4e60921747c", size = 2670131, upload-time = "2025-10-29T00:27:53.758Z" }, { url = "https://files.pythonhosted.org/packages/2d/0a/6c12e9262b99f395bd66535c4a4203bd70833c11f47ac0730fca6ba2b5f8/biopython-1.86-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d53a78bf960397826219f08f87b061ad7f227527d19986e830eeab60d370b597", size = 3209810, upload-time = "2025-10-29T00:36:45.88Z" }, { url = "https://files.pythonhosted.org/packages/3a/f9/265211154d2bb4cffe78a57b8e57cfbb165cf41cf3d1b68e2a6b073b3b8a/biopython-1.86-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb86e4383c02fdb2571a38947153346e6f5cd38e22de1df40f54d2a3c51d02a8", size = 3235347, upload-time = "2025-10-29T00:36:51.164Z" }, { url = "https://files.pythonhosted.org/packages/64/e5/58d8e48d3b4100a7fd8bae97f0dd7179c30f19861841d1a0bb7827e0033e/biopython-1.86-cp314-cp314-win32.whl", hash = "sha256:ffeba620c4786ea836efee235a9c6333b94e922b89de1449a4782dcc15246ff1", size = 2698198, upload-time = "2025-10-29T00:28:02.812Z" }, { url = "https://files.pythonhosted.org/packages/e2/ca/aa166eb588a2d4eea381c92e5a2a3d09b4b4887b0f0e8f3acf999fb88157/biopython-1.86-cp314-cp314-win_amd64.whl", hash = "sha256:efbb9bc4415a1e2c1c986ba261b02857bc0c9eed098b15493f1cc5c4a1e02409", size = 2734693, upload-time = "2025-10-29T00:27:58.312Z" }, { url = "https://files.pythonhosted.org/packages/50/da/8c227d701ec9c94d9870b1879982e3dd114da130b0816d3f9b937318d31a/biopython-1.86-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:caa70c1639b3306549605f9273753bdbf8cd6d6d352cecf23afbda3c911694f3", size = 2697389, upload-time = "2025-10-29T00:28:07.037Z" }, { url = "https://files.pythonhosted.org/packages/8c/1e/66b0b5622ef6a3a14c449d1c8d69749480b37518e4c1e3a8a86fc668dad7/biopython-1.86-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d077f01d1f69f77a26cac46163d4ea45eb4e6509a68feb7f15e665b7e1de0a99", size = 2673857, upload-time = "2025-10-29T00:28:11.488Z" }, { url = "https://files.pythonhosted.org/packages/76/05/7c8f9800e6960da2007eb75128c8ec0b22e1a0064e8802e8acfad53cdca8/biopython-1.86-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4506ce7dbdf885cb24d1f5439362c3c07f1b6f90761a0d20fe16a2a9ea5702a5", size = 3253007, upload-time = "2025-10-29T00:36:56.066Z" }, { url = "https://files.pythonhosted.org/packages/14/dd/a2177328d841fda0a12e67c65d06279691e25363a2805f561b3665cae114/biopython-1.86-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcd94717e83ba891ebd9acaecbf05ad38313095ca5706caf6c38fa3f2aa17528", size = 3272883, upload-time = "2025-10-29T00:37:01.189Z" }, { url = "https://files.pythonhosted.org/packages/ce/04/1aa91f64db5e0728d596fcf7302e2ae2035800c0676e94ea09645a948b91/biopython-1.86-cp314-cp314t-win32.whl", hash = "sha256:2f6b205dcb4101cefa5c615114bd35a19f656abb9d340eb3cf190f829e43800a", size = 2701649, upload-time = "2025-10-29T00:28:20.527Z" }, { url = "https://files.pythonhosted.org/packages/63/7c/4acaca39102d667175bb3d6502dea91c346f8674c06d5df0dbb678971596/biopython-1.86-cp314-cp314t-win_amd64.whl", hash = "sha256:efeee7c37f2331d2c55704df39e122189cc237ffd7511f34158418ad728131b8", size = 2741364, upload-time = "2025-10-29T00:28:15.752Z" }, ] [[package]] name = "black" version = "25.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pathspec", marker = "python_full_version < '3.10'" }, { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytokens", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, ] [[package]] name = "black" version = "26.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pathspec", marker = "python_full_version >= '3.10'" }, { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytokens", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "build" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, ] [[package]] name = "certifi" version = "2026.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.10.*' and implementation_name != 'PyPy' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and implementation_name != 'PyPy' and sys_platform == 'win32') or (python_full_version >= '3.10' and implementation_name != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[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, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "contourpy" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, ] [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version < '3.10'" }, ] [[package]] name = "coverage" version = "7.13.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "(python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, ] [[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, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "dash" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, { name = "importlib-metadata" }, { name = "nest-asyncio" }, { name = "plotly" }, { name = "requests" }, { name = "retrying" }, { name = "setuptools" }, { name = "typing-extensions" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/dd/3aed9bfd81dfd8f44b3a5db0583080ac9470d5e92ee134982bd5c69e286e/dash-4.0.0.tar.gz", hash = "sha256:c5f2bca497af288f552aea3ae208f6a0cca472559003dac84ac21187a1c3a142", size = 6943263, upload-time = "2026-02-03T19:42:27.92Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/8c/dd63d210b28a7589f4bc1e84880525368147425c717d12834ab562f52d14/dash-4.0.0-py3-none-any.whl", hash = "sha256:e36b4b4eae9e1fa4136bf4f1450ed14ef76063bc5da0b10f8ab07bd57a7cb1ab", size = 7247521, upload-time = "2026-02-03T19:42:25.01Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "execnet" version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] name = "flake8" version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mccabe" }, { name = "pycodestyle" }, { name = "pyflakes" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] [[package]] name = "flask" version = "3.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, ] [[package]] name = "fonttools" version = "4.60.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, { url = "https://files.pythonhosted.org/packages/2e/e4/8381d0ca6b6c6c484660b03517ec5b5b81feeefca3808726dece36c652a9/fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432", size = 2842063, upload-time = "2025-12-09T13:36:53.468Z" }, { url = "https://files.pythonhosted.org/packages/b4/2c/4367117ee8ff4f4374787a1222da0bd413d80cf3522111f727a7b8f80d1d/fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696", size = 2393792, upload-time = "2025-12-09T13:36:55.742Z" }, { url = "https://files.pythonhosted.org/packages/49/b7/a76b6dffa193869e54e32ca2f9abb0d0e66784bc8a24e6f86eb093015481/fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400", size = 4924020, upload-time = "2025-12-09T13:36:58.229Z" }, { url = "https://files.pythonhosted.org/packages/bd/4e/0078200e2259f0061c86a74075f507d64c43dd2ab38971956a5c0012d344/fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607", size = 4980070, upload-time = "2025-12-09T13:37:00.311Z" }, { url = "https://files.pythonhosted.org/packages/85/1f/d87c85a11cb84852c975251581862681e4a0c1c3bd456c648792203f311b/fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598", size = 4921411, upload-time = "2025-12-09T13:37:02.345Z" }, { url = "https://files.pythonhosted.org/packages/75/c0/7efad650f5ed8e317c2633133ef3c64917e7adf2e4e2940c798f5d57ec6e/fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef", size = 5063465, upload-time = "2025-12-09T13:37:04.836Z" }, { url = "https://files.pythonhosted.org/packages/18/a8/750518c4f8cdd79393b386bc81226047ade80239e58c6c9f5dbe1fdd8ea1/fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963", size = 2263443, upload-time = "2025-12-09T13:37:06.762Z" }, { url = "https://files.pythonhosted.org/packages/b8/22/026c60376f165981f80a0e90bd98a79ae3334e9d89a3d046c4d2e265c724/fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76", size = 2313800, upload-time = "2025-12-09T13:37:08.713Z" }, { url = "https://files.pythonhosted.org/packages/7e/ab/7cf1f5204e1366ddf9dc5cdc2789b571feb9eebcee0e3463c3f457df5f52/fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb", size = 2841690, upload-time = "2025-12-09T13:37:10.696Z" }, { url = "https://files.pythonhosted.org/packages/00/3c/0bf83c6f863cc8b934952567fa2bf737cfcec8fc4ffb59b3f93820095f89/fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef", size = 2392191, upload-time = "2025-12-09T13:37:12.954Z" }, { url = "https://files.pythonhosted.org/packages/00/f0/40090d148b8907fbea12e9bdf1ff149f30cdf1769e3b2c3e0dbf5106b88d/fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403", size = 4873503, upload-time = "2025-12-09T13:37:15.142Z" }, { url = "https://files.pythonhosted.org/packages/dc/e0/d8b13f99e58b8c293781288ba62fe634f1f0697c9c4c0ae104d3215f3a10/fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0", size = 4968493, upload-time = "2025-12-09T13:37:18.272Z" }, { url = "https://files.pythonhosted.org/packages/46/c5/960764d12c92bc225f02401d3067048cb7b282293d9e48e39fe2b0ec38a9/fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a", size = 4920015, upload-time = "2025-12-09T13:37:20.334Z" }, { url = "https://files.pythonhosted.org/packages/4b/ab/839d8caf253d1eef3653ef4d34427d0326d17a53efaec9eb04056b670fff/fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25", size = 5031165, upload-time = "2025-12-09T13:37:23.57Z" }, { url = "https://files.pythonhosted.org/packages/de/bf/3bc862796a6841cbe0725bb5512d272239b809dba631a4b0301df885e62d/fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384", size = 2267526, upload-time = "2025-12-09T13:37:25.821Z" }, { url = "https://files.pythonhosted.org/packages/fc/a1/c1909cacf00c76dc37b4743451561fbaaf7db4172c22a6d9394081d114c3/fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667", size = 2319096, upload-time = "2025-12-09T13:37:28.058Z" }, { url = "https://files.pythonhosted.org/packages/29/b3/f66e71433f08e3a931b2b31a665aeed17fcc5e6911fc73529c70a232e421/fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2", size = 2925167, upload-time = "2025-12-09T13:37:30.348Z" }, { url = "https://files.pythonhosted.org/packages/2e/13/eeb491ff743594bbd0bee6e49422c03a59fe9c49002d3cc60eeb77414285/fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1", size = 2430923, upload-time = "2025-12-09T13:37:32.817Z" }, { url = "https://files.pythonhosted.org/packages/b2/e5/db609f785e460796e53c4dbc3874a5f4948477f27beceb5e2d24b2537666/fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4", size = 4877729, upload-time = "2025-12-09T13:37:35.858Z" }, { url = "https://files.pythonhosted.org/packages/5f/d6/85e4484dd4bfb03fee7bd370d65888cccbd3dee2681ee48c869dd5ccb23f/fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d", size = 5096003, upload-time = "2025-12-09T13:37:37.862Z" }, { url = "https://files.pythonhosted.org/packages/30/49/1a98e44b71030b83d2046f981373b80571868259d98e6dae7bc20099dac6/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db", size = 4974410, upload-time = "2025-12-09T13:37:40.166Z" }, { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, ] [[package]] name = "fonttools" version = "4.61.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] [[package]] name = "h5py" version = "3.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/89/06cbb421e01dea2e338b3154326523c05d9698f89a01f9d9b65e1ec3fb18/h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed", size = 3332522, upload-time = "2025-06-06T14:04:13.775Z" }, { url = "https://files.pythonhosted.org/packages/c3/e7/6c860b002329e408348735bfd0459e7b12f712c83d357abeef3ef404eaa9/h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6", size = 2831051, upload-time = "2025-06-06T14:04:18.206Z" }, { url = "https://files.pythonhosted.org/packages/fa/cd/3dd38cdb7cc9266dc4d85f27f0261680cb62f553f1523167ad7454e32b11/h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03", size = 4324677, upload-time = "2025-06-06T14:04:23.438Z" }, { url = "https://files.pythonhosted.org/packages/b1/45/e1a754dc7cd465ba35e438e28557119221ac89b20aaebef48282654e3dc7/h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d", size = 4557272, upload-time = "2025-06-06T14:04:28.863Z" }, { url = "https://files.pythonhosted.org/packages/5c/06/f9506c1531645829d302c420851b78bb717af808dde11212c113585fae42/h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4", size = 2866734, upload-time = "2025-06-06T14:04:33.5Z" }, { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" }, { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" }, { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" }, { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" }, { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" }, { url = "https://files.pythonhosted.org/packages/3e/77/8f651053c1843391e38a189ccf50df7e261ef8cd8bfd8baba0cbe694f7c3/h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23", size = 3312740, upload-time = "2025-06-06T14:05:01.193Z" }, { url = "https://files.pythonhosted.org/packages/ff/10/20436a6cf419b31124e59fefc78d74cb061ccb22213226a583928a65d715/h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a", size = 2829207, upload-time = "2025-06-06T14:05:05.061Z" }, { url = "https://files.pythonhosted.org/packages/3f/19/c8bfe8543bfdd7ccfafd46d8cfd96fce53d6c33e9c7921f375530ee1d39a/h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c", size = 4708455, upload-time = "2025-06-06T14:05:11.528Z" }, { url = "https://files.pythonhosted.org/packages/86/f9/f00de11c82c88bfc1ef22633557bfba9e271e0cb3189ad704183fc4a2644/h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882", size = 4929422, upload-time = "2025-06-06T14:05:18.399Z" }, { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" }, { url = "https://files.pythonhosted.org/packages/6c/c2/7efe82d09ca10afd77cd7c286e42342d520c049a8c43650194928bcc635c/h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812", size = 3289245, upload-time = "2025-06-06T14:05:28.24Z" }, { url = "https://files.pythonhosted.org/packages/4f/31/f570fab1239b0d9441024b92b6ad03bb414ffa69101a985e4c83d37608bd/h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174", size = 2807335, upload-time = "2025-06-06T14:05:31.997Z" }, { url = "https://files.pythonhosted.org/packages/0d/ce/3a21d87896bc7e3e9255e0ad5583ae31ae9e6b4b00e0bcb2a67e2b6acdbc/h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb", size = 4700675, upload-time = "2025-06-06T14:05:37.38Z" }, { url = "https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13", size = 4921632, upload-time = "2025-06-06T14:05:43.464Z" }, { url = "https://files.pythonhosted.org/packages/3f/6d/0084ed0b78d4fd3e7530c32491f2884140d9b06365dac8a08de726421d4a/h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3", size = 2852929, upload-time = "2025-06-06T14:05:47.659Z" }, { url = "https://files.pythonhosted.org/packages/ec/ac/9ea82488c8790ee5b6ad1a807cd7dc3b9dadfece1cd0e0e369f68a7a8937/h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a", size = 3345097, upload-time = "2025-06-06T14:05:51.984Z" }, { url = "https://files.pythonhosted.org/packages/6c/bc/a172ecaaf287e3af2f837f23b470b0a2229c79555a0da9ac8b5cc5bed078/h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef", size = 2843320, upload-time = "2025-06-06T14:05:55.754Z" }, { url = "https://files.pythonhosted.org/packages/66/40/b423b57696514e05aa7bb06150ef96667d0e0006cc6de7ab52c71734ab51/h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6", size = 4326368, upload-time = "2025-06-06T14:06:00.782Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/e088f89f04fdbe57ddf9de377f857158d3daa38cf5d0fb20ef9bd489e313/h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216", size = 4559686, upload-time = "2025-06-06T14:06:07.416Z" }, { url = "https://files.pythonhosted.org/packages/b4/e4/fb8032d0e5480b1db9b419b5b50737b61bb3c7187c49d809975d62129fb0/h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43", size = 2877166, upload-time = "2025-06-06T14:06:13.05Z" }, ] [[package]] name = "h5py" version = "3.15.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" }, { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" }, { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" }, { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" }, { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" }, { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" }, { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, ] [[package]] name = "id" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[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, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] name = "importlib-resources" version = "6.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, ] [[package]] name = "jaraco-functools" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "kalign-python" version = "3.5.1" source = { editable = "." } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.optional-dependencies] all = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "seaborn" }, ] analysis = [ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "seaborn" }, ] benchmark = [ { name = "dash" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly" }, { name = "tqdm" }, ] biopython = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] dev = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "black", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "build" }, { name = "flake8" }, { name = "mypy" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "rich" }, { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "twine" }, ] docs = [ { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-rtd-theme" }, ] io = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] skbio = [ { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] test = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "rich" }, ] [package.dev-dependencies] dev = [ { name = "build" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, ] [package.metadata] requires-dist = [ { name = "biopython", marker = "extra == 'all'", specifier = ">=1.85" }, { name = "biopython", marker = "extra == 'biopython'", specifier = ">=1.85" }, { name = "biopython", marker = "extra == 'dev'", specifier = ">=1.85" }, { name = "biopython", marker = "extra == 'io'", specifier = ">=1.85" }, { name = "black", marker = "extra == 'dev'" }, { name = "build", marker = "extra == 'dev'" }, { name = "dash", marker = "extra == 'benchmark'", specifier = ">=2.14" }, { name = "flake8", marker = "extra == 'dev'" }, { name = "matplotlib", marker = "extra == 'all'", specifier = ">=3.9.4" }, { name = "matplotlib", marker = "extra == 'analysis'", specifier = ">=3.9.4" }, { name = "mypy", marker = "extra == 'dev'" }, { name = "myst-parser", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=1.19.0" }, { name = "pandas", marker = "extra == 'all'", specifier = ">=2.3.0" }, { name = "pandas", marker = "extra == 'analysis'", specifier = ">=2.3.0" }, { name = "pandas", marker = "extra == 'benchmark'", specifier = ">=2.0" }, { name = "plotly", marker = "extra == 'benchmark'", specifier = ">=5.18" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=6.0" }, { name = "pytest-benchmark", marker = "extra == 'dev'" }, { name = "pytest-benchmark", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pytest-xdist", marker = "extra == 'dev'" }, { name = "pytest-xdist", marker = "extra == 'test'" }, { name = "rich", marker = "extra == 'dev'" }, { name = "rich", marker = "extra == 'test'" }, { name = "scikit-bio", marker = "extra == 'all'", specifier = ">=0.6.3" }, { name = "scikit-bio", marker = "extra == 'dev'", specifier = ">=0.6.3" }, { name = "scikit-bio", marker = "extra == 'skbio'", specifier = ">=0.6.3" }, { name = "seaborn", marker = "extra == 'all'", specifier = ">=0.13.2" }, { name = "seaborn", marker = "extra == 'analysis'", specifier = ">=0.13.2" }, { name = "sphinx", marker = "extra == 'docs'" }, { name = "sphinx-rtd-theme", marker = "extra == 'docs'" }, { name = "tqdm", marker = "extra == 'benchmark'", specifier = ">=4.60" }, { name = "twine", marker = "extra == 'dev'" }, ] provides-extras = ["biopython", "skbio", "io", "analysis", "all", "benchmark", "dev", "test", "docs"] [package.metadata.requires-dev] dev = [ { name = "build", specifier = ">=1.2.2.post1" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, ] [[package]] name = "keyring" version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, { name = "jaraco-context" }, { name = "jaraco-functools" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, { name = "secretstorage", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] name = "kiwisolver" version = "1.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, { 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, upload-time = "2024-09-04T09:05:04.072Z" }, { 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, upload-time = "2024-09-04T09:05:05.119Z" }, { 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, upload-time = "2024-09-04T09:05:06.191Z" }, { 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, upload-time = "2024-09-04T09:05:07.919Z" }, { 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, upload-time = "2024-09-04T09:05:10.01Z" }, { 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, upload-time = "2024-09-04T09:05:11.598Z" }, { 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, upload-time = "2024-09-04T09:05:13.22Z" }, { 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, upload-time = "2024-09-04T09:05:15.925Z" }, { 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, upload-time = "2024-09-04T09:05:17.434Z" }, { 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, upload-time = "2024-09-04T09:05:18.997Z" }, { 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, upload-time = "2024-09-04T09:05:21.299Z" }, { 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, upload-time = "2024-09-04T09:05:23.588Z" }, { 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, upload-time = "2024-09-04T09:05:25.907Z" }, { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, ] [[package]] name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] name = "librt" version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] dependencies = [ { name = "mdurl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 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, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, ] [[package]] name = "matplotlib" version = "3.9.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "cycler", marker = "python_full_version < '3.10'" }, { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "importlib-resources", marker = "python_full_version < '3.10'" }, { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyparsing", marker = "python_full_version < '3.10'" }, { name = "python-dateutil", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, ] [[package]] name = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "cycler", marker = "python_full_version >= '3.10'" }, { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyparsing", marker = "python_full_version >= '3.10'" }, { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] name = "mdit-py-plugins" version = "0.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } 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, upload-time = "2024-09-09T20:27:48.397Z" }, ] [[package]] name = "mdit-py-plugins" version = "0.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[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, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "myst-parser" version = "3.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyyaml", marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, ] [[package]] name = "myst-parser" version = "4.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "pyyaml", marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] [[package]] name = "myst-parser" version = "5.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml", marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, ] [[package]] name = "narwhals" version = "2.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, ] [[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, upload-time = "2023-06-20T04:17:19.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, ] [[package]] name = "nest-asyncio" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] name = "nh3" version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, ] [[package]] name = "numpy" version = "2.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] [[package]] name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "python-dateutil", marker = "python_full_version < '3.11'" }, { name = "pytz", marker = "python_full_version < '3.11'" }, { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, ] [[package]] name = "pandas" version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "patsy" version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] name = "pillow" version = "12.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, ] [[package]] name = "platformdirs" version = "4.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "plotly" version = "6.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pyproject-hooks" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pluggy", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pluggy", marker = "python_full_version >= '3.10'" }, { name = "pygments", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-benchmark" version = "5.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "py-cpuinfo" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, ] [[package]] name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "pluggy" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[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, upload-time = "2024-03-01T18:36:20.211Z" } 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, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pytokens" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] name = "readme-renderer" version = "44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "nh3" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, ] [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] name = "retrying" version = "1.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, ] [[package]] name = "rfc3986" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, ] [[package]] name = "rich" version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] name = "roman-numerals" version = "4.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] name = "scikit-bio" version = "0.7.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "array-api-compat", version = "1.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "biom-format", marker = "python_full_version < '3.10'" }, { name = "decorator", marker = "python_full_version < '3.10'" }, { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "natsort", marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "patsy", marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "statsmodels", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/fe/be2d10a390ceea3a3657e77a5ebd16aa167509b10a709125f3a8e4fd663a/scikit_bio-0.7.0.tar.gz", hash = "sha256:771093ebfd154f02b9d88dcf81997737ce804e384bb3ab1cd389dfc60d566d11", size = 5468613, upload-time = "2025-07-16T18:48:05.138Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/8b/f90ee0325c7f59573bdf8d4a5a5717ca710f067a081fd46f3768600a200d/scikit_bio-0.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e857ad41a3915e53a600d51416ffbc945295ccbca6d7e6407533e8494a5baa6f", size = 6243968, upload-time = "2025-07-16T18:47:19.75Z" }, { url = "https://files.pythonhosted.org/packages/57/c6/0864476e63935459eb4d474f210e70ca1b0aa9689bb1beccbeae582587ce/scikit_bio-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a07476f9cc87b1e2033e74a6f0c00e2c29a05c6cf1985f7a8ec8bcdb13fc6cb", size = 6190829, upload-time = "2025-07-16T18:47:21.29Z" }, { url = "https://files.pythonhosted.org/packages/2c/ca/087675f3fd6608142be8c53e6875a1a730e0fa1250a85421677570445839/scikit_bio-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5dc9cee6a7d1dffde4e560ebf1f77488d896d6398fe4d149031a64738c3d69", size = 9554406, upload-time = "2025-07-16T18:47:22.655Z" }, { url = "https://files.pythonhosted.org/packages/c8/69/da203001d9a0d4f2dc14426617e5eed76c9ff059f6c2a94023bf48e204cb/scikit_bio-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93585d7fb9d2115685065c76edb365de1876eaac81b789528491456fbec8267c", size = 9544353, upload-time = "2025-07-16T18:47:24.308Z" }, { url = "https://files.pythonhosted.org/packages/45/97/3b928b1f075a6033326244e125f0e3a518fe75c17278359d1835ca4ccb24/scikit_bio-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:be10f43ba5d4a86e885931e008ca9e00d76da99b6b0761a136511afc136bcdc9", size = 6183322, upload-time = "2025-07-16T18:47:26.221Z" }, { url = "https://files.pythonhosted.org/packages/3d/66/5f238ff8fa25080d5baf82287297bfd1e6dbeab45ef3ff1b0f6e1a7c017e/scikit_bio-0.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:836c24609a33a283b5d57efff2647d2967caf20aa3ad77f4ae6cdb5495479efa", size = 6251825, upload-time = "2025-07-16T18:47:27.598Z" }, { url = "https://files.pythonhosted.org/packages/f9/8b/8ef6e40a818067bdaf57fa779eadbc8ef73f333dbd50fb09daeae13de1f2/scikit_bio-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f7ae35711fe6bcf83b43aa7b351c598c32fb2add4f0dc777179696070fed0af6", size = 6197964, upload-time = "2025-07-16T18:47:29.255Z" }, { url = "https://files.pythonhosted.org/packages/f0/66/6cb6e9ba1e67f65a27b34fcd33b457cb0fea73f93dc184ea1bf38562f012/scikit_bio-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18ebd6ad693c2e4714f18d74b487c5ef1e1e2de570074f50e5c4131dc9b92156", size = 9791543, upload-time = "2025-07-16T18:47:30.782Z" }, { url = "https://files.pythonhosted.org/packages/ad/27/e359f131018cf988f1e4f5e7380db7db43ba4202ffc9ba9e8f6cfb1988f5/scikit_bio-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79845e2ae1685250ce6bf538ae8ac06da33e3711b6f65485bd0edef0ccfd818d", size = 9780016, upload-time = "2025-07-16T18:47:33.082Z" }, { url = "https://files.pythonhosted.org/packages/db/99/6b0ce81f81e2cfe06c899455b149e7f8393cd3ba9a8905efe18c36894420/scikit_bio-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0fb859a61ad700803a76a9d4dbd49fc5f60f781ab7d897265e2fe7f6c8d112d", size = 6183062, upload-time = "2025-07-16T18:47:36.263Z" }, { url = "https://files.pythonhosted.org/packages/43/9b/bf339d439568a4fa0ab6e7e545c4487b1cf86e6245f98be0d922f4677ddc/scikit_bio-0.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eb82ad5f4866c35ae3bfe667382f533620eb188f4db68a9213a231299b7d7dc", size = 6256181, upload-time = "2025-07-16T18:47:37.697Z" }, { url = "https://files.pythonhosted.org/packages/1d/e9/7a5dd2e04bbd361907f5a1942fa4b6aeecfa453801f4c047c55b51036df2/scikit_bio-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e6e9504a27be5348bc218db9d6deaf4044c99341c900d2caf17c51023cba2bff", size = 6195263, upload-time = "2025-07-16T18:47:39.336Z" }, { url = "https://files.pythonhosted.org/packages/19/4b/b63123cf09c3375fa90cbfc02febf32cc20429a49c03d71f37febed7c52d/scikit_bio-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:366a5840796c9ea4d1cc9e77557c89bbd72946834a7e35bb2e9d05940712b993", size = 9711028, upload-time = "2025-07-16T18:47:40.98Z" }, { url = "https://files.pythonhosted.org/packages/f5/02/b178ca48305a230e080732dd02233e0824b1a9b41b273acf2444de116be2/scikit_bio-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3214c2720732c5a467c1b77d36dab70e91a18ac50d4dbe444460838fad907bb9", size = 9709823, upload-time = "2025-07-16T18:47:42.708Z" }, { url = "https://files.pythonhosted.org/packages/5f/a8/199cafe05bb77bf274f0a3db8f5199f4b2966db37589f9e5ec6a212c0b9c/scikit_bio-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:624cff62237f771ffebab63cf0df1605c59aeab35a334bb1feb5726b5b453f98", size = 6193245, upload-time = "2025-07-16T18:47:44.723Z" }, { url = "https://files.pythonhosted.org/packages/d4/31/b943fcf7d4579b4751002ad9a48e62e9f0c7db542dd440f7e3e82f50f58b/scikit_bio-0.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b874e9661000057c92673b0d85fa6e3508644843e3192969cea7c0916acad787", size = 6248217, upload-time = "2025-07-16T18:47:46.201Z" }, { url = "https://files.pythonhosted.org/packages/e7/af/5f3452f0dce0f0a3c7ccb9f067e410b50a8d188c4fb56426f8785317333d/scikit_bio-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77b57b51d1ff844165244d7834862d10a8bfa59866fd24cabfaccac9ad55661a", size = 6188166, upload-time = "2025-07-16T18:47:47.956Z" }, { url = "https://files.pythonhosted.org/packages/7d/20/ef1d2a8accb946cc31b5b987a46d197f69d1e89bab76b5e9cb4235a355f9/scikit_bio-0.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60c00fc17e7fd9f639af889759dd5da627e0efc6c96398bb00d79712756d6fda", size = 9666693, upload-time = "2025-07-16T18:47:51.615Z" }, { url = "https://files.pythonhosted.org/packages/1c/46/3f05eedb58e13c21f5ab7496e9735da2f008aa66038b268ea46138a3e48d/scikit_bio-0.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbc3ab7617eaef5d48b7de2582a9bc8d62fcbdddaa106b8dc61c42a4b0b1effd", size = 9669221, upload-time = "2025-07-16T18:47:53.286Z" }, { url = "https://files.pythonhosted.org/packages/40/e9/2f7d72ef933e9328b929295ada98ad2c5fbdc5b3c6bd2f4d886f14d4f90a/scikit_bio-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:d9ac56afe6119668469aafd96027ed2dff7db17d37475e63c5f291a371dd9363", size = 6190861, upload-time = "2025-07-16T18:47:55.227Z" }, { url = "https://files.pythonhosted.org/packages/b5/fa/3a7770c445111c6b957aba701887f22506faef8145965a6b9d2816cfd896/scikit_bio-0.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f83a75c5fbb123e5d5f0fd36b6655c03b96cd1d91e59362dcee55e4bfa44f0b", size = 6248340, upload-time = "2025-07-16T18:47:56.906Z" }, { url = "https://files.pythonhosted.org/packages/4e/07/e2a29ccbb31208c98ff2c7b61b00614885f781474891ebf249d85495e238/scikit_bio-0.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f81f5ed06d966bdddc67a1724c843e3e1462d342173a3807641ac21cb6e3aee3", size = 6195259, upload-time = "2025-07-16T18:47:58.58Z" }, { url = "https://files.pythonhosted.org/packages/89/15/829f7da8e5083f4d2e3dd1ef24faa6b32da2bf492547bd6d385e92fda127/scikit_bio-0.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25426738c7e5dec96716c0fc931604aed3893f106c2ae5ecc3c4e5c070462b1a", size = 9552546, upload-time = "2025-07-16T18:48:00.374Z" }, { url = "https://files.pythonhosted.org/packages/4a/34/687f9e4dc73e39a56277e2aa900577fccb3c99ef28a5acc5fc258028e2ac/scikit_bio-0.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43c0d63656f897fc947dc91feccf472c989121b80827d3c31e5b44eab64c8027", size = 9508572, upload-time = "2025-07-16T18:48:02.182Z" }, { url = "https://files.pythonhosted.org/packages/6c/a5/75fd40c8bf70836216fffe57bdd80c78959d5cc21c23927ada892b246f3d/scikit_bio-0.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:d76bede1883fde05726d2f90726e020f2dc37d2313c9c61566608f0012df77be", size = 6187164, upload-time = "2025-07-16T18:48:03.957Z" }, ] [[package]] name = "scikit-bio" version = "0.7.1.post1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "array-api-compat", version = "1.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "biom-format", marker = "python_full_version >= '3.10'" }, { name = "decorator", marker = "python_full_version >= '3.10'" }, { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "natsort", marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "patsy", marker = "python_full_version >= '3.10'" }, { name = "requests", marker = "python_full_version >= '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "statsmodels", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/d2/7c8d097db3f158cf2f346cc2586943ee3d85263177ddacc7d663d9f95131/scikit_bio-0.7.1.post1.tar.gz", hash = "sha256:cbd92418b711492837ea5ca3a088b540e725bea53a45bc2332b2631afd539f95", size = 5665966, upload-time = "2025-10-30T19:19:43.042Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/38/5fda6a940bb367ca083acaf393cf4f8d4ef9b9fee59a543db7e84af4dfcc/scikit_bio-0.7.1.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68d4bd49c405d1cda4337899b620027ca6090d011ca900576df70ce97d5febfa", size = 6528024, upload-time = "2025-10-30T19:18:50.83Z" }, { url = "https://files.pythonhosted.org/packages/21/c4/43dc66de1d6699014238928eae7d183e27e5f0838dbaa917711cae95526b/scikit_bio-0.7.1.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b141af25dd831d729a99dcfbf9410bf395bb4f2a10bd1f8d33390383bba6d3e", size = 6533109, upload-time = "2025-10-30T19:18:52.921Z" }, { url = "https://files.pythonhosted.org/packages/b1/e7/c0c3a2b93ecc8a076ec0b526a8596b14d8c97d9d669968ba27e0eb2738a9/scikit_bio-0.7.1.post1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6294f4d24cd5916f45a99291ca197497ff63e3068759a5d05b9141135c760d", size = 10774670, upload-time = "2025-10-30T19:18:54.422Z" }, { url = "https://files.pythonhosted.org/packages/ce/84/618d9eefeb933f0cb080e4d0ce6b561002f7967057a24e52278db067282f/scikit_bio-0.7.1.post1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:683fed27960fb805efc0a2560983035c6e8757a10c033a496c4db46c1f7d207e", size = 10800662, upload-time = "2025-10-30T19:18:57.827Z" }, { url = "https://files.pythonhosted.org/packages/28/44/570eb1a2bda69f03255b4129521ba4e5e045b590855fcd65dec269ef8ee4/scikit_bio-0.7.1.post1-cp310-cp310-win_amd64.whl", hash = "sha256:dd0073860b29561480fc2ef125019e349454497e18dce299d2cbbe93beccd7c6", size = 6447498, upload-time = "2025-10-30T19:18:59.923Z" }, { url = "https://files.pythonhosted.org/packages/e6/91/eb2a2eed239612ec22d69f948cd02a90a86ec0a3eb0ddf468c66e54a8609/scikit_bio-0.7.1.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d0593aa776942aaf27431835698b415d1e9f13cd8cd118f412b8ff4cbc5b8", size = 6537083, upload-time = "2025-10-30T19:19:02.16Z" }, { url = "https://files.pythonhosted.org/packages/74/c2/de147eef13bfcc19cb086061ef52a6dcb33613b535c5b8b903207a18b1f2/scikit_bio-0.7.1.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22c9d1d48fc037afe2bde68c7db479df177564a17781377e0e0c4a613b5283b9", size = 6535208, upload-time = "2025-10-30T19:19:03.481Z" }, { url = "https://files.pythonhosted.org/packages/72/2b/68f1ef336a4285b5898a2910d65ebc3f329035228d0fdabce0f1775402b4/scikit_bio-0.7.1.post1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b78d6a0b52f0acc4a28aa3a5a06b8846e147b8404d84d08e18125828f77a55e", size = 11067343, upload-time = "2025-10-30T19:19:04.995Z" }, { url = "https://files.pythonhosted.org/packages/5c/35/b100d2e6d5e0fd4de4af4c3be9e70c291702afb314b549374331df4f99e4/scikit_bio-0.7.1.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddf2f40b99eea14ecd3196e27e1d9f0b7071856edf8782e1b31fcd39b28c8112", size = 11085265, upload-time = "2025-10-30T19:19:07.205Z" }, { url = "https://files.pythonhosted.org/packages/5c/60/9896f3378ab768af4eb56749bca7eb229be1ef76f6417506fb2437330ed7/scikit_bio-0.7.1.post1-cp311-cp311-win_amd64.whl", hash = "sha256:4689b8e47ecd235c71a883c1cbcaad48b51c8c2676fd6d11d4e4134e1bf675fb", size = 6448277, upload-time = "2025-10-30T19:19:09.266Z" }, { url = "https://files.pythonhosted.org/packages/9a/9e/1709b0eb124cddba864b855df6c428e96c224d99ffb5d008329a5d8caec4/scikit_bio-0.7.1.post1-cp311-cp311-win_arm64.whl", hash = "sha256:593186762927e6248bba3fa20e2656be61c1e3210b0ddefe1428d4e8e01b80fd", size = 6342074, upload-time = "2025-10-30T19:19:10.529Z" }, { url = "https://files.pythonhosted.org/packages/ac/4e/df25894303407646477823d37409b88e9589a64432bcfd97b5c69e290f01/scikit_bio-0.7.1.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b445ed12f486d06fab36c86d1c858b4baa3978c33b7a4f76d967f6ffb7e1375", size = 6539786, upload-time = "2025-10-30T19:19:11.918Z" }, { url = "https://files.pythonhosted.org/packages/a9/e4/f35635cdf167a0335cae98d6b89da08de4f89c80f740304ba642030761ad/scikit_bio-0.7.1.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0373e14eb37403cfb64de65a047ac11b91b8d83534e98fe9e4404c3d387a25e9", size = 6544189, upload-time = "2025-10-30T19:19:13.261Z" }, { url = "https://files.pythonhosted.org/packages/cc/5a/25e6fa4db4e0cdbaecf395af06f8d95830ad1e57b0daf176bebe1c957ad6/scikit_bio-0.7.1.post1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd873c615777ab28c448921fc98639897c37ea58412fd704cfe1a96b9bedd0f6", size = 10887784, upload-time = "2025-10-30T19:19:14.829Z" }, { url = "https://files.pythonhosted.org/packages/78/fd/45a25d9c4ff917582f951f08c2f06ab719802f3d4f9a6dcb4fc291c763f7/scikit_bio-0.7.1.post1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb724ab26b393a074cbb5e9bb47277284a1dc68e70684147a6d0a3ea2d822cdd", size = 10946298, upload-time = "2025-10-30T19:19:17.003Z" }, { url = "https://files.pythonhosted.org/packages/ad/cd/cea3b6fbe18cd94226e38196e71b4bdb57b187722ade8df63b38a0c15560/scikit_bio-0.7.1.post1-cp312-cp312-win_amd64.whl", hash = "sha256:5e93c64077e815752d7c27b5117ac498b3847a4cf3332aa71a8fcf601ed0fe86", size = 6459262, upload-time = "2025-10-30T19:19:19.349Z" }, { url = "https://files.pythonhosted.org/packages/d9/61/8dcdd863634f77db5dac566cdd4fe09a01786e42f0ccf06d32e91f68f5a8/scikit_bio-0.7.1.post1-cp312-cp312-win_arm64.whl", hash = "sha256:d86eaeb7b1e101d1d8fa79de2a23ef76697e2252f945884f5ac08f829f0fe1f9", size = 6343827, upload-time = "2025-10-30T19:19:20.77Z" }, { url = "https://files.pythonhosted.org/packages/fd/96/a8c8bd5cddbc0159f67b8efe399b4f786887608292edd90e57b669dfb58c/scikit_bio-0.7.1.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8269b297ade34ed58939c1f8d5b9506071d2160ce83277f29b938fdad29fbc40", size = 6532159, upload-time = "2025-10-30T19:19:22.223Z" }, { url = "https://files.pythonhosted.org/packages/8b/e3/679a58a84b5c863b27ba130f3691083f6bb71c3f3142a20a1ebce5f3988f/scikit_bio-0.7.1.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d478ed7858e33d7af9a4bf9c267d46e46eae8194c8ed83fc62f23607bf660f1", size = 6536898, upload-time = "2025-10-30T19:19:24.037Z" }, { url = "https://files.pythonhosted.org/packages/e7/65/c776d8d32e69d2074bc2eb8c46c7807e1ac572548da9fe950eac249ede4f/scikit_bio-0.7.1.post1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bccf748c80dc9779ed93522513c842473de460e58e76bfa128ddf3cfdea2f604", size = 10848935, upload-time = "2025-10-30T19:19:26.074Z" }, { url = "https://files.pythonhosted.org/packages/f4/e0/fc497c545850e2e3683e8a30139dccfff0616f02658dfbd6b5f92bf2499d/scikit_bio-0.7.1.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ebdd76585f65a3e28f90833c3e4f5fc8241b36c1378bc65abad3330142fb504", size = 10899404, upload-time = "2025-10-30T19:19:27.821Z" }, { url = "https://files.pythonhosted.org/packages/71/5c/fb855a6f767c88c82bb28ec125457ea8d654b3187aea49bd2696df923513/scikit_bio-0.7.1.post1-cp313-cp313-win_amd64.whl", hash = "sha256:9ec664c8a9da363941570e0a2fc7813d57530b7de2ea1dba71487daaef3be9af", size = 6456865, upload-time = "2025-10-30T19:19:29.613Z" }, { url = "https://files.pythonhosted.org/packages/84/3d/f7549b21f314b2a23f5a6148e0a17c88d8170bd1599ae8c735f492a01ce5/scikit_bio-0.7.1.post1-cp313-cp313-win_arm64.whl", hash = "sha256:d8890f3f39d12f2fc2cbce2c8b84081d6ccdadf87c870b80e5bf45826b7262b8", size = 6342824, upload-time = "2025-10-30T19:19:31.184Z" }, { url = "https://files.pythonhosted.org/packages/29/7f/11d1c5e10001870e42f91e0236d6bb3fabd1bd0f6d43eb149fe74596e951/scikit_bio-0.7.1.post1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1bdb3279fa601edc31ecd0b9635ec7dfac594a441124fffafca5abaa3b68ad6c", size = 6530286, upload-time = "2025-10-30T19:19:32.857Z" }, { url = "https://files.pythonhosted.org/packages/6b/73/60707444c38f8a791046d16c0f54dc4fa98e4d34c01e38e306799a2f0754/scikit_bio-0.7.1.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bbd0f0d7928e5f65774b3b63d5581f813989d2f900892864d49aed24cbf6f265", size = 6543599, upload-time = "2025-10-30T19:19:34.188Z" }, { url = "https://files.pythonhosted.org/packages/ee/e4/5a06cce37472df5503192435637a51ee3a658e4ca7a7c2671f0a4f32737d/scikit_bio-0.7.1.post1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4eca29267f503d9900350013f786d04710f81d0f3254e4a7b6121892ddd202", size = 10855453, upload-time = "2025-10-30T19:19:35.678Z" }, { url = "https://files.pythonhosted.org/packages/d8/6d/5700bb155429a6aa74ad8ef94a905f52c901fd6de2c7d03b99222e77f1bf/scikit_bio-0.7.1.post1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:286b1f9d39cd23253bc94b7e510bfabc993dcf8be8062b3426afd14e3e0756cc", size = 10849124, upload-time = "2025-10-30T19:19:37.452Z" }, { url = "https://files.pythonhosted.org/packages/f4/39/05c6f52c53f16b00003099db1ad9b51f037a90382fe7e31c96376896efdc/scikit_bio-0.7.1.post1-cp314-cp314-win_amd64.whl", hash = "sha256:58b7030cfebd3af7444d8f7c1dd2184d296c5f94ba0163ca3e4b62f3e8dd0035", size = 6453898, upload-time = "2025-10-30T19:19:39.581Z" }, { url = "https://files.pythonhosted.org/packages/ff/95/ff64d260b2dd308338842287b0a165eb3b97e123ead5ff5206862475f812/scikit_bio-0.7.1.post1-cp314-cp314-win_arm64.whl", hash = "sha256:56e63b87fc01f72f247de616069ecd1142cfca5e1c10c2f72611703981841c41", size = 6342299, upload-time = "2025-10-30T19:19:41.73Z" }, ] [[package]] name = "scipy" version = "1.13.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, ] [[package]] name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] [[package]] name = "scipy" version = "1.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, ] [[package]] name = "seaborn" version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] [[package]] name = "secretstorage" version = "3.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "cryptography", marker = "python_full_version < '3.10'" }, { name = "jeepney", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] [[package]] name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "cryptography", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, { name = "jeepney", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "setuptools" version = "82.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "sphinx" version = "7.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "babel", marker = "python_full_version < '3.10'" }, { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "imagesize", marker = "python_full_version < '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, ] [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "babel", marker = "python_full_version == '3.10.*'" }, { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "imagesize", marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "packaging", marker = "python_full_version == '3.10.*'" }, { name = "pygments", marker = "python_full_version == '3.10.*'" }, { name = "requests", marker = "python_full_version == '3.10.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "babel", marker = "python_full_version == '3.11.*'" }, { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "imagesize", marker = "python_full_version == '3.11.*'" }, { name = "jinja2", marker = "python_full_version == '3.11.*'" }, { name = "packaging", marker = "python_full_version == '3.11.*'" }, { name = "pygments", marker = "python_full_version == '3.11.*'" }, { name = "requests", marker = "python_full_version == '3.11.*'" }, { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] [[package]] name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "babel", marker = "python_full_version >= '3.12'" }, { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "imagesize", marker = "python_full_version >= '3.12'" }, { name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "requests", marker = "python_full_version >= '3.12'" }, { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinx-rtd-theme" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jquery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, ] [[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, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[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, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[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, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, ] [[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, upload-time = "2019-01-21T16:10:16.347Z" } 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, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[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, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[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, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "statsmodels" version = "0.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "patsy" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, { url = "https://files.pythonhosted.org/packages/b6/c1/f3012162d55b43291267d15275433b208f63d2e91a4f82ad724679336d17/statsmodels-0.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d0c1b0f9f6915619e2a0d3853e5763d4d66876892ad352e7d7b93a737556978", size = 10151985, upload-time = "2025-12-05T23:14:22.161Z" }, { url = "https://files.pythonhosted.org/packages/0b/d9/9bcd801ae2881884848bd53b5dc985e8d2a1b20cb1a0350f2a4b4dbfce24/statsmodels-0.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e0fc891d6358bf376cc0ae1fee10a650478172ae9ba359daba1785fc496cd1a", size = 10031652, upload-time = "2025-12-05T23:14:37.709Z" }, { url = "https://files.pythonhosted.org/packages/62/37/3b609324f22c151267784c5830d3afef2cc7b22970d6cf957f1b799fca3b/statsmodels-0.14.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f52ef0f0b63b8fd11e1ef1c2a1e73a410720b8715c9a83a26d733b6815597fe", size = 10121095, upload-time = "2025-12-05T23:14:53.169Z" }, { url = "https://files.pythonhosted.org/packages/40/4d/adf7615db9cc7802608b6343789e66ff6e8220f1a84066e502fe51e4f90f/statsmodels-0.14.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b328eafa86a2a67303fdb1d25677d15b70cd2a5229aabec7670ec5ea840f1375", size = 10411609, upload-time = "2025-12-05T23:15:09.022Z" }, { url = "https://files.pythonhosted.org/packages/3b/9d/a3d33f4bda4e6a5f9b1118e81f93d5bc1620ad8d685df15d79b291ad9b7f/statsmodels-0.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:3bef39f8587754f2d644b2e831e102fa08ace9a5a1af4b583b122e6fd3e083ab", size = 9590613, upload-time = "2025-12-05T23:15:24.013Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tqdm" version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "twine" version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "id" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, { name = "packaging" }, { name = "readme-renderer" }, { name = "requests" }, { name = "requests-toolbelt" }, { name = "rfc3986" }, { name = "rich" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "werkzeug" version = "3.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ]