diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 172ab350..48782979 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -1,41 +1,29 @@
name: Coverage (Codecov)
on:
- push:
- branches: [ "main", "develop" ]
pull_request:
- branches: [ "main", "develop" ]
+ branches: ["main", "develop"]
jobs:
- test:
- name: Run tests and upload coverage
+ coverage:
+ name: Compute coverage
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - name: Set up Python 3.10
- uses: actions/setup-python@v5
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
with:
- python-version: "3.10"
+ enable-cache: true
- - name: Install pandoc
- uses: pandoc/actions/setup@v1
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- python -m pip install coverage
- python -m pip install sphinx myst_parser sphinx-rtd-theme nbsphinx pandoc jupyter jupyterlab
- pip install -e .
+ - name: Install the project
+ run: uv sync --locked --all-extras --dev
- name: Tests with coverage
- run: |
- coverage run --omit='./results/*','./docs/*','./examples/*' -m unittest discover tests
- coverage xml -o coverage.xml
+ run: uv run pytest --cov=nnodely --cov-branch --cov-report=xml tests
- name: Upload results to Codecov
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- files: coverage.xml
\ No newline at end of file
+ uses: codecov/codecov-action@v6
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 605f4a43..3207b354 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -1,130 +1,101 @@
-name: Publish New nnodely Release
+name: Publish
on:
push:
tags:
- - "v*" # Push events to matching v*, i.e., v1.0, v20.15.10
+ - "v*"
jobs:
- check-tag-version:
- name: Check tag version equal to file version
+ publish:
+ name: Build and publish to both TestPyPi and PyPi
runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ permissions:
+ id-token: write
+ contents: read
steps:
- - name: Checkout code
- uses: actions/checkout@v2
- - name: Extract tag version
+ - uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ enable-cache: true
+
+ - name: Validate tag matches project version
run: |
- TAG_NAME="${{ github.ref_name }}"
- TAG_VERSION=$(echo $TAG_NAME | sed 's/v//')
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
+ PROJ_VERSION=$(uv version --short)
+
echo "Tag version: $TAG_VERSION"
- echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV
- - name: Extract file version
- run: |
- FILE_VERSION=$(awk -F"'" '/^__version__/ {print $2}' nnodely/__init__.py)
- echo "File version: $FILE_VERSION"
- echo "FILE_VERSION=$FILE_VERSION" >> $GITHUB_ENV
- - name: Show tag and file versions
- run: |
- echo "The tag version: ${{ env.TAG_VERSION }}"
- echo "The file version: ${{ env.FILE_VERSION }}"
- - name: Check if tag and file versions match
- if: ${{ env.TAG_VERSION != env.FILE_VERSION }}
- run: |
- echo "The tag version ${{ env.TAG_VERSION }} does not match file version ${{ env.FILE_VERSION }}"
- exit 1
+ echo "Project version: $PROJ_VERSION"
- build:
- name: Build distribution of tagged version
- needs:
- - check-tag-version
- runs-on: ubuntu-latest
+ if [ "$TAG_VERSION" != "$PROJ_VERSION" ]; then
+ echo "Mismatch: tag=$TAG_VERSION project=$PROJ_VERSION"
+ exit 1
+ fi
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ github.ref_name }}
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.x"
-
- - name: Install pypa/build
- run: >-
- python3 -m
- pip install
- build
- --user
- - name: Build a binary wheel and a source tarball
- run: python3 -m build
- - name: Store the distribution packages
- uses: actions/upload-artifact@v4
- with:
- name: python-package-distributions
- path: dist/
-
- publish-to-pypi:
- name: Publish to PyPI
- needs:
- - build
+ - name: Build
+ run: uv build
- runs-on: ubuntu-latest
- environment:
- name: pypi
- url: https://pypi.org/p/nnodely # Replace with your PyPI project name
- permissions:
- id-token: write # IMPORTANT: mandatory for trusted publishing
+ # Check that basic features work and we didn't miss to include crucial files
+ - name: Basic test (wheel)
+ run: uv run --isolated --no-project --with dist/*.whl python -c "import nnodely"
- steps:
- - name: Download all the dists
- uses: actions/download-artifact@v4
- with:
- name: python-package-distributions
- path: dist/
- - name: Publish nnodely to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
+ - name: Basic test (source distribution)
+ run: uv run --isolated --no-project --with dist/*.tar.gz python -c "import nnodely"
+
+ - name: TestPyPi publish
+ run: uv publish --index testpypi
+
+ - name: PyPI publish
+ run: uv publish
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: python-package-distributions
+ path: dist/
github-release:
- name: >-
- Sign the Python with Sigstore and upload them to GitHub Release
- needs:
- - publish-to-pypi
+ name: Sign the Python with Sigstore and upload them to GitHub Release
runs-on: ubuntu-latest
+ needs:
+ - publish
permissions:
- contents: write # IMPORTANT: mandatory for making GitHub Releases
- id-token: write # IMPORTANT: mandatory for sigstore
+ contents: write
+ id-token: write
steps:
- - name: Download all the dists
- uses: actions/download-artifact@v4
- with:
- name: python-package-distributions
- path: dist/
- - name: Sign the dists with Sigstore
- uses: sigstore/gh-action-sigstore-python@v3.0.0
- with:
- inputs: >-
- ./dist/*.tar.gz
- ./dist/*.whl
- - name: Get the branch version
- run: |
- TAG_NAME="${{ github.ref_name }}"
- TAG_MESSAGE=${{ github.event.workflow_run.head_commit.message }}
- echo "Tag message: $TAG_MESSAGE"
- echo "TAG_MESSAGE=$TAG_MESSAGE" >> $GITHUB_ENV
- - name: Create GitHub release
- env:
- GITHUB_TOKEN: ${{ github.token }}
- tag: ${{ github.ref_name }}
- run: |
- echo "The branch version: $tag"
- gh release create $tag --title "$tag" --repo ${{ github.repository }} --notes "${{ env.TAG_MESSAGE }}"
- - name: Upload artifact signatures to GitHub Release
- env:
- GITHUB_TOKEN: ${{ github.token }}
- tag: ${{ github.ref_name }}
- # Upload to GitHub Release using the `gh` CLI.
- # `dist/` contains the built packages, and the
- # sigstore-produced signatures and certificates.
- run: >-
- gh release upload $tag dist/** --repo ${{ github.repository }}
\ No newline at end of file
+ - uses: actions/download-artifact@v8
+ with:
+ name: python-package-distributions
+ path: dist/
+
+ - name: Sign artifacts
+ uses: sigstore/gh-action-sigstore-python@v3.0.0
+ with:
+ inputs: |
+ ./dist/*.tar.gz
+ ./dist/*.whl
+ - name: Detect prerelease
+ id: version
+ run: |
+ VERSION="${GITHUB_REF_NAME#v}"
+
+ if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "PRERELEASE=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "PRERELEASE=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Release
+ uses: softprops/action-gh-release@v3
+ if: github.ref_type == 'tag'
+ with:
+ files: |
+ dist/*.tar.gz
+ dist/*.whl
+ generate_release_notes: true
+ prerelease: ${{ steps.version.outputs.PRERELEASE }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..57db3ccc
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,23 @@
+name: Lint
+
+on:
+ pull_request:
+ branches: ["main", "develop"]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ enable-cache: true
+
+ - name: Install the project
+ run: uv sync --locked --all-extras --dev
+
+ - name: Lint
+ run: uv run ruff check
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index cec03b02..dccfb921 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -1,51 +1,29 @@
-# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
-
name: Run Tests
on:
- push:
- branches: [ "main", "develop" ]
pull_request:
- branches: [ "main", "develop"]
+ branches: ["main", "develop"]
jobs:
- build:
-
+ test:
runs-on: ${{ matrix.os }}
strategy:
- fail-fast: false
+ fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: [ "3.10", "3.11", "3.12"]
- architecture: ['x64', 'x86']
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- - uses: actions/checkout@v4
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v3
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install pandoc
- uses: pandoc/actions/setup@v1
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- python -m pip install flake8 pytest
- python -m pip install sphinx myst_parser sphinx-rtd-theme nbsphinx pandoc jupyter jupyterlab
- pip install -e .
- - name: Lint with flake8
- run: |
- # stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- - name: Test with pytest
- run: |
- pytest
-
-
-
+ - uses: actions/checkout@v6
+ - name: Install uv and set the Python version
+ uses: astral-sh/setup-uv@v7
+ with:
+ enable-cache: true
+ python-version: ${{ matrix.python-version }}
+ - name: Install the project
+ run: uv sync --locked --all-extras --dev
+ - name: Run tests
+ run: uv run pytest tests
diff --git a/.gitignore b/.gitignore
index 3a8652ff..1eaa0598 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,182 +1,15 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-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/
-docs/out_docs/
-
-# 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/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# 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
+__pycache__
.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 found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
-
-# vscode test
.vscode
+!.vscode/settings.json
+!.vscode/extensions.json
-# MacOS system files
-.DS_Store
-
-# default results folder
results
+dist/
+docs/_build
+docs/out_docs
-# default temp folder
-temp
-
-# Todo for testing
-TODO/
-
-#trained_models folder
-trained_models
-trained_models_torch
\ No newline at end of file
+.DS_Store
+.coverage
+coverage.xml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..02018fe7
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,41 @@
+default_install_hook_types:
+ - pre-commit
+ - post-merge
+ - post-rewrite
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.3.0
+ hooks:
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: local
+ hooks:
+ - id: uv-sync
+ name: uv sync
+ entry: uv sync
+ language: system
+ pass_filenames: false
+ stages: [post-merge, post-rewrite]
+ - id: uv-lock
+ name: uv lock
+ entry: uv lock
+ language: system
+ pass_filenames: false
+ - id: ruff-check
+ name: ruff check
+ entry: uv run ruff check --fix
+ language: system
+ types: [python]
+ - id: ruff-format
+ name: ruff format
+ entry: uv run ruff format
+ language: system
+ types: [python]
+ - id: pytest
+ name: pytest
+ entry: uv run pytest
+ language: system
+ pass_filenames: false
+ types: [python]
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..24ee5b1b
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index a2940afa..c0248145 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,5 +22,3 @@ python:
- method: pip
path: .
- requirements: docs/requirements.txt
-
-
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..25a30545
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "ms-python.python",
+ "ms-python.vscode-pylance",
+ "charliermarsh.ruff"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..d410bbd2
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "python.languageServer": "Pylance",
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff",
+ "editor.formatOnSave": true
+ },
+ "ruff.enable": true,
+ "ruff.lint.enable": true
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..dc962b34
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,173 @@
+# Contributing
+
+Thanks for your interest in **nnodely**! 🎉
+We're just getting started and welcome contributions of all kinds — bug
+reports, documentation fixes, examples, and new features.
+
+## Setup
+
+All you need to do is:
+
+- install [uv](https://docs.astral.sh/uv/)
+- clone the repo
+- run `uv sync --dev`
+- install the hooks `uv run pre-commit install --install-hooks`
+
+You are now up and running, just make sure to run everything via `uv` (e.g.,
+`uv run ...`).
+
+### IDE
+
+We have config files for the IDEs we use, so `VSCode`, `PyCharm`, and `neovim`
+should be ready to go.
+
+- `VSCode`: you can find a `.vscode` folder which should prompt you to install
+ the necessary plugins and enable them as we expect them to be. Nonetheless
+ know that it will use `PyLance` and not bare `pyright` so you will see
+ different diagnostics. It should pick up the `uv` managed environment right
+ away.
+- `PyCharm`: enable `pyright` and `ruff` and you should be good to go. It
+ should also pick up `uv` automatically.
+- `neovim`: you have to add the `pyright` and `ruff` (e.g., using `mason`) and
+ enable them (e.g., `vim.lsp.enable(...)`). To pick up `uv` just run `uv run
+nvim .` in the project root.
+
+## Code Style
+
+This project follows a strict and largely automated Python code style. The goal
+is to keep the codebase consistent, readable, and easy to review, while
+minimising style-related discussion in PRs.
+
+In short: **let the tools do the work**.
+
+---
+
+## General Principles
+
+- Prefer clarity over cleverness.
+- Keep functions and classes small and focused.
+- Be consistent with existing code.
+
+---
+
+## Python Version
+
+We target all the [supported Python
+versions](https://devguide.python.org/versions/). Tests will catch most of the
+version specific behaviour, but please keep it in mind.
+
+---
+
+## Linting and Formatting
+
+This project uses `ruff` for both linting and formatting.
+
+- All code **must** pass `ruff` checks.
+- Formatting is enforced via `ruff format`.
+- Do **not** manually fight the formatter.
+
+A `pre-commit` hook will take care of this, but you can also run it manually with:
+
+```bash
+uv run ruff check --fix
+uv run ruff format
+```
+
+### Ignoring Rules and Formatting
+
+Disabling rules or formatting should be rare and justified:
+
+```python
+value = legacy_call() # noqa: PLW0603 # required by external API
+
+# fmt: off
+table = [
+ ("short", 1),
+ ("muchlonger", 2),
+]
+# fmt: on
+```
+
+---
+
+## Naming Conventions
+
+We use:
+
+- `snake_case` for functions and variables
+- `CamelCase` for classes
+- `UPPERCASE` for constants
+
+---
+
+## Type Hints
+
+- Required unless impractical.
+- Make custom types whenever your type gets too big, for example:
+
+ ```python
+ # This horrible mess
+ list[dict[str, list[int]]]
+
+ # Should become
+ CustomType: TypeAlias = list[dict[str, list[int]]]
+ ```
+
+- Heavily prefer strong typing (e.g., `Enum` and `dataclass`), for example:
+
+ ```python
+ # Instead of this
+ def func(flag: str) -> None:
+ ...
+
+ # Do this
+ class Flag(Enum):
+ ...
+
+ def func(flag: Flag) -> None:
+ ...
+ ```
+
+---
+
+## Docstrings and Comments
+
+- Use docstrings for public modules, classes, and functions
+- Follow the project's configured docstring style
+- Comments should explain why, not what
+- Avoid obvious or redundant comments
+
+---
+
+## Branching
+
+### Branching Model
+
+- `main` is the default branch
+ - Always stable.
+ - Always releasable.
+ - Protected (no direct commits).
+- `develop` is the rolling branch
+ - Put all development not quite stable yet.
+ - Protected (no direct commits).
+- All work happens on branches created from `develop`.
+
+### Branch Naming
+
+We follow [this](https://conventional-branch.github.io).
+
+---
+
+## Commit Messages
+
+We follow [this](https://www.conventionalcommits.org/en/v1.0.0/).
+
+---
+
+## GitHub Actions
+
+Testing GitHub Actions is a pain, but it becomes easier if you test at least
+some of their functionality with [act](https://github.com/nektos/act).
+
+For example, to test the `codecov.xml` action just setup `act` and run: `act
+--workflows .github/workflows/codecov.yml`.
diff --git a/README.md b/README.md
index 46da35cb..f7d87cf0 100644
--- a/README.md
+++ b/README.md
@@ -12,12 +12,12 @@
Modeling, control, and estimation of physical systems are central to many engineering disciplines. While data-driven methods like neural networks offer powerful tools, they often struggle to **incorporate prior domain knowledge**, limiting their interpretability, generalizability, and safety.
-To bridge this gap, we present ***nnodely*** (where "nn" can be read as "m," forming *Modely*) — a framework that facilitates the creation and deployment of **Model-Structured Neural Networks** (**MS-NNs**).
+To bridge this gap, we present ***nnodely*** (where "nn" can be read as "m," forming *Modely*) — a framework that facilitates the creation and deployment of **Model-Structured Neural Networks** (**MS-NNs**).
MS-NNs combine the learning capabilities of neural networks with structural **priors** grounded in **physics, control, and estimation theory**, enabling:
-- **Reduced training data** requirements
-- **Generalization** to unseen scenarios
-- **Real-time** deployment in real-world applications
+- **Reduced training data** requirements
+- **Generalization** to unseen scenarios
+- **Real-time** deployment in real-world applications
In short:
@@ -126,10 +126,10 @@ The `nnodely` main class defined in __nnodely.py__, it contains all the main pro
2. __loader.py__ contains the function for managing the dataset, the main function is `dataLoad`.
3. __trainer.py__ contains the function for training the network as the `trainModel`.
4. __exporter.py__ contains all the function for import and export: `saveModel`, `loadModel`, `exportONNX` etc..
-5. __validator.py__ contains all the function for validate the model and the `resultsAnalysis`.
+5. __validator.py__ contains all the function for validate the model and the `resultsAnalysis`.
6. All the operators derive from `Network` defined in __network.py__, that contains the shared support functions for all the operators.
-The folder `basic/` contains the main classes for the low level functionalities:
+The folder `basic/` contains the main classes for the low level functionalities:
1. __model.py__ containts the pytorch template model for the structured network.
2. __modeldef.py__ containts the operation for work with the json model definition.
3. __loss.py__ contains the loss functions.
@@ -153,14 +153,14 @@ The main basic layers without parameters are:
2. __arithmetic.py__ this file contains the aritmetic functions as: +, -, /, *., **.
3. __trigonometric.py__ this file contains all the trigonometric functions.
4. __part.py__ are used for selecting part of the data.
-5. __fuzzify.py__ contains the operation for the fuzzification of a variable,
+5. __fuzzify.py__ contains the operation for the fuzzification of a variable,
commonly used in the local model as activation function as in [[1]](#1) with rectangular activation functions or in [[3]](#3), [[4]](#4) and [[5]](#5) with triangular activation function activation functions.
Using fuzzification it is also possible create a channel coding as presented in [[2]](#2).
The main basic layers with parameters are:
-1. __fir.py__ this file contains the finite impulse response filter function. It is a linear operation on the time dimension (second dimension).
+1. __fir.py__ this file contains the finite impulse response filter function. It is a linear operation on the time dimension (second dimension).
This filter was introduced in [[1]](#1).
-2. __linear.py__ this file contains the linear function. Typical Linear operation `W*x+b` operated on the space dimension (third dimension).
+2. __linear.py__ this file contains the linear function. Typical Linear operation `W*x+b` operated on the space dimension (third dimension).
This operation is presented in [[1]](#1).
3. __localmodel.py__ this file contains the logic for build a local model. This operation is presented in [[1]](#1), [[3]](#3), [[4]](#4) and [[5]](#5).
4. __parametricfunction.py__ are the user custom function. The function can use the pytorch syntax. A parametric function is presented in [[3]](#3), [[4]](#4), [[5]](#5).
@@ -196,8 +196,8 @@ This folder contains the images used in the documentation.
To contribute to the nnodely framework, you can:
-- Open a pull request if you have a new feature or bug fix.
-- Open an issue if you have a question or suggestion.
+- Open a pull request if you have a new feature or bug fix.
+- Open an issue if you have a question or suggestion.
We welcome contributions and collaborations.
@@ -212,53 +212,53 @@ This project is released under the license [License: MIT](https://opensource.org
## References
-[1]
-Mauro Da Lio, Daniele Bortoluzzi, Gastone Pietro Rosati Papini. (2019).
-Modelling longitudinal vehicle dynamics with neural networks.
+[1]
+Mauro Da Lio, Daniele Bortoluzzi, Gastone Pietro Rosati Papini. (2019).
+Modelling longitudinal vehicle dynamics with neural networks.
Vehicle System Dynamics. https://doi.org/10.1080/00423114.2019.1638947 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/model_longit_vehicle_dynamics/model_longit_vehicle_dynamics.py))
-[2]
-Alice Plebe, Mauro Da Lio, Daniele Bortoluzzi. (2019).
-On Reliable Neural Network Sensorimotor Control in Autonomous Vehicles.
+[2]
+Alice Plebe, Mauro Da Lio, Daniele Bortoluzzi. (2019).
+On Reliable Neural Network Sensorimotor Control in Autonomous Vehicles.
IEEE Transaction on Intelligent Transportation System. https://doi.org/10.1109/TITS.2019.2896375
-[3]
-Mauro Da Lio, Riccardo Donà , Gastone Pietro Rosati Papini, Francesco Biral, Henrik Svensson. (2020).
+[3]
+Mauro Da Lio, Riccardo Donà , Gastone Pietro Rosati Papini, Francesco Biral, Henrik Svensson. (2020).
A Mental Simulation Approach for Learning Neural-Network Predictive Control (in Self-Driving Cars).
IEEE Access. https://doi.org/10.1109/ACCESS.2020.3032780 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/model_lateral_vehicle_dynamics/model_lateral_vehicle_dynamics.ipynb))
-[4]
-Edoardo Pagot, Mattia Piccinini, Enrico Bertolazzi, Francesco Biral. (2023).
+[4]
+Edoardo Pagot, Mattia Piccinini, Enrico Bertolazzi, Francesco Biral. (2023).
Fast Planning and Tracking of Complex Autonomous Parking Maneuvers With Optimal Control and Pseudo-Neural Networks.
IEEE Access. https://doi.org/10.1109/ACCESS.2023.3330431 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_car_parking/control_steer_car_parking.ipynb))
-[5]
+[5]
Mattia Piccinini, Sebastiano Taddei, Matteo Larcher, Mattia Piazza, Francesco Biral. (2023).
A Physics-Driven Artificial Agent for Online Time-Optimal Vehicle Motion Planning and Control.
IEEE Access. https://doi.org/10.1109/ACCESS.2023.3274836 (look [[code basic]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_artificial_race_driver/control_steer_artificial_race_driver.ipynb)
and [[code extended]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_artificial_race_driver_extended/control_steer_artificial_race_driver_extended.ipynb))
-[6]
+[6]
Hector Perez-Villeda, Justus Piater, Matteo Saveriano. (2023).
Learning and extrapolation of robotic skills using task-parameterized equation learner networks.
Robotics and Autonomous Systems. https://doi.org/10.1016/j.robot.2022.104309 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/equation_learner/equation_learner.ipynb))
-[7]
+[7]
M. Raissi. P. Perdikaris b, G.E. Karniadakis a. (2019).
Physics-informed neural networks: A deep learning framework for solving forward and inverse problems involving nonlinear partial differential equations
Journal of Computational Physics. https://doi.org/10.1016/j.jcp.2018.10.045 (look the [[example Burger's equation]](https://github.com/tonegas/nnodely-applications/blob/main/pinn/pinn_Burgers_equation.ipynb))
-[8]
+[8]
Wojciech Marian Czarnecki, Simon Osindero, Max Jaderberg, Grzegorz Åšwirszcz, Razvan Pascanu. (2017).
Sobolev Training for Neural Networks.
arXiv. https://doi.org/10.48550/arXiv.1706.04859 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/sobolev/Sobolev_learning.ipynb))
-[9]
+[9]
Mattia Piccinini, Matteo Zumerle, Johannes Betz, Gastone Pietro Rosati Papini. (2025).
A Road Friction-Aware Anti-Lock Braking System Based on Model-Structured Neural Networks.
IEEE Open Journal of Intelligent Transportation Systems. https://doi.org/10.1109/OJITS.2025.3563347 (look at the [[code]](https://github.com/tonegas/nnodely-applications/tree/main/vehicle/road_friction_aware_ABS))
-[10]
+[10]
Mauro Da Lio, Mattia Piccinini, Francesco Biral. (2023).
Robust and Sample-Efficient Estimation of Vehicle Lateral Velocity Using Neural Networks With Explainable Structure Informed by Kinematic Principles.
IEEE Transactions on Intelligent Transportation Systems. https://doi.org/10.1109/TITS.2023.3303776
diff --git a/case-studies/README.md b/case-studies/README.md
index f65f7b18..2ec3b9aa 100644
--- a/case-studies/README.md
+++ b/case-studies/README.md
@@ -8,9 +8,9 @@ The subfolders are organized as follows:
Contains three Python notebooks (plus all the other relative files) relative to the lateral vehicle dynamics model:
-- `lateral_dynamics_model.ipynp`: Design of the lateral vehicle dynamics model as presented in the paper
+- `lateral_dynamics_model.ipynp`: Design of the lateral vehicle dynamics model as presented in the paper
- `lateral_dynamics_control.ipynp`: Design and training of the lateral controller as presented in the paper
-- `lateral_dynamics_model_torch.ipynp`: A comparative implementation of the lateral dynamics model developed in native **PyTorch**
+- `lateral_dynamics_model_torch.ipynp`: A comparative implementation of the lateral dynamics model developed in native **PyTorch**
## 2. `mass_spring_damper`
@@ -25,4 +25,4 @@ Provides an example implementation of a Physics-Informed Neural Network (PINN),
## 4. `neuralODE`
-Contains a preliminary implementation of Neural ODEs within the nnodely framework, illustrating continuous-time modeling via parametric functions and integration operators for the mass-spring-damper-system.
\ No newline at end of file
+Contains a preliminary implementation of Neural ODEs within the nnodely framework, illustrating continuous-time modeling via parametric functions and integration operators for the mass-spring-damper-system.
diff --git a/case-studies/lateral_dynamics/dataset/test/test_set.csv b/case-studies/lateral_dynamics/dataset/test/test_set.csv
index 9c323599..c1555ec9 100644
--- a/case-studies/lateral_dynamics/dataset/test/test_set.csv
+++ b/case-studies/lateral_dynamics/dataset/test/test_set.csv
@@ -1103,4 +1103,4 @@ time,ax,ay,vx,curv,steer
55.146002028075564,1.0840258510987684,-2.6915592509185298,20.453059684147426,-0.006434242005669751,-0.3608481332433391
55.195998943798315,1.0851748822292842,-2.697148256568109,20.507079535715768,-0.006413818184476849,-0.35926049452967035
55.24600044829564,1.0880632184191892,-2.6997673049851385,20.561189941463603,-0.0063864243157048545,-0.3573006906046246
-55.2959998400224,1.0925590253392408,-2.699641748410116,20.615475400176003,-0.006352663418874661,-0.3550151089245937
\ No newline at end of file
+55.2959998400224,1.0925590253392408,-2.699641748410116,20.615475400176003,-0.006352663418874661,-0.3550151089245937
diff --git a/case-studies/lateral_dynamics/dataset/training/test1.csv b/case-studies/lateral_dynamics/dataset/training/test1.csv
index b9d55044..4fdce233 100644
--- a/case-studies/lateral_dynamics/dataset/training/test1.csv
+++ b/case-studies/lateral_dynamics/dataset/training/test1.csv
@@ -3722,4 +3722,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s
186,-0.1071997657,-1.682893157,26.17891693,-0.002456563614,0,0.06283919513,-0.1276736856,2305.977185
186.05,-0.1293833256,-1.641584277,26.17271233,-0.002397477173,0,0.05899731815,-0.1237370819,2307.311642
186.1,-0.1502973288,-1.598474264,26.16544533,-0.002335893822,0,0.05537641793,-0.1197138876,2308.640748
-186.15,-0.1705370843,-1.553611755,26.15716553,-0.002271854197,0,0.05186513811,-0.1156224832,2309.964296
\ No newline at end of file
+186.15,-0.1705370843,-1.553611755,26.15716553,-0.002271854197,0,0.05186513811,-0.1156224832,2309.964296
diff --git a/case-studies/lateral_dynamics/dataset/training/test2.csv b/case-studies/lateral_dynamics/dataset/training/test2.csv
index dec669df..0c06446d 100644
--- a/case-studies/lateral_dynamics/dataset/training/test2.csv
+++ b/case-studies/lateral_dynamics/dataset/training/test2.csv
@@ -2440,4 +2440,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s
121.9,0.009731287137,-1.63340795,27.77533531,-0.00211779615,0,0.08593299985,-0.1145167798,2307.353851
121.95,0.009520187043,-1.607111335,27.77551842,-0.002083710209,0,0.08586709201,-0.1123358831,2308.764477
122,0.009291882627,-1.579890013,27.77569962,-0.002048425176,0,0.08579877764,-0.1101001278,2310.169094
-122.038,0,0,0,-0.002021582034,0,0.0857468918,-0.1084142625,0.04364157261
\ No newline at end of file
+122.038,0,0,0,-0.002021582034,0,0.0857468918,-0.1084142625,0.04364157261
diff --git a/case-studies/lateral_dynamics/dataset/validation/test3.csv b/case-studies/lateral_dynamics/dataset/validation/test3.csv
index b8139727..046dd09c 100644
--- a/case-studies/lateral_dynamics/dataset/validation/test3.csv
+++ b/case-studies/lateral_dynamics/dataset/validation/test3.csv
@@ -2836,4 +2836,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s
141.7,0.01632077061,-1.733056784,27.7700386,-0.002248041434,0,0.08714535087,-0.1201838106,2306.726898
141.75,0.01580184698,-1.697605133,27.77050591,-0.002202034247,0,0.08702037483,-0.1172332019,2308.139691
141.8,0.01531017479,-1.660968184,27.77096176,-0.002154490374,0,0.08689385653,-0.1142256781,2309.546658
-141.85,0.0148289185,-1.623295903,27.77140617,-0.002105606111,0,0.08676507324,-0.1111633554,2310.947616
\ No newline at end of file
+141.85,0.0148289185,-1.623295903,27.77140617,-0.002105606111,0,0.08676507324,-0.1111633554,2310.947616
diff --git a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb
index ef3ffe6f..d4ce0344 100644
--- a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb
+++ b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb
@@ -46,7 +46,6 @@
],
"source": [
"# Import necessary packages\n",
- "import sys, os\n",
"import pandas as pd\n",
"\n",
"# import nnodely modules\n",
@@ -57,20 +56,22 @@
"\n",
"# import a library for plots\n",
"import matplotlib as mpl\n",
+ "\n",
"mpl.rcParams.update(mpl.rcParamsDefault)\n",
"import matplotlib.pyplot as plt\n",
- "plt.close('all')\n",
+ "\n",
+ "plt.close(\"all\")\n",
"SMALL_SIZE = 14\n",
"MEDIUM_SIZE = 22\n",
"BIGGER_SIZE = 26\n",
- "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n",
- "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
- "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
- "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n",
- "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
- "plt.rc('grid', linestyle=\"--\", color='grey')"
+ "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n",
+ "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
+ "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
+ "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n",
+ "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
+ "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")"
]
},
{
@@ -81,8 +82,10 @@
"outputs": [],
"source": [
"# Configurations\n",
- "path_folder = 'trained_models' # folder to save the model\n",
- "lat_dyna_control = nnodely(visualizer=MPLNotebookVisualizer(),seed=1,workspace=path_folder,log_internal=True)"
+ "path_folder = \"trained_models\" # folder to save the model\n",
+ "lat_dyna_control = nnodely(\n",
+ " visualizer=MPLNotebookVisualizer(), seed=1, workspace=path_folder, log_internal=True\n",
+ ")"
]
},
{
@@ -594,7 +597,7 @@
}
],
"source": [
- "lat_dyna_control.loadModel('vehicle_model')\n",
+ "lat_dyna_control.loadModel(\"vehicle_model\")\n",
"lat_dyna_control.neuralizeModel()"
]
},
@@ -621,76 +624,121 @@
"# ----------------------------------------------------------------\n",
"# Inputs\n",
"# ----------------------------------------------------------------\n",
- "curv_in = Input('controller_curv_in') # [1/m] path curvature\n",
- "vx_in = Input('controller_vx_in') # [m/s] longitudinal velocity\n",
- "steer_in = Input('controller_steer_in') # [rad] steering wheel angle\n",
- "ax_in = Input('controller_ax_in') # [m/s^2] longitudinal acceleration\n",
+ "curv_in = Input(\"controller_curv_in\") # [1/m] path curvature\n",
+ "vx_in = Input(\"controller_vx_in\") # [m/s] longitudinal velocity\n",
+ "steer_in = Input(\"controller_steer_in\") # [rad] steering wheel angle\n",
+ "ax_in = Input(\"controller_ax_in\") # [m/s^2] longitudinal acceleration\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Hyperparameters\n",
"# ----------------------------------------------------------------\n",
- "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n",
+ "samples_past_steer = (\n",
+ " 30 # number of samples in the past for the steering wheel angle prediction\n",
+ ")\n",
"samples_prediction = 30 # number of samples for future manoeuvre\n",
- "n_channels_vx = 8 # number of channels for activation function vx\n",
- "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
- "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n",
+ "n_channels_vx = 8 # number of channels for activation function vx\n",
+ "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
+ "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n",
"\n",
"# Exponential weights for normalisation of FIR parameters during training\n",
- "W_prev = np.flip(np.array([[np.exp(-(i/(samples_prediction/2))**2)] for i in range(-samples_prediction+1,1)]))\n",
- "W_past = np.array([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+2,1)])\n",
+ "W_prev = np.flip(\n",
+ " np.array(\n",
+ " [\n",
+ " [np.exp(-((i / (samples_prediction / 2)) ** 2))]\n",
+ " for i in range(-samples_prediction + 1, 1)\n",
+ " ]\n",
+ " )\n",
+ ")\n",
+ "W_past = np.array(\n",
+ " [\n",
+ " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n",
+ " for i in range(-samples_past_steer + 2, 1)\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "W_fir_init = Constant(\"W_fir_init\", sw=(samples_past_steer - 1), values=W_past)\n",
+ "W_fir_target = Constant(\"W_fir_target\", sw=samples_prediction, values=W_prev)\n",
"\n",
- "W_fir_init = Constant('W_fir_init', sw = (samples_past_steer-1), values=W_past)\n",
- "W_fir_target = Constant('W_fir_target', sw =samples_prediction, values=W_prev)\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Understeer correction function\n",
"# ----------------------------------------------------------------\n",
- "def understeer_corr_local_control(vx,curv, # inputs\n",
- " A, # constant\n",
- " ):\n",
+ "def understeer_corr_local_control(\n",
+ " vx,\n",
+ " curv, # inputs\n",
+ " A, # constant\n",
+ "):\n",
" return curv * (1 + A * torch.pow(vx, 2))\n",
"\n",
+ "\n",
"understeer_corr = ParamFun(understeer_corr_local_control)\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Local models\n",
"# ----------------------------------------------------------------\n",
"# Activation functions\n",
- "activ_fnc_ax = Fuzzify(centers=chan_ax, functions='Triangular')(ax_in.last())\n",
- "activ_fnc_vx = Fuzzify(centers=chan_vx, functions='Triangular')(vx_in.last())\n",
+ "activ_fnc_ax = Fuzzify(centers=chan_ax, functions=\"Triangular\")(ax_in.last())\n",
+ "activ_fnc_vx = Fuzzify(centers=chan_vx, functions=\"Triangular\")(vx_in.last())\n",
"\n",
"# Understeer coefficient: the value of the constant is taken by the model and is function of the acceleration ax\n",
- "A = Constant('A',values=[lat_dyna_control.parameters['A_0'][0][0],lat_dyna_control.parameters['A_1'][0][0],lat_dyna_control.parameters['A_2'][0][0],lat_dyna_control.parameters['A_3'][0][0],lat_dyna_control.parameters['A_4'][0][0]])\n",
- "A_ax = Sum( activ_fnc_ax * A ) # Constant A function of ax\n",
+ "A = Constant(\n",
+ " \"A\",\n",
+ " values=[\n",
+ " lat_dyna_control.parameters[\"A_0\"][0][0],\n",
+ " lat_dyna_control.parameters[\"A_1\"][0][0],\n",
+ " lat_dyna_control.parameters[\"A_2\"][0][0],\n",
+ " lat_dyna_control.parameters[\"A_3\"][0][0],\n",
+ " lat_dyna_control.parameters[\"A_4\"][0][0],\n",
+ " ],\n",
+ ")\n",
+ "A_ax = Sum(activ_fnc_ax * A) # Constant A function of ax\n",
"\n",
"# FIR target trajectory\n",
- "local_model_target = LocalModel(pass_indexes=True,\n",
- " input_function=lambda idx: Fir(output_dimension=1,W_init = 'init_constant',W_init_params={\"value\": 0.01}, W=f'Fir_target_{idx[0]}'))\n",
+ "local_model_target = LocalModel(\n",
+ " pass_indexes=True,\n",
+ " input_function=lambda idx: Fir(\n",
+ " output_dimension=1,\n",
+ " W_init=\"init_constant\",\n",
+ " W_init_params={\"value\": 0.01},\n",
+ " W=f\"Fir_target_{idx[0]}\",\n",
+ " ),\n",
+ ")\n",
"# FIR initial condition\n",
- "delta_ic = Fir(output_dimension=1, W=\"Fir_InitCondition\", W_init=\"init_constant\", W_init_params={\"value\": 0.01})(steer_in.sw([-samples_past_steer,-1])*W_fir_init)\n",
+ "delta_ic = Fir(\n",
+ " output_dimension=1,\n",
+ " W=\"Fir_InitCondition\",\n",
+ " W_init=\"init_constant\",\n",
+ " W_init_params={\"value\": 0.01},\n",
+ ")(steer_in.sw([-samples_past_steer, -1]) * W_fir_init)\n",
"\n",
"# Understeer correction\n",
- "out = understeer_corr( vx_in.sw([-1,(samples_prediction-1)]),curv_in.sw([-1,(samples_prediction-1)]), A_ax )\n",
+ "out = understeer_corr(\n",
+ " vx_in.sw([-1, (samples_prediction - 1)]),\n",
+ " curv_in.sw([-1, (samples_prediction - 1)]),\n",
+ " A_ax,\n",
+ ")\n",
"\n",
"# Local model\n",
- "delta_target = local_model_target(out*W_fir_target , activ_fnc_vx)\n",
+ "delta_target = local_model_target(out * W_fir_target, activ_fnc_vx)\n",
"\n",
"# NN output\n",
- "delta = delta_target + delta_ic\n",
+ "delta = delta_target + delta_ic\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Outputs\n",
"# ----------------------------------------------------------------\n",
- "steer_from_target = Output('controller_steer_from_target',delta_target)\n",
- "steer_from_ic = Output('controller_steer_from_ic',delta_ic)\n",
- "steer_control = Output('controller_steer_out',delta)\n",
+ "steer_from_target = Output(\"controller_steer_from_target\", delta_target)\n",
+ "steer_from_ic = Output(\"controller_steer_from_ic\", delta_ic)\n",
+ "steer_control = Output(\"controller_steer_out\", delta)\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Add controller model and connect to the vehicle model\n",
"# ----------------------------------------------------------------\n",
- "lat_dyna_control.addModel('control_steer',[steer_from_target,steer_from_ic,steer_control])\n",
- "lat_dyna_control.addClosedLoop(steer_control,steer_in)\n",
- "lat_dyna_control.addConnect(steer_control,'model_steer_in')"
+ "lat_dyna_control.addModel(\n",
+ " \"control_steer\", [steer_from_target, steer_from_ic, steer_control]\n",
+ ")\n",
+ "lat_dyna_control.addClosedLoop(steer_control, steer_in)\n",
+ "lat_dyna_control.addConnect(steer_control, \"model_steer_in\")"
]
},
{
@@ -1847,7 +1895,9 @@
],
"source": [
"# Generate the model\n",
- "lat_dyna_control.neuralizeModel(sample_time=0.05) # neuralize the model with the chosen sample time"
+ "lat_dyna_control.neuralizeModel(\n",
+ " sample_time=0.05\n",
+ ") # neuralize the model with the chosen sample time"
]
},
{
@@ -1922,15 +1972,48 @@
}
],
"source": [
- "lat_dyna_control.loadData('test_set', source='dataset/test',\n",
- " format=['', ('controller_ax_in','model_ax_in'),'', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), ('model_steer_in','controller_steer_in')], skiplines=1)\n",
+ "lat_dyna_control.loadData(\n",
+ " \"test_set\",\n",
+ " source=\"dataset/test\",\n",
+ " format=[\n",
+ " \"\",\n",
+ " (\"controller_ax_in\", \"model_ax_in\"),\n",
+ " \"\",\n",
+ " (\"controller_vx_in\", \"model_vx_in\"),\n",
+ " (\"model_curv_in\", \"controller_curv_in\"),\n",
+ " (\"model_steer_in\", \"controller_steer_in\"),\n",
+ " ],\n",
+ " skiplines=1,\n",
+ ")\n",
"\n",
- "lat_dyna_control.loadData('training_set', source='dataset/training',\n",
- " format=['', ('model_ax_in','controller_ax_in'), '', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), 'model_steer_in', ''],\n",
- " skiplines=1)\n",
- "lat_dyna_control.loadData('validation_set', source='dataset/validation',\n",
- " format=['', ('model_ax_in','controller_ax_in'), '', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), 'model_steer_in', ''],\n",
- " skiplines=1)"
+ "lat_dyna_control.loadData(\n",
+ " \"training_set\",\n",
+ " source=\"dataset/training\",\n",
+ " format=[\n",
+ " \"\",\n",
+ " (\"model_ax_in\", \"controller_ax_in\"),\n",
+ " \"\",\n",
+ " (\"controller_vx_in\", \"model_vx_in\"),\n",
+ " (\"model_curv_in\", \"controller_curv_in\"),\n",
+ " \"model_steer_in\",\n",
+ " \"\",\n",
+ " ],\n",
+ " skiplines=1,\n",
+ ")\n",
+ "lat_dyna_control.loadData(\n",
+ " \"validation_set\",\n",
+ " source=\"dataset/validation\",\n",
+ " format=[\n",
+ " \"\",\n",
+ " (\"model_ax_in\", \"controller_ax_in\"),\n",
+ " \"\",\n",
+ " (\"controller_vx_in\", \"model_vx_in\"),\n",
+ " (\"model_curv_in\", \"controller_curv_in\"),\n",
+ " \"model_steer_in\",\n",
+ " \"\",\n",
+ " ],\n",
+ " skiplines=1,\n",
+ ")"
]
},
{
@@ -1954,18 +2037,20 @@
"outputs": [],
"source": [
"# Default training parameters definition\n",
- "training_pars = {'num_of_epochs': 5,\n",
- " 'val_batch_size': 64,\n",
- " 'train_batch_size': 16,\n",
- " 'lr': 1e-3,\n",
- " 'train_dataset': 'training_set',\n",
- " 'validation_dataset': 'validation_set',\n",
- " 'models': 'control_steer',\n",
- " 'optimizer': 'Adam',\n",
- " 'shuffle_data': True,\n",
- " 'select_model': select_best_model,\n",
- " 'early_stopping': earlystopping.early_stop_patience,\n",
- " 'early_stopping_params': {'patience': 3, 'error': 'heading_error'}}"
+ "training_pars = {\n",
+ " \"num_of_epochs\": 5,\n",
+ " \"val_batch_size\": 64,\n",
+ " \"train_batch_size\": 16,\n",
+ " \"lr\": 1e-3,\n",
+ " \"train_dataset\": \"training_set\",\n",
+ " \"validation_dataset\": \"validation_set\",\n",
+ " \"models\": \"control_steer\",\n",
+ " \"optimizer\": \"Adam\",\n",
+ " \"shuffle_data\": True,\n",
+ " \"select_model\": select_best_model,\n",
+ " \"early_stopping\": earlystopping.early_stop_patience,\n",
+ " \"early_stopping_params\": {\"patience\": 3, \"error\": \"heading_error\"},\n",
+ "}"
]
},
{
@@ -2080,8 +2165,7 @@
],
"source": [
"# First training without few samples in the future\n",
- "lat_dyna_control.trainModel(training_params=training_pars,\n",
- " prediction_samples=5)"
+ "lat_dyna_control.trainModel(training_params=training_pars, prediction_samples=5)"
]
},
{
@@ -2201,10 +2285,12 @@
],
"source": [
"# Training with integral error 4s in the future\n",
- "lat_dyna_control.trainModel(training_params=training_pars,\n",
- " num_of_epochs=10,\n",
- " prediction_samples=80, # 4s in the future\n",
- " step=10)"
+ "lat_dyna_control.trainModel(\n",
+ " training_params=training_pars,\n",
+ " num_of_epochs=10,\n",
+ " prediction_samples=80, # 4s in the future\n",
+ " step=10,\n",
+ ")"
]
},
{
@@ -2264,19 +2350,19 @@
"plt.figure()\n",
"\n",
"for key, value in lat_dyna_control.parameters.items():\n",
- " if key.startswith('Fir_target'):\n",
+ " if key.startswith(\"Fir_target\"):\n",
" params_fir_target[key] = lat_dyna_control.parameters[key]\n",
"\n",
- " if key.startswith('Fir_InitCondition'):\n",
+ " if key.startswith(\"Fir_InitCondition\"):\n",
" params_fir_ic[key] = lat_dyna_control.parameters[key]\n",
"\n",
"for key, value in params_fir_target.items():\n",
" vals = np.array(value).flatten()\n",
" t = np.arange(0, sampling_time * len(vals), sampling_time)\n",
- " plt.plot(t,vals * W_prev.flatten(), 'o', label=key)\n",
+ " plt.plot(t, vals * W_prev.flatten(), \"o\", label=key)\n",
"\n",
- "plt.xlabel('time (s)')\n",
- "plt.ylabel('amplitude [m-1 rad-1]')\n",
+ "plt.xlabel(\"time (s)\")\n",
+ "plt.ylabel(\"amplitude [m-1 rad-1]\")\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.show(block=False)\n",
@@ -2284,10 +2370,10 @@
"plt.figure()\n",
"for key, value in params_fir_ic.items():\n",
" vals = np.array(value).flatten()\n",
- " t = np.arange(sampling_time, sampling_time * (len(vals)+1), sampling_time)\n",
- " plt.plot(t,np.flip(vals * W_past.flatten()),'o',label=key)\n",
- "plt.xlabel('time (s)')\n",
- "plt.ylabel('amplitude [-]')\n",
+ " t = np.arange(sampling_time, sampling_time * (len(vals) + 1), sampling_time)\n",
+ " plt.plot(t, np.flip(vals * W_past.flatten()), \"o\", label=key)\n",
+ "plt.xlabel(\"time (s)\")\n",
+ "plt.ylabel(\"amplitude [-]\")\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.show(block=False)"
@@ -2349,7 +2435,7 @@
}
],
"source": [
- "lat_dyna_control.analyzeModel('test_set',prediction_samples=200) # 10s prediction"
+ "lat_dyna_control.analyzeModel(\"test_set\", prediction_samples=200) # 10s prediction"
]
},
{
@@ -2403,32 +2489,34 @@
"data = pd.read_csv(\"dataset/test/test_set.csv\")\n",
"\n",
"sample_test_set = {\n",
- " 'controller_vx_in' : np.array(data['vx']),\n",
- " 'model_vx_in' : np.array(data['vx']),\n",
- " 'controller_curv_in' : np.array(data['curv']),\n",
- " 'model_curv_in' : np.array(data['curv']),\n",
- " 'controller_steer_in' : np.array(data['steer']),\n",
- " 'model_steer_in': np.array(data['steer']),\n",
- " 'model_ax_in' : np.array(data['ax']),\n",
- " 'controller_ax_in': np.array(data['ax'])\n",
+ " \"controller_vx_in\": np.array(data[\"vx\"]),\n",
+ " \"model_vx_in\": np.array(data[\"vx\"]),\n",
+ " \"controller_curv_in\": np.array(data[\"curv\"]),\n",
+ " \"model_curv_in\": np.array(data[\"curv\"]),\n",
+ " \"controller_steer_in\": np.array(data[\"steer\"]),\n",
+ " \"model_steer_in\": np.array(data[\"steer\"]),\n",
+ " \"model_ax_in\": np.array(data[\"ax\"]),\n",
+ " \"controller_ax_in\": np.array(data[\"ax\"]),\n",
"}\n",
"\n",
- "out_nn_test_set = lat_dyna_control(sample_test_set, sampled=False, prediction_samples=1100)\n",
+ "out_nn_test_set = lat_dyna_control(\n",
+ " sample_test_set, sampled=False, prediction_samples=1100\n",
+ ")\n",
"\n",
"# plot the results\n",
"plt.figure()\n",
- "plt.plot(data['steer'],label='telem')\n",
- "plt.plot(out_nn_test_set['controller_steer_out'],label='control')\n",
- "plt.xlabel('samples')\n",
- "plt.ylabel('steer (rad)')\n",
+ "plt.plot(data[\"steer\"], label=\"telem\")\n",
+ "plt.plot(out_nn_test_set[\"controller_steer_out\"], label=\"control\")\n",
+ "plt.xlabel(\"samples\")\n",
+ "plt.ylabel(\"steer (rad)\")\n",
"plt.legend()\n",
"plt.grid()\n",
"\n",
"plt.figure()\n",
- "plt.plot(data['curv'],label='telem')\n",
- "plt.plot(out_nn_test_set['model_curv'],label='control')\n",
- "plt.xlabel('samples')\n",
- "plt.ylabel('curvature (rad)')\n",
+ "plt.plot(data[\"curv\"], label=\"telem\")\n",
+ "plt.plot(out_nn_test_set[\"model_curv\"], label=\"control\")\n",
+ "plt.xlabel(\"samples\")\n",
+ "plt.ylabel(\"curvature (rad)\")\n",
"plt.legend()\n",
"plt.grid()"
]
@@ -3550,14 +3638,28 @@
],
"source": [
"# Save json model\n",
- "lat_dyna_control.saveModel('control_model')\n",
+ "lat_dyna_control.saveModel(\"control_model\")\n",
"\n",
"# Remove Minimize\n",
- "lat_dyna_control.removeMinimize(['heading_error','curv_error'])\n",
+ "lat_dyna_control.removeMinimize([\"heading_error\", \"curv_error\"])\n",
"lat_dyna_control.neuralizeModel()\n",
"\n",
"# Export ONNX\n",
- "lat_dyna_control.exportONNX(['controller_curv_in', 'controller_vx_in','controller_ax_in','controller_steer_in'],['controller_steer_out','controller_steer_from_ic','controller_steer_from_target'],'controller',models='control_steer')\n"
+ "lat_dyna_control.exportONNX(\n",
+ " [\n",
+ " \"controller_curv_in\",\n",
+ " \"controller_vx_in\",\n",
+ " \"controller_ax_in\",\n",
+ " \"controller_steer_in\",\n",
+ " ],\n",
+ " [\n",
+ " \"controller_steer_out\",\n",
+ " \"controller_steer_from_ic\",\n",
+ " \"controller_steer_from_target\",\n",
+ " ],\n",
+ " \"controller\",\n",
+ " models=\"control_steer\",\n",
+ ")"
]
}
],
diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb
index c8c9491c..83e20531 100644
--- a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb
+++ b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb
@@ -47,7 +47,6 @@
],
"source": [
"# Import necessary packages\n",
- "import sys, os\n",
"import pandas as pd\n",
"\n",
"# import nnodely modules\n",
@@ -58,20 +57,22 @@
"\n",
"# import a library for plots\n",
"import matplotlib as mpl\n",
+ "\n",
"mpl.rcParams.update(mpl.rcParamsDefault)\n",
"import matplotlib.pyplot as plt\n",
- "plt.close('all')\n",
+ "\n",
+ "plt.close(\"all\")\n",
"SMALL_SIZE = 14\n",
"MEDIUM_SIZE = 22\n",
"BIGGER_SIZE = 26\n",
- "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n",
- "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
- "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
- "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n",
- "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
- "plt.rc('grid', linestyle=\"--\", color='grey')"
+ "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n",
+ "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
+ "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
+ "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n",
+ "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
+ "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")"
]
},
{
@@ -87,8 +88,10 @@
"outputs": [],
"source": [
"# Configurations\n",
- "path_folder = 'trained_models' # folder to save the model\n",
- "lat_dyna_model = nnodely(visualizer=MPLNotebookVisualizer(),seed=1,workspace=path_folder,log_internal=True)"
+ "path_folder = \"trained_models\" # folder to save the model\n",
+ "lat_dyna_model = nnodely(\n",
+ " visualizer=MPLNotebookVisualizer(), seed=1, workspace=path_folder, log_internal=True\n",
+ ")"
]
},
{
@@ -132,58 +135,77 @@
"# ----------------------------------------------------------------\n",
"# Inputs\n",
"# ----------------------------------------------------------------\n",
- "vx_in = Input('model_vx_in') # [m/s] longitudinal velocity\n",
- "steer_in = Input('model_steer_in') # [rad] steering wheel angle\n",
- "ax_in = Input('model_ax_in') # [m/s^2] lateral acceleration\n",
+ "vx_in = Input(\"model_vx_in\") # [m/s] longitudinal velocity\n",
+ "steer_in = Input(\"model_steer_in\") # [rad] steering wheel angle\n",
+ "ax_in = Input(\"model_ax_in\") # [m/s^2] lateral acceleration\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Hyperparameters\n",
"# ----------------------------------------------------------------\n",
- "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n",
- "n_channels_vx = 8 # number of channels for activation function vx\n",
- "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
- "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n",
+ "samples_past_steer = (\n",
+ " 30 # number of samples in the past for the steering wheel angle prediction\n",
+ ")\n",
+ "n_channels_vx = 8 # number of channels for activation function vx\n",
+ "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
+ "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n",
"\n",
"# Exponential weights for normalisation of FIR parameters during training\n",
- "W_fir = np.array([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+1,1)])\n",
- "W_constant = Constant('W_fir',sw=30, values=W_fir)\n",
+ "W_fir = np.array(\n",
+ " [\n",
+ " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n",
+ " for i in range(-samples_past_steer + 1, 1)\n",
+ " ]\n",
+ ")\n",
+ "W_constant = Constant(\"W_fir\", sw=30, values=W_fir)\n",
+ "\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Understeer correction function\n",
"# ----------------------------------------------------------------\n",
- "def understeer_corr_local(vx, # input\n",
- " A # learnable parameter\n",
- " ):\n",
+ "def understeer_corr_local(\n",
+ " vx, # input\n",
+ " A, # learnable parameter\n",
+ "):\n",
" return 1 / (1 + A * torch.pow(vx, 2))\n",
"\n",
+ "\n",
"understeer_corr = ParamFun(understeer_corr_local)\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Local models\n",
"# ----------------------------------------------------------------\n",
"# Activation functions\n",
- "fuzzy_vx = Fuzzify(centers=chan_vx, functions='Triangular')(vx_in.last())\n",
- "fuzzy_ax = Fuzzify(centers=chan_ax, functions='Triangular')(ax_in.last())\n",
+ "fuzzy_vx = Fuzzify(centers=chan_vx, functions=\"Triangular\")(vx_in.last())\n",
+ "fuzzy_ax = Fuzzify(centers=chan_ax, functions=\"Triangular\")(ax_in.last())\n",
+ "\n",
"\n",
"# FIR input functions\n",
"def Input_Function_Gen(idx):\n",
" def fir_out(fir_input):\n",
- " out = Fir(W_init='init_constant', W_init_params={\"value\": 1}, W=f'Fir_{idx[0]}')(fir_input)\n",
+ " out = Fir(\n",
+ " W_init=\"init_constant\", W_init_params={\"value\": 1}, W=f\"Fir_{idx[0]}\"\n",
+ " )(fir_input)\n",
" return out\n",
"\n",
" return fir_out\n",
"\n",
+ "\n",
"# Local Input Model\n",
"local_input_model = LocalModel(pass_indexes=True, input_function=Input_Function_Gen)\n",
- "out_input_model = local_input_model((steer_in.sw(samples_past_steer) * W_constant ), fuzzy_vx)\n",
+ "out_input_model = local_input_model(\n",
+ " (steer_in.sw(samples_past_steer) * W_constant), fuzzy_vx\n",
+ ")\n",
+ "\n",
"\n",
"# Parametric functions: Understeer correction\n",
"def Understeer_Function_Gen(idx):\n",
" def osus_out(vx_input):\n",
- " A = Parameter('A_' + str(idx[0]), values=[[1e-5]])\n",
+ " A = Parameter(\"A_\" + str(idx[0]), values=[[1e-5]])\n",
" return understeer_corr(vx_input, A)\n",
+ "\n",
" return osus_out\n",
"\n",
+ "\n",
"# Local Understeer Correction Model\n",
"local_osus_corr = LocalModel(pass_indexes=True, input_function=Understeer_Function_Gen)\n",
"out_osus = local_osus_corr(vx_in.last(), fuzzy_ax)\n",
@@ -191,12 +213,12 @@
"# ----------------------------------------------------------------\n",
"# Outputs\n",
"# ----------------------------------------------------------------\n",
- "curv_out = Output('model_curv', out_input_model*out_osus) # output of the model\n",
+ "curv_out = Output(\"model_curv\", out_input_model * out_osus) # output of the model\n",
"\n",
"# ----------------------------------------------------------------\n",
"# Add model\n",
"# ----------------------------------------------------------------\n",
- "lat_dyna_model.addModel('vehicle_model', curv_out)"
+ "lat_dyna_model.addModel(\"vehicle_model\", curv_out)"
]
},
{
@@ -236,21 +258,16 @@
"# ------------------------------------------------------------------------\n",
"# Targets\n",
"# ------------------------------------------------------------------------\n",
- "curv_in = Input('model_curv_in') # [1/m] path curvature\n",
+ "curv_in = Input(\"model_curv_in\") # [1/m] path curvature\n",
"\n",
"# Definition of heading as integral of curvature multiplied by the longitudinal speed\n",
- "heading_target = Integrate(curv_in.next()*vx_in.next())\n",
- "heading_nn = Integrate(out_input_model*out_osus*vx_in.next())\n",
+ "heading_target = Integrate(curv_in.next() * vx_in.next())\n",
+ "heading_nn = Integrate(out_input_model * out_osus * vx_in.next())\n",
"\n",
"# Add a loss function: mean squared error for the curvature prediction\n",
- "lat_dyna_model.addMinimize('curv_error',\n",
- " curv_in.next(),\n",
- " curv_out,\n",
- " loss_function='mse')\n",
+ "lat_dyna_model.addMinimize(\"curv_error\", curv_in.next(), curv_out, loss_function=\"mse\")\n",
"# Add a loss function: mean squared error for the heading prediction\n",
- "lat_dyna_model.addMinimize('heading_error',\n",
- " heading_target,\n",
- " heading_nn)"
+ "lat_dyna_model.addMinimize(\"heading_error\", heading_target, heading_nn)"
]
},
{
@@ -742,7 +759,9 @@
],
"source": [
"# Generate the model\n",
- "lat_dyna_model.neuralizeModel(sample_time=0.05) # neuralize the model with the chosen sample time"
+ "lat_dyna_model.neuralizeModel(\n",
+ " sample_time=0.05\n",
+ ") # neuralize the model with the chosen sample time"
]
},
{
@@ -798,14 +817,40 @@
}
],
"source": [
- "# Dataset loading: all the csv files in the folders defined in 'source' are loaded, with the format defined in 'format' \n",
+ "# Dataset loading: all the csv files in the folders defined in 'source' are loaded, with the format defined in 'format'\n",
"# (the order of the columns in the csv files) and skipping the number of lines defined in 'skiplines'\n",
- "lat_dyna_model.loadData('training_set', source='dataset/training',\n",
- " format=['', 'model_ax_in', '', 'model_vx_in', 'model_curv_in', '', '', 'model_steer_in', ''],\n",
- " skiplines=1)\n",
- "lat_dyna_model.loadData('validation_set', source='dataset/validation',\n",
- " format=['', 'model_ax_in', '', 'model_vx_in', 'model_curv_in', '', '', 'model_steer_in', ''],\n",
- " skiplines=1)"
+ "lat_dyna_model.loadData(\n",
+ " \"training_set\",\n",
+ " source=\"dataset/training\",\n",
+ " format=[\n",
+ " \"\",\n",
+ " \"model_ax_in\",\n",
+ " \"\",\n",
+ " \"model_vx_in\",\n",
+ " \"model_curv_in\",\n",
+ " \"\",\n",
+ " \"\",\n",
+ " \"model_steer_in\",\n",
+ " \"\",\n",
+ " ],\n",
+ " skiplines=1,\n",
+ ")\n",
+ "lat_dyna_model.loadData(\n",
+ " \"validation_set\",\n",
+ " source=\"dataset/validation\",\n",
+ " format=[\n",
+ " \"\",\n",
+ " \"model_ax_in\",\n",
+ " \"\",\n",
+ " \"model_vx_in\",\n",
+ " \"model_curv_in\",\n",
+ " \"\",\n",
+ " \"\",\n",
+ " \"model_steer_in\",\n",
+ " \"\",\n",
+ " ],\n",
+ " skiplines=1,\n",
+ ")"
]
},
{
@@ -821,19 +866,20 @@
"outputs": [],
"source": [
"# Default training parameters definition\n",
- "training_pars = { 'num_of_epochs': 150,\n",
- " 'val_batch_size': 64,\n",
- " 'train_batch_size': 64,\n",
- " 'lr': 1e-3 ,\n",
- " 'train_dataset': 'training_set',\n",
- " 'validation_dataset': 'validation_set',\n",
- " 'optimizer': 'Adam',\n",
- " 'shuffle_data': True,\n",
- " 'prediction_samples': -1, # force to do not evaluate the integral\n",
- " 'select_model': select_best_model,\n",
- " 'early_stopping': earlystopping.early_stop_patience,\n",
- " 'early_stopping_params': {'patience': 20,'error': 'curv_error'}\n",
- " }"
+ "training_pars = {\n",
+ " \"num_of_epochs\": 150,\n",
+ " \"val_batch_size\": 64,\n",
+ " \"train_batch_size\": 64,\n",
+ " \"lr\": 1e-3,\n",
+ " \"train_dataset\": \"training_set\",\n",
+ " \"validation_dataset\": \"validation_set\",\n",
+ " \"optimizer\": \"Adam\",\n",
+ " \"shuffle_data\": True,\n",
+ " \"prediction_samples\": -1, # force to do not evaluate the integral\n",
+ " \"select_model\": select_best_model,\n",
+ " \"early_stopping\": earlystopping.early_stop_patience,\n",
+ " \"early_stopping_params\": {\"patience\": 20, \"error\": \"curv_error\"},\n",
+ "}"
]
},
{
@@ -1199,9 +1245,9 @@
],
"source": [
"# Training with intergal error 4s in the future\n",
- "lat_dyna_model.trainModel(training_params=training_pars,\n",
- " num_of_epochs=20,\n",
- " prediction_samples=80)"
+ "lat_dyna_model.trainModel(\n",
+ " training_params=training_pars, num_of_epochs=20, prediction_samples=80\n",
+ ")"
]
},
{
@@ -1243,16 +1289,16 @@
"plt.figure()\n",
"\n",
"for key, value in lat_dyna_model.parameters.items():\n",
- " if key.startswith('Fir_'):\n",
+ " if key.startswith(\"Fir_\"):\n",
" params_fir[key] = lat_dyna_model.parameters[key]\n",
"\n",
"for key, value in params_fir.items():\n",
" vals = np.array(value).flatten()\n",
" t = np.arange(0, sampling_time * len(vals), sampling_time)\n",
- " plt.plot(t,np.flip(vals * W_fir.flatten()), 'o', label=key)\n",
+ " plt.plot(t, np.flip(vals * W_fir.flatten()), \"o\", label=key)\n",
"t = np.arange(0, sampling_time * samples_past_steer, sampling_time)\n",
- "plt.xlabel('time (s)')\n",
- "plt.ylabel('amplitude [m-1 rad-1]')\n",
+ "plt.xlabel(\"time (s)\")\n",
+ "plt.ylabel(\"amplitude [m-1 rad-1]\")\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.show()"
@@ -1306,9 +1352,13 @@
],
"source": [
"# Dataset extraction\n",
- "lat_dyna_model.loadData('test_set', source='dataset/test',\n",
- " format=['', 'model_ax_in', '','model_vx_in', 'model_curv_in', 'model_steer_in'], skiplines=1)\n",
- "data_in = lat_dyna_model.getSamples('test_set',index=1,window=1000)\n",
+ "lat_dyna_model.loadData(\n",
+ " \"test_set\",\n",
+ " source=\"dataset/test\",\n",
+ " format=[\"\", \"model_ax_in\", \"\", \"model_vx_in\", \"model_curv_in\", \"model_steer_in\"],\n",
+ " skiplines=1,\n",
+ ")\n",
+ "data_in = lat_dyna_model.getSamples(\"test_set\", index=1, window=1000)\n",
"\n",
"# Model inference\n",
"out_nn = lat_dyna_model(data_in, sampled=True)\n",
@@ -1316,11 +1366,20 @@
"dataset = pd.read_csv(\"dataset/test/test_set.csv\")\n",
"\n",
"# Plot\n",
- "plt.figure(figsize=(12,6))\n",
- "plt.plot(0.05*np.arange(len(np.array(dataset['curv'])[30:])),np.array(dataset['curv'])[30:],label='target')\n",
- "plt.plot(0.05*np.arange(len(out_nn['model_curv'])), out_nn['model_curv'],'--',label='prediction')\n",
- "plt.xlabel('time (s)')\n",
- "plt.ylabel('curvature [1/m]')\n",
+ "plt.figure(figsize=(12, 6))\n",
+ "plt.plot(\n",
+ " 0.05 * np.arange(len(np.array(dataset[\"curv\"])[30:])),\n",
+ " np.array(dataset[\"curv\"])[30:],\n",
+ " label=\"target\",\n",
+ ")\n",
+ "plt.plot(\n",
+ " 0.05 * np.arange(len(out_nn[\"model_curv\"])),\n",
+ " out_nn[\"model_curv\"],\n",
+ " \"--\",\n",
+ " label=\"prediction\",\n",
+ ")\n",
+ "plt.xlabel(\"time (s)\")\n",
+ "plt.ylabel(\"curvature [1/m]\")\n",
"plt.grid(True)\n",
"plt.legend()\n",
"plt.show()"
@@ -1357,7 +1416,7 @@
],
"source": [
"# Save json model: will be used in the controller training\n",
- "lat_dyna_model.saveModel('vehicle_model')"
+ "lat_dyna_model.saveModel(\"vehicle_model\")"
]
}
],
diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb
index 4bd1ef67..733b2a73 100644
--- a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb
+++ b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb
@@ -49,7 +49,6 @@
],
"source": [
"# Import necessary packages\n",
- "import os\n",
"import numpy as np\n",
"import torch\n",
"from torch import nn\n",
@@ -57,20 +56,22 @@
"\n",
"# import a library for plots\n",
"import matplotlib as mpl\n",
+ "\n",
"mpl.rcParams.update(mpl.rcParamsDefault)\n",
"import matplotlib.pyplot as plt\n",
- "plt.close('all')\n",
+ "\n",
+ "plt.close(\"all\")\n",
"SMALL_SIZE = 14\n",
"MEDIUM_SIZE = 22\n",
"BIGGER_SIZE = 26\n",
- "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n",
- "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
- "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
- "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
- "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n",
- "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
- "plt.rc('grid', linestyle=\"--\", color='grey')\n",
+ "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n",
+ "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n",
+ "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n",
+ "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n",
+ "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n",
+ "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n",
+ "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")\n",
"\n",
"# Congigurations\n",
"device = \"cpu\"\n",
@@ -79,7 +80,7 @@
"torch.manual_seed(1)\n",
"np.random.seed(1)\n",
"\n",
- "path_folder = 'trained_models' # folder to save the model"
+ "path_folder = \"trained_models\" # folder to save the model"
]
},
{
@@ -117,14 +118,14 @@
"source": [
"# triangular activation function\n",
"class TriangularMembershipFunction(nn.Module):\n",
- " def __init__(self,centers: list) -> None:\n",
+ " def __init__(self, centers: list) -> None:\n",
" super().__init__()\n",
" self.centers = torch.tensor(centers, dtype=torch.float32) # (C,)\n",
" self.C = self.centers.numel()\n",
"\n",
" def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
" \"\"\"\n",
- " x: (batch,1) \n",
+ " x: (batch,1)\n",
" out: (batch,C) -> membership per channel/centre\n",
" \"\"\"\n",
" # get sizes\n",
@@ -133,39 +134,45 @@
" centers = self.centers\n",
"\n",
" # triangural membership function initalization\n",
- " phi = torch.zeros(batch,self.C)\n",
- " x = x.squeeze(1) # (C,)\n",
+ " phi = torch.zeros(batch, self.C)\n",
+ " x = x.squeeze(1) # (C,)\n",
"\n",
" # evaluate channel width and ceneters\n",
" x_contig = x.contiguous()\n",
- " idx0 = torch.clip(torch.bucketize(x_contig,centers)-1, 0, C-2) # [0, C-2] \n",
- " idx1 = idx0 + 1 # [1, C-1]\n",
- " c0 = centers[idx0] # left center\n",
- " c1 = centers[idx1] # right center\n",
- " ch_w = c1 - c0 # channels distance\n",
+ " idx0 = torch.clip(torch.bucketize(x_contig, centers) - 1, 0, C - 2) # [0, C-2]\n",
+ " idx1 = idx0 + 1 # [1, C-1]\n",
+ " c0 = centers[idx0] # left center\n",
+ " c1 = centers[idx1] # right center\n",
+ " ch_w = c1 - c0 # channels distance\n",
"\n",
" b = torch.arange(batch)\n",
"\n",
- " phi[b,idx0] = torch.clip( (c1 - x) / ch_w , 0 , 1 ) # left channel weight\n",
- " phi[b,idx1] = torch.clip( (x - c0) / ch_w , 0 , 1 ) # right channel width\n",
+ " phi[b, idx0] = torch.clip((c1 - x) / ch_w, 0, 1) # left channel weight\n",
+ " phi[b, idx1] = torch.clip((x - c0) / ch_w, 0, 1) # right channel width\n",
"\n",
" return phi # (batch, C)\n",
"\n",
"\n",
"class FirFuzzyLayer(nn.Module):\n",
- " def __init__(self,channels: list,window: int) -> None:\n",
+ " def __init__(self, channels: list, window: int) -> None:\n",
" super().__init__()\n",
" self.C = len(channels)\n",
- " self.phi_layer = TriangularMembershipFunction(channels) # activation functon weights\n",
- " self.fir_layer = nn.Conv1d( # parallel fir filters\n",
+ " self.phi_layer = TriangularMembershipFunction(\n",
+ " channels\n",
+ " ) # activation functon weights\n",
+ " self.fir_layer = nn.Conv1d( # parallel fir filters\n",
" in_channels=self.C,\n",
" out_channels=self.C,\n",
" kernel_size=window,\n",
" groups=self.C,\n",
- " bias=False) \n",
+ " bias=False,\n",
+ " )\n",
" nn.init.constant_(self.fir_layer.weight, 1.0)\n",
- " self.W_fir = torch.tensor([[np.exp(-(i/(window/2))**2)] for i in range(-window+1,1)],dtype=torch.float32).view(1,1,-1)\n",
- " \n",
+ " self.W_fir = torch.tensor(\n",
+ " [[np.exp(-((i / (window / 2)) ** 2))] for i in range(-window + 1, 1)],\n",
+ " dtype=torch.float32,\n",
+ " ).view(1, 1, -1)\n",
+ "\n",
" def forward(self, x: torch.Tensor, a: torch.Tensor) -> torch.Tensor:\n",
" \"\"\"\n",
" x: (batch,window) -> input signal with time windows\n",
@@ -173,24 +180,25 @@
" out: (batch,1) -> interpolated fir output\n",
" \"\"\"\n",
" # evaluate membership function\n",
- " phi = self.phi_layer(a) # (batch, C)\n",
+ " phi = self.phi_layer(a) # (batch, C)\n",
"\n",
" # replicate input for Conv1d\n",
- " x_fir = x.unsqueeze(1)*self.W_fir # (batch, 1, window)\n",
- " x_fir = x_fir.repeat(1,self.C, 1) # (batch, C, window)\n",
- " y = self.fir_layer(x_fir) # (batch, C, 1)\n",
+ " x_fir = x.unsqueeze(1) * self.W_fir # (batch, 1, window)\n",
+ " x_fir = x_fir.repeat(1, self.C, 1) # (batch, C, window)\n",
+ " y = self.fir_layer(x_fir) # (batch, C, 1)\n",
" # evaluate weightet output\n",
- " y = y.squeeze(-1) # (batch, C)\n",
+ " y = y.squeeze(-1) # (batch, C)\n",
" y = phi * y\n",
"\n",
- " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n",
- " \n",
+ " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n",
+ "\n",
+ "\n",
"class UsFuzzyLayer(nn.Module):\n",
- " def __init__(self,channels: list) -> None:\n",
+ " def __init__(self, channels: list) -> None:\n",
" super().__init__()\n",
" self.C = len(channels)\n",
- " self.phi_layer = TriangularMembershipFunction(channels)\n",
- " self.A = nn.Parameter(1e-5*torch.ones(1,self.C)) \n",
+ " self.phi_layer = TriangularMembershipFunction(channels)\n",
+ " self.A = nn.Parameter(1e-5 * torch.ones(1, self.C))\n",
"\n",
" def forward(self, x: torch.Tensor, a: torch.Tensor) -> torch.Tensor:\n",
" \"\"\"\n",
@@ -199,35 +207,37 @@
" out: (batch,1) -> output\n",
" \"\"\"\n",
" # evaluate membership function\n",
- " phi = self.phi_layer(a) # (batch,1)\n",
+ " phi = self.phi_layer(a) # (batch,1)\n",
"\n",
" # replicate for channels\n",
- " x = x.repeat(1,self.C) # (batch,C)\n",
+ " x = x.repeat(1, self.C) # (batch,C)\n",
"\n",
" # evaluate parametric function\n",
- " y = 1 / (1 + self.A * x**2) \n",
+ " y = 1 / (1 + self.A * x**2)\n",
" y = phi * y\n",
"\n",
- " return y.sum(dim=1).unsqueeze(-1) #(batch,1)\n",
- " \n",
+ " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n",
+ "\n",
"\n",
"class LateralDynamics(nn.Module):\n",
" def __init__(self, chan_vx: list, chan_ax: list, window: int) -> None:\n",
" super().__init__()\n",
- " self.fir_layer = FirFuzzyLayer(chan_vx,window)\n",
- " self.us_layer = UsFuzzyLayer(chan_ax)\n",
+ " self.fir_layer = FirFuzzyLayer(chan_vx, window)\n",
+ " self.us_layer = UsFuzzyLayer(chan_ax)\n",
"\n",
- " def forward(self, steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor) -> torch.Tensor:\n",
+ " def forward(\n",
+ " self, steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor\n",
+ " ) -> torch.Tensor:\n",
" \"\"\"\n",
- " steer: (batch,window) -> steer time window \n",
+ " steer: (batch,window) -> steer time window\n",
" vx: (batch,1) -> vx last\n",
" ax: (batch,1) -> ax last\n",
" out: (batch,1) -> rho next\n",
" \"\"\"\n",
" # apply fir\n",
- " rho_kin = self.fir_layer(steer,vx)\n",
+ " rho_kin = self.fir_layer(steer, vx)\n",
" # apply us correction\n",
- " corr_kus = self.us_layer(vx,ax)\n",
+ " corr_kus = self.us_layer(vx, ax)\n",
"\n",
" return rho_kin * corr_kus"
]
@@ -247,17 +257,19 @@
"# ----------------------------------------------------------------\n",
"# Hyperparameters\n",
"# ----------------------------------------------------------------\n",
- "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n",
- "n_channels_vx = 8 # number of channels for activation function vx\n",
- "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
- "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n",
+ "samples_past_steer = (\n",
+ " 30 # number of samples in the past for the steering wheel angle prediction\n",
+ ")\n",
+ "n_channels_vx = 8 # number of channels for activation function vx\n",
+ "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n",
+ "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n",
"\n",
- "prediction = 80 # 4s prediction\n",
+ "prediction = 80 # 4s prediction\n",
"\n",
"# ----------------------------------------------------------------\n",
"# MODEL\n",
"# ----------------------------------------------------------------\n",
- "model = LateralDynamics(chan_vx,chan_ax,samples_past_steer)"
+ "model = LateralDynamics(chan_vx, chan_ax, samples_past_steer)"
]
},
{
@@ -284,16 +296,18 @@
"outputs": [],
"source": [
"data_training = \"dataset/training/*.csv\"\n",
- "data_valid = \"dataset/validation/*.csv\"\n",
- "data_test = \"dataset/test/*.csv\"\n",
+ "data_valid = \"dataset/validation/*.csv\"\n",
+ "data_test = \"dataset/test/*.csv\"\n",
"\n",
- "head = ['ax', 'vx', 'curv', 'steer']\n",
+ "head = [\"ax\", \"vx\", \"curv\", \"steer\"]\n",
"sampling = 0.05\n",
"\n",
"\n",
"# loading of csv datasets\n",
"import pandas as pd\n",
"import glob\n",
+ "\n",
+ "\n",
"def load_csv_list(data_path: str):\n",
" dfs = []\n",
" for file in glob.glob(data_path):\n",
@@ -302,6 +316,7 @@
" dfs.append(df)\n",
" return dfs\n",
"\n",
+ "\n",
"class VehicleDataset(Dataset):\n",
" def __init__(self, df: pd.DataFrame, past_samples: int, prediction: int):\n",
" self.lb = past_samples\n",
@@ -309,9 +324,9 @@
" self.df = df\n",
"\n",
" self.steer = torch.tensor(df[\"steer\"].values, dtype=torch.float32)\n",
- " self.vx = torch.tensor(df[\"vx\"].values, dtype=torch.float32)\n",
- " self.ax = torch.tensor(df[\"ax\"].values, dtype=torch.float32)\n",
- " self.curv = torch.tensor(df[\"curv\"].values, dtype=torch.float32)\n",
+ " self.vx = torch.tensor(df[\"vx\"].values, dtype=torch.float32)\n",
+ " self.ax = torch.tensor(df[\"ax\"].values, dtype=torch.float32)\n",
+ " self.curv = torch.tensor(df[\"curv\"].values, dtype=torch.float32)\n",
"\n",
" def __len__(self):\n",
" return len(self.df) - self.lb - self.lf - 1\n",
@@ -320,17 +335,18 @@
" steer = self.steer[i : i + self.lb + self.lf]\n",
"\n",
" if self.lf > 0:\n",
- " vx = self.vx[i + self.lb -1: i + self.lb + self.lf -1]\n",
- " ax = self.ax[i + self.lb -1: i + self.lb + self.lf -1]\n",
+ " vx = self.vx[i + self.lb - 1 : i + self.lb + self.lf - 1]\n",
+ " ax = self.ax[i + self.lb - 1 : i + self.lb + self.lf - 1]\n",
" curv = self.curv[i + self.lb : i + self.lb + self.lf]\n",
" else:\n",
- " vx = self.vx[i + self.lb -1].unsqueeze(-1)\n",
- " ax = self.ax[i + self.lb -1].unsqueeze(-1)\n",
+ " vx = self.vx[i + self.lb - 1].unsqueeze(-1)\n",
+ " ax = self.ax[i + self.lb - 1].unsqueeze(-1)\n",
" curv = self.curv[i + self.lb].unsqueeze(-1)\n",
"\n",
" return steer, vx, ax, curv\n",
"\n",
- "# Concatenate datasets \n",
+ "\n",
+ "# Concatenate datasets\n",
"def build_dataset(csv_path, past_samples, prediction):\n",
" dfs = load_csv_list(csv_path)\n",
" datasets = [\n",
@@ -340,6 +356,7 @@
" ]\n",
" return ConcatDataset(datasets)\n",
"\n",
+ "\n",
"# non recurrent dataset\n",
"train_dataset = build_dataset(data_training, samples_past_steer, prediction=0)\n",
"valid_dataset = build_dataset(data_valid, samples_past_steer, prediction=0)\n",
@@ -573,33 +590,35 @@
"# ------------------------------------------------------------------------\n",
"# Non-recurrent training\n",
"# ------------------------------------------------------------------------\n",
- "EPOCHS = 150\n",
+ "EPOCHS = 150\n",
"\n",
"train_loss_list = []\n",
"valid_loss_list = []\n",
"\n",
- "def inference(steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor):\n",
+ "\n",
+ "def inference(\n",
+ " steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor\n",
+ "):\n",
"\n",
" # Make one-step prediction\n",
- " output = model(steer,vx, ax)\n",
+ " output = model(steer, vx, ax)\n",
" # Compute the loss\n",
" loss = loss_fn(output, curv)\n",
"\n",
" return loss\n",
"\n",
"\n",
- "\n",
"def train_one_epoch():\n",
" avg_loss = 0.0\n",
"\n",
" for i, data in enumerate(train_dataloader):\n",
" steer, vx, ax, curv = data\n",
- " \n",
+ "\n",
" # Zero your gradients for every batch!\n",
" optimizer.zero_grad()\n",
"\n",
" # compute loss and its gradient\n",
- " loss = inference(steer,vx,ax,curv)\n",
+ " loss = inference(steer, vx, ax, curv)\n",
" loss.backward()\n",
"\n",
" avg_loss += loss.item()\n",
@@ -609,7 +628,7 @@
" return avg_loss / len(train_dataloader)\n",
"\n",
"\n",
- "print('| {:^7} | {:^12} | {:^12} |'.format('EPOCHS','train_loss','valid_loss'))\n",
+ "print(\"| {:^7} | {:^12} | {:^12} |\".format(\"EPOCHS\", \"train_loss\", \"valid_loss\"))\n",
"for epoch in range(EPOCHS):\n",
" # Make sure gradient tracking is on, and do a pass over the data\n",
" model.train(True)\n",
@@ -619,29 +638,29 @@
" # statistics for batch normalization.\n",
" model.eval()\n",
" avg_vloss = 0.0\n",
- " \n",
+ "\n",
" with torch.no_grad():\n",
" for i, vdata in enumerate(valid_dataloader):\n",
" steer, vx, ax, curv = vdata\n",
- " \n",
- " vloss = inference(steer,vx,ax,curv)\n",
+ "\n",
+ " vloss = inference(steer, vx, ax, curv)\n",
" avg_vloss += vloss.item()\n",
" avg_vloss /= len(valid_dataloader)\n",
- " \n",
+ "\n",
" # save the total losses\n",
" train_loss_list.append(avg_loss)\n",
" valid_loss_list.append(avg_vloss)\n",
"\n",
- " print('| {:^7} | {:^12.6e} | {:^12.6e} |'.format(epoch+1, avg_loss, avg_vloss))\n",
+ " print(\"| {:^7} | {:^12.6e} | {:^12.6e} |\".format(epoch + 1, avg_loss, avg_vloss))\n",
" epoch += 1\n",
"\n",
"plt.figure()\n",
- "plt.title('Non recurrent training')\n",
- "plt.plot(train_loss_list, label='train loss')\n",
- "plt.plot(valid_loss_list, '--', label='validation loss')\n",
- "plt.yscale('log')\n",
- "plt.xlabel('Epochs')\n",
- "plt.ylabel('Loss')\n",
+ "plt.title(\"Non recurrent training\")\n",
+ "plt.plot(train_loss_list, label=\"train loss\")\n",
+ "plt.plot(valid_loss_list, \"--\", label=\"validation loss\")\n",
+ "plt.yscale(\"log\")\n",
+ "plt.xlabel(\"Epochs\")\n",
+ "plt.ylabel(\"Loss\")\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.show()\n",
@@ -708,36 +727,39 @@
"# ------------------------------------------------------------------------\n",
"# Recurrent training\n",
"# ------------------------------------------------------------------------\n",
- "EPOCHS = 20\n",
+ "EPOCHS = 20\n",
"\n",
"curv_loss_list = []\n",
"head_loss_list = []\n",
"curv_vloss_list = []\n",
"head_vloss_list = []\n",
"\n",
- "def inference_rec(steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor):\n",
+ "\n",
+ "def inference_rec(\n",
+ " steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor\n",
+ "):\n",
"\n",
" # recurrent training\n",
- " psi0_ref = 0\n",
- " psi_ref = 0\n",
+ " psi0_ref = 0\n",
+ " psi_ref = 0\n",
" psi0_comp = 0\n",
- " psi_comp = 0\n",
+ " psi_comp = 0\n",
" loss_curv = 0\n",
" loss_head = 0\n",
"\n",
" for j in range(prediction):\n",
- " steer_k = steer[:,j:j+samples_past_steer]\n",
- " vx_k = vx[:,j].unsqueeze(-1)\n",
- " ax_k = ax[:,j].unsqueeze(-1)\n",
- " curv_k = curv[:,j].unsqueeze(-1)\n",
+ " steer_k = steer[:, j : j + samples_past_steer]\n",
+ " vx_k = vx[:, j].unsqueeze(-1)\n",
+ " ax_k = ax[:, j].unsqueeze(-1)\n",
+ " curv_k = curv[:, j].unsqueeze(-1)\n",
"\n",
" # Make one-step prediction\n",
- " output = model(steer_k,vx_k, ax_k)\n",
+ " output = model(steer_k, vx_k, ax_k)\n",
" # Compute the loss\n",
" loss_curv += loss_fn(output, curv_k)\n",
- " psi_ref = psi0_ref + curv_k*vx_k*sampling\n",
- " psi_comp = psi0_comp + output*vx_k*sampling\n",
- " loss_head += loss_fn(psi_ref,psi_comp)\n",
+ " psi_ref = psi0_ref + curv_k * vx_k * sampling\n",
+ " psi_comp = psi0_comp + output * vx_k * sampling\n",
+ " loss_head += loss_fn(psi_ref, psi_comp)\n",
"\n",
" psi0_comp = psi_comp\n",
" psi0_ref = psi_ref\n",
@@ -750,7 +772,6 @@
" return loss, loss_curv, loss_head\n",
"\n",
"\n",
- "\n",
"def train_one_epoch_rec():\n",
" avg_loss = 0.0\n",
" avg_curv_loss = 0.0\n",
@@ -762,7 +783,7 @@
" optimizer.zero_grad()\n",
"\n",
" # compute loss and its gradient\n",
- " loss, loss_curv, loss_head = inference_rec(steer,vx,ax,curv)\n",
+ " loss, loss_curv, loss_head = inference_rec(steer, vx, ax, curv)\n",
" loss.backward()\n",
"\n",
" avg_loss += loss.item()\n",
@@ -773,10 +794,14 @@
" # Adjust learning weights\n",
" optimizer.step()\n",
"\n",
- " return avg_loss / len(train_dataloader_rec), avg_curv_loss / len(train_dataloader_rec), avg_head_loss / len(train_dataloader_rec)\n",
+ " return (\n",
+ " avg_loss / len(train_dataloader_rec),\n",
+ " avg_curv_loss / len(train_dataloader_rec),\n",
+ " avg_head_loss / len(train_dataloader_rec),\n",
+ " )\n",
"\n",
"\n",
- "print('| {:^7} | {:^12} | {:^12} |'.format('EPOCHS','train_loss','valid_loss'))\n",
+ "print(\"| {:^7} | {:^12} | {:^12} |\".format(\"EPOCHS\", \"train_loss\", \"valid_loss\"))\n",
"for epoch in range(EPOCHS):\n",
" # Make sure gradient tracking is on, and do a pass over the data\n",
" model.train(True)\n",
@@ -795,8 +820,8 @@
" with torch.no_grad():\n",
" for i, vdata in enumerate(valid_dataloader_rec):\n",
" steer, vx, ax, curv = vdata\n",
- " \n",
- " vloss, vloss_curv, vloss_head = inference_rec(steer,vx,ax,curv)\n",
+ "\n",
+ " vloss, vloss_curv, vloss_head = inference_rec(steer, vx, ax, curv)\n",
" avg_vloss += vloss.item()\n",
" avg_vloss_curv += vloss_curv.item()\n",
" avg_vloss_head += vloss_head.item()\n",
@@ -806,29 +831,29 @@
" avg_vloss_head /= len(valid_dataloader_rec)\n",
" head_vloss_list.append(avg_vloss_head)\n",
" curv_vloss_list.append(avg_vloss_curv)\n",
- " \n",
- " print('| {:^7} | {:^12.6e} | {:^12.6e} |'.format(epoch+1, avg_loss, avg_vloss))\n",
+ "\n",
+ " print(\"| {:^7} | {:^12.6e} | {:^12.6e} |\".format(epoch + 1, avg_loss, avg_vloss))\n",
" epoch += 1\n",
"\n",
"\n",
"plt.figure()\n",
- "plt.title('Recurrent training')\n",
- "plt.plot(curv_loss_list, label='train curvature loss')\n",
- "plt.plot(curv_vloss_list, '--', label='validation curvature loss')\n",
- "plt.yscale('log')\n",
- "plt.xlabel('Epochs')\n",
- "plt.ylabel('Loss')\n",
+ "plt.title(\"Recurrent training\")\n",
+ "plt.plot(curv_loss_list, label=\"train curvature loss\")\n",
+ "plt.plot(curv_vloss_list, \"--\", label=\"validation curvature loss\")\n",
+ "plt.yscale(\"log\")\n",
+ "plt.xlabel(\"Epochs\")\n",
+ "plt.ylabel(\"Loss\")\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.show()\n",
"\n",
"plt.figure()\n",
- "plt.title('Recurrent training')\n",
- "plt.plot(head_loss_list, label='train heading loss')\n",
- "plt.plot(head_vloss_list, '--', label='validation heading loss')\n",
- "plt.yscale('log')\n",
- "plt.xlabel('Epochs')\n",
- "plt.ylabel('Loss')\n",
+ "plt.title(\"Recurrent training\")\n",
+ "plt.plot(head_loss_list, label=\"train heading loss\")\n",
+ "plt.plot(head_vloss_list, \"--\", label=\"validation heading loss\")\n",
+ "plt.yscale(\"log\")\n",
+ "plt.xlabel(\"Epochs\")\n",
+ "plt.ylabel(\"Loss\")\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.show()\n",
@@ -865,14 +890,14 @@
],
"source": [
"# inference\n",
- "model = LateralDynamics(chan_vx,chan_ax,samples_past_steer)\n",
+ "model = LateralDynamics(chan_vx, chan_ax, samples_past_steer)\n",
"model.load_state_dict(torch.load(path_folder + \"/vehicle_model_torch.pt\"))\n",
"\n",
"curv_targ = []\n",
"curv_out = []\n",
"\n",
- "for i,data in enumerate(test_dataloader_analyse):\n",
- " steer, vx, ax , curv = data\n",
+ "for i, data in enumerate(test_dataloader_analyse):\n",
+ " steer, vx, ax, curv = data\n",
"\n",
" model.eval()\n",
" curv_ = model(steer, vx, ax)\n",
@@ -880,16 +905,25 @@
" curv_targ.append(curv)\n",
" curv_out.append(curv_)\n",
"\n",
- "curv_targ = torch.cat(curv_targ,dim=0).squeeze().numpy()\n",
- "curv_out = torch.cat(curv_out,dim=0).squeeze().detach().numpy()\n",
+ "curv_targ = torch.cat(curv_targ, dim=0).squeeze().numpy()\n",
+ "curv_out = torch.cat(curv_out, dim=0).squeeze().detach().numpy()\n",
"\n",
"# Plot comparison\n",
- "plt.figure(figsize=(12,6))\n",
- "plt.plot(sampling*np.linspace(0,len(curv_targ),len(curv_targ)),curv_targ, label='Target curvature')\n",
- "plt.plot(sampling*np.linspace(0,len(curv_out),len(curv_out)),curv_out, '--', label='Predicted curvature')\n",
- "plt.xlabel('Time (s)')\n",
- "plt.ylabel('Curvature (1/m)')\n",
- "plt.title('Curvature Prediction vs Target')\n",
+ "plt.figure(figsize=(12, 6))\n",
+ "plt.plot(\n",
+ " sampling * np.linspace(0, len(curv_targ), len(curv_targ)),\n",
+ " curv_targ,\n",
+ " label=\"Target curvature\",\n",
+ ")\n",
+ "plt.plot(\n",
+ " sampling * np.linspace(0, len(curv_out), len(curv_out)),\n",
+ " curv_out,\n",
+ " \"--\",\n",
+ " label=\"Predicted curvature\",\n",
+ ")\n",
+ "plt.xlabel(\"Time (s)\")\n",
+ "plt.ylabel(\"Curvature (1/m)\")\n",
+ "plt.title(\"Curvature Prediction vs Target\")\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.show()"
@@ -922,19 +956,25 @@
],
"source": [
"sampling = 0.05\n",
- "W_fir = torch.tensor([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+1,1)],dtype=torch.float32).squeeze(-1)\n",
+ "W_fir = torch.tensor(\n",
+ " [\n",
+ " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n",
+ " for i in range(-samples_past_steer + 1, 1)\n",
+ " ],\n",
+ " dtype=torch.float32,\n",
+ ").squeeze(-1)\n",
"\n",
"for name, param in model.named_parameters():\n",
- " if name.startswith('fir_layer'):\n",
+ " if name.startswith(\"fir_layer\"):\n",
" fir_param = param\n",
"\n",
"plt.figure()\n",
"\n",
- "for i in range(fir_param[:,0,0].detach().numpy().size):\n",
- " weight_loc = fir_param[i,0,:]*W_fir\n",
+ "for i in range(fir_param[:, 0, 0].detach().numpy().size):\n",
+ " weight_loc = fir_param[i, 0, :] * W_fir\n",
" weight_loc = weight_loc.detach().numpy()\n",
- " time = np.linspace(0,sampling*(len(weight_loc)-1),len(weight_loc))\n",
- " plt.plot(time, np.flip(weight_loc),'o',label=f\"Fir_{i}\")\n",
+ " time = np.linspace(0, sampling * (len(weight_loc) - 1), len(weight_loc))\n",
+ " plt.plot(time, np.flip(weight_loc), \"o\", label=f\"Fir_{i}\")\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.show()"
diff --git a/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb b/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb
index 19e7df06..c1106b78 100644
--- a/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb
+++ b/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb
@@ -103,22 +103,25 @@
"T_x = 0.1\n",
"\n",
"# Define the neural model\n",
- "x = Input('x') # MSNN input (Mass position)\n",
- "F = Input('F') # MSNN input (Force)\n",
- "x_free = Fir(W_init = 'init_negexp', W_init_params = {'first_value':0.1,'size_index':0,'lambda':3})(x.tw(T_x))\n",
+ "x = Input(\"x\") # MSNN input (Mass position)\n",
+ "F = Input(\"F\") # MSNN input (Force)\n",
+ "x_free = Fir(\n",
+ " W_init=\"init_negexp\",\n",
+ " W_init_params={\"first_value\": 0.1, \"size_index\": 0, \"lambda\": 3},\n",
+ ")(x.tw(T_x))\n",
"x_force = Fir(F.tw(T_x))\n",
- "x_n = Output('x_n', x_free + x_force)\n",
+ "x_n = Output(\"x_n\", x_free + x_force)\n",
"\n",
"# Add the neural models to the nnodely structure\n",
- "msd = Modely(seed=42, workspace='saved')\n",
- "msd.addModel('neural_msd', x_n)\n",
+ "msd = Modely(seed=42, workspace=\"saved\")\n",
+ "msd.addModel(\"neural_msd\", x_n)\n",
"\n",
"# These functions are used to impose the minimization objectives.\n",
"# Here it is minimized the error between the future position of x get from the dataset\n",
"# and the estimator designed using the neural network.\n",
"# The minimization is imposed via MSE error.\n",
- "x_t = Input('x_t') # Real position\n",
- "msd.addMinimize('x[t]', x_t.next(), x_n)\n",
+ "x_t = Input(\"x_t\") # Real position\n",
+ "msd.addMinimize(\"x[t]\", x_t.next(), x_n)\n",
"\n",
"# Nauralize the model and getting the neural network.\n",
"# The sampling time depends on the datasets.\n",
@@ -277,20 +280,22 @@
}
],
"source": [
- "data_struct = ['time', ('x','x_t'), '', 'F']\n",
- "msd.loadData(name = 'simulations',\n",
- " source = '../msd-data/data',\n",
- " format = data_struct, delimiter = ';')\n",
+ "data_struct = [\"time\", (\"x\", \"x_t\"), \"\", \"F\"]\n",
+ "msd.loadData(\n",
+ " name=\"simulations\", source=\"../msd-data/data\", format=data_struct, delimiter=\";\"\n",
+ ")\n",
"\n",
"# Neural network train\n",
- "param_model = {'num_of_epochs' : 80,\n",
- " 'train_batch_size' : 128,\n",
- " 'lr' : 0.0005,\n",
- " 'splits' : [70,20,10]}\n",
- "msd.trainModel(training_params = param_model)\n",
+ "param_model = {\n",
+ " \"num_of_epochs\": 80,\n",
+ " \"train_batch_size\": 128,\n",
+ " \"lr\": 0.0005,\n",
+ " \"splits\": [70, 20, 10],\n",
+ "}\n",
+ "msd.trainModel(training_params=param_model)\n",
"\n",
"# Save the neural model\n",
- "msd.exportPythonModel(name = 'msd_preliminary')"
+ "msd.exportPythonModel(name=\"msd_preliminary\")"
]
},
{
@@ -322,13 +327,15 @@
],
"source": [
"# Show the network performance on the test dataset\n",
- "samples = msd.getSamples(dataset='simulations', window=2000-10, index=50*2000-450)\n",
- "result = msd(samples, sampled=True, prediction_samples=2000-10, closed_loop={'x':'x_n'})\n",
+ "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=50 * 2000 - 450)\n",
+ "result = msd(\n",
+ " samples, sampled=True, prediction_samples=2000 - 10, closed_loop={\"x\": \"x_n\"}\n",
+ ")\n",
"\n",
- "plt.figure(figsize=(10,5))\n",
- "t = np.arange(len(result['x_n'])) * 0.01\n",
- "plt.plot(t, result['x_n'], label=\"pred\", linewidth=2)\n",
- "plt.plot(t, np.array(samples['x_t'])[:,0,0], '--', label=\"target\", linewidth=2)\n",
+ "plt.figure(figsize=(10, 5))\n",
+ "t = np.arange(len(result[\"x_n\"])) * 0.01\n",
+ "plt.plot(t, result[\"x_n\"], label=\"pred\", linewidth=2)\n",
+ "plt.plot(t, np.array(samples[\"x_t\"])[:, 0, 0], \"--\", label=\"target\", linewidth=2)\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.title(\"Model Rollout vs Target\")\n",
@@ -412,10 +419,17 @@
],
"source": [
"# Refine weights with recurrent train\n",
- "msd.trainModel(num_of_epochs = 10, prediction_samples = 1500, step = 500, lr=0.00001, closed_loop={'x':'x_n'}, training_params = param_model)\n",
+ "msd.trainModel(\n",
+ " num_of_epochs=10,\n",
+ " prediction_samples=1500,\n",
+ " step=500,\n",
+ " lr=0.00001,\n",
+ " closed_loop={\"x\": \"x_n\"},\n",
+ " training_params=param_model,\n",
+ ")\n",
"\n",
"# Save the neural model in json format\n",
- "msd.saveModel(name = 'msd_final')"
+ "msd.saveModel(name=\"msd_final\")"
]
},
{
@@ -437,13 +451,15 @@
],
"source": [
"# Show the network performance on the test dataset after recurrent training\n",
- "samples = msd.getSamples(dataset='simulations', window=2000-10, index=50*2000-450)\n",
- "result = msd(samples, sampled=True, prediction_samples=2000-10, closed_loop={'x':'x_n'})\n",
+ "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=50 * 2000 - 450)\n",
+ "result = msd(\n",
+ " samples, sampled=True, prediction_samples=2000 - 10, closed_loop={\"x\": \"x_n\"}\n",
+ ")\n",
"\n",
- "plt.figure(figsize=(10,5))\n",
- "t = np.arange(len(result['x_n'])) * 0.01\n",
- "plt.plot(t, result['x_n'], label=\"pred\", linewidth=2)\n",
- "plt.plot(t, np.array(samples['x_t'])[:,0,0], '--', label=\"target\", linewidth=2)\n",
+ "plt.figure(figsize=(10, 5))\n",
+ "t = np.arange(len(result[\"x_n\"])) * 0.01\n",
+ "plt.plot(t, result[\"x_n\"], label=\"pred\", linewidth=2)\n",
+ "plt.plot(t, np.array(samples[\"x_t\"])[:, 0, 0], \"--\", label=\"target\", linewidth=2)\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.title(\"Model Rollout vs Target\")\n",
@@ -587,14 +603,14 @@
}
],
"source": [
- "x_m = Input('x_m') # measured position\n",
- "kp = Parameter('P', values=0.5)\n",
- "ki = Parameter('I', values=0.5)\n",
- "kd = Parameter('D', values=0.5)\n",
- "e = x_t.next()-x_m.next()\n",
- "c = e*kp+Integrate(e)*ki+Differentiate(e)*kd\n",
- "controlForce = Output('F_PID', c)\n",
- "msd.addModel('PID',controlForce)\n",
+ "x_m = Input(\"x_m\") # measured position\n",
+ "kp = Parameter(\"P\", values=0.5)\n",
+ "ki = Parameter(\"I\", values=0.5)\n",
+ "kd = Parameter(\"D\", values=0.5)\n",
+ "e = x_t.next() - x_m.next()\n",
+ "c = e * kp + Integrate(e) * ki + Differentiate(e) * kd\n",
+ "controlForce = Output(\"F_PID\", c)\n",
+ "msd.addModel(\"PID\", controlForce)\n",
"\n",
"# Neuralization of the whole models\n",
"msd.neuralizeModel()"
@@ -679,16 +695,21 @@
],
"source": [
"# Train the PID controller\n",
- "msd.trainModel(models = 'PID',\n",
- " closed_loop = {'x' : 'x_n', 'x_m' : 'x_n'},\n",
- " connect = {'F' : 'F_PID'},\n",
- " prediction_samples = 500,\n",
- " step = 500, num_of_epochs = 20,\n",
- " lr = 0.05,\n",
- " training_params = param_model)\n",
+ "msd.trainModel(\n",
+ " models=\"PID\",\n",
+ " closed_loop={\"x\": \"x_n\", \"x_m\": \"x_n\"},\n",
+ " connect={\"F\": \"F_PID\"},\n",
+ " prediction_samples=500,\n",
+ " step=500,\n",
+ " num_of_epochs=20,\n",
+ " lr=0.05,\n",
+ " training_params=param_model,\n",
+ ")\n",
"\n",
"# Print the parameter of the PID\n",
- "print(f\"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}\")"
+ "print(\n",
+ " f\"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}\"\n",
+ ")"
]
},
{
@@ -698,7 +719,7 @@
"metadata": {},
"outputs": [],
"source": [
- "msd.exportPythonModel(name = 'msd_final_with_PID')"
+ "msd.exportPythonModel(name=\"msd_final_with_PID\")"
]
},
{
@@ -741,16 +762,20 @@
],
"source": [
"# Test the controller on step and triangular signal\n",
- "tt = np.linspace(0, 5, int(5/0.01), endpoint=False)\n",
- "data_target = np.concat([np.ones(511,dtype=np.float32)*1.0, # Step\n",
- " -2*np.ones(500,dtype=np.float32)*1.0,\n",
- " 2 * np.abs( 2 * (tt*1/5 - np.floor(tt*1/5 + 0.5)) ) - 1,\n",
- " 2 * np.abs( 2 * (tt*1/2.5 - np.floor(tt*1/2.5 + 0.5)) ) - 1])\n",
- "msd.loadData('test_control', {'x_t': data_target})\n",
+ "tt = np.linspace(0, 5, int(5 / 0.01), endpoint=False)\n",
+ "data_target = np.concat(\n",
+ " [\n",
+ " np.ones(511, dtype=np.float32) * 1.0, # Step\n",
+ " -2 * np.ones(500, dtype=np.float32) * 1.0,\n",
+ " 2 * np.abs(2 * (tt * 1 / 5 - np.floor(tt * 1 / 5 + 0.5))) - 1,\n",
+ " 2 * np.abs(2 * (tt * 1 / 2.5 - np.floor(tt * 1 / 2.5 + 0.5))) - 1,\n",
+ " ]\n",
+ ")\n",
+ "msd.loadData(\"test_control\", {\"x_t\": data_target})\n",
"\n",
"vis = MPLNotebookVisualizer()\n",
"vis.setModely(msd)\n",
- "vis.showResult('test_control')\n"
+ "vis.showResult(\"test_control\")"
]
}
],
diff --git a/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb b/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb
index 62616256..50482538 100644
--- a/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb
+++ b/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb
@@ -54,11 +54,11 @@
"\n",
" if init_negexp:\n",
" t = torch.arange(window_size).float()\n",
- " w = torch.exp(-3*t)\n",
+ " w = torch.exp(-3 * t)\n",
" w = 0.1 * w / w.sum()\n",
" self.weight.data[:] = w.flip(0)\n",
" else:\n",
- " self.weight.data.uniform_(.0, 1.0)\n",
+ " self.weight.data.uniform_(0.0, 1.0)\n",
"\n",
" def forward(self, x):\n",
" # x: (B, T)\n",
@@ -82,7 +82,7 @@
"\n",
" def step(self, x, u):\n",
" return self.x_free(x) + self.x_force(u)\n",
- " \n",
+ "\n",
" def forward(self, x, u):\n",
" return self.step(x, u)\n",
"\n",
@@ -90,9 +90,9 @@
" preds = []\n",
"\n",
" for k in range(horizon):\n",
- " u_k = u_seq[:, k:k+self.window_dim]\n",
+ " u_k = u_seq[:, k : k + self.window_dim]\n",
"\n",
- " x_next = self.step(x, u_k) # (B,1)\n",
+ " x_next = self.step(x, u_k) # (B,1)\n",
"\n",
" preds.append(x_next.squeeze(1))\n",
"\n",
@@ -120,23 +120,25 @@
"outputs": [],
"source": [
"def load_simulation_file(path):\n",
- " data = np.loadtxt(path, delimiter=';')\n",
+ " data = np.loadtxt(path, delimiter=\";\")\n",
"\n",
- " time = data[:,0]\n",
- " x = data[:,1]\n",
- " v = data[:,2]\n",
- " u = data[:,3]\n",
+ " time = data[:, 0]\n",
+ " x = data[:, 1]\n",
+ " v = data[:, 2]\n",
+ " u = data[:, 3]\n",
"\n",
" return x, v, u\n",
"\n",
+ "\n",
"from torch.utils.data import Dataset\n",
"from torch.utils.data import DataLoader\n",
"from torch.utils.data import random_split\n",
"\n",
+ "\n",
"class MSDSimDataset(Dataset):\n",
" def __init__(self, folder, window_dim, horizon=1, stride=1):\n",
" self.window_dim = window_dim\n",
- " self.data = [] # file list\n",
+ " self.data = [] # file list\n",
" self.index_map = [] # global window map\n",
" self.horizon = horizon\n",
" self.stride = stride\n",
@@ -147,9 +149,9 @@
" for f in files:\n",
" x, v, u = load_simulation_file(f)\n",
"\n",
- " x = torch.tensor(x, dtype=torch.float32)\n",
+ " x = torch.tensor(x, dtype=torch.float32)\n",
" v = torch.tensor(v, dtype=torch.float32)\n",
- " u = torch.tensor(u, dtype=torch.float32)\n",
+ " u = torch.tensor(u, dtype=torch.float32)\n",
"\n",
" self.data.append((x, v, u))\n",
"\n",
@@ -174,23 +176,24 @@
" return (\n",
" x[start : start + W],\n",
" x[start + W : start + W + H],\n",
- " u[start : start + W + H - 1]\n",
+ " u[start : start + W + H - 1],\n",
" )\n",
"\n",
+ "\n",
"WINDOW_DIM = 10\n",
"HORIZON = 1\n",
"\n",
"dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON)\n",
"n = len(dataset)\n",
- "n_train = int(0.7*n)\n",
- "n_val = int(0.2*n)\n",
- "n_test = n - n_train - n_val\n",
+ "n_train = int(0.7 * n)\n",
+ "n_val = int(0.2 * n)\n",
+ "n_test = n - n_train - n_val\n",
"\n",
- "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n",
+ "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n",
"\n",
"train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n",
- "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n",
- "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)"
+ "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n",
+ "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)"
]
},
{
@@ -222,7 +225,7 @@
"opt = torch.optim.Adam(model.parameters(), lr=5e-4)\n",
"loss_fn = nn.MSELoss()\n",
"\n",
- "best_val_loss = float('inf')\n",
+ "best_val_loss = float(\"inf\")\n",
"\n",
"train_losses = []\n",
"val_losses = []\n",
@@ -258,7 +261,9 @@
" best_val_loss = val_loss\n",
" torch.save(model.state_dict(), \"saved/best_model.pth\")\n",
" val_losses.append(val_loss)\n",
- " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")"
+ " pbar.set_description(\n",
+ " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n",
+ " )"
]
},
{
@@ -294,13 +299,15 @@
"best_model = NeuralMSD(window_dim=WINDOW_DIM)\n",
"best_model.load_state_dict(torch.load(\"saved/best_model.pth\"))\n",
"\n",
- "preds = best_model.rollout(x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x)-WINDOW_DIM).squeeze(0)\n",
+ "preds = best_model.rollout(\n",
+ " x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x) - WINDOW_DIM\n",
+ ").squeeze(0)\n",
"\n",
"target = x[WINDOW_DIM:]\n",
"t = np.arange(len(preds)) * 0.01\n",
- "plt.figure(figsize=(12,6))\n",
+ "plt.figure(figsize=(12, 6))\n",
"plt.plot(t, preds.detach().numpy(), label=\"pred\", linewidth=2)\n",
- "plt.plot(t, target.numpy(), '--', label=\"target\", linewidth=2)\n",
+ "plt.plot(t, target.numpy(), \"--\", label=\"target\", linewidth=2)\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.title(\"Model Rollout vs Target\")\n",
@@ -331,17 +338,19 @@
"# Recurrent dataset\n",
"HORIZON = 1500\n",
"\n",
- "dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5)\n",
+ "dataset = MSDSimDataset(\n",
+ " \"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5\n",
+ ")\n",
"n = len(dataset)\n",
- "n_train = int(0.7*n)\n",
- "n_val = int(0.2*n)\n",
- "n_test = n - n_train - n_val\n",
+ "n_train = int(0.7 * n)\n",
+ "n_val = int(0.2 * n)\n",
+ "n_test = n - n_train - n_val\n",
"\n",
- "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n",
+ "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n",
"\n",
"train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n",
- "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n",
- "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)"
+ "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n",
+ "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)"
]
},
{
@@ -364,7 +373,7 @@
"opt = torch.optim.Adam(model.parameters(), lr=1e-5)\n",
"loss_fn = nn.MSELoss()\n",
"\n",
- "best_val_loss = float('inf')\n",
+ "best_val_loss = float(\"inf\")\n",
"\n",
"train_losses = []\n",
"val_losses = []\n",
@@ -400,7 +409,9 @@
" best_val_loss = val_loss\n",
" torch.save(model.state_dict(), \"saved/best_model.pth\")\n",
" val_losses.append(val_loss)\n",
- " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")"
+ " pbar.set_description(\n",
+ " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n",
+ " )"
]
},
{
@@ -427,13 +438,15 @@
"best_model = NeuralMSD(window_dim=WINDOW_DIM)\n",
"best_model.load_state_dict(torch.load(\"saved/best_model.pth\"))\n",
"\n",
- "preds = best_model.rollout(x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x)-WINDOW_DIM).squeeze(0)\n",
+ "preds = best_model.rollout(\n",
+ " x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x) - WINDOW_DIM\n",
+ ").squeeze(0)\n",
"\n",
"target = x[WINDOW_DIM:]\n",
"t = np.arange(len(preds)) * 0.01\n",
- "plt.figure(figsize=(12,6))\n",
+ "plt.figure(figsize=(12, 6))\n",
"plt.plot(t, preds.detach().numpy(), label=\"pred\", linewidth=2)\n",
- "plt.plot(t, target.numpy(), '--', label=\"target\", linewidth=2)\n",
+ "plt.plot(t, target.numpy(), \"--\", label=\"target\", linewidth=2)\n",
"plt.legend()\n",
"plt.grid()\n",
"plt.title(\"Model Rollout vs Target\")\n",
@@ -476,7 +489,7 @@
" def reset_state(self, batch_size, device=None):\n",
" device = device or self.kp.device\n",
" self.integ_state = torch.zeros((batch_size, 1), device=device)\n",
- " self.prev_error = torch.zeros((batch_size, 1), device=device)\n",
+ " self.prev_error = torch.zeros((batch_size, 1), device=device)\n",
"\n",
" def forward(self, e):\n",
" \"\"\"\n",
@@ -492,11 +505,7 @@
"\n",
" self.prev_error = e\n",
"\n",
- " u = (\n",
- " self.kp * e +\n",
- " self.ki * self.integ_state +\n",
- " self.kd * deriv\n",
- " )\n",
+ " u = self.kp * e + self.ki * self.integ_state + self.kd * deriv\n",
"\n",
" return u"
]
@@ -521,11 +530,11 @@
" self.pid.reset_state(batch_size=x0.shape[0], device=x0.device)\n",
"\n",
" for k in range(target.shape[1]):\n",
- " e = target[:,k] - x[:, -1]\n",
+ " e = target[:, k] - x[:, -1]\n",
" # shift control sequence and append new control\n",
" u_seq = torch.cat([u_seq[:, 1:], self.pid(e.unsqueeze(1))], dim=1)\n",
" x_next = self.plant.step(x, u_seq)\n",
- " \n",
+ "\n",
" # shift state sequence and append new state\n",
" x = torch.cat([x[:, 1:], x_next], dim=1)\n",
" xs.append(x_next.squeeze(1))\n",
@@ -553,17 +562,19 @@
"# PID dataset\n",
"HORIZON = 500\n",
"\n",
- "dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5)\n",
+ "dataset = MSDSimDataset(\n",
+ " \"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5\n",
+ ")\n",
"n = len(dataset)\n",
- "n_train = int(0.7*n)\n",
- "n_val = int(0.2*n)\n",
- "n_test = n - n_train - n_val\n",
+ "n_train = int(0.7 * n)\n",
+ "n_val = int(0.2 * n)\n",
+ "n_test = n - n_train - n_val\n",
"\n",
- "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n",
+ "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n",
"\n",
"train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n",
- "val_loader = DataLoader(val_ds, batch_size=2048, shuffle=False)\n",
- "test_loader = DataLoader(test_ds, batch_size=2048, shuffle=False)"
+ "val_loader = DataLoader(val_ds, batch_size=2048, shuffle=False)\n",
+ "test_loader = DataLoader(test_ds, batch_size=2048, shuffle=False)"
]
},
{
@@ -589,7 +600,7 @@
"opt = torch.optim.Adam(pid.parameters(), lr=0.05)\n",
"loss_fn = nn.MSELoss()\n",
"\n",
- "best_val_loss = float('inf')\n",
+ "best_val_loss = float(\"inf\")\n",
"\n",
"train_losses = []\n",
"val_losses = []\n",
@@ -625,7 +636,9 @@
" best_val_loss = val_loss\n",
" torch.save(pid.state_dict(), \"saved/best_controller.pth\")\n",
" val_losses.append(val_loss)\n",
- " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")"
+ " pbar.set_description(\n",
+ " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n",
+ " )"
]
},
{
@@ -644,7 +657,9 @@
],
"source": [
"# Print the parameter of the PID\n",
- "print(f\"Trained PID parameters: kp={pid.kp.item():.3f}, ki={pid.ki.item():.3f}, kd={pid.kd.item():.3f}\")"
+ "print(\n",
+ " f\"Trained PID parameters: kp={pid.kp.item():.3f}, ki={pid.ki.item():.3f}, kd={pid.kd.item():.3f}\"\n",
+ ")"
]
},
{
@@ -668,13 +683,20 @@
"best_pid.load_state_dict(torch.load(\"saved/best_controller.pth\"))\n",
"best_system = ControlledSystem(best_model, best_pid)\n",
"\n",
- "tt = torch.linspace(0, 5, int(5/0.01))\n",
- "data_target = torch.cat([torch.ones(511,dtype=torch.float32)*1.0, # Step\n",
- " -2*torch.ones(500,dtype=torch.float32)*1.0,\n",
- " 2 * torch.abs( 2 * (tt*1/5 - torch.floor(tt*1/5 + 0.5)) ) - 1, # Triangle\n",
- " 2 * torch.abs( 2 * (tt*1/2.5 - torch.floor(tt*1/2.5 + 0.5)) ) - 1])\n",
+ "tt = torch.linspace(0, 5, int(5 / 0.01))\n",
+ "data_target = torch.cat(\n",
+ " [\n",
+ " torch.ones(511, dtype=torch.float32) * 1.0, # Step\n",
+ " -2 * torch.ones(500, dtype=torch.float32) * 1.0,\n",
+ " 2 * torch.abs(2 * (tt * 1 / 5 - torch.floor(tt * 1 / 5 + 0.5))) - 1, # Triangle\n",
+ " 2 * torch.abs(2 * (tt * 1 / 2.5 - torch.floor(tt * 1 / 2.5 + 0.5))) - 1,\n",
+ " ]\n",
+ ")\n",
"best_system.eval()\n",
- "preds = best_system(torch.zeros_like(data_target[1:WINDOW_DIM+1].unsqueeze(0)), data_target[WINDOW_DIM+1:].unsqueeze(0))"
+ "preds = best_system(\n",
+ " torch.zeros_like(data_target[1 : WINDOW_DIM + 1].unsqueeze(0)),\n",
+ " data_target[WINDOW_DIM + 1 :].unsqueeze(0),\n",
+ ")"
]
},
{
@@ -697,18 +719,39 @@
"source": [
"df = pd.read_csv(\"PID_test_nnodely.csv\")\n",
"\n",
- "plt.figure(figsize=(12,6))\n",
+ "plt.figure(figsize=(12, 6))\n",
"t = torch.arange(len(preds.squeeze(0).detach().numpy())) * 0.01\n",
- "plt.plot(t.numpy(), data_target[WINDOW_DIM+1:].numpy(), '-', label=\"Target\", linewidth=1, color='black')\n",
- "plt.plot(t.numpy(), df['estimate'].to_numpy(), '--', label=\"nnodely PID\", linewidth=3, color='tab:blue')\n",
- "plt.plot(t.numpy(), preds.squeeze(0).detach().numpy(), '-.', label=\"PyTorch PID\", linewidth=3, color='tab:orange')\n",
+ "plt.plot(\n",
+ " t.numpy(),\n",
+ " data_target[WINDOW_DIM + 1 :].numpy(),\n",
+ " \"-\",\n",
+ " label=\"Target\",\n",
+ " linewidth=1,\n",
+ " color=\"black\",\n",
+ ")\n",
+ "plt.plot(\n",
+ " t.numpy(),\n",
+ " df[\"estimate\"].to_numpy(),\n",
+ " \"--\",\n",
+ " label=\"nnodely PID\",\n",
+ " linewidth=3,\n",
+ " color=\"tab:blue\",\n",
+ ")\n",
+ "plt.plot(\n",
+ " t.numpy(),\n",
+ " preds.squeeze(0).detach().numpy(),\n",
+ " \"-.\",\n",
+ " label=\"PyTorch PID\",\n",
+ " linewidth=3,\n",
+ " color=\"tab:orange\",\n",
+ ")\n",
"plt.xlim(0, 20)\n",
"plt.ylim(-3.5, 1.5)\n",
"plt.grid()\n",
"plt.title(\"PID Controller Performance Comparison\")\n",
"plt.xlabel(\"Time [s]\")\n",
"plt.ylabel(\"Position [m]\")\n",
- "plt.legend(loc='lower right', fontsize=24)\n",
+ "plt.legend(loc=\"lower right\", fontsize=24)\n",
"plt.show()"
]
}
diff --git a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json
index 8a4622e2..46ab942d 100644
--- a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json
+++ b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json
@@ -111,4 +111,4 @@
"Sub13": ["Sub", ["SamplePart10", "SamplePart12"]],
"Sub29": ["Sub", ["SamplePart26", "SamplePart28"]],
"TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]],
- "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
\ No newline at end of file
+ "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
diff --git a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py
index 1d916b74..1704b63d 100644
--- a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py
+++ b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py
@@ -1,107 +1,241 @@
import torch
+
def nnodely_basic_model_update_state(data_in, rel):
data_out = data_in.clone()
max_dim = min(rel.size(1), data_in.size(1))
data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]
return data_out
+
def nnodely_basic_model_timeshift(data_in):
return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)
+
class TracerModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.all_parameters = {}
self.all_constants = {}
- self.all_constants["SampleTime"] = torch.tensor(0.009999999776482582, requires_grad=False)
- self.all_parameters["D"] = torch.nn.Parameter(torch.tensor([8.177507400512695]), requires_grad=True)
- self.all_parameters["I"] = torch.nn.Parameter(torch.tensor([18.563440322875977]), requires_grad=True)
- self.all_parameters["P"] = torch.nn.Parameter(torch.tensor([16.827768325805664]), requires_grad=True)
- self.all_parameters["PFir3W"] = torch.nn.Parameter(torch.tensor([[-0.18469615280628204], [-0.12608873844146729], [-0.06660982221364975], [-0.005940185859799385], [0.05625353008508682], [0.1205686405301094], [0.18780383467674255], [0.2590898871421814], [0.3359234631061554], [0.4205183684825897]]), requires_grad=True)
- self.all_parameters["PFir5W"] = torch.nn.Parameter(torch.tensor([[-1.662617978581693e-05], [4.8189936933340505e-05], [7.177559018600732e-05], [5.9527203120524064e-05], [0.00011585278843995184], [0.00019496057939250022], [0.00014330405974760652], [0.00018421869026497006], [0.00017740165640134364], [8.003542461665347e-05]]), requires_grad=True)
+ self.all_constants["SampleTime"] = torch.tensor(
+ 0.009999999776482582, requires_grad=False
+ )
+ self.all_parameters["D"] = torch.nn.Parameter(
+ torch.tensor([8.177507400512695]), requires_grad=True
+ )
+ self.all_parameters["I"] = torch.nn.Parameter(
+ torch.tensor([18.563440322875977]), requires_grad=True
+ )
+ self.all_parameters["P"] = torch.nn.Parameter(
+ torch.tensor([16.827768325805664]), requires_grad=True
+ )
+ self.all_parameters["PFir3W"] = torch.nn.Parameter(
+ torch.tensor(
+ [
+ [-0.18469615280628204],
+ [-0.12608873844146729],
+ [-0.06660982221364975],
+ [-0.005940185859799385],
+ [0.05625353008508682],
+ [0.1205686405301094],
+ [0.18780383467674255],
+ [0.2590898871421814],
+ [0.3359234631061554],
+ [0.4205183684825897],
+ ]
+ ),
+ requires_grad=True,
+ )
+ self.all_parameters["PFir5W"] = torch.nn.Parameter(
+ torch.tensor(
+ [
+ [-1.662617978581693e-05],
+ [4.8189936933340505e-05],
+ [7.177559018600732e-05],
+ [5.9527203120524064e-05],
+ [0.00011585278843995184],
+ [0.00019496057939250022],
+ [0.00014330405974760652],
+ [0.00018421869026497006],
+ [0.00017740165640134364],
+ [8.003542461665347e-05],
+ ]
+ ),
+ requires_grad=True,
+ )
self.all_constants["SamplePart10"] = torch.tensor([[1.0]], requires_grad=True)
self.all_constants["SamplePart12"] = torch.tensor([[1.0]], requires_grad=True)
self.all_constants["SamplePart17"] = torch.tensor([[1.0]], requires_grad=True)
- self.all_constants["SamplePart26"] = torch.tensor([[0.0, 1.0]], requires_grad=True)
- self.all_constants["SamplePart28"] = torch.tensor([[1.0, 0.0]], requires_grad=True)
+ self.all_constants["SamplePart26"] = torch.tensor(
+ [[0.0, 1.0]], requires_grad=True
+ )
+ self.all_constants["SamplePart28"] = torch.tensor(
+ [[1.0, 0.0]], requires_grad=True
+ )
self.all_constants["SamplePart8"] = torch.tensor([[1.0]], requires_grad=True)
- self.all_constants["TimePart1"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True)
- self.all_constants["TimePart4"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True)
+ self.all_constants["TimePart1"] = torch.tensor(
+ [
+ [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
+ ],
+ requires_grad=True,
+ )
+ self.all_constants["TimePart4"] = torch.tensor(
+ [
+ [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
+ ],
+ requires_grad=True,
+ )
self.all_parameters = torch.nn.ParameterDict(self.all_parameters)
self.all_constants = torch.nn.ParameterDict(self.all_constants)
def update(self, closed_loop={}, connect={}, disconnect=False):
pass
-
+
def forward(self, kwargs):
- getitem = kwargs['x_m']
+ getitem = kwargs["x_m"]
relation_forward_sample_part12_w = self.all_constants.SamplePart12
- einsum = torch.functional.einsum('bij,ki->bkj', getitem, relation_forward_sample_part12_w); getitem = relation_forward_sample_part12_w = None
- getitem_1 = kwargs['x_t']
+ einsum = torch.functional.einsum(
+ "bij,ki->bkj", getitem, relation_forward_sample_part12_w
+ )
+ getitem = relation_forward_sample_part12_w = None
+ getitem_1 = kwargs["x_t"]
relation_forward_sample_part10_w = self.all_constants.SamplePart10
- einsum_1 = torch.functional.einsum('bij,ki->bkj', getitem_1, relation_forward_sample_part10_w); getitem_1 = relation_forward_sample_part10_w = None
- sub = einsum_1 - einsum; einsum_1 = einsum = None
- getitem_2 = kwargs['Sub13_int14']
- update_state = nnodely_basic_model_update_state(getitem_2, sub); getitem_2 = None
+ einsum_1 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_1, relation_forward_sample_part10_w
+ )
+ getitem_1 = relation_forward_sample_part10_w = None
+ sub = einsum_1 - einsum
+ einsum_1 = einsum = None
+ getitem_2 = kwargs["Sub13_int14"]
+ update_state = nnodely_basic_model_update_state(getitem_2, sub)
+ getitem_2 = None
relation_forward_sample_part28_w = self.all_constants.SamplePart28
- einsum_2 = torch.functional.einsum('bij,ki->bkj', update_state, relation_forward_sample_part28_w); relation_forward_sample_part28_w = None
+ einsum_2 = torch.functional.einsum(
+ "bij,ki->bkj", update_state, relation_forward_sample_part28_w
+ )
+ relation_forward_sample_part28_w = None
relation_forward_sample_part26_w = self.all_constants.SamplePart26
- einsum_3 = torch.functional.einsum('bij,ki->bkj', update_state, relation_forward_sample_part26_w); relation_forward_sample_part26_w = None
- sub_1 = einsum_3 - einsum_2; einsum_3 = einsum_2 = None
+ einsum_3 = torch.functional.einsum(
+ "bij,ki->bkj", update_state, relation_forward_sample_part26_w
+ )
+ relation_forward_sample_part26_w = None
+ sub_1 = einsum_3 - einsum_2
+ einsum_3 = einsum_2 = None
all_constants_sample_time = self.all_constants.SampleTime
- truediv = sub_1 / all_constants_sample_time; sub_1 = None
+ truediv = sub_1 / all_constants_sample_time
+ sub_1 = None
all_parameters_d = self.all_parameters.D
- mul = truediv * all_parameters_d; truediv = all_parameters_d = None
- mul_1 = sub * all_constants_sample_time; all_constants_sample_time = None
- getitem_3 = kwargs['Sub13_int13']
+ mul = truediv * all_parameters_d
+ truediv = all_parameters_d = None
+ mul_1 = sub * all_constants_sample_time
+ all_constants_sample_time = None
+ getitem_3 = kwargs["Sub13_int13"]
relation_forward_sample_part17_w = self.all_constants.SamplePart17
- einsum_4 = torch.functional.einsum('bij,ki->bkj', getitem_3, relation_forward_sample_part17_w); getitem_3 = relation_forward_sample_part17_w = None
- add = einsum_4 + mul_1; einsum_4 = mul_1 = None
+ einsum_4 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_3, relation_forward_sample_part17_w
+ )
+ getitem_3 = relation_forward_sample_part17_w = None
+ add = einsum_4 + mul_1
+ einsum_4 = mul_1 = None
all_parameters_i = self.all_parameters.I
- mul_2 = add * all_parameters_i; all_parameters_i = None
+ mul_2 = add * all_parameters_i
+ all_parameters_i = None
all_parameters_p = self.all_parameters.P
- mul_3 = sub * all_parameters_p; sub = all_parameters_p = None
- add_1 = mul_3 + mul_2; mul_3 = mul_2 = None
- add_2 = add_1 + mul; add_1 = mul = None
- getitem_4 = kwargs['F']
+ mul_3 = sub * all_parameters_p
+ sub = all_parameters_p = None
+ add_1 = mul_3 + mul_2
+ mul_3 = mul_2 = None
+ add_2 = add_1 + mul
+ add_1 = mul = None
+ getitem_4 = kwargs["F"]
relation_forward_time_part4_w = self.all_constants.TimePart4
- einsum_5 = torch.functional.einsum('bij,ki->bkj', getitem_4, relation_forward_time_part4_w); getitem_4 = relation_forward_time_part4_w = None
+ einsum_5 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_4, relation_forward_time_part4_w
+ )
+ getitem_4 = relation_forward_time_part4_w = None
size = einsum_5.size(0)
relation_forward_fir5_weights = self.all_parameters.PFir5W
size_1 = relation_forward_fir5_weights.size(1)
- squeeze = einsum_5.squeeze(-1); einsum_5 = None
- matmul = torch.matmul(squeeze, relation_forward_fir5_weights); squeeze = relation_forward_fir5_weights = None
- to = matmul.to(dtype = torch.float32); matmul = None
- view = to.view(size, 1, size_1); to = size = size_1 = None
- getitem_5 = kwargs['x']
+ squeeze = einsum_5.squeeze(-1)
+ einsum_5 = None
+ matmul = torch.matmul(squeeze, relation_forward_fir5_weights)
+ squeeze = relation_forward_fir5_weights = None
+ to = matmul.to(dtype=torch.float32)
+ matmul = None
+ view = to.view(size, 1, size_1)
+ to = size = size_1 = None
+ getitem_5 = kwargs["x"]
relation_forward_time_part1_w = self.all_constants.TimePart1
- einsum_6 = torch.functional.einsum('bij,ki->bkj', getitem_5, relation_forward_time_part1_w); getitem_5 = relation_forward_time_part1_w = None
+ einsum_6 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_5, relation_forward_time_part1_w
+ )
+ getitem_5 = relation_forward_time_part1_w = None
size_2 = einsum_6.size(0)
relation_forward_fir2_weights = self.all_parameters.PFir3W
size_3 = relation_forward_fir2_weights.size(1)
- squeeze_1 = einsum_6.squeeze(-1); einsum_6 = None
- matmul_1 = torch.matmul(squeeze_1, relation_forward_fir2_weights); squeeze_1 = relation_forward_fir2_weights = None
- to_1 = matmul_1.to(dtype = torch.float32); matmul_1 = None
- view_1 = to_1.view(size_2, 1, size_3); to_1 = size_2 = size_3 = None
- add_3 = view_1 + view; view_1 = view = None
- getitem_6 = kwargs['x_t']; kwargs = None
+ squeeze_1 = einsum_6.squeeze(-1)
+ einsum_6 = None
+ matmul_1 = torch.matmul(squeeze_1, relation_forward_fir2_weights)
+ squeeze_1 = relation_forward_fir2_weights = None
+ to_1 = matmul_1.to(dtype=torch.float32)
+ matmul_1 = None
+ view_1 = to_1.view(size_2, 1, size_3)
+ to_1 = size_2 = size_3 = None
+ add_3 = view_1 + view
+ view_1 = view = None
+ getitem_6 = kwargs["x_t"]
+ kwargs = None
relation_forward_sample_part8_w = self.all_constants.SamplePart8
- einsum_7 = torch.functional.einsum('bij,ki->bkj', getitem_6, relation_forward_sample_part8_w); getitem_6 = relation_forward_sample_part8_w = None
- return ({'F_PID': add_2, 'x_n': add_3}, {'SamplePart8': einsum_7, 'Add6': add_3}, {'Sub13_int13': add}, {'Sub13_int14': update_state})
-
+ einsum_7 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_6, relation_forward_sample_part8_w
+ )
+ getitem_6 = relation_forward_sample_part8_w = None
+ return (
+ {"F_PID": add_2, "x_n": add_3},
+ {"SamplePart8": einsum_7, "Add6": add_3},
+ {"Sub13_int13": add},
+ {"Sub13_int14": update_state},
+ )
+
+
class RecurrentModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.Cell = TracerModel()
- self.inputs = ['x_m', 'x_t', 'F', 'x', ]
+ self.inputs = [
+ "x_m",
+ "x_t",
+ "F",
+ "x",
+ ]
self.states = dict()
def forward(self, kwargs):
n_samples = min([kwargs[key].size(0) for key in self.inputs])
- self.states['Sub13_int14'] = kwargs['Sub13_int14']
- self.states['Sub13_int13'] = kwargs['Sub13_int13']
- results = {'F_PID':[], 'x_n':[], }
+ self.states["Sub13_int14"] = kwargs["Sub13_int14"]
+ self.states["Sub13_int13"] = kwargs["Sub13_int13"]
+ results = {
+ "F_PID": [],
+ "x_n": [],
+ }
X = dict()
for idx in range(n_samples):
for key in self.inputs:
@@ -113,7 +247,9 @@ def forward(self, kwargs):
results[key].append(out[key])
for key, val in closed_loop.items():
self.states[key] = nnodely_basic_model_timeshift(self.states[key])
- self.states[key] = nnodely_basic_model_update_state(self.states[key], val)
+ self.states[key] = nnodely_basic_model_update_state(
+ self.states[key], val
+ )
for key, val in connect.items():
self.states[key] = nnodely_basic_model_timeshift(val)
return results
diff --git a/case-studies/mass_spring_damper/mass_spring_damper.py b/case-studies/mass_spring_damper/mass_spring_damper.py
index 60122de7..7261091d 100644
--- a/case-studies/mass_spring_damper/mass_spring_damper.py
+++ b/case-studies/mass_spring_damper/mass_spring_damper.py
@@ -12,94 +12,117 @@
T_x = 0.1
# Define the neural model
-x = Input('x') # MSNN input (Mass position)
-F = Input('F') # MSNN input (Force)
-x_free = Fir(W_init = 'init_negexp', W_init_params = {'first_value':0.1,'size_index':0,'lambda':3})(x.tw(T_x))
+x = Input("x") # MSNN input (Mass position)
+F = Input("F") # MSNN input (Force)
+x_free = Fir(
+ W_init="init_negexp",
+ W_init_params={"first_value": 0.1, "size_index": 0, "lambda": 3},
+)(x.tw(T_x))
x_force = Fir(F.tw(T_x))
-x_n = Output('x_n', x_free + x_force)
+x_n = Output("x_n", x_free + x_force)
# Add the neural models to the nnodely structure
-msd = Modely(seed = 42, workspace = 'saved')
-msd.addModel('neural_msd', x_n)
+msd = Modely(seed=42, workspace="saved")
+msd.addModel("neural_msd", x_n)
# These functions are used to impose the minimization objectives.
# Here it is minimized the error between the future position of x get from the dataset
# and the estimator designed using the neural network.
# The minimization is imposed via MSE error.
-x_t = Input('x_t') # Real position
-msd.addMinimize('x[t]', x_t.next(), x_n)
+x_t = Input("x_t") # Real position
+msd.addMinimize("x[t]", x_t.next(), x_n)
# Nauralize the model and getting the neural network.
# The sampling time depends on the datasets.
msd.neuralizeModel(0.01)
# Data load carica i file CSV e costruisce automaticamente il dataset compatibile con la struttura della rete.
-data_struct = ['time', ('x','x_t'), '', 'F']
-msd.loadData(name = 'simulations',
- source = 'msd-data/data',
- format = data_struct, delimiter = ';')
+data_struct = ["time", ("x", "x_t"), "", "F"]
+msd.loadData(
+ name="simulations", source="msd-data/data", format=data_struct, delimiter=";"
+)
# Neural network train
-default_par = {'num_of_epochs' : 80,
- 'train_batch_size' : 128,
- 'lr' : 0.0005,
- 'splits' : [70,20,10]}
-msd.trainModel(training_params = default_par)
+default_par = {
+ "num_of_epochs": 80,
+ "train_batch_size": 128,
+ "lr": 0.0005,
+ "splits": [70, 20, 10],
+}
+msd.trainModel(training_params=default_par)
# Save the neural model in json format
-msd.saveModel(name = 'msd_preliminary')
+msd.saveModel(name="msd_preliminary")
# Show the network performance on the test dataset
-msd.analyzeModel(splits = [70,20,10])
+msd.analyzeModel(splits=[70, 20, 10])
vis = MPLVisualizer()
vis.setModely(msd)
vis.showResult("simulations_test")
# Refine weights with recurrent train and analyze the model showing the performance (mse, FVU, AIC).
-msd.trainAndAnalyze(num_of_epochs = 10, prediction_samples = 1500, step = 500, lr=0.00001,
- closed_loop={'x':'x_n'}, training_params = default_par)
+msd.trainAndAnalyze(
+ num_of_epochs=10,
+ prediction_samples=1500,
+ step=500,
+ lr=0.00001,
+ closed_loop={"x": "x_n"},
+ training_params=default_par,
+)
# Show the network performance on the test dataset on recurrent
vis.showResult("simulations_test")
# Save the neural model in json format
-msd.saveModel(name = 'msd_final')
+msd.saveModel(name="msd_final")
# Definition of the PID controller network
-x_m = Input('x_m') # measured position
-kp = Parameter('P', values=0.5)
-ki = Parameter('I', values=0.5)
-kd = Parameter('D', values=0.5)
-e = x_t.next()-x_m.next()
-c = e*kp+Integrate(e)*ki+Differentiate(e)*kd
-controlForce = Output('F_PID', c)
-msd.addModel('PID',controlForce)
+x_m = Input("x_m") # measured position
+kp = Parameter("P", values=0.5)
+ki = Parameter("I", values=0.5)
+kd = Parameter("D", values=0.5)
+e = x_t.next() - x_m.next()
+c = e * kp + Integrate(e) * ki + Differentiate(e) * kd
+controlForce = Output("F_PID", c)
+msd.addModel("PID", controlForce)
# Neuralization of the whole models
msd.neuralizeModel()
# Train the PID controller
-msd.trainModel(models = 'PID',
- closed_loop = {'x' : 'x_n','x_m' : 'x_n'},
- connect = {'F' : 'F_PID'},
- prediction_samples = 500,
- step = 500, num_of_epochs = 20,
- lr = 0.05,
- training_params = default_par)
+msd.trainModel(
+ models="PID",
+ closed_loop={"x": "x_n", "x_m": "x_n"},
+ connect={"F": "F_PID"},
+ prediction_samples=500,
+ step=500,
+ num_of_epochs=20,
+ lr=0.05,
+ training_params=default_par,
+)
# Print the parameter of the PID
-print(f"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}")
+print(
+ f"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}"
+)
# Test the controller on step and triangular signal
import numpy as np
-tt = np.linspace(0, 5, int(5/0.01), endpoint=False)
-data_target = np.concatenate([np.ones(511,dtype=np.float32)*1.0, # Step
- -2*np.ones(500,dtype=np.float32)*1.0,
- 2 * np.abs( 2 * (tt*1/5 - np.floor(tt*1/5 + 0.5)) ) - 1,
- 2 * np.abs( 2 * (tt*1/2.5 - np.floor(tt*1/2.5 + 0.5)) ) - 1])
-msd.loadData('test_control', {'x_t': data_target})
-msd.analyzeModel('test_control',
- prediction_samples = 2000,
- closed_loop = {'x' : 'x_n','x_m' : 'x_n'},
- connect = {'F' : 'F_PID'},
- batch_size = 1)
-vis.showResult('test_control')
\ No newline at end of file
+
+tt = np.linspace(0, 5, int(5 / 0.01), endpoint=False)
+data_target = np.concatenate(
+ [
+ np.ones(511, dtype=np.float32) * 1.0, # Step
+ -2 * np.ones(500, dtype=np.float32) * 1.0,
+ 2 * np.abs(2 * (tt * 1 / 5 - np.floor(tt * 1 / 5 + 0.5))) - 1,
+ 2 * np.abs(2 * (tt * 1 / 2.5 - np.floor(tt * 1 / 2.5 + 0.5))) - 1,
+ ]
+)
+msd.loadData("test_control", {"x_t": data_target})
+msd.analyzeModel(
+ "test_control",
+ prediction_samples=2000,
+ closed_loop={"x": "x_n", "x_m": "x_n"},
+ connect={"F": "F_PID"},
+ batch_size=1,
+)
+vis.showResult("test_control")
diff --git a/case-studies/mass_spring_damper/msd-data/stats.txt b/case-studies/mass_spring_damper/msd-data/stats.txt
index 765759d4..c13bd6c6 100644
--- a/case-studies/mass_spring_damper/msd-data/stats.txt
+++ b/case-studies/mass_spring_damper/msd-data/stats.txt
@@ -1 +1 @@
-{"model_attributes":["linear"],"num_simulations":100,"elapsed_time":5.742344375,"total_samples":200100,"params":{"time":20,"sampling_time":0.001,"data_sampling_time":0.01,"m":1,"k":3,"c":0.175,"a1":3,"a2":2,"d":0.5,"s":1,"x0":[0,1],"v0":[0,0.5],"force":[-3,3]}}
\ No newline at end of file
+{"model_attributes":["linear"],"num_simulations":100,"elapsed_time":5.742344375,"total_samples":200100,"params":{"time":20,"sampling_time":0.001,"data_sampling_time":0.01,"m":1,"k":3,"c":0.175,"a1":3,"a2":2,"d":0.5,"s":1,"x0":[0,1],"v0":[0,0.5],"force":[-3,3]}}
diff --git a/case-studies/mass_spring_damper/saved/msd_final.json b/case-studies/mass_spring_damper/saved/msd_final.json
index df315ad3..602f2f30 100644
--- a/case-studies/mass_spring_damper/saved/msd_final.json
+++ b/case-studies/mass_spring_damper/saved/msd_final.json
@@ -44,4 +44,4 @@
"Fir5": ["Fir", ["TimePart4"], "PFir5W", null, 0],
"SamplePart8": ["SamplePart", ["x_t"], -1, [0, 1]],
"TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]],
- "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
\ No newline at end of file
+ "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
diff --git a/case-studies/mass_spring_damper/saved/msd_preliminary.json b/case-studies/mass_spring_damper/saved/msd_preliminary.json
index 9b325312..afd36bae 100644
--- a/case-studies/mass_spring_damper/saved/msd_preliminary.json
+++ b/case-studies/mass_spring_damper/saved/msd_preliminary.json
@@ -44,4 +44,4 @@
"Fir5": ["Fir", ["TimePart4"], "PFir5W", null, 0],
"SamplePart8": ["SamplePart", ["x_t"], -1, [0, 1]],
"TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]],
- "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
\ No newline at end of file
+ "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}}
diff --git a/case-studies/neuralODE/neuralODE_msd.ipynb b/case-studies/neuralODE/neuralODE_msd.ipynb
index 3743ea1b..a9ede6aa 100644
--- a/case-studies/neuralODE/neuralODE_msd.ipynb
+++ b/case-studies/neuralODE/neuralODE_msd.ipynb
@@ -12,6 +12,7 @@
},
{
"cell_type": "code",
+ "execution_count": 1,
"id": "67c44ae2",
"metadata": {
"ExecuteTime": {
@@ -19,20 +20,6 @@
"start_time": "2026-02-27T15:24:10.484610Z"
}
},
- "source": [
- "import os\n",
- "from nnodely import *\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nnodely.support import earlystopping\n",
- "from nnodely.support.odeint.adjoint import odeint_adjoint\n",
- "\n",
- "def init_random_range(indexes, params_size, dict_param={'min_value': 0.0, 'max_value': 1.0}):\n",
- " import numpy as np\n",
- " min_val = dict_param.get('min_value', 0.0)\n",
- " max_val = dict_param.get('max_value', 1.0)\n",
- " return np.random.uniform(low=min_val, high=max_val)"
- ],
"outputs": [
{
"name": "stdout",
@@ -42,7 +29,21 @@
]
}
],
- "execution_count": 1
+ "source": [
+ "from nnodely import *\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "\n",
+ "def init_random_range(\n",
+ " indexes, params_size, dict_param={\"min_value\": 0.0, \"max_value\": 1.0}\n",
+ "):\n",
+ " import numpy as np\n",
+ "\n",
+ " min_val = dict_param.get(\"min_value\", 0.0)\n",
+ " max_val = dict_param.get(\"max_value\", 1.0)\n",
+ " return np.random.uniform(low=min_val, high=max_val)"
+ ]
},
{
"cell_type": "markdown",
@@ -58,6 +59,7 @@
},
{
"cell_type": "code",
+ "execution_count": 2,
"id": "dcc7da2c",
"metadata": {
"ExecuteTime": {
@@ -65,6 +67,7 @@
"start_time": "2026-02-27T15:24:11.102685Z"
}
},
+ "outputs": [],
"source": [
"def ode_func_torch(t, state, weight_fir):\n",
" import torch\n",
@@ -98,9 +101,7 @@
" out = torch.cat((state[:, 1:, :], out), dim=1)\n",
"\n",
" return out"
- ],
- "outputs": [],
- "execution_count": 2
+ ]
},
{
"cell_type": "markdown",
@@ -117,6 +118,7 @@
},
{
"cell_type": "code",
+ "execution_count": 3,
"id": "616f6513",
"metadata": {
"ExecuteTime": {
@@ -124,38 +126,14 @@
"start_time": "2026-02-27T15:24:11.275795Z"
}
},
- "source": [
- "model_name = 'neuralODE_msd'\n",
- "msd = Modely(seed=42, workspace='saved')\n",
- "\n",
- "x = Input('x') # MSNN input (Mass position)\n",
- "F = Input('F') # MSNN input (Force)\n",
- "T_x = 0.1\n",
- "init_value = 0.1\n",
- "weight_fir = Parameter('weight_fir', dimensions=(2, 1, 10), init=init_random_range, init_params={'min_value': -init_value, 'max_value': init_value})\n",
- "paramFun = ParamFun(ode_func_torch)\n",
- "neuOde = NeuralODE(func=paramFun, dt=0.01, rtol=1e-7, atol=1e-9, method='dopri5')\n",
- "state = Concatenate(x.tw(T_x), F.tw(T_x))\n",
- "ans = neuOde(state, weight_fir)\n",
- "x_est = TimePart(Select(ans, 0), 0.09, 0.1)\n",
- "x_est.closedLoop(x)\n",
- "\n",
- "# Output and loss\n",
- "x_n = Output('x_n', x_est)\n",
- "\n",
- "x_t = Input('x_t')\n",
- "msd.addModel('msd', [x_n])\n",
- "msd.addMinimize('x[t]', x_t.next(), x_n, loss_function='mse')\n",
- "msd.neuralizeModel(sample_time = 0.01)"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n",
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {},\n",
+ "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {},\n",
" 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n",
" 'code': 'def FNeuralODE5(state, *weights):\\n'\n",
" ' from '\n",
@@ -288,12 +266,40 @@
" 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n",
" 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n",
" 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n",
- " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n"
+ " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n"
]
}
],
- "execution_count": 3
+ "source": [
+ "model_name = \"neuralODE_msd\"\n",
+ "msd = Modely(seed=42, workspace=\"saved\")\n",
+ "\n",
+ "x = Input(\"x\") # MSNN input (Mass position)\n",
+ "F = Input(\"F\") # MSNN input (Force)\n",
+ "T_x = 0.1\n",
+ "init_value = 0.1\n",
+ "weight_fir = Parameter(\n",
+ " \"weight_fir\",\n",
+ " dimensions=(2, 1, 10),\n",
+ " init=init_random_range,\n",
+ " init_params={\"min_value\": -init_value, \"max_value\": init_value},\n",
+ ")\n",
+ "paramFun = ParamFun(ode_func_torch)\n",
+ "neuOde = NeuralODE(func=paramFun, dt=0.01, rtol=1e-7, atol=1e-9, method=\"dopri5\")\n",
+ "state = Concatenate(x.tw(T_x), F.tw(T_x))\n",
+ "ans = neuOde(state, weight_fir)\n",
+ "x_est = TimePart(Select(ans, 0), 0.09, 0.1)\n",
+ "x_est.closedLoop(x)\n",
+ "\n",
+ "# Output and loss\n",
+ "x_n = Output(\"x_n\", x_est)\n",
+ "\n",
+ "x_t = Input(\"x_t\")\n",
+ "msd.addModel(\"msd\", [x_n])\n",
+ "msd.addMinimize(\"x[t]\", x_t.next(), x_n, loss_function=\"mse\")\n",
+ "msd.neuralizeModel(sample_time=0.01)"
+ ]
},
{
"cell_type": "markdown",
@@ -310,6 +316,7 @@
},
{
"cell_type": "code",
+ "execution_count": 4,
"id": "f0f3f7be",
"metadata": {
"ExecuteTime": {
@@ -317,29 +324,26 @@
"start_time": "2026-02-27T15:24:13.191582Z"
}
},
- "source": [
- "data_struct = ['time', ('x_t', 'x'), '', 'F']\n",
- "msd.loadData(name = 'simulations',\n",
- " source = 'data',\n",
- " format = data_struct, delimiter = ';')"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[1;32m============================ nnodely Model Dataset =============================\u001B[0m\n",
- "\u001B[32mDataset Name: simulations\u001B[0m\n",
- "\u001B[32mNumber of files: 100\u001B[0m\n",
- "\u001B[32mTotal number of samples: 199100\u001B[0m\n",
- "\u001B[32mShape of x: (199100, 10, 1)\u001B[0m\n",
- "\u001B[32mShape of F: (199100, 10, 1)\u001B[0m\n",
- "\u001B[32mShape of x_t: (199100, 1, 1)\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n"
+ "\u001b[1;32m============================ nnodely Model Dataset =============================\u001b[0m\n",
+ "\u001b[32mDataset Name: simulations\u001b[0m\n",
+ "\u001b[32mNumber of files: 100\u001b[0m\n",
+ "\u001b[32mTotal number of samples: 199100\u001b[0m\n",
+ "\u001b[32mShape of x: (199100, 10, 1)\u001b[0m\n",
+ "\u001b[32mShape of F: (199100, 10, 1)\u001b[0m\n",
+ "\u001b[32mShape of x_t: (199100, 1, 1)\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n"
]
}
],
- "execution_count": 4
+ "source": [
+ "data_struct = [\"time\", (\"x_t\", \"x\"), \"\", \"F\"]\n",
+ "msd.loadData(name=\"simulations\", source=\"data\", format=data_struct, delimiter=\";\")"
+ ]
},
{
"cell_type": "markdown",
@@ -354,6 +358,7 @@
},
{
"cell_type": "code",
+ "execution_count": 6,
"id": "7fab61e7",
"metadata": {
"ExecuteTime": {
@@ -361,36 +366,23 @@
"start_time": "2026-02-27T15:24:39.965159Z"
}
},
- "source": [
- "msd.exportPythonModel(name = model_name)\n",
- "msd.importPythonModel(name = model_name)\n",
- "msd.neuralizeModel(sample_time=0.01)\n",
- "param_model = {'num_of_epochs' : 20,\n",
- " 'train_batch_size' : 128,\n",
- " 'lr' : 1e-3,\n",
- " 'splits' : [70, 20, 10]}\n",
- "msd.trainModel(training_params = param_model, minimize_gain=None)\n",
- "\n",
- "# Save the neural model\n",
- "msd.exportPythonModel(name = model_name)"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[1;32m=============================== Save JSON Model ================================\u001B[0m\n",
- "\u001B[32mModel saved in: saved/neuralODE_msd.json\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m========================== Export Python Torch Model ===========================\u001B[0m\n",
- "\u001B[32mModel exported in: saved/neuralODE_msd.py\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m=============================== Load JSON Model ================================\u001B[0m\n",
- "\u001B[32mModel loaded from: saved/neuralODE_msd.json\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n",
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {},\n",
+ "\u001b[1;32m=============================== Save JSON Model ================================\u001b[0m\n",
+ "\u001b[32mModel saved in: saved/neuralODE_msd.json\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m========================== Export Python Torch Model ===========================\u001b[0m\n",
+ "\u001b[32mModel exported in: saved/neuralODE_msd.py\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m=============================== Load JSON Model ================================\u001b[0m\n",
+ "\u001b[32mModel loaded from: saved/neuralODE_msd.json\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {},\n",
" 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n",
" 'code': 'def FNeuralODE5(state, *weights):\\n'\n",
" ' from '\n",
@@ -523,14 +515,14 @@
" 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n",
" 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n",
" 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n",
- " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m========================== Import Python Torch Model ===========================\u001B[0m\n",
- "\u001B[32mModel imported from: saved/neuralODE_msd.py\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n",
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {},\n",
+ " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m========================== Import Python Torch Model ===========================\u001b[0m\n",
+ "\u001b[32mModel imported from: saved/neuralODE_msd.py\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {},\n",
" 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n",
" 'code': 'def FNeuralODE5(state, *weights):\\n'\n",
" ' from '\n",
@@ -663,76 +655,90 @@
" 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n",
" 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n",
" 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n",
- " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m======================== nnodely Model Train Parameters ========================\u001B[0m\n",
- "\u001B[32mmodels: ['msd']\u001B[0m\n",
- "\u001B[32mnum of epochs: 20\u001B[0m\n",
- "\u001B[32mupdate per epochs: 1088\u001B[0m\n",
- "\u001B[34mâ””>len(train_indexes)//(batch_size+step)\u001B[0m\n",
- "\u001B[32mshuffle data: True\u001B[0m\n",
- "\u001B[32mprediction samples: 0\u001B[0m\n",
- "\u001B[32mstep: 0\u001B[0m\n",
- "\u001B[32mclosed loop: {}\u001B[0m\n",
- "\u001B[32mconnect: {}\u001B[0m\n",
- "\u001B[32mtrain dataset: simulations_train\u001B[0m\n",
- "\u001B[32m\t- batch size: 128\u001B[0m\n",
- "\u001B[32m\t- num of samples: 139370\u001B[0m\n",
- "\u001B[32m\t- num of first samples: 139370\u001B[0m\n",
- "\u001B[32mvalidation dataset: simulations_val\u001B[0m\n",
- "\u001B[32m\t- batch size: 128\u001B[0m\n",
- "\u001B[32m\t- num of samples: 39820\u001B[0m\n",
- "\u001B[32m\t- num of first samples: 39820\u001B[0m\n",
- "\u001B[32mtest dataset: simulations_test\u001B[0m\n",
- "\u001B[32m\t- num of samples: 19910\u001B[0m\n",
- "\u001B[32m\t- num of first samples: 19910\u001B[0m\n",
- "\u001B[32mminimizers: {'x[t]': {'A': 'SamplePart10',\n",
+ " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m======================== nnodely Model Train Parameters ========================\u001b[0m\n",
+ "\u001b[32mmodels: ['msd']\u001b[0m\n",
+ "\u001b[32mnum of epochs: 20\u001b[0m\n",
+ "\u001b[32mupdate per epochs: 1088\u001b[0m\n",
+ "\u001b[34mâ””>len(train_indexes)//(batch_size+step)\u001b[0m\n",
+ "\u001b[32mshuffle data: True\u001b[0m\n",
+ "\u001b[32mprediction samples: 0\u001b[0m\n",
+ "\u001b[32mstep: 0\u001b[0m\n",
+ "\u001b[32mclosed loop: {}\u001b[0m\n",
+ "\u001b[32mconnect: {}\u001b[0m\n",
+ "\u001b[32mtrain dataset: simulations_train\u001b[0m\n",
+ "\u001b[32m\t- batch size: 128\u001b[0m\n",
+ "\u001b[32m\t- num of samples: 139370\u001b[0m\n",
+ "\u001b[32m\t- num of first samples: 139370\u001b[0m\n",
+ "\u001b[32mvalidation dataset: simulations_val\u001b[0m\n",
+ "\u001b[32m\t- batch size: 128\u001b[0m\n",
+ "\u001b[32m\t- num of samples: 39820\u001b[0m\n",
+ "\u001b[32m\t- num of first samples: 39820\u001b[0m\n",
+ "\u001b[32mtest dataset: simulations_test\u001b[0m\n",
+ "\u001b[32m\t- num of samples: 19910\u001b[0m\n",
+ "\u001b[32m\t- num of first samples: 19910\u001b[0m\n",
+ "\u001b[32mminimizers: {'x[t]': {'A': 'SamplePart10',\n",
" 'B': 'TimePart8',\n",
- " 'loss': 'mse'}}\u001B[0m\n",
- "\u001B[32moptimizer: Adam\u001B[0m\n",
- "\u001B[32moptimizer defaults: {'lr': 0.001}\u001B[0m\n",
- "\u001B[32moptimizer params: [{'params': 'weight_fir'}]\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m================= nnodely Training =================\u001B[0m\n",
- "\u001B[32m| Epoch |\u001B[0m\u001B[32m x[t] |\u001B[0m\u001B[32m Total |\u001B[0m\n",
- "\u001B[32m| |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\n",
- "\u001B[32m| |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m val |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m val |\u001B[0m\n",
- "\u001B[32m|--------------------------------------------------|\u001B[0m\n",
- "\u001B[32m| 1/20 |\u001B[0m\u001B[32m2.946e-05|\u001B[0m\u001B[32m3.003e-05|\u001B[0m\u001B[32m2.946e-05|\u001B[0m\u001B[32m3.003e-05|\u001B[0m\n",
- "\u001B[32m| 2/20 |\u001B[0m\u001B[32m2.333e-05|\u001B[0m\u001B[32m2.359e-05|\u001B[0m\u001B[32m2.333e-05|\u001B[0m\u001B[32m2.359e-05|\u001B[0m\n",
- "\u001B[32m| 3/20 |\u001B[0m\u001B[32m1.811e-05|\u001B[0m\u001B[32m1.807e-05|\u001B[0m\u001B[32m1.811e-05|\u001B[0m\u001B[32m1.807e-05|\u001B[0m\n",
- "\u001B[32m| 4/20 |\u001B[0m\u001B[32m1.361e-05|\u001B[0m\u001B[32m1.329e-05|\u001B[0m\u001B[32m1.361e-05|\u001B[0m\u001B[32m1.329e-05|\u001B[0m\n",
- "\u001B[32m| 5/20 |\u001B[0m\u001B[32m9.822e-06|\u001B[0m\u001B[32m9.341e-06|\u001B[0m\u001B[32m9.822e-06|\u001B[0m\u001B[32m9.341e-06|\u001B[0m\n",
- "\u001B[32m| 6/20 |\u001B[0m\u001B[32m6.726e-06|\u001B[0m\u001B[32m6.203e-06|\u001B[0m\u001B[32m6.726e-06|\u001B[0m\u001B[32m6.203e-06|\u001B[0m\n",
- "\u001B[32m| 7/20 |\u001B[0m\u001B[32m4.319e-06|\u001B[0m\u001B[32m3.798e-06|\u001B[0m\u001B[32m4.319e-06|\u001B[0m\u001B[32m3.798e-06|\u001B[0m\n",
- "\u001B[32m| 8/20 |\u001B[0m\u001B[32m2.513e-06|\u001B[0m\u001B[32m2.064e-06|\u001B[0m\u001B[32m2.513e-06|\u001B[0m\u001B[32m2.064e-06|\u001B[0m\n",
- "\u001B[32m| 9/20 |\u001B[0m\u001B[32m1.273e-06|\u001B[0m\u001B[32m9.422e-07|\u001B[0m\u001B[32m1.273e-06|\u001B[0m\u001B[32m9.422e-07|\u001B[0m\n",
- "\u001B[32m| 10/20 |\u001B[0m\u001B[32m5.204e-07|\u001B[0m\u001B[32m3.270e-07|\u001B[0m\u001B[32m5.204e-07|\u001B[0m\u001B[32m3.270e-07|\u001B[0m\n",
- "\u001B[32m| 11/20 |\u001B[0m\u001B[32m1.508e-07|\u001B[0m\u001B[32m6.744e-08|\u001B[0m\u001B[32m1.508e-07|\u001B[0m\u001B[32m6.744e-08|\u001B[0m\n",
- "\u001B[32m| 12/20 |\u001B[0m\u001B[32m2.458e-08|\u001B[0m\u001B[32m5.847e-09|\u001B[0m\u001B[32m2.458e-08|\u001B[0m\u001B[32m5.847e-09|\u001B[0m\n",
- "\u001B[32m| 13/20 |\u001B[0m\u001B[32m1.724e-09|\u001B[0m\u001B[32m1.689e-10|\u001B[0m\u001B[32m1.724e-09|\u001B[0m\u001B[32m1.689e-10|\u001B[0m\n",
- "\u001B[32m| 14/20 |\u001B[0m\u001B[32m1.613e-10|\u001B[0m\u001B[32m 2.83e-11|\u001B[0m\u001B[32m1.613e-10|\u001B[0m\u001B[32m 2.83e-11|\u001B[0m\n",
- "\u001B[32m| 15/20 |\u001B[0m\u001B[32m4.815e-11|\u001B[0m\u001B[32m6.983e-12|\u001B[0m\u001B[32m4.815e-11|\u001B[0m\u001B[32m6.983e-12|\u001B[0m\n",
- "\u001B[32m| 16/20 |\u001B[0m\u001B[32m1.415e-11|\u001B[0m\u001B[32m1.566e-12|\u001B[0m\u001B[32m1.415e-11|\u001B[0m\u001B[32m1.566e-12|\u001B[0m\n",
- "\u001B[32m| 17/20 |\u001B[0m\u001B[32m1.537e-11|\u001B[0m\u001B[32m2.816e-12|\u001B[0m\u001B[32m1.537e-11|\u001B[0m\u001B[32m2.816e-12|\u001B[0m\n",
- "\u001B[32m| 18/20 |\u001B[0m\u001B[32m2.932e-11|\u001B[0m\u001B[32m3.224e-12|\u001B[0m\u001B[32m2.932e-11|\u001B[0m\u001B[32m3.224e-12|\u001B[0m\n",
- "\u001B[32m| 19/20 |\u001B[0m\u001B[32m2.718e-11|\u001B[0m\u001B[32m7.241e-11|\u001B[0m\u001B[32m2.718e-11|\u001B[0m\u001B[32m7.241e-11|\u001B[0m\n",
- "\u001B[32m| 20/20 |\u001B[0m\u001B[32m2.717e-11|\u001B[0m\u001B[32m1.739e-12|\u001B[0m\u001B[32m2.717e-11|\u001B[0m\u001B[32m1.739e-12|\u001B[0m\n",
- "\u001B[32m|--------------------------------------------------|\u001B[0m\n",
- "\u001B[1;32m============================ nnodely Training Time =============================\u001B[0m\n",
- "\u001B[32mTotal time of Training: 685.4026439189911\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[34mThe selected model is the LAST model of the training.\u001B[0m\n",
- "\u001B[1;32m=============================== Save JSON Model ================================\u001B[0m\n",
- "\u001B[32mModel saved in: saved/neuralODE_msd.json\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m========================== Export Python Torch Model ===========================\u001B[0m\n",
- "\u001B[32mModel exported in: saved/neuralODE_msd.py\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n"
+ " 'loss': 'mse'}}\u001b[0m\n",
+ "\u001b[32moptimizer: Adam\u001b[0m\n",
+ "\u001b[32moptimizer defaults: {'lr': 0.001}\u001b[0m\n",
+ "\u001b[32moptimizer params: [{'params': 'weight_fir'}]\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m================= nnodely Training =================\u001b[0m\n",
+ "\u001b[32m| Epoch |\u001b[0m\u001b[32m x[t] |\u001b[0m\u001b[32m Total |\u001b[0m\n",
+ "\u001b[32m| |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\n",
+ "\u001b[32m| |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m val |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m val |\u001b[0m\n",
+ "\u001b[32m|--------------------------------------------------|\u001b[0m\n",
+ "\u001b[32m| 1/20 |\u001b[0m\u001b[32m2.946e-05|\u001b[0m\u001b[32m3.003e-05|\u001b[0m\u001b[32m2.946e-05|\u001b[0m\u001b[32m3.003e-05|\u001b[0m\n",
+ "\u001b[32m| 2/20 |\u001b[0m\u001b[32m2.333e-05|\u001b[0m\u001b[32m2.359e-05|\u001b[0m\u001b[32m2.333e-05|\u001b[0m\u001b[32m2.359e-05|\u001b[0m\n",
+ "\u001b[32m| 3/20 |\u001b[0m\u001b[32m1.811e-05|\u001b[0m\u001b[32m1.807e-05|\u001b[0m\u001b[32m1.811e-05|\u001b[0m\u001b[32m1.807e-05|\u001b[0m\n",
+ "\u001b[32m| 4/20 |\u001b[0m\u001b[32m1.361e-05|\u001b[0m\u001b[32m1.329e-05|\u001b[0m\u001b[32m1.361e-05|\u001b[0m\u001b[32m1.329e-05|\u001b[0m\n",
+ "\u001b[32m| 5/20 |\u001b[0m\u001b[32m9.822e-06|\u001b[0m\u001b[32m9.341e-06|\u001b[0m\u001b[32m9.822e-06|\u001b[0m\u001b[32m9.341e-06|\u001b[0m\n",
+ "\u001b[32m| 6/20 |\u001b[0m\u001b[32m6.726e-06|\u001b[0m\u001b[32m6.203e-06|\u001b[0m\u001b[32m6.726e-06|\u001b[0m\u001b[32m6.203e-06|\u001b[0m\n",
+ "\u001b[32m| 7/20 |\u001b[0m\u001b[32m4.319e-06|\u001b[0m\u001b[32m3.798e-06|\u001b[0m\u001b[32m4.319e-06|\u001b[0m\u001b[32m3.798e-06|\u001b[0m\n",
+ "\u001b[32m| 8/20 |\u001b[0m\u001b[32m2.513e-06|\u001b[0m\u001b[32m2.064e-06|\u001b[0m\u001b[32m2.513e-06|\u001b[0m\u001b[32m2.064e-06|\u001b[0m\n",
+ "\u001b[32m| 9/20 |\u001b[0m\u001b[32m1.273e-06|\u001b[0m\u001b[32m9.422e-07|\u001b[0m\u001b[32m1.273e-06|\u001b[0m\u001b[32m9.422e-07|\u001b[0m\n",
+ "\u001b[32m| 10/20 |\u001b[0m\u001b[32m5.204e-07|\u001b[0m\u001b[32m3.270e-07|\u001b[0m\u001b[32m5.204e-07|\u001b[0m\u001b[32m3.270e-07|\u001b[0m\n",
+ "\u001b[32m| 11/20 |\u001b[0m\u001b[32m1.508e-07|\u001b[0m\u001b[32m6.744e-08|\u001b[0m\u001b[32m1.508e-07|\u001b[0m\u001b[32m6.744e-08|\u001b[0m\n",
+ "\u001b[32m| 12/20 |\u001b[0m\u001b[32m2.458e-08|\u001b[0m\u001b[32m5.847e-09|\u001b[0m\u001b[32m2.458e-08|\u001b[0m\u001b[32m5.847e-09|\u001b[0m\n",
+ "\u001b[32m| 13/20 |\u001b[0m\u001b[32m1.724e-09|\u001b[0m\u001b[32m1.689e-10|\u001b[0m\u001b[32m1.724e-09|\u001b[0m\u001b[32m1.689e-10|\u001b[0m\n",
+ "\u001b[32m| 14/20 |\u001b[0m\u001b[32m1.613e-10|\u001b[0m\u001b[32m 2.83e-11|\u001b[0m\u001b[32m1.613e-10|\u001b[0m\u001b[32m 2.83e-11|\u001b[0m\n",
+ "\u001b[32m| 15/20 |\u001b[0m\u001b[32m4.815e-11|\u001b[0m\u001b[32m6.983e-12|\u001b[0m\u001b[32m4.815e-11|\u001b[0m\u001b[32m6.983e-12|\u001b[0m\n",
+ "\u001b[32m| 16/20 |\u001b[0m\u001b[32m1.415e-11|\u001b[0m\u001b[32m1.566e-12|\u001b[0m\u001b[32m1.415e-11|\u001b[0m\u001b[32m1.566e-12|\u001b[0m\n",
+ "\u001b[32m| 17/20 |\u001b[0m\u001b[32m1.537e-11|\u001b[0m\u001b[32m2.816e-12|\u001b[0m\u001b[32m1.537e-11|\u001b[0m\u001b[32m2.816e-12|\u001b[0m\n",
+ "\u001b[32m| 18/20 |\u001b[0m\u001b[32m2.932e-11|\u001b[0m\u001b[32m3.224e-12|\u001b[0m\u001b[32m2.932e-11|\u001b[0m\u001b[32m3.224e-12|\u001b[0m\n",
+ "\u001b[32m| 19/20 |\u001b[0m\u001b[32m2.718e-11|\u001b[0m\u001b[32m7.241e-11|\u001b[0m\u001b[32m2.718e-11|\u001b[0m\u001b[32m7.241e-11|\u001b[0m\n",
+ "\u001b[32m| 20/20 |\u001b[0m\u001b[32m2.717e-11|\u001b[0m\u001b[32m1.739e-12|\u001b[0m\u001b[32m2.717e-11|\u001b[0m\u001b[32m1.739e-12|\u001b[0m\n",
+ "\u001b[32m|--------------------------------------------------|\u001b[0m\n",
+ "\u001b[1;32m============================ nnodely Training Time =============================\u001b[0m\n",
+ "\u001b[32mTotal time of Training: 685.4026439189911\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[34mThe selected model is the LAST model of the training.\u001b[0m\n",
+ "\u001b[1;32m=============================== Save JSON Model ================================\u001b[0m\n",
+ "\u001b[32mModel saved in: saved/neuralODE_msd.json\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m========================== Export Python Torch Model ===========================\u001b[0m\n",
+ "\u001b[32mModel exported in: saved/neuralODE_msd.py\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n"
]
}
],
- "execution_count": 6
+ "source": [
+ "msd.exportPythonModel(name=model_name)\n",
+ "msd.importPythonModel(name=model_name)\n",
+ "msd.neuralizeModel(sample_time=0.01)\n",
+ "param_model = {\n",
+ " \"num_of_epochs\": 20,\n",
+ " \"train_batch_size\": 128,\n",
+ " \"lr\": 1e-3,\n",
+ " \"splits\": [70, 20, 10],\n",
+ "}\n",
+ "msd.trainModel(training_params=param_model, minimize_gain=None)\n",
+ "\n",
+ "# Save the neural model\n",
+ "msd.exportPythonModel(name=model_name)"
+ ]
},
{
"cell_type": "markdown",
@@ -744,6 +750,7 @@
},
{
"cell_type": "code",
+ "execution_count": 7,
"id": "5dd1d858",
"metadata": {
"ExecuteTime": {
@@ -751,47 +758,17 @@
"start_time": "2026-02-27T15:39:15.286428Z"
}
},
- "source": [
- "# Recursive plot using getSamples\n",
- "msd.importPythonModel(name = model_name)\n",
- "msd.neuralizeModel(sample_time=0.01)\n",
- "samples = msd.getSamples(dataset='simulations', window=2000-10, index=0)\n",
- "result = msd(samples, sampled=True, prediction_samples=2000-10)\n",
- "\n",
- "#region Plotting\n",
- "t = np.arange(len(result['x_n'])) * 0.01\n",
- "plt.figure(figsize=(12, 7))\n",
- "plt.subplot(2, 1, 1)\n",
- "plt.plot(t, result['x_n'], label=r'Estimated $x$', linewidth=2)\n",
- "plt.plot(t, np.array(samples['x_t']).squeeze(-1).squeeze(-1), label=r'True $x$', linestyle='--', linewidth=2)\n",
- "plt.title('Position estimation')\n",
- "plt.xlabel('Time [s]')\n",
- "plt.ylabel(r'$x$ [m]')\n",
- "plt.grid()\n",
- "plt.legend()\n",
- "plt.subplot(2, 1, 2)\n",
- "plt.plot(t, np.array(samples['F'])[:, 0, 0], label=r'Applied force $F$')\n",
- "plt.title('Applied force')\n",
- "plt.xlabel('Time [s]')\n",
- "plt.ylabel('Force [N]')\n",
- "plt.grid()\n",
- "plt.legend()\n",
- "plt.tight_layout()\n",
- "#endregion\n",
- "\n",
- "plt.show()"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[1;32m=============================== Load JSON Model ================================\u001B[0m\n",
- "\u001B[32mModel loaded from: saved/neuralODE_msd.json\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n",
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {},\n",
+ "\u001b[1;32m=============================== Load JSON Model ================================\u001b[0m\n",
+ "\u001b[32mModel loaded from: saved/neuralODE_msd.json\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {},\n",
" 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n",
" 'code': 'def FNeuralODE5(state, *weights):\\n'\n",
" ' from '\n",
@@ -924,14 +901,14 @@
" 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n",
" 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n",
" 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n",
- " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m========================== Import Python Torch Model ===========================\u001B[0m\n",
- "\u001B[32mModel imported from: saved/neuralODE_msd.py\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n",
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {},\n",
+ " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m========================== Import Python Torch Model ===========================\u001b[0m\n",
+ "\u001b[32mModel imported from: saved/neuralODE_msd.py\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {},\n",
" 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n",
" 'code': 'def FNeuralODE5(state, *weights):\\n'\n",
" ' from '\n",
@@ -1064,33 +1041,68 @@
" 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n",
" 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n",
" 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n",
- " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n"
+ " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n"
]
},
{
"data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAKyCAYAAAAEvm1SAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XV0FFcbx/HvbjwhQRIgaAjuGqy4e3F3KS1eKLRQtFhxKVCgUCAUL+5Wgru7B4dAIO7JzvvHwlJeKJrkZpPnc849nZnM7Pw2Cdvss1d0gIYQQgghhBBCCCGEEPFIrzqAEEIIIYQQQgghhEh6pCglhBBCCCGEEEIIIeKdFKWEEEIIIYQQQgghRLyTopQQQgghhBBCCCGEiHdSlBJCCCGEEEIIIYQQ8U6KUkIIIYQQQgghhBAi3klRSgghhBBCCCGEEELEOylKCSGEEEIIIYQQQoh4J0UpIYQQQgghhBBCCBHvpCglhBBCiATPy8sLTdPeaBEREdy9e5eZM2eSIkWKWL1fhQoV0DSNChUqAJAhQwY2b96Mm5ub6Rxvb28WLlwYq/eNS6qfQ7169fD09DTt///3WAghhBBJjw7QVIcQQgghhHgfLy8vnJyc6N69u+mYtbU1xYoVY+zYsZw5c4YyZcrE2v0cHR3Jmzcvly9fJigoiPbt27No0SKyZMnC3bt3AShcuDCBgYHcvn071u4bl1Q/By8vLwAqVaoEvP09FkIIIUTSY6k6gBBCCCHExwgMDOTYsWNvHDtw4ADJkiVj1KhRlCxZ8q2vf66goKAPPtbZs2dj5V4qqXwOH/M9FkIIIUTiJsP3hBBCCGHWTp48CfDGsLRmzZpx4sQJgoKCePz4MbNnz35jiJ+trS2zZs3i/v37hIeHc+XKFX744QfT1/89tOxVDyOAO3fumIa7/f/QNycnJyZPnszNmzcJCwvjwoULdOzY8Y2s3t7ejBgxggkTJvDkyRNCQ0PZtm0b2bNnf+9z1Ol0/PTTT9y4cYPw8HCuXbtGz5493zgna9asbNiwAV9fX0JCQjh8+DC1atUC+Kjn4ObmhqZpNG7cmHXr1hEcHMyTJ08YPHgwjo6OzJ8/H39/f548ecK4cePeuLebmxuenp48fPiQyMhIfHx88PT0JFWqVICxl1TFihWpWLGi6fv6ruF7xYoVY9u2bfj6+hIQEMDGjRvJmzfvWz+XypUrs2PHDkJCQnj8+DHjxo1Dr5c/a4UQQghzpEmTJk2aNGnSpCXk5uXlpXl5eb3za3369NE0TdOKFSumAdrgwYO1mJgYbcaMGVr16tW17777Tnv27Jl29uxZzdbWVgO0OXPmaLdv39aaN2+uVahQQRs3bpymaZrWoUMHDdAqVKigaZqmVahQQXNxcdFGjhypaZqmNWjQQMuaNasGaN7e3trChQs1QLO1tdXOnz+vPXnyROvatatWvXp17ffff9c0TdMGDRpkyurt7a35+flpmzZt0mrWrKm1atVKe/bsmXb48OH3Pv85c+ZoERER2vDhw7Vq1appo0eP1qKjo7UhQ4ZogKbT6bTLly9ru3fv1mrVqqVVrVpV27RpkxYVFaVly5bto56Dm5ubpmma5ufnp40cOVKrVKmStmTJEk3TNO3KlSvajBkztMqVK2uzZs3SNE3TmjRpogGanZ2d5u3trR0/flxr0KCBVrFiRe3nn3/WIiMjtdmzZ2uAlidPHu3UqVPaqVOntJIlS2qOjo5vfI8BrWLFilpERIS2fft2rV69elrTpk21M2fOaP7+/lquXLne+Lk8fvxYGzJkiFapUiVt8uTJmqZpWteuXZX/nkqTJk2aNGnSPrkpDyBNmjRp0qRJk/be5uXlpe3du1ezsLAwtdSpU2tNmjTRnj17ph06dEgDtBQpUmhhYWGmYsirVrZsWU3TNK1bt24aoF25ckWbO3fuG+cMGTJEq127tga8VTBp3769pmma5ubmZjr/3wWd7777TtM0TStVqtQbjzlv3jwtNDRUS5kypema27dva3q93nTO0KFDNU3TtFSpUr3zuefIkUOLiYnRfvzxxzeOjxw5UgsNDdVSpUqlpU2bVtM0TWvZsqXp605OTtrkyZO1vHnzftRzeFWUWr58uenradKk0TRN0/bt2/fGvf39/bWpU6dqgFaoUCFt//79mru7+xvnbNiwQbty5cobP8N/Fxb//3t89OhR7eLFi298b5InT675+vpqK1eufOOakSNHvnGvW7duaRs3blT+eypNmjRp0qRJ+7Qm/ZyFEEIIYRYqVKhAdHS0qT19+pQVK1Zw6tQpWrZsCUCpUqWwtbVl+fLlb1x78OBB7ty5Q8WKFQHjcLKuXbuyZcsWevToQZYsWRg9ejRbt279rGwVK1bE29ubo0ePvnF8yZIl2NnZUapUKdOxEydOYDAYTPsPHjwAwMHB4Z2PXblyZfR6PZs2bcLCwsLUNm7ciJ2dHeXKlcPHx4dLly4xb948Fi1aRMuWLdHr9fzwww9cvnz5k57L4cOHTdtPnz4FeGvuJz8/P9NwyHPnzlG+fHnu3LlD9uzZqVWrFj/88AN58uTBxsbmo+5pb29P8eLFWbVq1Rvfm4CAADZt2mT6ub1y5MiRN/YfPHjwn98/IYQQQiRcUpQSQgghhFk4deoUHh4eeHh4UKxYMfLly0eKFCmoWbMm9+7dAzDNYfTkyZO3rn/y5ImpkPL9998zePBg3N3dmTlzJt7e3hw6dIiCBQt+VrZUqVL95z2BN+azCg0NfeOcV0WY/5oTydnZGYDLly+/UZQ7ceIEAOnTpwegWrVqeHp6UqNGDZYtW4aPjw8rVqx4494fIzAw8K1jISEh772mb9++PH36lBs3brBgwQIqVqxISEgIOp3uo+6ZIkUK9Hr9B39ur7zreyhzSgkhhBDmR1bfE0IIIYRZCAoK4tSpU+8958WLFwC4urpy/fr1N76WLl06bt++DUBkZCRjx45l7NixZMqUiXr16jF06FCWLVtG/vz5Pznbixcv3jlZebp06QDw9fX95Md8xd/fH4BKlSoRFBT01tdfFeQeP35Mjx496NGjB4UKFaJJkyYMHDgQX1/ftyZFj00tW7ZkypQpDBgwgIULF/L8+XMAVq5cSYkSJT7qMfz9/TEYDLi6ur71tXTp0n3R908IIYQQCZd8pCSEEEKIROPYsWOEh4ebhvO9UrZsWdzc3Dh48CC2trZcu3aNfv36AXD//n1+//13li9f/sYKfv8WExPz3vvu27cPd3f3N4bpAbRp04aIiAiOHz/+2c9p//79ALi4uHDq1ClTS506NaNGjcLZ2ZlSpUrx5MkTPDw8AOOQuqFDh3LhwgXTc/rQc/hcZcuWxc/Pj0mTJpkKUg4ODpQtW/aN3kvvu39oaCgnT56kWbNmb1zj5ORE3bp1OXjwYJxkF0IIIYRa0lNKCCGEEImGn58f48aNY9iwYURFRbFp0ybc3d0ZNWoUly5dwtPTk/DwcE6dOsXw4cOJjIzk/Pnz5MqViw4dOrB69ep3Pu6r3kqNGjVi69atXLt27Y2vL1q0iB49erB+/XqGDRuGt7c3X3/9NZ07d2bEiBEEBAR89nO6ePEif/31F/PmzSNLliycPHmSXLlyMXbsWLy9vbl+/TpWVlaEhoby119/MWLECJ48eULVqlUpUqQI06dP/6jn8LmOHz9O9+7dmTRpEps2bSJ9+vQMGDAAV1dX/Pz8TOf5+/tTunRpKlWqxJkzZ956nEGDBrFjxw62bt3KrFmzsLa2ZtCgQdjY2DBy5MhYySqEEEKIhEf5bOvSpEmTJk2aNGnva/+/ctuH2rfffqtdvHhRCw8P1x4+fKjNnDlTS5EihenryZIl06ZNm6bduXNHCw8P1+7du6dNmDBBs7W11eDtleEcHBy0nTt3auHh4drmzZs1eHPlOkBzdnbW5s2bp/n4+GhhYWHamTNntI4dO76R6/+vgXevivf/zcLCQhsyZIh28+ZNLSIiQrt37542a9Ys06p+gJY9e3Zt9erV2pMnT7Tw8HDtwoUL2jfffGP6+oeew6vV99q3b//GvTVN04YPH/7e5zFixAjt3r17WmhoqHbjxg1t2rRpWpcuXTRN07TcuXNrgFaxYkXT97tly5ZvfY9ffd/37dunhYSEaC9evNDWr19vWj3wXT+Xz/39kCZNmjRp0qQljKZ7uSGEEEIIIYQQQgghRLyROaWEEEIIIYQQQgghRLyTopQQQgghhBBCCCGEiHdSlBJCCCGEEEIIIYQQ8c6silI2NjbMnz8fPz8/Hj16ZFrK+X3c3NwICgqiQoUK8ZBQCCGEEEIIIYQQQnwMS9UBPsXEiRPx8PCgcuXKuLm54enpyd27d1mzZs1/XjN79mySJUsWjymFEEIIIYQQQgghxIeYTU8pe3t7unTpQp8+fThz5gzr169nwoQJ9OzZ8z+vadWqFY6OjvGYUgghhBBCCCGEEEJ8DLPpKVWoUCGsrKw4fPiw6djBgwcZPHgwOp0OTdPeOD9VqlRMmDCB6tWrc+nSpc+6Z/r06QkKCvqi3EIIIYQQQgghhBBJjaOjI48ePXrvOWZTlEqXLh2+vr5ERUWZjvn4+GBnZ4ezszO+vr5vnD9lyhQ8PT25fPnyZ90vffr0PHz48IsyCyGEEEIIIYQQQiRVGTJkeG9hymyKUvb29kRERLxx7NW+jY3NG8erVKlC2bJlyZ8//2ff71UPqQwZMkhvKSGEEEIIIYQQQoiP5OjoyMOHDz9YTzGbolR4ePhbxadX+6GhoaZjtra2zJ07l+7duxMeHv7F9w0KCpKilBBCCCGEEEIIIUQsM5uJzh8+fIiLiwsWFhamY66uroSGhuLv7286VqJECbJly8aaNWveKCht27aN2bNnx3dsIYQQQgghhBBCCPEOZtNT6uzZs0RFRVGqVCkOHToEQNmyZTlx4sQbk5wfP36c7Nmzv3HtzZs36dKlC7t27YrXzEIIIYQQQgghhBDi3cymKBUWFoanpydz5syhY8eOZMiQgf79+9OxY0cA0qZNS0BAAOHh4dy6deut6x8+fMizZ8/iO7YQQgghhBBCCCGEeAezKUoB9OvXj9mzZ+Pl5UVAQADDhw9n3bp1ADx58oQOHTrg6empOKUQQgghhBBCCJE02dvb4+Ligk6nUx1FxBFN0/D19X1jfu/PpQO0D56VBDk6OhIYGIiTk5NMdC6EEEIIIYQQQryHTqejY8eOVKxYUXUUEU/27t3LwoUL35hS6ZWPramYVU8pIYQQQgghhBBCJDwdO3akQoUKrFy5kqtXrxIdHa06kogjlpaW5M6dm2bNmgGwYMGCz3+s2AolhBBCCCGEEEKIpMfBwYGKFSuycuVKtmzZojqOiAev5vJu3rw5K1as+OyhfPrYDCWEEOZGp9djbWenOoYQQgghhBBmy9nZGYCrV68qTiLi06uft4uLy2c/hvSUEkIkOY7OqWjUvQ21vy5FLndnbC0hNFJjhdd9jmzcxYkNWwgPDlEdUwghhBBCCLPwalJzGbKXtLz6eX/JpPZSlBJCJBl6Sws6D+/NDz2qkzPlq/9hRgIQEaPDNW8+GuTNT43uXdg8dRbH1mx856R9QgghhBBCCCG+nAzfE0IkCSnSpuHvY38x6+fK/ypIvXbneTQaxgq/nZMjTYcP5JtZE2VonxBCCCGEEELEESlKCSESvXTZ3dl9ah4Nizpi+fJV70VgBKPHraVUyR9wz9KZKkU6MaF+S05u3AaAjd7A6I55WX10IQ4pkitML4QQQgghhIhL3t7eaJr2zlahQoUPXl+oUCFKly4NgJubG5qm4ebmFus5/32fTzV8+HC8vLxiOdGXk6KUECJRc8mUnl2HfqNohtcvd38s2kfGtK0YNmghx49f5+7dpzx69AKf23dYPngkC3v+QO30frjYxlC3oAN/75+Djb29wmchhBBCCCGEiEt9+vTB1dX1rXb48OEPXrtu3Tpy5swJwP3793F1deX+/fuxnvHf90kspCglhEi07JM70XnWFAIxDsGLjtFo23EG33WcRHh45H9ed3HfYZbP32jar1kgGX9un4FOLy+ZQgghhBBCJEYBAQH4+Pi81aKioj547b8n+jYYDPj4+GAwGGI945dMKJ5QyTssIUSipNPpaDVuBGnc3Tjy1IG15yJo3HQ8Sxft/KjrBw/4g+Fj1pn2W5RNQ9+J/eMqrhBCCCGEECIB69WrF3fu3CEsLIwTJ05QpkwZALy8vMiSJQuLFi1i4cKFbwzfe7Vdu3ZtvL29CQoKYtq0aeTLl48TJ04QHBzMpk2bSJYsGQBfffUVBw4cICQkhODgYLZs2YKrq+s77wOQMWNGNmzYQEhICN7e3gwbNgz9yw/S8+TJY3qsf/75BxcXl/c+vxYtWhAaGmq6H8CCBQs4d+4cTk5Osf79/DdN2tvN0dFR0zRNc3R0VJ5FmjRpn96qdGmvTb5wRJt84Yg2Yu8WLWV61896nLmeP2kGbZNm0DZpgZGbtVK1Kip/btKkSZMmTZo0adKkJaTm5uamLV68WHNzc1Oe5XOat7e31r59+//8euHChbXw8HCtdu3ampubmzZlyhTt0aNHmk6n01KmTKndu3dP6927t+bk5KS5ublpmqZpbm5upu39+/drBQoU0Fq0aKFpmqZdv35dq1q1qvbVV19pvr6+2vfff685OTlpfn5+2tChQ7UsWbJoX331lXbt2jVt+vTpGvDWfQDt+PHj2rx587ScOXNqFSpU0K5evaoNGTJEs7a21m7fvq15enpquXLl0rp166ZFRkZqXl5e7/0+nD17Vvvtt980QBsxYoR27949LX369J/1c//YmoolQgiRyOQqVoCmfTsSYjB2n106cAR+j5581mN16ziRYsVzUDRPWpJZacxf2JdiWY8TERoay6mFEEIIIYRIXApWr0zNHt9g4xB/87NGhISyfeYfnN/l9UnXzZkzh5kzZ75x7O7du+TPn58sWbKgaRp3797l7t27DBkyhM2bN6PX6/Hz8yMmJoaAgAACAwNJmTLlW489atQoLly4wIULF5g2bRrLly9n9+7dAOzevZvcuXNjZ2fHqFGjmDJlCgB37txhzZo1lChRAuCt+1SuXBk3NzdKliyJpmlcv36d/v37s2jRIk6fPo2zszPdunUjNDSUa9euUbFiRdKkSfPe78HgwYNZvXo1T548oVevXpQrV45Hjx590vfxU0lRSgiRqOgtLViwchjFsgRxyMfApHEruHH0xGc/nsFgoE6lAVz3XoijnQV50+oZu2A4P7T4KRZTCyGEEEIIkfhU6tCatFmzxPt9K3Zo/clFqWHDhrF27do3jr2aT2rHjh1cuHCBixcvcvr0aTZs2MC8efOIiYn5qMe+ffu2aTssLIw7d+68sW9jY4OPjw+enp707duXwoULkzdvXgoVKsShQ4fe+Zh58uTB2dmZwMBA0zG9Xo+9vT358+fnxo0bhP7rg/QTJ05Qp06d9+bcsmULly9fZtiwYVSvXp3Lly9/1PP7ElKUEkIkKn3G9qF0NlsAvkoTzKWNG774MX18/Oj9/XwWzv0WgG8b52N+8YJcOXH+ix9bCCGEEEKIxMpr4RJq9uwa7z2l9i5a+snXPX36lFu3br3za2FhYZQsWZIKFSpQr149OnbsSLdu3ShWrNhH9SSKjo5+Y/9dk6CnT5+ekydPcurUKXbt2sW8efOoU6cOpUqVeudjWlpacvXqVerXr//W1xo2bPjWpOiRkf+90NMrNWrUIHfu3FhYWODj4/PB82ODFKWEEImGa5aMDOlVGePwZBgzfgP37sbOi6nnH5vp0KUWFYpn5mGIFV/368aVlt1i5bGFEEIIIYRIjM7v8vrkHksJUalSpahcuTJjx45l7969DBo0CB8fH8qWLcuqVavQNO2L79GwYUNevHhBvXr1TMd69er1RnHp3/e5du0amTNn5tmzZ6beUlWrVqVDhw4sW7aMnDlz4uTkZPpakSJF3nv/IkWKsGrVKjp37kyHDh0YNWoUzZo1++Ln9SGy+p4QItGYsnAIKW2NL9SXvf0YM3RBrD5+h2YjmXckgs33nUiTvzAFqlSI1ccXQgghhBBCqJE8eXLSpk37VrO3tycsLIzhw4fTuXNn3NzcaNGiBcmSJeP8eePIiZCQEHLnzv3O+aQ+1vPnz8mcOTOVK1fG3d2dH3/8kcaNG2NjY2M659/32blzJ3fv3mXJkiXkz5+fsmXL8scffxAaGsrOnTu5d+8ef/75J7lz56Z9+/Y0b978P+/t5ubGli1bGDt2LCtWrGDYsGE0btz4g4Ws2KJ8pvuE2GT1PWnSzKsVrVhSC4verBm0TVq0YZPmUTJPnNwnX8WyplX9Bm39W9NbWih/7tKkSZMmTZo0adKkqWyJYfW9/zJ48GAN0Fq3bq1dvXpVCwsL065evao1b97cdH23bt20oKAgbc2aNe9cfe/f35f/X+lv4cKF2sKFCzW9Xq/9/vvv2osXL7Tnz59r27dv1/r06aP5+/tr1tbWb90H0Nzd3bXNmzdrISEhmo+PjzZz5kzN1tZWA7QsWbJou3fv1kJDQ7WjR49qEydOfOfqeylTptSuXLmizZ49+43jmzdv1rZt2/bZP/dPqKmo/wVIiE2KUtKkmVc7cH2FZtA2aQZtk7Zu16Q4vdd382aYClMlGtZT/tylSZMmTZo0adKkSVPZzL0oJS32f+4fW1OR4XtCCLNXs1VdSmd3ACAiSqNbu7Fxer9tM+eiQyNn8gimTu2E3tIiTu8nhBBCCCGEEImRFKWEEGbvlzHt0b+c/++Pvw7y5PGLOL3f3XMXqep0jzqZgijtZknbvu3i9H5CCCGEEEIIkRhJUUoIYdaKVa9Ivox2AIREGBjcZ0a83Hfpop2m7YH966O3kN5SQgghhBBCCPEppCglhDBrZTu2ZeH1lBzysee3P/YQHBwWL/edMnoxz4OiAciVxoL6HRvGy32FEEIIIYQQIrGQopQQwmzlLleazPnzEmnQs+7AAwb3nh5v946Kiub3ebtN+wN+ahJv9xZCCCGEEEKIxMCsilI2NjbMnz8fPz8/Hj16RL9+/f7z3FatWnHt2jVCQ0M5dOgQxYsXj8ekQoj4ULlzW9P2rjkL4v3+E4b9SWikBoBHVgeKlJfXGSGEEEIIIYT4WGZVlJo4cSIeHh5UrlyZ7t27M3z4cBo3bvzWeWXLluXPP/9k5MiR5MuXj8OHD7Nt2zYcHBwUpBZCxIV8pYqQt3ghAHxu3+Hinv3xniEkJJy1W88DYKmHwWO6xHsGIYQQQgghhDBXZlOUsre3p0uXLvTp04czZ86wfv16JkyYQM+ePd8619XVlVGjRrF06VK8vb0ZOXIkzs7O5M2bV0FyIURcGDW5G11yvaBiumAubFyPpmlKcgzr/zuGl7euWTIDzunTKskhhBBCCCGEEObGbIpShQoVwsrKisOHD5uOHTx4kJIlS6LT6d44d/Xq1YwdOxYAW1tb+vbti4+PD5cvX47XzEKIuJEuS0aql0iPtQUUTBnOZa/47yX1yp1bjzhw+iEA9lYa3w+X3lJCCCGEEEII8THMpiiVLl06fH19iYqKMh3z8fHBzs4OZ2fnd15TuXJlgoODGT58ON9//z0hISHxFVcIEYd+ntADe0tj96R9J+9z9/ZjpXkm/7rctF2+msdbhXIhhBBCCCGEEG8zm6KUvb09ERERbxx7tW9jY/POay5evEixYsUYNmwYixYtomTJknGeUwgRt2zs7WlRp4Bpf/jAPxSmMdq8Zh9rj/qy8HpKToa4kqOUTHguhBBCCCGEOVi4cCGapv1nq1ChguqIiZrZFKXCw8PfKj692g8NDX3nNU+fPuXcuXOMGTOGXbt28d1338V5TiFE3Oo6sD3O9sZeUpfuBHDI66zaQC/9MmQR/pEWAJRqUl9xGiGEEEIIIcTH6NOnD66urri6utKnTx/u379v2nd1dX1jCiER+yxVB/hYDx8+xMXFBQsLC2JiYgDjhOahoaH4+/u/ca6HhwcxMTGcOXPGdOzy5csy0bkQiUDnjlVM29OnrlOY5E2X9h0k0Pc5Ti7O5K9UnmSpUhL8wk91LCGEEEIIIcR7BAYGEhgYCEBAQAAxMTH4+PgoTpV0mE1PqbNnzxIVFUWpUqVMx8qWLcuJEyfeWnWrc+fO/Prrr28cK1asGFeuXImXrEKIuOFRqST5Mxh7SPqHxrDo9/VqA/2LITqGkxu2AGBhZUHZRrUUJxJCCCGEEEJ8KTc3NzRNY8iQIbx48YIZM2aYjrm5uZnOGz58OF5eXgBkzJiRDRs2EBISgre3N8OGDUOv/+/yS4sWLQgNDcXV1dV0bMGCBZw7dw4nJ6e4e3IJgNkUpcLCwvD09GTOnDl4eHhQv359+vfvz/Tp0wFImzYttra2APzxxx9UrlyZ3r17kz17dkaMGEGJEiWYNm2awmcghPhSA4a1Q/9yDvHVm84SHR2jNtD/ubR7D6XThNAxpx/jhjdSHUcIIYQQQggRS8qUKYOHh4epBvE+a9eu5enTpxQpUoQOHTrQqlUrfv755/88f8WKFVy/ft10zogRI6hatSq1atUy9eJKrMxm+B5Av379mD17Nl5eXgQEBDB8+HDWrTMO33ny5AkdOnTA09OTM2fO0LBhQ8aOHcu4ceO4ePEiNWrU4NGjR4qfgRDiczmkcKLGV26AhkGDcUPmqY70lnuXb5DHMYTk1noc09uQq0herp25rDqWEEIIIYQQyvTtW5++/Rp88LzTp2/RoP7oN46t3zCEokWzffDaqVPWM3Xqhs+N+FGmTZvG7du3Ad7oIfX/KleujJubGyVLlkTTNK5fv07//v1ZtGgRo0eP/s/rBg8ezOrVq3ny5Am9evWiXLlySaKGYVZFqbCwMDp06ECHDh3e+tr/L8G+ZcsWtmzZEk/JhBBxrWT9Wpz3s6NAynCu3XrK7ZsPVUd6i8FgYLvXVZrXzouFHnoNbEPP5v/9iYgQQgghhBCJnZOTPRkzunzwvPv3fd86ljp18o+61snJ/rOyfYo7d+581Hl58uTB2dn5jR5Oer0ee3t7UqVKxYsXL9553ZYtW7h8+TLDhg2jevXqXL6cND7cNquilBAi6Spavx4nntlz8pkd8zsPUh3nP/02fhnNaxs/AalbPR+9dLq35r0TQgghhBAiqQgMDOXBg7cLTv/v2bOAdx77mGsDA0M/K9unCA8PN22/6+97S0tL03+vXr1K/fpvr8gdEPD2c3ylRo0a5M6dGwsLiyQ10boUpYQQCZ5bofyky2Hstnv79HkuHz+vONF/O7L/HA+eR5DR2YbMKfRUbFAZr3X/qI4lhBBCCCGEElOnbvjsoXX/P5wvoYiMjATA0dHRdCxr1qwAXLt2jcyZM/Ps2TNTb6mqVavSoUMH2rVr987HK1KkCKtWraJz58506NCBUaNG0axZszh+FgmD2Ux0LoRIuorXr2PaPrpmo8IkH2f1+hOm7W69ZcJzEf90Oh1ps7lTqGoFqrZpROHqlUiXMzu696z6IoQQQgghPo6Pjw/37t1jwIABuLu70759e+rUMb5n2blzJ3fv3mXJkiXkz5+fsmXL8scffxAaGorBYHjrsdzc3NiyZQtjx45lxYoVDBs2jMaNG1OkSJH4flpKyF+nQogEzcrWlu86VSS5dQwRoaGc3+WlOtIHTR+7mFc9eiuWyIze0kJtIJFkpHF3o+Ggfgz32szifQs4s6s/O//qyOkd/bh2cTrnfTYxaeVE3PLnVh1VCCGEEMJsaZpG586dKVGiBJcvX6Zp06aMGTMGMM4z+/XXX6PX6zl27Bhr1qxh69at9O7d+63HSZkyJdu3b2fDhg2MHz8egOPHj7Nt2zbGjh0br89JFRm+J4RI0Nr1akFlt2jAj81HHhEZFqY60gfdvf2Y6w+CyZUpGS72UKt5TbYslYUXRNyxc3Kidu9vKdW0AfqXvaEeh0a/cU4yKwP5XCBfs9x803Ayy3beZlCXUfg/eaoishBCCCFEguPp6Ymnp+cbx+7evfvWwmoAu3fvJk+ePG8cmzhxIgDe3t7UrVv3g/fz8/N76zGAj7o2sZCeUkKIBK1j52qm7d2bDylM8mnWbjxu2m7XpbbCJCKxK1CuOMeuL6FD1zqmglRkWDj7tx/h1GUfjp59wKVbzwmLjDFd42hl4Ns6WTh7dQFVmiedP3qEEEIIIUTCIj2lhBAJVnr3jHhkdwIgPEpj3tSVihN9vLmTlzOwe2WiDDrSZXVDb2GBISbmwxcK8Qkad2vFH1Nbk9LGQJaoEK48iWL73L848vc6wgKDWPCvcy0s9NSoVZyff2nPV0UzAZA5ucbav7rRJk06Ns2Yp+ZJCCGEEEKIJEt6SgkhEqxeQzpg/XI6pj1H7xAWFqE20Ce45/2EcX/fYs7VVJwIdCabR9KYqFDEn04/f4vnb61IaWOcMFNniGLToIHs+XMxYYFBb50fE2Ng6+ZjlC3Wnbr1x+IbGIlBgz2PHKjYtRONBveP76cghBBCCCGSOClKCSESrKZfe5i2p09coTDJ51m1YBMxmnH8ecFqlRSnEYlJ0+6tmDmyHvaWxhn1vR8HUzBvNw78c+qjrt+68Qg53doxZNo+bgfZAFCmRWPq9O0eZ5mFEEIIIYT4f1KUEkIkSGVqlSOri7Gb1BP/KHZtOqw40ae7sv8QkWHhABSoWhGdXl5yxZcr93UV5k1tha2FsSB18eZziuTujPftx5/0OP7+IYzrN4mlg0aYlieu3KktDXp3jvXMQgghhBBCvIu8QxJCJEi9BjQ3ba/Z+HG9PxKayLBwrhwwFtNSuKSkQLkSihMJc5cxexZWLeuLk7WxIHXrYRClC39HYGDoZz/m6c07WDNqAgCFUoWxbEpDaraSyc+FEEII8fFefcBlY2OjOImIT69+3jFfMHeuTHQuhEhwLCwtqVbKDQBNg8kjFypO9PlCbpynXhcP3JJFkuLHZnTYd1R1JGGmLKysWLVjImkdjAUpH/8Iynr0ICQk/Isf++jqDdSqmo/K35YGwHPedxQ/dYV712598WMLIYQQIvF7/Pgx4eHhfPfdd6xatYqnT59+UaFCJGwWFhakSZOGZs2aER4ezpMnTz77sXSAFnvREg9HR0cCAwNxcnIiKOjtCWOFEHGn+XdNWT67HQAX7wRS0L214kSfL0/BbFw6Nw2Ae34GsqSqrzaQMFs/z/2F0V2LAhAZrVGixA+cP3Mj1h7fwcGWK3f+IqOLLQBn70dSPFsLYqKiYu0eQgghhEi8UqdOzTfffEPu3LlVRxHx5OrVq8ybN49nz5699bWPralITykhRILjkLMgW+8nI3fyCJYs3ac6zhe5cv4WD19EkiGVNZlS6MlXPD+XTlxUHUuYmewliuH8VTX+eRhOhXQhDBi8PFYLUgAhIeHUrPwTJ09Nw9ZKR+FM1oxfOJz+bYbE6n2EEEIIkTg9e/aMX3/9leTJk+Pk5IROp1MdScQRTdMIDAwkICAATfuyfk7SU+o/SE8pIdSwtrNlxN6t2NjbERoQyIiKdYiJjlYd64ssWj+WdvULADB50QkGdBypOJEwJ1a2NvRfuwSXTBkBOPTHHNbO8Iyz+3Xq0ZD5MzsBEBEDlWuP58jOg3F2PyGEEEIIkfh8bE1FJjoXQiQoeSuUxcbeDoBzu/aYfUEKYMn8LabtGlULKEwizFGN7t+YClK3T51l3czFcXq/BbPWsevIHQBsLGDRkv5YyqSlQgghhBAiDkhRSgiRoBSpXc20fWbrLoVJYs8/Ww4TGGZckSRXelvSZEqrOJEwF3k88tP02yYAREVEsGrEr1/cRfpjNK/zM/4hxslJc6S2YNTsgXF+TyGEEEIIkfRIUUoIkWAkd0lFn5aFyJIskqCnz7h96qzqSLFC0zQOnb4PgJUe2nZrojiRMBczFw2kTY4gqqYP4sSyZTy7cy9e7uvvF0SP3n+Y9nu0KY573uzxcm8hhBBCCJF0SFFKCJFgfPNDKwq6RNEwSyClHB6iGQyqI8WaVcv3mLbrf11SYRJhLuq2/ZoKeZOj00Eup3BOrN0Yr/dfvmArR86/Wt5XR6dfvo/X+wshhBBCiMRPilJCiASjWdOypu2lC3coTBL7Vv65mUjjaCiK5EyFtZ2d2kAiQdNbWjBxUkf0Lxet+WPJYe7defL+i+JAuya/cOiuAc8bKbDPXYxcX0lBVQghhBBCxB4pSgkhEgRXt/QUcncAICRSY/mfmxQnil3h4ZGcvf4cgEiDjrJ1KihOJBKyrgM7kyuNJQAvgqP5sdsUJTlu3XhAr+9mEGkw/rlQp293Wd5ZCCGEEELEGilKCSEShG4/tcXq5SuS17G7REfHqA0UB8ZO3MiC6ylZdCMVyXPmUx1HJFAWlpb82LeOaX/0r2sJD49Ulufcjn+4f+kKABly56RInerKsgghhBBCiMRFilJCiASh0dfFTdvzZq5TmCTu7Fi1nRchxpXTcpctpTiNSKi6DelCllTG/z0/9ovkt3FLlebRNI0tU38HwMEyhnFTumFhZaU0kxBCCCGESBzMqihlY2PD/Pnz8fPz49GjR/Tr1+8/z61duzZnzpwhKCiIc+fOUa9evXhMKoT4FDkK5CR3ehsA/EJj2Py3l+JEcSMiJBTvM+cAcMmUERe3TIoTiYTGwsqK/n1qmfbHjP0bQwKY8P/GsZOkeXGZjjn9qJrDgkGTvlcdSQghhBBCJAJmVZSaOHEiHh4eVK5cme7duzN8+HAaN2781nkFChRg7dq1LFiwgMKFCzN37lxWr15NwYIFFaQWQnxI9x9bY/Fympqd+66jaZraQHHo6sEjpu185UorTCISot4jviNzCuP/mh8+j2D25JWKE722Y+kG0xDbnp3KY2VrqzaQEEIIIYRIFDRzaPb29lpoaKhWoUIF07HBgwdrXl5eb53766+/alu3bn3j2Pbt27XRo0d/9P0cHR01TdM0R0dH5c9dmrTE3m4+W6cZtE2aQduklataXHmeuGzZCufVjj7drT0J3aodub5MeR5pCafp9Hrt3KP1pn8LXfs0VZ7p/9u1BytM+X6e0k95HmnSpEmTJk2aNGkJs31sTcVsekoVKlQIKysrDh8+bDp28OBBSpYs+dZKQJ6engwcOPCtx0iePHmc5xRCfJoiZYuS1cW4ytgT/ygO7D6hOFHcunPhKgVShJLGLoaC7o5Y29mpjiQSiAJVKrDneRq23nfkxM0g5v22WnWkt/wy/PX8Vj26VJS5pYQQQgghxBcxm6JUunTp8PX1JSoqynTMx8cHOzs7nJ2d3zj36tWrnD9/3rSfN29eqlSpwj///BNveYUQHyddsZIsvZmCU762LFt7SnWcOBcTY+Dc9ecA2FlCw3a1FScSCUXFDq0xoONagA1t201NkMNYl/25CW+fMADSOer4/pdvFScSQgghhBDmzGyKUvb29kRERLxx7NW+jY3Nf17n7OzMmjVrOHToEBs2bIjTjEKIT1e4ZlWehlvi9cCWEQN+Vx0nXmzb9rr41qhZBYVJRELhXqQgbgXzAfDo2g2uH0m4PQZHj349z1WfbtXQW1ooTCOSEhsHe+p/05y5m6dz5PYabvht4X7wVu4EbuX0g/WsObKAXmP6kNY9s+qoQgghhPhIZlOUCg8Pf6v49Go/NDT0ndekSZOGPXv2oNfradKkSYL81FmIpMytUH6cM6YH4OaxkwS/8FOcKH4smbvetF26qJu6ICLBqP1tG9P23kXLFCb5sIUz13Df1/ihUMYUerr0b6c4kUjsUqZzpfHQH1l0eiPr/mjDN3WyUtLdmmwpDGRwiCGzYwyFM1jQsFRqpv9clVvXf2fmzgXkLF1CdXQhhBBCfIDZFKUePnyIi4sLFhavP5F1dXUlNDQUf3//t85Pnz49+/fvx8bGhooVK+Lr6xuPaYUQH6No7eqm7TPbdilMEr+8bz7gwYtIwPimPneRPIoTCZWy5c/J5C55qJspkGRhTzm7fbfqSB80cfJ603bvXnXUBRGJmpWtDfV+6MXALav4qllDfKLsCY/RffA6e0sNO7ecfPvHdLov+h3X7FnjIa0QQgghPofZFKXOnj1LVFQUpUqVMh0rW7YsJ06ceKsHlL29Pdu3b8dgMFChQgUeP34c33GFEB+gt7BgVN9KFHEOw8YQxoV/9qmOFK8OHL1l2m7RSd7UJ2UDx3TF2gJyJI/EOeAmMdHRqiN90OyJy3gREgNAljS2FKpQUnEikdgUKl+c2V5LqNihFZYvJ9QPCQplzT+3mDhjJ+Ur/Exql9ZYWdYnmUMT8uXtTu/+f7H3xD1uPovGP9L4IWa2YkXou3Ihtbt3Qm8hQ02FEEKIhMZSdYCPFRYWhqenJ3PmzKFjx45kyJCB/v3707FjRwDSpk1LQEAA4eHh/Pzzz2TLlo2KFSuavvbqMQIDA1U9BSHEvzRoV4e8qSEvIaSJCiY8OER1pHi1/u+9tKxt7CFVpXJBRqiNIxSxsrWlYbVcpv3RP89TmObjxcQYmDL7Hyq2bMhlf1uKNmrIuX3HVMcSiUTbHzowY2wTbC3gr5sxPA+OZp/ncvZ6LifsHX/HhYZGcOXKfa5cuc/MyavQ6/UUqFaJmj2+IY27G1bWVsyb2JSLXSvSuFIvgv0CFDwrIYQQQvwXzVyanZ2dtmjRIi0oKEh78OCB1qdPH9PXNE3T2rdvrwHalStXtHdZuHDhR9/L0dFR0zRNc3R0VP68pUlLjG3T4bmaQdukGbRN2vDJvZTnie9mY2OlhUcbn39AxCZNb2GhPJO0+G89hnU1/Ts4f3uJ8jyf0iwsLbWhuzdoky8c0SaeO6S5ZM6oPJM0829jFo7UImI2mf5dnLqzSkvj7vZZj2Vpba3V7tNN2/twt+nxrvqs17Lmz6H8eUqTJk2aNGmJvX1sTcVshu+BsadThw4dcHR0JGPGjEyfPt30NZ1Oh6enJwB58uRBp9O91V71qhJCqGVta0v5osYJzmMMMGvCUsWJ4l9ERBRX7xk/8be3glLVSn3gCpEYde1SzbQ9a8ZmhUk+XUx0NAeX/Q2AXq+nXJvmihMJczdt1QR+al8Eq5d/nV6960+9Mt156n33sx4vOjKSrdNns3PRSmIMxmM501hw4PAU8hTLF0uphRBCCPElzKooJYRIHFp1a4Ljy8U0z970w9fnhdpAisxdtJ8Ndx2ZfSUV9plzqI4j4plHpRLky2gLQECYgfm/rVKc6NMdXb2BiNAwAMo1qkmy5I6KEwlz9dvqifRqkgf9y3nMt+27QeGcHXn08MsXqhk7eB71m0wgOMJYmUrnCHv3jaNA6cJf/NhCCCGE+DJSlBJCxLt2Haqatpct3asuiGIr5m/gdpANUQa9LF2eBP00vL3pDfjfm84S86orhxkJCwzC5/h+amYMomeRcL7/5RvVkYQZGjJzEN0b5Ub38t/Dyk3nqVOxH5GRsTfp/9Z1Byhf9kcCQo0T9Kd2gB07RuKW2z3W7iGEEEKITydFKSFEvEqW3JGSeZ0BiIyBedNWKE6kjt+jJ/jeewCAW6H8WNvZKU4k4outgz3VSmUGwKDB2EF/KE70+R4cP0qeFBFY6qFD63Kq4wgz07Zfe4Z2+8pUoF2+8Rwtvx4cJ/c6e/IapUv2Na0c6eqo45+DU3FOlzpO7ieEEEKID5OilBAiXnXp1xq7l+t+Hr3gQ3BgqNpAil0/chwASysrsnoUVhtGxJvOP7TG6eUQ1tPXX3Dn9kO1gb7Amr+288Q/CoCsLpbUaFrtA1cIYVS4nAe/j2tqmkNqz7G7tGkwNE7vefWiN5XL/0hwhAZAVmcLth2cgd7SIk7vK4QQQoh3k6KUECJetWj5uieF54IdCpMkDA/OniFPinBqZgzihwFNVMcR8SRnqRL4hBnfBP/5xzbFab7c4hWHTds/DGqpMIkwF3ZOTtQd/DPn/WzRNLjk7U/t8t+jaVqc3/v86evUrz+GyGjjvbJlcKTN8B/i/L5CCCGEeJsUpYQQ8SZFGmcKZnUCICRSY8mcdYoTqffsxnVqZgwmT4oIqpbJqjqOiAcp07kSnSk/y26l5Lf94SyYsVp1pC82Yeh8Io0joihfMDXp3NKpDSQSNJ1OR6uxw0iZIR1Hnzow+5/nVCndO1bnkPoQrx3H6DVgCTcDLFl+KwWFGjSkeP3a8XZ/IYQQQhhJUUoIEW/yVCzPwuup2P0wGUu3XCEqKv7egCRUt6/d47G/8fuQKaUlmbJlUpxIxDWPf73x3bRkc6L4d/DC15/dR+8BYG0BP435TnEikZCVb9uCvBXKABD8wo8hbX7kqY9fvOeYN20VbTv9ToTB+Odw46E/4pojW7znEEIIIZIyKUoJIeJNkdrVCI/Rc8HPlpGD5quOk2AcO2t8M6/XQfNO9RSnEXFJp9OZemMYDAZObTT/oXuvjBuxyLTd7OvC6F4tpSbEv+Qplo82P3YBjP8Glg4cgb/PU2V5jq3dxOFVxl67VjY2tBk3AmtbW2V5hBBCiKRGilJCiHiRIm0ashUrAoDP7Ts8unZDcaKEY8v61/PxVKtRVGESEdfK1KlA3hxpAI0bR08ofTMe2w7uPsGtJ2EAuDrq+bptHcWJREKjt7BgyZoRtM8TiodLKAeXrjQt9qDShvHTeHzjFsksY+hVPTVz14xVHUkIIYRIMqQoJYSIF4VrVjVtn966U2GShOdvz63EGIzbRfOkURtGxKkff25Jq2wBtMvuT+AF9W/GY9uS5QdN2z36NFSYRCREw2cNpIibLZZ6KJYqhBPLV6iOBEB0ZCQbR4+jdTZ/MiWLok2tHDTqWF91LCGEECJJkKKUECJeTBvdiCrpg8lgH8WZrbtUx0lQAv2DuO1j7GHibK+jQMkCihOJuGBtZ0eFYukBSGEdg9faxLf65LRRC4mMgefhFsSkyoStYzLVkUQCkatIHn7oXNq0/9PQ5Ty6n3B6Cl4/fZG1284DYKGDWb91JllyR8WphBBCiMTPUnUAIUTiV6h0QQpksEGnCyetZTDP7z9QHSnBOXryNjnq5QOgcesaXDh2QXEiEdva9miCo7Vx+/SNF/g8eq42UBwI8Aui9+TD5KpdD9BRpGY1jvwtq2wKmLdsCPaWGgCHzj5h1oTlihO9rXOTYZR/tILMLjakTabjj9VjaFWtt+pYIolxzpSB3IVykTVnJpySJyMyKgo/3wAunLrG3Ss3CA8OUR1RCCFilRSlhBBxrnv/lrya83jLrktqwyRQW9cfpO3LolTFivkVpxFxoU27KqbtpX95KUwStzYvWE2u2l8DUKJhXSlKCVr3bkXZ3E4AhEdptG40VHGid4uKiqZd6wns2T4UvQ6aVnZnYaMq7Fr7j+poIhGzsrWhZqu6tOtUkyJ5XcngpMPGQnvrvGgDTL+YkkdXb3Lj6AlOb90p83MKIRIFHfD2q57A0dGRwMBAnJycCAoKUh1HCLN2328DGVIYRwsXLTmQs8elMPX/7OxtCQj6G0s9+IWBs72swpeYOKZ0wufpUmwtISIaXFI2IyQ4THWsONN35SIy5s0FwMRGbXhy45biREIVu2QO3Hi4nPROxk8mxs/6h0E9p6kN9QGeG36l7dfGDwfuvYghR/rmREVEKE4lEhun1C6Ua9OMIf3rUjJ9zAfPfxFhgeeNlG8+RoA3+9dsY+lvyzDEfPgxhBAiPn1sTUXmlBJCxKkqDSqZClJ3n0VIQeo/hIWGs/vscw48sWfbo+SkzZpFdSQRi7r0a43ty77JRy88SdQFKYDj6zYB4GITTbeB7RWnESpNWjjEVJC67xvOkD6/KU70Yd+2GIFPYDQAmVNZMO6PgYoTicTE2s6Wmj278vO21VTu1JZ7EQ5vfN0vOJrjl3xYv+sqnqtPsXTDObYf9ObAibs8vHL9X2dqNCiWAs8pzbjyeA1NujaN3ycihBCxRIbvCSHiVI++TUzbf69PfKuNxabx03fw9QDj/CXZS3rgc/uO2kAi1rRoUc607fnndoVJ4sf5nXtYOa8T6ZMZCMxchNHdrImKjFQdS8SzTDmy0L5+AV51yv/umxnEvFpqNAELD4ugW7fZrF3aC4DvWnnw29gs3L12R20wYfaqNq1JzV7foUuZ1nTsjp/GvsBnHNl3hvm/reb2rcfvfQyHFMkpWL0yjTo2wDl/KgBypLZgxZx2HOhdnw5NhnL3qnecPg8hhIhN0lNKCBFnrGxsqFIyMwAxBpg+2lNxooTt5olTpu3sxYsqTCJiU5qMaSmc1biKV0ikxpK569UGigdBL/x48uQFAE420KlfK8WJhAqtfvyWKM3YS2rf6UdsW79fcaKPt37ZTvaf9QHAzhJG/DZAcSJhziytrZm5dhKbl/egaVEbQCM6Kop9i5czukZjKhXuxM99ZnywIAUQ4h/AkVXrGFy/M4PHbOBZYBQAeh1UyJecs2d/o9OPHeL2CQkhRCySopQQIs50+r4FjjbG7TM3/Xh4z0dtoATu0bWbhAYGApCteFF0r2aHF2at249tsXr5f9v9J+4THZ005v1Y+Ocu03a7f03yLpKG9Lly4FKiPJ43UrLb24JuHcarjvTJOjYfyZNgWH/XiRfpi+JetJDqSMIMOadPy8HLS+jeMBfWFpDJIZq0fleZ2LA1Gyf+RpDv563EGhkZza9D5pPRpRlT/9hHRLSxR2JyG/hjXGPWHJiLtZ1dbD4VIYSIE1KUEkLEmfYdq5m2PRfues+ZAkAzGPA+dYa0tlGUz25FsQoeqiOJWOCQNTdX/a2JMsC839erjhNv/py+kjDjB/gUy5kCl3Sp1QYS8apu3+7o9XpiNB2jRyzm6oXbqiN9Mu/r92jU6U+8g6wBaDioH3oLC8WphDkpUt6DU5fmUSLb6+LQpj3X+bZWd3zv3o+Ve0RFRfPDt5MoWKAnV+4ZP9jS66Bh2fScuLYY18zpYuU+QggRV6QoJYSIE47OKcmR2bhKTGiUxvxpKxUnMg9FU4XRKnsA5V1Dad6uhuo44gs5OqdClykP2x44MXJbMBuW71YdKd6Eh0Ww96TxTZe1BfQc1E5xIhFfcpYuQa4ypQB4/uARh5avUZzo8x1bs5H7l68CkCF3TorXr604kTAXFetX5p+dI8icwtjrOTJGo1vfxdSv8gMhIeGxfr8bV+9RwL0tMxcdxvBybfUCmWwZv342rjmyxfr9hBAitkhRSggRJwrVqMLiWyn529uJ2SvPEREukxx/jM3rXs+5Uq5sboVJRGzIX6UCer3xf7WntnuhaZriRPHrj5nrTdtNG5dSF0TEq19nfk9ya+Mw1W0z5hITFaU40efTDAY2jJv6ao9uQ74hWfJkSjOJhK9mi9psWNWPFDbG13y/kBiqVBvG3Gl/x+l9DQYDvTv+SuvOcwiL0jjkY4+vVVq6L5hFxrzyN4UQImGSopQQIk54fF0b0PEgxJpJIxaojmM2dm/YT7hxJXLyZUmOTi8v0+asYLVKpu1zO/coTKLGhmU7eR5iXG0tVzpb8haVN0WJXdNvGtOwqCMdcvhR0PI+Z7eZ/9Bt7zPneX7mKI2zBNKuiI5xc39SHUkkYHXb1OXvxd1xtDYWpB69iKBwvm855HU23jKsXLiFokX7snrPHcC4Yt9382eQpVCBeMsghBAfS97tCCFiXdps7mTKlweA+5eu4HNLlib+WNHRMVy9a5wTwsEKKtUtpziR+FxpMqShZpV8WOo0fO894PH1m6ojKbF1j3Hok14HvQa2UZxGxLVhw1sCxp/34W0HEk3vwKMrV5M5mbHHV8dGhUnvll5xIpEQZSlUgPHTe+BgZfy9v/s0jGL5unL/bvwv9HLt4i3mdOnFrZNnALBzTMaYFZOp3KDSB64UQoj4ZVZFKRsbG+bPn4+fnx+PHj2iX79+H7ymTJky3Lp1Kx7SCSFeqdC8vmn75MatCpOYp/2Hrpi26zeVPx7NVefvW1LfPZTv8jwnXXDSLEgBzBi/zLRdr3p+hUlEXGv6TWPyZTAuueoXGsOYgbMVJ4o9e7YcYv9ZY2HBzgpmeP6sOJFIaFyzZ6Xz75PY/Tg5twOt8H4SSrG8XfB58kJZpojQUOZ168u1Q0fJ5hhB05wRrFvRj+KViivLJIQQ/8+silITJ07Ew8ODypUr0717d4YPH07jxo3/8/z8+fOzevVq03weQoi4Z21rw9xhVWjq7k92hxDObDX/oRvxbcPK18O8yn6VS2ES8SUaNigJgJUeDu88rDiNOicPnePGk3BuBFhz0j8FGfPK73RiNXxES9P2jLn/EBkZrTBN7OvecQLRxtGo1CvnRsESUmQVRo4uznwzZyr2Tk5Eazomr7pGiYLf8uJ5oOpoRIVHsLDPQArYPcNSD442sG3LMPIWy6s6mhBCAKADzKJftb29Pb6+vtSqVYt9+/YBMHjwYKpWrUqlSm/3JOjatSuTJk3i9u3bJE+eHHd390+6n6OjI4GBgTg5OREUFBQrz0GIpKDn0C78NtLYU+r0TX88crRVnMj86HQ6giI2Ym8FYdHgZN+QmKjE9eYusUuZJhWPH3libQEhkRop7BsQE2NQHUuZUk2+punwQQDs+2sFGydMV5xIxLZmXRuzYm4HwNhLyjVFE6IS4evWyp1TaFotBwB7zjylatHOihMJ1SysrOizeBYZ8hvna7p34TJzuvQiIjRUcbI3pc+UlpMX5uCa3BKARwEGCufpgu/jZ4qTicTOwsoKt4L5KF3ZgyoV8+Pu5kIyBxvsbCwBAyFh0QQGR3Dn7lMunPVm344jnD98JtEM/07KPramYjZdiAoVKoSVlRWHD7/+tPngwYOULFkSnU731vm1atWiffv2TJ069a2vCSHiTpfOVU3bc37fojCJ+dI0jUu3/QCws4TqDWUIn7np1Ls51hbG7cNnHyXpghTAuZ1eREcaV+AsUqsaegsLxYlEbBsytIVpe8bcfxJlQQqgT6dfTYtRVCicBo/yRdUGEsr9tX0aP9XPiJNVDH6Pn/Bnz/4JriAF8Oi+D2VLfs+Ll4tPpE+uZ9eRmVhZWytOJhIjGwd7SjSsR5fZUxh1cAc9Fs2mXZ/WdKidjQr5klMsiy1501mSN501xbPaU6VgSjrXy8W0oTU5c/AXph3bSrvJYyhYrRKW8jua6JlNUSpdunT4+voS9a9lhX18fLCzs8PZ2fmt8xs2bMi6deviM6IQSV5+j3zkz+wAgH+YgYW/rVKcyHztP3DZtF23UXmFScTnaNT4K9P2ssUyhDUsMIjL+w4B4OTiTI6SHooTidhUo1l18me0BcA/1MCYnxLPXFL/78mDZ6zaegkACx1MntlbcSKh0o8T+9K8UmZS28XQIqs/a4YMJ/iFn+pY/+n2tbvUrT3CVFgt5GbPGq/f1IYSiUruYvlZ7vU7j3xX0XXcT+QpWxobezsA/CI+7gOpgEg90XbJKVS9Mu2njGWE12b6zhhOjgI54zK6UMhsilL29vZERES8cezVvo2NjYpIQoj/8+MvndC/7Li4YcflJN875Ets/NuLoCg9V/xtiHBIrTqO+ASOKZwomiMFAOFRsHzeRrWBEohTm3dgodPI7hRBv59bqY4jYtHQX9qZtv9ceijR9pJ6pX/XCYS9/Iy0TH5nylQrpTaQUKJC3Qr80rcyrwZsLFp2iCvHz6sN9RGO7j9D5+7zMbwcGVX3qwxM+HOI2lDC7OUrWYgtJxZy9tg4mlfMREpbjbwpwgEI8HnGiQ1bmTtsGj8M+ZtKVYeTPds3uKZti2vatuTO3Y3qdcYwaPQGVm69zI5jDwkNeD3Uy87JkR86luLimclsPfEnBUsVVPU0RRyxVB3gY4WHh79VfHq1H5oAu8gKkdRYWVtTt7LxEwyDBuOH/ak4kXk7sPMYUw4acErtSHjq7OgtLDDExKiOJT5Cu15NsXn5YeDRi0+IjIx6/wVJxI0jx+iY4zmO1hCaLit97O0IDw1THUt8oUx5cpDHPSVgIDRKY+SAWaojxTlfnxcsWX+Wb5oWRq+DPsM6cWjXUdWxRDxyck7BkiX9TK/1B88+pleHX9WG+gTL520gd74sDO1jnHKhb4eSHN1fj7WemxQnE+bGyTklUz2H0bJmDmwtNF5NV61pEHr/NtPaT+X+pderSnu94zGePvXn+rUH7N76+nVUp9eTvUQxitWtSYV6FUjvYPywo6ZHGqocGsNarzv0bj+aZw994vLpiXhiNj2lHj58iIuLCxb/mofC1dWV0NBQ/P391QUTQgDQ+YdWpDCO3uD87UCuXripNlAicOvkGQBsHRzIkFu6LJuLho3KmLZXLd+rLkgCExEWzvnrzwGwt4IOvZspTiRiQ5nWzVl8IyXb7ifj98XHCAoIUR0pXvzUbRJXn8Gq28m5n6IA6XPlUB1JxKMV26eQIbnxbdTTgCjqVuyrONGnG/79dNZ53QaMZYRWP3bFKbWL2lDCrLTt25YbdxfTsU72lwUpiDZobPjnOnny9qCqR9c3ClKfQjMYuHH0BCuGjGJcow6s++cGUS8/m7XSQ/MqWbhxaz4Dfu0ZW09HKGQ2RamzZ88SFRVFqVKvu0iXLVuWEydOyMz8QiQA3bvVMm3PnbtVYZLE41VRCiCbRxGFScTHsrCypERe43DLqBj4a85axYkSlmVL/jFtt2otE/ibuxRp01C0dg0M6Dh9L5IhvaaojhRv/J8H0PV7Tx6GWgFQo0cXxYlEfOk3thc1PYyv89EGaNZkLIFmWoxtUbM/x28Gsco7OXcNLrSbNBq9pSxEId4vRepUrDs8n4WTm5Ha4fXxbQduky3rNzSs+gPXr96LtfvdvfWAxlX74e7eiTW7b/BqdhAnGxg/sAbHby4ne77ssXY/Ef/MpigVFhaGp6cnc+bMwcPDg/r169O/f3+mTzcuK502bVpsbW0VpxQiaSpTvRT5M9kD4BdqYP6UFYoTJQ63T50FwEqvUb6GzFliDnKXLMYlfzt8wy244O1PSJAMT/u3hTNWmSbYLZ7HGaeUTmoDiS9Srm1zLKyMM0EcWrGGyLCk9ft+dPVG/J8Yh47kr1SedDnlTVFil694fkb2r2HanzhrN/t3n1SY6MtERUZRpXg3rt429mJ1L1qIOt93V5xKJGSZC+Tl5zULqVHC1TSP7O3HIVSqPpI65ftw/27cDad7dP8ZTav1o8RXP3H2pr/puEe2ZJw+PZXO/dvG2b1F3DKbohRAv379OHXqFF5eXsyaNYvhw4ebVth78uQJzZs3V5xQiKSpZIM63A40flq8bO0pYmTuo1jhc8ubeul96Z7nOT82cUenN6uX7CQp21elOPzUgb9upqRn/6Wq4yQ44aERHLlg/IPVxgI6922hOJH4XCnTpKJ2m3oARIVHcHDZ34oTxb/oyEi8Fi55uafRftC3SvOIuKW3sGDZ2uHYWxlHaBy/4svg3tMVp/pyIf4BLP5hMNEvVziv2L4VRaqWU5xKJEQlG9Wjx6LZWKRMw55HyYgxwMxFh8iVqRX7dp2Itxxnjl2maI629P9lPaGRxn+PyayhYe9ONBzUD0tr63jLImKHWb3DCQsLo0OHDjg6OpIxY0ZTLykAnU6Hp6fnW9d4enri7u4enzGFSFIcUqYgTYlybLiXnNmnrfil/0zVkRKVYP9A9DrjHDwVa5f58AVCqfyVjH/Ix0RFc87rkOI0CdPihTtN282bl1eYRHyJn8f1oGvBCOpmCuTR4T0Ev/BTHUmJo2s2kcbwnJZZAxjdPjfFKxRTHUnEkUodWxFlmwKAoAiNBtUGqA0Ui+5duMymSTPQo/FVmhD2bPqRzDkyqY4lEgi9hQWNBven2S8/mwo+2/ZcoWjJH+ndcZyy1banjPiTvLm/49ztQM6/sOVagC1lWzWl24KZODqnUpJJfB6zKkoJIRKeUk3qY/VyJczty7bg6/NCcaLE5fCR66btuo0rKEwiPiRtNnecM2YAjEMvwwKDPnBF0rR0zlpCIo3bRbInxzmt/OFobvQWFrRpUhydDnIkj+TSrn8+fFEiFR0RQfiNc7jaR6PXwa9TZehTYpQ2mzvVu3Vh35NkrLyZjJ59F/Hkoa/qWLHq4LK/yRN9g5JpwkhuC+t3TECn06mOJRSztrNl64k/Gfh9TdOxg8v+Zk6XXlw4+XmTmMeme96PKJq9DT17zCYqIgKALIUK0Gf5n+T2KKA4nfhYUpQSQnw2C0tLyjRvDIAhJoZDK1YrTpT4bFq737Rd5qvcCpOID2nQ/muSWxuHrl702v+Bs5Ou6KhoDpx+CBhX0Pm2f2vFicSn6tCnJWkdjW9Wbz4O45/NSbtX4M+9ppjmSqtQKA35PfKqDSRild7CgpZjhpp6iCz9fTV/zU6ci1j0bD2CUOMoPgq7J2Pin4PVBhJKJXdJxcFLnlQv4oyHSxiFUwazfPAo1v06hZjoaNXxTDRN4+CKtcxo09U0z1+JXCk5dWQsHfvK3xjmQIpSQojP1n1IF4rndEKHxqW9B3nx8LHqSInOPxv3E/Hy//t5sySXTy0TsB+6lqdTTj/aZffj0bnTquMkaAvnvV6hs0njrxQmEZ+jV+96pu3fZ29TmCRhePb4OWt3XgPAQg/jfuutOJGITRXaNidTvjwA+Ny+w/ZZ8xQnijs3L3vzw+CVpv1e7UtSvpa8RidFqTOm5djF+Xi4GxcyMmiwe9EqTm5MuCtsP7x6nWktOhF97yo1MgZhZwl/TG7B4El9VEcTHyBFKSHEZ9Hp9QzsW4s6mYLokMOPc+sT56eGqkVHx3D9QTBgnMTxq6olFScS75I5eyaypjF+im6IiuT2pVuKEyVsaxZt5o6fxrGndpwNT4d9clmFz1yUrFyCgpmNb1ICwgzMGveX4kQJw8AeU4h8ucZH9ZIZyJY3q9pAIla458nK37NbUThVGJohhhVDRxMdGak6VpyaO3EJmw/eA4y9Wf9aMgArGyvFqUR8Sp0xLYdOzyFnWuPPPSoGOnWfz69DEn5BNuj5C35t24eLtwMAsNDByH5VmbxoqOJk4n2kKCWE+CzdB3cknZPxJeRFQBhn9h5XnCjxOnzshmm7XtNKCpOI/9KhZxPT0sj7j99RmsUcGAwGeo/eyeGnDvjF2FCgisyXZi4Gj+7Mqw6bKzecISoq4QzhUOnBnUds3n8bAEs9TJjVV3EiERsW/v0LqWw1KqUPIYvfWe6dv6Q6UrxoWftHfIKMVdZMqSz5Y9VoxYlEfHlVkMqe2hKA8CiN+o0nsHjOBsXJPp7/i0BK5G7PjqMPANDpoG/7EizaPElxMvFfpCglhPgsP/SuY9qeOGm9uiBJwNb1B03bZcvmUZhE/Je6tT1M28sX73zPmeKVs9t3m7YL16qmMIn4WGkypKFq8fQARBvglwG/K06UsPzUcyrRLxehqlMuC5ncM6gNJL5Iuz6tKJ8vBQAR0Ro/fjNObaB4FBIUQpdvZmLQjPtt6ualagP58CCxS5nGmYOn/r8gNZ7tGw4oTvbpYmJiqFW6G8u3XTMda1cnF6v3/KYwlfgvn1SUio6O/uQWFRUVV9mFEIq06d6ULC7GLr2P/aP5Y9JSxYkStx3r9pqGheTPmlJtGPGWZMmTUTCrcfhZaKTGxmU7FCcyDw8uX8X3nvFTzOzFi8ryzWbg5/E9sDW+V2HvyYc8fvBUbaAE5tblO+w6ZvydtraAsTO/VxtIfDbHlE5MGNPCtD9ljhe3bzxQmCj+bVm5m1U7jCsAW+hh0cK+2NrbKU4l4oqtgz17TswhR5o3C1K7Npn3Qhata/fn96UnTPuNKrmzcvc0dYHEO31SUUqn09GkSRMqV678Ua1p06bo9dIZS4jEZuiw5qbtGXN2ommawjSJX2REJDcehgAQoVmQvWAuxYnEv7X6thHWFsbtE5efEh0dozaQGTmzfRfJrWIomTaC3kM6qo4j3sPCypKWDYqY9kcNWaAwTcL1U+/fTL1LGlbLjVNKmS/NHP2+bCRpHIzjVG/7hDG0z3TFidTo3Hgwj/yNQ3StbK1p+kNXxYlEXNBbWtBnzq9kT2csOkZGQ4PGE8y+IPVKzzYjGTfn9arITatkY/HWyQoTif9n+Skn79ixgz179hAYGPhR5ydPnpwdO+QTYyESk8792pAjrQ0Az4JjmDQs4U96mBiMn72X0u3aEh6jxyVnbm6ev/bhi0S8aNSknGl77eqD7zlT/L/Aq+fplMsPgLQtSjFGpuFJsPJXKseJF44U0YUT8CKAA7tkHsF3uXjyCocu+pLOLT0nn9lRoGZ1Di1frTqW+ARFyxalWfVsgHHFsY5tJ2IwGBSnUiMsNJwO7afyy9whnHiejIKNm5Jp7U7uX7qiOpqIJTqdjua/DCZN4RKs9I7h64z+9OzxOzs3Ja6/Z37uNhG9Xs+PXcsSbQCHPB7U7NWV7TP+UB1NADrgs7o42Nvb06VLF3Lnzo2Njc1bX+/cufOXZlPK0dGRwMBAnJycCAoKUh1HiARBp9Ph7buWzKmM9ewfR29i0lB5MY8PmQvmo8/S+QCc3rKDpQNHqA0kAOO/iRehG0lua5xjJ3XqNgS8CFAdy6w8CVxPGkcLNA3yFOrH9Qs3PnyRiHdd50wlV5lSACz5vj9n/kkcn6DHhfS5svPDauOqhM8fPGJc3WYYYqQHpbk4fnM5HtmSAbD2nxs0qdpPcSL1qnbtQK1e3wLw6NoNprboiEF6BScKtft0o0qXdgBERUTwZ48fuHHslOJUcWfC/EFkrVKTu8HGFZO3TPudPX/KKrJx5WNrKp89tm7FihUMHTqUlClTotPp3mpCiMSnz4hvTAWph35RTJZeUvHmweWrRISGAZDVo8gHzhbxpWqDiiS3NW5fuR8sBanPsHO/cc4SnQ66D2ipOI14l1QZ0pkKUr73H3B2z2HFiRK2R9ducuWA8XvknDE9BavJqqnmon2fVqaCVEikxretRilOlDDs+fMvHl4xvlanz5WDiu1bK04kYkOjbi2p0qUtAIaYGJb8ODxRF6QAfuzyK1MHvZ7svM733SnZqJ7CRAK+oChVqVIl6tWrR8uWLenUqdNbTQiRuOgtLajVrIZproxRY1bLXFLxyBAdw91zFwBIkTYNzhllVaeEIIdHYW4HWhFlgJ27z6uOY5b+mL7GtF2vZiGFScR/KfGvP9iPrdkkr/0fwWvBEtN2pY5tFCYRH8vS2prBQ17PmTlp5k6eP/VTmCjhMMTEsGrEWAwxMTjbRLNwagtKVfb48IUiwarWqApLZ7SiTqYgLHUa68dP4+KefapjxYtDK9awZdrr1WNHT+9D3xHfKEwkPrsodeXKFezsZAUGIZKKMs0bczE6PYtvpGDbWX/+mLxMdaQkx/D4FlXTB9Ehxwt69m+mOo4ALLPkY8O95My+4szUMdL9+3Mc3HWMxwHGYSDuqa3J75FXcSLxbxaWlgwbUJcM9lHEREVxYv1m1ZHMwq2TZ3hw8TI5k0fwY700dOjd/MMXCaUqtGvJzqcunPa1xftpBKMG/P7hi5KQB5evEXRsN62z+5PJMYbFS3+SBa3MVI6COVmx5HtsLCBn8kgy+J5JcnPf7fnzL/Z6LqN0mhCqZAxj/NCvadO9sepYSdZnv5J06NCB2bNnM2zYMNq3b0/btm3faEKIxMMhZQpqdO8CgF+kJd+1/VVxoqQp8P5dCqSKIKWNgfIV8quOk+QlS5WSTPnzAHD/2i0eeSet5cJj03av15Pmdv+hxXvOFPGtQ58WlMqko1nWAIpbehP0/IXqSGYj+vJh6mQKIq1dDAMGNFIdR7yHU5rUVPmmHREGPV4P7ahQcVCSndz8faYPmkJAmLGnZHZXW8bM6q84kfhUTqmSs/2f8aR82bfknm8EHeoNUBtKkc2TZxL++B4AlnqY91sHajWprDhV0vTZRalvvvmGHDly0K1bN4YPH84vv/xiaiNGjIjFiEII1Wr27IqdkyMAx9dv5v7Fy4oTJU2bVuwi+uXfyIVyplYbRpDrq5KmT4mvHpA5dr7EnKmrTNu1q0nBNSH59rtapu01K/YoTGJ+fv/VE98Q44t2noz21Gwkb3YSqrp9u2Njbw/A4VXreHBFFlx4l4AXgfT76fXQ1D5dypEzfzaFicSn0Ol0bNo/E3cX4/ywgeEGqpTtR3BQqOJkamiaRo2S33LmtnECbhsLWLW0L8XLylQC8e2zi1KdO3emZcuWpEuXjqxZs77RsmWTFychEouKdcrSq1t1LHQa4cEhbJ02W3WkJCs0OBRvn3AAUtnryFskt+JESVvl+hXR64yfGF85cERxGvN2Yv8ZHvhFA5DZ2YqiZQoqTiQAcuTPQZFsTgAERWjMnSTDtj9FTHQ0fy55/dowbEwHdWHEfypV/SvK1q8OQGhAIDtmySIu77N45iq8zj4FwNYSlq37RXEi8bEmLx5BuXwpAIiKgUZNxnPr2j2lmVSLioiiQtFvufEkAgAHa9iyfSSZ3NMrTpa0WH7uhb6+vly6dCk2s4gExDaZA2kypiV3HjecU6fAOXUKbO1s0FvosdDr0VvoiIqKISgghKCAEA4evkJIQBBBvs8JDw5RHV/EEr2FBfMW9CVbmjAKpIrg+xGbZOiGYifO3CVHulwANGxVnctnripOlDRZWFoyrF0+LC1ecPW5jgEvJ6EXn2/bP5f4ponx08lufZvzzSGZOF61ASM7YfFyQeUtXteJioxSG8gMjf5xJj06liGZNZTIlZKiXxXk9GH53U5I5s7/npzp/Tjpa8ewiQsIDQhUHSnBa9dgCFeu/0EyayiaPTl9R3zD1BFSzEvImnRpRM9WRU37g0auZc8W6eUNEBwQRPni3Tl96Q/SOVng4qDH6+hvFMrenpAgeV8bHz67KNWjRw9mzZrFyJEj8fb2Jjo6+o2v379//4vDibhjYWlJ4ZL5KFWhCIWK5iBbVld2Xw0j2j4VKVzTYOvgQHanCOplDvqox5t+0RkDxr9co8IjyGPvR4E0Bh4/C8bb+xlXr97jwunrHD9wlsf3nsTlUxOxaNKCwWRLYw2AITqatTMXK04kdmw5SqvaxqJUxUoFGKM4T1JVp3l1HKwBNKIC/DBEx6iOZPZ+n7ySKjU8uB5gg7c+k+o4SZ7ewoKGNfKZ9scNW6AwjfkKCQxm1ZaLdGqYH70ORk/uRu3S3VTHEi+17tmcApnsAI38TsEcW7tRdSSz8PDuY0ZN2sr4n2sD8MvAeiyfv4EnD54qTibeJUeBHMyf2RHLl2Ok1u2+wZSRC9WGSmB8HjylRpWfOXhwPE42kDWNDf+cmM1XeTvI/HLxQAd81rq+MTGv/wD/99LAOp0OTdOwtPzseleC4OjoSGBgIE5OTgQFfVxhJqHS6/WUquRB9a/LUKpUbvJkT4NrCkus/m/w5to7TtwNtjbtZ3aIpLH7hz8tijHAb5dd3jhWKV0whZ3D33m+X5iG98MgLl55wN4951j91zaCn8uSuwlN4dIFOXpgDNYWxv1W38xnxfwNakMJnFI68tx3GRZ6eBZsIK1jfdWRkqS/tk6mda2cAIyZuZehvSYrTpQ49Fn2J5kLGFff+7VuM3zvygdcqnTo05IF01oBcPleMPndWipOZL7SZXbl9u152FhAlAFy5uzG3VuyMIJqOr2e28/W4JbK+J5l+KQdjBowU3Eq83Lq1jKKZDXOObr75GOqF++qOJH4f5Y2Nuy/vIRSWW0BuP4olAJZWhMVFf2BK5OmGo0qs35VX2xevv9ZseMGrWr2UxvKjH1sTeWzK0fu7u6fe6mIR1/368GycTWxt/rwucmsDESGheP/xIcAn2c81EJwLJyKwKAwAgNCCQ2LwBBjwGDQQNOwtLbEwd4GKxtrDuzfg52TI04uzji6OKNzSfGf90lppyNldieKZs/LVxUKU6BzL57duYf3mfPcPn2W0Ad3uHRShoaqpNPpWPr3EFNBasexh1KQSiAC/YK46xtB1jQ2pE6mJ0e+bNy4dEt1rCSnfMmspu3Fc9YpTJK4nN2+21SUKlyjCrv/WKQ2UBL2Xfc6pu15f+5SmMT8Pb73hG2H7tKgvBtWehg9vQ9t6ybN1a4SkgFju5sKUo/9oxjz0++KE5mfdk1HcfL4BGwsoHKxdJSvU4H9W/apjiX+pfHg/pwKT0/qwGBSW0dQs8IPUpB6jx1r99Dn54z8Pq4podF6orMVp2Sjehxbu0l1tETtk3pKbdmyhZYtWxIY+HFjrZMnT86yZcuoU6fOh09OYBJDT6lkzin5Ze9W2mTzI7Xdv3u2gW+IgXtPgrh+04cL525z8shFzh6/jO/jZ7F2fzsHO4qWLkDhEnnJk8+d7NnTkc0tFRldbEzV5yM+9hx9Zm+6RodG9zwvCI8ycPraM3ZsO8XyeRu4d/thrOUSH/bb0l9M484DwjSyZ+nA86cyl1RCsXzHFJpXzwHAwF+3MuFnmXw+PmXLl40bF6cB8MAvisypZKn32JLCNS1Dd60H4PGNW0xq1EZtoCTKPU9Wrl+cjoUegiM1UidvSkR4hOpYZi1f0TycPTkBCx2ERUGGdG3wfx6gOlaSZetgzz2fFbg4GKee+KbvEv6ctlJxKvM0ZdEw2jQvxe6HyTh9/h5Tm3UgJlqKHglB8QZ1aDFqCAARoaGs+fEnTu07qTiVeRgy7Qcy1WxIcJSFcdGKHv25dviY6lhmJ056StWsWZOKFSvi5/dxQ61SpUpFjRo1PuUWIhYFP/fjyOr1FGlTkuS6ME6dvcPe3afYsXZvvBQYwkLCOLT7OId2H3/juF6vp1i5wlSsVoJgy+RYpM9Kpvx5sLSyIrVtDNYWGtYWOioWSkPFQrUY+1MtHvhFcejEHVb+tZNNy3fK2N44VLtZNbq1eD0RYr+BS6UglcDs2nbcVJSqWLkwExTnSWradn09ZPLgMW+FSRIf/yc+3Dt3gXJlclG9YhqOVS/FgZ1HVcdKcnoOaofFyyH+2/belIJULLh0+gqHLvpSLr8L90OtKd+0HhvnLFEdK8kaM6u/qSB17WGoFKS+wIAuY3lo/wfp8+QhXY5sVOzQmn/me6qOleSlcXej4aAfTPt//zKeM1KQ+mijv5/M1z9GU6FtCywsLWk3ZQwz233L4+syOiEufFJPqX/PI/WxzHV+qcTQU8qcWNrYkClfbmo0qEiP9qXJmcEBK4t3nxsUoXHswhN+HrKUM3sOExMlqwHFlrQZ03Dh6jxcHIzvRtbuuU2TKn0UpxL/L1XqlKw/t4pHYTacu/SAX79upTpSknL02l+UyJkCgMbtfmfdX9vUBkpkJi8cQt8OJQFYtvUaber0V5woadHpdPy8bQ35c7iQP0UYHeoN4JSsFhcrPCoWp9vcqfhHWuB7/wHj67XA8Bl/W4sv45IuNbfvLCDZy2lU6zadytbVe9SGMnMZcuekz/I/sbC0JCoigkmN2uB7T+ZNU8XWwZ7tZzy5GpOOkGgLjqxez+pfxquOZXZ0ej3tJo2mYLVK6NAo5vCM7o1/5NKZa6qjmY2Prano//Mr72BhYfHJLTYLUjY2NsyfPx8/Pz8ePXpEv37/PelY4cKFOXr0KCEhIRw/fpyiRYv+57lCveiICLxPn2POsOkUcGtBqpQtaN9zMWv33Oah/5tdgB1tdJQunJ5Wk35l5IFttJ00mqJ1qmPn5KgofeKgt7Dg99UTTAWpu76RtK77o+JU4l1ePPNjtdddHoZa4eLuTrJUKVVHSjJsbG0omDUFYByCs2mFzLUT2+ZMXsGr9VOqlc+hNkwSlLN0CVJlSMejUCumr7woBalYdHLvCY7uOQGAS6aMFKhaUW2gJGrqgkGmgtSxK8+lIBULHl69zv6/jL3NrG2sGbdoJDqdTnGqpGvp1omUz2FLm+z+2PvdYcP4aaojmSXNYGDZz7/w+PJlvnYLpJy7nl1e40iTzuXDF4tP8klFKdUmTpyIh4cHlStXpnv37gwfPpzGjRu/dZ69vT1bt27lwIEDFCtWjMOHD7Nlyxbs7e3f8agiIQoJCuGvWX/TpEofMqVsSM78fRg9cy9nbgcRZYDbgdZo6LB1cKBwjSq0HvcLF+6v4vzdFYyd/SNZcmZW/RTMTt1+PbjjkJOjT+0IjYKGdYcTESZDNhKqWydPm7azehRRmCRpadiuNrYvP2s5c/050TJZaKy7cfEmN54YV29NnUxP1frlFSdKWko1eT089ehqWeAitnktfD1kr1InmTMtvjmnd6V2RePKqQYNenaZojhR4rFz9nwsAx7TPGsAnSqlZtxs6eWqwreDOtKwvPF9kI1eY/3UOUTJEOzPFhUewcqfh+OojwTANbklXsdnYmNrozhZ4mI2RSl7e3u6dOlCnz59OHPmDOvXr2fChAn07NnzrXObN29OWFgYAwYM4OrVq3z//fcEBQXRtGlTBclFbLh56TbDek2mWLZWpE3Tht5dp3JiwxZC/PwBsLUwkMUxmvyZHRj4XTluXZ3Ftcd/M81zKPmK5lYb3gyUa92MCu1aAjoOPrShXI0xnD12UXUs8R63TrwuSmWTolS8yVeyMAGRxv91bt0qczPElY1bz5i2u/RooC5IEuPk4ky+iuUACHzmy+X9hxQnSnxuHj/F/UtX0KNRrXQWWn4rCyXEp6rdOrP0dkqOPLVj/V5v6QkYiyLDwjmxYgXp7I0f1vTuXJ6c+bN+4CoRm/IUyc2kX16/psxafIQd62Q1xC91/9YD6tb5hZCXM8bkyejAlkMz1YZKZMymKFWoUCGsrKw4fPiw6djBgwcpWbLkW91DS5UqxcGDB984dujQIUqXLh0vWUXc8n8ewL4121gxZDQjKtVlVsfueO/eQWD46+nRdDrI4WpL73YluHBqInd81zJv9RhKVCymMHnCVK7Z1zQY2Ne0v2bMRM54ycTCCZ33mfOksoqkcKow+naU17b4EpgqOwuup2ThVSf+/O1v1XESrd8nLMXw8iW9aplsasMkIb/O/J7WuYLJlzKcc1u2YYiW+Y7iwtUtG+mY049amYIZPqyF6jhJhmuObHh8XZsog549N2No22CI6kiJzpKZq/A65wuAjSUsWTNCbaAkxMramnVbx+BgZdw/eyuAvp1+VRsqETmx7xQdvp1H9Mu1tioXdWXe6tFqQyUin12USpYsWWzm+KB06dLh6+tL1L8mtfbx8cHOzg5nZ+e3zn306NEbx3x8fMiYMWO8ZBXxxxATw+2TZ5jUeySp7L+mTtNprNx1k2fBb67Ol9nZis6NC3LUawQjtiylRvcupMuZXVHqhGPg+B5sW9qVDPbGf1c7fp/PsTUbFacSHyMiJJSKLs+plD6EstltcMueSXWkRC9leldcs2cFdJw5cRWfBz6qIyVad67f5dqjMABS2eup06yK4kSJn06no3GdgqSzj6Z6hmAenTyiOlKitXPZJsIjjb1Jcqa3o27zqooTJQ21e3+HXm9867Nn/mLCAgMVJ0qc2jcaQrBxpBMeOVPSc1A7tYGSiAUbxpPT1ThZWkCYRt3KP6BpH72emfgIaxZuZPCE7ab9zo0L8dPY7xQmSjw+uyh14MAB0qZNG5tZ3sve3p6IiDfHw77at7Gx+ahz//88kfhsW/0PLav3Ja1jfSrUGMOiDZd46Pd6zpdHoZY4Zs5K9W6d6b/mLwZt/Ztf5v9Cg7a1TX+oJBVjZv/ImAE1sbfUaJQlgAde29g5+0/VscQnOHXuvmm7YZuaCpMkDbnLvu6RduXA4fecKWLDuo2vh0d2+u5rhUmShubfNCRNMuP/B68/CuP4vjMfuEJ8LkNMDPP/ej00ctjI9grTJA2VG1ahZFXja7j/Ex8OLFutOFHi9eD2Q36d/noRkNHDmuCSVhZkiUttezajZQ3jh+0GDTp3nc2je/LBWVyYOGgW89a+nuJk9E91aNqhjsJEicNnvws/c+YMx44dI1euXG8cL1SoEFu2bPniYP8vPDz8raLSq/3Q0NCPOvf/zxOJ24GdR+nUYCCZUjWkWJnB/L78NDtPPX3jHJdMGejd1oO1i7vhF7qeLUfm0KZbE6ysrRSljnt6CwtW7JrGoO/K8Wrk697Tj/mt7xi1wcQn27n99Zv2qtVlhdG4VqRyWdP21QPSiySuzZm4lJiXH/JWLuUuKznFsR69X09wvmjxXnVBkoixP80i8OXnp8VypKBEeZkbMC7N/r0bHXP6USZtCF7zFhIdIRM/x6Vff/yNC/eMvV2dbHV4rpVhTnHFPbc7Mye1Rf/yf5EL1pxl7ZJtakMlct82HsTu08b3lBZ6WPTHd/Ia/oU+uyjVqVMnFi1axMGDBylTpgw5cuRg5cqVnDp1ipiY2J+D4OHDh7i4uGBhYWE65urqSmhoKP7+/m+d6+rq+sYxV1dXHj9+HOu5hHk4c/g8PVsNp1P59vxSuR5rRk/k+tETuFhFkNzaONTP0UZHrVIZWPx7e/yC17LnzJ9892M77JMlnlUbM7ln4NTNJTSr+nqOlnV7valevGuc/LsVcWvdkm2meXeK5E2nNkwi5+DowJQuOWmVzZ881s94ePW66kiJ3gPvh1y+HwJAcjsdX7eqoThR4pXRPQMl86QCICwKfhu9UHGixC80KIQVm4yTbOt0MHqSDAGJK90GdSCHqw2WeshqF8yx9VtVR0oS2rUYTeTLPy1rfZWZBq2qqw2UCOn0ekbPH47jy74Yl++H8F3z4WpDJRF1v+rGpfvGwqt/lCV1Bv2EnZOj4lTm64vGK40YMYIpU6awa9cuLl68iKOjI6VLl+brr2O/m/3Zs2eJioqiVKlSpmNly5blxIkTb42XPXr0KF999dUbx8qUKcPRozJ5szCuKHR45VrmftObn6q1YORvezh7O8g0cR2AvRVULJyG38c35bnfSg5d9qRC0zrY2Jtvgapzv1ZcvDKHQlmM88EZNPht8XEaV+qtOJn4XM8e+/LghXE+sHTJLcicNYPiRIlXs85fY20Bae2isQl5pjpOkvHXsoOc9rVl+a3kGNLlUB0n0fpx1DdYvvyLcOdhb0JDwtQGSiKGff8b4S9nGKhYLD3ZcrupDZQIWVpbMXRgQ9P+mIkbiYqMes8VIracO3Ke+avPmfZn/94NG1trhYkSn6pdO/AkeS623HPEN0SjbpUBGAyGD18ovlhkRCSVS/Vg741IVnsnJ1lGN9pPGYuFpaXqaGbps4tSadKkYdq0aQwZMoTLly8TFRXFokWLOHHiRGzmMwkLC8PT05M5c+bg4eFB/fr16d+/P9OnTwcgbdq02NraArB69WpSpEjBtGnTyJMnD9OmTcPBwYFVq1bFSTZhvp7c92FEn6kUzdaKdOnaM3DsVo5d9TN9sgPG1UOK5HSmwfDBjDq4g56L51KzZ1fK1qmIg6ODuvAfKUf+HBy5+hfzJrc0fZISGgWdei/m+/aj1IYTX+zUhYem7UZtaylMkrg1bFLOtL15k3zAEV9m/bqYPfdteRJmRaHqlWUIXxxp+vXrYQcTRy1WmCRpefrQhy0H7gBgqYex0+VDotg2bMr3uDoZ3+7ceRbB9JELFCdKWvq2G8m9F9FoGjyKTka5lo1VR0o0snoUofp3nQC46mfJV5WGc+fG/Q9cJWLTs0fPaFK5F36+fgDkKOlB0xGDFKcyT59dlPL29qZ8+fI0bdoUDw8PGjduzB9//EH//v1jM98b+vXrx6lTp/Dy8mLWrFkMHz6cdevWAfDkyROaN28OQFBQEHXr1qVcuXKcOnWKUqVKUbt2bZlTSrzX86cvmDB4NqXztMPFuRU9Bq1m37lnhEXB7UBrDJoOCytL3IsUpNq3HVmyYiB+/iu48nAVS7ZO4tsBbRNUT5WU6VxpNLg/3Rb/Qf6sryeYvHgvhCJFvmfxTFnOPjHYue31BwGVq8m8UnGlVGHjv22DBkvnrlOcJukICwzk+tHjgPE1za1gfsWJEp/Gnb4mraPxz8GbT8I5/M/JD1whYtPQvjNNPbW/rpwT5zQyIXRscUrpRK/O5U37Awd6KkyTNEVFRvLNN7NYcTs5ex8no/J3XUjhGn8LZSVWDimS03rcCPQvp7XZMftPbp6QxSlUePHgEQt6DSAq3DhPXdmGNZm2dKTiVOZHB3zWWpHNmzdn5cqVbxwrUqQImzdvZt26dfTs2TM28inj6OhIYGAgTk5OBAUFqY4jFLK2taZwhVJkKFacHCU9SJs1CxY6je55npuGO/zb8xAD1+74ceHifc6cvMqOTYe5e807XrKmcE5O625NiMiQj1ylS5j+Z5U3RThl0wQzfd4+hvScFC9ZRPxIkyE1j+4vQK+DxwHRZEjR8MMXiU9SvEJRju39BYDrj8PJnb6p4kRJi8fXtWk5ZigA+5esZMP4aWoDJTJ7zy+kfAEXAIZO2smYATMUJ0p6dp9ZQOXCqTFoMHjKHsb3n6o6UqIwd/VYvmlcAICztwMpmq214kRJV+MhA/iqeSMALu09yIJeAxQnMm8HLi8hMFkGrgbYcuPYSeZ27YMmw/aUKlClAj1njKKRezDOtjFMnLufn76bqDqWch9bU/nsotR/cXNzY9u2beTNmzc2HzbeSVFK/BenNKkpXq0Mg/vXo3Cu1KSwe/9wkrV3nLj6OBKfW9743n+AfVQQ7i4WXL96j+uXbvPg9iMe3XvyyWPArW2tKVAsD19VLoZHidyULOZO1rS2WOrhr5sp8A03jmmOCA3l8Mo1HF/5N08fylw4idGd52vJnMq4YmRm9295cOeR4kSJy7TFw+jdtjgAc1eeoVuLYYoTJS22yRyYdHgzeV000lsFkz9dQ6KjZWGG2GCf3IkTt1eSK2U0EdGQ1qUlwYHBqmMlOWWql2LGslGc9rXjtrcP4+o2wyCLj3yRjO4ZuHZ9DnaWoGlQqdYY9u+Qodeq2Dom46cNy3FKbSyAL/tpKKe27lacyjxNXjSUvu1LAHD0AdQo0oEg3+eKUwmAsXN/ZmDX0oDxdaf3oJXMGr9EcSq1PramEuszcd29e/etScaFSEwCnz7jn6Xr+WfpegDye+SlRoPylC2Xn/y5XcnsYoPVv3pQPQ+3wM4xGVkKFyBL4QIUSBlG1QwhbzymQTOueBQSaSAyykBUtIHHfpEs2vMYLcaAhZUlFlZWtC3ngltqG5zsLLC3Mq7Y8y55U4Sz/lQQx9dv5tDy1YQGBMbRd0MkBPuO3aHkV/l4EGJFpgL5pCgVy6pWymfaXrZQVm2Kb+HBIZSwfYRHOidAR8uuDfjr9zWqYyUKxerVYvujFBx4GsOjA7ulIKXIoZ1H+f3vC+QpWxrnjOkpWK0SZ7fLG/Yv8dvCQdi9fJez96yPFKQUCw8KZt2vU+gwZQyFncPpvKYPRQpew1vmQPok1RtVplfbEqb9xTNWSUEqAfn527HkzDWLRhUyo9PBtLHNef7MnxULNquOluDFek+pxEJ6SonPZWtnS/lapSnxVQHyFXRn940YXLNnJVX6dACUSRtCidQfXtnIJ8ySZbdSvHGsubs/6R2i//Oa5yEGTl32YcEfW/n7zw1vrUwpEqd8FcvSaYaxi/D+v1ayYcI0tYESkZQuKfDx+QtLPfiHaaSyj/3VZcWHDRjbjfGDagOw++RjqhfvqjhR4jBg3VJcs2cFYPzXLXjqfVdxoqQrW/GidF8wC4CHV64zpVl7xYnMV5osGbl8ZS6pbA1EGaBQkX5cPX9DdSwBrD2ygAalUgNw9NIzvsrfSXEi8+GcNhWXby4kdTLjJ98b99+hQYVeilOJ/6e3sGDv+UWUzZsCgMgYqN9kIjvW71cbTBFlPaWESOrCw8LZudaLnWu93jhubWdLynSufFWxMKW/ykNWd1fSpHEihZMdjg7WONpZYm+tw0oPFnqIecdovhhNR7QBgiM0/EOi8PUL48YtH06fvM4/Ww5x9ujFeHqWIiG5ffocBoMBvV5PNo8iH75AfLRWXRuY5o47dv6x2jBJ2LzJyxn1Y22sLaBMoXRY21gTGRGpOpZZy1K4oKkgdevUGSlIKXbrxGnuXbxM5vx5yZQ3BxUbVmXvOukt9TmqfdeFJbdSUjhVGN6nTktBKgHp12EU1S78hoMVlMqXmq59m/PH1JUfvlCwbvcUU0Hq/vMoWtSKu8XFxOczxMRQvURXjlxeRKHM9lhbwJpV/alaNYij+2Uy+v8iRSkh4klkWDg+t++w7vYd1i1Y/95z9Xo9tg52GNBhYWFBdFQUMVHR9I+OlokMxVvCAoN4dPUGGfPmIl2u7Ng5OREWKEM2Y0Px8kUwaKDXwcb1h1XHSbL8n/tz+oY/pXKnwM4K2nZrxJ/TVqiOZdZa9myBHg0DOo6t3qg6jgAOeC5jyoKfKeoSztezvyWbFKU+WYbcOSlapwYxGuy/GcnYliNURxL/cueaN5Pm7md4T+OqiOPHtGLtsp34+vgpTpawjZjej7L5nQGIiIFG9UcQHhqhOJX4L+EhYVQs+i3HLy0gR1or7K10bNs+grJf9efSWSmSv8s71g4TQqhmMBgIDQohPCiYEP8AIkJCiY6MlIKU+E+3Tp3BQqeR2TGaqo0qq46TaNzUZ2b2lVSsv2XL8nnrVcdJ0laueN31vU37qgqTmL+0GdMwokMBuuR6QWFHP87t8vrwRSLOndnpRQ6HYJJZGXBPa0uLTvVURzI7dX94vfr3rrkLCQ8Oec/ZQoWRfSZz+aGxoJLcTs/KreMVJ0rYvqpakoE9Kpn2h4/fwqlD5xUmEh8j4Lk/FUr04IG/cdGK5HZ69h6YSOasGRQnS5ikKCWEEImAY6gP3fM8p6l7IF06V1MdJ1FIm82dlOlciTTo2b77Iv7PA1RHStL+nLac8JdT6pXMnxY7B1u1gcxY/1++wcoCHKw0wh7dJTpCPnFPCDSDgTkL9pr2h/3SWl0YM9Sg/deUKF8UgOcPHnJ41TrFicS7aAYDrZuOJOLl63mlouno/qP8rr+LYwpHVq0ehLWFcf+fU4+ZMHiO2lDioz2595hKZfvzLMQ4x+/DSAcajx2FnZOj4mQJjxSlhBAiEfhnw170L1djLJY/vdowiUSecq9Xkr1y4IjCJAIgOCCYk9deAGBrCe17NFGcyHy1bFTctD3l12UKk4j/N27g7/i+fAOTO6MDDVpVV5zIPOgtLPhtakfa5fCjYrpg9syZT0xUlOpY4j+cO3KeSfMOmvbHj2pOZvd0ChMlTN/8+hNO9lYA+ATG0LjqD4oTiU9169JNqlQezN47Ov555EDGfHn4dt5v2Dk5qY6WoEhRSgghEoEn95/wyN/4sWO65Jaky5xWcSLzl6dcadP2lQMyn1RCsHzZXtN2qzYyTPVz1GtZnfQpjB+733kWyZ5NBxQnEv8WFRHB3MWv36yPHNNBXRgzMmh8DzKmtMRCB6n1QRzftFN1JPEBw3pM4Mwd4/BKB2sda7aNU5woYSnZqB4ZylZh6a0U3PTX06zprwT6y4rw5uji8Qu0rNKTQF/jB2uZ8ubmOylMvUGKUkIIkUicuvAQAJ0OGretrTiNeUudzplxbbJQPUMQKUIf4nvvgepIAlj02yrCXg758MiTGhtbG7WBzFDv/s1M20tWHlKYRPyX0f1n8CLMuJ0/iyO1Gld6/wVJnINTMvp1fz3P3M9DlmKQOTgTPE3TaF5vCMEvF1LN5u5Cufoy/QBAxry5aPizsVdUaLSe5q2mcWDnMcWpxJfwuX2H2Z16EOj7HIAiRbNx7Y4nhTxyK06WMEhRSgghEondO0+ZtqvWKKYwiflr820jHK018qWMIHXUU9VxxEthwSGs3X+ffx46sOhGKtyLF1Udyay4uDpTrpCxF2VENEwZ8afiROJdIkLDmL/sqGl/zPjOCtMkfBP/+JGUdsbx6xfuBrPk9zWKE4mPdfPiTUZM2s75F7YsuZmSyn1645AiuepYSjm7pqb95LFY2Rg/dDmwdBVntu1SnErEhqfed5ndqQd2Yb40yRJAxpSWHDg4gYo1SqiOppwUpYQQIpFY+9c2NONUJBTLL6t7fIm6X5c0ba9fc/A9Z4r4Nm70cs772REWo6dwDVmF71P0H/mNacJcr1MPZfL+BGxUv+n4hxu3C2dLTpW6ZdQGSqAyuKWjfeMiAGga9Ok+U3Ei8ammDJ7FjBXniDLocErtQvNRQ1RHUkav17P7yG90KuOArYWBO+cusGnSDNWxRCx66n2X33v+jH+YsTdnMhsd2zYPoVGbGoqTqSVFKSGESCQe33vCw5fzSqVPIfNKfS6dTodH3jQARMXAyj83Kk4k/u3qwaOEBQUDkL9yeSytrRUnMh+tm7wutk4bv1JhEvEhIYHBLPr7de/X0RO6KkyTcM3w/Bk7S+P23rNP2btVhqSao1XDfyXouXG+nXwVy1K+TbMPXJE4zVszhkJZkuHuGEX9jC9Y/MNgYqKjVccSsezi8QuULt4H7+cxANhY6ljh2ZPvB7dTnEwdKUoJIUQicvriI8A4r1SjNrUUpzFP1RpUwNHGOBTk8r1gQgKDFScS/xYTFcXFPfsBsHNMRq6vpNv7x6jbvDoZUhrfvd99HsXOdV6KE4kPGd57Cs/D4LSvLWcMWUiVUVZW/bfCpfJTt1wWwPgBQs9O49UGEp8tyPc5ywePAsDJKoZlczrQqks9xaniV/dBHejYID9g7PU3ZMgSAnyeKU4l4srda96ULPANFx9GAGCphymjm7Jw9Uh0Op3idPFPilJCCJGI7N5x0rRdrYaHwiTmq3nb112od++5oDCJ+C/ndu4mg30UldIFM2O6zLfzMTKVLM3JZ3aERev4a7msJmkOgvwDadN/PfueJCNMs6Zql/aqIyUoM+f3x/LlO5m1/9zkytnragOJL3Lt0FGubVpH6+z+pHeIYc7Mb3DLmjQKsRVqfcXkUY1N+3+uu8Ci36Q3a2Ln+/gZpfJ0YP/l10Pp2zcuwuELf+KU3EFhsvgnRSkhhEhE1v61HYMGLyIs0Cd3Vh3HLFUok8O0vWT+JoVJxH+5efQUNTMEUNg5nHL5XcjgJkNV38c2mQOZylTggI8Dv520ZsKQOaojiY+076+VhAUal4H3qF8bl8wZFSdKGHJ4FCJnNuMw65Ao6NNxjOJEIjYsGD6VW0+Mk6kls9GxZc8k9PrE/XY1k3t61qz+CZuX8/0dvvyCro1/VhtKxJvQoGAqF2rP/I3XMbycF7ZkvtScub6IZKlSqg0XjxL3v3IhhEhiHt17zMitgXjeSMk1MmGbLGl90vKlMmZJj3tq44o3PoExnDsqPaUSoqjISP45dg8ACz30GdxBbaAErmidGtjY2wFwYvNOggNkSKq5CA8KZu/i5QBYWFrydR+ZWwqg5vc9WHIzBXseOfDbgkM8feSrOpKIBdFRUTSq9TOBxhFN5HVzZPGGsWpDxSFrW2t2HZpOKnvjW/J7L6KoVaan4lQivhmiY+ha/wf6jt5K+MspxC6Hp6b1uBFKc8UnKUoJIUQic/6gcQif3sKCbB5FFKcxL22+bcCrofyHT99XG0a81+xpq03bTRoUV5gk4SvdtIFp+8jf65XlEJ/nwF8rCfd/gYdLKDN6F6VBq+qqIylVtE51shQqgIaOHSeeMLTnRNWRRCy6ceE6vQeuMPUaaVU3H31+TpwTQG85MJOc6WwBCIqAWpUHEuQfpDiVUGXGsNnUaTqVnTfheqANtsmSqY4Ub6QoJYQQicyNY6/nlcpeUuaV+hRVqhc1ba/7e7/CJOJDdq/z4mGAcUnlLKltKF62kOJECVPtZtWoXzYD9pYG7p67yOPrN1VHEp8oIjSUmIsHKOcaip2lxviJnVRHUsbazpY6fXuY9jdOmI4hOkZhIhEXFk9bytw1F037E0Y2pVzVxPX3zNh5Q6jikQ6AaAO06zyLK+dkXrSkzmv9Hhp5tGbJT8OZ3+MH1XHijRSlhBAikbl18gyGGOMf6YXLSQ+Sj6XT6TgU5MqyW8nZ/8CK1Ys2q44kPmDTjtfDK3sPbK0wScL1488tKZs2lC65XhB59bjqOOIzje43nachxq4jOdI70K5bQ8WJ1BgzdxA53FIBcHnfIa4dPqY4kYgrPZsP5uCVQACsLGDd+iGky5hacarYUaxuTexK1+ayn3G6gGGTdrJh6XbFqURCERoQyJmtOwnx81cdJd5IUUoIIRKZ8KBg3KLu0ja7H8PrOJG7QHbVkcxChjy5cHR2xifMCs+1ZwkPDVMdSXzA9LGLTUM86lTKlSSXUX4f10xpKV3A+CYuMgaWzlyhOJH4XJFhYUybs8e0P2pk6yT3+17AIy99WheldTZ/KqYNZOOk31RHEnFIMxioV74X9/yMPWJTOVjgdXQG1tZWipN9mVxflaT5yMEYNB07Hibj+wlejPtphupYQiglRSkhhEiEIp49wcXW2FuqWcfaitOYhzzlvzJtXzlwRGES8bGunbvO5YfG4mEKez1N2svv+r/9NLorVi//0ttz/L7MVWLmJgycYRqymsnFhr5DO6gNFM/++GsglnrQ6eDO5es8u3NPdSQRxwJ8X9Dg6xEERRoLsE8tUlOj93eKU30+twJ5aT91LBZWlgAcWrGW336aojiVEOpJUUoIIRKhLesPmrarVi2sLogZyV22lGn7qhSlzMbKvw+btrv2+FphkoSnRYNipu0Jo5YoTCJigyE6hrETN5r2B//UADt7W4WJ4k/rbo0pmdu4PHpwJHRvM0pxIhFfzh48Q4fv5rP1rj0nfO2p2L4VZVs1VR3rk9VtVpWTx8aTLqU1AOd3ebHuVylICQFmVpT69ddfefr0Kc+fP2f8+PEf1W05W7ZshIaGxkM6IYRIODYs3UHky7lfC+V0URvGDLjnzMwP9TNRzCWUqMfe+Ps8VR1JfKRZv3oS8fJ3vWCe9KZPoJO6ph3rkdbJAoCbPhEc2H74A1cIczBn7AIuP4oAIKW9nkl/DFCcKO7Z2dsyZXxb0/6MPw/g80Beo5OSdQvXM6rfNNN+/Z++p0ht81mFslqDCqxa2gdnO2jqHkDQ9QssHfQLmsGgOpoQCYLZFKX69etHq1ataNiwIY0bN6Z169b069fvvddkzJiRzZs3Y2dnF08phRAiYQgLCeX6I2NB3tFGx1dVZMLz92n7XQMyJ4umvGsobviojiM+gf8zP/7afZ/V3k4s9XYmZ+mSqiMlCH1+aGza9lx6QGESEZs0TaNfn7mmudQ6NS9BZvd0akPFsZlLhpHa0Vhg9X4WydBekxUnEiocW7uJXXMXAqDX6xk2cyBT5/+kONWH1WxUkfWr+mP78vOSe8/CmfrNAKIjItQGEyIBMZuiVJ8+fRg2bBiHDh1i7969/PTTT/Ts2fM/z69fvz6nTp0iQv7BCyGSqAOHXy8t3Lh1NYVJEr5atV4Pc1q9wkthEvE5pv26jPsh1mjoKF5f5pXKlicLJfM6AxASBdNH/qk4kYhNO1fvYu/55wDYWMLUPwcqThR3ipQqQNv6BQAwaND1m99Mq8uKpGf7zD84tGIN2RwjqJslhD6dyzJ1fsLtLdjym/qsX/UDdi/nZr/pE0mZQt8Q8CJAbTAhEhizKEqlS5eOzJkzs3//ftOxgwcPkiVLFlxdXd95TZ06dRg6dCh9+vSJr5hCCJGgbFi117RdoVxedUESOCtrKwplNy4xHhYNG5ZsU5xIfKqrh44Q9PwFAPkqlcMhZQq1gRQbMu47LF7OcLB1322CA4LVBhKxrkeH8YRE6Tj21I47TgVIkTaN6khxYtHyQVi+fLey6cA9/tmwT20g8T/27jwuqur/4/h72EHBDWVxQcS9RStNS8q0zTTTstJWs2y3b2WWlpXftm+lLdpqZZlaWflV61tW1s9wTzMTMzcQFUUWBVF2ZDm/P3CmCFBUZi4Dr+fjcR7N3Dlz7/s2DMN8POdcyy38z6vK2r7J8TvuwTsu1FuzHrc2VCXGPDFSs6aPlk/ZID/Fpx3R+WfdrcwDmdYGA2ohtylKSVJycrJjW1pa2fSKVq1aVfqcu+66S++//77zwwFALfV/Xy9VblHZ7a5tG8nLy9PaQLXUNbdc4RhWv3HHIRUfKbI2EE5YaXGJfvv6O0mSl7e3oq+70uJE1vHw9NQV/cuK0KVGen4ifwvVRdtjt2rUpP/T6v0NJF9/DXr4Pqsj1biHnr5DZ7QNlCQdLpBGX/+UxYlQGxhjNHLAvzT357+uvnjfredrwU+vyNOzdny1nTrzCU17/lpHQTV2d656dr1d6Snp1gYDaqna8c6V5Ofnp6ioqEpbw4YNJancVDz7bV9fX0vyAkBtV1pSos27yoaI+3lJA4b1szhR7XT9TRc7bn/33W8WJsGp+PWrb9WxUaGubXtYc1+/Th4eteZPHJc67aJofbknWEv2NdCPGzK06dfNVkeCkyx6+0PlHir7HX/2oMsV1fNsixPVHL+GDdRuwGCl5JX9i8FzryxSRtpBi1OhtjDG6OZLH9CcH3c5tg29pJNWbfxAgUENLMvl4empH9a8q3/ddp7s1+Na8edBndf1NmUxZQ+oUq35i61Xr17asWNHpe3cc8+VVL4AZb/NlfUAoGpLl5d9IS0plc6/mAWgKxPdM0JS2aiSWW/PtzgNTtb+XYlqo/1q3bBIjQM8dPM9Q62OZIk+N1yrolKb/sj010Nj3rE6DpwoPytb370x3XH/rhcfU7PgRhYmqjlXjh2josAQfbGzkd7/KVWvPTX9+E9CvWJKSzXy8n9pyqzfVXL0InbnntZC2xNn65xeXVyeJ7BZU9357msyLTs7tn2xZLf6db9dhfkFLs8DuJNaU5RatmyZbDZbpe3TTz+VpHLrR9lvp6SkWJIXANzB7Pe/0YLdQXpnazPtLGlmdZxa59wLuqt5w7JpjTv3F2rvziSLE+FUzJq91HH73vuvsi6IRUKiItWhVw9JZUW6+DXrLE4EZ1v736+VtOlP9Wqep4f7NdCHXz5jdaRT1qFXD5133VBJZVeSfXwk0/ZQtfG3TdJDz3+vgqPr34c29tGqlS/r1vuucVmGrn2j9cj8Oep43rnacshPa1J9NfHVn3XDJQ+wMD9QDbWmKHUsKSkpSkxMVHR0tGNbdHS0EhMTlZqaamEyAKjdtqz7U3/sPKxiY1O7s7vL08vL6ki1yq13D3HcXrJ8u4VJUBOmvzxLOUfKbvfo3Eyt24ZZG8jF+t1yneP2qs/nyxhjYRq4gjFGy997Xz2C8+TlIQ2+KEoXDzrP6lgnrWnzJhrxzF+LVi96/R1lpvC3Po7t7UnvaNC1U5Vqv6aDzUPnj3lIg8c9IC8nLvVyVq+uWrByuu54a7ICm5VdMCXrQLpuGvSoXhz3utOOC9Q1blGUkqR3331XL7/8svr27au+ffvqpZde0rRp0xyPBwcHq0ED6+YQA0BtFb+2bJ0k3wB/tTnzNIvT1C6X9Pvr/8cnM761MAlqQl52rn5as1eS5OkhPfrM7RYncp2WEWH6aOIFurRlthopR7/97zurI8FF1i9bp0+/j5ck2WzSRx8/Im9v9/wHiPk/TtG/+vorxL9IO9b9rl++XGh1JLiJmK+W6KzT79Yv8blakRag3BJvXTTyRj321afqPuASeXrW3MVewls119xFL+vX1S9raJ+W6hhU9q8hf/68TK8Mu0W7ft9YY8cC6gO3KUpNmTJFX3zxhRYuXKh58+Zpzpw5ev31vyrQ69at07hx4yxMCAC10461fy3e3bF3TwuT1C6+DQL0U0YLLU5qqLV7SrTqx1+sjoQa8PqLnzluXze0h4VJXOvJl++Rn5d0epNCBR+KV0FOrtWR4EL/unmSUrPLRsa1DvbV23MmWpzoxI179k717R6iZn4lujrisBa9/Bqj/XBC0hKTFd35Rr3wxAwVHb0oVrNWLXXXa88o9dB8vTFzgkLDmp70/rucHqm5303Wrt0favjArrJf7O/Mxjn6ctJ/NPPBCcrNPFQDZwLULzZJ/LavRGBgoLKyshQUFKTs7Gyr4wDASWsc0kJzf/tCbQOPyJafrc5h11odqVY489J+GvnafyRJKz+bp4UvvmZxItSUuLQFat/CW5J0zU1v6qvPfrQ4kXP5+Poo7dB/1cjPJmOk8y95Vmt/Zj2p+mbY6GH6/P3b5Gkru3DDxZc/q2U/ucfPwRnndNava6bI9+gAr39PW6pnH3rV2lBway0iIzR0wsPqdH4vXRiao3OCyxYbLy41Wrc5Tf/9fJm+nP2D9iWlV7kPm82mrmdE6qY7r9JVg3uqa0RQuceLS6VvViXpnhv+rQP70px6PoA7qm5NhaJUFShKAahL9hz8Sq2alA1dj+p4v3bF77E4kfVGPP+Ueg4ZKEl6/+6HtH31WosToaZMevNRTRpzoSRp9Z8HFH1G3Z7G9/hL9+qF8WU/y7G7c3V25AiLE8Eq85a9q2EXtpIk7TtYqKiwETpypNjiVMfm7e2lrXvnql2InyRp9dZDiu56i8WpUFd0PO9cffzJo+oV6SebreLjh3KLlZSWo9T9h7V4RYLW/ZmigEZBahwSoosv7Kw7Lgiq8JziUmlJbLqeeOgtbVix3gVnAbin6tZU3Gb6HgDg5K3+PdFx+6a76t9Vyf7J5uGhLheULQZckJurHet+tzgRatKrT013LHje+7TmatehlbWBnOy+uy513J780jwLk8Bqo4ZOUMrRaXwtm/rqk69fsDjR8X04/3lHQepgvtG1lz9qcSLUJXG//Krzo67TJde+oe/WZyi3qHxlqnEDL53errEu6R2hq265UkMefVCX3jVKPYcMVEnziHJ9M/JtmrN4p7r3fkJXnDOKghRQQyhKAUA98NW85Y7blw84x8IktcPQmwboqtO81bbhEe1cu04lRUVWR0INyjmUrR9WlRViE3O8dd6QARYncp6b775aLZuUTVVMPFisz9+bb3EiWCkn87Duvv8DFZeW3b92QFfdMeYaa0Mdw12P3KCbB5ddcKLESHeN+VCpe7naHmpezIKfdGWP2xQeerPumvi1vlmbpp0HpYKSSoZPHZVdKK3fW6RPFu/UkFEfKqzxNRo54EFtWbfJhcmBuo/pe1Vg+h6AusTX30+ZWfPk5yXlFRkF+Q1VaWmp1bEssyDmDQ29KFKS9OQrP+k/j75hcSLUtK49z9Ajn7yrQ0c8tX9XoiYPuaFOLpq8cddnOqNtoCRpwsuLNXnCWxYnQm3w9vyXde81XZVR4KlP1+Vp/BW3qjA3z+pY5fToc4ZWLP2PYx2pGV9t0V1Xj7c2FOqdhk2bqHWHtorq1EY5BSU6dDhfeVlZys7I1IHEvSo+umA6gBPHmlKniKIUgLpm3Y7PdE5U2ZfXQde+ru/n/2xxIuvsz/5KwQ09VWKktu3u0b7d+6yOBCe498O31P7cspGB7931L8X94h6LPlfXBZf20rIfn5QkZeYbhTa+VkVHjlicCrWBp5eXPls9W6kBbVVsbIpdvERzxj1pdSwH/wYB2rnvU4U0KqtIrduRrd6dbpapx/9YAgB1DWtKAQDK+TnmT8ft6266xMIk1urV9ywFNyxb9D0hrZCCVB226vO/prL1GTHMwiTO8e+XRjtuf7pwAwUpOJQUF+veqx9R9uEcSVL3yy/WJXePsjhVGZvNpmv//bg25jRWqZFSs40GXfggBSkAqKcoSgFAPTF35iLH7QvO72BhEmvdevcQx+0ly7ZZmATO9mfMch1OOyCbjAYPPle9+55ldaQa06BxI+U3iVBmoYfyi6Vnx71pdSTUMgf3pWjuxGcdU7UHP3CnHn72LotTSYMevl/dB1yizZl+mhfnq2uvfVHpKQesjgUAsAhFKQCoJ2JXb1RadtmXk8gWfgoJD7Y4kTUu63+a4/acD76xMAmcrbS4RKmrf9aojpkaGpmrl16/z+pINeaCm4dre26gPo5voken/aL0lHSrI6EW2rx0pb6b+o4aeJXo+naH9OpTg/XwxFssy3PBTder36ibJEmlJSV69q5ntPrHXyzLAwCwHkUpAKhHfv0jWZLkYZOG3z7Y4jSuF9mhjaKOXno8LbtUa5astTgRnG3+u5/K21a2fGZ091B1Oj3S4kSnzi+woaJvvE6SVFxUrM9e+dDiRKjNYmZ+Kp/EDQrxL5EkTXnuet398HUuz/Hp/17UUy/f67g//4VXtHX5KpfnAADULhSlAKAe+fSTn7U8NUCz4xtrb0kTq+O43J0PX++4vXTNLguTwFUyUg7om+Vlr7WHTXrh9QcsTnTqom+4Vv6BDSVJv339nQ6lplmcCLXdnYPHKmZzlqSy98Fbr9yqW+++ymXHn/vtS7ph8Om6pGWOTm9SoB/f/VBr5n3lsuMDAGovilIAUI98PWeRfk3xUUahlzr16W11HJcbPOgcx+1PPvrewiRwpafHvqWio2soX9mvg4JbuG9BNrx1iF597loF+xarpLhYP384x+pIcAMlRUUaeO5orY7PlSR5ekgfvXun06fy+fh46cdf3tbwQX9Nm96yfJUWvzPDqccFALgPilIAUI8U5uZpV+wfkqTmEa3VrFVLixO5Tkh4sDq3KhtdkpkvffflTxYngqvE/7Fdy/4oW3PJx1N69jX3HS318jsP64wWRrd0OKTGyRuUkcTVI1E9hXn5uvScO/TbrgJJZSOmXn3+er3x4WOy2Ww1frywlsH6PW6WLundRpJkjDT1s1g9fOOTNX4sAID7oigFAPXM9lVrHLe79u1jYRLXCj3tdP13dyOtT/fTohW7uPx4PfPM4x+qtGxpKd107Tny8/OxNtBJaB7aVNcOKBtxUlIqvfMCa0nhxORn5+qis27X0i3Zjm1jbr9Av/zxgZoFN6qx4wwdcbG2xn+orhFBkqSiUunfb6/U2JueqrFjAADqBopSAFDPbF66Us39itW7eZ5efGqo1XFcpmu/C5Wc563lqQ31wrOfWR0HLrbqh5XasDtPkhTo66GJf1tw2V289sF4+XqV3V72R7piV2+0NhDcUt7hbF121m36YlmyzNFC7bmnh+g/X05Vi8iIU9p3k6aBWvDTK5r/2UMK8i/7mpFzxKZRD32m5x54+VSjAwDqIIpSAFDPpCXs0iUhB3VeSJ7O7xSoiHbhVkdyOk8vL53WN1qSlJ+dox1rf7M4Eazwn+c/d9wec2d/+fp6W5jmxLSNaqnrr+gqSSoulcb/6y2LE8GdFR85ohv73aPxry1VXrFNO7O8ldO8kx6ZP0eDHr5PDZo0PuF9drngfH2+YoaGXtJJ9tmA8QdK1Oeiifrszbk1ewIAgDqDohQA1EMrf9vjuD3y/mEWJnGNqJ5nyz8oUJK0dfkqlRQXW5wIVlj40UJtSiqUJAX6eWjkgzdanKj63pg5Qd6eZbd/+i1V61estzYQ3J4xRq+Me1WXDn5Z/12fI8kmL29v9b/9Fj21eL5id8zWC6/dp86dW1W65lRQUIC6dOug6Buv09gvZ2n0O69qp0crFZdKR0psmvXDTp3Z9kZt+oURfQCAqtkkGatD1EaBgYHKyspSUFCQsrOzj/8EAHAj194xVF/OuEOS9Nv2TJ3b+VaLEznXrB+mqeUZZyghy0dv/+sp/fFTjNWRYJGrRw3V86/dpzX7A7T5jwS9fv1tMqZ2/yl0xjmd9Puvr8jTQzpSIp1x1ljFb4q3OhbqEG8/X11y1yhddNuN8vL2VmTgEQ2NyHI8nn+kVPsz85Wde0Q+3p5qHOir4EY+Ssjy1bd7g8rtKyRvt9566m2t+XGlq08DAFCLVLemQlGqChSlANRlXt7eOpizQA19yr7kNg68VgX5hVbHcgofH28dylkgP2+poFhq1mSE8nNyrY4FCz342Ydqc0bZVLiPH5qgTUuWWZzo2JbFztAF3UIkSV8s2a0bLnHfqweidmsU0lwX3XaTHvvXAJ0XXnTc/tlFHpqxvakkac+mLVoyY7Y2xyyv9YVeAIDzVbemwvQ9AKiHiouK9HvcQUmSj6d0wx2DLU7kPCNGXyW/o0sHxe44TEEKWvzuDMfty++/Ux6enhamObZB1/ZzFKTyiqRH7njR4kSoyw6nHdDXL09V/w7X6Jq75mjeihRt2V+qrCMeKv7bBUsLSmxKzfVQbEKWYmZ8rMlDbtC0G+/Qnz8voyAFADghXlYHAABY45v/rdWFp18hSRo24iLNfOu/FidyjltGXea4PX/BaguToLbYtuIXJW78UxHdTlerju101xN3aPpz71sdq1K9rh+mtHxPhfiX6MP/xio5MdnqSKgHCvPy9NUHX+qrD76UJDVo0lgNmzZR4+AmKsjLV1ZGpg7vP6DS4hKLkwIA3B3T96rA9D0AdV3zliHat2eGvDykQ3mlatpgiNWRapyvn48ys/4rP2+bCkuk0JBbdDjjkNWxUAtEnnWm3vjqDV0Qmit/z1Kd1vU+7Yjba3WscjpfcJ7ufOc12WQUXrpft51/gwpy862OBQAAcFxM3wMAHNOBfWnanlIgSWoc4KF+A3pbnKjm3XLP1fLzLrtq1Pr4QxSk4LBrwx/y3L9TQT6l8vaUpn/yhNWRyvHw8tSQRx+UJBnZ9PKEtylIAQCAOoeiFADUYz8u+dNxe9R9Q60L4iQ3j7zUcfvLL1ZYmAS10f23vai84rKiZf+erXTxgF4WJ/pLv9tuVIvICEnSrt83asP3P1mcCAAAoOa5VVHqxRdf1P79+5WRkaGXX35ZNputyr69evXSqlWrlJ2drW3btumOO+5wYVIAcA8z3pin7Yd99O2eQG3Ma2Z1nBrlH+CnXqeXLRBdUCx99PpcixOhttm1OV6fLtrmuP/h7Efk5WX9oueDhl2kT6eNUKh/kUpLSvTVy69bHQkAAMAp3KYoNXbsWN144426+uqrNWzYMN10000aO3ZspX1DQkL0/fffa+nSpTrrrLM0adIkvfnmmxo4cKCLUwNA7bZ1/RbNWLJf8Vm+Cj/tdDUKaW51pBozasy18j16OY912w4q5zDrA6KiR0Y9p5SjPxptmvvrpbcesjSPn5+PPpjxL7UIKNWIdod1cPWPStqy3dJMAAAAzuI2RakHH3xQTz/9tFatWqWlS5dq/PjxGjNmTKV9hw4dqtTUVE2cOFE7duzQF198odmzZ+vGG290cWoAqP02/RTjuH3GxRdZF6SG3XBzf8ftuXOXWZgEtVlO5mE99uTnKj162Zcxo/uqU9e2luV5e86TCm3sLUlKOlyq1x99xbIsAAAAzuYWRamwsDC1adNGy5cvd2xbuXKl2rZtq9DQ0Ar9f/jhB40aNarC9kaNGjk1JwC4o41/K0qdeWk/C5PUHA9PT63NDdWS5AaKz/TU7De/sDoSarFP3/hUMZsOSZJ8PG36/OtnjrlEgLMMvPpCjRx2liSpxEj3j3lfBbl5Ls8BAADgKm5TlJKk5ORkx7a0tDRJUqtWrSr0T0xM1Nq1ax33mzdvrhEjRmjJkiVOTgoA7ictYZcy9ySqfVCh7h8Spe7ndrE60inr0KuHbA2b6I+D/nr+w/XKy861OhJquTuue0pZR8oKUd3aN9ULUysfje0sTYODNHvOWHkcrYXNX7ZX3366yKUZAAAAXK3WFKX8/PwUFRVVaWvYsKEkqbCw0NHfftvX1/e4+50/f75SU1P13nvvOe8EAMCNtS7YrcFtstWlyRHd/dD1Vsc5ZecMHuC4zVXLUB174nbr36+V/awcKbGp4yWXqUGTxi47/tf/94qaNihbZH1XRolGDX7UZccGAACwSq0pSvXq1Us7duyotJ177rmSyheg7Lfz8qoe1t6gQQN9++236tixo6688krl5+c79yQAwE199PZCx+0rLj3DwiSnzjcgwLE2Vu6hw9q6fLW1geA2pj3xhj5dvl+fJDRWYmmwbnjhKdk8nP+n0rSPxqtPt7JR4QXFNt04/GXl5zC6DwAA1H21pii1bNky2Wy2Stunn34qSeXWj7LfTklJqXR/gYGBWrx4sU4//XT1799fO3bscP5JAICbWvvzWiUdLpUktQn2VdczoyxOdPKemnyvBrQrVph/kWJ/+D+VFBdbHQluwhij+655VEkphyVJXS44X1c8cLdTj3nrPVfpgVHRR48vPfNmjNYu+cWpxwQAAKgtak1R6lhSUlKUmJio6Ohox7bo6GglJiYqNTW1Qn+bzaYFCxaoXbt26tu3r7Zs2eLKuADglv5vxV/F+wcm3GxhklNzy4jz1a1ZgUZEHdaB2F+tjgM3k51xUJ9O+LejmHnx6Ft01+O3O+VYjUND1G/0KBWV1YP15bIkvTz2VaccCwAAoDZyi6KUJL377rt6+eWX1bdvX/Xt21cvvfSSpk2b5ng8ODhYDRo0kCTdcccd6tevn0aPHq1Dhw4pJCREISEhatKkiVXxAaDWe2/qPMftq67oZmGSk9en39lq3cxHkrQns0QrFi0/zjOAiuLXrNP/prwhD5vRJeE5mv6fq/XurAk1eoyGTZvo7ven6aBvqBbubqRfEvJ1y4CHavQYAAAAtZ3bFKWmTJmiL774QgsXLtS8efM0Z84cvf76647H161bp3HjxkmShg0bJk9PTy1atEipqamOtmDBAqviA0Ctt3bJGu3KKJEkhTX21gWX9LA40Yl7YPyNjttf/7DJwiRwdys/m6e8Dct1RtOyC6vcfWsfvTmjZhYfD2rRXPfMeFMtIiMkSbFbU3V5r7tV/LcLugAAANQHNknG6hC1UWBgoLKyshQUFKTs7Gyr4wCAS7z+ybN68KazJEnzFm/X8AHjLE5UfZ6eHsrIWaggPw+VlEqdu41Vwp/xVseCG/Pw9NSHP7ypkZe0dmxb9PN2Dbtigo4cObm1yu761zW6dexIrc1uLsmmQ6lpeuvWe5SZUnE5AgAAAHdV3ZqK24yUAgA431svzVHJ0fVtLruwgzxccOWxmnLDHYMV5FeW98+9+RSkcMpKS0p0xxUP6LOlyY5tg/p30sa4merctc0J7atBAz99ueg/mj5tlM6P8FDP4Hyl703SO7ePoSAFAADqLff5tgEAcLqEP+O1ZV+BJKmRv4cGXd/f4kTVd/+DQx23P/18hXVBUKeUFpfo1kvu05TPNjsWJO8U0VixG9/SWx+OU1BQwDGf7+XlqXsfGqbE1M907cAz/tqed1Bv3nKXMvYmOTM+AABArUZRCgBQzqzPVurXA/6aHd9YpWEdrY5TLRHtwtWzS7AkKfuITe+8MNPiRKhLSktKNP6mCbr3qW+VWVD2p5OPl0333d5Xe9M+U6+rB6lhs7KLqXh4eKh9+zANvSZaH33+lFIyvtTbr9+mpg29JUlFpdJ73ybootNvVU5GpmXnBAAAUBuwplQVWFMKQH3VsGkTPb3kf/L08tLhtAN6/vKrVVpSYnWsY3rn06d1z409JUnzl+3RdRfdb3Ei1FXtunXVh18+o+j2fvL0kLZk+mrxvkBJUnbGQSk/W08PaFDpc+PSjcY9+rG+/ZgLrwAAgLqNNaUAACcl52Cmtq5YLUlqFNJcnfr0tjjR8fXrd6YkyRjp1ec/sTgN6rKdG7eof5fhGnb3J/plV5H+OOjneCywWVM1bl1+rakSI21Pl8ZOWapuESMoSAEAAPyNl9UBAAC1z9r53+j0fhdKknoPG6yty1dZnKhq4Z066LuMcG0qKJJ/dorW/N8vVkdCHWdKS/W/GV/ofzO+UORZZ6rLhX0UedaZahwWIv/AhtqUJqWl52rT5j2aN+sHrftphUqKiqyODQAAUOtQlAIAVLBt5S8qzjyg3h0aaORdZ2jr/G5as3yj1bEq1fvaIZJs2pvro/8+/4XVcVDP7Nrwh3Zt+KPctictygIAAOBumL4HAKigtKREDVL/1AWheWrqV6rHnh5pdaRK+TVsoHMGD5AkFebl6/fvFlucCAAAAEB1UZQCAFTqlX9/qKLSstuXX9he/v6+1gaqxIDbrpNfg7JFpdd/+4MKc/MsTgQAAACguihKAQAqtXtLgtbvKLtShr+3Tf96/BaLE5UXGNRAn752ja6PPKR2gYVa8QlT9wAAAAB3QlEKAFCld9/9znH7vnsvtzBJRROev1P+3ja1bFCspnlJ2r8r0epIAAAAAE4ARSkAQJU+fWuukg4bSVLrYD8NGd7f4kRlbDabRo+80HF/ygufW5gGAAAAwMmgKAUAqFJpcYk+/vwXx/0nJtWOKXy33TdUzYO8JUnx+4v17affWpwIAAAAwImiKAUAOKYpT0xXVqFNktSjc7BO697e4kTSxCdHOG6/9+HPFiYBAAAAcLIoSgEAjin7YKa+jkmQJNls0nNT7rU0z/W3DlC70ABJUkq29MYz71uaBwAAAMDJoSgFADiu58a/o6JS6UiJTUGR7eXp7W1Zlmeev9Vxe/qsFSouLLQsCwAAAICTR1EKAHBcO/7Yrne+3aMP45poY34LnTv0SktyXHVdP3VqHShJOpAnTR7/liU5AAAAAJw6ilIAgGp59fE3VVBS9rHRf/Qt8vTycnmGKa/f5bj94dxfVZiX5/IMAAAAAGoGRSkAQLUkbdmmLctXSZKahoep59BBLj1+eKcOWpMXrsQcb+3PlZ4f+7pLjw8AAACgZlGUAgBU24/vfiRJ8vUs1Suv3K52HVq67NgDH7pX6YU+WrC7kW5/ZJ7ysnJcdmwAAAAANY+iFACg2vb+uUVH4n7X7R0ydX6Eh96f84RLjtuhd091iT5PknRwX4oWfzTXJccFAAAA4DwUpQAAJ+SjZ99S6dHb/Xu1Ub/Lejr1eM2CG+n6Jx9x3P/h7Q9UUlTk1GMCAAAAcD6KUgCAExK/cas+/S7Ocf+9jx6Wh4fzPk6+XPQfjb0kUG0aHNHujZv0+7c/OO1YAAAAAFzHrYpSL774ovbv36+MjAy9/PLLstlsVfa97LLLFBsbq7y8PMXGxmrAgAEuTAoAddujdzyvA3llv4PbtwzUM1Puccpxrr3pEvU7t42a+JZqYOtsff/qNBljnHIsAAAAAK7lNkWpsWPH6sYbb9TVV1+tYcOG6aabbtLYsWMr7RsVFaWFCxfq448/1mmnnaZZs2bpq6++UkREhItTA0DdlJV+UP+e8r3s9aFHHxygzl1r9nds8xaN9cH7Yxz3ZyzcrB0bNtfoMQAAAABYy7hDS0xMNCNHjnTcv+mmm8yuXbsq7du3b1/z+uuvl9uWkZFhrrvuumofLzAw0BhjTGBgoOXnTqPRaLWx2Ww2E7Nlrik135hS8435c+ds4+npUWP7X7XhPce+tx/42vg1bGD5OdNoNBqNRqPRaLTjt+rWVNxipFRYWJjatGmj5cuXO7atXLlSbdu2VWhoaIX+y5Yt08MPPyxJ8vLy0u233y5fX1/9+uuvLssMAHWdMUa3Xfu0DhWWTePrGtlE786aUCP7fmby3Tqve7gkKa/YpptveFkFObk1sm8AAAAAtYPbFKUkKTk52bEtLS1NktSqVasqnxcVFaX8/Hx9+OGHevbZZ5WYmOjcoABQzyRuidfEl39QiSm7f/uN5+nsPmee0j6vvaG/Jo670nF/8odrte7/Vp/SPgEAAADUPl5WB7Dz8/NTy5YtK32sYcOGkqTCwkLHNvttX1/fKvd54MAB9ezZU+edd55ee+017dixQwsWLKjB1ACAdye9pT59z9T1F4Tph6RAXTZhorbferdyMw+d8L76XdpDc2Y/JI+j17H4YeNhPT/mPzUbGAAAAECtUGuKUr169dLSpUsrfezRRx+VVFaA+mcxKi8vr8p9ZmVlKTY2VrGxserataseeOABilIA4ASjr3xYKV99KI+QYDVv20Z3Tn9dH9zz8AkVpoKaNdb8r56Ur1dZRerP1BJd32+MSotLnJQaAAAAgJVqzfS9ZcuWyWazVdo+/fRTSSq3fpT9dkpKSoV9de3aVdHR0eW2bdmyRcHBwU48AwCovwpycvXCzQ/qcNoBSVLrrp31wKzpGnXvkGo9Pziite6dOV1LDzSRMdKOg9LAvmOVcxKjrQAAAAC4h1pTlDqWlJQUJSYmlis0RUdHKzExUampqRX6Dx48WB988EG5beecc462bt3q9KwAUF8dSk3T9Dsf0KG0/ZKky3s014fvjNZvf76v8/p0qfQ5zUMaK/rG6zT2y4/VIjJCu7J99OVmmy6NHqukuJ2ujA8AAADAxWwquwxfrTd+/Hg98MADuummmyRJn376qV599VW9/vrrkqTg4GDl5+crNzdXLVu21ObNmzV9+nTNmDFDl112mV577TWdd9552rBhQ7WOFxgYqKysLAUFBSk7O9tp5wUAdU2TsFA9PPN1Tbi0obz+9k8fe1KytWZdvPanZapFSBP1PreDAgIbas6uYJV9HEkp8Qn6cMw4ZSZX/AcHAAAAAO6hujUVtylKeXh4aMqUKRo1apSKi4v14Ycf6vHHH3c8vmvXLn388cd65plnJJWtUTV16lSdeeaZ2r17tyZMmKBvvvmm2sejKAUAJ8/H309Pv/dv3Tf8DDX2KT1m368TA7Uz21erPp+vRa+/o8JjrBUIAAAAoParc0UpV6MoBQCnLuqsMzRp6kPqd064wgOKZbP99VipkVLzvPTF/+3UG5PeVeLGP60LCgAAAKDGUJQ6RRSlAKDmBDUPVo/L+ur0s9qrWXAjHczI0u9rt2jj0l+Utf+A1fEAAAAA1CCKUqeIohQAAAAAAMCJq25NxS2uvgcAAAAAAIC6haIUAAAAAAAAXI6iFAAAAAAAAFyOohQAAAAAAABcjqIUAAAAAAAAXI6iFAAAAAAAAFyOohQAAAAAAABcjqIUAAAAAAAAXM7L6gC1XWBgoNURAAAAAAAA3EZ1aykUpapg/x+4b98+i5MAAAAAAAC4n8DAQGVnZ1f5uE2ScV0c9xIeHn7M/3kAAAAAAACoKDAwUMnJycfsQ1EKAAAAAAAALsdC5wAAAAAAAHA5ilIAAAAAAABwOYpSAAAAAAAAcDmKUgAAAAAAAHA5ilIAAAAAAABwOYpSAAAAAAAAcDmKUgAAAAAAAHA5ilJ1gK+vr2bMmKHMzEwlJydr7NixVfbt3r271qxZo9zcXP366686++yzXZgUNS08PFzz5s1TRkaGkpKS9Oqrr8rX17fSvl999ZWMMeXaoEGDXJwYNWXo0KEVXs958+ZV2vfiiy/Wpk2blJubqyVLligyMtLFaVETRo4cWeE1N8aopKSk0v6xsbEV+p522mkuTo1T5ePjo02bNqlv376ObW3bttVPP/2knJwcbd68WZdeeukx9zFixAjt2LFDubm5WrBggZo1a+bs2KgBlb32vXr10qpVq5Sdna1t27bpjjvuOOY+MjMzK/weaNCggbOj4xRV9tpPnTq1wmt5//33V7mPBx98UElJScrKytKMGTPk7+/viug4Rf987WfOnFnpZ/+SJUsqfX7jxo0r9D1w4IArTwEn4Fjf5erbZ72huXd74403TGxsrDnrrLPM0KFDzeHDh82wYcMq9AsICDDJyclmypQppnPnzmbq1KkmJSXFBAQEWH4OtJNrq1evNosWLTJdu3Y10dHRJi4uzkyePLnSvnFxcebGG280ISEhjubj42P5OdBOrj3xxBPm66+/Lvd6NmrUqEK/1q1bm+zsbDN27FjTtWtX8/nnn5uNGzdanp924s3Pz6/c692qVSsTFxdnXnvttQp9PTw8TF5enrngggvKPcfT09Py86BVv/n6+pr58+cbY4zp27evY3tsbKyZM2eO6dy5s5kwYYLJyckxrVu3rnQfPXv2NLm5ueaWW24xZ5xxhomJiTHffPON5edGO/HXPiQkxBw8eNC88MILpn379mb48OEmLy/PDBw4sNJ9hIeHG2OMiYyMLPd7wOpzo534ay/J/Pjjj2b8+PHlXkt/f/9K93HNNdeYzMxMM2jQINOjRw/z559/mjfffNPyc6Od+GsfFBRU7jXv1auXyc/PN0OGDKl0H+eff745cOBAuec0b97c8nOjVd6O9V2unn3WWx6AdgotICDA5OXllfvQmjhxoomJianQd9SoUSYhIaHctri4ODNy5EjLz4N24q1Tp07GGGNatGjh2DZixAiTlJRUoa+Pj48pKioyHTp0sDw3rWbanDlzzAsvvHDcfs8880y53wf+/v7m8OHD5X5n0NyzTZgwwcTHx1daXI6KijLFxcXG19fX8py0k2tdunQxGzZsMLGxseW+oPTr189kZ2eX+weln376yUyaNKnS/cyaNcvMnDnTcb9Vq1ampKTEtG3b1vJzpJ3Ya3/33XebLVu2lOs7ffp088knn1S6n4svvtjs27fP8vOhnfprL8ns3bvXXHrppdXaz7Jly8r9TujTp4/Jzc2tsohFs74d67X/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l6MoBQAAAAAAAJejKAUAAAAAAACXoygFAAAAAAAAl6MoBQAA4AQzZ86UMabKNnLkSBljFBER4fQsMTExMsYoPz//mP3effddRz5X5AIAAPWbTZKxOgQAAEBdExQUJH9/f0nS8OHDNW7cOPXs2dPxeGZmppo0aaIDBw6otLTUqVliYmL022+/acqUKdq/f3+V/QIDA9WpUyetW7dObdu2VWJiolNzAQCA+s3L6gAAAAB1UVZWlrKysiRJhw8fVklJidLS0sr1+ed9Z8rJyTlmQUqSsrOzdeDAARclAgAA9R3T9wAAACwQERFRbpqcMUbXXnuttmzZotzcXH322Wdq27atlixZotzcXC1fvlzh4eGO5w8dOlSbN29Wbm6u1q5dqwsvvLDax+7Xr582bNig/Px8JSQk6K677qrx8wMAADgeilIAAAC1xLPPPqvbbrtNgwYN0rBhw7R69Wq9++67Ov/88xUWFqbHHntMknTmmWdq1qxZev7553XmmWfqk08+0ffff6+oqKjjHsPDw0Pz5s3TvHnz1LlzZz311FN655131KVLF2efHgAAQDlM3wMAAKglXn/9df3666+SpA0bNmj79u3673//K0maP3++unfvLkkaN26cPvjgA82dO1eS9Oabb6pv37669957NW7cuGMeo1GjRmrWrJnS0tKUmJioxMREJScnKyUlxXknBgAAUAlGSgEAANQSO3fudNzOz8/X7t27y9339fWVJHXp0kVjxoxRdna2ow0ePFgdO3Y87jEyMzP1zjvvaMaMGdq9e7fefPNNHT58WIcOHarp0wEAADgmilIAAAC1RHFxcbn7VV2Vz8vLSy+//LK6d+/uaF27dtU999xTrePcf//9Ou200/T++++rV69eWrt2rQYMGHDK+QEAAE4ERSkAAAA3s337dkVGRiohIcHR7rrrLl1xxRXHfW5ISIjeeust7dixQ//5z3907rnnasmSJbrqqqtckBwAAOAvrCkFAADgZl5//XWtWLFC69at06JFizR48GCNHTtW/fv3P+5zDx48qGuuuUY2m02vvvqqWrZsqe7du2vBggUuSA4AAPAXRkoBAAC4mbVr1+qWW27Rfffdpy1btuiuu+7SDTfcoBUrVhz3uUVFRbrqqqvUvXt3/fHHH/ryyy/14YcfasaMGS5IDgAA8BebJGN1CAAAADhPTEyMli5dqmeeeea4fSMiIrR79261bdtWiYmJLkgHAADqK0ZKAQAA1AMNGzZUixYtjtknMDBQzZs3d1EiAABQ31GUAgAAqAfGjRt33JFPkydP1rp161yUCAAA1HdM3wMAAAAAAIDLMVIKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAOCoTz75RMYYjR071mnHGDlypIwxioiIkCTNnDlTu3btOuX9RkREyBijkSNHVtnH09NTM2fOVFZWlg4fPqyLLrrolI8LAABwsihKAQAASAoKCtLVV1+tP/74Q3fddZfLjvvcc8/p6quvdsmxBgwYoNtuu02vv/66rrzySq1bt84lxwUAAKgMRSkAAABJN9xwgyTpwQcfVKdOndS/f3+XHHfnzp2KjY11ybGaNWsmqWx01ooVK5Sbm+uS4wIAAFSGohQAAICk22+/XUuWLNHSpUsVHx+vu+++u9zjMTExmjlzph5//HGlpqbq0KFDWrhwodq0aePoM2nSJO3atUuDBg3S1q1blZubq19++UV9+/at8riVTd+744479Oeff6qgoECJiYmaNGmSPDzK/9l29dVXKzY2Vnl5eVq/fr26d+9+zPObOXOmZs2aJUnatWuXYmJiJEm+vr568skntXXrVuXn5ysuLk6PPfaYbDZbuXOfM2eO5s2bp5ycHP3444+SpMDAQL3xxhtKSkpSTk6Ofv31Vw0cOPCEzwUAANRfhkaj0Wg0Gq0+t65duxpjjBk2bJiRZCZOnGgKCwtNixYtHH1iYmLMwYMHTVxcnLnuuuvMiBEjzO7du83OnTuNv7+/kWQmTZpkcnJyTEZGhnnggQfMwIEDzZIlS0xBQYHp1q2bkWRGjhxpjDEmIiLCSDIzZ840u3btchxnwoQJpqSkxEydOtVceuml5tFHHzV5eXlmxowZjj5XXnmlKSkpMbNnzzaXXXaZGT9+vMnOzjbGGDNy5MhKz7Fdu3bm2WefNcYYM3ToUNOlSxcjyfz4448mKyvLPPLII+aSSy4xL7zwgikqKjLvvfdeuXM/cuSI+eijj0z//v3NJZdcYjw8PMwvv/xiMjIyzP33328uvvhiM3v2bHPkyBETHR1d7XOh0Wg0Go1Wr5vlAWg0Go1Go9Esba+88oo5cOCA8fb2NpJMq1atTHFxsXn88ccdfWJiYkxhYaGJjIx0bOvevbsxxpi7777bSGVFKWOMufnmmx19/Pz8THJyspk7d66Rjl2UCgoKMrm5ueadd94pl+/22283xhjTtWtXI8n8+uuv5pdffinX57HHHjtmUaqyYw8YMMAYY8zw4cPL9Zs4cWK548XExJicnBzj4+Pj6DNo0CBjjDFXXXWVY5vNZjOrVq0yTz/9dLXPhUaj0Wg0Wv1tjJ0GAAD1mpeXl26++WZ99dVXCggIUKNGjZSdna2VK1fqzjvvLDeNbeXKleWm2sXGxiohIaHc9LyioiLNnTvXcb+goEDffffdMafw2Z133nkKCAjQ//73P3l6ejraN998I0m69NJL5efnp3POOcexze7LL7884XO/6KKLVFRUpHnz5pXb/sknn0hSucxbt27VkSNHHPejo6N15MiRcjmMMerTp4+effbZap0LAACo37ysDgAAAGClK6+8UiEhIRo9erRGjx5d4fHLL79cP/zwgyRp3759FR7fv3+/mjZt6rifmpqqkpKSY/apin0h8u+//77Sx8PDw9WkSRN5eHgoPT293GMpKSnH3f8/NW3aVOnp6SotLS23PTU1VZLUuHFjx7acnJwKWTMyMmSMqXTf1TkXAABQv1GUAgAA9dqoUaOUkJCgO+64o9x2m82mhQsX6p577nEUpYKDgys8PyQkRDt27HDctxdj/tln//79x81y6NAhSdKNN96ouLi4Co+npaXp4MGDKikpUUhISLnHKjvu8Rw8eFDBwcHy8PAoV5gKCwuTpAqFr39mreyY3bt3l81mq9a5AACA+o3pewAAoN4KCQnRFVdcoc8//1zLli0r15YuXap58+Zp0KBBjlE90dHR5UY8nX322WrXrp2WLFni2BYQEKDLLrvMcd/Pz08DBw4s16cqa9asUWFhoVq2bKn169c7WnFxsV588UVFRkaqsLBQq1ev1rBhw8o9d/DgwSd8/suWLZO3t7euu+66cttvvvlmSWXTFauyYsUK+fj4aMCAAeW2z5w5U0888US1zgUAANRvjJQCAAD11q233ipvb+9ya0D93ezZs3XnnXfqzjvvlCQ1aNBA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07hx4/Txxx/r+eef1yWXXKJmzZrp3nvvVV5enrZv366LLrpILVq0qHR/6enpKikp0eHDh5Wenq7BgwerZcuW6tWrlw4dOiRJuv/++/XNN984CmlTp07Vzp07JZUtkt68eXONGjVKOTk52rJlix544AGVlJQcN+s/nX322fL09FRGRoZj28aNGxUdHV2t1+NkUZQC4BThHaMkSclxZSOlJCmcK/ABAAAAtZsxVieolhEjRqigoECLFy+WVLZI98GDBzVy5MhyRam/j1L67bff1KJFC8eUumM9VpkuXbqoWbNmysrKcmzz8PBQQECAmjZtqq5duyo+Pl55eXmOx9etW6dBgwZV65y6dOmiuLg4R0FKklavXi1vb295eZWVb3bv3u14rFOnToqLi1NOTo5j28cffyyprJh1rKwHDx4sd+yzzz5bc+fO1aRJkxzbnLmWlB1FKQBO4Zi+t32H9u/cLUlqFNJcDRo3Uu6hwxYmAwAAAFCVt0be4xbT92644QYFBASUK7p4eXnpuuuuK7euUlFRkeO2fc0o+6Llx3qsMl5eXtq2bZuGDBlS4bHDh8u+49hstnLbjxw5Uu1zqmyhc3su+3//3ufv+U8m69+dffbZevzxx5WQkFDtvDWBhc4B1LiGTZsoqHlw2ZUqEnaqMC9P6XuTJP1VrAIAAABQOx3JL3BZOxkdOnTQ2WefrQceeEDdu3d3tOHDh6tRo0a6+uqrHX27d+/uuN2jRw/t27fPMUWtssf+OYLo77Zv3642bdrowIEDSkhIUEJCgiIjI/XMM8/IGKM///xTHTt2VFBQkOM5Z511VrXPa/v27erYsaOaNGni2HbeeeepqKhIJSUlFfrHx8erffv28vf3d2ybMmWKpk2bdtysfxcZGakmTZpow4YN1c5aUyhKAahx9sJTxp4kxwdN8vYd5R4DAAAAgJNxww03KCMjQ++//742b97saF9++aU2b96skSNHOvpOmzZN55xzji6++GI9++yzevvtt6v1WGV+/PFHJSYm6pNPPtHpp5+u6Ohovf/++8rLy1Npaan+7//+T3v27NGHH36ozp07a+TIkRo+fHi1z+unn37Szp07NWfOHJ1++um66KKL9Oabb+qzzz4rN6XPbvHixUpNTdV7772nTp06afDgwbrnnnu0ePHi42b9u3POOUfFxcUuX+RcoigFwAnC7VP3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tm3k5eOj4iNHLE4EAAAAAKgupxelnnrqKUnSe++9V+ExY4y8vKoXoVu3bvL29tbq1asd21auXKmJEyfKZrPJGFOuf9OmTTV58mRddtll2rx58ymcAVB3efv5KrhNK0lScly8xWlOXNb+A8rNPKQGTRorJKqt9m2NszoSAAAAAKCanL6mlKenZ5WtugUpSQoLC1N6erqKiooc29LS0uTv769mzZpV6P/aa69p1qxZ2rJlS42cB1AXhUa1k4enp7IzDionI9PqOCfFPsIrnCl8AAAAAOBWnD5SSpL8/Px08803q0uXLvL09NS2bdv0xRdfKDOz+l+CAwICVFhYWG6b/b6vr2+57RdffLGio6N1+umnn3p4oA4LOzp1L8UNp+7ZJcftUIdePRznAgAAAABwD04fKXXaaacpPj5eTz75pCIiIhQREaEnnnhC27ZtU5cuXaq9n4KCggrFJ/v9vLw8xzY/Pz+99957uu+++yqsXwWgvPBOR9eTcsNFzu3sBbVwilIAAAAA4FacXpSaNm2afvrpJ0VFRenaa6/V1VdfrcjISH333XeaOnVqtfezb98+BQcHy9PT07EtNDRUeXl5OnTokGPbueeeq6ioKM2fP1/Z2dnKzs6WJH3//fd69913a+q0gDohzI0XObejKAUAAAAA7snp0/fOO+883XfffSopKXFsKykp0UsvvaTffvut2vuJjY1VUVGRevfurVWrVkmSoqOjtW7dunKLnP/6669q3778l9MdO3Zo9OjR+umnn07xbIC65a+RUu63yLld6o5dKi0pUYMmjRXUormy9h+wOhIAAAAAoBqcXpRKSUlR+/btFRdX/qpY7du3V1ZWVrX3k5+fr1mzZmn69OkaNWqUWrZsqXHjxmnUqFGSpJCQEB0+fFgFBQVKSEio8Px9+/bpwAG+rAJ2jUNaKCAoSCVFxdq/K9HqOCet+MgR7d+9R6FRkQrvGEVRCgAAAADchNOn702fPl0zZszQ7bffrtNPP12nn366Ro8erQ8++EAzZsw4oX2NHTtW69evV0xMjN5++21NmjRJCxculCSlpqZq+PDhzjgFoE6yT91L27VbJX+7qqU7Sjk60ovFzgEAAADAfTh9pNQrr7yiBg0a6OWXX1bTpk0lSWlpaXrttdf06quvntC+8vPzddttt+m2226r8JjNZqvyecd6DKivwjt1kOTe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",
"text/plain": [
""
- ],
- "image/png": "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iy/Wpk2blJubqyVLligyMtLFaVETRo4cWeE1N8aopKSk0v6xsbEV+p522mkuTo1T5ePjo02bNqlv376ObW3bttVPP/2knJwcbd68WZdeeukx9zFixAjt2LFDubm5WrBggZo1a+bs2KgBlb32vXr10qpVq5Sdna1t27bpjjvuOOY+MjMzK/weaNCggbOj4xRV9tpPnTq1wmt5//33V7mPBx98UElJScrKytKMGTPk7+/viug4Rf987WfOnFnpZ/+SJUsqfX7jxo0r9D1w4IArTwEn4Fjf5erbZ72huXd74403TGxsrDnrrLPM0KFDzeHDh82wYcMq9AsICDDJyclmypQppnPnzmbq1KkmJSXFBAQEWH4OtJNrq1evNosWLTJdu3Y10dHRJi4uzkyePLnSvnFxcebGG280ISEhjubj42P5OdBOrj3xxBPm66+/Lvd6NmrUqEK/1q1bm+zsbDN27FjTtWtX8/nnn5uNGzdanp924s3Pz6/c692qVSsTFxdnXnvttQp9PTw8TF5enrngggvKPcfT09Py86BVv/n6+pr58+cbY4zp27evY3tsbKyZM2eO6dy5s5kwYYLJyckxrVu3rnQfPXv2NLm5ueaWW24xZ5xxhomJiTHffPON5edGO/HXPiQkxBw8eNC88MILpn379mb48OEmLy/PDBw4sNJ9hIeHG2OMiYyMLPd7wOpzo534ay/J/Pjjj2b8+PHlXkt/f/9K93HNNdeYzMxMM2jQINOjRw/z559/mjfffNPyc6Od+GsfFBRU7jXv1auXyc/PN0OGDKl0H+eff745cOBAuec0b97c8nOjVd6O9V2unn3WWx6AdgotICDA5OXllfvQmjhxoomJianQd9SoUSYhIaHctri4ODNy5EjLz4N24q1Tp07GGGNatGjh2DZixAiTlJRUoa+Pj48pKioyHTp0sDw3rWbanDlzzAsvvHDcfs8880y53wf+/v7m8OHD5X5n0NyzTZgwwcTHx1daXI6KijLFxcXG19fX8py0k2tdunQxGzZsMLGxseW+oPTr189kZ2eX+weln376yUyaNKnS/cyaNcvMnDnTcb9Vq1ampKTEtG3b1vJzpJ3Ya3/33XebLVu2lOs7ffp088knn1S6n4svvtjs27fP8vOhnfprL8ns3bvXXHrppdXaz7Jly8r9TujTp4/Jzc2tsohFs74d67X/e/vhhx/M7Nmzq9zPHXfcYVatWmX5+dCO3471Xa6+fdYzfc/NdevWTd7e3lq9erVj28qVK9WrVy/ZbLZyfXv37q2VK1eW27Zq1Sqdd955LsmKmpWamqrLL79c+/fvL7e9UaNGFfp26tRJxhjt3LnTVfHgZF27dlVcXNxx+/Xu3VvLly933M/Pz9fvv//O+97NNWnSROPHj9eECRN05MiRCo937dpVe/fuVWFhoQXpUBP69u2rmJiYCu/V3r176/fff1deXp5j28qVK6t8T//zd0BSUpL27Nmj3r17Oyc4TllVr/0PP/ygUaNGVehf2ee+VP3PCdQeVb32gYGBatWqVbVeTw8PD/Xs2bPc+37NmjXy8fFRt27dajwzakZVr/3f9e/fXxdeeKGeeOKJKvvwvncfx/ouV98+6ylKubmwsDClp6erqKjIsS0tLU3+/v4V5pGGhYUpOTm53La0tDS1atXKJVlRsw4fPqwff/zRcd9ms2nMmDGVzjHv0qWLDh8+rDlz5ig5OVlr167VgAEDXBkXNaxTp066/PLLtX37du3YsUMvvviivL29K/TjfV833XvvvUpOTtb8+fMrfbxLly46cuSIvvnmG6WkpGjp0qXq2bOni1PiVEyfPl1jx45Vfn5+ue0n+p7md4D7qeq1T0xM1Nq1ax33mzdvrhEjRlS5tkyXLl0UEBCgmJgYJScna9GiRerQoYNTs+PUVPXad+nSRaWlpZo4caL27t2r2NhY3XrrrZXuo3HjxvL39y/3vi8pKVFGRgbv+1qsqtf+7yZMmKCPP/5YSUlJVfbp0qWLWrVqpbVr1yopKUlz585VaGioMyLjFB3ru1x9+6ynKOXmAgICKvxLuP3+Pxe8rqpvVQtjw71MnjxZZ599tiZOnFjhsc6dOysgIECLFy/WgAED9N133+mbb77ROeecY0FSnKo2bdqoQYMGKiws1PXXX69x48bppptu0pQpUyr05X1fN40ePVpvvvlmlY937txZTZo00YwZMzRw4EBt2bJFS5YscZs/TlC1E31P8zugbvLz89P8+fOVmpqq9957r9I+nTt3VtOmTfX8889ryJAhys/P15IlS9SwYUMXp8Wp6ty5s4wx2rZtmwYOHKgZM2bo/fff19ChQyv0DQgIkCTe93VMZGSk+vfvf8zPfqnsZyUoKEgPP/ywhg8frvDwcH377bfy8OBrf2339+9y9e2z3svqADg1BQUFFX7Y7Pf/PtzvWH3/2Q/u56WXXtJDDz2k4cOHa/PmzRUef+655/TGG2/o0KFDkqQ//vhD55xzju666y7dfffdLk6LU7Vnzx41bdpUmZmZkqSNGzfKw8NDn3zyicaOHavS0lJH36re9/afBbifHj16qFWrVvr888+r7HPnnXcqICBA2dnZkqT77rtPffr00S233KIXX3zRVVHhBAUFBRVGQh/rs5zP/rqnQYMG+vrrr9WxY0dFR0dXObJiwIAB8vb2Vm5uriTppptu0t69ezV48GDNnTvXlZFximbPnq1vvvnG8bm/adMmdezYUffee6+++uqrcn0LCgokVfzHad737m3YsGGKjY3V1q1bj9nvtNNOkzHG8XNw7bXXKiUlRb169dIvv/ziiqg4Cf/8LlffPuspmbq5ffv2KTg4WJ6eno5toaGhysvLq/Clc9++fRWGb4aGhiolJcUVUeEkb7zxhh555BHdfPPNWrBgQaV9jDEVfh62bt2qli1buiAhnMH+h6nd1q1b5e/vr6ZNm5bbzvu+7hkwYICWL19+zMJiSUmJoyBlt23bNt7zdcCJvqf5HVC3BAYGavHixTr99NPVv39/7dixo8q+R44ccRSkpLJ/Nd+1axe/B9xUZZ/7lb2WGRkZys/PL/e+9/T0VLNmzXjfu7EBAwZUKEBWJj8/31GQkqQDBw4oIyOD930tVtl3ufr2WU9Rys3FxsaqqKio3CJm0dHRWrdunYwx5fquWbNG559/frltffr00Zo1a1ySFTXv6aef1j333KMRI0boiy++qLLfzJkz9eGHH5bb1r17d23bts3ZEeEEl112mdLT0+Xv7+/Y1r17d6Wnpys9Pb1c3zVr1ig6Otpx39/fX2eddRbvezfWq1cvrVq16ph9fv75Zz399NOO+zabTWeeeSbv+TpgzZo1Ovvss+Xn5+fYFh0dXeV7+p+/A1q1aqXWrVvzO8AN2Ww2LViwQO3atVPfvn21ZcuWY/bfsWOHRo4c6bgfEBCgDh068HvADT3zzDP66aefym2r6u84Y4zWrVtX7n1/3nnnqaioSBs3bnR6VjhHz549j/vZHxgYqIMHD+qiiy5ybAsPD1dwcDDv+1qqqu9y9fGz3vJLANJOrb377rtm06ZNpkePHmbIkCHm0KFD5uqrrzaSTEhIiPHz8zOSTGBgoElLSzNTp041Xbp0MVOnTjXJycnlLjVJc5/WuXNnU1RUZJ599lkTEhJSrv3ztb/66qtNYWGhueWWW0xUVJR56qmnTG5uromIiLD8PGgn3ho2bGj27t1rPv30U9OxY0czYMAAk5SUZB599FHj4eFhQkJCjLe3t5FkIiIiTF5enhk/frzp2rWr+fzzz01sbKzl50A7+bZr1y4zfPjwctv++bo//PDDJjMz0wwePNh07NjRvP322yYlJcU0bNjQ8vy0E29/vzy4h4eH+fPPP83cuXNN165dzfjx401WVpZp3bq1kWS8vb1NSEiI8fDwMJJM7969TUFBgbn99tvNGWecYX7++Wfz9ddfW35OtBN/7UePHm2Ki4vNwIEDy33mN2nSpNLXftq0aWb37t2mb9++pmvXrmb+/Pnmjz/+cDxOq93t7699jx49zJEjR8wjjzxi2rVrZ+655x6Tn59vevfubSQZPz8/x99/kszw4cPNoUOHzJAhQ0yPHj3Mpk2bzLRp0yw/J9qJv/ZS2d9yxphyr7G9/fO1//rrr82GDRtMjx49zFlnnWWWL19uFi1aZPk50Sq2Y32Xq4ef9ZYHoJ1i8/f3Nx9//LHJzs42SUlJ5sEHH3Q8ZowxI0eOdNzv2bOnWb9+vcnLyzNr1qwx3bt3tzw/7eTa+PHjTVUqe+3vuOMOs337dpOfn29+++03c8EFF1h+DrSTb127djU//vijycrKMvv27TNPP/20kf76w+Xvf8wMGDDAbNu2zeTm5pqffvrJtG3b1vL8tJNveXl55rLLLiu3rbLX/fHHHze7d+82+fn5ZunSpea0006zPDvt5No/X9uoqCizdOlSk5+fbzZt2mQuvvhix2N9+/Y1xphy/+gwcuRIk5iYaLKzs838+fNN06ZNLT8n2om/9t9//32ln/kxMTGVvva+vr7mlVdeMfv27TM5OTnmf//7n2nVqpXl50Q78ddekrnqqqtMbGysycvLM1u2bHH8A7RU9h63//1nb+PHjzepqakmMzPTzJgxw/j6+lp+TrSTe+3PPfdcY4wxPj4+Ffr+87Vv3Lix+fDDD83+/fvN4cOHzezZs03jxo0tPydaxXa873L16bPedvQGAAAAAAAA4DKsKQUAAAAAAACXoygFAAAAAAAAl6MoBQAAAAAAAJejKAUAAAAAAACXoygFAAAAAAAAl6MoBQAAAAAAAJejKAUAAAAAAACXoygFAAAAAAAAl6MoBQAA4AQzZ86UMabKNnLkSBljFBER4fQsMTExMsYoPz//mP3effddRz5X5AIAAPWbTZKxOgQAAEBdExQUJH9/f0nS8OHDNW7cOPXs2dPxeGZmppo0aaIDBw6otLTUqVliYmL022+/acqUKdq/f3+V/QIDA9WpUyetW7dObdu2VWJiolNzAQCA+s3L6gAAAAB1UVZWlrKysiRJhw8fVklJidLS0sr1+ed9Z8rJyTlmQUqSsrOzdeDAARclAgAA9R3T9wAAACwQERFRbpqcMUbXXnuttmzZotzcXH322Wdq27atlixZotzcXC1fvlzh4eGO5w8dOlSbN29Wbm6u1q5dqwsvvLDax+7Xr582bNig/Px8JSQk6K677qrx8wMAADgeilIAAAC1xLPPPqvbbrtNgwYN0rBhw7R69Wq9++67Ov/88xUWFqbHHntMknTmmWdq1qxZev7553XmmWfqk08+0ffff6+oqKjjHsPDw0Pz5s3TvHnz1LlzZz311FN655131KVLF2efHgAAQDlM3wMAAKglXn/9df3666+SpA0bNmj79u3673//K0maP3++unfvLkkaN26cPvjgA82dO1eS9Oabb6pv37669957NW7cuGMeo1GjRmrWrJnS0tKUmJioxMREJScnKyUlxXknBgAAUAlGSgEAANQSO3fudNzOz8/X7t27y9339fWVJHXp0kVjxoxRdna2ow0ePFgdO3Y87jEyMzP1zjvvaMaMGdq9e7fefPNNHT58WIcOHarp0wEAADgmilIAAAC1RHFxcbn7VV2Vz8vLSy+//LK6d+/uaF27dtU999xTrePcf//9Ou200/T++++rV69eWrt2rQYMGHDK+QEAAE4ERSkAAAA3s337dkVGRiohIcHR7rrrLl1xxRXHfW5ISIjeeust7dixQ//5z3907rnnasmSJbrqqqtckBwAAOAvrCkFAADgZl5//XWtWLFC69at06JFizR48GCNHTtW/fv3P+5zDx48qGuuuUY2m02vvvqqWrZsqe7du2vBggUuSA4AAPAXRkoBAAC4mbVr1+qWW27Rfffdpy1btuiuu+7SDTfcoBUrVhz3uUVFRbrqqqvUvXt3/fHHH/ryyy/14YcfasaMGS5IDgAA8BebJGN1CAAAADhPTEyMli5dqmeeeea4fSMiIrR79261bdtWiYmJLkgHAADqK0ZKAQAA1AMNGzZUixYtjtknMDBQzZs3d1EiAABQ31GUAgAAqAfGjRt33JFPkydP1rp161yUCAAA1HdM3wMAAAAAAIDLMVIKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAOCoTz75RMYYjR071mnHGDlypIwxioiIkCTNnDlTu3btOuX9RkREyBijkSNHVtnH09NTM2fOVFZWlg4fPqyLLrrolI8LAABwsihKAQAASAoKCtLVV1+tP/74Q3fddZfLjvvcc8/p6quvdsmxBgwYoNtuu02vv/66rrzySq1bt84lxwUAAKgMRSkAAABJN9xwgyTpwQcfVKdOndS/f3+XHHfnzp2KjY11ybGaNWsmqWx01ooVK5Sbm+uS4wIAAFSGohQAAICk22+/XUuWLNHSpUsVHx+vu+++u9zjMTExmjlzph5//HGlpqbq0KFDWrhwodq0aePoM2nSJO3atUuDBg3S1q1blZubq19++UV9+/at8riVTd+744479Oeff6qgoECJiYmaNGmSPDzK/9l29dVXKzY2Vnl5eVq/fr26d+9+zPObOXOmZs2aJUnatWuXYmJiJEm+vr568skntXXrVuXn5ysuLk6PPfaYbDZbuXOfM2eO5s2bp5ycHP3444+SpMDAQL3xxhtKSkpSTk6Ofv31Vw0cOPCEzwUAANRfhkaj0Wg0Gq0+t65duxpjjBk2bJiRZCZOnGgKCwtNixYtHH1iYmLMwYMHTVxcnLnuuuvMiBEjzO7du83OnTuNv7+/kWQmTZpkcnJyTEZGhnnggQfMwIEDzZIlS0xBQYHp1q2bkWRGjhxpjDEmIiLCSDIzZ840u3btchxnwoQJpqSkxEydOtVceuml5tFHHzV5eXlmxowZjj5XXnmlKSkpMbNnzzaXXXaZGT9+vMnOzjbGGDNy5MhKz7Fdu3bm2WefNcYYM3ToUNOlSxcjyfz4448mKyvLPPLII+aSSy4xL7zwgikqKjLvvfdeuXM/cuSI+eijj0z//v3NJZdcYjw8PMwvv/xiMjIyzP33328uvvhiM3v2bHPkyBETHR1d7XOh0Wg0Go1Wr5vlAWg0Go1Go9Esba+88oo5cOCA8fb2NpJMq1atTHFxsXn88ccdfWJiYkxhYaGJjIx0bOvevbsxxpi7777bSGVFKWOMufnmmx19/Pz8THJyspk7d66Rjl2UCgoKMrm5ueadd94pl+/22283xhjTtWtXI8n8+uuv5pdffinX57HHHjtmUaqyYw8YMMAYY8zw4cPL9Zs4cWK548XExJicnBzj4+Pj6DNo0CBjjDFXXXWVY5vNZjOrVq0yTz/9dLXPhUaj0Wg0Wv1tjJ0GAAD1mpeXl26++WZ99dVXCggIUKNGjZSdna2VK1fqzjvvLDeNbeXKleWm2sXGxiohIaHc9LyioiLNnTvXcb+goEDffffdMafw2Z133nkKCAjQ//73P3l6ejraN998I0m69NJL5efnp3POOcexze7LL7884XO/6KKLVFRUpHnz5pXb/sknn0hSucxbt27VkSNHHPejo6N15MiRcjmMMerTp4+effbZap0LAACo37ysDgAAAGClK6+8UiEhIRo9erRGjx5d4fHLL79cP/zwgyRp3759FR7fv3+/mjZt6rifmpqqkpKSY/apin0h8u+//77Sx8PDw9WkSRN5eHgoPT293GMpKSnH3f8/NW3aVOnp6SotLS23PTU1VZLUuHFjx7acnJwKWTMyMmSMqXTf1TkXAABQv1GUAgAA9dqoUaOUkJCgO+64o9x2m82mhQsX6p577nEUpYKDgys8PyQkRDt27HDctxdj/tln//79x81y6NAhSdKNN96ouLi4Co+npaXp4MGDKikpUUhISLnHKjvu8Rw8eFDBwcHy8PAoV5gKCwuTpAqFr39mreyY3bt3l81mq9a5AACA+o3pewAAoN4KCQnRFVdcoc8//1zLli0r15YuXap58+Zp0KBBjlE90dHR5UY8nX322WrXrp2WLFni2BYQEKDLLrvMcd/Pz08DBw4s16cqa9asUWFhoVq2bKn169c7WnFxsV588UVFRkaqsLBQq1ev1rBhw8o9d/DgwSd8/suWLZO3t7euu+66cttvvvlmSWXTFauyYsUK+fj4aMCAAeW2z5w5U0888US1zgUAANRvjJQCAAD11q233ipvb+9ya0D93ezZs3XnnXfqzjvvlCQ1aNBA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07hx4/Txxx/r+eef1yWXXKJmzZrp3nvvVV5enrZv366LLrpILVq0qHR/6enpKikp0eHDh5Wenq7BgwerZcuW6tWrlw4dOiRJuv/++/XNN984CmlTp07Vzp07JZUtkt68eXONGjVKOTk52rJlix544AGVlJQcN+s/nX322fL09FRGRoZj28aNGxUdHV2t1+NkUZQC4BThHaMkSclxZSOlJCmcK/ABAAAAtZsxVieolhEjRqigoECLFy+WVLZI98GDBzVy5MhyRam/j1L67bff1KJFC8eUumM9VpkuXbqoWbNmysrKcmzz8PBQQECAmjZtqq5duyo+Pl55eXmOx9etW6dBgwZV65y6dOmiuLg4R0FKklavXi1vb295eZWVb3bv3u14rFOnToqLi1NOTo5j28cffyyprJh1rKwHDx4sd+yzzz5bc+fO1aRJkxzbnLmWlB1FKQBO4Zi+t32H9u/cLUlqFNJcDRo3Uu6hwxYmAwAAAFCVt0be4xbT92644QYFBASUK7p4eXnpuuuuK7euUlFRkeO2fc0o+6Llx3qsMl5eXtq2bZuGDBlS4bHDh8u+49hstnLbjxw5Uu1zqmyhc3su+3//3ufv+U8m69+dffbZevzxx5WQkFDtvDWBhc4B1LiGTZsoqHlw2ZUqEnaqMC9P6XuTJP1VrAIAAABQOx3JL3BZOxkdOnTQ2WefrQceeEDdu3d3tOHDh6tRo0a6+uqrHX27d+/uuN2jRw/t27fPMUWtssf+OYLo77Zv3642bdrowIEDSkhIUEJCgiIjI/XMM8/IGKM///xTHTt2VFBQkOM5Z511VrXPa/v27erYsaOaNGni2HbeeeepqKhIJSUlFfrHx8erffv28vf3d2ybMmWKpk2bdtysfxcZGakmTZpow4YN1c5aUyhKAahx9sJTxp4kxwdN8vYd5R4DAAAAgJNxww03KCMjQ++//742b97saF9++aU2b96skSNHOvpOmzZN55xzji6++GI9++yzevvtt6v1WGV+/PFHJSYm6pNPPtHpp5+u6Ohovf/++8rLy1Npaan+7//+T3v27NGHH36ozp07a+TIkRo+fHi1z+unn37Szp07NWfOHJ1++um66KKL9Oabb+qzzz4rN6XPbvHixUpNTdV7772nTp06afDgwbrnnnu0ePHi42b9u3POOUfFxcUuX+RcoigFwAnC7VP3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tm3k5eOj4iNHLE4EAAAAAKgupxelnnrqKUnSe++9V+ExY4y8vKoXoVu3bvL29tbq1asd21auXKmJEyfKZrPJGFOuf9OmTTV58mRddtll2rx58ymcAVB3efv5KrhNK0lScly8xWlOXNb+A8rNPKQGTRorJKqt9m2NszoSAAAAAKCanL6mlKenZ5WtugUpSQoLC1N6erqKiooc29LS0uTv769mzZpV6P/aa69p1qxZ2rJlS42cB1AXhUa1k4enp7IzDionI9PqOCfFPsIrnCl8AAAAAOBWnD5SSpL8/Px08803q0uXLvL09NS2bdv0xRdfKDOz+l+CAwICVFhYWG6b/b6vr2+57RdffLGio6N1+umnn3p4oA4LOzp1L8UNp+7ZJcftUIdePRznAgAAAABwD04fKXXaaacpPj5eTz75pCIiIhQREaEnnnhC27ZtU5cuXaq9n4KCggrFJ/v9vLw8xzY/Pz+99957uu+++yqsXwWgvPBOR9eTcsNFzu3sBbVwilIAAAAA4FacXpSaNm2afvrpJ0VFRenaa6/V1VdfrcjISH333XeaOnVqtfezb98+BQcHy9PT07EtNDRUeXl5OnTokGPbueeeq6ioKM2fP1/Z2dnKzs6WJH3//fd69913a+q0gDohzI0XObejKAUAAAAA7snp0/fOO+883XfffSopKXFsKykp0UsvvaTffvut2vuJjY1VUVGRevfurVWrVkmSoqOjtW7dunKLnP/6669q3778l9MdO3Zo9OjR+umnn07xbIC65a+RUu63yLld6o5dKi0pUYMmjRXUormy9h+wOhIAAAAAoBqcXpRKSUlR+/btFRdX/qpY7du3V1ZWVrX3k5+fr1mzZmn69OkaNWqUWrZsqXHjxmnUqFGSpJCQEB0+fFgFBQVKSEio8Px9+/bpwAG+rAJ2jUNaKCAoSCVFxdq/K9HqOCet+MgR7d+9R6FRkQrvGEVRCgAAAADchNOn702fPl0zZszQ7bffrtNPP12nn366Ro8erQ8++EAzZsw4oX2NHTtW69evV0xMjN5++21NmjRJCxculCSlpqZq+PDhzjgFoE6yT91L27VbJX+7qqU7Sjk60ovFzgEAAADAfTh9pNQrr7yiBg0a6OWXX1bTpk0lSWlpaXrttdf06quvntC+8vPzddttt+m2226r8JjNZqvyecd6DKivwjt1kOTe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"
+ ]
},
- "metadata": {},
- "output_type": "display_data",
"jetTransient": {
"display_id": null
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
}
],
- "execution_count": 7
+ "source": [
+ "# Recursive plot using getSamples\n",
+ "msd.importPythonModel(name=model_name)\n",
+ "msd.neuralizeModel(sample_time=0.01)\n",
+ "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=0)\n",
+ "result = msd(samples, sampled=True, prediction_samples=2000 - 10)\n",
+ "\n",
+ "# region Plotting\n",
+ "t = np.arange(len(result[\"x_n\"])) * 0.01\n",
+ "plt.figure(figsize=(12, 7))\n",
+ "plt.subplot(2, 1, 1)\n",
+ "plt.plot(t, result[\"x_n\"], label=r\"Estimated $x$\", linewidth=2)\n",
+ "plt.plot(\n",
+ " t,\n",
+ " np.array(samples[\"x_t\"]).squeeze(-1).squeeze(-1),\n",
+ " label=r\"True $x$\",\n",
+ " linestyle=\"--\",\n",
+ " linewidth=2,\n",
+ ")\n",
+ "plt.title(\"Position estimation\")\n",
+ "plt.xlabel(\"Time [s]\")\n",
+ "plt.ylabel(r\"$x$ [m]\")\n",
+ "plt.grid()\n",
+ "plt.legend()\n",
+ "plt.subplot(2, 1, 2)\n",
+ "plt.plot(t, np.array(samples[\"F\"])[:, 0, 0], label=r\"Applied force $F$\")\n",
+ "plt.title(\"Applied force\")\n",
+ "plt.xlabel(\"Time [s]\")\n",
+ "plt.ylabel(\"Force [N]\")\n",
+ "plt.grid()\n",
+ "plt.legend()\n",
+ "plt.tight_layout()\n",
+ "# endregion\n",
+ "\n",
+ "plt.show()"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "outputs": [],
"execution_count": null,
- "source": "",
- "id": "439c02826528ca8"
+ "id": "439c02826528ca8",
+ "metadata": {},
+ "outputs": [],
+ "source": []
}
],
"metadata": {
diff --git a/case-studies/neuralODE/saved/neuralODE_msd.json b/case-studies/neuralODE/saved/neuralODE_msd.json
index 47ab6487..32187883 100644
--- a/case-studies/neuralODE/saved/neuralODE_msd.json
+++ b/case-studies/neuralODE/saved/neuralODE_msd.json
@@ -54,4 +54,4 @@
"Select7": ["Select", ["NeuralODE5"], 2, 0],
"TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]],
"TimePart3": ["TimePart", ["F"], -1, [-0.1, 0]],
- "TimePart8": ["TimePart", ["Select7"], 0.1, [0.09, 0.1]]}}
\ No newline at end of file
+ "TimePart8": ["TimePart", ["Select7"], 0.1, [0.09, 0.1]]}}
diff --git a/case-studies/neuralODE/saved/neuralODE_msd.py b/case-studies/neuralODE/saved/neuralODE_msd.py
index d87c5470..8517e971 100644
--- a/case-studies/neuralODE/saved/neuralODE_msd.py
+++ b/case-studies/neuralODE/saved/neuralODE_msd.py
@@ -1,16 +1,20 @@
import torch
+
def nnodely_basic_model_update_state(data_in, rel):
data_out = data_in.clone()
max_dim = min(rel.size(1), data_in.size(1))
data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]
return data_out
+
def nnodely_basic_model_timeshift(data_in):
return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)
+
def nnodely_layers_neuralODE_FNeuralODE5(state, *weights):
from nnodely.support.odeint.adjoint import odeint_adjoint as odeint
+
def ode_func_torch(t, state, weight_fir):
import torch
import torch.nn.functional as F
@@ -20,80 +24,190 @@ def ode_func_torch(t, state, weight_fir):
# 2 channels = [position_window, force_window]
#
# weight_fir: FIR weights applied independently to each channel
-
+
# Apply two independent FIR filters (grouped convolution):
# this produces one-step predictions for each channel
# (B, W, 2) -> (B, 2, W) -> conv -> (B, 2, 1)
out = F.conv1d(state.transpose(1, 2), weight_fir, groups=2)
-
+
# Restore time dimension ordering
# (B, 2, 1) -> (B, 1, 2)
out = out.transpose(1, 2)
-
+
# Combine the two FIR contributions:
# next_velocity = free_response + forced_response
out[:, :, 0] = out[:, :, 0] + out[:, :, -1]
-
+
# Set the force prediction to zero, for the intermediate time steps done in the integration process (if a variable step integrator, such as Dopri5, is used)
out[:, :, -1] = 0.0
-
+
# Shift the sliding window forward by one step
# drop the oldest sample and append the new prediction
# resulting shape: (B, W, 2)
out = torch.cat((state[:, 1:, :], out), dim=1)
-
+
return out
-
- ans = odeint(lambda t, y: ode_func_torch(t, y, *weights), state, t=torch.tensor([0.0, 0.01]), rtol=1e-07, atol=1e-09, method='dopri5', adjoint_params=list(weights))
+
+ ans = odeint(
+ lambda t, y: ode_func_torch(t, y, *weights),
+ state,
+ t=torch.tensor([0.0, 0.01]),
+ rtol=1e-07,
+ atol=1e-09,
+ method="dopri5",
+ adjoint_params=list(weights),
+ )
return ans[-1]
+
class TracerModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.all_parameters = {}
self.all_constants = {}
- self.all_parameters["weight_fir"] = torch.nn.Parameter(torch.tensor([[[-5.0214056968688965, -4.025257587432861, -3.0458261966705322, -1.9209206104278564, -0.7650308012962341, 0.5171442031860352, 1.7535150051116943, 3.087076187133789, 4.083190441131592, 5.219380855560303]], [[0.00026014805189333856, 0.0007117472123354673, 0.0013745456235483289, 0.002709923079237342, 0.003882204182446003, 0.005919742863625288, 0.0070823198184370995, 0.008177743293344975, 0.009529106318950653, 0.008549299091100693]]]), requires_grad=True)
+ self.all_parameters["weight_fir"] = torch.nn.Parameter(
+ torch.tensor(
+ [
+ [
+ [
+ -5.0214056968688965,
+ -4.025257587432861,
+ -3.0458261966705322,
+ -1.9209206104278564,
+ -0.7650308012962341,
+ 0.5171442031860352,
+ 1.7535150051116943,
+ 3.087076187133789,
+ 4.083190441131592,
+ 5.219380855560303,
+ ]
+ ],
+ [
+ [
+ 0.00026014805189333856,
+ 0.0007117472123354673,
+ 0.0013745456235483289,
+ 0.002709923079237342,
+ 0.003882204182446003,
+ 0.005919742863625288,
+ 0.0070823198184370995,
+ 0.008177743293344975,
+ 0.009529106318950653,
+ 0.008549299091100693,
+ ]
+ ],
+ ]
+ ),
+ requires_grad=True,
+ )
self.all_constants["SamplePart10"] = torch.tensor([[1.0]], requires_grad=True)
self.all_constants["Select7"] = torch.tensor([1.0, 0.0], requires_grad=True)
- self.all_constants["TimePart1"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True)
- self.all_constants["TimePart3"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True)
- self.all_constants["TimePart8"] = torch.tensor([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True)
+ self.all_constants["TimePart1"] = torch.tensor(
+ [
+ [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
+ ],
+ requires_grad=True,
+ )
+ self.all_constants["TimePart3"] = torch.tensor(
+ [
+ [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
+ ],
+ requires_grad=True,
+ )
+ self.all_constants["TimePart8"] = torch.tensor(
+ [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True
+ )
self.all_parameters = torch.nn.ParameterDict(self.all_parameters)
self.all_constants = torch.nn.ParameterDict(self.all_constants)
def update(self, closed_loop={}, connect={}, disconnect=False):
pass
+
def forward(self, kwargs):
- getitem = kwargs['x_t']
+ getitem = kwargs["x_t"]
relation_forward_sample_part10_w = self.all_constants.SamplePart10
- einsum = torch.functional.einsum('bij,ki->bkj', getitem, relation_forward_sample_part10_w); getitem = relation_forward_sample_part10_w = None
- getitem_1 = kwargs['x']
+ einsum = torch.functional.einsum(
+ "bij,ki->bkj", getitem, relation_forward_sample_part10_w
+ )
+ getitem = relation_forward_sample_part10_w = None
+ getitem_1 = kwargs["x"]
relation_forward_time_part1_w = self.all_constants.TimePart1
- einsum_1 = torch.functional.einsum('bij,ki->bkj', getitem_1, relation_forward_time_part1_w); getitem_1 = relation_forward_time_part1_w = None
- getitem_2 = kwargs['F']; kwargs = None
+ einsum_1 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_1, relation_forward_time_part1_w
+ )
+ getitem_1 = relation_forward_time_part1_w = None
+ getitem_2 = kwargs["F"]
+ kwargs = None
relation_forward_time_part3_w = self.all_constants.TimePart3
- einsum_2 = torch.functional.einsum('bij,ki->bkj', getitem_2, relation_forward_time_part3_w); getitem_2 = relation_forward_time_part3_w = None
- cat = torch.cat((einsum_1, einsum_2), dim = 2); einsum_1 = einsum_2 = None
+ einsum_2 = torch.functional.einsum(
+ "bij,ki->bkj", getitem_2, relation_forward_time_part3_w
+ )
+ getitem_2 = relation_forward_time_part3_w = None
+ cat = torch.cat((einsum_1, einsum_2), dim=2)
+ einsum_1 = einsum_2 = None
all_parameters_weight_fir = self.all_parameters.weight_fir
- fneural_ode5 = nnodely_layers_neuralODE_FNeuralODE5(cat, all_parameters_weight_fir); cat = all_parameters_weight_fir = None
+ fneural_ode5 = nnodely_layers_neuralODE_FNeuralODE5(
+ cat, all_parameters_weight_fir
+ )
+ cat = all_parameters_weight_fir = None
relation_forward_select7_w = self.all_constants.Select7
- einsum_3 = torch.functional.einsum('ijk,k->ij', fneural_ode5, relation_forward_select7_w); fneural_ode5 = relation_forward_select7_w = None
- unsqueeze = einsum_3.unsqueeze(2); einsum_3 = None
+ einsum_3 = torch.functional.einsum(
+ "ijk,k->ij", fneural_ode5, relation_forward_select7_w
+ )
+ fneural_ode5 = relation_forward_select7_w = None
+ unsqueeze = einsum_3.unsqueeze(2)
+ einsum_3 = None
relation_forward_time_part8_w = self.all_constants.TimePart8
- einsum_4 = torch.functional.einsum('bij,ki->bkj', unsqueeze, relation_forward_time_part8_w); unsqueeze = relation_forward_time_part8_w = None
- return ({'x_n': einsum_4}, {'SamplePart10': einsum, 'TimePart8': einsum_4}, {'x': einsum_4}, {})
-
+ einsum_4 = torch.functional.einsum(
+ "bij,ki->bkj", unsqueeze, relation_forward_time_part8_w
+ )
+ unsqueeze = relation_forward_time_part8_w = None
+ return (
+ {"x_n": einsum_4},
+ {"SamplePart10": einsum, "TimePart8": einsum_4},
+ {"x": einsum_4},
+ {},
+ )
+
+
class RecurrentModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.Cell = TracerModel()
- self.inputs = ['F', 'x_t', ]
+ self.inputs = [
+ "F",
+ "x_t",
+ ]
self.states = dict()
- def forward(self, kwargs, n_samples = None):
- n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs])
- self.states['x'] = kwargs['x']
- results = {'x_n':[], }
+ def forward(self, kwargs, n_samples=None):
+ n_samples = (
+ n_samples
+ if n_samples
+ else min([kwargs[key].size(0) for key in self.inputs])
+ )
+ self.states["x"] = kwargs["x"]
+ results = {
+ "x_n": [],
+ }
X = dict()
for idx in range(n_samples):
for key in self.inputs:
@@ -105,7 +219,9 @@ def forward(self, kwargs, n_samples = None):
results[key].append(out[key])
for key, val in closed_loop.items():
self.states[key] = nnodely_basic_model_timeshift(self.states[key])
- self.states[key] = nnodely_basic_model_update_state(self.states[key], val)
+ self.states[key] = nnodely_basic_model_update_state(
+ self.states[key], val
+ )
for key, val in connect.items():
self.states[key] = nnodely_basic_model_timeshift(val)
return results
diff --git a/case-studies/pinn/pinn_Burgers_equation.ipynb b/case-studies/pinn/pinn_Burgers_equation.ipynb
index 9a320310..2cb39068 100644
--- a/case-studies/pinn/pinn_Burgers_equation.ipynb
+++ b/case-studies/pinn/pinn_Burgers_equation.ipynb
@@ -18,6 +18,7 @@
},
{
"cell_type": "code",
+ "execution_count": 1,
"id": "19b3470f12bc42b4",
"metadata": {
"ExecuteTime": {
@@ -25,10 +26,6 @@
"start_time": "2026-02-25T17:39:06.884462Z"
}
},
- "source": [
- "from nnodely import *\n",
- "import numpy as np"
- ],
"outputs": [
{
"name": "stdout",
@@ -38,7 +35,10 @@
]
}
],
- "execution_count": 1
+ "source": [
+ "from nnodely import *\n",
+ "import numpy as np"
+ ]
},
{
"cell_type": "markdown",
@@ -50,6 +50,7 @@
},
{
"cell_type": "code",
+ "execution_count": 2,
"id": "f6f28e7b27c45c9e",
"metadata": {
"ExecuteTime": {
@@ -57,24 +58,29 @@
"start_time": "2026-02-25T17:39:07.631468Z"
}
},
+ "outputs": [],
"source": [
- "t = Input('t')\n",
- "x = Input('x')\n",
+ "t = Input(\"t\")\n",
+ "x = Input(\"x\")\n",
"x_last = x.last()\n",
"t_last = t.last()\n",
"\n",
- "xt = Concatenate(x_last,t_last)\n",
+ "xt = Concatenate(x_last, t_last)\n",
"for hidden in range(4):\n",
- " xt = Tanh(Linear(20, b = True, b_init = 'init_constant', b_init_params = {'value':0})(xt))\n",
- "u = Linear(1, b = True, b_init = 'init_constant', b_init_params = {'value':0})(xt)\n",
+ " xt = Tanh(\n",
+ " Linear(20, b=True, b_init=\"init_constant\", b_init_params={\"value\": 0})(xt)\n",
+ " )\n",
+ "u = Linear(1, b=True, b_init=\"init_constant\", b_init_params={\"value\": 0})(xt)\n",
"\n",
- "f = Differentiate(u,t_last) + Differentiate(u,x_last) * u - (0.01 / np.pi) * Differentiate(Differentiate(u,x_last),x_last)\n",
+ "f = (\n",
+ " Differentiate(u, t_last)\n",
+ " + Differentiate(u, x_last) * u\n",
+ " - (0.01 / np.pi) * Differentiate(Differentiate(u, x_last), x_last)\n",
+ ")\n",
"\n",
- "U = Output('U',u)\n",
- "F = Output('F',f)"
- ],
- "outputs": [],
- "execution_count": 2
+ "U = Output(\"U\", u)\n",
+ "F = Output(\"F\", f)"
+ ]
},
{
"cell_type": "markdown",
@@ -86,6 +92,7 @@
},
{
"cell_type": "code",
+ "execution_count": 3,
"id": "3997a16202edbedd",
"metadata": {
"ExecuteTime": {
@@ -93,12 +100,11 @@
"start_time": "2026-02-25T17:39:07.654224Z"
}
},
- "source": [
- "u_target = Input('u_target').last()\n",
- "bound_cond = Input('b').last()"
- ],
"outputs": [],
- "execution_count": 3
+ "source": [
+ "u_target = Input(\"u_target\").last()\n",
+ "bound_cond = Input(\"b\").last()"
+ ]
},
{
"cell_type": "markdown",
@@ -110,6 +116,7 @@
},
{
"cell_type": "code",
+ "execution_count": 4,
"id": "bca4c69105a806fb",
"metadata": {
"ExecuteTime": {
@@ -117,20 +124,13 @@
"start_time": "2026-02-25T17:39:07.658898Z"
}
},
- "source": [
- "pinn = Modely(visualizer=TextVisualizer(), seed=42)\n",
- "pinn.addModel('pinn',[U,F])\n",
- "pinn.addMinimize('errorU',u * bound_cond, u_target * bound_cond)\n",
- "pinn.addMinimize('errorF',f, bound_cond * 0)\n",
- "pinn.neuralizeModel()"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n",
- "\u001B[32m{'Constants': {'Constant18': {'dim': 1, 'values': [0.0031830989755690098]},\n",
+ "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n",
+ "\u001b[32m{'Constants': {'Constant18': {'dim': 1, 'values': [0.0031830989755690098]},\n",
" 'Constant23': {'dim': 1, 'values': [0.0]}},\n",
" 'Functions': {},\n",
" 'Info': {'SampleTime': 1,\n",
@@ -1555,12 +1555,18 @@
" 'Tanh10': ['Tanh', ['Linear9']],\n",
" 'Tanh12': ['Tanh', ['Linear11']],\n",
" 'Tanh6': ['Tanh', ['Linear5']],\n",
- " 'Tanh8': ['Tanh', ['Linear7']]}}\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n"
+ " 'Tanh8': ['Tanh', ['Linear7']]}}\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n"
]
}
],
- "execution_count": 4
+ "source": [
+ "pinn = Modely(visualizer=TextVisualizer(), seed=42)\n",
+ "pinn.addModel(\"pinn\", [U, F])\n",
+ "pinn.addMinimize(\"errorU\", u * bound_cond, u_target * bound_cond)\n",
+ "pinn.addMinimize(\"errorF\", f, bound_cond * 0)\n",
+ "pinn.neuralizeModel()"
+ ]
},
{
"cell_type": "markdown",
@@ -1572,6 +1578,7 @@
},
{
"cell_type": "code",
+ "execution_count": 5,
"id": "b5e35e8f7b0e19a2",
"metadata": {
"ExecuteTime": {
@@ -1579,24 +1586,26 @@
"start_time": "2026-02-25T17:39:07.704197Z"
}
},
+ "outputs": [],
"source": [
"import torch\n",
+ "\n",
"# Create boundary conditions\n",
"Nu = 100\n",
- "tt_0 = torch.zeros(Nu//2, dtype=torch.float32)\n",
- "xx_0 = 2 * torch.rand(Nu//2, dtype=torch.float32) - 1\n",
+ "tt_0 = torch.zeros(Nu // 2, dtype=torch.float32)\n",
+ "xx_0 = 2 * torch.rand(Nu // 2, dtype=torch.float32) - 1\n",
"uu_0 = -torch.sin(torch.pi * xx_0)\n",
- "b_0 = torch.ones(Nu//2, dtype=torch.float32)\n",
+ "b_0 = torch.ones(Nu // 2, dtype=torch.float32)\n",
"#\n",
- "tt_1 = torch.rand(Nu//4, dtype=torch.float32)\n",
- "xx_1 = torch.ones(Nu//4, dtype=torch.float32)\n",
- "uu_1 = torch.zeros(Nu//4, dtype=torch.float32)\n",
- "b_1 = torch.ones(Nu//4, dtype=torch.float32)\n",
+ "tt_1 = torch.rand(Nu // 4, dtype=torch.float32)\n",
+ "xx_1 = torch.ones(Nu // 4, dtype=torch.float32)\n",
+ "uu_1 = torch.zeros(Nu // 4, dtype=torch.float32)\n",
+ "b_1 = torch.ones(Nu // 4, dtype=torch.float32)\n",
"#\n",
- "tt_2 = torch.rand(Nu//4, dtype=torch.float32)\n",
- "xx_2 = -torch.ones(Nu//4, dtype=torch.float32)\n",
- "uu_2 = torch.zeros(Nu//4, dtype=torch.float32)\n",
- "b_2 = torch.ones(Nu//4, dtype=torch.float32)\n",
+ "tt_2 = torch.rand(Nu // 4, dtype=torch.float32)\n",
+ "xx_2 = -torch.ones(Nu // 4, dtype=torch.float32)\n",
+ "uu_2 = torch.zeros(Nu // 4, dtype=torch.float32)\n",
+ "b_2 = torch.ones(Nu // 4, dtype=torch.float32)\n",
"# Internal points\n",
"Nf = 10000\n",
"tt_3 = torch.rand(Nf, dtype=torch.float32)\n",
@@ -1604,13 +1613,13 @@
"uu_3 = torch.zeros(Nf, dtype=torch.float32)\n",
"b_3 = torch.zeros(Nf, dtype=torch.float32)\n",
"\n",
- "data = {'x':torch.cat((xx_0, xx_1, xx_2, xx_3)),\n",
- " 't':torch.cat((tt_0, tt_1, tt_2, tt_3)),\n",
- " 'u_target':torch.cat((uu_0, uu_1, uu_2, uu_3)),\n",
- " 'b':torch.cat((b_0, b_1, b_2, b_3))}"
- ],
- "outputs": [],
- "execution_count": 5
+ "data = {\n",
+ " \"x\": torch.cat((xx_0, xx_1, xx_2, xx_3)),\n",
+ " \"t\": torch.cat((tt_0, tt_1, tt_2, tt_3)),\n",
+ " \"u_target\": torch.cat((uu_0, uu_1, uu_2, uu_3)),\n",
+ " \"b\": torch.cat((b_0, b_1, b_2, b_3)),\n",
+ "}"
+ ]
},
{
"cell_type": "markdown",
@@ -1622,6 +1631,7 @@
},
{
"cell_type": "code",
+ "execution_count": 6,
"id": "4a20e7b55167eb6c",
"metadata": {
"ExecuteTime": {
@@ -1629,52 +1639,43 @@
"start_time": "2026-02-25T17:39:07.718011Z"
}
},
- "source": [
- "from nnodely.support import earlystopping\n",
- "pinn.loadData('dataset2',data)\n",
- "pinn.trainModel(train_dataset='dataset2', train_batch_size=128, num_of_epochs=5000, lr=0.0005, \n",
- " minimize_gain={'errorU':1,'errorF':0.0005}, \n",
- " early_stopping=earlystopping.early_stop_patience, \n",
- " early_stopping_params={'patience':500, 'error':'errorU'}, \n",
- " select_model=earlystopping.select_best_model)"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[1;32m============================ nnodely Model Dataset =============================\u001B[0m\n",
- "\u001B[32mDataset Name: dataset2\u001B[0m\n",
- "\u001B[32mNumber of files: 1\u001B[0m\n",
- "\u001B[32mTotal number of samples: 10100\u001B[0m\n",
- "\u001B[32mShape of b: (10100, 1, 1)\u001B[0m\n",
- "\u001B[32mShape of x: (10100, 1, 1)\u001B[0m\n",
- "\u001B[32mShape of t: (10100, 1, 1)\u001B[0m\n",
- "\u001B[32mShape of u_target: (10100, 1, 1)\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[_setup_recurrent_variables] The value of the prediction_samples=0 but the network has no recurrent variables.\u001B[0m\n",
- "\u001B[1;32m======================== nnodely Model Train Parameters ========================\u001B[0m\n",
- "\u001B[32mmodels: ['pinn']\u001B[0m\n",
- "\u001B[32mnum of epochs: 5000\u001B[0m\n",
- "\u001B[32mupdate per epochs: 78\u001B[0m\n",
- "\u001B[34mâ””>(n_samples-batch_size)/batch_size+1\u001B[0m\n",
- "\u001B[32mshuffle data: True\u001B[0m\n",
- "\u001B[32mearly stopping: early_stop_patience\u001B[0m\n",
- "\u001B[32mearly stopping params: {'error': 'errorU', 'patience': 500}\u001B[0m\n",
- "\u001B[32mtrain dataset: dataset2\u001B[0m\n",
- "\u001B[32m\t- batch size: 128\u001B[0m\n",
- "\u001B[32m\t- num of samples: 10100\u001B[0m\n",
- "\u001B[32mminimizers: {'errorF': {'A': 'Sub22',\n",
+ "\u001b[1;32m============================ nnodely Model Dataset =============================\u001b[0m\n",
+ "\u001b[32mDataset Name: dataset2\u001b[0m\n",
+ "\u001b[32mNumber of files: 1\u001b[0m\n",
+ "\u001b[32mTotal number of samples: 10100\u001b[0m\n",
+ "\u001b[32mShape of b: (10100, 1, 1)\u001b[0m\n",
+ "\u001b[32mShape of x: (10100, 1, 1)\u001b[0m\n",
+ "\u001b[32mShape of t: (10100, 1, 1)\u001b[0m\n",
+ "\u001b[32mShape of u_target: (10100, 1, 1)\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[_setup_recurrent_variables] The value of the prediction_samples=0 but the network has no recurrent variables.\u001b[0m\n",
+ "\u001b[1;32m======================== nnodely Model Train Parameters ========================\u001b[0m\n",
+ "\u001b[32mmodels: ['pinn']\u001b[0m\n",
+ "\u001b[32mnum of epochs: 5000\u001b[0m\n",
+ "\u001b[32mupdate per epochs: 78\u001b[0m\n",
+ "\u001b[34mâ””>(n_samples-batch_size)/batch_size+1\u001b[0m\n",
+ "\u001b[32mshuffle data: True\u001b[0m\n",
+ "\u001b[32mearly stopping: early_stop_patience\u001b[0m\n",
+ "\u001b[32mearly stopping params: {'error': 'errorU', 'patience': 500}\u001b[0m\n",
+ "\u001b[32mtrain dataset: dataset2\u001b[0m\n",
+ "\u001b[32m\t- batch size: 128\u001b[0m\n",
+ "\u001b[32m\t- num of samples: 10100\u001b[0m\n",
+ "\u001b[32mminimizers: {'errorF': {'A': 'Sub22',\n",
" 'B': 'Mul30',\n",
" 'gain': 0.0005,\n",
" 'loss': 'mse'},\n",
" 'errorU': {'A': 'Mul27',\n",
" 'B': 'Mul28',\n",
" 'gain': 1,\n",
- " 'loss': 'mse'}}\u001B[0m\n",
- "\u001B[32moptimizer: Adam\u001B[0m\n",
- "\u001B[32moptimizer defaults: {'lr': 0.0005}\u001B[0m\n",
- "\u001B[32moptimizer params: [{'params': 'PLinear12W'},\n",
+ " 'loss': 'mse'}}\u001b[0m\n",
+ "\u001b[32moptimizer: Adam\u001b[0m\n",
+ "\u001b[32moptimizer defaults: {'lr': 0.0005}\u001b[0m\n",
+ "\u001b[32moptimizer params: [{'params': 'PLinear12W'},\n",
" {'params': 'PLinear12b'},\n",
" {'params': 'PLinear15W'},\n",
" {'params': 'PLinear15b'},\n",
@@ -1683,92 +1684,106 @@
" {'params': 'PLinear6W'},\n",
" {'params': 'PLinear6b'},\n",
" {'params': 'PLinear9W'},\n",
- " {'params': 'PLinear9b'}]\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[1;32m=========================== nnodely Training ===========================\u001B[0m\n",
- "\u001B[32m| Epoch |\u001B[0m\u001B[32m errorU |\u001B[0m\u001B[32m errorF |\u001B[0m\u001B[32m Total |\u001B[0m\n",
- "\u001B[32m| |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\n",
- "\u001B[32m| |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m train |\u001B[0m\n",
- "\u001B[32m|----------------------------------------------------------------------|\u001B[0m\n",
- "\u001B[32m| 50/5000 |\u001B[0m\u001B[32m 9.018e-02 |\u001B[0m\u001B[32m 1.413e+00 |\u001B[0m\u001B[32m 7.515e-01 |\u001B[0m\n",
- "\u001B[32m| 100/5000 |\u001B[0m\u001B[32m 1.886e-02 |\u001B[0m\u001B[32m 4.612e-02 |\u001B[0m\u001B[32m 3.249e-02 |\u001B[0m\n",
- "\u001B[32m| 150/5000 |\u001B[0m\u001B[32m 3.863e-03 |\u001B[0m\u001B[32m 1.532e-03 |\u001B[0m\u001B[32m 2.698e-03 |\u001B[0m\n",
- "\u001B[32m| 200/5000 |\u001B[0m\u001B[32m 1.946e-03 |\u001B[0m\u001B[32m 3.386e-04 |\u001B[0m\u001B[32m 1.142e-03 |\u001B[0m\n",
- "\u001B[32m| 250/5000 |\u001B[0m\u001B[32m 1.838e-03 |\u001B[0m\u001B[32m 5.892e-04 |\u001B[0m\u001B[32m 1.214e-03 |\u001B[0m\n",
- "\u001B[32m| 300/5000 |\u001B[0m\u001B[32m 1.732e-03 |\u001B[0m\u001B[32m 3.606e-04 |\u001B[0m\u001B[32m 1.046e-03 |\u001B[0m\n",
- "\u001B[32m| 350/5000 |\u001B[0m\u001B[32m 1.636e-03 |\u001B[0m\u001B[32m 4.242e-04 |\u001B[0m\u001B[32m 1.030e-03 |\u001B[0m\n",
- "\u001B[32m| 400/5000 |\u001B[0m\u001B[32m 1.564e-03 |\u001B[0m\u001B[32m 2.444e-04 |\u001B[0m\u001B[32m 9.043e-04 |\u001B[0m\n",
- "\u001B[32m| 450/5000 |\u001B[0m\u001B[32m 1.543e-03 |\u001B[0m\u001B[32m 1.600e-04 |\u001B[0m\u001B[32m 8.514e-04 |\u001B[0m\n",
- "\u001B[32m| 500/5000 |\u001B[0m\u001B[32m 1.53e-03 |\u001B[0m\u001B[32m 1.101e-04 |\u001B[0m\u001B[32m 8.200e-04 |\u001B[0m\n",
- "\u001B[32m| 550/5000 |\u001B[0m\u001B[32m 1.505e-03 |\u001B[0m\u001B[32m 6.108e-05 |\u001B[0m\u001B[32m 7.830e-04 |\u001B[0m\n",
- "\u001B[32m| 600/5000 |\u001B[0m\u001B[32m 1.504e-03 |\u001B[0m\u001B[32m 3.504e-05 |\u001B[0m\u001B[32m 7.694e-04 |\u001B[0m\n",
- "\u001B[32m| 650/5000 |\u001B[0m\u001B[32m 3.171e-04 |\u001B[0m\u001B[32m 1.438e-04 |\u001B[0m\u001B[32m 2.304e-04 |\u001B[0m\n",
- "\u001B[32m| 700/5000 |\u001B[0m\u001B[32m 8.892e-05 |\u001B[0m\u001B[32m 1.083e-04 |\u001B[0m\u001B[32m 9.863e-05 |\u001B[0m\n",
- "\u001B[32m| 750/5000 |\u001B[0m\u001B[32m 4.373e-05 |\u001B[0m\u001B[32m 8.569e-05 |\u001B[0m\u001B[32m 6.471e-05 |\u001B[0m\n",
- "\u001B[32m| 800/5000 |\u001B[0m\u001B[32m 2.790e-05 |\u001B[0m\u001B[32m 5.551e-05 |\u001B[0m\u001B[32m 4.170e-05 |\u001B[0m\n",
- "\u001B[32m| 850/5000 |\u001B[0m\u001B[32m 3.441e-05 |\u001B[0m\u001B[32m 5.859e-05 |\u001B[0m\u001B[32m 4.650e-05 |\u001B[0m\n",
- "\u001B[32m| 900/5000 |\u001B[0m\u001B[32m 6.985e-06 |\u001B[0m\u001B[32m 1.973e-05 |\u001B[0m\u001B[32m 1.336e-05 |\u001B[0m\n",
- "\u001B[32m| 950/5000 |\u001B[0m\u001B[32m 6.454e-06 |\u001B[0m\u001B[32m 1.649e-05 |\u001B[0m\u001B[32m 1.147e-05 |\u001B[0m\n",
- "\u001B[32m|1000/5000 |\u001B[0m\u001B[32m 7.318e-06 |\u001B[0m\u001B[32m 1.616e-05 |\u001B[0m\u001B[32m 1.174e-05 |\u001B[0m\n",
- "\u001B[32m|1050/5000 |\u001B[0m\u001B[32m 7.773e-06 |\u001B[0m\u001B[32m 1.579e-05 |\u001B[0m\u001B[32m 1.178e-05 |\u001B[0m\n",
- "\u001B[32m|1100/5000 |\u001B[0m\u001B[32m 5.03e-06 |\u001B[0m\u001B[32m 1.188e-05 |\u001B[0m\u001B[32m 8.453e-06 |\u001B[0m\n",
- "\u001B[32m|1150/5000 |\u001B[0m\u001B[32m 3.668e-06 |\u001B[0m\u001B[32m 8.326e-06 |\u001B[0m\u001B[32m 5.997e-06 |\u001B[0m\n",
- "\u001B[32m|1200/5000 |\u001B[0m\u001B[32m 2.6e-06 |\u001B[0m\u001B[32m 7.771e-06 |\u001B[0m\u001B[32m 5.185e-06 |\u001B[0m\n",
- "\u001B[32m|1250/5000 |\u001B[0m\u001B[32m 8.389e-06 |\u001B[0m\u001B[32m 1.483e-05 |\u001B[0m\u001B[32m 1.161e-05 |\u001B[0m\n",
- "\u001B[32m|1300/5000 |\u001B[0m\u001B[32m 2.944e-06 |\u001B[0m\u001B[32m 6.592e-06 |\u001B[0m\u001B[32m 4.768e-06 |\u001B[0m\n",
- "\u001B[32m|1350/5000 |\u001B[0m\u001B[32m 4.007e-06 |\u001B[0m\u001B[32m 8.243e-06 |\u001B[0m\u001B[32m 6.125e-06 |\u001B[0m\n",
- "\u001B[32m|1400/5000 |\u001B[0m\u001B[32m 4.226e-06 |\u001B[0m\u001B[32m 8.995e-06 |\u001B[0m\u001B[32m 6.610e-06 |\u001B[0m\n",
- "\u001B[32m|1450/5000 |\u001B[0m\u001B[32m 2.225e-05 |\u001B[0m\u001B[32m 2.984e-05 |\u001B[0m\u001B[32m 2.604e-05 |\u001B[0m\n",
- "\u001B[32m|1500/5000 |\u001B[0m\u001B[32m 1.576e-05 |\u001B[0m\u001B[32m 2.343e-05 |\u001B[0m\u001B[32m 1.959e-05 |\u001B[0m\n",
- "\u001B[32m|1550/5000 |\u001B[0m\u001B[32m 5.138e-06 |\u001B[0m\u001B[32m 1.073e-05 |\u001B[0m\u001B[32m 7.933e-06 |\u001B[0m\n",
- "\u001B[32m|1600/5000 |\u001B[0m\u001B[32m 2.006e-06 |\u001B[0m\u001B[32m 4.534e-06 |\u001B[0m\u001B[32m 3.270e-06 |\u001B[0m\n",
- "\u001B[32m|1650/5000 |\u001B[0m\u001B[32m 3.014e-06 |\u001B[0m\u001B[32m 7.368e-06 |\u001B[0m\u001B[32m 5.191e-06 |\u001B[0m\n",
- "\u001B[32m|1700/5000 |\u001B[0m\u001B[32m 1.711e-06 |\u001B[0m\u001B[32m 3.738e-06 |\u001B[0m\u001B[32m 2.724e-06 |\u001B[0m\n",
- "\u001B[32m|1750/5000 |\u001B[0m\u001B[32m 1.402e-06 |\u001B[0m\u001B[32m 3.050e-06 |\u001B[0m\u001B[32m 2.226e-06 |\u001B[0m\n",
- "\u001B[32m|1800/5000 |\u001B[0m\u001B[32m 5.405e-06 |\u001B[0m\u001B[32m 6.894e-06 |\u001B[0m\u001B[32m 6.15e-06 |\u001B[0m\n",
- "\u001B[32m|1850/5000 |\u001B[0m\u001B[32m 4.134e-06 |\u001B[0m\u001B[32m 4.535e-06 |\u001B[0m\u001B[32m 4.334e-06 |\u001B[0m\n",
- "\u001B[32m|1900/5000 |\u001B[0m\u001B[32m 1.579e-06 |\u001B[0m\u001B[32m 2.762e-06 |\u001B[0m\u001B[32m 2.171e-06 |\u001B[0m\n",
- "\u001B[32m|1950/5000 |\u001B[0m\u001B[32m 2.646e-06 |\u001B[0m\u001B[32m 5.012e-06 |\u001B[0m\u001B[32m 3.829e-06 |\u001B[0m\n",
- "\u001B[32m|2000/5000 |\u001B[0m\u001B[32m 1.381e-06 |\u001B[0m\u001B[32m 1.573e-06 |\u001B[0m\u001B[32m 1.477e-06 |\u001B[0m\n",
- "\u001B[32m|2050/5000 |\u001B[0m\u001B[32m 1.613e-06 |\u001B[0m\u001B[32m 3.587e-06 |\u001B[0m\u001B[32m 2.6e-06 |\u001B[0m\n",
- "\u001B[32m|2100/5000 |\u001B[0m\u001B[32m 2.693e-06 |\u001B[0m\u001B[32m 3.447e-06 |\u001B[0m\u001B[32m 3.07e-06 |\u001B[0m\n",
- "\u001B[32m|2150/5000 |\u001B[0m\u001B[32m 1.465e-06 |\u001B[0m\u001B[32m 1.942e-06 |\u001B[0m\u001B[32m 1.704e-06 |\u001B[0m\n",
- "\u001B[32m|2200/5000 |\u001B[0m\u001B[32m 1.141e-06 |\u001B[0m\u001B[32m 1.933e-06 |\u001B[0m\u001B[32m 1.537e-06 |\u001B[0m\n",
- "\u001B[32m|2250/5000 |\u001B[0m\u001B[32m 4.073e-06 |\u001B[0m\u001B[32m 3.550e-06 |\u001B[0m\u001B[32m 3.812e-06 |\u001B[0m\n",
- "\u001B[32m|2300/5000 |\u001B[0m\u001B[32m 2.918e-06 |\u001B[0m\u001B[32m 2.235e-06 |\u001B[0m\u001B[32m 2.576e-06 |\u001B[0m\n",
- "\u001B[32m|2350/5000 |\u001B[0m\u001B[32m 2.835e-06 |\u001B[0m\u001B[32m 1.932e-06 |\u001B[0m\u001B[32m 2.384e-06 |\u001B[0m\n",
- "\u001B[32m|2400/5000 |\u001B[0m\u001B[32m 7.148e-06 |\u001B[0m\u001B[32m 6.592e-06 |\u001B[0m\u001B[32m 6.870e-06 |\u001B[0m\n",
- "\u001B[32m|2450/5000 |\u001B[0m\u001B[32m 8.410e-07 |\u001B[0m\u001B[32m 1.945e-06 |\u001B[0m\u001B[32m 1.393e-06 |\u001B[0m\n",
- "\u001B[32m|2500/5000 |\u001B[0m\u001B[32m 6.379e-07 |\u001B[0m\u001B[32m 9.164e-07 |\u001B[0m\u001B[32m 7.772e-07 |\u001B[0m\n",
- "\u001B[32m|2550/5000 |\u001B[0m\u001B[32m 1.717e-06 |\u001B[0m\u001B[32m 2.593e-06 |\u001B[0m\u001B[32m 2.155e-06 |\u001B[0m\n",
- "\u001B[32m|2600/5000 |\u001B[0m\u001B[32m 2.722e-06 |\u001B[0m\u001B[32m 7.063e-06 |\u001B[0m\u001B[32m 4.892e-06 |\u001B[0m\n",
- "\u001B[32m|2650/5000 |\u001B[0m\u001B[32m 5.706e-07 |\u001B[0m\u001B[32m 1.253e-06 |\u001B[0m\u001B[32m 9.120e-07 |\u001B[0m\n",
- "\u001B[32m|2700/5000 |\u001B[0m\u001B[32m 3.990e-07 |\u001B[0m\u001B[32m 1.038e-06 |\u001B[0m\u001B[32m 7.186e-07 |\u001B[0m\n",
- "\u001B[32m|2750/5000 |\u001B[0m\u001B[32m 9.519e-07 |\u001B[0m\u001B[32m 1.662e-06 |\u001B[0m\u001B[32m 1.307e-06 |\u001B[0m\n",
- "\u001B[32m|2800/5000 |\u001B[0m\u001B[32m 4.111e-06 |\u001B[0m\u001B[32m 3.098e-06 |\u001B[0m\u001B[32m 3.605e-06 |\u001B[0m\n",
- "\u001B[32m|2850/5000 |\u001B[0m\u001B[32m 1.347e-06 |\u001B[0m\u001B[32m 1.464e-06 |\u001B[0m\u001B[32m 1.405e-06 |\u001B[0m\n",
- "\u001B[32m|2900/5000 |\u001B[0m\u001B[32m 5.103e-07 |\u001B[0m\u001B[32m 1.178e-06 |\u001B[0m\u001B[32m 8.44e-07 |\u001B[0m\n",
- "\u001B[32m|2950/5000 |\u001B[0m\u001B[32m 5.521e-07 |\u001B[0m\u001B[32m 1.155e-06 |\u001B[0m\u001B[32m 8.536e-07 |\u001B[0m\n",
- "\u001B[32m|3000/5000 |\u001B[0m\u001B[32m 5.444e-07 |\u001B[0m\u001B[32m 7.651e-07 |\u001B[0m\u001B[32m 6.548e-07 |\u001B[0m\n",
- "\u001B[32m|3050/5000 |\u001B[0m\u001B[32m 4.684e-07 |\u001B[0m\u001B[32m 6.802e-07 |\u001B[0m\u001B[32m 5.743e-07 |\u001B[0m\n",
- "\u001B[32m|3100/5000 |\u001B[0m\u001B[32m 7.432e-07 |\u001B[0m\u001B[32m 1.625e-06 |\u001B[0m\u001B[32m 1.184e-06 |\u001B[0m\n",
- "\u001B[32m|3150/5000 |\u001B[0m\u001B[32m 1.323e-06 |\u001B[0m\u001B[32m 2.188e-06 |\u001B[0m\u001B[32m 1.755e-06 |\u001B[0m\n",
- "\u001B[32m|3200/5000 |\u001B[0m\u001B[32m 1.093e-06 |\u001B[0m\u001B[32m 1.363e-06 |\u001B[0m\u001B[32m 1.228e-06 |\u001B[0m\n",
- "\u001B[32m|3250/5000 |\u001B[0m\u001B[32m 1.243e-06 |\u001B[0m\u001B[32m 1.959e-06 |\u001B[0m\u001B[32m 1.601e-06 |\u001B[0m\n",
- "\u001B[32m|3300/5000 |\u001B[0m\u001B[32m 1.701e-06 |\u001B[0m\u001B[32m 3.564e-06 |\u001B[0m\u001B[32m 2.632e-06 |\u001B[0m\n",
- "\u001B[32m|3350/5000 |\u001B[0m\u001B[32m 5.400e-07 |\u001B[0m\u001B[32m 7.495e-07 |\u001B[0m\u001B[32m 6.448e-07 |\u001B[0m\n",
- "\u001B[32m|3400/5000 |\u001B[0m\u001B[32m 6.398e-07 |\u001B[0m\u001B[32m 1.927e-06 |\u001B[0m\u001B[32m 1.283e-06 |\u001B[0m\n",
- "\u001B[32m|3450/5000 |\u001B[0m\u001B[32m 7.234e-06 |\u001B[0m\u001B[32m 1.095e-05 |\u001B[0m\u001B[32m 9.094e-06 |\u001B[0m\n",
- "|3485/5000 | 6.045e-07 | 9.439e-07 | 7.742e-07 |\u001B[34mStopping the training at epoch 3485 due to early stopping.\u001B[0m\n",
- "\u001B[1;32m============================ nnodely Training Time =============================\u001B[0m\n",
- "\u001B[32mTotal time of Training: 694.5884737968445\u001B[0m\n",
- "\u001B[32m================================================================================\u001B[0m\n",
- "\u001B[33m[trainModel] If not validation set is provided the selected model can differ from the optimal.\u001B[0m\n",
- "\u001B[34mSelected the model at the epoch 3461.\u001B[0m\n"
+ " {'params': 'PLinear9b'}]\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[1;32m=========================== nnodely Training ===========================\u001b[0m\n",
+ "\u001b[32m| Epoch |\u001b[0m\u001b[32m errorU |\u001b[0m\u001b[32m errorF |\u001b[0m\u001b[32m Total |\u001b[0m\n",
+ "\u001b[32m| |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\n",
+ "\u001b[32m| |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m train |\u001b[0m\n",
+ "\u001b[32m|----------------------------------------------------------------------|\u001b[0m\n",
+ "\u001b[32m| 50/5000 |\u001b[0m\u001b[32m 9.018e-02 |\u001b[0m\u001b[32m 1.413e+00 |\u001b[0m\u001b[32m 7.515e-01 |\u001b[0m\n",
+ "\u001b[32m| 100/5000 |\u001b[0m\u001b[32m 1.886e-02 |\u001b[0m\u001b[32m 4.612e-02 |\u001b[0m\u001b[32m 3.249e-02 |\u001b[0m\n",
+ "\u001b[32m| 150/5000 |\u001b[0m\u001b[32m 3.863e-03 |\u001b[0m\u001b[32m 1.532e-03 |\u001b[0m\u001b[32m 2.698e-03 |\u001b[0m\n",
+ "\u001b[32m| 200/5000 |\u001b[0m\u001b[32m 1.946e-03 |\u001b[0m\u001b[32m 3.386e-04 |\u001b[0m\u001b[32m 1.142e-03 |\u001b[0m\n",
+ "\u001b[32m| 250/5000 |\u001b[0m\u001b[32m 1.838e-03 |\u001b[0m\u001b[32m 5.892e-04 |\u001b[0m\u001b[32m 1.214e-03 |\u001b[0m\n",
+ "\u001b[32m| 300/5000 |\u001b[0m\u001b[32m 1.732e-03 |\u001b[0m\u001b[32m 3.606e-04 |\u001b[0m\u001b[32m 1.046e-03 |\u001b[0m\n",
+ "\u001b[32m| 350/5000 |\u001b[0m\u001b[32m 1.636e-03 |\u001b[0m\u001b[32m 4.242e-04 |\u001b[0m\u001b[32m 1.030e-03 |\u001b[0m\n",
+ "\u001b[32m| 400/5000 |\u001b[0m\u001b[32m 1.564e-03 |\u001b[0m\u001b[32m 2.444e-04 |\u001b[0m\u001b[32m 9.043e-04 |\u001b[0m\n",
+ "\u001b[32m| 450/5000 |\u001b[0m\u001b[32m 1.543e-03 |\u001b[0m\u001b[32m 1.600e-04 |\u001b[0m\u001b[32m 8.514e-04 |\u001b[0m\n",
+ "\u001b[32m| 500/5000 |\u001b[0m\u001b[32m 1.53e-03 |\u001b[0m\u001b[32m 1.101e-04 |\u001b[0m\u001b[32m 8.200e-04 |\u001b[0m\n",
+ "\u001b[32m| 550/5000 |\u001b[0m\u001b[32m 1.505e-03 |\u001b[0m\u001b[32m 6.108e-05 |\u001b[0m\u001b[32m 7.830e-04 |\u001b[0m\n",
+ "\u001b[32m| 600/5000 |\u001b[0m\u001b[32m 1.504e-03 |\u001b[0m\u001b[32m 3.504e-05 |\u001b[0m\u001b[32m 7.694e-04 |\u001b[0m\n",
+ "\u001b[32m| 650/5000 |\u001b[0m\u001b[32m 3.171e-04 |\u001b[0m\u001b[32m 1.438e-04 |\u001b[0m\u001b[32m 2.304e-04 |\u001b[0m\n",
+ "\u001b[32m| 700/5000 |\u001b[0m\u001b[32m 8.892e-05 |\u001b[0m\u001b[32m 1.083e-04 |\u001b[0m\u001b[32m 9.863e-05 |\u001b[0m\n",
+ "\u001b[32m| 750/5000 |\u001b[0m\u001b[32m 4.373e-05 |\u001b[0m\u001b[32m 8.569e-05 |\u001b[0m\u001b[32m 6.471e-05 |\u001b[0m\n",
+ "\u001b[32m| 800/5000 |\u001b[0m\u001b[32m 2.790e-05 |\u001b[0m\u001b[32m 5.551e-05 |\u001b[0m\u001b[32m 4.170e-05 |\u001b[0m\n",
+ "\u001b[32m| 850/5000 |\u001b[0m\u001b[32m 3.441e-05 |\u001b[0m\u001b[32m 5.859e-05 |\u001b[0m\u001b[32m 4.650e-05 |\u001b[0m\n",
+ "\u001b[32m| 900/5000 |\u001b[0m\u001b[32m 6.985e-06 |\u001b[0m\u001b[32m 1.973e-05 |\u001b[0m\u001b[32m 1.336e-05 |\u001b[0m\n",
+ "\u001b[32m| 950/5000 |\u001b[0m\u001b[32m 6.454e-06 |\u001b[0m\u001b[32m 1.649e-05 |\u001b[0m\u001b[32m 1.147e-05 |\u001b[0m\n",
+ "\u001b[32m|1000/5000 |\u001b[0m\u001b[32m 7.318e-06 |\u001b[0m\u001b[32m 1.616e-05 |\u001b[0m\u001b[32m 1.174e-05 |\u001b[0m\n",
+ "\u001b[32m|1050/5000 |\u001b[0m\u001b[32m 7.773e-06 |\u001b[0m\u001b[32m 1.579e-05 |\u001b[0m\u001b[32m 1.178e-05 |\u001b[0m\n",
+ "\u001b[32m|1100/5000 |\u001b[0m\u001b[32m 5.03e-06 |\u001b[0m\u001b[32m 1.188e-05 |\u001b[0m\u001b[32m 8.453e-06 |\u001b[0m\n",
+ "\u001b[32m|1150/5000 |\u001b[0m\u001b[32m 3.668e-06 |\u001b[0m\u001b[32m 8.326e-06 |\u001b[0m\u001b[32m 5.997e-06 |\u001b[0m\n",
+ "\u001b[32m|1200/5000 |\u001b[0m\u001b[32m 2.6e-06 |\u001b[0m\u001b[32m 7.771e-06 |\u001b[0m\u001b[32m 5.185e-06 |\u001b[0m\n",
+ "\u001b[32m|1250/5000 |\u001b[0m\u001b[32m 8.389e-06 |\u001b[0m\u001b[32m 1.483e-05 |\u001b[0m\u001b[32m 1.161e-05 |\u001b[0m\n",
+ "\u001b[32m|1300/5000 |\u001b[0m\u001b[32m 2.944e-06 |\u001b[0m\u001b[32m 6.592e-06 |\u001b[0m\u001b[32m 4.768e-06 |\u001b[0m\n",
+ "\u001b[32m|1350/5000 |\u001b[0m\u001b[32m 4.007e-06 |\u001b[0m\u001b[32m 8.243e-06 |\u001b[0m\u001b[32m 6.125e-06 |\u001b[0m\n",
+ "\u001b[32m|1400/5000 |\u001b[0m\u001b[32m 4.226e-06 |\u001b[0m\u001b[32m 8.995e-06 |\u001b[0m\u001b[32m 6.610e-06 |\u001b[0m\n",
+ "\u001b[32m|1450/5000 |\u001b[0m\u001b[32m 2.225e-05 |\u001b[0m\u001b[32m 2.984e-05 |\u001b[0m\u001b[32m 2.604e-05 |\u001b[0m\n",
+ "\u001b[32m|1500/5000 |\u001b[0m\u001b[32m 1.576e-05 |\u001b[0m\u001b[32m 2.343e-05 |\u001b[0m\u001b[32m 1.959e-05 |\u001b[0m\n",
+ "\u001b[32m|1550/5000 |\u001b[0m\u001b[32m 5.138e-06 |\u001b[0m\u001b[32m 1.073e-05 |\u001b[0m\u001b[32m 7.933e-06 |\u001b[0m\n",
+ "\u001b[32m|1600/5000 |\u001b[0m\u001b[32m 2.006e-06 |\u001b[0m\u001b[32m 4.534e-06 |\u001b[0m\u001b[32m 3.270e-06 |\u001b[0m\n",
+ "\u001b[32m|1650/5000 |\u001b[0m\u001b[32m 3.014e-06 |\u001b[0m\u001b[32m 7.368e-06 |\u001b[0m\u001b[32m 5.191e-06 |\u001b[0m\n",
+ "\u001b[32m|1700/5000 |\u001b[0m\u001b[32m 1.711e-06 |\u001b[0m\u001b[32m 3.738e-06 |\u001b[0m\u001b[32m 2.724e-06 |\u001b[0m\n",
+ "\u001b[32m|1750/5000 |\u001b[0m\u001b[32m 1.402e-06 |\u001b[0m\u001b[32m 3.050e-06 |\u001b[0m\u001b[32m 2.226e-06 |\u001b[0m\n",
+ "\u001b[32m|1800/5000 |\u001b[0m\u001b[32m 5.405e-06 |\u001b[0m\u001b[32m 6.894e-06 |\u001b[0m\u001b[32m 6.15e-06 |\u001b[0m\n",
+ "\u001b[32m|1850/5000 |\u001b[0m\u001b[32m 4.134e-06 |\u001b[0m\u001b[32m 4.535e-06 |\u001b[0m\u001b[32m 4.334e-06 |\u001b[0m\n",
+ "\u001b[32m|1900/5000 |\u001b[0m\u001b[32m 1.579e-06 |\u001b[0m\u001b[32m 2.762e-06 |\u001b[0m\u001b[32m 2.171e-06 |\u001b[0m\n",
+ "\u001b[32m|1950/5000 |\u001b[0m\u001b[32m 2.646e-06 |\u001b[0m\u001b[32m 5.012e-06 |\u001b[0m\u001b[32m 3.829e-06 |\u001b[0m\n",
+ "\u001b[32m|2000/5000 |\u001b[0m\u001b[32m 1.381e-06 |\u001b[0m\u001b[32m 1.573e-06 |\u001b[0m\u001b[32m 1.477e-06 |\u001b[0m\n",
+ "\u001b[32m|2050/5000 |\u001b[0m\u001b[32m 1.613e-06 |\u001b[0m\u001b[32m 3.587e-06 |\u001b[0m\u001b[32m 2.6e-06 |\u001b[0m\n",
+ "\u001b[32m|2100/5000 |\u001b[0m\u001b[32m 2.693e-06 |\u001b[0m\u001b[32m 3.447e-06 |\u001b[0m\u001b[32m 3.07e-06 |\u001b[0m\n",
+ "\u001b[32m|2150/5000 |\u001b[0m\u001b[32m 1.465e-06 |\u001b[0m\u001b[32m 1.942e-06 |\u001b[0m\u001b[32m 1.704e-06 |\u001b[0m\n",
+ "\u001b[32m|2200/5000 |\u001b[0m\u001b[32m 1.141e-06 |\u001b[0m\u001b[32m 1.933e-06 |\u001b[0m\u001b[32m 1.537e-06 |\u001b[0m\n",
+ "\u001b[32m|2250/5000 |\u001b[0m\u001b[32m 4.073e-06 |\u001b[0m\u001b[32m 3.550e-06 |\u001b[0m\u001b[32m 3.812e-06 |\u001b[0m\n",
+ "\u001b[32m|2300/5000 |\u001b[0m\u001b[32m 2.918e-06 |\u001b[0m\u001b[32m 2.235e-06 |\u001b[0m\u001b[32m 2.576e-06 |\u001b[0m\n",
+ "\u001b[32m|2350/5000 |\u001b[0m\u001b[32m 2.835e-06 |\u001b[0m\u001b[32m 1.932e-06 |\u001b[0m\u001b[32m 2.384e-06 |\u001b[0m\n",
+ "\u001b[32m|2400/5000 |\u001b[0m\u001b[32m 7.148e-06 |\u001b[0m\u001b[32m 6.592e-06 |\u001b[0m\u001b[32m 6.870e-06 |\u001b[0m\n",
+ "\u001b[32m|2450/5000 |\u001b[0m\u001b[32m 8.410e-07 |\u001b[0m\u001b[32m 1.945e-06 |\u001b[0m\u001b[32m 1.393e-06 |\u001b[0m\n",
+ "\u001b[32m|2500/5000 |\u001b[0m\u001b[32m 6.379e-07 |\u001b[0m\u001b[32m 9.164e-07 |\u001b[0m\u001b[32m 7.772e-07 |\u001b[0m\n",
+ "\u001b[32m|2550/5000 |\u001b[0m\u001b[32m 1.717e-06 |\u001b[0m\u001b[32m 2.593e-06 |\u001b[0m\u001b[32m 2.155e-06 |\u001b[0m\n",
+ "\u001b[32m|2600/5000 |\u001b[0m\u001b[32m 2.722e-06 |\u001b[0m\u001b[32m 7.063e-06 |\u001b[0m\u001b[32m 4.892e-06 |\u001b[0m\n",
+ "\u001b[32m|2650/5000 |\u001b[0m\u001b[32m 5.706e-07 |\u001b[0m\u001b[32m 1.253e-06 |\u001b[0m\u001b[32m 9.120e-07 |\u001b[0m\n",
+ "\u001b[32m|2700/5000 |\u001b[0m\u001b[32m 3.990e-07 |\u001b[0m\u001b[32m 1.038e-06 |\u001b[0m\u001b[32m 7.186e-07 |\u001b[0m\n",
+ "\u001b[32m|2750/5000 |\u001b[0m\u001b[32m 9.519e-07 |\u001b[0m\u001b[32m 1.662e-06 |\u001b[0m\u001b[32m 1.307e-06 |\u001b[0m\n",
+ "\u001b[32m|2800/5000 |\u001b[0m\u001b[32m 4.111e-06 |\u001b[0m\u001b[32m 3.098e-06 |\u001b[0m\u001b[32m 3.605e-06 |\u001b[0m\n",
+ "\u001b[32m|2850/5000 |\u001b[0m\u001b[32m 1.347e-06 |\u001b[0m\u001b[32m 1.464e-06 |\u001b[0m\u001b[32m 1.405e-06 |\u001b[0m\n",
+ "\u001b[32m|2900/5000 |\u001b[0m\u001b[32m 5.103e-07 |\u001b[0m\u001b[32m 1.178e-06 |\u001b[0m\u001b[32m 8.44e-07 |\u001b[0m\n",
+ "\u001b[32m|2950/5000 |\u001b[0m\u001b[32m 5.521e-07 |\u001b[0m\u001b[32m 1.155e-06 |\u001b[0m\u001b[32m 8.536e-07 |\u001b[0m\n",
+ "\u001b[32m|3000/5000 |\u001b[0m\u001b[32m 5.444e-07 |\u001b[0m\u001b[32m 7.651e-07 |\u001b[0m\u001b[32m 6.548e-07 |\u001b[0m\n",
+ "\u001b[32m|3050/5000 |\u001b[0m\u001b[32m 4.684e-07 |\u001b[0m\u001b[32m 6.802e-07 |\u001b[0m\u001b[32m 5.743e-07 |\u001b[0m\n",
+ "\u001b[32m|3100/5000 |\u001b[0m\u001b[32m 7.432e-07 |\u001b[0m\u001b[32m 1.625e-06 |\u001b[0m\u001b[32m 1.184e-06 |\u001b[0m\n",
+ "\u001b[32m|3150/5000 |\u001b[0m\u001b[32m 1.323e-06 |\u001b[0m\u001b[32m 2.188e-06 |\u001b[0m\u001b[32m 1.755e-06 |\u001b[0m\n",
+ "\u001b[32m|3200/5000 |\u001b[0m\u001b[32m 1.093e-06 |\u001b[0m\u001b[32m 1.363e-06 |\u001b[0m\u001b[32m 1.228e-06 |\u001b[0m\n",
+ "\u001b[32m|3250/5000 |\u001b[0m\u001b[32m 1.243e-06 |\u001b[0m\u001b[32m 1.959e-06 |\u001b[0m\u001b[32m 1.601e-06 |\u001b[0m\n",
+ "\u001b[32m|3300/5000 |\u001b[0m\u001b[32m 1.701e-06 |\u001b[0m\u001b[32m 3.564e-06 |\u001b[0m\u001b[32m 2.632e-06 |\u001b[0m\n",
+ "\u001b[32m|3350/5000 |\u001b[0m\u001b[32m 5.400e-07 |\u001b[0m\u001b[32m 7.495e-07 |\u001b[0m\u001b[32m 6.448e-07 |\u001b[0m\n",
+ "\u001b[32m|3400/5000 |\u001b[0m\u001b[32m 6.398e-07 |\u001b[0m\u001b[32m 1.927e-06 |\u001b[0m\u001b[32m 1.283e-06 |\u001b[0m\n",
+ "\u001b[32m|3450/5000 |\u001b[0m\u001b[32m 7.234e-06 |\u001b[0m\u001b[32m 1.095e-05 |\u001b[0m\u001b[32m 9.094e-06 |\u001b[0m\n",
+ "|3485/5000 | 6.045e-07 | 9.439e-07 | 7.742e-07 |\u001b[34mStopping the training at epoch 3485 due to early stopping.\u001b[0m\n",
+ "\u001b[1;32m============================ nnodely Training Time =============================\u001b[0m\n",
+ "\u001b[32mTotal time of Training: 694.5884737968445\u001b[0m\n",
+ "\u001b[32m================================================================================\u001b[0m\n",
+ "\u001b[33m[trainModel] If not validation set is provided the selected model can differ from the optimal.\u001b[0m\n",
+ "\u001b[34mSelected the model at the epoch 3461.\u001b[0m\n"
]
}
],
- "execution_count": 6
+ "source": [
+ "from nnodely.support import earlystopping\n",
+ "\n",
+ "pinn.loadData(\"dataset2\", data)\n",
+ "pinn.trainModel(\n",
+ " train_dataset=\"dataset2\",\n",
+ " train_batch_size=128,\n",
+ " num_of_epochs=5000,\n",
+ " lr=0.0005,\n",
+ " minimize_gain={\"errorU\": 1, \"errorF\": 0.0005},\n",
+ " early_stopping=earlystopping.early_stop_patience,\n",
+ " early_stopping_params={\"patience\": 500, \"error\": \"errorU\"},\n",
+ " select_model=earlystopping.select_best_model,\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1780,6 +1795,7 @@
},
{
"cell_type": "code",
+ "execution_count": 7,
"id": "51fa79586fe9b9be",
"metadata": {
"ExecuteTime": {
@@ -1787,153 +1803,155 @@
"start_time": "2026-02-25T17:50:42.919168Z"
}
},
- "source": [
- "# Custom visualizer for results\n",
- "class FunctionVisualizer(TextVisualizer):\n",
- " def showResults(self):\n",
- " import matplotlib.pyplot as plt\n",
- " plt.figure()\n",
- " plt.title('Initial Condition')\n",
- " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
- " t = torch.zeros(100, dtype=torch.float32)\n",
- " u_target = -torch.sin(torch.pi * x)\n",
- " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n",
- " plt.plot(x.tolist(), u['U'], label=f'network')\n",
- " plt.plot(x.tolist(), u_target.tolist(), label=f'target')\n",
- " plt.grid(True)\n",
- " plt.legend(loc='best')\n",
- " plt.xlabel('x[t=0]')\n",
- " plt.ylabel('u')\n",
- "\n",
- " plt.figure()\n",
- " plt.title('U value at t=[0.25,0.5,0.75,1]')\n",
- " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
- " t = torch.ones(100, dtype=torch.float32)*0.25\n",
- " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n",
- " plt.plot(x.tolist(), u['U'], label=f'network t=0.25')\n",
- " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
- " t = torch.ones(100, dtype=torch.float32)*0.5\n",
- " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n",
- " plt.plot(x.tolist(), u['U'], label=f'network t=0.5')\n",
- " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
- " t = torch.ones(100, dtype=torch.float32)*0.75\n",
- " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n",
- " plt.plot(x.tolist(), u['U'], label=f'network t=0.75')\n",
- " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
- " t = torch.ones(100, dtype=torch.float32)\n",
- " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n",
- " plt.plot(x.tolist(), u['U'], label=f'network t=1')\n",
- " plt.grid(True)\n",
- " plt.legend(loc='best')\n",
- " plt.xlabel('x')\n",
- " plt.ylabel('u')\n",
- "\n",
- " plt.figure()\n",
- " plt.title('Boudary Condition')\n",
- " t_1 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n",
- " x_1 = torch.ones(100, dtype=torch.float32)\n",
- " u_1_target = torch.zeros(100, dtype=torch.float32)\n",
- " u_1 = self.modely({'x': x_1.tolist(), 't': t_1.tolist()})\n",
- " plt.plot(t_1.tolist(), u_1['U'], label=f'network x=1')\n",
- " plt.plot(t_1.tolist(), u_1_target.tolist(), label=f'target x=1')\n",
- " t_2 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n",
- " x_2 = -torch.ones(100, dtype=torch.float32)\n",
- " u_2_target = torch.zeros(100, dtype=torch.float32)\n",
- " u_2 = self.modely({'x': x_2.tolist(), 't': t_2.tolist()})\n",
- " plt.plot(t_2.tolist(), u_2['U'], label=f'network x=-1')\n",
- " plt.plot(t_2.tolist(), u_2_target.tolist(), label=f'target x=-1')\n",
- " plt.grid(True)\n",
- " plt.legend(loc='best')\n",
- " plt.xlabel('x[t]')\n",
- " plt.ylabel('u')\n",
- "\n",
- " plt.figure()\n",
- " plt.title('Function Integration')\n",
- " t_3 = torch.linspace(0, 1, steps=100, dtype=torch.float32).numpy()\n",
- " x_3 = torch.linspace(-1, 1, steps=100, dtype=torch.float32).numpy()\n",
- " T, X = np.meshgrid(t_3, x_3)\n",
- " u_2 = self.modely({'x': X.flatten().tolist(), 't': T.flatten().tolist()})\n",
- " plt.contourf(T, X, np.array(u_2['U']).reshape(100,100))\n",
- " plt.xlabel('t')\n",
- " plt.ylabel('x')\n",
- " plt.show()\n",
- "\n",
- "res = FunctionVisualizer()\n",
- "res.setModely(pinn)\n",
- "res.showResults()"
- ],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n",
- "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n"
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n",
+ "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n"
]
},
{
"data": {
+ "image/png": "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S7fNChYsqMAyP3PZqa99+/YqMTFRvffee6p8+fLqyy+/VLGxsapMmTIKUJ06dVLR0dGqbdu2qm7duur06dNq9uzZmm+/7NQIKF9fXxUXF5fpOJs2bVLHjx9XdevWVbVr11b79u1TW7du1by+f0//Pe3eQj+D2r9ReWXKrCEKDg5Od42M8uXLqz179qj4+Hh1+vRp1aJFi3Trv/rqq+r8+fMqNjZW7dq1S9NroTyY9Hq9mj59urpz5466ceOGmjRp0iNrrF+/vlJKmS+c9u/JYDCoadOmqbCwMHX//n21efNm5enpaVX1vfHGG+rEiRMqLi5OnT171vwL7sHk5+enQkJCVExMjFq3bp0qWrSo1dS3fft2lZl/Xzvl888/V1euXFHx8fFqz549qmrVqprU5ejoqJYvX65iYmJUaGioGjBggHnZfy+SWq9ePXX06FEVFxengoKCVK1atSx6m2WnvnPnzmW6zR5c18USP3PZ3X49e/ZUf//9t4qPj1dHjhxRTZo0STfWsGHD1PXr11VUVJRavHix5tfJepIa3377bRUeHp7pOIULF1ZLlixRN27cUHfv3lUrV65UhQsX1ry+f0//bYgs8TOo+98/hBBCCCHyLDmGSAghhBB5njREQgghhMjzpCESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIWyWUgqlFNu3bwegZs2aNGzYMMvPL1q0KL/88gv37t3jn3/+oWvXrgC4urqax162bFmuZBdCPFvSEAkhbFqHDh145513ANiwYQMVK1bM8nOXL19OoUKFaNiwIePHj2fx4sXUq1ePGzdu4Obmxo8//phbsYUQz5i91gGEECI33blzx3z3cJ1Ol+XnlStXjjZt2lC2bFlCQkL466+/aNiwIf3796d79+5ERkYSHx+fS6mFEM+a7CESQli1Hj16kJCQQPny5QGoVKkS8fHxvPHGG+nWCwgIoGzZsixfvpxly5bh5+dn/trrv1OZMmVo0KABV69eJSQkxDxGYGBgtr5yE0JYD9lDJISwakuXLuW9995j5syZvPHGGyxatIj169ezefPmdOt16NCBkydPMm3aNJYvX05SUhI7duzIdMybN2/i7u5OeHh4uvmRkZF4enrmWi1CCO1IQySEsHoffPABJ0+eZPXq1VSqVIkOHTpkWCcqKorU1FTu3r3LvXv3AEhISHjomE5OTiQmJqabl5iYiMFgyNnwQgiLIF+ZCSGs3sWLF5k8eTJdu3ZlyJAh3L59+7HP6dKlCzExMZlOpUqVIiEhIUPzYzAYiIuLy60yhBAakj1EQgibULNmTVJSUmjevDmrV69+7PqbN2/m0KFDmS4LDw8nLCwMNze3dPPd3NyIiIjIkbxCCMsiDZEQwuq98cYbtGzZktatW7N582ZWrVpFQEBAhvWUUuZ/379/n/v37z90zKCgIMqWLYuHhwdhYWEA+Pj4EBQUlPMFCCE0J1+ZCSGsWoECBfjmm28YP348O3fuZO7cuSxcuDDTY31iY2OpXLkyRYoUeey4wcHB7Nixg1WrVlG9enV69OhBly5dmDdvXm6UIYSwAEommWSSyVqnOXPmqL///ls5ODgoQBUoUECFhYWpCRMmKKWU8vX1Na/br18/FRMTo9atW5elsYsXL642bdqk4uLi1OXLl1Xnzp3TLV+2bJlatmyZ5u+BTDLJ9PST7n//EEIIm6OUomnTpuzduzdXxn9w247u3bvnyvhCiGdHvjITQti0okWLUrhw4RwdU6fT4erqiqOjY46OK4TQjjREQgibtn79etasWZOjY5YoUYLr16/TqVOnHB1XCKEd+cpMCCGEEHme7CESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIYQQIs+ThkgIIYQQeZ40REIIIYTI86QhEkIIIUSe938qBo+/palfkAAAAABJRU5ErkJggg==",
"text/plain": [
""
- ],
- "image/png": "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S7fNChYsqMAyP3PZqa99+/YqMTFRvffee6p8+fLqyy+/VLGxsapMmTIKUJ06dVLR0dGqbdu2qm7duur06dNq9uzZmm+/7NQIKF9fXxUXF5fpOJs2bVLHjx9XdevWVbVr11b79u1TW7du1by+f0//Pe3eQj+D2r9ReWXKrCEKDg5Od42M8uXLqz179qj4+Hh1+vRp1aJFi3Trv/rqq+r8+fMqNjZW7dq1S9NroTyY9Hq9mj59urpz5466ceOGmjRp0iNrrF+/vlJKmS+c9u/JYDCoadOmqbCwMHX//n21efNm5enpaVX1vfHGG+rEiRMqLi5OnT171vwL7sHk5+enQkJCVExMjFq3bp0qWrSo1dS3fft2lZl/Xzvl888/V1euXFHx8fFqz549qmrVqprU5ejoqJYvX65iYmJUaGioGjBggHnZfy+SWq9ePXX06FEVFxengoKCVK1atSx6m2WnvnPnzmW6zR5c18USP3PZ3X49e/ZUf//9t4qPj1dHjhxRTZo0STfWsGHD1PXr11VUVJRavHix5tfJepIa3377bRUeHp7pOIULF1ZLlixRN27cUHfv3lUrV65UhQsX1ry+f0//bYgs8TOo+98/hBBCCCHyLDmGSAghhBB5njREQgghhMjzpCESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIWyWUgqlFNu3bwegZs2aNGzYMMvPL1q0KL/88gv37t3jn3/+oWvXrgC4urqax162bFmuZBdCPFvSEAkhbFqHDh145513ANiwYQMVK1bM8nOXL19OoUKFaNiwIePHj2fx4sXUq1ePGzdu4Obmxo8//phbsYUQz5i91gGEECI33blzx3z3cJ1Ol+XnlStXjjZt2lC2bFlCQkL466+/aNiwIf3796d79+5ERkYSHx+fS6mFEM+a7CESQli1Hj16kJCQQPny5QGoVKkS8fHxvPHGG+nWCwgIoGzZsixfvpxly5bh5+dn/trrv1OZMmVo0KABV69eJSQkxDxGYGBgtr5yE0JYD9lDJISwakuXLuW9995j5syZvPHGGyxatIj169ezefPmdOt16NCBkydPMm3aNJYvX05SUhI7duzIdMybN2/i7u5OeHh4uvmRkZF4enrmWi1CCO1IQySEsHoffPABJ0+eZPXq1VSqVIkOHTpkWCcqKorU1FTu3r3LvXv3AEhISHjomE5OTiQmJqabl5iYiMFgyNnwQgiLIF+ZCSGs3sWLF5k8eTJdu3ZlyJAh3L59+7HP6dKlCzExMZlOpUqVIiEhIUPzYzAYiIuLy60yhBAakj1EQgibULNmTVJSUmjevDmrV69+7PqbN2/m0KFDmS4LDw8nLCwMNze3dPPd3NyIiIjIkbxCCMsiDZEQwuq98cYbtGzZktatW7N582ZWrVpFQEBAhvWUUuZ/379/n/v37z90zKCgIMqWLYuHhwdhYWEA+Pj4EBQUlPMFCCE0J1+ZCSGsWoECBfjmm28YP348O3fuZO7cuSxcuDDTY31iY2OpXLkyRYoUeey4wcHB7Nixg1WrVlG9enV69OhBly5dmDdvXm6UIYSwAEommWSSyVqnOXPmqL///ls5ODgoQBUoUECFhYWpCRMmKKWU8vX1Na/br18/FRMTo9atW5elsYsXL642bdqk4uLi1OXLl1Xnzp3TLV+2bJlatmyZ5u+BTDLJ9PST7n//EEIIm6OUomnTpuzduzdXxn9w247u3bvnyvhCiGdHvjITQti0okWLUrhw4RwdU6fT4erqiqOjY46OK4TQjjREQgibtn79etasWZOjY5YoUYLr16/TqVOnHB1XCKEd+cpMCCGEEHme7CESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIYQQIs+ThkgIIYQQeZ40REIIIYTI86QhEkIIIUSe938qBo+/palfkAAAAABJRU5ErkJggg=="
+ ]
},
- "metadata": {},
- "output_type": "display_data",
"jetTransient": {
"display_id": null
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
},
{
"data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHFCAYAAAAT5Oa6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAA8zlJREFUeJzsnXVc1Mkbx9+7SyMoopQotmJ7iqKiWGfHzzi769Sz487ubs+zE+tsz7MTW0zsVkRFQUEFpFnm9weyJxIC0sz79ZqXMvk8u192P8w8M6MABBKJRCKRSCRZGGVaGyCRSCQSiUSS1khBJJFIJBKJJMsjBZFEIpFIJJIsjxREEolEIpFIsjxSEEkkEolEIsnySEEkkUgkEokkyyMFkUQikUgkkiyPFEQSiUQikUiyPFIQSSQSiUQiyfJIQSSRpDDOzs44OzvHWe7m5sb69evT3I7UpkSJEly4cCHeOtmzZ8fJyYnq1asnqm8bGxuEEJpUqFAhAMzMzNi8eTPe3t58+vSJrVu3YmFh8d3+atWqxenTp/nw4QNv375l165dFCxYMFqdqVOnRhszKg0fPjzevgsVKsS///7Lx48fef/+PcuWLcPIyOi7Nr169SrW8UxNTeNt165dO+7evUtgYCD379+nS5cu8dafOHFirONEpRo1agCgq6tLaGhojHJ/f//v+hJFkyZNECL65QmbNm3S9JUavyeSrItWWhsgkUiyJr/88gtVq1aNt065cuXo0qUL69atS9IYU6dO5eDBg7x69QqVSsXhw4cxNjamb9++aGtrM2vWLI4dO8ZPP/1EeHh4rH1UrVqVY8eOsW/fPjp27IihoSHjx4/nwoULlCpVCh8fH42tzs7OjB49Olp7d3f3OO3Lnj07p06dwtPTk65du2JmZsacOXMoUKAADRs2jLOdqakp1tbWjBgxgvPnz0cr+/TpU5ztWrZsyZYtW1i8eDFHjhzhf//7H05OToSEhLB9+/ZY26xZs4YjR45Ey9PR0WH79u28ffuWK1euAFCqVCm0tbXp2LEjz54909RVq9Vx2vM1jo6ObN26NUb+xIkT+euvv9i7d2+C+pFIfgQhk0wypVxydnYWzs7OcZa7ubmJ9evXp7kdqZ0mTpwoROR0QJzJ0dFRCCGEo6Njovq2sbERQgjRtWtXTV67du2EEELY2tpq8mxtbYVarRYdOnSIs699+/aJW7duCYVCocmztLQU4eHhYvjw4Zq8V69eiSlTpiTKzlGjRonPnz8LU1NTTV6DBg2EEEJUrVo1znZ16tQRQghRsGDBRI338OFDsW3btmh527ZtE48fP05UP/Pnzxd+fn7CxsZGk9ezZ08RGhoqdHR0EtVXtmzZxLRp00RYWJjw9vaO85lIrd8TmbJukktmEkk6RldXl0+fPjF37txo+SqVinfv3rFo0SIA9PT0mDFjBo8fPyY4OBhfX1+OHTtG2bJlY+03akmpa9eu0fLXr1+Pm5tbtLxmzZpx9epVgoKCePv2LYsWLcLAwCBeu79nz8SJE5k0aRIAQggmTpwYow9HR0dOnz4NwOnTpzXLffEt33xr+9fUr1+fhw8f8uDBA03egwcPePDgAY0aNYqz3eXLl1m0aFG0pZy3b9/i6+urWYqLmrG5efNmvK9LbDadO3dOM8sEcOzYMfz8/OK1qVy5cvj5+fH8+fMEj2VjY0OxYsVizLTs2rWLIkWKULhw4QT1U6pUKQYNGsTkyZOjzX6VK1eOhw8fEhoammCbAHr27Env3r357bffWLJkSaLaSiTJiRREEkk6JiQkhF27dtGmTZto+T///DO5c+dm06ZNAGzcuJEePXowc+ZM6tWrx7BhwyhZsmSsSxCJoX379uzbt4+HDx/yv//9j0mTJtG5c2f27dsXb7vv2bNmzRrWrFkDgL29veb/X3Pjxg369+8PQP/+/TX/t7e3jzO1aNEiTptsbW15/PhxjPynT59SrFixONvNmDEjRuxKjRo1yJkzJ/fu3QMixQBExsC8ePGC0NBQbty4QYMGDeLsNy6bIiIicHNzi9emcuXK8eHDB3bt2sWnT5/w9/dn27Zt8cZD2draAsQY7+nTpwDxjvc1c+fOxc3NTSPGv7YpPDyco0eP8vnzZ3x8fFixYgXZsmWLt7/9+/eTP39+Vq1alaDxJZKUQsYQSSTpnE2bNtGzZ08cHBw08SLt27fnwYMHXL9+HW1tbYyMjBg4cCA7d+4E4OzZsxgbG7NgwQLMzc3x8vJK0tizZ8/m8OHDdO7cWZP35MkTTp48SaNGjTh06FCMNgmxx8PDg9evXwORMzCx4e/vz/379wG4f/++ZmYnrvrfI3v27Dx58iTWcYyNjRPcj6mpKatXr8bDwwMnJyfgP0FkYWFBr1690NXVZeDAgRw4cIBGjRpx7NixOG3y8/NLtE3lypUjT548rFq1ikWLFmFra8uUKVM4c+YM5cuXJzAwMNaxgBjjRQU9J+Q1KF26NA0aNKBXr14xYoPKlCmDQqFgzZo1TJs2DTs7OyZOnEiJEiVwdHSMESwdRWJmuSSSlEQKIokkhRFCoFAovlsnLs6cOYO7uzvt2rXj/Pnz6Orq8r///Y9Zs2YBEBYWpgnAtbKyomjRohQtWpQmTZoAkctuSaFYsWLkzZuXGTNmoFKpotnj6+vLzz//HKsgSil7ovjalm8RQhARERFrmVIZ94R4XG2+xcLCgqNHj2JpaUndunX5/PkzADt27ODhw4ccPnxY09fRo0e5desWU6ZMiVMQJdWm3r17Ex4ezrVr1wA4f/489+7d48KFC3Tp0oUVK1YkaqzvjRfFgAED8PLyYuPGjdHyFQoFzZo14/379xoRe+7cOTw9PdmyZQv169ePEZgtkaQ35JKZRJLCBAQExCsCdHV1CQgIiLePLVu28Msvv6BUKmnSpAnZsmVjy5YtmvJ69epx//59PDw82LdvH506dSIkJATgu2IsLqK2by9fvpzw8PBoKXv27FhZWcXZNiXsieJbW75OX+9u+hZfX99Yt7MbGxvj6+v73XFLlSqFi4sL1tbWNGjQQLO7CiK3wB88eDCaqAgPD483jutHbHJxcdGIoSguXrzIp0+f4hwvqr9vx4uaGfrea6BUKmnRogU7d+4kLCwsWpkQgjNnzmjEUBQHDx4EiPc1kEjSC3KGSCJJYTw9PSldunSsZTo6OuTOnRtPT894+9i0aRNjxoyhVq1atGvXjrNnz/Ly5UsAChYsyD///MM///xDkyZNNEsQ/fr1i3PrdtSM1LezLV/He0Rt3x4xYoQmuPlrPn78GGvfSbEnMVSsWDHOsijRFRuPHj2ifPnyMfILFy4cTdzERs2aNfnnn3/w9fWlevXqMb74GzZsiL6+Pnv27ImWr6+vz/v37+O16dtgZqVSSYECBWL0FYWxsTGtWrXiypUrmhgmiBSaOjo6cY736NEjINLfr4O/o8b/Otg8NipXrkzu3LnZsWNHjDJLS0saN27M0aNHefXqlSZfX18fIN7XQCJJT6T5VjeZZMrMqWPHjkIIISpXrhyjrE2bNkIIIapUqfLdfq5evSpWr14tAgICRI8ePTT5v/zyixBCiDJlykSr/++//wohhChQoICA6Nvuc+TIIYQQYsKECZr6Wlpawt3dXbi5uQlAKJVK4enpKf76669o/VpYWIjjx4+LVq1axWpnQu0ZN27cd7fdOzg4JNu2+86dOwu1Wh1j270QQrRv3z7OvsqVKyc+f/4s7ty5IywtLWOts3btWuHr6ytMTEw0eQYGBuLVq1fCyckpzr7Hjx8v/P39Ra5cuTR5Udvu43omdHV1RUBAgNi8eXO0/ObNmwshhKhVq1ac4z179izWbfePHj367ms6dOhQERoaKnR1dWOU5c2bVwghxLRp06LlDx48WISFhYkiRYok6H2L7ygGue1eplRIaW6ATDJl6qSlpSWuXr0qvL29xbBhw4Sjo6OoXbu2GDdunPDz8xNbtmxJUD+DBg0SYWFhIjAwUBgbG2vyCxUqJEJDQ8Xhw4dF3bp1RePGjcWuXbuEWq0WQghRqlQpATHPITp//rx4//69aN++vahfv744dOiQePfunUYQQeTZMuHh4WLx4sWibt26onXr1uLWrVvCx8dH5M+fP1Y7E2rPgAEDhBBCtGvXLs6+SpUqJYQQYs6cOTEEVnwpNkGko6MjHjx4IF68eCHatWsn2rVrJ168eCFu3bolVCqVpl65cuWiiabr16+LkJAQ0bJlS1G5cuVoKeocoOLFiwt/f3/h4uIimjVrJlq0aCFcXFzEhw8fovn2bd+5cuUS7969E66uruJ///uf6Nmzp/Dx8REHDx6M5s/XY0GkkBJCiPnz54s6deqIIUOGiE+fPom9e/dq6hgZGYnKlStHE1tdu3YVQgixdOlSUb9+fbFs2TIhhBBt2rSJZlPlypWFkZFRNBvWrVsnHj58GOdrvnbtWhESEiLGjh0rateuLSZMmCCCg4PFwoULv9t3VJKCSKY0TmlugEwyZfpkYGAgpk+fLu7fvy8+f/4sfH19xfXr18XAgQOjHfgXX8qdO7cIDQ0V27dvj1HWqlUrcefOHREYGChev34tdu3aJapXry7UarXo16+fgJiCqHDhwuLw4cMiICBAvH37VkybNk2MGTMmmiCCyBmfq1eviqCgIPH+/Xvxzz//aERNXCkh9lhaWorLly+LkJAQsXTp0lj7USgUYsuWLSIwMFDcuXMnwa93bIIIENbW1mL37t3Cz89P+Pj4iL///ltYWFhEq+Pm5qZ5nQoUKCDi4+sv6PLly4vDhw8Lb29v4efnJw4cOCBKliwZZ99RqWTJkuL48eMiICBAeHp6ihUrVohs2bJFq/PtWAqFQvTt21fzGr969UrMmjVL6OnpaepEHWr57WvQp08f8fjxYxEUFCTu3bsnOnXqFK08SjR9Oyt38OBBcenSpThfcx0dHTF27Fjx8OFDERQUJJ48eSJ+//33aM93XH1HJSmIZErjlOYGyCSTTDIla4pLEKV1KlCggDh8+HCqjTd58mTRtm3bNPf76+Tk5BTr8vH3khREMqV0krvMJBJJpqVQoUJUrlwZHR2dtDYFgDFjxnD8+PFUGcvS0pLWrVtz8eLFVBkvIdja2lK5cmXu3LmT4DYFCxakcuXKP3xcg0SSENJclckkk0wyJWeKmiGKolChQmluE0TGEKXWWLq6uqJ48eJp7vPXydTUVFhbWyeqzaZNm2JdopRJpuROii//kUgkEolEIsmyyCUziUQikUgkWR4piCQSiUQikWR5pCCSSCQSiUSS5ZFXdyQCKysrzc3QEolEIpFIMgZGRka8efMm3jpSECUQKysrPDw80toMiUQikUgkSSBPnjzxiiIpiBJI1MxQnjx5knWWyMjICA8Pj2TvN72Q2f2DzO9jZvcPMr+P0r+MT2b3MSX9i+r7e/1KQZRI/P39U+RhTKl+0wuZ3T/I/D5mdv8g8/so/cv4ZHYf09I/GVQtkUgkEokkyyMFkUQikUgkkiyPFEQSiUQikUiyPDKGSCKRSCTpEgMDA3LlyoVCoYi3nqGhIcHBweTNm5eAgIBUsi51yew+JtU/IQTe3t4EBgb+sA1SEEkkEokkXaFQKOjevTs1a9ZMUH2lUsmVK1cYM2YMERERKWtcGpHZffxR/06fPs369esRIunXs0pBJJFIJJJ0Rffu3XF0dGT79u08fPiQ8PDweOsrlUpsbW158OBBphQLkPl9TKp/WlpaFC9enDZt2gCwbt26H7JDpPeko6Mj7ty5IxwdHeOsU65cOeHi4iICAgLElStXxE8//RStvF27duLp06ciICBA7NmzR5iamibKBiMjIyGEEEZGRsnqW0r1m15SZvcvK/iY2f3LCj5mJP8MDQ3Fxo0bRePGjRPcRqlUigoVKgilUpnm9qdUyuw+/qh/jRs3Fhs3bhQGBgYxyhL6/Kf7oGpdXV3+/vtvSpUqFWcdAwMDDh06xLlz56hQoQIXL17k4MGDGBgYAGBnZ8fatWuZPHky9vb2mJiYsGHDhlTyQCKRSCQJxdTUFICHDx+msSWSjETU85IrV64k95GuBZGtrS0uLi4UKlQo3npt27YlKCiIkSNH8vDhQ4YMGYK/vz+//PILAAMGDGDHjh1s2rSJO3fu0LlzZxo1akT+/PlTwQuJRCKRJJSoAOrvLZNJJF8T9bx8LwA/PtK1IHJ0dMTZ2ZkqVarEW8/e3p7z589Hy7tw4YKmnb29PWfPntWUvX79mpcvX2Jvb5/8RkskEolEIslwpOug6hUrViSonqWlJffu3YuW5+XlpVlms7S0jHGhm5eXF9bW1om2ycjIKNFtEtJfcvebXsjs/kHm9zGz+weZ38eM5J+hoSFKpVKTEoJKpYr2b0ZEW1ubrl27smbNmljLk8vHrl27MmHChO+uvCTEptho164dU6ZMwdLSkmPHjtGnTx98fHxirWtlZcWiRYuoVasWQUFBnD59mr59+2q20C9cuJBBgwZFazNw4ECWLVsWo6+o58XQ0DDGc57Q5z5dC6KEYmBgQEhISLS8kJAQdHV1E1SeGFLqxvuU6je9kNn9g8zvY2b3DzK/jxnBv+DgYK5cuYKtrS05c+ZMVNsyZcqkkFUpT+PGjfn111+5fv16vPV+1Md8+fKho6ND+fLlk82mKEqWLMnKlSuZOXMmjx49YuTIkezevZuhQ4fGWn/dunX4+/vTt29fjI2NmTBhAkuXLuXPP/8EoHLlyixZsoQDBw5o2nz+/DlW283NzcmbNy/Xr19HT08vQfZ+S6YQRMHBwTHEja6urkZlfq88Mcjb7hNHZvcPMpePWjo66BtnQ6FQgkKBQhH5F/t911vktbbO8P7FRWZ6D2MjI/mXN29exowZw4MHD3B3d09QG5VKRZkyZbh9+zZqtTqFLUwZypQpQ2hoKK6urrGWJ5eP3xsnqXUBBg8ezPbt25k2bRoAly5dws3NjY8fP/LixYtodYsVK0aZMmWwtLTk3bt3qFQqzM3N6d+/P927dwciv28PHjzIiRMnvju2jY0Nr169ol+/frx69SpaWdTz/z0yhSDy8PDAwsIiWp6FhQVv375NUHlikLfdJ43M7h9kPB/NCthQsGJ5zAvkx6yADWYFbMiZxzLWukvuX6HjX3N48+QZnk+f8/reQ55cuUZEeMb88omLjPYeJpaM4F9AQAARERGalBjUanWandFjY2PDixcvaNmyJXPnziVPnjycOHGCLl268PHjRwAcHBxYuHAhJUuW5OnTp0yaNIk9e/bg6OioOT9HrVYzdOhQOnbsiJ2dHQAdOnRgy5YtNGvWDFdXV/T19fnw4QMlSpTg+fPnDB8+nH79+mFpaYmLiwuDBg3i7t27AAghmDJlCv379+fixYvs2bMHgIiICBQKBdu3b6dIkSLUrFkTX19fjT/f2pQ/f35Onz4d62akDRs20L17dypXrsysWbM078HLly95+fIllSpV4vnz59HavHnzhvr16+Pp6RktP3v27ERERGBkZIS1tTUPHz5M0Hsa9bwEBAQk+RnPFILIxcWFUaNGRcurVq0a06dP15Q7ODjg5OQEgLW1NXnz5sXFxSXVbZVI0gpdAwMKV65A8Wr2FKtmj6m1Vaz11OHhkae9CoEQAqVKBVpaWBQphEWR/+IOPn/4yM2jJ7l+4Agvb9+LtS+JJDnR0Y99KUShVKLS0UFbXw+RjIIoNCg40W3GjBlD+/btUSgU/PvvvwwfPpxx48Zhbm7OgQMHGDt2LEeOHMHe3p4NGzbw7t07Ll68yODBgxkxYgR2dnaYmpoyd+5cjI2N8fPzw9HRkYiICMqWLcvBgwdxdHTk5cuXPHv2jIkTJ9KvXz969+7NkydP+OOPPzhy5AhFixbVrII0bdqUatWqoVKpqFSpksbWhQsXUq5cORwcHKKJISCGTe/fv8fOzi7WGKagoCAgcfG6vr6+HDt2TPOzQqGgTZs2nDp1CojcZR4REcHYsWNp2LAhPj4+LFiwgI0bNyb6PUkoGVYQmZub4+vrS3BwMLt27WLWrFksWrSIlStX8uuvv2JoaMiOHTsAWL58OadPn+bSpUtcvXqVxYsXc+DAgRhTeBJJZqRA+TJUbtWcsvVqR/tCCQ8N5fmNW3g8eMz7F+68c4tMAZ+ifzAaGxvj/t6Lus2aYpzHAovCBSlapRLGuUxxaN8ah/at8X75mnNbtnNxx940nTXKkcOQsmULUK5cQQoUMMckpxE5c2YjZ04jsmXTY8H8f3ByOplm9kmSzoCNKylQPv74mbbJPKbbjVv81bVvotpMnDiRq1evArBlyxbNLM9vv/3GiRMnWLp0KQDPnj2jfPnyDBkyhNatW+Pr64tarcbLywsvLy/evn1L9erVOXjwIDVq1ODIkSOULVsWgLp163LkyBEgMsh49OjR7N+/H4DevXvz7NkzOnXqxKpVqwBYuXIljx8/BtAIopEjR/LLL79QrVo13r17F8OPsLCwaDYBeHt7x+v7j8Trzp49m2LFitG5c2cAihcvjhCChw8fsmTJEhwdHVm1ahV+fn78888/3+0vKWRYQeTp6Um3bt1wcnLC39+fJk2asGLFCvr06cPt27dp1KiRRh27uLjw66+/MmXKFHLmzMmxY8fo3bt3GnsgkaQc+sbGVG7RhMqtmmFWwEaT7/3qNQ/PXeLB+Us8v+aaoL+AhRDk0NHj8cXLmqlopUpFkcoV+alJfUrXqUmufNa0GD2cau1ac2DhUu45n0sx36LQ0dGiQoXCODiUoEpVW8qXL4iNjVm8bWbM7MKmTc6Z8uqDTM8P3FGVmjx58kTzfz8/P7S1tYHIGY+mTZtGW87R1tbWCJVvOXbsGDVr1uTq1atYWFgwZswY5syZA0QKotGjR2NmZoapqSmXL1/WtAsPD+fatWvY2tpq8r7949/KyooZM2bw6tWrGEtW8XH37l1sbGxi5G/evJl+/folOV531qxZDB48mDFjxmh2jG/cuJH9+/drlhvv3LlD0aJF6devnxRE3x629O3PV69epUKFCnG2d3Jy0iyZSSSZlWymJjh2aU/Vti3RMzQEICQwiJtHTnB5z7+437qbLONEqNU8uniZRxcvs1t/LhWaNqB+/16YFbChx59zeHrlOv/O+xOPB7F/2CcFhUJBhQqFadLEjpq1SlOpUlH09HRi1Hv+3JNbt9x4/MgDb28/Pnzw5+PHz6xZOwhLy5w4OpbC2fl2stklSR3+6to33iWzsmXKcuv2rTRfMgsNDY32c9R3lZaWFps3b2bGjBnRysPCwmLt59ixY4wcORIXFxcuXbrE2bNnyZ8/P8WKFaNIkSI4OzujpRX7V7hKpYq2tBUcHN2PiIgIGjZsyLp16xg7dizjx49PkG+NGjXSCLyv8fPzA5IWr/vnn3/Sr18/unTpEkMcRomhKB48eEDt2rUTZGtSyDCCSCKRxI2xWW5qdeuIfevmmi8Nj4ePubBtNzePnCAkIPE7KhNKaFAQl3bs5cbBo9Tu2QXHLu0oXKkCQ7at5+iyNZxctSHJN1Bra2tRr155mjWrROMmdlhZmUYrf/fuE+fP3+fihQdcvfqE27df4OsbEGtfDRpUoM+vDejQwVEKogxKXAJFqVSiDg0lLCg43c7+PXr0iKpVq/Ls2TNN3rBhw9DV1WXmzJkxfkdOnDjBpk2baNSoEefOnePjx4+4u7szYcIEzp8/r5l18fT0xN7entu3I59pLS0tKlSowPHjx+O0xdPTk1OnTjFy5Eg2btzIhg0botkVxbc2vXz5Ml4fExuvO2HCBPr27Uu7du3Yu3dvtO30kydPpmrVqvz888+avHLlyqX4lS5pfqlbRkjyclfpX3r0UdfQQDQa3E/MunZazL9zScy/c0kM2rxa2Naolmb+5bAwF51mT9bY02vZfGGQ3ThRY1pYmIgJE9oJjzdOIkLs1yRfv+1i567RomfPeqJIEatE9enoWEpEiP3iw8e/hY6OVrp5D1MjZST/bGxsxMaNG4WNjU2C26SHi09tbGyEECKa3RMnThTOzs4CEPny5ROBgYFi6tSponDhwqJ9+/YiODhYdOjQQQCiVatW4tOnT6Jw4cJCpVIJQFy+fFkEBweL6tWrC6VSKXbu3CnUarUYPny4ZoyRI0cKT09P0aRJE1G8eHGxfv168f79e5ErVy4BCCFEtIvRu3btKtzc3DQ/Ozs7i0OHDsXqU2w2xZfs7e1FcHCw6NGjhyhdurQ4deqU2Ldvn6bc2NhYmJiYCEAUL15chIWFiSlTpghzc3NhaWkp6tWrJywtLQUgKlasKEJDQ8Xw4cNFwYIFRd++fUVQUJCwt7dP9HOTiOc/7X8BMkKSgkj6l558VCiVonLLpmKi8wGN8Oi/YZkoYm+Xbvyza95IzLoaKdTGHt0j8pa0/W6bSpWKis1bRoiQ0L0aEeTxxkksWfKrqFevfAwhk5ikVCrFy1frRYTYL5o1q5zm72FqpozkX2YVRICoU6eOuHbtmggODhbPnj0Tv/32m6bMxMREXLt2TQQFBYkKFSoIQEyZMkUEBwcLXV1doVQqxZgxY4QQQpQsWTKa71OnThVv374VAQEB4vjx49HKvyeISpcuLcLCwkSLFi1i+BSbTd9LXbt2Fe7u7sLf31/s3r1b5MyZU1O2fv16zevxxx9/iLiIqt+sWTNx8+ZNERgYKO7fvx+rjQl5bqQgSuYkBZH0L734WLBCOTFsp5NGCI3av12UrOmQLv2zLFpYjD64U8y/c0nMvn5GVPpfk1jrVa5cTBw+MjnabNDZc7NF27bVhbZ20kXQt2nevB4iQuwXf2/7PU3fw9ROGcm/jCqIUjpldh9/1L/kEEQyhkgiySDoGWWjybDfqNL6fwAE+vlxbPk6Lm7bjTqd3gz+9vFTFrbtRrtp4yldx5G2U8ei1FLhsmsfAHZ2RZg0uSMNG0ZuiAgLC2fz5tP8teQArq4xYxp+lK1bzzBseAuaNatEtmz6fP4clOxjSCSSjIkURBJJBqB0HUdajBlOdrPcAFza+Q+HFi8n0NcvjS37PsGfA9gwZBRNhw+kZrcO/DJxFPksjWlfJx/Nm9sDEB6uZqPTSaZN28GLF14pZsuNG8949Og1xYpZ07x5ZbZsOZ1iY0kkkoyFFEQSSTomW04TWo3/nTJ1awLwzs2dnZNn8fz6zTS1Kynsn7+E3DkNGNKvDoMXt0GpiBRCmzY5M33adp4/T/h5KD/Ctr/PMnFSB9p3cJSCSCKRaFCmtQESiSR2ilevwog9mylTtybqsHCOr1rP/NZdMqQYMjLSZ+bMriwZakcpkxCUCnjsq03bgX/Ts8fiVBNDAH//fRaAevXKkyuXcaqNK5FI0jdyhkgiSWdo6+nSdPhAqrVrBcDbJ8/YMmoSbx8/TWPLEo9SqaRHj7pMndYJc3MTAM6evcvh+6GYOdSjat+BPLz/mgdnL6SaTY8fe3D9+lMqVChM69bVWLHicKqNLZFI0i9yhkgiSUdYFSvCkG3rNWLo7KbtLGrXI0OKoZo1S3P9xiJWrR6IubkJDx++plnTKdR0HM2c/pO4vGc/SpWK9tPHk8M8/is3kpu/t54BoH0Hx1QdVyKRpF+kIJJI0gmVWzVj0JbVWBQqgN97b1b2Gcy+OYsI/+YqgPSOtXUuduwcxSnnGZQtW4CPHz8zZPAqypQewIEDkZdeCiHYPW0ur+49wDBHdjrOnowyllu0U4rt288RERFB9eolyZs3d6qNK5FI0i9SEEkkaYy2ni7tpo2nzaTRaOvqcu/0eea17MTjS1fS2rREoaWlYvjwFtx/sIzWrasRHq5m6V8HKFK4D3/+uZ/wcHW0+uqwMDaNGE/w5wAKVihHvf49U81WDw8fbt9+AUCZMvlTbVyJRJJ+kTFEEkkakssmL10XzMCqaGEi1GoOL1mJ87rNSb77K61wcCjBsuX9KVXKBoDz5+/Tv98y7t51j7edz2sPdk6eRee5U6nTqyvPrrryxOVqaphMYGAIECnkJBKJRM4QSSRpRMmaDgzdth6rooXx8/ZhRa+BnFq7KUOJoezZDVm1agBnz82mVCkb3r/3pXu3RTjWGPVdMRTFzSMnuLTrH5RKJR1mTiSbqUkKWx1J1IyVlpb8GJSkPdra2vTq1SvFx+natStubm4JqpsUm9q1a8fTp08JCAhgz549mJqaxln3f//7H0IIhBCo1WquXbvGjh07EjVeciI/CSSSNKBOr650WzwbvWyGPLvuyoJfuvLsmmtam5Uo/vc/e+7dX0qv3vUBWL3qCLbF++HkdDLRom7f7EW8ffIM41ymtJs2PiXMjYFaHXkrukolPwYlaU/79u0ZO3ZsWpsRjcTaZGdnx9q1a5k8eTL29vaYmJiwYcOGOOuXKFGCf//9FwsLC6ysrKhfvz69e/dOBsuThlwyk0hSES1dXdpOGcNPjeoBcGHbbv6ZvZCIb+Jr0jPm5jlY8ldfWreuBsCjR6/p0/svzp27l+Q+w4JD2DRiHEN3bMDWoQolazpw7/T55DI5Vv6bIZJLZpK0R6FQpLUJMUisTQMGDGDHjh1s2rQJgM6dO+Pu7k7+/Pl58eJFjPq2trbcvXsXLy8vlEolVlZW+Pr6JofpSUL+aSSRpBLGZrn5bcMyfmpUD3VYOLumzGHP9HkZSgy1a1eDe/f/C5qeOWMH5coO+iExFIXX8xec3bQdgCbDBqBMYaESHh45QyQFkSQ5sLGxQQhBixYtePr0KUFBQezfvx8Tk/+WgB0cHLh69SqBgYHcvn2bli1bAuDo6MiGDRvInz8/QgiGDBnC1av/xdJ16NABtVqNlZUVAIaGhoSEhFCoUCEUCgUjRozg2bNnBAYGcurUKUqVKqVpK4Rg8uTJvH//nn379kWzWaFQsGPHDlxdXcmePXu0sm9tsrGxwc3NTbPE9XVav349APb29pw9e1bTx+vXr3n58iX29vaxvmYlSpTg8ePHSXm5UwQpiCSSVMCqWBGGbF1LvlIlCPjky8o+g7i0c29am5VgBCFs3DiYrX+PJGdOI27ceIZdxaGMHbuJkJCwZBvn5Bon/H0+YFbARnOJbUohZ4gyHgYGunEmPT3teMuTkpLCmDFjaN++PY6OjtjZ2TF8+HAAzM3NOXDgABs2bKB06dLMnj2bDRs24ODgwMWLFxk8eDCvXr3CwsKCY8eOUa5cOYyNI09Sd3R0JCIigrJly2p+fvnyJc+ePWPChAmMGDGCIUOG8NNPP+Hu7s6RI0cwMDDQ2NS0aVOqVavGqFGjotm6cOFCypUrR/369WPMzHxr06tXr7Czs8PCwiJGGjx4MACWlpa8efMmWj9eXl5YW1vH+loVK1aM+vXr8+jRIx4/fsyAAQPQ1tZO0uueHMglM4kkhSlWzZ4u86ehZ2iI59PnrB04kg+v33y/YTqhaVM74AzN/1eZsLBwpk/bzowZO2Nso08OQgICObZ8La3GjaRev55cP3CE4M8BP9Snrq4eTRu3pUH9Vhw6vJM9/0RO50tBlLE4d3421aqVSNUxz5+/T43qfySqzcSJEzWzO1u2bMHOzg6A3377jRMnTrB06VIAnj17Rvny5RkyZAitW7fG19cXtVqNl5cXXl5evH37lurVq3Pw4EFq1KjBkSNHNIKobt26HDlyBICBAwcyevRo9u/fD0Dv3r159uwZnTp1YtWqVQCsXLlSMxNTqVIlAEaOHMkvv/xCtWrVePfuXQw/wsLCotkE4O3tHa/vBgYGhISERMsLCQlBVzemuMyXL59mpqtNmzYUKlSIpUuXMmfOHI3ASm3kDJFEkoJUbtmUnn/NRc/QkCeXr7Gky68ZRgwZGxuwwWkom7cMBUK5e/cl9pVHMGXKthQRQ1G47N6H1/MXZMtpQp1eXZLcj46OLq1adGWL03F+6zeGQgWL0b/vKAoVKg7IXWYZjYyy+fLJkyea//v5+WlmPGxtbWnatCn+/v6aNGDAAIoWLRprP8eOHaNmzZqYmZlhYWHBmjVrKFeuHPCfIDIzM8PU1JTLly9r2oWHh3Pt2jVsbW01ed/G71hZWTFjxgxCQkLw9Ez4PYJ3796NZn9UWr58OQDBwcExxI+uri6BgYEx+nr58iU5c+ake/fu3Lp1i3/++YcFCxbQu3dvlMq0+Z2UM0QSSQrRYGAffu7THYBr/x5mx8QZqMPD09iqhFG9ekmcNg4lf35z1OoIVKqi1KrZBR+fjyk+dkS4mgMLltLzr7lU79SWi9v38vFtwj+0VSotmjZpS6f2fTE1jbwSxNPzNd4+7yhV8idGDptO/4Ft5AxRBqNG9T/iXMZSKpWULVuWW7duERERkWxjRp1VlRhCvzlZPiowWUtLi82bNzNjxoxo5WFhsS85Hzt2jJEjR+Li4sKlS5c4e/Ys+fPnp1ixYhQpUgRnZ2e0tGL/ClepVKi+Ovk9ODg4WnlERAQNGzZk3bp1jB07lvHjE7azs1GjRrEuafn5+QHg4eGBhYVFtDILCwvevn0ba38fP0b/PHFzc0NfX5+cOXN+dzYqJZCCSCJJZpRaKtpMGo1d88YAHFu+lqPL1qSxVQlDR0eLKVM6MmJkS5RKJc+evaXvrys4ceI6oaGpJ+bunznPk8vXKFK5Io0G92XLqEkJaletSh369B5JvrwFgEghtGnrco4d34eRUXac1h6iWNFStGrRWW67z4DEJVCUSiXBwWEEBoYkqyBKTh49ekTVqlV59uyZJm/YsGHo6uoyc+bMGEdVnDhxgk2bNtGoUSPOnTvHx48fcXd3Z8KECZw/f14z6+Lp6Ym9vT23b98GIoVXhQoVOH78eJy2eHp6curUKUaOHMnGjRvZsGFDNLui+Namly9fxuuji4sLDg4OODk5AWBtbU3evHlxcXGJUbdevXps3bqVvHnzEhQUBEDRokXx9vZOEzEEcslMIklWtHR16bZgJnbNG6MOD2f7+OkZRgwVK2bNJZd5/P5Ha5RKJevWHqN8ucFcufLk+41TgP3zlxAREcFPjeuTt6RtvHWLFC7BgrkbmTZlGfnyFuDDR28WLp5E5+4NOHR4F+HhYXz86M2K1XMA6N51MAhDQM4QSVKHZcuWUbFiRaZOnUrhwoVp3749M2bMwN098gDTgIAATExMKFy4MCqVig8fPnDz5k06duzI+fORR1C4urrSpk0bTfwQwIIFC5gyZQpNmjShePHirF69Gj09PbZv3/5dm3bu3ImLiwtLliyJtfxbm77H8uXL6dy5Mz169KB06dJs3LiRAwcOaJbsjI2NNbvuLl68SFBQEGvWrKFo0aI0aNCAwYMHM3fu3O+Ok1JIQSSRJBN62Qzps3IhJWtVJyw4hA1DRnPlnwNpbVaC6NWrHtdvLKJ8+UJ4e/vRssV0evVawufPQWlmk8eDx9w4cBSAun26xlone3YThg+dyoqluylfrjKhoSFs+XsFnbvV498DfxMeHn054tDhXdy8dRl9fQNMc9RACCmIJKnDy5cvadq0KQ0bNuTu3btMmzaN4cOHs3XrVgBOnTrF06dPuXPnjiZW6OjRyOf/ypXIew1dXV1RKpXRBNH8+fNZvXo1q1ev5vr161hbW1OzZs0Ez7IMGjSIn3/+mRYtWsQoi82m+HBxceHXX39l4sSJXLx4kY8fP9K9e3dN+eLFi9mzZw8Anz9/pn79+uTOnZtr166xevVq9u7dy7x58xJkd0ohZPp+MjIyEkIIYWRklCH6TS8ps/sX5ePn0BAxcvdmMf/OJTHt4nFR4KeyaW5XQpKJSTaxc9doESH2iwixXxw9NkVYWuZMN++hWQEbMf/OJTH31gWRK5+1Jl+pVIrmTTuIfbsvC+fjj4Tz8Udi7Kh5wtzM6rt9WufJL44evC2cjz8Sb9+cFuPHt8v0z2lG8s/GxkZs3LhR2NjYJLiNUqkUFSpUEEqlMs3tT6mU2X38Uf/ie24S+vzLGSKJ5AfJYWHONrd7WBQthJ+3D0u79cPtxq20Nuu7VK9ekpu3/qRVq6qEhoYxcsQ6GtSfyNu3H9LaNA3v3Ny5f+YCSqWSGp3bAVDCthzLl+xkyKCJGBvn4OmzBwwc0p7ps0bg9e77O/hee7xg4+bIrc9PH5uhp2uYoj5IJJKMgQyqlkh+gFw2eem+bD6+oSF89HjL8l4D8XntkdZmxYtSqWTcuDaMn9AOlUrF48cedGg/lxs3YgZVpgfObPybEo7VqNa0KSUV1tR2bAjA589+rN2wiH/3byMiInHHAGzbsZYunXoC2cmRI28KWC2RSDIaUhBJJEnEonBBfl39J8a5TMmpq8f8fsPSvRiyssrJ5i0jqFmzNADr159g0MCVBAQEf6dl2vHy5l0M3AIoYmCNyrEhERERHDm6h9Xr5vPpU9Jms9TqcELDPqOjkx1tLZ1ktlgikWREpCCSSJJAHtui/LpyMYYmOfB8/Iy+zdowyNsnrc2Kl0aNKrLBaSi5chnj7x9I/37L2bLldFqbFScKhYJaNRvRu+dwLPTzgIBPBDJ6cFcePrz9w/1HzSopE7B7RiKRZH6kIJJIEolN2VL0XrYAfWMjXt65z98jxjOnZce0NitOtLRUzJjRhREjIy+SvH79Ke3bzeHp09gPS0sPlCtbmb59fqdY0chLKt+9e8t7c0GImR6Gha0gGQVRXIfbSSSSrIUMqpZIEkH+cmXos3IR+sZGPLvuyoreAwny909rs+IkX77cnDk7SyOG/lz8L9Wqjky3Yii/TWFmTF3BwnkbKVa0FAEBn1m7fhFdejRgz9/rQQGOXdony1hRgkilSrvLJCUSSfoh3QsiXV1d1qxZw8ePH3nz5g3Dhg2LtZ6zszNCiBhp7dq1AOTIkSNG2fv371PTFUkGp0D5MvResUBzL9mafsMICYh5R096oUkTO264LqZKleJ8+vSZli2mM2TI6lQ9cTqh5MyZm+FDprBm5b9Usa9FeHgYe/7ZRKduP7N563JCQoK5tPMfQgKDyGNblMKVKvzwmOqoGSK5ZCaRSMgAS2Zz586lYsWK1K5dGxsbG5ycnHB3d2f37t3R6rVs2RIdnf+CIytXrsyOHTtYtmwZACVKlMDb25tSpUpp6qTXI94l6Y8CP5Wl9/IF6BoY8MTlGmsHjiAsOPH3HKUG3y6RXbnymHZt5/DihVcaWxYTPT0D2v7Sg7a/9EBfP3L7+9lzR1m9dgGvPV5Eqxvk58fVfQdxaN8axy7teXrl+g+NHaGOFIYJOYFXIpFkftK1IDIwMKBXr140bNgQV1dXXF1dmTNnDgMGDIghiL6+JE6pVDJjxgzmzJnD9euRH5q2trY8fvwYL6/096UgSd8UrFCOXsvmo2tgwONLV1g36Pd0K4by5DFl2/bfqVatBACLF+3j9983EBaWvmaFlEolDeq1pEe3wZoLWO/dd2XFqjncvXcjznbnNm+natuWlHCshomlRaIuff0WtSaoOl1/DEokklQiXS+ZlS1bFm1tbS5evKjJO3/+PJUrV9bcIBwb3bp1I2fOnMyePVuTV6JECR4/fpyi9koyHwV+KkuvZZEzQ48uXmbtwPQrhurVK4/rzT+pVq2EZols6NA16U4MlSldkeV/7WLk8OmYmprh8eYlk6YOZsDgdvGKIQDvl695djWyTrmGdX/IDrU6KoZICiJJ2qOtrU2vXr1SfJyuXbvi5uaWoLpJsaldu3Y8ffqUgIAA9uzZg6mpaZx2fB3ColaruXbtGmFh/123888//8QIdWncuHGi7EkM6fqTwNLSEm9v72gvkJeXF/r6+piamsZ5V8sff/zBokWLCAgI0OTZ2tqira3N5cuXyZMnD+fOnWPo0KF4eibuL0wjI6OkOfOd/pK73/RCRvYvb6kSdFo4A10DfZ5evsbOMVPR19EBnejn1qS1j0qlgtFjWjNiRHOUSiW3brrRteti3NzeJYtNyeWfuZkV3boMolrVSCHzOcCfHTvXcODQDsLDwxLc/wPncxSpXJGKjRtwbee+H7AocslcW0s7zd/DlCYj+WdoaIhSqdSkhBC17JmRlz87duzI2LFjWbduXazlyeVj1GuakNf2ezZ9i52dHWvXrqV///7cvHmTRYsW4eTkRLNmzWLU3blzJ8eOHdP8rKenx+nTp9m7d6/GthIlStC5c2dOnjypqffx48dYbY96XgwNDWM85wl97tO1IDIwMCAkJPpf41E/6+rqxtqmZs2aWFtbs3r16mj5xYsX5/379wwdOhSFQsGMGTM4cOAAlSpVSlQskYdHyhy8l1L9phcymn9vAz+z+8UDQiPU5DM0ZmD3/izrOSDeNmnhoyAEuAFEnYFkQ9lyDbl1a2Cyj5VU/9TqCF6++MBL94+ICAGAVZ7s5C9UkCbNlwPLE9VfUHg4Kx9dx6JoIdzeeWKqZ5Akux7ef4rnmwhKliql8S2jPaeJJSP4FxwczJUrV7C1tSVnzpyJalumTJkUsirlsbGxQUdHh/Lly8db70d9zJcvX4LGSYxNUYwfP56TJ09y9+5dtLS0mDt3Lvv376dx48a8eRP/tTrdunVDoVCwbds2ypcvj7a2NgUKFCAgIAArKytNva///zXm5ubkzZuX69evo6enlyB7vyVdC6Lg4OAYwifq58DA2Hf3tG7dmsOHD0eLKQIoWbIkQgiCg4M19d6+fUvlypW5dOlSgm3KkycP/sm4zdrIyAgPD49k7ze9kBH9syxamC5/zkbf2IgXN24xffg4eoTEvUyWVj7a2xdj/YaBWFnl5PPnYAYPXsOunRe/3zCR/Ih/1arWpUfXIeTObQHArdtXWLt+AS/cn/6QTe3nTKGYgz09xv6B82qnJPWxaMESCuavwqOHj6nm0DDDPaeJISP9HubNm5cxY8bw4MED3N3dE9RGpVJRpkwZbt++rVkKTW1sbGx4/vw5rVu3Zvbs2eTJk4eTJ0/StWtXzfeRg4MD8+fPp2TJkjx9+pQpU6awZ88eHB0dmTRpEgDXrl1j2LBhdOjQgcqVKwPQoUMHNm3aRLNmzThy5Ah6enqaTULPnz9n2LBh9O3bF0tLS1xcXBgyZAh3794FIpeGp06dSr9+/bh06RJ79+4lNDQUV1dXjQApXLgwtWvXxtfXV+PPtzYVLFiQU6dOkT9//hi+Ozk50aNHD4oUKcKcOXNwdXXVlL18+RIjI6Noed9iYmJCp06dmDlzJtevX0etVlO6dGmEEBw4cCBB76mNjQ2vXr2iX79+vHr1KlpZ1POfENL8ltu4UpUqVURYWJhQqVSavJo1a4qAgAChUChibfP06VPRqVOnBPXv5eUlWrdunaC68rb7rOGfZdFCYsq5I2L+nUtigNMKoaOvny59HDq0uQgN+0dEiP3i7r2lwtY2b7p6D/PnLyIWzN2ouYn+700nRXWHn5PNpvKN6on5dy6J0Qd3JrmP5X+tFM7HH4kjh3dnuOc0Nd7DtErx3Vqup6cfazLQNxRV7KsKA33DOOskJSXWbiGEuHr1qrCzsxOVKlUSnp6eYtq0aQIQ5ubm4tOnT+K3334ThQoVEh07dhR+fn7CwcFBaGtri0GDBomXL18Kc3NzUaJECREWFiaMjY0FIFauXCnUarUYN26cUCqVolGjRuLJkycCEBMnThSenp6iadOmonjx4mL9+vXi9evXwsDAQABCCCFu3LghihYtKmxtbUXXrl2Fm5ubAMSiRYvE48ePhZmZWQx/vrVJqVSKXLlyCXNz8xgpyk4/Pz9Rv379aP24uLiIESNGxPvajRkzRty7dy/abfdt2rQR79+/F1u3bhVv3rwRly9fFg0aNEjSc5PQ5z9dzxDdvHmTsLAw7O3tuXDhAhCpsK9evYoQIkZ9U1NTChUqpKkbhZGREe7u7rRs2ZLTp08DkdNuuXLl4uHDhynuhyRjYFbAhl9X/Ylhjuy437rL6v7DCA0KSmuzomFsbMDadYNp1aoqAFu3nuHXPn+lm7vIDA2y0bXLQFr+rxMqlRYhIcFs3baK7TvXEhKSfDbecz5HSGAQufJZk7ekLa/uPUh0H+Fftt0nNE5FkrYsWfQ3pUr+lKpj3rl7nUFDOySqzcSJE7l69SoAW7Zswc7ODoDffvuNEydOsHTpUgCePXtG+fLlGTJkCK1bt8bX1xe1Wo2XlxdeXl68ffuW6tWrc/DgQWrUqMGRI0coW7YsAHXr1uXIkSMADBw4kNGjR7N//34AevfuzbNnz+jUqROrVq0CYOXKlZpNRZUqVQJg5MiR/PLLL1SrVo13797F8CMsLCyaTUCccbtRxBXmEleISxS9evVi3rx50fKKFy+OgYEBR48eZdasWbRo0YL9+/djb2+v2T2e3KTrT4KgoCCcnJxYsWIFFStWpHnz5owYMYLFixcDkWuGX68VlipViqCgoBgR9P7+/pw7d46FCxdSsWJFypcvz7Zt2zhy5IhmWlGStclpbUXf1UswMs3Jq/sPWdVvaLo7dLFUKRuuXF1Aq1ZVCQ0N47f+y+nUcV66EUM/123OxvVH+KVVN1QqLc6eO0rXno3YuHlpsoohgNCgIO6fPgdA+UY/J6mP8PAoQZRxA3GzErH9EZweefLkieb/fn5+aGtHnoRua2tL06ZN8ff316QBAwZQtGjRWPs5duwYNWvWxMzMDAsLC9asWUO5cuWA/wSRmZkZpqamXL58WdMuPDyca9euYWtrq8l78eJFtL6trKyYMWMGISEhidpYdPfu3Wj2R6XlyyPjAOMKc4krxAWgYsWKWFtbs23btmj5U6dOJU+ePDg5OXH79m0mT57M4cOH6dOnT4LtTSzpeoYIYNiwYSxfvhxnZ2d8fX2ZOHEie/fuBcDT05Nu3brh5BQZQ2Bubs6nT59i7adr167Mnz+fQ4cOoaury759+xg0aFBquSFJx2Q3z03f1UvIbp6bt0+esfrXIQT7f05rs6LRsWNNVq4agIGBLi9fvueX1jO5evXJ9xumAgULFGPwwAmUKV0RgJev3FiydCrXrl/4Tssfw/Xwcco3qke5BnXZP/8vRDybI/IYZ+OXUsU58vg5D70/AJE33gMoFVIQZQQGDe2Anp5+rGVKhZKyZcty69YtIkTyHbgbHJz4GeLQ0NBoP0cdEaOlpcXmzZuZMWNGtPKvd1F/zbFjxxg5ciQuLi5cunSJs2fPkj9/fooVK0aRIkVwdnaO8x4+lUoVbTdaVOxsFBERETRs2JB169YxduxYxo8fnyDfGjVqpBF4X+Pn5wdEBu1bWFhEK7OwsODt27ivCmrQoAFnz56N8d0thIiR9+DBA0qWLJkgW5NCuhdEQUFBdOvWjW7dusUo+/Ysoh07drBjx45Y+/n06RM9e/ZMCRMlGZhspib0Xb0EU2sr3ru/YmWfwQR88v1+w1RCR0eLBQt60f+3yLM3jh1zpWOHefj4+KWxZWBgYEi3LoM0y2NBQYFs2rKMXXs2xPkhn5w8vHCZQD8/spvlpmCFcprzib6miKkJw6vZ0alsSXS0VAyyr0D5ZRvwDQ7RzBApFOl6olzyFXEJFKVSSWhYCMEhQen2BoJHjx5RtWpVnj17pskbNmwYurq6zJw5M8YM2IkTJ9i0aRONGjXi3LlzfPz4EXd3dyZMmMD58+c1sy6enp7Y29tz+3bkhcdaWlpUqFCB48ePx2mLp6cnp06dYuTIkWzcuJENGzZEsyuKb216+fJlvD66uLjg4OCgmaSwtrYmb968uLi4xNmmcuXKMcJcANavX09ERES07+1y5cpx586deG34EeQngSTLom9szK8rF2NWwIYPb96yotdA/L19vt8wlbC2zsWZs7M0YmjK5L9p1HBSuhBDtWs1xmndf8tjp88eoWvPhvy9fXWqiCEAdVgYd46fBmIum5W1yM2WX5pwZ0APelQog46WisDQMKyzGzGvQS0AwsMj7ZRLZpLUYNmyZVSsWJGpU6dSuHBh2rdvz4wZMzQ76QICAjAxMaFw4cKoVCo+fPjAzZs36dixI+fPnwfA1dWVNm3aaOKHABYsWMCUKVNo0qQJxYsXZ/Xq1ejp6bF9+/bv2rRz505cXFxYsmRJrOXf2vQ9li9fTufOnenRowelS5dm48aNHDhwQLNkZ2xsjImJSbQ2pUqV4v79+zH6+vfff+nUqROdO3emUKFCjB8/HgcHhzhtTQ6kIJJkSXT09em9bD5WxYrg++49K3oN4pNn+rnWpXbtMly/sYjKlYvx8eNnmjSezKRJW9P8r9+8eQsyf84Gxo9ZQC5TM169duP30T2ZPHUw798n/RqNpOJ6OPKv4LI/10alpcVPVubsbv8/rvbryi+liqNUKjjw8CnVV2+h4cadREQIupYvRaOiBeUMkSRVefnyJU2bNqVhw4bcvXuXadOmMXz4cLZu3QrAqVOnePr0KXfu3NHECh09ehSAK1euAJGCSKlURhNE8+fPZ/Xq1axevZrr169jbW1NzZo1vxsAHcWgQYP4+eefadGiRYyy2GyKDxcXF3799VcmTpzIxYsX+fjxI927d9eUL168mD179kRrY25uHuOYHIC9e/fSv39/xo0bx927d2nevDkNGjRI8FEMSSXNt1lmhCS33Wce/7R0dMSvq/8U8+9cElPOHRHmhQqkGx8VCoUYNaq1CAuP3FJ//cYiUaCAeZq/h+HhatG3z+/i2KE7kVvVD9wSnTr0E9ra2mlqm0KpFBNP7RebzxwUJwf2FKGTR4jQySNE0MRhYmOrxqKUWa5o9WfWcxShk0cI9xF9xbQxY4Tz8Ufi/JlT6fI5Te73MKP4F9/26biSUqmMtmU7M6bM7uOP+pfpt91LJMmNUqWi05wpFLW3IzgggNX9huH1zC2tzQIit9RvcBrK//5nD8D69Sf4rf9ygoNDv9MyZanwUzWuurjT9pfItfxLLs78uXQanp6v09QuhQIaFMpP89POlFIpIZcJ4eoI/r7zgNlnXXjs8zFGm8mnLtC4aEGK5zalfngYnwGFDKqWSCRkgKBqiSS5UCgUtJk8mtJ1HAkLCWHdwN95dTfm2nVaULJkPnbvGUPRonkICQlj0MCVrF59NE1tymVqxoD+Y3Gs0YDgoDDee3vy519TOX/hRJrapaNS0b6MLcOq2mFrFnlxpFqh4FZOUzpOmMWzD5/ibBscHk6PvYc516sDZbWUPPBxIyCPYSpZLpFI0jNSEEmyDM1GDsaueWPU4eFsGjEu1l1JaUGbNg6sXTcYQ0O9dLGlXqlU0rxpB3p2H4qhYTbU6nBsCuSmTYfqvH8f8wC31MIimyG9K5ald8WyWBhFihi/4BA23HlItvGjCDAwwDNi5nf7uebhybzzV/ijhj2Fnp3nnnmdlDZdIpFkAKQgkmQJ6vbpRo3ObQHYNn4a906fT2OLQKVSMnt2N4YNjwxmPHHiJh3az8XbO+12kRUuZMvwoVMpXqw0APcf3GTF6tncvn09SWeyJAcVrCwYaP8TrUsWQ0crcnnrta8/S1yus/b6bfxCQhnZqzsWhQpgU7Y09898/72devoSvatWIifBZPONubQmkUiyHlIQSTI9VX5pQcOBvwKwd+YCbhxI26UoADOzHGzb/js1a0YKj1kzdzJ+/GbU6rTZRaanp0+3LgNp3bIbKpWKz5/9WLV2PgcObidbtmypbo9CAQ2LFGSEQyUcbKw1+RdferDU5QZ7Hzwh/Ksdd+637n4RRKUSJIhC1Wo+hEeQU0uJMkKkiA8SiSRjIQWRJFNTpl5tWo4bAcDxles5v3VnGlsElSsXY+euUVhb58LPL5BuXRfyzz9xH1yW0lSyq8HQQROxsIgUHqecD7J0xUw+fHif6rZoKZW0K12c4dUqUdI8FwCh4Wp23H3IX5dvcONN7EcjvLh5h8otm5K/XOkEjxX+5URjZQa5EkIikaQsUhBJMi1F7O3oOGsSSqWSizv2cuSvVWltEn36NODPJX3Q0dHmwYNXtGwxg0eP0ma3lkkOU37rP4Y6tZoA4On5mkVLJnP5ytlUt0VbpaRLuVKMqmGPTQ5jIDI+aPW1W/zpcp23/gHxtne/FXl6bb5SJVBqqYgIV393zNAvs3EKOUMkkUiQgkiSSbEuUZzui2ehpa3NrWOn2DN93vcbpSC6utr89VdfevaqB8Du3Rfp3m0Rnz+nTVxO/Xot6P/rKIyNc6BWq9m914n1TksIDk7dC221lEo6lyvJ6Br25DfJDoCnfwBLXK6z6totfINDvtNDJO/c3An088PA2BirooV5ff/Rd9toBJGcIZJIJEhBJMmE5MpnTe/lC9A1MOCxy1W2jJoU78WfKY21dS527R5NpUpFUavVjBu7mdmzd6WJLVaWeRk2ZAoVfqoKwOMn95i/cDyPn9xLVTuUCgUdy5RgbM0qFMyZA4C3/p+Ze/4Ka67dJvjLKdIJRQiB++172DpUIX+50gkSRGHqyFkkZTq9+0oikaQu8sx6SabCyDQnfVYuIltOE17df8iGwaNQp9LdWrFRo0Yprl1fSKVKRfHx8aNRw8lpIoaUShVtWvdg7ar9VPipKiEhwaxYNYd+A35JdTH0P9siuPbvxtqWDSmYMwee/gGMOOJMsUVr+MvlRqLFUBQvbkYum9mUTVgcUegXQaSQgkiSDtDW1qZXr14pPk7Xrl1xc0vYYbQ/YtOYMWNYv359ktqmFVIQSTINuoYG9F6+EFPrPHi/es2a/sMICUzdJaCvGTy4GSdOTsPMLAc3bz7HruIwjh93TXU7ChUsxrIlO+j36x/o6elzw/USPfo0ZfvOtUREfD/WJrmoXTAfF/t0Yke75tiamfIhMIjRx85QbPFq/rx0PclCKAr3L4IofwIFUUhUnJGMIZKkA9q3b8/YsWPT2oxoJNWmdu3aMXny5BSwKGWRS2aSTIFKW5tui2aRx7Yo/j4fWPXrUD7HcnVDaqCnp8PKVQPo3DnyVvXNm535tc9SgoISFg+TXGhra9OpQ386tOuNlpY2nz/7sXzlbA4dSd0ZqlJmuZhZz5H6RQoA8DkklD9drrPgwlX8QpLvWpKXd+4ToVaTM48lxrlz4fc+/sstQzRLZlIQSdIehUKR1ibEILE2qVQqlixZQrdu3Xj27FkKWZVyyBkiSYZHoVDQfto4itrbERIYyJr+w/B5lTY7t/Lly835C3Po3LkW4eFqhg5ZTZfOC1JdDJUsUZ7VK/bRpVN/tLS0OXvuKF17NkpVMWRpZMiKZvW42q8L9YsUIDRczV8uNyi+eA2TTl1IVjEEEBIYyNsnkR/CNmVLfb9+1IxURES6/DKSZCxsbGwQQtCiRQuePn1KUFAQ+/fvx8TERFPHwcGBq1evEhgYyO3bt2nZsiUAjo6ObNiwgfz58yOEYMiQIVy9elXTrkOHDqjVaqysrAAwNDQkJCSEQoUKoVAoGDFiBM+ePSMwMJBTp05RqtR/z78QgsmTJ/P+/Xv27dsXzWaFQsGOHTtwdXUle/bs0cq+tcnGxgY3NzeEEDFS1NJYtmzZKFOmDJUrV+bSpUvJ+wKnAnKGSJLhaTz0N8o3qoc6LJwNQ0YnKKA2JahevQTrNwwkd+7svH/vS9s2szl9+k6q2qCnp0+vHsNo0bwTSqWSDx/es3jJFM6eP5Z6NmhpMbyaHSMcKmGoow3A7nuPGHfiXLz3jCUH7rfukqd4UfKXK82dE6fjrRsSFimIFBERqFTyb8OMgIG2dqz5SqUCPZUSA20tIpJxxi8wCfGHY8aMoX379igUCv7991+GDx/OuHHjMDc358CBA4wdO5YjR45gb2/Phg0bePfuHRcvXmTw4MGMGDECOzs7TE1NmTt3LsbGxvj5+eHo6EhERARly5bl4MGDODo68vLlS549e8bEiRPp168fvXv35smTJ/zxxx8cOXKEokWLEvglZKBp06ZUq1YNlUpFpUqVNLYuXLiQcuXK4eDggK+vbzQ/vrXp/fv32NnZoVLFvAw5KChyt6yvry8ODg6Jfs3SC1IQSTI0Dh1aU6t7RwC2T5jO40tX0sQOwXP+2TcaLS0V168/pVXLGbx8mboHG/5U3p7hQ6dhZZkXgMNHd7N85Wz8/X2/0zL5aFWiKLPq19ScJeTy6g2/Hz2Ny6s3qTL+i5t3qNq2ZYLiiIK+fNkpItRoaUlBlN453bM9VfPlib9SY8dkHfOC+2tqrduWqDYTJ07UzO5s2bIFOzs7AH777TdOnDjB0qVLAXj27Bnly5dnyJAhtG7dGl9fX9RqNV5eXnh5efH27VuqV6/OwYMHqVGjBkeOHKFs2bIA1K1blyNHjgAwcOBARo8ezf79+wHo3bs3z549o1OnTqxaFXn22sqVK3n8+DGARhCNHDmSX375hWrVqvHuXcw7CsPCwqLZBODtHf8ydEZHCiJJhqVUbUea/zEUgEOLV3D9wJFUt0FXV5tly38F7qOlpWLTJmd+7fMXwcHJuxwUH4YG2ejb53eaNI68q83Ty4MFiyZw9Vrq3ddW2jwXCxrWwbFApBh7+cmP0cfOsPNe6s7Wvbh1FwDrEsXQ0tEhPDTu9yFYM0Mk0NKK+VevJH0hMsh5UU+e/Hcxs5+fH9pfZrVsbW1p2rQp/v7+mnJtbW2NUPmWY8eOUbNmTa5evYqFhQVjxoxhzpw5QKQgGj16NGZmZpiamnL58mVNu/DwcK5du4atra0m78WLF9H6trKyYsaMGbx69QpPT88E+3b37l1sbGxi5G/evJl+/foluJ/0ihREkgxJ/rKl6TR7cuQp1Nv3cHKNU6rbYGWVkz17x1KpUlFAwejRG5k9a0eq2lC5Ug2GD5lK7twWAPzz7xZWrZlPUFD8JzsnFyb6ekysVY1f7cqiUioJCgtj3vmrzLtwhaCwH9s1lhR8Xr3G3+cDRqY5sbYtxotbcS9ZBn0RS4qICCmIMgC11m2Ld8msbNmy3Lp1K82XzEK/EeFR8WlaWlps3ryZGTNmRCsPi2OMY8eOMXLkSFxcXLh06RJnz54lf/78FCtWjCJFiuDs7IyWVuxf4SqVKtrSVnBwcLTyiIgIGjZsyLp16xg7dizjx49PkG+NGjXSCLyv8fNLuwupkxMpiCQZjlw2eemxZA7aerrccz7H3pkLUt2GKlWKs3vPGCwsTPjwwZ+cOX9m2dL2qTa+kVF2fus7mvr1WgDw2uMFc+eP5fada6kyvkIB3cqXZlrd6uQ2NABg191HjDp2hpe+afvh6H7rDqVqO2JTtlS8gig4NGqGSI1SKZfMMgJxCRSlUkmwOoLAsHAi0um5Uo8ePaJq1arRdl8NGzYMXV1dZs6cGWMG7MSJE2zatIlGjRpx7tw5Pn78iLu7OxMmTOD8+fOa+CBPT0/s7e25ffs2ECm8KlSowPHjx+O0xdPTk1OnTjFy5Eg2btzIhg0bYt0V9q1NL1++TLL/GQH5KSDJUBia5KD3sgUYmuTA/fY9Nv8xgQh16p2lA9Cjx884n56BhYUJd+68oFbN8SjIlWrjV6tSh/WrD1C/XgsiIiLYsWsdvX5tnmpiqGIeC8736sjK5vXJbWjAPS9vfl6/nQ4796e5GIL/ls2+d9FrUNjXM0Tyo1CSsixbtoyKFSsydepUChcuTPv27ZkxYwbu7u4ABAQEYGJiQuHChVGpVHz48IGbN2/SsWNHzp+PXP52dXWlTZs2mvghgAULFjBlyhSaNGlC8eLFWb16NXp6emzfvv27Nu3cuRMXFxeWLFkSa/m3NmV25AyRJMOgpaND98WzyZXPGp/XHqwbOJLQoODvN0wmVColCxb0YuCgpgDs2nWB7t0WoVTGPo2f3Bgb5WDQgHHUqR05vvvLZ8yZN4b7D26myvi5DfRZUKcaXctHbun1DQ5hivMFll+5SXg6+qs86sTq7wmi4KigaiGXzCQpz8uXL2natCmzZ89m5MiReHh4MHz4cLZu3QrAqVOnePr0KXfu3MHBwYHr169z9OhRypYty5UrkZtFXF1dad26dTRBNH/+fIyNjVm9ejXGxsZcvHiRmjVrJjgAetCgQdy4cYMWLVqwd+/eaGWx2ZTZETJ9PxkZGQkhhDAyMsoQ/aaXlFz+KRQK0XnuVDH/ziUx9cJRYVbAJlX9MDHJJo4dnyoixH4RIfaL8ePbCYVCkWrvYXWHn8XuHReE8/FH4sSR+6J3z+FCW1sndXzPnl2EXzojvMcMFKGTR4jQySPEmv81EObZDNL8+Yotaenqijk3zon5dy4JE0uLOOv1rlxRhE4eITwXLxalSxeWv4fpJNnY2IiNGzcKG5uE/44rlUpRoUIFoVQq09z+lEqZ3ccf9S++5yahz7+cIZJkCBoO6ku5BnUJDwtjw+BRvHNzT7WxbW3zsu/fcRQubMXnz0F06byAf/5xSZWxjY1NGDxgPLVrNQbAze0xs+eN4dHj1DnfqG4hGxY2rkvEsf0Y6+pyzcOTIYdOcuX121QZPymEh4Tw7oU7lkUKYVbAho9vY99FEzVDpBRqdHV1U9NEiUSSDpGCSJLuqdyqGXV6dQFgx8SZPLvmmmpjN2pUka1/j8TY2AA3Ny/+13wad+68SJWxazjUY8igSZiYmKJWh/P39tVs3Lw0zl0pyUnhnDmYU78mTYoXjswwMGTQnoOsdLlGRtj9/OH1GyyLFCKntVWcdf6LIVKjo6OTWqZJJJJ0ihREknRNkcoVaTV2JADHlq/l+v7DqTb20KHNmTuvB0qlkjNn7vJL65l4e6d80LCxsUlkrFCtJgA8d3vE7HljePz4bsqPravDaMcqDKz8EzpaKsLUata43mPg9r1smbIgQ4ghAJ/XkQdBmlrHfZDffzNEEejEsZ1bIpFkHaQgkqRbcufPR5cF01Fpa3Hj4FGOLluTKuNqa2uxfHk/evSsB8DqVUcYMGAlYalwrk61qnUYNmQKOU1yoVaHs3XbajZtSflZIZVSQfefSjOplgNm2SK30R96/Jw/jp7mTUgYg/QNUnT85MbndeRddqZ54xZEmqs7RATaOlIQSSRZHSmIJOkSg+zG9PxrHgbGxry4eYftE2Z8v1EykCuXMbt2j6ZGjVKo1WqGD1vLn3/uT/FxjYyyM7D/OH6u2wyIjBWaNW90qswK1S1kw5z6NSllnhuAR+99GHn0NEeeuH2xzSjFbUhufF5FzRDFvWQWqrntXi1niNIRUWffxHXooEQSG1HPy4+caC6fOEm6Q6WlRdeFM8ltk5cPHm9ZP/iPeK9gSC5sbfOy/8AECha0wNc3gHZt53D06I0UH9e+ck1GDJ2KqakZarWabTtW47TprxSfFbLNbcqseo40LFoQAJ/AIKaevsiqq7fS1Tb6pODz2gP4zgzRF0EkZ4jSFz4+PgAUL1481sMCJZLYKF68OPBj961JQSRJd7Qa/zuF7X4i+HMAaweM4POHjyk+Zt265dix8w9y5MjGs2dvadpkCg8fvk7RMQ0NjRjQfwwN6rUEIs8VmjXnDx4+StkdZGaGBoyvVZWeP5VBS6UkTK1m6WVXZpy5xKfgkBQdO7X44PGWiIgI9AwNMTTJQcDHTzHqRJ8hkkHV6YWAgABOnz5NmzZtAHj48CHh4fEvVyuVSszNzbGxsUm3J1X/KJndx6T6p6WlRfHixWnTpg2nT5/WnOCdFNK1INLV1WXp0qW0atWKoKAg5s2bx4IFsV/T8M8//9C8efNoeU2aNOHgwYMADB48mJEjR2JsbMyOHTsYOHAgQUFBKe6DJHE4dmlP5ZZNiVCr2fT7eDyfPk/xMX/9tQFL/uqLlpaKc+fu0bLFDHx8UjZ4umKFaowcNh0zM8svp02vZ73TYkJDU06Q6GlpMbhKBUY6VMJYL3Kb+b4HTxh97AxPP3xKsXHTgvDQUPzevSeHhTmmefPEKojCvp4h0k7XH4VZjvXr1wPQtm3bBNVXKpXkzZuXV69eZUqxAJnfxx/17/Tp05rnJqmk60+BuXPnUrFiRWrXro2NjQ1OTk64u7uze/fuGHVLlChBx44dOXnypCbv48fImYWWLVsyadIkOnXqhJeXFxs2bGDOnDkMHDgw1XyRfJ9i1expMuw3AP6dt4SH5y6l6HhKpZK5c7szdNj/ANi0yZnevf4kNDTlgqf19Azo2+d3mjeNvPfstccLZs8dzd17Kbc0p1BA+9K2TKlTnXw5jAG45uHJ70dPc949ZWfB0hKf128iBZF1Hl7evhejPFQd+aGrlNvu0x1CCNatW8e2bdvIlSuX5oLUuDA0NOT69ev069ePgIDUudg4tcnsPibVPyEE3t7ePzQzFK2/9JgMDAxEYGCgcHR01OSNHTtWODs7x6iro6MjwsLCRJEiRWLt68yZM2LixIman6tVqyYCAgKEvr5+gu2RJ1WnrH+58+cT0y4cE/PvXBJtJo1OhedLV+zZO1Zz8vTYsW1S3McypSuKLRtPCOfjj4Tz8UdiYP+xQk8v4c9gUlKN/HnFpT6dNCdMPx3aR7QvbSsUiuT3L72ltlPHivl3Lom6fbrFWp4/R3YROnmECJo6SvTp3TFD+pjZ30PpX9bxMSX9y/AnVZctWxZtbW0uXryoyTt//jxjx45FoVBEiyQvVqwYQgieP4+5vKJUKrGzs2PSpEmaPBcXF3R0dChbtiwuLqlz4rAkbvSMstHjzznoGxvhduMWu6fPS9HxzM1z8O/+CdjZFSE4OJRuXReyY8f5FBtPW1uHXj2G0rplN5RKJZ5eHsyZNwbXmyn37BU1NWFmPUeafjlY0S84hNnnLrPE5QbB34nHyCz4vIo/sDpUs2SmRiV3NEkkWZ50+ylgaWmJt7d3tJ02Xl5e6OvrY2pqGi2S3NbWFl9fXzZt2kTNmjV59eoVEydO5MiRI+TIkQN9fX3evHmjqa9Wq/Hx8cHa2jrRdiX3FuSo/jLi1uaE8D3/FEolHeZOxayADb6e79g1fjoGenqgp5ci9hQvnocdO3/HxiY3Pj7+tGs7jytXnvzQ6x+fj4UL2TJk0GTy5Y3cyXX8xD7WrF9AUFBAirznOfX1+L1KBbqVtUVbpSI8IgKnWw+Yc+ka3oHBaOvrk9j9VBn1GQ30/gCAmU2+WG3X0Y98xpRCYGRoCGQ8HxNKRn0PE0pm9w8yv48p6V9C+0y3gsjAwICQkOgBplE/f3vvUPHixTEwMODo0aPMmjWLFi1asH//fuzt7fHy8orW9uu+knJ/kYeHR6LbpGW/6YW4/Dvj6c5177doKZT0r1aHye4vU8wGgTdwDQgHDDE1rcWJE+2Trf+vfYyIELi7+eD+4gMI0NFRUayEOTXrjmT6rJHJNmYUIjyMiCsXiDh3EkKCAVAUsUWvbmP65janbzKMkdGe0beB/vz9/B6l7Cri5xczSF4EBxE+ZwIAf4wcAWQ8HxOL9C/jk9l9TEv/0q0gCg4OjiFYon7+Nnhq6tSp/Pnnn3z69AmA27dvU6FCBfr06cPYsWOjtf26r6QEYeXJkwd/f/9Et4sLIyMjPDw8kr3f9EJ8/pX+uRatJo8G4O9xUxl38kyK2fFLm2osW/YrOjpaXLz4kA4dFvDxw+dk6ftbH63z5Gfo4CkUKVwCgHMXjrFi5Wz8P/smy3jf0qxoQSbWqEz+LwHTt728mXD6EuderQSG/HD/GfUZNciRnd8P7eRzeCg5c5kSHhr9XCddlYo3Q3sBsHzpUsZOnpfhfEwoGfU9TCiZ3T/I/D6mpH9RfX+PdCuIPDw8yJUrFyqVCvWXtX4LCwsCAwM1wicKIUSMvAcPHlCyZEl8fHwICgrCwsKCR48eAaBSqTA1NeXt28Tf2O3v758iD2NK9Zte+NY/y6KFaTpqKAAnVjvh8s+BFBv7jz9aM3NWVwC2bTtL926LCAlJ/kMPP3/+TP2fW9K753B0dHTx8/vEoiWTcT59KNnHAqiYx4J5DWpRNV9kjIyHnz8TTp5ny637RKTApWMZ7Rn19/cnOCAAPUNDtI2N+OjmHq084KudS4ov23wzmo+JRfqX8cnsPqalf+lWEN28eZOwsDDs7e25cOECAA4ODly9ejXG0dzr168nIiKCnj17avLKlSvHnTt3EEJw9epVHBwcOHMmcgaiSpUqhIWFcevWrdRzSKJB39iY7otnoaOvx8PzLhz5a1WKjKNUKvnzzz70/60xAPPn7eX339f/0NHucREcFMbUScspU7oiAC5XzjBvwTh8fN4l+1h5sxsxrW512peJnIEKCA1j/oUrLLhwjcAUPt06o+HzyoM8xYtiap2Hd98IogghEChQINCVQdUSSZYn3X4KBAUF4eTkxIoVK+jevTt58uRhxIgRdO/eHQBzc3N8fX0JDg7m33//Zdu2bZw+fZqLFy/SoUMHHBwc6NOnDwDLli1j5cqV3L17Fw8PD5YvX87q1avlwYxpgEKppPOcyZha58H71Ws2/zERkQKHjOnp6bBl6whatKhCREQEw4auSbE7yWrXasJVF3fKlK5IUFAgy1fOYv/B7ck+jqGONiOqVWJYtYroa2sTESHYdOseE0+e541/8iz/ZTZ8Xr+JFER5Y7/TLEKpRBWhRk+Zbj8KJRJJKpGuPwWGDRvG8uXLcXZ2xtfXl4kTJ7J3714APD096datG05OTuzdu5f+/fszbtw48uXLx71792jQoAHu7pF/EW7fvp38+fOzcuVKdHV12b17N7///ntaupZlaTCgD8Wq2RMaFMyGIaMJiiXY9UcxMcnGvn/H4+BQguDgUDp1nM+ePRe/3zCRZM9uwrAhU6jhUA+1OoIHD28xbeYI3rxJ3sBwhQI6linB1LrVyWMcuVvi7ItXjDjizM23yT8DlZnQbL2PY0dphEKJCjXaWqrUNEsikaRD0rUgCgoKolu3bnTr1i1G2bcnl65du5a1a9fG2dfs2bOZPXt2cpsoSQSl6zhSt3dkLM+OiTN4+/hpso9hbZ2Lw0cmU7JkPj5+/EzzZlM5f/5+so9jX7kmI4dPJ6dJLsLCwihqa8n/WvfG1zd5A6er5LVifsPaVMxjAcDzD58YdewM/zx4kqzjZFY0l7zGceu9+PI5oqOSgkgiyeqka0EkyTyY5s1Du2njATiz8W9cDx9P9jFsbfNy5Ohk8ubNzevX3jRsMJF795J3tkZPz4Df+o6iSePIO5bc3B6zaMkkbt2+lqz3C+XNbsSMn2vQtrQtEHmw4oyzLvzlckNzoKDk+0TNEOWMQxBFKJSAFEQSiUQKIkkqEBahps2MCehlM+TZNVcOLFya7GNUqVKc/QcmkDOnEQ8fvqZ+vQm8evU+WccoYVuOMX/MIU8eGwB27FrHmnUL0dNL/HlWcaGvrcWIapUYXs0OA53IOKF1N24z6dQF3gUkz109WQmf15EHsppax35adUTUDJFSCiKJJKsjBZEkxTnxxg3zQgXw8/Zh08jxRIQn7wxHgwYV2LV7NAYGuri4PKRpk6nJelu9SqVFl0796di+LyqVCq93b5g1ZxQ3b10GSDZB1KZUMWbWcyRv9sjzhM6+eMWIw87c9JRxQknl01tPItRqdPT1MMplir+3T7TyCGWkINL6MlMkkUiyLlIQSVKUCs0b8+CTNxHhajaNHB/jC+lHadeuBk4bh6KtrcXhw9f5pfVMAgNDvt8wgVjnyc+YUXOxLV4GgOMn/mXxX1MICEi+czLKWZqxsGFtqtlEBv6++OjLqGNn2HP/cbKNkVVRh4fz8a0XptZW5MqbJ6YgIiqGSAoiiSSrIwWRJMWwLlGchkP7AXBy5TqeX3NN1v779m3IX0v7olQq+fvvM3TruoiwsOS7uLRJozb07zsafX0D/P19Wbh4Is5nDidb/7kNDZhSx4Hu5UujVCoICA1jzrnLLLx4LctcwJoafHj9BlNrK3Ja58HN9Xa0sqglM5WcIZJIsjxSEElSBH1jY7oumIGWjg6FjEyYtGVnsvY/dmwbpk7rDMDyZYcYOHBlsgU1Z89uwoih03CoVheAG66XmDnnD7y9vZKlf22Vkv6VyjOuZlWyf1lu+/v2fcYcP4uHnzxPKLnxfv2aIlQkVyy33kfNEGkrFTHKJBJJ1kIKIkmK0H7aOHLmseTDaw/6/1wxWfueO7cHw0e0AGDa1G1MmLAl2fq2q+jAHyNmYmpqRmhoKGvWL2DX7g3Jdrp1vcL5md+gFsVymwJw3cOTYYdPcenVm2TpXxKTD18Cq2PbaaZWyBgiiUQSiRREkmSnRpd2lKxVnfDQUHaMm8a0Bq2SpV+lUsny5f3o3acBAMOGrmHRon3J0re2tg69ew7nl1bdAHjx4gnTZg7n2fNHydJ/oZw5mFu/Jk2KFwbg3edAxp08i5PrXVLgJhHJV8S30ywqvF8LOUMkkWR1pCCSJCv5ypSkyZDfAPhn9iI8Hz9Lln61tFRscBpKhw6ORERE0LvXEtavP5EsfdvkK8T4MQsoVKg4AHv2bmTlmnmEhv54cLahjjaja9gzuEoFdLW0CFOrWXrZlelnLuEbnHzB35K48Xn1Gog8C+tbohZZtRRSEEkkWR0piCTJhr6xMZ3nTkWlrcXNIye4tGMvRkZGP9yvrq4227b/TvPm9oSFhdO503x27DifDBZD86Yd6PfrH+jq6vHhozdz5o3m8pWzydJ3+9K2zKhXQ3PdxtEnbow84sxD7w/J0r8kYUTNEBnnMkVHX4/QoGBNWdQMkUrqIYkkyyMFkSTZaDdtLDmtLPF++Zodk2YmS58GBrr8s28cdeuWIygohF9az+LQoWs/3K+xUQ5+HzGDalXrAJG308+ZO5qPn378WIDyluYsbFSbqvkiZySeffjEiMOnOPj4+Q/3LUk8QX7+BPr6YZDdmJzWefB88t+spUYQySUziSTLIwWRJFmo0bkdpWrVIDw0lI3DxxKSDKcqGxnpc+DgRKpXL4m/fyDNmk7lzJm7P9xv2TKVGDtqLrlzWxAaGsqqNXPZvXfjD/drqq/H7Jr29PipDEqlgs8hocw658LiS9cJSebDKCWJw+e1BwbZjTG1toomiKION5DnVEskEimIJD+MdYniNB7aH4B9cxbj8fDHDxTMkcOQw0cmU7lyMT59+kzDBpO4fPnHApxVKi26dh5Ax/a/olQqcX/5jKnTh/5w4LSWUon68jmu9myn2Ua/9VbkNvo3/nIbfXrA55UHeUvaxgisVn+JaJeCSCKRSEEk+SF0DQ3oPHcqWtra3D7uzMXte364T1NTY44dn0L58oXw9vajfr0JuLr+WHC2uZkV48bMp1TJnwA4cGgHS5fPIDg46If6rVUgH4ua1CXi6L9k19PF9Y0XQw+f4uJLjx/qV5K8aHaafRNYHU6kIJKb7iUSiRREkh+i1biR5MpnzYc3b5MlbsjcPAfHT0yjVCkbvLw+UrfOuB++sb66w8+MHDYdI6PsfP7sx/yFEzh99sdOnLbJYczsejVpWbJoZIa+AcP2HWHZxatEyH306Q6f15EC1fSbs4jCI77MEMm3TCLJ8khBJEkyFZs1okKTBqjDw9nyxySC/H7sfi9Ly5ycPDWd4sWt8fDwoW6dcTx69DrJ/Wlr69C/7yj+16wjAPfuuzJ1xnC8vJI+exN1G/0IBzv0tbUJV0ew7tY9+v+9B6epC6UYSqd8eht5ynh2c7No+eGaJTP5vkkkWR0piCRJIpdNXlqOHQ7AseVreXHz9ndaxI+1dS5OnppOkSJWuLu/o07tsTx/7pnk/vJaF2DiuEWas4W2blvFug2LUauTfkdYyxJFmV2/JjY5Im+jd37+kuGHT+EeFMJv+gZJ7leS8vi+9wYge+5c0fLDReRJREqphySSLI8URJJEo9LWpvOcqegaGPD0ynVOrvmxHVr58uXmlPMMCha0wM3Ni9q1xuDu/i7J/dWt04xhgyehr2/Ih4/ezJz9O9euX0hyfyXNcrGgYW1qFcwHgPsnP0YdPc3uL7fRJ8dZS5KUxdfrPQCGJjnQ0tEhPDQUgLAv998p5QyRRJLlkYJIkmgaD+mHdYlifP7wkS2jJiF+4FLVAgXMOXlqOvnzm/P06Rvq1B7Hq1fvk9SXrq4egwdMoOGXq0JuuF5i+qyRfPiQtP5y6OkyoVY1+tqVQ0ulJDgsnHkXrjD3/BWCwuRt9BmJID8/wkJC0NbVxTi3KR883gJfCSK51CmRZHmkIJIkimLV7HHs0h6AbeOn4/dlKSIpFCxowSnnGeTLl5vHjz2oXWsMb94k7RRnm3yFmDR+MfnzF0GtVrNx81I2b11ORBLEmlKhoGv5UkyrW53chpFLYf/cf8zvR8/w4pNvkuyTpD2+796TK6812c1yawRRqDoClFIQSSQSKYgkiSCbqQntp48H4PzWnTw4m/RlqEKFLDnlPJ28eXPz4MEr6tQei6fnxyT1Vf/n/zF44ET09Q3w8XnH1BnDuXX7SpL6qmRtyeJGdaiQxwKAB+98GHb4FCefuyepP0n6we+dN7nyWmNslluTFxYhBZFEIolECiJJgmk3dRxGpjl5++QZ+xcsTXI/hQtbcsp5BtbWubh//yW1a43l3btPie7n2yWya9cvMGPWyCRdv2GezYDpdWvQpXwpAHyDQ5jifIHlV24S/gNLgpL0g++7yKXT7F8JolB1OGhpoZCCSCLJ8khBJEkQDh1+wbZ6VcJCQtj8+wTCQ5J2U3uRIlaccp5Bnjym3Lv3kjq1kyaG8uYtyKTxiyhYoBgRERFs2LiELX+vSPQSmZZSyW+VyzOuZlXNKdMbbtxh3IlzvEuG60ck6Yeo5V3jr3aaharVgBZKKXolkiyPFESS72JZtBBNhw8AYP+8JXg+TdolpUWKWOF8egZWVqbcvetOndpjef8+8TE5dWo1YfjQKZG7yD68Z9rMEbjedEl0P7UK5GNho9qUMIv8grzm4cnggye46pH07f6S9EvUTrPs5l/PEEXeMSeXzCQSiRREknjR0tWl0+wpaOnocO/0eS5s252kfgoVtuDAgbFYWZly584L6tYZl2gxpK2tzW99x9C8WQcAbri6MG3mcD5+TFxgt7WxEXPq16R1qWIAvA8IZNyJc2xwvYP8Xsy8xDZDFBIeuVtQIeQMkUSS1ZGCSBIvTYf9hkXhgvi992b7hOlJ6kPwmYMHx2NpacKdOy+oU3ss3t5+ierDwsKaSeMXU6xoZIzPxs3LcNq0JFFLZDoqFUOqVmB0jSoY6mijjohgxdWbTD51gU/BSVsClGQcNDFE0QRRGIBcMpNIJFIQSeKmePUqOHT4BYBt46YR8PFTovsoWMgCuPRDYqhqldqMGjkLI6Ps+Pp+ZPqskVy9di5RffxcKD+LGtWmSK6cAJx3f82Qgye57ZW0M4okGQ/fd19miL4Kqg7+cp6UQgoiiSTLIwWRJFaymZrQbuo4AM5s2saji5cT3UfhwpYcPDgOCOHevZfUrTMuUWJIqVTRq8dQ2rftDUTeRTZ52hDev094jE/e7EbMa1CLFiUiL2F96/+Z0cfOsPX2g0T5Isn4+L2PFL+6BvroZTMk+HMAwWFfZojkkplEkuWRgkgSK22njMXINCdvHj/l0KLliW5fuLAlzqdnYmWVEzCiWdMZiYoZypkzNxPGLqBsmUoA7Ny9gVVr5hH+ZYnje+ioVAytWpHRNewx0Im8hPWvyzeYevoi/iGhifZHkvEJCw4h0M8PA2Njspvl/iKIZAyRRCKJRAoiSQyqtWtFiRrVCAsJYcsfEzX3PiWUyEMXI7fW37//ihIleiRqZqhc2cqMHzOfnDlzExDwmTnzRnP2/LEEt69T0IbFjetQ9Mvy2NkXrxh88CT33iX9VG1J5sDX6z0GxsYYm+XG6/kLzQyRXDKTSCRSEEmiYV6oAE2HDwTgwIK/Er3FvlAhS5xPRx66eO/eS5o1ncHz5/0S1FahUNC+bW96dBuCSqXi2fNHTJoyiNceLxLUPo9xNubWr6XZPfbW/zOjjp7h7ztyeUwSid97byyLFNIczhj45TwtGVQtkUiUaW1AfOjq6rJmzRo+fvzImzdvGDZsWJx1GzVqhKurK/7+/ty6dYumTZtGK//48SNCiGjJ0NAwpV3IUKi0tek0ezLaero8OH+J81t3Jap95N1k0zViKDEB1EZG2Zk+ZTm9ew5HpVJx5NgefhvUJkFiSEupZGjVitwZ0IPWpYqhjojgz0vXKb1knRRDkmhE7TSL2nofFPplhkgumUkkWZ50PUM0d+5cKlasSO3atbGxscHJyQl3d3d2745+Fk7p0qXZs2cPI0eO5NChQ9SvX59du3ZhZ2fH7du3sbKyIkeOHBQsWJDAwP9OHw4ICEhtl9I1jQb3xapYEfx9PrB93LREtS1QwJxTzjPImzc39+//dwK1kZHRd9sWLVqKSeMWY2lpTWhoCIuXTOHQkYSJsWr58vBXk58paR75BXfxpQeDDpyQu8ckseL3Zdk06nDGqBkihVCnmU0SiSR9kG4FkYGBAb169aJhw4a4urri6urKnDlzGDBgQAxB1KFDB06dOsWSJUsAWLZsGc2aNaNNmzbcvn0bW1tb3rx5g5ubW1q4kiEoWsWOml0jDzzcPmEG/j4Jv3W+QAFznE/PJF++/y5qTeh1HE0bt2VA/3Ho6Ojg8eYlk6YM4umz78/q5DY0YFY9RzqXKwlEHq445vhZNt68Kw9XlMTJtzNEmiUzIRBylkgiydKkW0FUtmxZtLW1uXjxoibv/PnzjB07FoVCgfjqW8/JyQkdHZ0YfWTPnh2AEiVK8Pjx45Q3OoNimCM77aZF3mJ/YdvuRN1inz9/5MxQvny5efjwNXVqj8XL69N32+np6TN08GTq1W0eOe7Fk8yc8wcBAf7xtlMooFeFskyrWx0TfT0iIgRrb9xm3IlzfAwKTrDdkqxJ1GnVUTFEAV8dyCm+nFotkUiyJulWEFlaWuLt7U1Y2H/brL28vNDX18fU1BRv7/92DD18+DBa2xIlSlCnTh1WrFgBgK2tLQYGBjg7O1OsWDFcXV0ZMmQIT548SbRdCVkCSkp/yd1vYmg7bTzZzXLz/sVLTq/ckGBbbGxyc+DgOPLly83jx29o1nQGAQHh0drH5l8eKxtG/T4Hm3yFUKvD2bR1OXv/2YhSGf/rUNrMlPk/V6eCpTkAt7ze8/uJ81x7+w60tDEy0k6K+z9MengPU5LM5F/Y58hl8hzmZhgZGaHQUmnKRLg6U/gYG5npPYyNzO4fZH4fU9K/hPapANLlAkOnTp2YNm0a+fPn1+QVKFCA58+fY21tjYeHR6ztTE1NOX/+PF5eXtSqVQshBKdOnSJv3rz07dsXPz8//vjjDypVqkSJEiX4/PlzguwxMjLCzy9xJyxnBO58eMfxN89RKhR0KFgKM/2EBZoLAoFLQBBgCFRBgd53273z8ufRfU/UaoGOjooSpS3JYWIQ/1ghwUQ4HyXi6gUQAnR0UdZqgNKuCgqlKt62EsnX+IeFsvrRDRTA4JKVUYd9hllTAFAMnYBWJv2ykUgkYGxsjL9/3KsQ6XaGKDg4GF1d3Wh5UT9/HRj9NWZmZhw/fhylUknr1q01y2oNGjRAW1tbE0TdsWNHXr16RdOmTfn7778TZVeePHnifUETi5GRER4eHsneb0IwzWfNr+uXoaOvx5Elq5iwdWeC2uXLl4sDB8djY5ObJ0/e0LjRtDiXyaL8y5fPhtYte9CsSXsA7ty9ztwFY/j0ySfesZoXK8j0mlWxNIoUansePmW88yU8Z/yZcEdTmLR8D1ODzOSfUqVk3OmDKFUqrAvkx8RQG9e2LVEgEOHqTOFjbGSm9zA2Mrt/kPl9TEn/ovr+HulWEHl4eJArVy5UKhVqdeQOEAsLCwIDA/n06VOM+lZWVpw6dQqAmjVrRltSCw0NJfSrwwVDQkJwc3MjT548ibbL398/RR7GlOo3LlRaWvQcPxIdfT2eXL7GsVXro8VlxUX+/OYcODgOG5vcPHr0mtq1xvL2bfwB2MHBYYwdtYBiRUsDsOXvlazbsJiIiLh39hTKmYPFjetQr3ABAJ74fGTQgROcfO6eCC9Tl9R+D1ObzOKfv88HspvlRmWoj4/PeyKUKlQR4Yjw0EzjY1xI/zI+md3HtPQv3Z5DdPPmTcLCwrC3t9fkOTg4cPXq1Rhf3AYGBhw5coSIiAgcHR15+/ZttPKnT5/StWvXaPWLFCkSI/YoK9FgYB/ylrQl4JMvf4+dkiAxFLmbbAY2NmYJFkM/la/CtcvuFCtaGn9/X8aM78uadQviFEM6KhVjHavg2r8b9QoXIDgsnCnOF/hp2YZ0LYYkGYf/dprlJjQ0FKGI/BgUYQm7FkYikWRO0u0MUVBQEE5OTqxYsYLu3buTJ08eRowYQffu3QEwNzfH19eX4OBgxowZQ6FChahZs6amLKoPPz8/Dh48yOTJk3nx4gXv379n6tSpvH79mkOHDqWVe2lK4UoVqNmtIwA7Js7ENwFn9kQeuvjfbrLatcbg6fkxzvpKpZIunX6jc8f+hIdF8OTpfSZMHoin5+s429QumI8ljetqbqQ//vQFgw+e4OmHT4lzUCKJh6idZsZmuXgVriZCqQQ1CLXcZSaRZGXSrSACGDZsGMuXL8fZ2RlfX18mTpzI3r17AfD09KRbt244OTnRqlUrDAwMuHLlSrT2GzZsoHv37vz++++EhYWxdetWsmfPzqlTp2jUqBERWfC4foPsxnSYMRGlUsmlXf9w99SZ77YpWNAC59ORhy5GnTMUnxgyyWHK2NHzqPBTVQCsrLPTqm0vPnyIPV7IIpshcxvUpG1pWyDyyo0Rh53Zee9REjyUSOIn6g+A7Ga5CQ+PQCgiA/PltnuJJGuTrgVRUFAQ3bp1o1u3bjHKFAqF5v+2trbx9hMSEsKIESMYMWJEcpuY4fhl0miym+fmnZs7/85Z/N36hQtbcvLUdI0Yql1rTLznDJUtY8f4MQswNTUjKCiAZStnsv/ADsLCYl4Qq1Qo6FupHJNrO5BdTxd1RATLrrgy+dQF/OSN9JIUQnMWUe5cqNVqhPLLklm4XDKTSLIy6VoQSZKXyq2aUaZuTcLDwtj8xwRCv3OQYdGieTjlPB0rK1PNdRxxiaFvL2Z1c3vMpGmD+fgx9uU4uzwW/NXkZ8pbRS5vXn39lt8OHOfm23c/5KNE8j00MUTmX2aIvhzdIJfMJJKsjRREWQTzgvn53x9DATj850o8HsR/cretbV5OnpqOhYUJd+68oG6dcbx/7xtrXWNjE0b/MRv7So4AHD22l0VLJhMcHBTjQCwTfT2m1nGgV4WyKJUKPgUFM/7keVZfu0WEvHNDkgpo7jPLnYuIiAgivgRVI5fMJJIsjRREWQAtHR06zZ2Kjr4ejy5e5ozT1njrlyplw4mT0zAzy8HNm8/5ue54fHxiP5SyVMmfmDB2IblzWxASEsyff02N9WJWBdC5XElm1XMkt2HkQYybbt5j9LEzvAuI/VwpiSQliJohirq+478lMymIJJKsjBREWYCmwwdgVbQw/j4f+HtM/Fvsy5YtwPET08iVy5jr159S7+fxfPwY8zRvhUJB2za96NV9CCqVFi9fuTF56mCeu8UMhBaeHhxs35zKeSwAuP/Om4EHTnDOPe4dZxJJSuH7ZYbI0CQHKm1tjSBCxhBJJFkaKYgyOSVrOuDQ4RcA/h47Nd5b7O3sinDk6BRMTLJx5cpjGtSfwKdPATHqGRubMPr3WdhXrgnAiZP7WbB4IkFB0esa6+owvVZVwlcvpnIeCz6HhDLtzCX+vHSd8Cy4w0+SPgjy8yMsOARtPV2ym+X6b4ZIxhBJJFkaKYgyMcZmuWk7dRwApzds5dEFlzjrVqtWgoOHJmJsbMCFC/dp3Ggyfn4xl7LKlrFj7Oj55M5lHrlEtnQqhw7HXCLrWKYEM+s5YmFkCEKw9+Ezhh08jodfwu6Ok0hSEt/378mV1xrj3Lk1BzMSHvfJ6RKJJPMjBVEmRaFU0nHmRAxzZOfVvQccWrw8zrq1a5dh37/jMTTUw9n5Ns2aTiUgIPoONKVSSacO/ejS6TdUKhUvXz5nyvQhPHsefYmsjHluFjeuQzUbawCefvhEsYEj6VW+Iv7+UgxJ0gd+77zJldea7Oa5Iw9mRM4QSSRZHSmIMik/9+lG4UoVCAkMZPPvE1DHETDasGEFdu8Zg56eDocPX6dVyxkEB0c/A8jU1Iwxf8zlp/KR16gcObaHxUumEhz83wxSdj1dJtWuRl+7cqiUSgJCw5hx5hLr7j7Ce9HqlHNUIkkC/13f8d+SGWo5QySRZGWkIMqEFLL7iZ/79QRg19Q5eL+MPXi5detqbN4yHB0dbf75x4V2bWcTGhpdONlXcuSPkbPIkSMnQUEBLPxzMsdP7NOUKxTQrXxpptWtrtk9tvPuQ/44eobXfv4xtt1LJOkBzeGMZrkRUYe8yhkiiSRLIwVRJiObqQmdZk9GqVRyec9+bhw4Gmu97t3rsmr1AFQqFX//fYauXRYS/lUMhba2Nn16jaR1y8hLcZ8+e8CUaUN59dpNU8cujwWLG9el4pfdYw/f+zD44Emc3V6moIcSyY/z3/UduRDKSEEkt91LJFkbKYgyEQqFgg4zJmKcOxdvnzxj78z5sdYbMqQ5Cxb2AmDVyiP077882r1uefMWZPyY+RQpXAKA3Xs3snL1XM31G+bZDJhWtwZdy5cCwDc4hKmnL7LssqvcPSbJEPx3wWtuItRRM0RyyUwiycpIQZSJqN2rC8WqViYkMIhNI8YRFhwSo86kSR2YMLE9AHPn7OaPPzZEK2/UoDUD+o9FX9+AT58+MHveaFwunwZAW6VkoH0FxtSwx1hPF4ANN+4w/uQ5vD7LwxUlGQfN4Yy5cyHefTmKQgoiiSRLIwVRJqFghXI0+K03AHtnzMPr+Yto5QqFgoULezFocDMAxo7ZyMyZOzXlxkY5GD50CjWq1wfghuslZsz+HR+fyLvFGhYpyLwGNSmSKycAV16/Zeihk1z18Exp1ySSZCfqcEZjs1xEvP8YmSkFkUSSpZGCKBNgZJqTTnOmoFSpuLrvEFf3HYpWrq2txfoNQ+jQIfKusYEDVrB06UFN+U/l7Rn1+xxy5zInLCyUdRsWs2PXOiIiIiieKydzGtSiQZECALz1/8y44+fYfPse8uoxSUbF733kDJGugYEmhkgKIokkayMFUQZHqVLRac4UspvlxvPpc/ZMnxet3NBQj127R1O//k+EhYXTresi/v77DBAZON2z+1Da/hK5I+3ly+dMmzmcJ0/vY6Kvx7iaVehnVx4tlZLQcDV/ulxn5lkX/ENCY9ghkWQkwoJDCPTzw8DY+L/LXaUgkkiyNFIQZXAaDuxD4UoVCA4IwGnYGEKDgjRlpqbGHDg4gcqVixEQEEzrVjM5evQGAAULFGPMqLkUKlgMgH37/2b5ylmEh4XQt1I5JtaqhqmBPgD7Hz7lj6OnefrhU6r7J5GkFL5e7zEwNv7vHKIIKYgkkqyMFEQZmJK1qlO7ZxcAtk+YwTs3d01Zvny5OXxkMra2efHx8aNxo8lcufIYpVJJm9Y96NFtMNraOnz86MO8heO4eOkU9QsXYE79mtiamQJwz8ub4UdOceq53EYvyXz4vffGskghhCpSECnkOUQSSZZGCqIMimlea9pPGw/AmU3buH3slKasXLmCHDw0EUvLnLx69Z4G9Sfy4MErLCysGTVyFmXL2AFw4eJJ5i0cRx4dJfs7taL+lzgh74BAJjtfZM31W6gjZKCQJHPi994HQJ5ULZFIACmIMiRaurp0XTAdfWMj3G7c4sCCvzRlP/9cnl27R2FkZMCdOy9o1HASHh4+NG3Sjn59fkdf35DAwAD+Wjada+cPM7VWVXpVKINKGRkntOTyDWaddcE3li37Eklmwt87cqeZUEUFVcsztCSSrIwURBmQNpNGkad4Ufx9PrBx5Hgivpww3bVrHVatHoC2thanTt2iZYsZ6OoaM2fWWuwqOABw6/ZVFi0cS9sCFmwa1FNzntDe+48ZfewMzz/6pplfEklqErX1XqhUkRkyhkgiydJIQZTBqNGlHRWaNEAdHs6mEePw+3LA3Pjx7Zg8pSMAmzc707PHn9Sp3Yzf+o0hm6ERISHBrFm7AD23a5xsU5+82Y0BuObhye9HT3PePfb7ziSSzIqf95clsy+CSCGXzCSSLI0URBmIIvZ2NB02AIB/5y7m2TVXdHS0WL1mEJ071wJg1sydLF50gmmTV1DJrjoA9+67cn7LQv74qRhlyzUE4MVHX8afOMeOew/leUKSLIn/l+s70JIzRBKJRAqiDEPOPJZ0njv1y+GLBzm/dRempsbs2TuG6tVLEh6u5rf+K/B8m4N1qw9gYGBIaGgIp7YvpSYf6NM4Uhx9Cgpm9rnL/HX5BiHh8gtAknXxjRJE2lEzRDKGSCLJykhBlAHQ0dej++LZGObIzss799k1ZQ7Filmz/8B4Che24tOnz/zaewNVKnekQ5vKALjfuYSh6yGmFs4HGBESHs7yKzeZddaFD0HBaeuQRJIO0OwykzFEEokEKYjSPQqFgrZTxmJVrAj+Ph/YMHQUjtVt2bFzFCYm2Xj+/B1LFj2kZ7c56OrqofbzIch5M61z6qJTOB8Af9++z8STF3jxSQZMSyRRhIeEEOTnr9l2L2OIJJKsjRRE6Zx6/XpSrkFd1GHhOA0bQ9e29sxf0BOVSsWF8548fWTJ/5o5oAoPRXl5L+WC32KY2wCAY0/dGHfiHDffvktjLySS9Inve28iTCI3GCgi5JKZRJKVkYIoHfNT43rU6xd5z9g/M+Yw7jdHuvf4mfBwBf/uDcLI0IGC1mpMnl4ij8ctjLRVoK3NlddvGXfiLKfdXqWxBxJJ+sb/vQ8RuU0if5BLZhJJlkYKonRK/rKlaTtlLAA3dm5nztDqVKlii9dbA1yvG5HD0BALr/uYPb9MNkUEaKt48M6HCSfPse/h0zS2XiLJGPh5eyOURQA5QySRZHWkIEqHmFhZ0G3xLLR0dAi8d4VZfcqR08QC16s5+fwpGwU+PcLC/TqG6mBQwLMPn5jmfJG/7zwgQu6hl0gSjN87byKiYojkDJFEkqWRgiidoWtoQM+/5mFkakK+kBc0bm6D15vcXD1vjNm7ZxR97YpeaAAAr339mX7mEk6udwmXf91KJInGz9uHiKjLXeXvkESSpZGCKB2h0tKi26JZ5C1akOom3lhrm3P7ak6M3V/wk8dx9EI+A/DG7zNzzl9m7fXb8iwhieQH8Hv3XnOXmZwhkkiyNsq0NiA+dHV1WbNmDR8/fuTNmzcMGzYszrrlypXDxcWFgIAArly5wk8//RStvF27djx9+pSAgAD27NmDqalpSpufKBQKBW2njsWuxk/8z9wfrTcW+Bz5RMkLeyn8/Dx6IZ956/+ZYYdPUXzxGpZddpViSCL5Qfy8fVBHLZkJgSKN7ZFIJGlHuhZEc+fOpWLFitSuXZv+/fszceJEWrVqFaOegYEBhw4d4ty5c1SoUIGLFy9y8OBBDAwit5/b2dmxdu1aJk+ejL29PSYmJmzYsCGVvYmfOn27075tbaqHCoIP+VHw+H4KuV1ENzSAt/4BDD98imKL1vCXyw2Cw8PT2lyJJFPg994bteq/j0GdqEMaJRJJliNJS2anTp1CxBO8W6dOnSQbFIWBgQG9evWiYcOGuLq64urqypw5cxgwYAC7d++OVrdt27YEBQUxcuRIAIYMGUKjRo345ZdfcHJyYsCAAezYsYNNmzYB0LlzZ9zd3cmfPz8vXrz4YVt/FFdvD8Z0/B+5zr7CzOMB2uEhAHj4BzDzzEWcXO/K2SCJJAXwe+9DhPK/eSEdVbr+G1EikaQgSRJEp0+fjt6JlhYFCxakcePGTJs2LTnsomzZsmhra3Px4kVN3vnz5xk7diwKhSKaILO3t+f8+fPR2l+4cIEqVarg5OSEvb09s2bN0pS9fv2aly9fYm9vn+aCqH//PuQ6cJTcr5+gioic+XkdEMyUE6fZcus+YfJ+JYkkxQgNCiJc/Pc7pi1niCSSLEuSBNGUKVNize/atSutWrVi/vz5P2QUgKWlJd7e3oSFhWnyvLy80NfXx9TUFG9v72h17927F629l5cXpUqV0pS/efMmRrm1tXWi7TIyMkp0m/gYZWaK4csHAHgKJeMOHWffw2dECIGegSF6yTpa6hP1eiX365aeyOw+Znb/QkNDiVAoUAqBibFxprzrL7O/h5ndP8jcPtoXLcacXl1wO34wRfxLaJ/JusvszJkzLFu2LFn6MjAwICQkJFpe1M+6uroJqhtV73vlicHDwyPRbeLj/d6NfPbR5//t3XdYFFcXwOHf7tKbiCIgoKKxgLF3RbHEEhNbYuzGqLHkM8bEXmLvvSUxxa7pMfYea+wVNbE3BERQpPfdne8PZAOCCgoswnmfZ5+Emdk79zAsHmbuPdfKuzFu5auwZuLsbG0/r8ju71telN9jzK/xnbl7GkWlAUXLuVOnUBV2MHaXckx+vYYp8nt8kH9iVOLjiDt7Bu35U1g+fgAh99A+fkBAQAAqlXGmN7xUQuTu7p5um62tLSNGjMi2R1Dx8fHpEpaUr2NjYzN1bMpxL9qfFa6urkRFRWX5fc9ia2tLYGBgtrebV+T3+CD/x5jf41u9eTkVVcljhxo1qM+FgPsveMfrJ79fw/weH+SfGOu4OvN5I2+auhbFFAVTQEFFdDEXYutUpY6bW7bHl/K9e5GXSoju3r2bblC1SqXC39+fPn36vEyT6QQGBlK0aFE0Gg26J6tQOzs7ExsbS3h4eLpjnZ2d02xzdnYmKCgoU/uzIioqKkd+GHOq3bwiv8cH+T/G/BpfTHQ0erUGdJAYH58vY0yRX69hivweH7yeMRayMKd7FS8+865LaTvrJ1sVYqwKE1umBFFVijJh6R/8OvALo8b3UgmRh4dHmq8VRSExMZHg4OBs6RSAr68vSUlJ1K1bl6NHjwLg7e3N6dOn0yVjJ06cYPTo0Wm2NWjQgOnTpxv2e3t7s2bNGgDc3Nxwd3fnxIkT2dZfIcTrKS4mBuXJHSKZZSZE9vF0LMKgOtXoWa0SlibJExZ0ahMeFS2NtpI7tpXgTLglS2es4eRPf8DC5Ubt70slRPfu3cvufqQTFxfHmjVr+Pbbb+nduzeurq4MHz6c3r17A+Dk5ERERATx8fH88ccfzJo1i0WLFvHdd98xYMAArK2t+e233wBYtmwZBw8e5Pjx45w+fZrFixezbds2o88wE0IYX1zKHSKkDpEQr0qlgpZvePBp3eq0eOO/mycxVoV54FQeTRVn3MtFcTNWxfYAW/as+Y2/vluVJwaL5+mlO4YOHcqyZcs4cOAAERERTJw4kY0bNwLw4MEDPvroI9asWUNUVBTvvvsu3377Lf379+fixYu0bt3aMEboxIkTDBgwgClTpuDg4MCePXvo16+fMUMTQuQRMVHRKKrkIq5yh0iIl2OqUdOlkifDG9TGs1jyShAK8NihJIFOXlhWsMCjbCRx8QHs8HfgboItpzdvZ+vcJcbteCp5OiGKi4vjo48+4qOPPkq37+lR6KdPn6ZGjRrPbGvNmjWGR2ZCCJEiJiIC/ZO/Tk3VcodIiKywNjOlb43KDKlXA/dCdgBo1aYEO5UnyKUikZrHeDdKwso6ln0H/sHXpCLqwrZcO3aS3ybNfG6R59yWpxMiIYTIadERESh2JQCwsnzdK38JkTtszc0YVKcaQ+rVpIiVJQBxGnOC3arwwMmTmwE3qe1xi7oVrUlMTGLosPWo67bD1dORBzdvs3b4l+jz2AoMkhAJIQq0uOhY9E8GVRcqVMjIvREib7MzN2NQneoMqVcDhyeJUBimhJapS4jjGzx4FMKN0z8ydHgtLC2tuXMnmC5d5lCpx8dU9CxHVOhjVnw6nPioaCNHkp4kREKIAi0pKRHlyaMyW3vjD+wUIi+yNjNlcN3qfF6vpiERCkpUCKvQiDCnciQmafnjt+U0aKRh3HgfALZsOclHvRbSqF9fKjZpSFJCAquGjOJxYNZL3uQGSYiEEAVaYlISetWThCgPzHQRIi8x02j4uGZlxjSqi5NNcg2h2xExBHnUgvL1QKXm+IkD/Pr71yz77kPq1q2AVqtj7Jg1zJu3kVrt38Hnw64A/DJuKn4X/jFmOM8lCZEQokBLTExEUSc/MrO1k4RICAC1SkXXSp5MaNoAj8LJj5LvhEVw3KQYJVv1BZWa0NAQlnw1lZi422zdPg43t6I8fhxFpw9msX//Rdy8yvP+lyMA2P3Ncnx37zNmSC8kCZEQokBLSkoyFGa0tbUxcm+EML7GHu7MadmYqi5OAARFRbPudjDl3htMSSdXALZt/5Xvls/jnXeqsHLVLCwtzbl8+R7t2k7j1q0grAvb02vhTEzNzfn34BH2frvSmCFliiREQogCLTHxv0dm1tZWRu6NEMZToagDM1v48E75MgCEx8Wz8MR5tJWa0fqTLwAICLzLvAXjuXDxFGPHdmLa9J4AbNt2mu7d5hIVFYdao6HHnCk4FHfh4d17/DR2cp6aXv8skhAJIQq01I/MrC0lIRIFT2FLCyY2aUD/mlUw0ahJ0un4/swFNgTFMHDIVFxdSwLw56Z1/LBiPklJCXz77SD6D2gFwNw5GxgzZi16vR6A1p8NpFzdWiTExrL6izF5ckZZRiQhEkIUaImJiYY7RFKHSBQkapWKvjUqM6WZt6GW0NarNxm37yj1W3dn8pABqNVqgkPuM2feWM6dP46VlTkbN33Ju+/WQqfT8dng71m2bIehzcotmtKkTw8Afp0wgwc3bxsltpchCZEQokDTanWGO0SW5uZG7o0QuaNBCVcWtW5GFZdiAPwT/JChO/dzNU7hy3FLqehVDYBde/7kq6+nExMbTbFi9mzdNoFatcoSG5tAt65z2bLlpKHNIu5udJ48FoADK9dzIY8Pon6aJERCiAJNq9XBk4TIwtQEE3NztAkJRu6VEDmjmLUVs1v60L1KRQDC4uKZtP8o35/xxcenNT8MmYyNtS3R0ZHMXziBg4d3AlCihCN7/5pG2bLFefgwgrZtpnLy5DVDuxpTU3rOm4qFjTW3zp5nx5JvjRLfq5CESAhRoGm1evRPEiK1To9dUYc8WzhOiJelVqnoX6sKU5p6Y29pgV6vsOLcRSbsO0K0TmHYF9N5u9X7APzz7zmmzRhGcMh9AMqXd2PP3im4uzty504wLVuM5+bNtJ+RNsM+xd2rAjFh4fw4aiJ6Xd5aliMzJCESQhRoOt1/d4g0ej12RYtKQiTylerFnfj63ebUcHUG4GzgAz7d9hdn7z/A3b00c8cvxsOjHDqdjnU/fsO6H5eh1ycnNFWrlmbX7skUK2bP5cv3aNF8PPfvP07T/ptNfWjYvRMAP42bQkTww9wNMJtIQiSEKNC0Wr1hDJFGr2BXrKiReyRE9rAxM2VyM28G1a6OWq0iPC6e8fuO8MOZC+gVhWZN3mXYF1OwtLQmNDSEqTOGcuHiacP769f3ZNv2Cdjb23DmzA3ebjWJ0NDINOcoXNyZzlP/Gzd09e/juRpjdpKESAhRoKUeVK3R67BzLGLkHgnx6t4tX4bF7zTDvZAdAD9duMzI3QcJiYnF1NSMzz4ZS7s2yUtqnDt/nGkzhhEWHmp4f9Omldm8ZTzW1hYcPvwPbdtMJTIyNs051CYaes6ZipWdHXcvXGLH0tdv3FBqkhAJIQo0rVaHonkyhkivx7ao3CESry8XW2sWtW5GB69yANx6HM6nW/ey77YfAMUcXZg8cSkVyldCr9ez/qdlrFn3laGGEECzZlXYsnU8lpbm7Nx5lo7vzyQuLv1Eg5b/60fJKm8SGxHJ+hET0Gtfv3FDqUlCJIQo0FLPMtPoFQrJIzPxmupdvRKzW/hgb2mBVqdnwbHTTD90nLgkLQDVqtZlwriF2Ns7EBEZxvSZIzh95u80bTRvXo1Nm8dhaWnO1q2n+KDjTBITtenOVaZmNZr2Ta5S/dvEGYQFPcj5AHOYJERCiAItbUKkx66oPDITr5fShQvxTdsWNC2dXFH6dEAQA7fs5lLwI8MxnTr2of/Hw9FoNFy/8S8TJg8mODgwTTstW1Zn46ZxWFiYsXnzCTp3mp1hMmRpZ0u3mRNRq9Wc3LCFS/sO5WyAuUQSIiFEgabV6lM9MtNh5+xk5B4JkTlqlYrBdaszuak3VmamxCYmMXH/EZaeOIf+ydphFhaWjBw2gyaNWwOwe89GFiyeSGJi2kdgrVrVYOOmcZibm7JpU3IylJSUPhkC6DhhFPbOTjz082fT7EU5GmNukoRICFGg6XR60Px3h6iQk6OReyTEi1Uo6sD37VtR1704AAdu3+OTLbu5HRZhOMbZ2Y1pk76mTJkKaLVJfPXNDDZv/SldW82bVzMkQ3/+eYyuXeY+Mxmq2bY1VVs2Q5ek5cdRE0mMi8uZAI1AEiIhRIGWPMtMBYBG0WNlZ4e5lRUJsbEveKcQuU+jUjGyYW3GN66PuYkJEfEJjNpzkJVnL6U5rmqVOkwav5hChQrzOOwREyd/xj//nk3Xno/Pm2mSoS6d5yQ/Rs5AETdXOowdCsDub5bj/++V7A/QiCQhEkIUaKnHEKme/ENg71yM4Nt3jdgrIdJTgoPY070DVZ2T72LuuH6bQVv3EBiZdjX5Du16MOiTMWg0Jly7/g/jJw3i4cP0g57r1avA1m0TsLIyZ9u203TtMveZyZBao6HbrIlYWCcvzbF/5brsD9DIJCESQhRoWq3O8MhMrU0CwN7ZSRIikWeYqNUMrVMN7Q+LqersSFhcPMN27mf9hctpjzMxZcjgCbzbOrlq9N6/tjBv4ZfpxgsB1KxZlh07J2FjY8mePef5oOPMZz4mA2jatyelqlQiLiqan8dMQUk1TT+/kIRICFGgabU6FJPkR2Yq3X93iITICyoWK8qKDq2oXtwZ9Dp23rzLgI07eRAdk+Y4e3sHJk9YSuVKNdHpdPywYj6//r4iwzYrVy7Frt2TKVTImoMHL9Gh/XQSEpKe2Qc3r/K0GNgXgI0z5ueLKfYZkYRICFGg6XR6UGuA5MKMkHyHSAhj0qhVjPCuzZc+9TEz0RAWF0/Rrh/Rw7sJUU8lQ6U9yjN9yjc4O7sRHRPF1OlDOXX6cIbtlivnyp69U3FwsOXo0cu0eXdKhkUXU5iYm9N1xkQ0piZc2LOfs9t2ZWuceYkkREKIAk9JfmImCZHIE7wci7C8w9vUfLIY67arNxl54Dg3Zi5Nd2yD+s0YN3oulpbWBAb6MXbCQO7du51huyVLFmPvX1MpVsyec+du8e47U4iJiX9uX1oPGYhzGQ8iH4WyYeqcVw8uD5OESAhR4KUkRBrlSULkIgmRyH0atYqh9WsxoUnyDLKwuHiG7tjPjxcvY2trm+747l0H8HGf5FlfZ88dY/K0z4mKikh3HICzc2H2/jUVd3dHLl++R6uWE4iIiMnw2BRv1K6BT88uAPw2YQYx4Rm3nV9IQiSEKPAMi7uSXMzO3knGEIncVaGoA8s7vE1tNxcAtl+7xf+27iEoKn3SYmpqxoih02n+VlsANm5ez9fLZqLTZTwo2sHBlj17p/LGG8W5ffsBLZqP59GjyAyPTWFha0OXaV8CcOy3jVz5+9irhPdakIRICFHgKcljqtEk50PyyEzkGrVKxRf1azKxSQMsTE0Ij4tn2M4DrLvwb4bH29sXYfSI2Xh5VkWn07Lkq2ls2fbzM9u3tbVkx85JvPlmSQIDQ2n+1pfcv//4hf1qP+oLCrs48+heAFvnpX9Ulx9JQiSEKPAMj8xUgKJgZmmBVSE7YiOe/1e0EK+ifFEHfkhVbXrXjTsM3Lyb+1HRGR4fFRXP/NlrcHR0JjIynElTh3De98Qz27ewMGPT5i+pXbscjx5F0qL5eO7cCX5hv95s6kOtdq3R63T8PHZKvqpG/TySEAkhCjy9SmX4/9hHj7BydMTe2UkSIpEj1CoVQ+rVYHJTbyxMk6tND991gDXn/3nme+rVacL50/44Ojpz795txk4YSGCg3zOPNzHR8MuvI2nSpDKRkbG0ajmBK1f8X9g3G4fCdJwwEoADq37k7oVLL3hH/qE2dgeeZ+bMmYSEhBAaGsrs2bNRpfql9bQ6depw9OhRoqKiuHr1Kn379k2z39fXF0VR0rwqVqyY0yEIIV4DepVi+P+o+8k1VgrLwGqRA8oXdeBg367MbtkYC1MTdt+4Q7WvVz83GerR7RPGjJqLXq9w3vcE//us03OTIZVKxYqVQ2jbtg5xcQm0bTOVc+duZap/H0wchW0RB+5fu8Hub5ZnOb7XWZ69QzR06FC6detGhw4dMDU1Zf369YSEhDB//vx0xzo5ObFz506WLVtGr169qFGjBqtWrSIoKIgdO3agVqspV64cjRo14vr164b3PXr0KDdDEkLkUfpUf2vFBD8EZByRyF5qlYrP69dk0pOxQpHxCYzYfZBV5559B8bMzJyRw6bTrGkbAFzd7WnfcQgxMVHPPdeSJf3p2bMJSUlaPug4i8OHn51spVazbWvebOqDNimJn8ZORpf07GKN+VGeTYiGDBnChAkTOHr0KACjRo1i2rRpGSZE7du358GDB4wbNw6Amzdv0qRJE7p168aOHTvw8PDAzMyMU6dOkZDw7AJUQoiCSUFBr1KhVhRiHqYkRDLTTGSPCk/GCtVJNVbof1v2EBD57MSmSJFiTJ34FZ6eVdBqk/juhzn8vmENen3Ga42lmDKlO4M+fRe9Xk+vDxeyY8eZTPWxsIsz7Ud/AcDur38g6Hrm7ijlJ3kyIXJxcaFEiRIcPvxfpc0jR45QqlQpnJ2defAgbdnwXbt24evrm66dQoUKAeDl5YW/v78kQ0KIDCmKHkWlAUVL/MNQQO4QiVdnolYzrEEtvmxcD3OT5Blkw3cdZK3v8+/YlC9XiamTv8axqBMRkWFMmjKEW7cvA2ue+74vvmjHl+OT6wZ9Ouhbfvkl42rVT1OpVHSeOg5LWxvu+l7iwKofM/W+/CbPJkQA9+/fN2wLDk4eGe/m5pYuIfLz88PP77/nqY6OjnTp0oVJkyYB4OnpSWJiIlu3bqVmzZpcu3aNESNGcPr06Sz3LaPiWK8ipb3sbjevyO/xQf6PMb/Hl0xBr9ag0WvRPVkWoYhr8XwTc36/hnkxvjcdi7CklQ9VnJJXpt9z249he/7mfnTMc/vZqGFLPhs0ATMzc+7du8X0WcMIehDwwhh79mzM/AUfAzB50i/8+OORTH8/6nbqQNk6NUmMi2fLjPnYWFtnJdRskZPXMLNtqgDlhUflAAsLC1xdXTPc5+rqyqFDh9IMolapVOj1ery9vQ2P0Z7V7p49eyhWrBjVqlUjLi6OlStX8u6779KvXz/u3btHv3796NGjB15eXgQEBGSqv7a2tkRGyowTIfKjO7fPUuzXzZglxfG41wB+jHqEnakZH5evbuyuideMotWi//sv9EcPgF4PFpZoWrVDVan6cycGKYrCnVuPuHc3DIAiRa3xfNMZExPNi8/JfeDck69KA56oePa5UguNj2X9rUvoFIVmLqWoUsQ5U+97HdnZ2REV9ezHlEa7Q1SnTh0OHjyY4b4RI0YAYG5ubnjMZW5uDkBsbOwz27S2tmbz5s2UK1cOb29v4p7UTujXrx9WVlaGb8T//vc/GjRoQM+ePZk5c2aW+u3q6vrcb2hW2draEhgYmO3t5hX5PT7I/zHm9/gA9u75BUdV8qTbXh070WLVN4TFxVLI3h7lyfpmr7P8fg3zSny1ijuxuKUP5YsUBmDL9duM+usIIdMWPfd9VlbWDB0yldq1GgHwx4ZVrP95GfpUP3vPirFZs8r88utwzMxMWL1qP0OGdMt0fzUmJvT9fjHFK5TlxvHTTBrWIgvRZq+cvIYpbb+I0RKip+8Apebi4sLcuXNxdnY2PApzdk7OWoOCgjJ8j62tLTt37uSNN96gadOm3Lx507BPp9Ol+wZfvXr1mXeonicqKipHPnA51W5ekd/jg/wfY36OT6fTon+y4n1kSAg6rRaNiQmYmRL1MP/MRs3P1xCMF5+NmSlT32rIJ7WqoVarCI6O4fPt+9hw+foL3+vu5sG0yd9QokRpEhLimTt/HPsObHvm8aljbNDAi3XrP8fMzIRff/2bjz9enCaJepFWn/aneIWyxIRH8OPYyXniZ8OYP6N5sg5RUFAQfn5+eHt7G7Z5e3vj5+eXbvwQJD9O+/PPPyldujQ+Pj5cvnw5zf79+/czYcKENMdXrlyZq1ev5lwQQojXhl6vQ3lyh8hUrSbySRIkM83Ei7Qq64HvoN4MqlMdtVrF6nOXqPzVqkwlQ3Vr+/DNV79TokRpQkKC+OyLbs9NhlKrXr0M27ZPwNragp07z/JhzwVZSoZKVK5Is48/BOCPqXOIehSa6ffmV3lyUDXAsmXLmD17tmGMz6xZs9JMuS9atChxcXHExMTQt29fmjRpQtu2bQkPD8fJKXl2SGJiImFhYWzdupUJEyZw/vx5rl27xpAhQ7C3t2f16tXGCE0Ikcfo9TrDHSIzjZrwByEUdnHG3sWZe5cuv+DdoiBysrFifqumdKpUAYDbj8P539Y97L99L1Pv7951AH0++hy1Ws3FS2eYNOUzwsIzl5R4eZVg1+4pFCpkzaFD/9Dx/ZkkJWW8sGtGzCwt6DZjImqNhrPbdnFxz/5Mvzc/y7MJ0dy5cylWrBgbN25Eq9WyYsUKFi5caNh/+vRpVq9ezeTJk3n//ffRaDRs3749TRsHDx6kSZMmLFy4EAsLC5YuXYqTkxMnT57krbfeIjo64/VihBAFS+o7RGYaDeFBD6BaZblDJNJRqaB39UrMbO5DYUsLdHo9i4+fZcqBY8RmopChpaU1o0fMpFHDlgBs3vozX30zHa02c0UQPTyKsWPneIoWtePUqeu0bTOFuLislZRpM2wwjiXdCX8QzJ8z0tf2K6jybEKk1+sZNmwYw4YNy3C/h4eH4f/ffvvtF7Y3c+bMLA+gFkIUDHq9Hv2ThMhcoyH8QQggtYhEWp6ORfi6TXO8S7oBcDbwAZ9s3YNvUEim3u/uXpqpk76iZIkyJCUlsuSrqWzb8Vumz68Qx+Yt4yhevAgXL97h7VYTiYrK2sKrFRt7U7/zewD88uU04p+xkGxBlGcTIiGEyC16vRbF8MhMw8Mndc/sneQOkQBLUxPGNqrL0Aa1MNVoiE5IZNKBo3x98hw6feYq1zSo34wxI+dgbW3Dw0fBTJwymCtXLmS6D05O9sAJSpZ05Pr1QFo0n0BYWNaSGdsiDnSaPBaAg6t/4sbJzFWxLigkIRJCFHg6nR696r+ESO4QiRRvly3Nonea4VE4eeWDbVdv8vmO/dyLyFxdOrVaTe9en9Gj2ycAXLh4islTP8/0eCEAR8dCbNk6FojBz+8hzd8aT0hIeFZDofO0L7FxKEzg1evsWPJtlt+f30lCJIQo8HQ6LYr6ySwzEzVhQbLifUHnXsiWea2a0MGrHAD3wiP5Ysc+tl7L/Bpf9vYOjB+7gOrV6gHw+4bVfPfDXHS6zA+AdnCwZc/eqVSo4AZY0LbNdPz9H2YpFoAGXTvi6V2PpPgEfhw9qcAt3JoZkhAJIQq81IOqzTUmhD/wB8CmiAMaExN02sz/AyZeb6YaNZ/Xq8lYn3pYm5mi1elZfOIs0w4eIyYx80nEmxVrMOHLhTgWdSIuLob5Cydkekp9ikKFrNm1ezJVqnjw4EEYzs4duHs3c+OVUnMqXYo2Qz8FYNvCrwi+dSfLbRQEkhAJIQq81IUZzTRqYsLCSYpPwNTCnEJOjjwOzLggrMhfmpYuweLWzSjvWASAI34BDNn+F5eCs1ac84P3P2JAvxFoNCbc9bvJpCmf4Xcva6vH29pasmPnJGrWLEtISDht28zg9OmeWWoDQGNqSvfZkzG1MOfKkeMc+emPLLdRUEhCJIQo8HR6HYrKFEgeQwQQHhyCY0l37J2dJCHK50oUsmN2Sx/er1gegAdRMYzZc4gfL2atBpWNjR0jh82goXdzAPbt38q8hROIj3/2klMZSUmG6tWrQGhoJM3fGs/duy9XOPHdoYNwrVCO6Mdh/PrltJdqo6CQhEgIUeDpdNo0g6oBwh8EP0mIZKZZfmVhYsJw71qM8K6NpakpOr2eb06dZ8qBY0TEZ622j2eFykwYtxBnZzcSExNZ9t1MNm35Kct9SkmGGjTw4vHjKFo0n8ClS3dfahV4Lx9vGvXoDMDPX04lKvRxltsoSCQhEkIUeDqtzjCo2kyT/N/wB0+m3jvn39W/C7L2nmWZ07IxpZ7MHjt0x5+hO/dl+fEYwAcde9O/7zBMTEwJDPRjyvQvuH7j3yy383Qy1Pyt8Zw/n7VHbSnsijnSZeo4AA6t/Zmrfx9/qXYKEkmIhBAFnlab9N8dIpPkX4v/Tb2XO0T5SWUnR+a93YTGHiUA8I+IZNTuQ/zx77Ust2VnV5hRw2dQv15TAA4c3MH8heOJic16scPsTIZUajXdZ03CurA9/pevsn3Rspdqp6CRhEgIUeDp9M+7QyRT7/MDR2srJjVtQJ/qldCo1cQnaVl47DSz/z6VqSU3nla9Wl3GjJpL0SLFSExM4KtlM9i67ZeX6luhQtaGMUOvmgwBNOvXizdqVSchNpb1I8bLFPtMkoRICFHgabVJaSpVQ+qESO4Qvc7MNBoG1anGWJ96FLIwB+D3f64ydu9h/MIzV1wxNY3GhN69PqNr536o1Wr87t1i6vQvuHU763eYILnO0O49U6hR441sSYY8qlWm5Sd9AdgwbR6P7gW8dFsFjSREQogCT6vVojf/b3FXQKpV5wPveZVjRvNGlHawB+Dc/QcM23mAo/cCX6q94i7ufDlmPp6eVQDYuu0Xvv52JgkJ8S/VnqNjIfb+NZXKlT0ICQmnRfPxXLx496XaArAubE+PuVNRazSc2bqTs1t3vnRbBZEkREKIAk+n1aJYJidC5k8SopRq1db2hTCztCAx7uX+0RO5r6arM3NbNqbBk0VY70dGM2HfEdZd+Aclc0uPpfN2y/f59H/jsLKyJioqgnkLvuTwkT0v3UcXFwf+2jcNT0937t8Ppflb47lyxf+l21OpVHSbMRF7p2KE3PHjz2nzXrqtgkoSIiFEgafVag2r3afcIUqIiSUuKhpLWxvsnZ0IueNnzC6KTChRyJbRLRvRuZInADGJSSw4epr5R0+/1DghSB44PezzyTRq2BJIXotsxqyRhDx8+dpUJUo48te+abzxRnHu3XtIs6bjuHXr1WpdNf34Qyp41yUxLp41w8aREJu12kdCEiIhhCBJl3q1e7Vhe/iD4CcJUTFJiPIwewtzdHu2cqJ3Z8xNNOj1Cusv/MuEfUe4H5X1GV8patX0ZuTwmRQtUoykpERWrl7Mb3+sRK/Xv3Sb5cu7sWfvFNzdHbl1K4i3mn2Jn1/Wl+NIrUyt6rQa1A+AP6fP5cGNlx+DVJBJQiSEKPCSkv67Q2T65A4RJCdELmXLUNhFahHlReYmGj6pXY0xjeqiP3EYcxMNf926y5g9h7jwIOsLoKawsLBkQL8RtG/bHYC7fjeZPnM4N29deaX+VqtWhl27J+PoWIjLl+/Rovl47t9/tWKJtkUc6DFnCmqNhlMbt3F6845Xaq8gk4RICFHg6ZISDXeILEz+S4hSZug4lixhlH6JjKlVKrpV9mRiU29K2tslbyzmzAffrGTzpawtt/G0il7VGD1yFm6upQD4c9M6vl8+76UHTqdo2LAiW7aOp1Aha06fvkHrtycRGpr1WW6pqTUaus+ejF3RIgTduMWfM2Tc0KuQhEgIUeAlaXX/jSEy+e/XYvDtuwAUK13KCL0SGWnxRilmNG9E5SflEAIioph9/CzLjpxk/8jJL92uqakpvXp+SpdO/dBoNISEBDF73hjOnX/1Cs9vv12DPzaMwdLSnIMHL9Gu7VSiouJevd3B/SlbpyYJsbGsHTaOpCwuNyLSkoRICFHgJSUlojxJiMxTJUQp44acJCEyupquzsxo3shQYTo8Lp45R07x1YlzmFpa8q1a/YIWnq3sG16MGjGLMqWTF3fdvWcjS7+ZTkxM1Cv3u2fPJixf8RmmpiZs3XqKzp1mEx+f+MrtVmrmQ9O+HwLw64QZMsYtG0hCJIQo8LRabbrCjAAhT+4QObi6YGJmhjbx1f8hE1lTrkhhpjRryHsVywGQoNXyzSlfZh8+weMnpRBMX7JtExNTenQbSPeuAzAxMSUsLJQFiydw5Ohf2dL3YcM6MHdeHwDWrTtA3z6L0Wp1r9xuMY+SdJk2HoCDa37iwu59r9ymkIRICCFISvpvLTPzVGOIokIfExsZiZWdHUVLusvsnVxU3NaGLxvX46NqlTDRqA0zx6YcOMa9iFcbewNQpnR5Ro+czRtlkqfoHzy0k0VLJxMREfbKbatUKubM6c2w4R0AmD9vIyNHrkJ52SJIqZhZWdJr4UwsbKy5deY82xd988ptimSSEAkhCjytVofOJPk+g/2T5R1ShNzxo1SVSjiVLiUJUS4obGnBCO/aDKpTDUvT5Guy7epNxu87wr8hWV+J/mkmJqZ069KfHt0GYmpqRkREGIuWTObg4eyp6mxiomH5is/48MPkBV9HDF/J/Pkbs6VtRVFoN2YozmU8iAh+yNrh49Bnwx0nkUwSIiFEgafV6kgyswCgiKUFKhWGisYht5MTomIeJY3Yw/zP0tSEwXWrM7xBbewtk6/FUb8Axv31N8decqmNp5UrW5GRw2ZQpkwFAA4f2cOixZMICw/NlvZtbS357ffRtGxZHa1WR98+i1m37kC2tA1wNjSIis180CYlsWbYWKJDX/1ulviPJERCiAJPq9Oje5IQmajVOFhaEhqbPAsoZaaZkyREOcJUo6ZP9cqM9amLi60NAJcePOTLv/5m543b2XMOwwyyj9FoTAgPf8ySr6Zw4FD2rfXl4uLAtu0TqFatDDEx8XT6YBY7d57NtvZL16rO3w/uAbBlzmL8LvyTbW2LZJIQCSEKPK1Wh0qjIsnEHFNtAk7WVoaEKGX2jky9z14qFXR505MJTRtQ5sniq7cfhzPlwFF+vnTlpdcce5qXZ1VGDJtOqZJvALD/wHaWfD01W8YKpfD0dGfHzkmULFmM4OAw3n1nCmfP3sy29h3civPB1LEowPltuzj6y4Zsa1v8RxIiIUSBp9PqUKkUkkwtMdUmUMzGissPkx+jGGoRlSqJSqXKloGxBd3bZUsz9S1vQy2hB1ExzDh8nBVnL5Kke/llMVKzsLCkb+8veK99T9RqNY8fP2ThkknZNoMsRcOGFdm0+UsKF7bh2rUAWr89iTt3grOtfTNLS3ovno2lnR3OljZsn7c029oWaUlCJIQo8HQ6PSoVJJlaQlw4xaytDPseB95Hm5iIqYU5hYs78zjw1RbhLMjql3Bl+lsNDavQR8QnMO/IKZaeOPfSi69mpHq1egz/YhouLsnn2bXnT775dhZRURHZdg6Arl19WLlqCObmphw9epl2bafx+PGr1y5Krcu0Lyle7g2iHoXSv0F1BiZm3/dJpCUJkRCiwNNq9ajUyXeIAIrZWBv2KXo9D/38cSlbhmKlS0lC9BIqOzkyuZk375QvA0BcUhLfnDzP3COnDLWEsoO1tS0D+42m9dsdAXgQHMiCRRM4feZItp0jxbhxnZg6rScAGzYco2eP+dlScDG1Zh/3okqLpmiTkvh17BQmHnsnW9sXaUlCJIQo8LS65EdmiSkJUao7RJD82MylbBmKeZTk6t+vvpRDQVG6cCEmNm1A5zc9UatVaHV6Vp2/xPSDx19pFfqMPAyJ4uslv+NQuCgAGzev54cVC4iLi8nW85iamvDtd4Po3fstAObN/ZNRo1Zn+6NUz0YNaDW4PwB/TptLwD+vtrCseDFJiIQQBZ5O++SRmVnGCVGIYaZZqVzu2evJycaKsT71+LhGZUyfVP7+7dJVJu0/ws3H4dl6LgcHR4Z9PoV/LwbhULgo9+7dZu6CL/nn3+yb4ZWiUCFr/tgwhmbNqqDT6Rj86Xd8+232zVRL4VTGgx5zJqNWqzn6ywZO/rkVW1vbbD+PSEsSIiFEgadNNagawCnVIzOQNc0yy87cjGENavFZvZpYmyUXVdx14w4T/vob3wch2X6+1q06MrD/SGxtC6FSwS+/rWDFqkUkJWX/EiseHk5s2z4RT093oqJi6dJ5TrZOq09hVciOPkvnYGFtzY2TZ9g0e2G2n0Nk7OVXw8sFM2fOJCQkhNDQUGbPno1KpXrmsYsWLUJRlDSvQYMGGfZ36dKFmzdvEhMTw59//kmRIkVyIwQhxGtAq9WhTpUQOWbwyAyQ4ozPYG6i4fP6Nbn2eT/G+NTD2syUE/73eWvVr7RdvyHbk6HixUswf84aRgybjq1tIW7cvEyN2iX58edlOZIM1a/vyYmT8/H0dCcg4BGNGo7OkWRIbaKh14IZFHV345F/AGuHSSXq3JRn7xANHTqUbt260aFDB0xNTVm/fj0hISHMnz8/w+O9vLwYPXo0q1evNmyLjExe76ZWrVqsWLGCgQMH4uvry5IlS1i9ejVt2rTJjVCEEHlc8qBqSDRNLs749COzh3730Ov1WBe2x7qwPTFh4UboZd6jVqnoUaUiE5vWx72QHQBXQkIZv+9vtlzNvjo8hvOpNXzw/kf07vUZ5uYWxMfHsXLNYvbs3Ui/gTlTtblbNx9WrEyeSXb27E3atplKUNDjHDlX+1Ff8EbtGsTHxLBy8Ehis2HNNpF5eTYhGjJkCBMmTODo0aMAjBo1imnTpj0zIfL09GTu3LkEB6ev//Dpp5/y22+/sW7dOgB69uyJn58fpUqV4u7duzkWgxDi9aDT6587qDopPoGw+w8o4lYcp9KluH3W1wi9zFvalC/D1Lca4lUseRDzvfBIphw4xvoL/6LPgVpNZcpUYOTQ6ZQr9yYAZ88dY8GiCdwP8s+x8TUTJ3Zl4qRuAGzceJyePeYTG5uQI+eq16kDDbq8j16v58dRkwi+dSdHziOeLU8mRC4uLpQoUYLDhw8bth05coRSpUrh7OzMgwcP0hxva2uLm5sb169fz7C9unXrMmvWLMPXAQEB3Lt3j7p160pCJIRIU5gRwMrMFBszU6JT1XwJuXOXIm7FKeZRskAnRPVLuDKjeSPql3AFIDQ2jtl/n2TZqfMk5MDjHTMzcz7sMYgunfqi0ZgQFRXBN9/NYtfuP7P9XCksLMxYuWoIXbo0AmDO7D8YM2ZtjhXlLFunJh3GDAVg55JvuXwo+8sEiBfLswkRwP379w3bUu78uLm5pUuIPD090ev1jBs3jrfffpvQ0FAWLFjA2rVrDe2lbiulPTc3tyz3Lbv/EklpL7/OIMjv8UH+jzG/xwdgZm6BSq2g15gSp9NjqVFT2qkYd8L/e2QR/qT+kFv5cq/d9yI7rmH5IvaMb1iHt98oBUBsUhLfnf2HJad9iUxIxMzSCrPs6GwqFb2q8+n/xuFaPHns1pGje/l+xTzCw0PTxJKdP6NOTvb8/MswatQoQ1KSlqFfrGTt2oPY2Ni8ctsZKeLuSq8F09GYmHBx9z5O/7Ypwzjy++cwJ+PLbJsqwCh16C0sLHB1dc1wn6urK4cOHUoziFqlUqHX6/H29jY8Rkvx4YcfsnLlSkaOHMnevXvx8fFh3rx5dOnShU2bNqHVannrrbc4ePCg4T2HDh1iz549TJ8+PVP9tbW1NYxJEkLkLwqRXDx/i7BQa+pe+gNNVBia3oNQu5cyHHPxcTB/3b9DSZtCvF/K03idzWVKZAS6Q3tQfE+DooBKjapaLTQ+zVHZFsqRc2q1Om7deERQYHJlaTNzDeXKO1G0WM4kJSkUIoDTQDxgCtRARdEcO1+8TsvPt/4hLDEeF0sbPvDwwkSdp+c6vdbs7OyIinp2JXGj3SGqU6dOmgQltREjRgBgbm5OQkKC4f8BYmNj0x2/du1atm7dSlhY8qC6S5cuUa5cOT755BM2bdpEfHy84f0pzM3NM2zrRVxdXZ/7Dc0qW1tbAgMDs73dvCK/xwf5P8b8Hh9A1apv8NWS5QDcehBMOWszurdry/abdw3HlKhckT7fLuTCjWv0rlzHSD19OS9zDW3NzPisdhUG1qiElWnyFPpt1+8w7cgpbjxelmN9rV2rEZ/0H02RIsnrnO3a8yer1y4mNvbZBRaz42f03Xdr8v0P/8Pa2oJr1wLp0nket29n35pkT1NrNHSfP50ytasT8SCEuX07MeA5g/Xz++cwJ+NLaftFjJYQPX0HKDUXFxfmzp2Ls7Mzfn7J9T+cnZ0BCArKuGx+SjKU4sqVKzRt2hSAwMBAw/tTODs7P7Ot54mKisqRH8acajevyO/xQf6PMT/HFxkZRcqvo/BEHViDjUaVJt67/yZXCrZ3cSZBm0RiNi45kVsycw1NNWr616zCOJ96FH0yuPyoXwBj9h7mhP/95773VdjbOzD4f1/StEny8hQBgXeZt+BLLlw8nek2XvZndOzYTkybnrwMx+7d5+jSeQ4REdlb4fppHcYOo0zt6iTExvLDoGE8uOefqffl588hGDe+PDmGKCgoCD8/P7y9vQ0JUcr/Pz1+CGDy5MnUr1+f5s2bG7ZVrVqVq1evAnDixAm8vb1Zs2YNkDwOyd3dnRMnTuRCNEKIvC6lDhFApC75v07WaYszxoRHEP04DBuHwjiWKkHglYwncbzOOlYsz9S3GlLGwR6Aqw9DGbf3MFuv3crR8zZ/qx2DPhlDIbvC6HRafvtjFavXLiUxMWdmdKWwsDBj+YrP6NbNB4ClS7YydOhydDp9jp63QZf38e7aEb1ez09jJhN0PftLFIisy5MJEcCyZcuYPXs2AQEBAMyaNSvNlPuiRYsSFxdHTEwMW7duZcyYMQwbNoyNGzfSokULPvzwQ5o0aWJo6+DBgxw/fpzTp0+zePFitm3bJjPMhBDAk9Xu1cmJUNSThOjp4owAwXfuYuNQmGIepfJVQtSghCuzWzamtlvyhJYHUTFMPnCU1ecvodPn3DDTYo4uDP18CnVqJ8/munnrCnPnj+P6jX9z7JwpXFwc2LhpHLVrlyMpScvgT7/j++935fh5y9evQ7tRnwPJM8r+2X/4+W8QuSbPJkRz586lWLFibNy4Ea1Wy4oVK1i48L8S5qdPn2b16tVMnjyZM2fO0LFjR6ZMmcLUqVO5e/cu3bp1M9wBOnHiBAMGDGDKlCk4ODiwZ88e+vXrZ6zQhBB5jFabXIcIIFqf/OzMySZ9QhRyx48yNarlmyU8yhd1YEbzRrSp8AYA0QmJLDh2moXHzhCTquRAdlOpVLR9tyv9Px6OlZU1iYkJrFn3Fb/+vhKdTptj501Ro8YbbNw0Dje3ooSGRvJBx1kcPHgpx8/rVLoUPedNQ2NiwunN29m/Yl2On1NkXp5NiPR6PcOGDWPYsGEZ7vfw8Ejz9ZYtW9iyZcsz21uzZo3hkZkQQqSWvJZZ8v9H6ZNn+TxdnBEg5HbyI/zXfQmPYtZWjG9Sn77VK2OiUaPV6Vl57iJTDx4jODrrk02yws21FMOHTqNK5VoAXPrnLHMXfIm//+0cPW+Kzp0bsnLVECwtzbl8+R7t2k7j1q2sjyfNKuvC9vT9eh6WtjbcOnue3yfPzvFziqzJswmREELkFp3uvztEsTw7IXrd1zSzMjVhsE9dhjWoja15ctWgbVdvMnbvYa4+ypnlKFKo1Ro6dezNRx8Oxtzcgri4GL5fMZ/NW37KsYKHqalUKqZO7cHYcZ0A2L79NN27zSMyMmcTQACNqSkfLZxJETdXHvkHsObzMeiScu4OnHg5khAJIQq8lNXuAWJVyb8Wiz214j1gWE6hWKmSmJibo03I2UG/2UWtUqE/f4pTfbrgYpsc15nAB4zafZC//QJy/PwepcoxcvgMKpSvBMDps0eYv3ACwcEvngqdHWxsLFm7bijt29cFkitPjx27Dr0+ZwdPp/hg4mhK16hKXGQUKwYNJyY8IlfOK7JGEiIhRIGn1epQPamHl6AxAfQUtrTAVKMmKdWMo/AHwUQ+fISdY1Hcvcpz5/xF43Q4C5qXKcWct5ug2/o7LrbW3AmLYMJff/Pbv1fJ6RszJiamdOvSnx7dBmJqapYry248zcPDiU2bv6RSpVLExyfSv99XrF9/INfO3+zjXtRq1xqdVsva4V8Scscv184tskYSIiFEgafV6g3T7rUaU5J0sZhqNBSztiIwMjrNsXd9L1G5eRNKVaucpxOiyk6OzGzhQ/MnS21gYcn4XftZePgEibrsX3PsaeXKvcnIYTMoU7o8AEeO/sXCJZN4/Phhjp87RePGlfj9j9EUKWJHUNBj3uswg5Mnr+Xa+au0bEbrIQMB2DhzAdePn8q1c4usk4RICFHgpR5DpDE1IyTmEa52thSztk6XEN3xvZicEFWtZIyuvpCrnQ2Tm3rTo0pF1GoVCVoty33/Zcivm/hm2qIcT4ZMTc346MPBdP6gLxqNhvDwxyz5agoHDu3M0fM+bdCgd1i4qB8mJhpOnbrOex2mc/9+zo6TSq1EJS+6ThsPwKG1P3P8t425dm7xciQhEkIUeIqiGGaZmZqa8DAi9klClH5g9V3f5OnZparkrYTI1tyMkd61+axeDSyfLLXx26WrjN/3N4+0ej63TB9LdqvoVY2Rw2ZQokRpAPYd2MbSr6cRERH2gndmH1NTE776agD9+rcCYN26Awzo/xXx8Ym51ofCLs70WToXUwtz/j3wN1vnf5Vr5xYvTxIiIYQAwx0iU1Mzw9TzYhnUIgq8cp2khARsHApTtIQbj+7l/KDk5zFRq/m4ZmXGN65vKCb5911/Ru85xOnA5Mr+Ob1Curm5BX0/+pz33+uFWq0mNDSEhUsmcfTYvhw979McHe1YveZLvL290Ol0jB61hvnzc/fOjLm1FX2/nodtEQcCr1xn/aiJKLk0eFu8GkmIhBACDIOqTUxMeBjzJCHK4A6RLimJgH+v4lG9CqWqVjZqQtTesyzT32pI2aIOAFx7GMrYXFhqI7UqlWsxYuh0XF2TSxHs2vMnXy+bSXR0ZK71AUAhnIOHpuPmVoTw8Gi6dZ3Hrl1nc7UPao2GD+dNx6VsGSJCHrJi8HAS4+JytQ/i5UlCJIQQYFhs2tTElOCUhCiDqfeQ/NjMo3oVSlWrxJktO3KtjynquhdnVgsf6pdwBSA4OoapB4+x8uwltLl0N8LCwooB/YbTvm13AEJCgpi/aAKnTuf+UhSdOjcAjuHmVoQrV/xp324aN27k3EK0z9Jh7DAqeNclITaOlYNHEBGcewPIxauThEgIIcAwhkhjYkJQdPJK504Z3CGC5IHVTcj9cURvONgzvXkjOniVAyAmMYmFx06z4OhponNwqY2nVa9WjxFDp+Hs7AbAtu2/8u33c4iJjX7BO7OXRqNm1qyPGDa8A6Bn585zdO0yO1eKLT7N58Ou1O/UAb1ez4+jJxJwOfdms4nsIQmREEIA6gwemWW0wCuA34V/AHApWwYLWxvio3I2EShqZcm4xvXoX7MKphoNOr2e1ef/YcqBowRFxeTouVOztrJh4IBRvNs6udpzUFAA8xaO49z5E7nWhxQODrb8/MsImjev9mTLG3Tt0t0oydCbTX14d9inAGyZu4R/D/yd630Qr04SIiGE4L87RCYmJoZB1Rkt8AoQ/TiMh37+OJZ0p2TlN7l2NGcSAgsTEwbXrc7IhnUoZGEOwI7rtxm39zD/hjzKkXM+S93aPgz9fAqOjs4A/LlxLT+sXEh8fO4nIJUqlWLjpnGULu1MTEw8//vkO9au3ZsrS4A8zb2iJ91nTUKtVnP0lw38vf7XXO+DyB6SEAkhBKkSIs2L7xBB8jgix5LulKpaKUcSoi6VKjDtrUaUsLcD4Pz9YEbtOcjBO/7Zfq7nsbUtxKefjKVF8/YABATeZe78cVy8dCZX+5GiY8cGrFr9OdbWFty6FUSH9tPx88u9+kKpFS7uTJ+v5mJmacGVv4+xadZCo/RDZA9JiIQQgqfuEMUkP4ZytLJCpSLDJS7uXrhErXat8ahaOVv7UcfNhXmtmlDHvTgA/hGRTPjrCD9dupzjS208raF3cz4fPBEHB0d0Oh2/b1jN6rVLSEiIz92OAGq1munTezJqdEcA9uw5T9cucwgLi87xsgIZsbSz5eNvFmBXtAj3r91g3fDx6HOhArjIOZIQCSEE/02712hMeRiTPFXaRKOmiKUlj2LTT52++2TZjhKVvVBrNK/8j6F7IVtmNG9E50qeAEQnJDLnyEkWHz9LXJL2ldrOqsL2Rfhs8AQaN0oubnjX7yZz5o3hylXjLFXi4GDLjz8Np2XL6gDMm/snY8asQaczTn2flNXrnct4EB4cwvJBw0iIzf1HhyJ7SUIkhBD8N+1eo9Gg1esJjY2jiJUlxaytMkyIgm/dIS4qGktbG1zKliHw6vWXOq+lqQnDG9RmuHctLE1N0esVVp+/xKT9R3kQnXsDplM0f6sdgz4ZQyG7wuh0Wn7+9QfWrv+apKTcm8WWWpUqHvy5cRweHk7ExMTzcd8l/PqrcQctd5o8hjdq1yA+Oobl/xsq0+vzCUmIhBCCtI/MAEKiY5MTIhsrLj8MTXe8oij4XfiHCt51KVW10kslRJ3eLM/MFj64F0oeJ3Tojj/Dd+3nwoPc/we2mKMLXwyZRN06jQG4fuNf5swfy61bV3O9Lym6dvXhh+WDsbIy59atIN7rMINLl+4arT8ArT7tT802b6PTalkzdCxB13OvCKbIWZIQCSEEqeoQaZ4kRDGxeFIkw2rVKe5euGRIiI7+siHT56pe3In5rZrQoGRyHZ+7YRGM3nOIPy+/3F2mV6FSqWjXpiv9+g7HysqaxMQE1qz7il9/X4lOl7uP6lKYmGiYN68Pnw1pC8DOnWfp0X0eYWG5W+foabU7tKH5gN4A/DFljqxen89IQiSEEID6SUakVmsACHkysPpZ1arhv4VeS2ayQKOLrTVTmjWkV7U3AYhNTGLOkZMsOHqGeG3uJx8lSpRmxNDpvFkxeWzOpX/OMm/hl9y7dzvX+5LC2bkwv/42ioYNKwIwY/pvTJjwI3ojrwdWoWE9Ok4YCcDe71dxauNWo/ZHZD9JiIQQgtRrmSWvFB/yZOr9s6pVA9y7+C96nY4ibsWxcyxK5MOMawNZmJjwef0ajPSug425GQA/XviXL//6m8DI3L/rYWJiStfO/ejR7RPMzMyIjY3hhxXz2Lz1Z6PU8klRv74nv/8xGhcXByIiYuj14UK2bDlptP6kcK/oyYfzpqMxMeH05h3sWvq9sbskcoAkREIIAZiZJScCtjYOqFQqQqJfXIsoITaWoOu3cPUsR6mqlbi490Ca/WqVih5VKjKpaQPcCiVPDT/hf59hO/cbVqLPbRW9qjH8i6mUKlU2uT8nD7Jw8SRCHgYZpT8pBg9uw7z5fTA1NeGff/x4/70ZRlmP7GlF3Fzp+/U8zK0suXb0BL9NmmHsLokcIgmREEIAFpZ6VCoFU1NzHIs6/3eH6DmPzCB5XTNXz3K8UbtGmoSoVVkPZjRvxJtOjgD4hUcy4a+/+fnSlZwL4jm0Wh0D+4+iVYv3UavVhIWF8vWyGew7sM0o/UlhbW3BD8sH06VLIwB++eUw/T5eSkxM7tc6epqNQ2H6f7cI2yIOBFy+xpqh49BrpdZQfiUJkRBCkFz4z8IyibhYM9zdPQiJDgaef4cI4OqRE3h37Yhnw/oA1HMvzpRmDfHxcAfgcWwcs/4+ybJT50kw0j+m9eo04dTxu7Ru9QEAO3dt4Nvv5xAZFW6U/qSoUMGNPzaMwcurBElJWkYMX8mSJXljbI6ZpSV9ls6laAk3QgPus/x/Q6XWUD4nCZEQQgCgwsoq0ZAQ3T97B+C5s8wAbp46Q1J8AhWsLNjdvwdNXJPX+opP0vL1yXPM/vsk4fEJOd77jDg5uTLk0/HUq9uExAQd9+/fY97C8Zz3zf3FWJ/WqZM3y1d8ho2NJYGBoXT6YBbHjxtvin9qGhMTei2YQcnKFYkJC+eHT74gKtQ4y4OI3CMJkRBCAKDC0joRHoG7mwcXDiffDXhRQlTezpYmf+2lWmICuDqTpNOx+vw/zDx0goDIqNzoeDoajQkfdOxNrx6DsLCwJCkpiTfKOdGxSwNCQ3N3UdinmZmZMG9eHz4d3AaAffsu0K3rXB4+jDBqv1KoVCq6TB9PBe+6JMTGsXzQMB7evWfsbolcIAmREEIAKXeIANzdPQwr3luZmWJjZkp0YtpKzRWLFeXLxvV4v2J5SExAD5y1saXntAXcDjPeP+6V3qzB559NpLRHeQB8L5zk++VzuXzlIomJxrlTlaJkyWL8+tsoatcuB8DMGb8xfrzxp9Sn1m7U51Rv3QJdkpY1X4zh3qXLxu6SyCWSEAkhBAAqrKyfJERuHsQmJRGTmIS1mSlONtZEPw4HwMuxCGN96tGxYnnU6uTaRZtu3iV6YD9CbWwInrrAKL0vbF+Egf1HGlalDw9/zLffz2b33k1GWfz0ae++W4s1a4dSuLANoaGRfNhzATt3njV2t9Jo1q8XDbt3AuDncVO4dsz4U/5F7pGESAghAFBhaZV8F8jZyRVzcwuCo2Mo7WBPuSIONC1dkp5VK1L3ySr0AH/+e51pB4/xT8gjhrZpg6tnOSp41+Pstl251mu1Wk3bNl3p+9Hn2NjYodfr2b7jN5avXGj0QdOQXHV6+vSejBj5PgAnTlylc6c5+PvnrfW/6n7QntafDQRg48wFnN+518g9ErlNEiIhhABAhampjsTEWMzMrHBzLcnDmFhKO9izucd7hqO0Oj1br91k+sHjXEy1qOflw0dx9SyHl0+DXEuIKleqyeBBX/JGGU8Arl3/h0VLJnH12qVcOf+LlCjhyM+/jKRevQoALFq4mVGjVpOUZJwlQZ6lWusWvP/lCCC5CvWRn343co+EMUhCJIQQAKhRqSAm9iFmZiVxdyvNnbAI6jy5I/RP8EPW+v7LzxcvG8YXpXb50BGaD+hN+QZ1UZtocrRejaOjMwP7jaRpk3cAiIwMZ/mqhWzf8VueGY/Trl1dVq4aQuHCNoSFRdO3z2I2bTL+7LanVWzSkK7Tx6NWqzn6ywapQl2ASUIkhBAAJI8Hio0NpbB9SdzdPRi/fT1n7z/g0F1/fINCnvtu/3+uEBX6GNsiDpSuXpWbp7J/fIyZmTmdOvahe9cBWFhYotPp2Lb9V1auWUJkZFi2n+9lmJmZMHv2Rwz5vB0AJ09eo0vnOfj5Pf/7Zwxl69biw3nT0JiYcGbLTjbOmG/sLgkjUhu7Ay8yc+ZMQkJCCA0NZfbs2ahSlqR+yqpVq1AUJd1r3759hmPCwsLS7be2fn4VWiFEQZH8uyUuPhRIHljtFx7J4uNnX5gMASiKwpW/jwHg5dMge3umUvFW0zasXbWLvr0/x8LCkgsXTzNg0HssWjo5zyRDZcsW59jxeYZkaP68jTRqODpPJkMlq7xJ78WzMTEz4+JfB/l1wnSjruMmjC9P3yEaOnQo3bp1o0OHDpiamrJ+/XpCQkKYPz99Fj9kyBBGjx5t+LpUqVIcPHiQJUuWAFC8eHHs7e0pXbo0samqjcY8WdFaCFHQJSdE8QnJBfjc3T2y3MLlQ0ep3f5dvBo1YMvcJdnSqzcr1uB/A0fjWaEyAA+CA/l++TwOHNyRLe1nl169mrH0qwHY2Fjy6FEkvT9axPbtp43drQy5epbj42/mG9YnWz9yAnqdLMlR0OXphGjIkCFMmDCBo0ePAjBq1CimTZuWYUIUGRlJZGSk4es1a9bw+++/s3nzZgA8PT25f/8+d+7cyZ3OCyFeM8kJUWJi8t0Wd7esJ0TXj59Cm5SEY6kSFC3pziM//5fujbt7aT7u/TmNGrYEICYmmh9//pYNG9cavZ5Qara2lnyz7H90794YgP37L/BhzwXcv583KzsXL1+WgT8sxcrOjttnfVn9xRh0SUkvfqPI9/JsQuTi4kKJEiU4fPiwYduRI0coVaoUzs7OPHjw7JWimzZtSqNGjShXrpxhm5eXF9evX8/RPgshXmfJCVGSNgKdToe1tQ0ODo48fpz56eEJMbHcPnOecvVq4+XTgMNrf8lyL4oWKUavDwfzdsv30GhM0Ol0bN/5O6vXLCEsPDTL7eWkunXLs/7H4ZQu7YxWq2PihB+ZPXtDnhnY/TTnsmUY+MMSrArZcffCJZYPGkZinPEXkRV5Q55OiADu379v2BYcnLzYopub23MTotGjR7N69WoCAgIM2zw9PbGysuLAgQOUL1+e8+fP8/nnn3Pjxo0s9Su7C5yltJcXCqflhPweH+T/GPN7fJASW3JCZGlpSsjDIFyc3ShfriL//Ju1wdG3TpyhXL3aVG7qw/mN2zPfB5tCdGjfkzbvdMHc3AKAEycPsu6nb/D3v52qn1mX3ddQo1EzYmQHRoxoj4mJBj+/h3zc9ytOnbphlHGZmYnPsVQJPvp6HtaF7Qm8fJWfh4/HTK3B7DX5uc7vn8OcjC+zbaoAo40is7CwwNXVNcN9rq6uHDp0KM0gapVKhV6vx9vb2/AY7WkeHh7cuHGDSpUqceXKFcP2/fv34+7uzsCBA4mMjGTUqFHUrl0bLy8voqOjX9hXW1vbNI/khBD5i8JV4CbgwaXzhXgcGku5CsUo7mafpXbCE+JZecMXNSoGVKiOpYnpc49PStIRcC+MgHvh6HTJd1YK2VtS+o2iFLK3fLlgcpBCDOALpAzkdgXeRMXz4zSmxwlx/HbnMrHaJIpZWNPRwxMLTZ69HyByiJ2dHVFRz15f0Kg/EXXq1OHgwYMZ7hsxIrlIlrm5OQkJCYb/B9IMin7a+++/j6+vb5pkCKBVq1aYmpoaBlF3794df39/2rRpw88//5zpPru6uj73G5pVtra2BAYGZnu7eUV+jw/yf4z5PT5IjjEgcD8A33//HVf/daBdm27MmbOQlasXZrm9/iu/pniFsrQb+DEnftuY4TFWVta0fbcb7dp0x9raBoDbd66x/qdlnDl75OWDyUB2XcPu3X2YNbsndnZWRETEMnToSv74/Vg29vTlPC8+R4+S9FoyG5siDgRdv8nswaP432v4c5zfP4c5GV9K2y9i1ITo6TtAqbm4uDB37lycnZ3x8/MDwNnZGYCgoKBnttmqVSs2bdqUbntiYiKJiYmGrxMSErhz584z71A9S1RUVI78MOZUu3lFfo8P8n+M+T2+lEdmep2WW7evAclLeLxMzMd+30jH8SOp8k4L9q5Ym2afnV1h3u/wIe+174GNjR0At25fY826pRw5+leOTv1+2Wvo6FiI777/lPbt6wLw99//8mHPBXluOv3T8RUvX5ZeS+dg41CYwKvX+a7fZ8SEG2/h3eyQ3z+Hxowvz9YhCgoKws/PD29vb8M2b29v/Pz8njt+qFatWhk+Trt58ya9evUyfG1lZUXZsmW5evVq9nZcCPGaSk6ITEw0+Psnz0Z9mZlmAOd37iUpPgGXsmUoUckLgMKFizKg30h+Wb+PD3v8DxsbO+7evcGkqUPoN7Adfx/Zmyfr4LRpU5uLl5bSvn1dEhOTGD1qNU0aj81zydDT3Ct68smKr7BxKMy9fy6zrO/g1z4ZEjkrTz9EXbZsGbNnzzYMjp41a1aaKfdFixYlLi7O8BisZMmS2NnZcfny5XRtbd++ncmTJ3P37l0ePnzI1KlTCQgIYMeOvFXLQwhhLMkJkcZEg39AckLk7OyGqakpSVmclh0fFc2Fvfup2eZtmnXtSqHGWt5u9T5mZsmP/a/f+Jf1Py3L8TtCr8LOzooFC/rSp28LAC5evMOHPRdw8eJd43YsE0pVqcTHyxZgaWvDXd9L/PDJF8RHS8058Xx5OiGaO3cuxYoVY+PGjWi1WlasWMHChf89zz99+jSrV69m8uTJADg5OQHJFamfNnLkSJKSkvjpp58oVKgQ+/fvp3Xr1nl2eqgQIrf9d4coNDSE2NgYrKysKe5SAr97t7LcWuBRXzo27Ei1Wh+getL2v5fPs+7Hbzh56vAL3m1cb71VleUrPqNECUf0ej3z521k/Pj1JCbmrUVZM/JG7Rr0WToHcysrbp4+x8pPR5DwnHGnQqTI0wmRXq9n2LBhDBs2LMP9Hh5pb2efOnXqmWOSEhISGD58OMOHD8/2fgoh8oOUhCh5JIF/wB3Kl3sTd3ePLCVEb1asTtfO/ahfryk8KXFz7d5Vli2ezoWLp7K919nJxsaSuXN7M2Dg2wDcvHmfPr0Xc+RI+rvueZGnTwPenzwGEzMzrh8/xcrPRpIUn3eKWIq8LU8nREIIkXuSEyETEw0A/v5PEqJMjiOqU7sRXTv3p0rlWkDyH3RXA66iqubK5ftBeT4ZatasCj8sH0ypUsl32pcu2cqYMWuIjX09EopLj0P4YNqXqDUaLu49wI+jJ6FNNZFGiBeRhEgIIYDUj8wAwzii561pplar8WnYkm5dB/BGGU8AkpIS2b13E7/+toLIxGjG792ER/UqFPMoScgdvxyOIesKFbJm/vw+hrFCd+4E06f3Ig4d+sfIPcs8756d2Xv/NmqNhhN/bOaPqXNQZDiEyCJJiIQQAjAMqtY8eWT2pDp0RneINBoT3mr6Lt26DqTEk4QpLi6GLdt+4Y8Nq3kU+t8MrCt/H6diY29qt3+XbQu/zukgsqRt2zp8s+wTihcvAsBXS7cyduw6oqPjjNyzzFGpVLw77FMa9+oGwN9rfmbTvOxZVFcUPJIQCSEE8PQdonsZ3CEyNTWjVYsOdO3cHxcXNwAiI8PZsHEtGzevJyoq/bTuk39uoWJjb2q2a82Opd+i1xp/VXUnJ3sWLupHly6NALh2LYCP+y7l6NHXY6wQgIm5Od1mTKBKi6YANHIuwaTvVhm5V+J1JgmREEIATydEgYHJj7cK2RXG2dmNxo1a0fG9XhQpUgyAx2GP+P2PVWze+jNxcc+e0n3l72NEPgrFrmgRvBp588/+Qzkcx7OpVCr69m3BnLm9KVzYBq1Wx/x5G5k8+Wfi41+f8TbWhe3ps3QOpapUQpuYyOYZCxj6xyZjd0u85iQhEkII4OlZZvHxcQSH3MepWHHWrNhhqCEUHHKf335fyfadv5OQ8OKV0vVaHWc2b6dp3w9p/FE3oyVEClFs3/ElDRokj3U6e/Ym/ft9xfnzWS8pYExFS7rTb9kCirq7ERsRyarPR/Pw2k1jd0vkA3m2UrUQQuSutHeIAO49GUdkZmaO371bzJo7mh69WvDnpnWZSoZS/P3j7yTGxeNRrTKejRpkb7dfwMLCjHFffgD8TYMGnkRHxzH0i+XUrTPstUuGStesxmfrf6CouxuhAYEs7dmf22fOG7tbIp+QO0RCCAE8Pe0eYPWapYQ9fsSRY3+9UlXpyIePOPLz7zTt05PWnw3g6t/HcqVC9Tvv1GLxkv6ULu0M6Nm58xyfDPyKe/ce5vi5s1u9Th3oMHooGlMT/C7+y8rBI4h+nL4IrxAvSxIiIYQAMrpDdPmKL5ev+GZL6wdWrqfeBx0oXr4sVVu9xfmde7Ol3YyUKOHIosX9DYux+vs/wt29JV06d3vtFgbVmJjQYeww6n3QHoBz23fz26SZUnBRZDt5ZCaEEMDT0+6zW2xEJAdX/whAq0/7o06VeGUXCwszJkzowuUry2jfvi5JSVpmz/qD2rVGoMIl28+X02yKFOaTFV9R74P26PV6ts7/ih9HT5JkSOQISYiEEALI6A5Rdju87leiQh9TtIQbtTu0yda233+/PpevfMOkyd2xsjLn4MFLVK3y2WtVbTq1UlUr88Wvq/GoXoW4yChWDBpmSCiFyAmSEAkhBJAbCVFiXBx/fb8agBYD+mBibv7KbVaqVIq/9k3j9z/GUKqUE/fuPaRzp9k0bTKWK1f8X7l9Y2jcqxv/W/U19k7FCLnjx+LuH3P1yAljd0vkc5IQCSEEAKYAuLo65NhjM4Djv2/icWAQhZwc8e7a8aXbcXYuzA8/DOa872KaNq1CXFwCkyf9hGeFT/j99yPZ2OPcY2lnR58lc2gzfDAaExPO7djDoi59eHj3nrG7JgoASYiEEAIAO8LCorG3t6FmzbI5dhZdUhJ7li0HoNnHH2JtXyhL77eyMmf8+C5cv/EdfT9ugVqt5rffjuDl+T8mT/6ZuLjX7/EYQInKFRn622oqNmmINjGR36fM5sdRE0mIjTV210QBIQmREEIAKlQcOvgvAC1aVMvRc53dtpugG7ewKmRHl2njM/UeExMN/fu34tr175g8pTs2NpYcO3aF+vWG06XzbPz8Ql7cSB6kNtHQclA/Bq/9DgdXFx75B7CkRz9O/L7J2F0TBYwkREII8cT+/RcBaJ7DCZFep0ueLZWQgJdPAxr26PzMY1UqFZ07N+Tfy9/w7XeDcHUtwu3bD+j0wSy8G4zkxIlrOdrXnFTMoySfrf+BFgP7oNZoOLttFws7fUTglevG7poogKQOkRBCPHHgwCUA6tYtj52dFZGROfe4Juj6TbbOW8p744bz7tBB3DnnS8DltMlN69Y1mTqtB9WqlQEgJCScaVN/5fvvd5GYqM2xvuU0lUpFg67v8+4Xn2JqYU5sRCR/TJ3Dhd37jN01UYDJHSIhhHji3r1HXLsWgImJhiZNKuf4+Y7+soFL+w5hYmpKjzlTMbeyApIrTJ88tYBt2ydSrVoZIiJiGP/lOsqU7sdXX217rZMhpzIeDFrzLR3GDMPUwpxrR08w970ekgwJo5M7REIIkcrePecpX96N5s2rsnlzzk/1/nXCDNy8yuNY0o2pq6fS2ENlGNQdExPPN19vZ/bsDTx+/HpVmH6aiZkZb/X/iCZ9emBiakp8TAzbF37DsV//NHbXhAAkIRJCiDT27DnPp4Pb5Pg4ohTauFhiDm9k8NjOOL5ZDoDo6Di+/mo78+dv5NGjyFzpR04qW6cm740bTjGPkgD8c+AwG6fPJzz49RwILvInSYiEECKVgwf/ISlJS9myxfHwcOLOneAcOY+NjSUff9yCL4a2w93dEdCToFNxIdSc8cPXsHfdHzly3txUtKQ7bYcNpmKThgBEhDxk48wFXPrroHE7JkQGJCESQohUoqPjOHbsKj4+b9K8eTW+/35Xtrb/xhsuDBr0Dh/1fotChawBCAp6zOLFWwiyLUvVdu1oNXIYsVo4+vPrmRRZ2tnRYmAfGnR5H42pCbokLUd/3cDub5YTHxVt7O4JkSFJiIQQ4il/7T2fnBC1yJ6ESK1W07JlNT4d3Ia3365h2H71agDz521k3br9hoHSERGx+HzYlffGDsPMwpwDq16f9bssbKzx7vYBPh92xaqQHQD/HjzC1vlLpdq0yPMkIRJCiKfs2XOeqdN60qxZZTQaNTqd/qXaKVXKid6936LXR80oUcIRAL1ez/btZ/j6q23s3euLoihp3rNl7hIS4+JpPqA37w79FDNLS3Z/s/yVY8pJFrY2NOzeiUY9O2Nll5wIBd24xeY5i7lx4rSReydE5khCJIQQTzl79haPH0fh4GBLrVpls1T80NbWknbt6tLro2Y0a1bFsP3x4yhWr/qLb77Zwe3bD57bxq6vvicxLp53Pv+EFp/0pWSVN/l98izC7j//fbnN3qkY9Tq/R4PO72FpZwvAg1t32PvtSi7s2Y+if7lEUghjkIRICCGeotfr+euvC3Tq5E3z5tVemBBZWZnzzju16NylIa1b18TCwixNO6tW7mXTphMkJCRlug/7V6wlLjKKdiOHUL5+HUZs/JEdi7/l6C8bjJ5olKlVHe+uHXmzaSPUGg2QfEfor+9WcWHvAaP3T4iXIQmREEJkYO+e88kJUYtqTJ36S7r9xYs70Lp1TVq/U4vmzatibW1h2Hf1agC//HyINWv2v9IaY8d/38iNU2foNHkMZWpUo8OYoVRt9RYbZ8wn8GruLm/h4Facqi3fosa7LXF+o7Rh+42TZzjy0x/8e+Bwusd/QrxOJCESQogM7N17HkhexqNx40o4OhbCza0IJUsWo5HPm1StWjrN8bduBfHbr3/z669/c/Hi3WzrxyM/f5b1HkTdD9rz7tBBeFSrzNDf13Dn3AWO/rKBi38dRJeU+TtPWeHgVpw3mzaiWqvmlKjkZdieEBvLmS07OfrLBoJv3cmRcwuR2yQhEkKIDNy795CrVwOoUMGN/QdmpNuv1+s5efI6O7afZvv2M/j63s6xviiKwvHfNnLl8DHeHTqIys2b4FG9Ch7VqxAV+pjTm7Zx/fhp7l26TELsy6+/Zu9UjDK1qvNG7Rq8UbsGDq4uhn16nY6bp85yfudfXNy7n/jomOwITYg8QxIiIYR4hq+WbmXGzF48fhyFv/8jAgIeERgQysWLd9m162yuV5EOfxDM+pETsHMsSt3321K3Y3sKOTnStO+HNO37IXqdjqDrt7h74RLBt+8SGx5BTHgEJGkJS4jHqYwHRQBzayvMra0o7OyEU+lSOJXxoFjpkoYZYil0SVruXrjEhd37uLB3P9GhYbkarxC5SQXIQ99MsLW1JTIyEjs7O6Kism9NoZxqN6/I7/FB/o8xv8cHr2+MahMNFRs3pHLzJpSqUinNHZ2XodfpCLhyjZunznLz1DnunLtAYlxcNvU257yu1y8r8nuMORlfZtt+Le4Q7d69m59++ok1a9Y885hSpUrxww8/UK9ePfz8/Pj888/Zu3evYX+zZs1YtGgRpUuX5sSJE3z88cfcuSPPvoUQry+9Vselvw4alsKwK+ZIqSpvUqpqJeydnbC2L4SVfSFsCttTxKkYYY9CiY+OISEmlviYGKJCHxNy+y7BT14P/fzRJiQYNyghjEjJqy+VSqUsWbJEURRF6dWr13OP9fX1VdatW6dUqFBBGT16tBIdHa24u7srgOLu7q5ERUUpQ4cOVby8vJRffvlFuXDhQpb6YmtrqyiKotja2mZrjDnVbl555ff4CkKM+T2+ghCjxPf6v/J7jDkZX2bbVpNHFS9enH379tG2bVvCwsKee2yTJk0oU6YMAwYM4OrVq8yaNYvjx4/Tp08fAD7++GPOnDnDggULuHz5Mr1796ZUqVL4+PjkRihCCCGEyOPybEJUvXp1/P39qVGjBhEREc89tm7dupw7d47YVLMrjhw5Qr169Qz7Dx8+bNgXFxfHuXPnDPuFEEIIUbDl2TFE27ZtY9u2bZk61sXFhfv376fZFhwcjJubW6b2Z4WtrW2W35OZ9rK73bwiv8cH+T/G/B4f5P8YJb7XX36PMSfjy2ybRkuILCwscHV1zXBfUFBQmrs9L2JlZUXCUwMBExISMDc3z9T+rAgMDMzye4zZbl6R3+OD/B9jfo8P8n+MEt/rL7/HaMz4jJYQ1alTh4MHD2a4r3379mzevDnTbcXHx1OkSJE028zNzQ1JVXx8fLrkx9zcnPDw8Cz1GcDV1TXbp90HBgZme7t5RX6PD/J/jPk9Psj/MUp8r7/8HmNOxpfS9osYLSE6dOgQKpUqW9oKDAykYsWKabY5OzsTFBRk2O/s7Jxuv6+vb5bPFRUVlSM/jDnVbl6R3+OD/B9jfo8P8n+MEt/rL7/HaMz48uyg6qw4ceIE1atXx8Liv8UVvb29OXHihGG/t7e3YZ+lpSXVqlUz7BdCCCFEwfbaJkRFixbF2toaSL7b5O/vz6pVq/Dy8mLUqFHUrl2bFStWALBy5UoaNGjAqFGj8PLyYtWqVdy5c+eZj+yEEEIIUbC8tgnR6dOnGT58OJC8yGK7du1wcXHh7Nmz9OjRgw4dOuDv7w+An58f7733Hr179+b06dMUKVKE9u3bG7H3QgghhMhL8uy0+9Q8PDxeuO3WrVs0btz4mW3s2rWLChUqZHfXhBBCCJEPvLZ3iIQQQgghsoskREIIIYQo8CQhEkIIIUSBJwmREEIIIQq812JQdV4ia5llTX6PD/J/jPk9Psj/MUp8r7/8HmNeWMtMBSjZfvZ8qHjx4vl+DRkhhBAiv3J1dU230HtqkhBlQfHixfN1yXQhhBAiP7K1tX1uMgSSEAkhhBBCyKBqIYQQQghJiIQQQghR4ElCJIQQQogCTxIiIYQQQhR4khAJIYQQosCThEgIIYQQBZ4kREIIIYQo8CQhykW7d++mV69ezz2mVKlS7N27l+joaP7991+aN2+eZn+zZs24dOkSMTEx7Nu3Dw8Pj5zscqbNnDmTkJAQQkNDmT17NiqVKsPjVq1ahaIo6V779u0zHBMWFpZuv7W1dW6FkqHMxgewaNGidP0fNGiQYX+XLl24efMmMTEx/PnnnxQpUiQ3QniurMRXp04djh49SlRUFFevXqVv375p9vv6+qaLv2LFijkdQjrm5uYsX76csLAw7t+/z9ChQ595bNWqVTlx4gQxMTGcOnWK6tWrp9mfF69ZVuJr3bo158+fJyoqigsXLtCmTZs0+/PiZy4r8W3atCld/9955x3D/iFDhhAQEEBkZCTLly/H0tIyN0J4oczGeODAgQx/b65YsQIAe3v7dPsePnyYm6E8l5mZGZcuXcLHx+eZx+SVz6Air5x9qVQqZcmSJYqiKEqvXr2ee6yvr6+ybt06pUKFCsro0aOV6Ohoxd3dXQEUd3d3JSoqShk6dKji5eWl/PLLL8qFCxeMHt/QoUMVPz8/pUGDBkrjxo2VgIAAZdiwYRkea2dnpzg5ORlederUUeLi4pR27dopgFK8eHFFURTFw8MjzXGvS3yAsmfPHmXUqFFp+m9paakASq1atZSYmBilZ8+eSqVKlZQDBw4oW7dufW3ic3JyUh4/fqxMnz5deeONN5TOnTsrsbGxSuvWrRVAUavVSmxsrNKwYcM08Ws0mlyPa8mSJYqvr69SrVo1pX379kpERITy/vvvpzvOyspKuX//vjJ37lylQoUKyqJFi5SgoCDFysoqz16zrMRXqVIlJT4+Xhk8eLBSpkwZ5X//+5+SkJCgVK5cWYG8+ZnLSnyAcv36daVbt25p+m9mZqYAynvvvaeEhYUp77zzjlKzZk3ln3/+UZYuXWr0+LISY+HChdPE1rZtWyU+Pl6pUaOGAij169dXHj58mOYYR0dHo8cHKObm5sqGDRsURVEUHx+fDI/JQ59B43/D8vOrePHiyv79+5W7d+8qjx8/fm5C1KRJEyUqKsrwQwAoe/fuVSZOnKgAyuTJk5UDBw4Y9llaWioRERHP/CHLrZefn1+auLp3767cuXMnU+/dtWuXsnbtWsPXzZo1UwIDA41+3V4lPn9/f6V58+YZ7luzZo2yatUqw9dubm6KTqdTSpUq9VrEN2DAAOXy5ctptn377bfK+vXrFUApU6aMotVqFXNzc6NeMysrKyU2NjbNZ2PcuHFpPj8pr969eyu3bt1Ks+369euG70levGZZiW/mzJnKjh070mzbtWuXMm3aNAXy5mcuK/GZmZkpSUlJStmyZTNs69ChQ4bfoYDSoEEDJSYmxvBHyusQY+qXWq1W/vnnH2XKlCmGbX379lWOHj1q9Ov29MvT01M5f/684uvr+9yEKK98BuWRWQ6rXr06/v7+1KhRg4iIiOceW7duXc6dO0dsbKxh25EjR6hXr55h/+HDhw374uLiOHfunGG/Mbi4uFCiRIk0/Tpy5AilSpXC2dn5ue9t2rQpjRo1YuzYsYZtXl5eXL9+Pcf6m1VZjc/W1hY3N7dnxvD0NQwICODevXvUrVs3+zufCVmNb9euXfTu3Tvd9kKFCgHJ18/f35+EhISc63QmVKlSBVNTU44dO2bYduTIEerUqZPucWDdunU5cuRImm1Hjx595ufO2NcMshbfmjVrGD16dLo2Ul+zvPSZg6zFV758eRRF4fbt2+naUavV1KpVK831O3HiBGZmZlSpUiXnAsiErMSY2kcffYSDgwOzZ882bMuL1xDAx8eHAwcOvPDfqLzyGZSEKIdt27aNXr16ERoa+sJjXVxc0i0+FxwcjJubW6b2G4OLiwtAmn4FBwcDvLBfo0ePZvXq1QQEBBi2eXp6YmVlxYEDB7h//z7bt2+nbNmyOdDzzMlqfJ6enuj1esaNG4e/vz++vr58+OGHadrLS9cwq/H5+flx8uRJw9eOjo506dLFMAbM09OTxMREtm7dSlBQEAcPHqRWrVo5GUKGXFxcePToEUlJSYZtwcHBWFpapht78Lp+7jIb39WrV7l48aLhay8vL5o1a5bmmuWlzxxkLT5PT08iIiJYt24d9+/f5+TJk7Rq1QpIHltjaWmZ5vrpdDpCQ0ONev0gazGmNmrUKBYtWkRMTIxhm6enJ25ubpw8eZKAgAB+/vnnF/5Bmhu+/fZbhg4dSlxc3HOPyyufQZNsba0AsrCwwNXVNcN9QUFBae72vIiVlVW6v6wTEhIwNzfP1P6c8rwYbWxsDP1I3Sfguf3y8PCgadOmDBkyJM32ChUq4ODgwNixY4mMjGTUqFHs27cPLy8voqOjXzWUDGVnfBUqVEBRFK5evcrSpUvx8fHh+++/JzIykk2bNhnlGubE9Utpd8OGDTx48IDvvvsOSI6/cOHCLF++nAkTJtCvXz/D9Uud+Oa0Z32fIX1cefVz9zxZiS+1IkWKsGHDBo4ePcrmzZsB43zmXiQr8VWoUAErKyt2797NrFmz6NChA1u3bqVu3bqG5D6vXT94uWvYuHFj3Nzc+OGHH9Jsr1ChAg8fPuSLL75ApVIxY8YMtm3bRu3atdHr9TkTQDbKK59BSYheUZ06dTh48GCG+9q3b2/4pZMZ8fHx6f4yMDc3NyRV8fHx6X4AzM3NCQ8Pz1Kfs+p5MY4YMcLQj6c/zM9LBt9//318fX25cuVKmu2tWrXC1NTU8NdP9+7d8ff3p02bNvz888+vGkqGsjO+tWvXsnXrVsLCwgC4dOkS5cqV45NPPmHTpk3PvIZZSZyzKieun7W1NZs3b6ZcuXJ4e3sb/gLs168fVlZWREVFAfC///2PBg0a0LNnT2bOnJldIb3Qs77PkD6uF10TY1yzF8lKfCmKFSvG3r17UavVdOzYEUVRAON85l4kK/FNnTqVJUuWGH4PXrx4kRo1atC/f3/GjRuX5r2p2zLm9YOXu4YdO3Zk586dht8vKSpWrIiiKMTHxxuOCwoKok6dOhw/fjwHep+98spnUB6ZvaJDhw6hUqkyfGUlGQIIDAxMd5vT2dmZoKCgTO3PKc+L8ccffzT0I3WfgOf2q1WrVmzatCnd9sTExDS3ghMSErhz584z73Bkh+yO7+lfVleuXDH03xjXMLvjs7W1Zffu3bz55ps0bdqUmzdvGvbpdDpDMpTi6tWrOXr9MhIYGEjRokXRaDSGbc7OzsTGxqb7AyKvfu6eJyvxARQvXpzDhw9jbm5O48aNefTokWGfMT5zL5KV+BRFSbct5TMXGhpKXFxcmuun0WgoUqSIUa8fZP0awrN/b8bFxRmSIYCHDx8SGhpq1GuYFXnlMygJUR5y4sQJqlevjoWFhWGbt7c3J06cMOz39vY27LO0tKRatWqG/cYQFBSEn59fmn55e3vj5+fHgwcPnvm+WrVqcfTo0XTbb968maZWk5WVFWXLluXq1avZ2/FMymp8kydPZu/evWm2Va1a1dD/p6+hm5sb7u7uRruGWY1PpVLx559/Urp0aXx8fLh8+XKa/fv372fChAlpjq9cuXKuXz9fX1+SkpLSDLr09vbm9OnThjsjKU6cOEH9+vXTbGvQoMEzP3fGvmaQtfisrKzYtWsXer0eHx+fdP+I5LXPHGQtvlWrVhnq8aRI+cwpisLp06fTXL969eqRlJTEhQsXcjaIF8hKjJD8uLNMmTLpfm/a2try+PFjGjdubNhWvHhxihYtatRrmBV56TNo9Kl5BeV1586ddNPuixYtqlhbWydP+XsynfLnn39WvLy8lFGjRimRkZGGOkQlS5ZUYmNjlVGjRhnqEPn6+ho9rlGjRikBAQGKj4+P4uPjowQEBChffPFFhjGmxKEoSoa1ThYvXqzcvXtX8fHxUby8vJQNGzYoFy9eVNRq9WsRX82aNZXExERl2LBhSunSpZWBAwcqcXFxSt26dRVAqVu3rhIfH6/06dNHqVSpkrJ//35l8+bNr831+/jjjxWtVqu0bt06Tc2TwoULK4DyxRdfKGFhYUqbNm2UcuXKKV9//bUSFBSk2NjY5Hpcy5YtUy5duqTUrFlTadeunRIeHq506NBBgeR6ShYWFgqg2NraKsHBwcqiRYsUT09PZdGiRcr9+/cN5S/y4jXLSnzTpk1TYmJilFq1aqW5ZnZ2dgrkzc9cVuLr0KGDkpCQoPTs2VMpU6aMMn78eCUmJkYpWbKkAiidO3dWwsPDlXbt2ik1a9ZULl26pCxevNjo1y8rMQKKj4+PEhsbm2E7mzdvVs6fP6/UrFlTqVatmnL48GFl+/btRo8v9evpafd59DNo/G9UQXlllBDduXMnTY2MMmXKKAcPHlTi4uKUS5cuKc2aNUtzfKtWrZSrV68qMTExyt69e41aCyXlpVarlfnz5yuPHz9WQkJClJkzZz43xtq1ayuKohgKp6V+mZubK/PmzVMCAwOV6OhoZcuWLYqbm9trFV/btm0VX19fJTY2Vrl8+bLhF1zKq1evXoqfn58SFRWlbNiwQXFwcHht4tu5c6eSkdS1U8aMGaPcvXtXiYuLUw4ePKhUrFjRKHFZWloqq1evVqKiopSAgABlyJAhhn1PF0mtVauWcvbsWSU2NlY5ceKEUrVq1Tx9zbIS35UrVzK8Zil1XfLiZy6r169v377KtWvXlLi4OOXMmTNKw4YN07Q1atQo5cGDB0pYWJiyfPlyo9fJepkYO3XqpNy/fz/Dduzt7ZUVK1YoISEhSkREhLJ27VrF3t7e6PGlfj2dEOXFz6Dqyf8IIYQQQhRYMoZICCGEEAWeJERCCCGEKPAkIRJCCCFEgScJkRBCCCEKPEmIhBBCCFHgSUIkhBBCiAJPEiIhhBBCFHiSEAkhhBCiwJOESAghhBAFniREQgghhCjwJCESQhRIffr0IT4+njJlygBQvnx54uLiaNu2rZF7JoQwBlnLTAhRYB04cICoqCjatm3LoUOHCAgIoHv37sbulhDCCCQhEkIUWGXLluXChQv8+eefvPXWW1SsWJHQ0FBjd0sIYQTyyEwIUWDduHGDWbNm0b17d4YPHy7JkBAFmCREQogCrUqVKmi1Wpo2bWrsrgghjEgSIiFEgdW2bVtatmzJu+++S/fu3WnSpImxuySEMCJFXvKSl7wK2svGxka5d++eMnr0aAVQ5s2bp1y/fl0xNzc3et/kJS955f5LBlULIQqkJUuW0LJlS958802SkpKwsbHh2rVrrF69mnHjxhm7e0KIXCYJkRBCCCEKPBlDJIQQQogCTxIiIYQQQhR4khAJIYQQosCThEgIIYQQBZ4kREIIIYQo8CQhEkIIIUSBJwmREEIIIQo8SYiEEEIIUeBJQiSEEEKIAk8SIiGEEEIUeJIQCSGEEKLAk4RICCGEEAXe/wHrMSeClNXfqwAAAABJRU5ErkJggg==",
"text/plain": [
""
- ],
- "image/png": "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"
+ ]
},
- "metadata": {},
- "output_type": "display_data",
"jetTransient": {
"display_id": null
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
},
{
"data": {
+ "image/png": "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",
"text/plain": [
""
- ],
- "image/png": "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"
+ ]
},
- "metadata": {},
- "output_type": "display_data",
"jetTransient": {
"display_id": null
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
},
{
"data": {
+ "image/png": "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dddd57LO4XC4fHzy5EnnWDzh6XFiY2MVHh7uEiSna9OmjS6++GJJqvX1yM/Pr7V/XccZNWqUZs6cqUsvvVSFhYXavn17rXGeS8uWLev9WoaHh6t58+bOdd76WgJ2RTgBcDLGuMycSFKzZs08Ps7Ro0fVsWPHWuv79OmjoqIi7dq166yv//HHHyWdusH8TPHx8Zb57bujR4+qtLRUAwcOrHP7nj17FBsbK0lq3bq19u/f79xWc9P52fTr10+LFi3SvHnz9Nxzzyk3N1eSNGvWLP385z93e5w//vhjvV9LSSosLHT+M4Cz438jADiVlJS4/NaadOqym6c++ugjderUyeXZQpGRkXrnnXd07733Sjp1qag+33zzjfLy8vQ///M/Lus7duyoPn36aNOmTR6PyRvOHPOGDRsUExOjsLAwbdu2zblcc801mjJlii644AJt3rxZJ06cqPUMrdtvv/2cn69v376KiIjQ1KlTndEUHh6uG2+8UZIUFhZW57jOtGHDBt18880uERweHq677rpLW7du1fHjx8998gAkEU4ATrN8+XL1799fEydO1IABAzRnzhwNGjTI4+OkpaXp+++/17vvvqtRo0bppptu0jvvvKPGjRvrb3/7m6RTszXXXnutbrjhhlr36xhj9Oc//1lDhw7VG2+8oaFDh2r06NH68MMP9eOPP2rOnDleOV9PHT16VK1bt9bQoUMVFxenlStXasOGDVq2bJkeeOAB9e/fX48++qhSU1NVXV2twsJC7d27V//4xz80c+ZMPfLII7rxxhv18ssva8SIEZJOXQqrz9atWyVJf/3rXzVgwADdfvvt+vDDD9WtWzdJP91Tdua4zjRt2jRFRUVp3bp1GjlypEaMGKHVq1erc+fOeuKJJ7z9ZQJsjXAC4PT0009rwYIFevTRR/Xuu+8qPj7eOUPkiWPHjumGG25QZmam/vrXv+rtt99WRESEBgwYoH379kmSnnvuOcXFxen9999Xjx49ah0jIyNDI0eO1BVXXKGlS5dqzpw52rJli3r16lXn/UH+kJaWpn379mnZsmXO504NHz5cb731lp544gm9//77euCBBzRnzhzdddddztc99NBDmj9/vh555BEtW7ZM7dq104wZMyTVfV9TjQ0bNujBBx9U3759tWrVKs2ZM0c//PCDc7aq5nLdmeM6086dO5WcnKzDhw8r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",
"text/plain": [
""
- ],
- "image/png": "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dddd57LO4XC4fHzy5EnnWDzh6XFiY2MVHh7uEiSna9OmjS6++GJJqvX1yM/Pr7V/XccZNWqUZs6cqUsvvVSFhYXavn17rXGeS8uWLev9WoaHh6t58+bOdd76WgJ2RTgBcDLGuMycSFKzZs08Ps7Ro0fVsWPHWuv79OmjoqIi7dq166yv//HHHyWdusH8TPHx8Zb57bujR4+qtLRUAwcOrHP7nj17FBsbK0lq3bq19u/f79xWc9P52fTr10+LFi3SvHnz9Nxzzyk3N1eSNGvWLP385z93e5w//vhjvV9LSSosLHT+M4Cz438jADiVlJS4/NaadOqym6c++ugjderUyeXZQpGRkXrnnXd07733Sjp1qag+33zzjfLy8vQ///M/Lus7duyoPn36aNOmTR6PyRvOHPOGDRsUExOjsLAwbdu2zblcc801mjJlii644AJt3rxZJ06cqPUMrdtvv/2cn69v376KiIjQ1KlTndEUHh6uG2+8UZIUFhZW57jOtGHDBt18880uERweHq677rpLW7du1fHjx8998gAkEU4ATrN8+XL1799fEydO1IABAzRnzhwNGjTI4+OkpaXp+++/17vvvqtRo0bppptu0jvvvKPGjRvrb3/7m6RTszXXXnutbrjhhlr36xhj9Oc//1lDhw7VG2+8oaFDh2r06NH68MMP9eOPP2rOnDleOV9PHT16VK1bt9bQoUMVFxenlStXasOGDVq2bJkeeOAB9e/fX48++qhSU1NVXV2twsJC7d27V//4xz80c+ZMPfLII7rxxhv18ssva8SIEZJOXQqrz9atWyVJf/3rXzVgwADdfvvt+vDDD9WtWzdJP91Tdua4zjRt2jRFRUVp3bp1GjlypEaMGKHVq1erc+fOeuKJJ7z9ZQJsjXAC4PT0009rwYIFevTRR/Xuu+8qPj7eOUPkiWPHjumGG25QZmam/vrXv+rtt99WRESEBgwYoH379kmSnnvuOcXFxen9999Xjx49ah0jIyNDI0eO1BVXXKGlS5dqzpw52rJli3r16lXn/UH+kJaWpn379mnZsmXO504NHz5cb731lp544gm9//77euCBBzRnzhzdddddztc99NBDmj9/vh555BEtW7ZM7dq104wZMyTVfV9TjQ0bNujBBx9U3759tWrVKs2ZM0c//PCDc7aq5nLdmeM6086dO5WcnKzDhw8r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"
+ ]
},
- "metadata": {},
- "output_type": "display_data",
"jetTransient": {
"display_id": null
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
}
],
- "execution_count": 7
+ "source": [
+ "# Custom visualizer for results\n",
+ "class FunctionVisualizer(TextVisualizer):\n",
+ " def showResults(self):\n",
+ " import matplotlib.pyplot as plt\n",
+ "\n",
+ " plt.figure()\n",
+ " plt.title(\"Initial Condition\")\n",
+ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
+ " t = torch.zeros(100, dtype=torch.float32)\n",
+ " u_target = -torch.sin(torch.pi * x)\n",
+ " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n",
+ " plt.plot(x.tolist(), u[\"U\"], label=\"network\")\n",
+ " plt.plot(x.tolist(), u_target.tolist(), label=\"target\")\n",
+ " plt.grid(True)\n",
+ " plt.legend(loc=\"best\")\n",
+ " plt.xlabel(\"x[t=0]\")\n",
+ " plt.ylabel(\"u\")\n",
+ "\n",
+ " plt.figure()\n",
+ " plt.title(\"U value at t=[0.25,0.5,0.75,1]\")\n",
+ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
+ " t = torch.ones(100, dtype=torch.float32) * 0.25\n",
+ " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n",
+ " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.25\")\n",
+ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
+ " t = torch.ones(100, dtype=torch.float32) * 0.5\n",
+ " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n",
+ " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.5\")\n",
+ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
+ " t = torch.ones(100, dtype=torch.float32) * 0.75\n",
+ " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n",
+ " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.75\")\n",
+ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n",
+ " t = torch.ones(100, dtype=torch.float32)\n",
+ " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n",
+ " plt.plot(x.tolist(), u[\"U\"], label=\"network t=1\")\n",
+ " plt.grid(True)\n",
+ " plt.legend(loc=\"best\")\n",
+ " plt.xlabel(\"x\")\n",
+ " plt.ylabel(\"u\")\n",
+ "\n",
+ " plt.figure()\n",
+ " plt.title(\"Boudary Condition\")\n",
+ " t_1 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n",
+ " x_1 = torch.ones(100, dtype=torch.float32)\n",
+ " u_1_target = torch.zeros(100, dtype=torch.float32)\n",
+ " u_1 = self.modely({\"x\": x_1.tolist(), \"t\": t_1.tolist()})\n",
+ " plt.plot(t_1.tolist(), u_1[\"U\"], label=\"network x=1\")\n",
+ " plt.plot(t_1.tolist(), u_1_target.tolist(), label=\"target x=1\")\n",
+ " t_2 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n",
+ " x_2 = -torch.ones(100, dtype=torch.float32)\n",
+ " u_2_target = torch.zeros(100, dtype=torch.float32)\n",
+ " u_2 = self.modely({\"x\": x_2.tolist(), \"t\": t_2.tolist()})\n",
+ " plt.plot(t_2.tolist(), u_2[\"U\"], label=\"network x=-1\")\n",
+ " plt.plot(t_2.tolist(), u_2_target.tolist(), label=\"target x=-1\")\n",
+ " plt.grid(True)\n",
+ " plt.legend(loc=\"best\")\n",
+ " plt.xlabel(\"x[t]\")\n",
+ " plt.ylabel(\"u\")\n",
+ "\n",
+ " plt.figure()\n",
+ " plt.title(\"Function Integration\")\n",
+ " t_3 = torch.linspace(0, 1, steps=100, dtype=torch.float32).numpy()\n",
+ " x_3 = torch.linspace(-1, 1, steps=100, dtype=torch.float32).numpy()\n",
+ " T, X = np.meshgrid(t_3, x_3)\n",
+ " u_2 = self.modely({\"x\": X.flatten().tolist(), \"t\": T.flatten().tolist()})\n",
+ " plt.contourf(T, X, np.array(u_2[\"U\"]).reshape(100, 100))\n",
+ " plt.xlabel(\"t\")\n",
+ " plt.ylabel(\"x\")\n",
+ " plt.show()\n",
+ "\n",
+ "\n",
+ "res = FunctionVisualizer()\n",
+ "res.setModely(pinn)\n",
+ "res.showResults()"
+ ]
},
{
"cell_type": "code",
+ "execution_count": 7,
"id": "938ccccb00979139",
"metadata": {
"ExecuteTime": {
@@ -1941,9 +1959,8 @@
"start_time": "2026-02-25T17:50:52.695142Z"
}
},
- "source": [],
"outputs": [],
- "execution_count": 7
+ "source": []
}
],
"metadata": {
diff --git a/codecov.yml b/codecov.yml
index 1575fa2e..f3dc1ddf 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -3,4 +3,8 @@ coverage:
project:
default:
target: auto
- threshold: 1% # the leniency in hitting the target
\ No newline at end of file
+ threshold: 1%
+ patch:
+ default:
+ target: 90%
+ threshold: 0%
diff --git a/docs/_autodoc/dataset_creation/index.rst b/docs/_autodoc/dataset_creation/index.rst
index b3fc20fc..490fe0b6 100644
--- a/docs/_autodoc/dataset_creation/index.rst
+++ b/docs/_autodoc/dataset_creation/index.rst
@@ -13,7 +13,7 @@ Once loaded, via the :func:`loadData() `, such as interpolation, as well as utilities for extracting specific temporal intervals. These features enable controlled experimentation under different operating conditions while maintaining alignment with the model's temporal structure.
.. rubric:: Multi-File handling
-
+
The framework also supports a multi-file dataset mode, where a directory of data files is treated as a single logical dataset. Data from different files are processed independently and concatenated while preserving temporal coherence, ensuring that valid temporal windows are constructed separately for each sequence. This capability is essential for recurrent training and closed-loop prediction scenarios, where temporal consistency across multiple trajectories must be strictly maintained.
.. .. rubric:: Key Benefits
diff --git a/docs/_autodoc/dataset_creation/loader_module.rst b/docs/_autodoc/dataset_creation/loader_module.rst
index 2a65971e..87ba3a52 100644
--- a/docs/_autodoc/dataset_creation/loader_module.rst
+++ b/docs/_autodoc/dataset_creation/loader_module.rst
@@ -11,4 +11,4 @@ Data Loader module
.. autofunction:: nnodely.operators.loader.Loader.loadData
For more examples of how to use the data loader, please refer to the
-:doc:`relative tutorial <../tutorials/examples/dataset>`.
\ No newline at end of file
+:doc:`relative tutorial <../tutorials/examples/dataset>`.
diff --git a/docs/_autodoc/export/exporter_module.rst b/docs/_autodoc/export/exporter_module.rst
index 5ac63a59..ddd3775f 100644
--- a/docs/_autodoc/export/exporter_module.rst
+++ b/docs/_autodoc/export/exporter_module.rst
@@ -15,4 +15,4 @@ Export module
.. autofunction:: nnodely.operators.exporter.Exporter.onnxInference
.. autofunction:: nnodely.operators.exporter.Exporter.exportReport
-For more examples of how to use the export module, please refer to the :doc:`export tutorial <../tutorials/examples/export>`.
\ No newline at end of file
+For more examples of how to use the export module, please refer to the :doc:`export tutorial <../tutorials/examples/export>`.
diff --git a/docs/_autodoc/export/index.rst b/docs/_autodoc/export/index.rst
index ebe5da0f..6d85bd35 100644
--- a/docs/_autodoc/export/index.rst
+++ b/docs/_autodoc/export/index.rst
@@ -69,7 +69,7 @@ research workflows and real-world deployment, ensuring that models can be easily
integrated into diverse application environments.
.. rubric:: Additional tools
-
+
Training and validation reports (PDF) can be generated from results using
:func:`exportReport() `.
diff --git a/docs/_autodoc/getting_started/index.rst b/docs/_autodoc/getting_started/index.rst
index 534c95bf..121b2b06 100644
--- a/docs/_autodoc/getting_started/index.rst
+++ b/docs/_autodoc/getting_started/index.rst
@@ -174,13 +174,13 @@ Reacher Estimator
Here is simple two-joint planar manipulator. The inputs are the joint angles :math:`\theta_1` and :math:`\theta_2`, while the outputs are the end-effector coordinates :math:`(x, y)`.
The link lengths :math:`l_1` and :math:`l_2` are unknown and are estimated from data using *nnodely* as learnable parameters.
-
+
The kinematic model is given by:
-.. math::
- x = l_1 \cos(\theta_1) + l_2 \cos(\theta_1 + \theta_2), \quad
+.. math::
+ x = l_1 \cos(\theta_1) + l_2 \cos(\theta_1 + \theta_2), \quad
y = l_1 \sin(\theta_1) + l_2 \sin(\theta_1 + \theta_2).
@@ -188,7 +188,7 @@ The kinematic model is given by:
**Local Module Path Configuration and Package Import**
-First, we ensure Python can locate modules in the current working directory,
+First, we ensure Python can locate modules in the current working directory,
enabling the import of nnodely components for use in the script.
.. code-block:: python
@@ -204,8 +204,8 @@ enabling the import of nnodely components for use in the script.
-
- Input variables are created using the :class:`Input` class. The learnable parameters are given within the :class:`Parameter`. The :class:`Output` class defines the model output and takes two arguments:
+
+ Input variables are created using the :class:`Input` class. The learnable parameters are given within the :class:`Parameter`. The :class:`Output` class defines the model output and takes two arguments:
the name of the output and its structure.
@@ -219,7 +219,7 @@ enabling the import of nnodely components for use in the script.
l1 = Parameter('l1') #parameters to be estimated
l2 = Parameter('l2') #parameters to be estimated
-
+
x_out = Output('x_out', (l1 * Cos(theta1.last())) +
(l2 * Cos(theta1.last() + theta2.last())))
y_out = Output('y_out', (l1 * Sin(theta1.last())) +
@@ -227,21 +227,21 @@ enabling the import of nnodely components for use in the script.
**Model composition**
-:class:`addModel` adds the defined output to the model.
-:class:`addMinimize` defines the loss function. This function uses the following inputs: The first input is the name of the error (`x-error` and `y-error` in this case). The second and third inputs are the variables whose
-difference we want to minimize. The fourth input is the loss function to be used, in this case the mean square error (`mse`).
+:class:`addModel` adds the defined output to the model.
+:class:`addMinimize` defines the loss function. This function uses the following inputs: The first input is the name of the error (`x-error` and `y-error` in this case). The second and third inputs are the variables whose
+difference we want to minimize. The fourth input is the loss function to be used, in this case the mean square error (`mse`).
:class:`neuralizeModel` builds the discrete-time MS-NN where its input parameter is the sampling time.
.. code-block:: python
- # Model composition
+ # Model composition
model = Modely(seed=0)
model.addModel('x_out', x_out)
model.addModel('y_out', y_out)
model.addMinimize('x-error', x_tip.last(), x_out, 'mse') # Objectives
model.addMinimize('y-error', y_tip.last(), y_out, 'mse') # Objectives
- model.neuralizeModel(sample_time=0.02)
+ model.neuralizeModel(sample_time=0.02)
**Data loading**
@@ -251,15 +251,15 @@ difference we want to minimize. The fourth input is the loss function to be used
data_struct = ['step', 'T1','T2','theta1', 'theta2', 'x_tip', 'y_tip',
'thetadot1', 'thetadot2', 'thetaddot1', 'thetaddot2'] # dataset creation
-
+
data_folder = os.path.join(os.getcwd(), 'dataset', 'data')
-
+
model.loadData(name='reacher_data', source=data_folder,
- format=data_struct, delimiter=';') # Data loading
+ format=data_struct, delimiter=';') # Data loading
**Training**
-Trains the model for `200` epochs (batch size `128`, learning rate `0.01`) using a `70/20/10`
+Trains the model for `200` epochs (batch size `128`, learning rate `0.01`) using a `70/20/10`
train-validation-test split.
@@ -279,7 +279,7 @@ train-validation-test split.
Code
-
+
--------------------------------------------------------
Applications
@@ -318,4 +318,3 @@ For the tutorial please refer to the link below.
Tutorials
-
diff --git a/docs/_autodoc/glossary/index.rst b/docs/_autodoc/glossary/index.rst
index c344a89f..be52db67 100644
--- a/docs/_autodoc/glossary/index.rst
+++ b/docs/_autodoc/glossary/index.rst
@@ -15,8 +15,8 @@ An MS‑NN architecture defined through inputs, outputs, and building blocks (re
.. rubric:: Input / Output / Parameter
-- **Input**: variables entering the model.
-- **Output**: signals predicted or calculated by the model.
+- **Input**: variables entering the model.
+- **Output**: signals predicted or calculated by the model.
- **Parameter**: quantities learned during training or fixed constants.
.. rubric:: Stream
@@ -61,4 +61,4 @@ Operations to save and/or convert a trained MS‑NN into standard formats (e.g.,
.. rubric:: Dataset
-Collection of data (training / test / validation) used to train and evaluate the model.
\ No newline at end of file
+Collection of data (training / test / validation) used to train and evaluate the model.
diff --git a/docs/_autodoc/inference/index.rst b/docs/_autodoc/inference/index.rst
index a369793e..dce30664 100644
--- a/docs/_autodoc/inference/index.rst
+++ b/docs/_autodoc/inference/index.rst
@@ -6,4 +6,4 @@ The framework supports both single forward pass and recursive temporal horizon i
.. toctree::
:maxdepth: 1
- inference_module
\ No newline at end of file
+ inference_module
diff --git a/docs/_autodoc/inference/inference_module.rst b/docs/_autodoc/inference/inference_module.rst
index a11e82c4..d876a051 100644
--- a/docs/_autodoc/inference/inference_module.rst
+++ b/docs/_autodoc/inference/inference_module.rst
@@ -5,4 +5,4 @@ Inference module
.. automethod:: Composer.__call__
-For more examples of how to use the export module, please refer to the :doc:`inference tutorial <../tutorials/examples/inference>`.
\ No newline at end of file
+For more examples of how to use the export module, please refer to the :doc:`inference tutorial <../tutorials/examples/inference>`.
diff --git a/docs/_autodoc/model_composition/composer_module.rst b/docs/_autodoc/model_composition/composer_module.rst
index 50aee0ae..ee07a62a 100644
--- a/docs/_autodoc/model_composition/composer_module.rst
+++ b/docs/_autodoc/model_composition/composer_module.rst
@@ -37,4 +37,4 @@ Key Composer operators:
.. autofunction:: nnodely.operators.composer.Composer.removeConnection
.. autofunction:: nnodely.operators.composer.Composer.neuralizeModel
-For further examples please refer to the :doc:`relative tutorial <../tutorials/examples/states>`.
\ No newline at end of file
+For further examples please refer to the :doc:`relative tutorial <../tutorials/examples/states>`.
diff --git a/docs/_autodoc/model_composition/index.rst b/docs/_autodoc/model_composition/index.rst
index b2b59253..515914d9 100644
--- a/docs/_autodoc/model_composition/index.rst
+++ b/docs/_autodoc/model_composition/index.rst
@@ -72,4 +72,4 @@ distinct levels:
relation_module
composer_module
- modely_execution_model
\ No newline at end of file
+ modely_execution_model
diff --git a/docs/_autodoc/model_composition/modely_execution_model.rst b/docs/_autodoc/model_composition/modely_execution_model.rst
index 723ec159..ac147bd0 100644
--- a/docs/_autodoc/model_composition/modely_execution_model.rst
+++ b/docs/_autodoc/model_composition/modely_execution_model.rst
@@ -25,7 +25,7 @@ This dynamic composition can be performed by calling one of the following method
.. code-block:: python
msd.trainAndAnalyze(models='PID', closed_loop={'x':'x_n', 'x_m':'x_n'}, connect={'F':'F_PID'}, ...)
-
+
.. .. automethod:: nnodely.nnodely.Modely.trainAndAnalyze
.. :no-index:
diff --git a/docs/_autodoc/model_composition/relation_module.rst b/docs/_autodoc/model_composition/relation_module.rst
index cd310cff..34937ebe 100644
--- a/docs/_autodoc/model_composition/relation_module.rst
+++ b/docs/_autodoc/model_composition/relation_module.rst
@@ -23,7 +23,7 @@ Common Stream operators:
:meth:`~nnodely.basic.relation.Stream.sw` (sample window) and
:meth:`~nnodely.basic.relation.Stream.tw` (time window). These are the
primitives used by temporal building blocks (FIR, recurrent windows, etc.).
-
+
.. automodule:: nnodely.basic.relation
:undoc-members:
:no-inherited-members:
diff --git a/docs/_autodoc/model_definition/index.rst b/docs/_autodoc/model_definition/index.rst
index 17a62ea5..a8f359ff 100644
--- a/docs/_autodoc/model_definition/index.rst
+++ b/docs/_autodoc/model_definition/index.rst
@@ -12,9 +12,9 @@ In addition to these core components, **nnodely** provides a library of reusable
.. toctree::
:maxdepth: 1
-
+
msnn_ins_out_param/input_module
msnn_ins_out_param/parameter_module
msnn_ins_out_param/initializer_module
msnn_ins_out_param/output_module
- layers/index
\ No newline at end of file
+ layers/index
diff --git a/docs/_autodoc/model_definition/layers/equationlearner_module.rst b/docs/_autodoc/model_definition/layers/equationlearner_module.rst
index 69060cbc..475e8060 100644
--- a/docs/_autodoc/model_definition/layers/equationlearner_module.rst
+++ b/docs/_autodoc/model_definition/layers/equationlearner_module.rst
@@ -11,4 +11,4 @@ EquationLearner module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the equation learner module, please refer to the :doc:`EquationLearner tutorial <../../tutorials/examples/equation_learner>`.
\ No newline at end of file
+For more examples of how to use the equation learner module, please refer to the :doc:`EquationLearner tutorial <../../tutorials/examples/equation_learner>`.
diff --git a/docs/_autodoc/model_definition/layers/fir_module.rst b/docs/_autodoc/model_definition/layers/fir_module.rst
index a482ecf1..753c7aac 100644
--- a/docs/_autodoc/model_definition/layers/fir_module.rst
+++ b/docs/_autodoc/model_definition/layers/fir_module.rst
@@ -10,4 +10,4 @@ FIR module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the FIR module, please refer to the :doc:`FIR tutorial <../../tutorials/examples/fir>`.
\ No newline at end of file
+For more examples of how to use the FIR module, please refer to the :doc:`FIR tutorial <../../tutorials/examples/fir>`.
diff --git a/docs/_autodoc/model_definition/layers/fuzzify_module.rst b/docs/_autodoc/model_definition/layers/fuzzify_module.rst
index ba2078e5..61d0f69c 100644
--- a/docs/_autodoc/model_definition/layers/fuzzify_module.rst
+++ b/docs/_autodoc/model_definition/layers/fuzzify_module.rst
@@ -10,4 +10,4 @@ Fuzzify module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the fuzzify module, please refer to the :doc:`Fuzzify tutorial <../../tutorials/examples/fuzzify>`.
\ No newline at end of file
+For more examples of how to use the fuzzify module, please refer to the :doc:`Fuzzify tutorial <../../tutorials/examples/fuzzify>`.
diff --git a/docs/_autodoc/model_definition/layers/linear_module.rst b/docs/_autodoc/model_definition/layers/linear_module.rst
index dc1630c0..de0a0359 100644
--- a/docs/_autodoc/model_definition/layers/linear_module.rst
+++ b/docs/_autodoc/model_definition/layers/linear_module.rst
@@ -11,4 +11,4 @@ Linear module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the linear module, please refer to the :doc:`Linear tutorial <../../tutorials/examples/linear>`.
\ No newline at end of file
+For more examples of how to use the linear module, please refer to the :doc:`Linear tutorial <../../tutorials/examples/linear>`.
diff --git a/docs/_autodoc/model_definition/layers/localmodel_module.rst b/docs/_autodoc/model_definition/layers/localmodel_module.rst
index 8168d4f6..e687c318 100644
--- a/docs/_autodoc/model_definition/layers/localmodel_module.rst
+++ b/docs/_autodoc/model_definition/layers/localmodel_module.rst
@@ -11,4 +11,4 @@ Localmodel module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the local model module, please refer to the :doc:`LocalModel tutorial <../../tutorials/examples/localmodel>`.
\ No newline at end of file
+For more examples of how to use the local model module, please refer to the :doc:`LocalModel tutorial <../../tutorials/examples/localmodel>`.
diff --git a/docs/_autodoc/model_definition/layers/parametricfunction_module.rst b/docs/_autodoc/model_definition/layers/parametricfunction_module.rst
index c518882b..2f2084e3 100644
--- a/docs/_autodoc/model_definition/layers/parametricfunction_module.rst
+++ b/docs/_autodoc/model_definition/layers/parametricfunction_module.rst
@@ -11,4 +11,4 @@ Parametric Function module
:undoc-members:
:no-inherited-members:
-For more examples of how to use the parametric function module, please refer to the :doc:`Parametric Function tutorial <../../tutorials/examples/parametric_functions>`.
\ No newline at end of file
+For more examples of how to use the parametric function module, please refer to the :doc:`Parametric Function tutorial <../../tutorials/examples/parametric_functions>`.
diff --git a/docs/_autodoc/model_definition/layers/part_module.rst b/docs/_autodoc/model_definition/layers/part_module.rst
index d8e611e6..23cc18a8 100644
--- a/docs/_autodoc/model_definition/layers/part_module.rst
+++ b/docs/_autodoc/model_definition/layers/part_module.rst
@@ -27,4 +27,4 @@ Part module
:undoc-members:
:no-inherited-members:
-For more examples and tutorials, see :doc:`Partitioning tutorial <../../tutorials/examples/partitioning>`.
\ No newline at end of file
+For more examples and tutorials, see :doc:`Partitioning tutorial <../../tutorials/examples/partitioning>`.
diff --git a/docs/_autodoc/model_definition/layers/trigonometric_module.rst b/docs/_autodoc/model_definition/layers/trigonometric_module.rst
index c583acdf..2a2a4ca8 100644
--- a/docs/_autodoc/model_definition/layers/trigonometric_module.rst
+++ b/docs/_autodoc/model_definition/layers/trigonometric_module.rst
@@ -29,4 +29,4 @@ Trigonometric module
.. autoclass:: nnodely.layers.trigonometric.Sech
:undoc-members:
- :no-inherited-members:
\ No newline at end of file
+ :no-inherited-members:
diff --git a/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst b/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst
index ac01447b..ea2bedff 100644
--- a/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst
+++ b/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst
@@ -33,4 +33,4 @@ In addition to standard random initialization, **nnodely** provides structured i
:undoc-members:
:no-inherited-members:
-For more examples of how to use the parameter module, please refer to the :doc:`relative tutorial <../../tutorials/examples/parameter>`.
\ No newline at end of file
+For more examples of how to use the parameter module, please refer to the :doc:`relative tutorial <../../tutorials/examples/parameter>`.
diff --git a/docs/_autodoc/training/earlystopping_module.rst b/docs/_autodoc/training/earlystopping_module.rst
index 24dd1e39..47929be8 100644
--- a/docs/_autodoc/training/earlystopping_module.rst
+++ b/docs/_autodoc/training/earlystopping_module.rst
@@ -8,4 +8,4 @@ Early Stopping module
.. autofunction:: nnodely.support.earlystopping.early_stop_patience
.. autofunction:: nnodely.support.earlystopping.select_best_model
.. autofunction:: nnodely.support.earlystopping.mean_stopping
- .. autofunction:: nnodely.support.earlystopping.standard_early_stopping
\ No newline at end of file
+ .. autofunction:: nnodely.support.earlystopping.standard_early_stopping
diff --git a/docs/_autodoc/training/optimizer_module.rst b/docs/_autodoc/training/optimizer_module.rst
index cce81064..064efc5d 100644
--- a/docs/_autodoc/training/optimizer_module.rst
+++ b/docs/_autodoc/training/optimizer_module.rst
@@ -13,4 +13,4 @@ Optimizer module
.. autoclass:: nnodely.basic.optimizer.Adam
:undoc-members:
:inherited-members:
- :exclude-members: add_option_to_params, replace_key_with_params, get_torch_optimizer
\ No newline at end of file
+ :exclude-members: add_option_to_params, replace_key_with_params, get_torch_optimizer
diff --git a/docs/_autodoc/training/trainer_module.rst b/docs/_autodoc/training/trainer_module.rst
index b28779f8..ff1041a0 100644
--- a/docs/_autodoc/training/trainer_module.rst
+++ b/docs/_autodoc/training/trainer_module.rst
@@ -11,7 +11,7 @@ This modality is suitable for architectures without internal feedback, where
predictions depend only on the provided inputs.
.. rubric:: Recurrent Training
-
+
Targets architectures with closed-loop dependencies, such as feedback connections or coupled multi-model structures. In this modality, training is performed over a finite prediction horizon. The model is rolled out forward in time, and parameter updates are applied only after completing the full rollout using :func:`trainModel `.
Early stopping strategies from the :doc:`Early Stopping module ` can be applied to control convergence in long-horizon training.
@@ -25,4 +25,4 @@ Early stopping strategies from the :doc:`Early Stopping module `.
\ No newline at end of file
+For more example please refer to the :doc:`training tutorial <../tutorials/examples/training>`.
diff --git a/docs/_autodoc/tutorials/examples/data/example.csv b/docs/_autodoc/tutorials/examples/data/example.csv
index 6e0457f6..6dd01025 100644
--- a/docs/_autodoc/tutorials/examples/data/example.csv
+++ b/docs/_autodoc/tutorials/examples/data/example.csv
@@ -29,4 +29,4 @@ time,x,x_s,F
27,28,29,30
28,29,30,31
29,30,31,32
-30,31,32,33
\ No newline at end of file
+30,31,32,33
diff --git a/docs/_autodoc/tutorials/examples/dataset.ipynb b/docs/_autodoc/tutorials/examples/dataset.ipynb
index 12dbb739..675003d7 100644
--- a/docs/_autodoc/tutorials/examples/dataset.ipynb
+++ b/docs/_autodoc/tutorials/examples/dataset.ipynb
@@ -99,13 +99,13 @@
}
],
"source": [
- "in1 = Input('in1')\n",
- "target = Input('target')\n",
+ "in1 = Input(\"in1\")\n",
+ "target = Input(\"target\")\n",
"relation = Fir(in1.tw(0.05))\n",
- "output = Output('out', relation)\n",
+ "output = Output(\"out\", relation)\n",
"\n",
"model = Modely(visualizer=TextVisualizer())\n",
- "model.addMinimize('out', output, target.last())\n",
+ "model.addMinimize(\"out\", output, target.last())\n",
"model.neuralizeModel(0.01)"
]
},
@@ -145,9 +145,9 @@
}
],
"source": [
- "train_folder = 'data'\n",
- "data_struct = ['in1', '', 'target']\n",
- "model.loadData(name='dataset', source=train_folder, format=data_struct)"
+ "train_folder = \"data\"\n",
+ "data_struct = [\"in1\", \"\", \"target\"]\n",
+ "model.loadData(name=\"dataset\", source=train_folder, format=data_struct)"
]
},
{
@@ -182,7 +182,14 @@
}
],
"source": [
- "model.loadData(name='dataset_2', source=train_folder, format=data_struct, skiplines=4, delimiter='\\t', header=None)"
+ "model.loadData(\n",
+ " name=\"dataset_2\",\n",
+ " source=train_folder,\n",
+ " format=data_struct,\n",
+ " skiplines=4,\n",
+ " delimiter=\"\\t\",\n",
+ " header=None,\n",
+ ")"
]
},
{
@@ -220,12 +227,13 @@
],
"source": [
"import numpy as np\n",
+ "\n",
"data_x = np.array(range(10))\n",
"data_a = 2\n",
"data_b = -3\n",
- "dataset = {'in1': data_x, 'target': (data_a*data_x) + data_b}\n",
+ "dataset = {\"in1\": data_x, \"target\": (data_a * data_x) + data_b}\n",
"\n",
- "model.loadData(name='dataset_3', source=dataset)"
+ "model.loadData(name=\"dataset_3\", source=dataset)"
]
},
{
@@ -263,12 +271,16 @@
],
"source": [
"import pandas as pd\n",
+ "\n",
"# Create a DataFrame with random values for each input\n",
- "df = pd.DataFrame({\n",
- " 'in1': np.linspace(1,100,100, dtype=np.float32),\n",
- " 'target': np.linspace(1,100,100, dtype=np.float32)})\n",
+ "df = pd.DataFrame(\n",
+ " {\n",
+ " \"in1\": np.linspace(1, 100, 100, dtype=np.float32),\n",
+ " \"target\": np.linspace(1, 100, 100, dtype=np.float32),\n",
+ " }\n",
+ ")\n",
"\n",
- "model.loadData(name='dataset_4', source=df)"
+ "model.loadData(name=\"dataset_4\", source=df)"
]
},
{
@@ -305,12 +317,17 @@
}
],
"source": [
- "df = pd.DataFrame({\n",
- " 'time': np.array([1.0,1.5,2.0,4.0,4.5,5.0,7.0,7.5,8.0,8.5], dtype=np.float32),\n",
- " 'in1': np.linspace(1,10,10, dtype=np.float32),\n",
- " 'target': np.linspace(1,10,10, dtype=np.float32)})\n",
+ "df = pd.DataFrame(\n",
+ " {\n",
+ " \"time\": np.array(\n",
+ " [1.0, 1.5, 2.0, 4.0, 4.5, 5.0, 7.0, 7.5, 8.0, 8.5], dtype=np.float32\n",
+ " ),\n",
+ " \"in1\": np.linspace(1, 10, 10, dtype=np.float32),\n",
+ " \"target\": np.linspace(1, 10, 10, dtype=np.float32),\n",
+ " }\n",
+ ")\n",
"\n",
- "model.loadData(name='dataset_resampled', source=df, resampling=True)"
+ "model.loadData(name=\"dataset_resampled\", source=df, resampling=True)"
]
},
{
@@ -348,7 +365,7 @@
}
],
"source": [
- "sample = model.getSamples(dataset='dataset_4', window=5)\n",
+ "sample = model.getSamples(dataset=\"dataset_4\", window=5)\n",
"model(sample, sampled=True)"
]
}
diff --git a/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json b/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json
index e1c9212d..20b0e9af 100644
--- a/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json
+++ b/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json
@@ -1 +1 @@
-{"weights_Phi_layer": [[0.012685388824826905]], "weights_y_layer": [[-0.1854345338982826, 0.8310009650237228]], "weights_lateral_force_Fy_layer": [[-0.19011648493285369, -0.08908118349376112, 1.336387850702053, 1.2920572574778424, -0.09438500021422562, 6.008296562778322e-05, -0.2602301908451158, -0.14530553136816854, 1.3363877377724573, 1.4067596762669665, 0.7879800384173099, 0.3761416745650699, -6.00737156551326e-05], [-0.36005925993705845, 0.31715296267094517, -0.6690789258949266, -0.2554781443995069, 0.31625396631947944, -0.3083455766488089, 0.15216951805631235, -0.4206261340059274, -0.6689552394638391, 0.22550729485249751, 0.1626431985847517, 1.033870940161089, 0.3083458634524217], [0.18802284870005584, -0.31661460582086853, 0.8534668520952942, -0.9812678363224603, 0.24311718237352536, -0.24094412228551954, 0.3072880036082372, 0.17855881988596073, 0.8535322187062506, 0.14222280861767367, 0.3735082621873329, 0.4281402634564149, 0.24090550284449572], [0.7059571634884887, 0.831036154928734, -1.5520232570684374, -1.1949558744521236, 1.0878698744282658, -1.418785910127414, 1.1105076474360718, 0.7796338840890075, -1.5520090193775626, 1.4340218664669733, 1.4600207192669248, 2.0975927853738883, -1.4191253818247085], [1.5256341940706433, 0.9566175382540345, 1.6143430229487328, -1.5732299938434673, 1.4456796215396086, 1.2677360377185034, -1.1754116849251022, 0.9002310736225679, -1.614095846537853, -1.5902608121180453, -1.6560649232874438, 1.4212434156973413, -1.2677278329655355], [-0.2943392917601912, -0.20962344102468206, -0.39777282371248085, -0.24818156146119716, 0.25709724945468077, 0.7976733265366256, -0.47110536863291785, -0.4547936310130406, 0.3977737195448417, 0.05919297447455203, 0.5504838589795269, 0.48094966877918677, 0.7976733529905506], [0.2882358412363946, -0.1769694998492831, 0.31494048781607825, 0.4120887280056081, 0.02440918889325766, 0.11130295760561043, 0.844947413739056, -0.27388447106061653, -0.31488950511700137, 0.44670920167256334, 0.27619535288612646, 0.031508029779568555, 0.1109016477647062], [-0.17117628439315022, 0.8712691462442139, 1.154838105565089, -0.5919095645359649, -0.6519344279453698, 0.22211601204221534, 0.08262748151809965, 0.3598295238234417, -1.1550202286608042, -0.9517794739723651, 0.163217589777445, -0.12628295768104714, 0.22201749256009923], [1.4506073395342782, -0.9325832775205263, -2.1979358075291233, 2.8070465077590003, -1.1346862854456579, 1.3769406818632672, 2.475248535808836, 2.0976190504310988, 2.19821296078414, 2.0255837749393124, 3.083828011008372, 2.50001192339801, -1.3769242385855402], [1.9357170786694566, -1.0112798443932023, 2.000754627001145, 2.4634621666033816, -1.694712934160906, -1.3599774709358892, -1.4821823368748877, 0.1430931122186942, 2.000786017219198, -1.9266467245466596, -2.175009991370894, 0.7244597098063941, -1.3599925425879837], [-0.999649832233769, 1.1789553679187095, -1.0439958121675497, 0.6859639814252193, -1.207373719896859, 1.4355651469453192, -1.0252723585630958, -1.0277168693514158, -1.0439392352227312, 1.1185833489292838, 1.2486301466781338, 1.2271341647965177, -1.4355998395140415], [0.5472048599834014, -0.2964604823481814, -0.755279693792939, 0.496730028445273, 0.03189984598977768, -0.011526076792924516, 0.13793532007135406, -0.3922534852030546, 0.7552757947568585, 0.541882492971732, 1.3892197075267785, 0.029209525577498854, 0.011427767016954781], [-0.33289867240184234, -0.8250106024590829, 1.3307613450025935, -0.17274867992122075, 1.167809311448035, 0.11023086130586764, 1.387883975990627, 1.6525955037269457, 1.330712816962725, -1.1008274196962873, 0.12774339481905536, 1.6778832697823363, -0.1102327756404915], [1.3799212851947305, -0.11798177584035434, -1.2706691824287746, 1.3389620861547382, -0.07167511836865471, 0.574528497078781, 1.053126427807511, 0.525765012439538, 1.2706790577938978, 1.323968682481893, 1.395683119845944, 0.9919769987157268, -0.5745468641121597], [1.470852592611058, -0.2753007632206784, 1.3267311312902044, 1.4002748745291318, -0.32148568082965384, -0.2445208276668468, -1.6411299244249362, 1.665228483180153, 1.326825077761312, -1.3355204169065926, -1.6876677699164133, 1.709701263209955, -0.24440222805773965], [0.061201334164515474, -0.2613894929273045, 0.17194839021789546, 0.357392249323869, -0.1969047844516283, -0.40138265636368453, 0.054160782803064425, 0.04499566865007523, 0.17179140127081347, 0.28445081911658165, 0.26876231918645477, 0.2834999443210322, 0.40140992216043186], [-0.7383867680261166, 0.28140178858383064, -1.0607846672606085, 1.1293524025011914, -0.38584968559452604, 0.6814052979441493, -0.8472337078996004, -0.8521757690270018, -1.0603091109428728, 1.1219117352503318, 1.161103667708072, 1.1370906079460585, -0.6814279198878799], [-0.5754205744199418, 0.7515354938234574, -0.7427595435202464, 0.08173230816182779, -0.5818699059495528, -0.8576746961203272, 0.9107580156302137, -0.4636739933720752, -0.7435719493943217, 0.9818698110846921, -1.39877838441067, 0.10852623836507357, -0.8577092093212803], [1.1803303176988844, -0.8602509152319485, 2.08823235980041, 0.5640822448256174, -1.1113029573112245, -1.783096430959948, 0.38542138435556245, 0.8968062265303769, -2.0880454626597094, 1.5287272459021344, 0.6517334709095841, 1.3030554584249463, 1.7831668896262234], [0.9965967442529087, -0.9555740352345297, 0.9315736169296287, 0.952402105850403, -1.0038521933218492, -1.0384630844945648, -1.2568415867731368, 1.359454269250626, 0.9315953305928442, -1.0105122965663116, -1.4543433160625407, 1.5251210878543582, -1.0382923330987461], [-0.0003252008154921961, -0.0007757975430400393, 0.00027610923975581776, -0.000492314483248814, 0.00037227465197929875, 0.00012994623625124898, 0.00014116358428038455, 0.0005370449387972119, -4.011652901164747e-05, 0.0005026686325048509, 0.00042415295448027955, 0.0006194169481779365, -0.00014805157424955308], [0.25408255894477966, 0.4008486804612262, -0.9585384454176571, 0.07462983204257474, 0.20304621135962272, -0.7509778599485446, 0.1957008083070563, 0.21407418595698074, -0.9583716769231316, 1.1536953456759063, 0.5223846802781359, 0.4893487538764833, -0.7512212443147893], [0.5872378239364147, -1.028692570295371, -1.1345470507881132, 0.9921306627222031, -1.431783879088534, -0.5131283754255125, 0.031786765651124815, -0.24578096239017852, 1.1346001691321415, 1.3546533350024696, -0.04328366980497708, -0.9381339085116982, 0.5132369498728799], [-0.14295606844165948, -0.01877309912889, 0.5876429218771214, 0.48027938625599703, -0.3365035305830312, -0.9956153538141037, -0.4515785924356622, 0.5789253255140729, 0.5882234039953237, -0.05064944535197737, -0.592386760754478, 0.6410803440586128, -0.9955860588207823], [0.000787086437955529, -8.908694807142095e-06, -0.0006681074216258374, 0.0002151332500077205, -0.0005916405206218054, 0.0009121619779000141, 0.0002014354532319387, -3.0046087688955496e-05, 0.00039327018433161, 4.262729131982468e-05, 0.0016545339586900472, 0.0007591541430962376, 0.00018828612339093865]], "weights_rolling_resis_Fr_layer": [[0.009512348136131527, 0.015974305622861035, 0.012354668768985456, -0.02350446610289629, -0.021171149395172454], [0.04485560728012199, -0.005721244207683364, 0.009212919587585475, 0.056730803145390786, 0.08301125650143018], [-0.027671052741331956, 0.0005604850752721769, 7.768705209179708e-05, 0.019376156352589505, -0.0036748612480807553], [0.13136716489951636, -0.00505070341472879, 0.05868154169374028, -0.09000068012576608, -0.015475440377495217], [0.13123488929110713, 0.04738146152770957, -0.09140000602220295, -0.06928203303665631, -0.02297547485706099], [-0.1292347084279552, -0.09376875839320302, 0.09143687406062925, -0.042666819353032216, -0.08618952991465577], [0.03811870391839511, 0.04299551986113498, -0.015645139143944567, -0.005474475667232196, -0.10268401640164736], [0.010408554061713201, -0.01680342808995886, -0.026197713491986078, 0.007333479240267465, 0.024039377702831342], [0.03636583768453933, 0.019818467772646533, -0.0037000940948564843, 0.03470735249075659, 0.035761192938559644], [0.10914620691295635, 0.0681068573436399, -0.06704391198934014, -0.0058835924268194634, -0.0900478676267965], [-0.18273868602774165, -0.018173976405129886, -0.033806530873410706, -0.014822716241073402, 0.0011868321653409092], [0.08779567256832134, -0.0007543581153410836, -0.026161440641287743, -0.0008218318939369428, -0.05193780327764801], [0.0794895474049251, 0.013610288551551318, 0.00965207990980787, -0.09109264333425719, 0.07350332727372452], [0.11045172299216201, 0.10204791507802281, -0.01748638178864372, 0.0892068942348556, -0.11044337166665709], [0.2592527992617235, -0.059380721328938506, -0.36878016964982613, -0.08994677022091373, 0.536723089245246], [0.1481965010752139, 0.009199935103273497, 0.006816411716118778, 0.07245631603959318, -0.016350624468557635], [0.09861027451021276, 0.003809132398771833, 0.05702825577816732, 0.004509477844508769, -0.04145490208647068], [0.10552877411008127, 0.04774693387230193, -0.019785790434331273, -0.033777708832308954, 0.08008065558794654], [-0.009534395253850227, 0.2371943342468657, 0.05340427321482067, 0.054605243213691625, 0.22608070427496613], [0.5809629742447168, 0.03565806917804284, -0.2612010705948309, 0.28708726638020116, 0.062365224169860664], [9.396158363284712e-05, 0.00010777149932499175, 9.883206328784566e-05, 9.817530963461989e-05, 9.044142390143377e-05], [0.15581565021743365, 0.13103577835449093, -0.07876624531554803, -0.210951410006127, -0.0012785118547123297], [0.13182698748047716, 0.1762004260101664, 0.09210441306630922, 0.2060755656713025, 0.013773614352975106], [0.14984011485073506, 0.13626731231521377, -0.017230767623647506, 0.10871804112668679, 0.06346689814508452], [0.0001097756900344383, 9.740100575111105e-05, 0.00011760808818914617, 0.00010664388733340848, 8.993184159731601e-05]], "weights_FC1_layer_steer": [[0.38688127715207987, -0.3871187885731928, -0.3870036745961848, 0.38676281255272826, 0.38703086352404237, -0.3868947521837845, 0.3871124432809551, -0.3869712978151479, 0.38692919331713455, 0.38688800029402004]], "biases_FC1_layer_steer": [0.6827290688825456, -0.6837825068595632, -0.6832820008581203, 0.6822021789791652, 0.6833963624234348, -0.68279452477573, 0.6837620722967017, -0.6831272126615047, 0.682948780966369, 0.6827714454347539], "weights_FC2_layer_steer": [[2.0884597157646563], [-2.087764579846399], [-2.085872928095188], [2.089101087015271], [2.0872048698362446], [-2.087166620124699], [2.0860502186132233], [-2.0884596607292907], [2.0867457700638337], [2.085633475017681]], "biases_FC2_layer_steer": [-1.361573075727897], "lambda_forget_weight": [[0.995]], "max_Tyf_engine": 399.59648198652155, "max_Tyf_brake": 2317.418462624429, "max_Tyr_brake": 941.7535062079378, "max_vx": 22.441584159075212, "min_vx": 4.0, "max_ax": 2.870634750517449, "min_ax": -1.8177452194551227, "max_ay": 4.319774302260081, "max_steer": 7.41147685692138}
\ No newline at end of file
+{"weights_Phi_layer": [[0.012685388824826905]], "weights_y_layer": [[-0.1854345338982826, 0.8310009650237228]], "weights_lateral_force_Fy_layer": [[-0.19011648493285369, -0.08908118349376112, 1.336387850702053, 1.2920572574778424, -0.09438500021422562, 6.008296562778322e-05, -0.2602301908451158, -0.14530553136816854, 1.3363877377724573, 1.4067596762669665, 0.7879800384173099, 0.3761416745650699, -6.00737156551326e-05], [-0.36005925993705845, 0.31715296267094517, -0.6690789258949266, -0.2554781443995069, 0.31625396631947944, -0.3083455766488089, 0.15216951805631235, -0.4206261340059274, -0.6689552394638391, 0.22550729485249751, 0.1626431985847517, 1.033870940161089, 0.3083458634524217], [0.18802284870005584, -0.31661460582086853, 0.8534668520952942, -0.9812678363224603, 0.24311718237352536, -0.24094412228551954, 0.3072880036082372, 0.17855881988596073, 0.8535322187062506, 0.14222280861767367, 0.3735082621873329, 0.4281402634564149, 0.24090550284449572], [0.7059571634884887, 0.831036154928734, -1.5520232570684374, -1.1949558744521236, 1.0878698744282658, -1.418785910127414, 1.1105076474360718, 0.7796338840890075, -1.5520090193775626, 1.4340218664669733, 1.4600207192669248, 2.0975927853738883, -1.4191253818247085], [1.5256341940706433, 0.9566175382540345, 1.6143430229487328, -1.5732299938434673, 1.4456796215396086, 1.2677360377185034, -1.1754116849251022, 0.9002310736225679, -1.614095846537853, -1.5902608121180453, -1.6560649232874438, 1.4212434156973413, -1.2677278329655355], [-0.2943392917601912, -0.20962344102468206, -0.39777282371248085, -0.24818156146119716, 0.25709724945468077, 0.7976733265366256, -0.47110536863291785, -0.4547936310130406, 0.3977737195448417, 0.05919297447455203, 0.5504838589795269, 0.48094966877918677, 0.7976733529905506], [0.2882358412363946, -0.1769694998492831, 0.31494048781607825, 0.4120887280056081, 0.02440918889325766, 0.11130295760561043, 0.844947413739056, -0.27388447106061653, -0.31488950511700137, 0.44670920167256334, 0.27619535288612646, 0.031508029779568555, 0.1109016477647062], [-0.17117628439315022, 0.8712691462442139, 1.154838105565089, -0.5919095645359649, -0.6519344279453698, 0.22211601204221534, 0.08262748151809965, 0.3598295238234417, -1.1550202286608042, -0.9517794739723651, 0.163217589777445, -0.12628295768104714, 0.22201749256009923], [1.4506073395342782, -0.9325832775205263, -2.1979358075291233, 2.8070465077590003, -1.1346862854456579, 1.3769406818632672, 2.475248535808836, 2.0976190504310988, 2.19821296078414, 2.0255837749393124, 3.083828011008372, 2.50001192339801, -1.3769242385855402], [1.9357170786694566, -1.0112798443932023, 2.000754627001145, 2.4634621666033816, -1.694712934160906, -1.3599774709358892, -1.4821823368748877, 0.1430931122186942, 2.000786017219198, -1.9266467245466596, -2.175009991370894, 0.7244597098063941, -1.3599925425879837], [-0.999649832233769, 1.1789553679187095, -1.0439958121675497, 0.6859639814252193, -1.207373719896859, 1.4355651469453192, -1.0252723585630958, -1.0277168693514158, -1.0439392352227312, 1.1185833489292838, 1.2486301466781338, 1.2271341647965177, -1.4355998395140415], [0.5472048599834014, -0.2964604823481814, -0.755279693792939, 0.496730028445273, 0.03189984598977768, -0.011526076792924516, 0.13793532007135406, -0.3922534852030546, 0.7552757947568585, 0.541882492971732, 1.3892197075267785, 0.029209525577498854, 0.011427767016954781], [-0.33289867240184234, -0.8250106024590829, 1.3307613450025935, -0.17274867992122075, 1.167809311448035, 0.11023086130586764, 1.387883975990627, 1.6525955037269457, 1.330712816962725, -1.1008274196962873, 0.12774339481905536, 1.6778832697823363, -0.1102327756404915], [1.3799212851947305, -0.11798177584035434, -1.2706691824287746, 1.3389620861547382, -0.07167511836865471, 0.574528497078781, 1.053126427807511, 0.525765012439538, 1.2706790577938978, 1.323968682481893, 1.395683119845944, 0.9919769987157268, -0.5745468641121597], [1.470852592611058, -0.2753007632206784, 1.3267311312902044, 1.4002748745291318, -0.32148568082965384, -0.2445208276668468, -1.6411299244249362, 1.665228483180153, 1.326825077761312, -1.3355204169065926, -1.6876677699164133, 1.709701263209955, -0.24440222805773965], [0.061201334164515474, -0.2613894929273045, 0.17194839021789546, 0.357392249323869, -0.1969047844516283, -0.40138265636368453, 0.054160782803064425, 0.04499566865007523, 0.17179140127081347, 0.28445081911658165, 0.26876231918645477, 0.2834999443210322, 0.40140992216043186], [-0.7383867680261166, 0.28140178858383064, -1.0607846672606085, 1.1293524025011914, -0.38584968559452604, 0.6814052979441493, -0.8472337078996004, -0.8521757690270018, -1.0603091109428728, 1.1219117352503318, 1.161103667708072, 1.1370906079460585, -0.6814279198878799], [-0.5754205744199418, 0.7515354938234574, -0.7427595435202464, 0.08173230816182779, -0.5818699059495528, -0.8576746961203272, 0.9107580156302137, -0.4636739933720752, -0.7435719493943217, 0.9818698110846921, -1.39877838441067, 0.10852623836507357, -0.8577092093212803], [1.1803303176988844, -0.8602509152319485, 2.08823235980041, 0.5640822448256174, -1.1113029573112245, -1.783096430959948, 0.38542138435556245, 0.8968062265303769, -2.0880454626597094, 1.5287272459021344, 0.6517334709095841, 1.3030554584249463, 1.7831668896262234], [0.9965967442529087, -0.9555740352345297, 0.9315736169296287, 0.952402105850403, -1.0038521933218492, -1.0384630844945648, -1.2568415867731368, 1.359454269250626, 0.9315953305928442, -1.0105122965663116, -1.4543433160625407, 1.5251210878543582, -1.0382923330987461], [-0.0003252008154921961, -0.0007757975430400393, 0.00027610923975581776, -0.000492314483248814, 0.00037227465197929875, 0.00012994623625124898, 0.00014116358428038455, 0.0005370449387972119, -4.011652901164747e-05, 0.0005026686325048509, 0.00042415295448027955, 0.0006194169481779365, -0.00014805157424955308], [0.25408255894477966, 0.4008486804612262, -0.9585384454176571, 0.07462983204257474, 0.20304621135962272, -0.7509778599485446, 0.1957008083070563, 0.21407418595698074, -0.9583716769231316, 1.1536953456759063, 0.5223846802781359, 0.4893487538764833, -0.7512212443147893], [0.5872378239364147, -1.028692570295371, -1.1345470507881132, 0.9921306627222031, -1.431783879088534, -0.5131283754255125, 0.031786765651124815, -0.24578096239017852, 1.1346001691321415, 1.3546533350024696, -0.04328366980497708, -0.9381339085116982, 0.5132369498728799], [-0.14295606844165948, -0.01877309912889, 0.5876429218771214, 0.48027938625599703, -0.3365035305830312, -0.9956153538141037, -0.4515785924356622, 0.5789253255140729, 0.5882234039953237, -0.05064944535197737, -0.592386760754478, 0.6410803440586128, -0.9955860588207823], [0.000787086437955529, -8.908694807142095e-06, -0.0006681074216258374, 0.0002151332500077205, -0.0005916405206218054, 0.0009121619779000141, 0.0002014354532319387, -3.0046087688955496e-05, 0.00039327018433161, 4.262729131982468e-05, 0.0016545339586900472, 0.0007591541430962376, 0.00018828612339093865]], "weights_rolling_resis_Fr_layer": [[0.009512348136131527, 0.015974305622861035, 0.012354668768985456, -0.02350446610289629, -0.021171149395172454], [0.04485560728012199, -0.005721244207683364, 0.009212919587585475, 0.056730803145390786, 0.08301125650143018], [-0.027671052741331956, 0.0005604850752721769, 7.768705209179708e-05, 0.019376156352589505, -0.0036748612480807553], [0.13136716489951636, -0.00505070341472879, 0.05868154169374028, -0.09000068012576608, -0.015475440377495217], [0.13123488929110713, 0.04738146152770957, -0.09140000602220295, -0.06928203303665631, -0.02297547485706099], [-0.1292347084279552, -0.09376875839320302, 0.09143687406062925, -0.042666819353032216, -0.08618952991465577], [0.03811870391839511, 0.04299551986113498, -0.015645139143944567, -0.005474475667232196, -0.10268401640164736], [0.010408554061713201, -0.01680342808995886, -0.026197713491986078, 0.007333479240267465, 0.024039377702831342], [0.03636583768453933, 0.019818467772646533, -0.0037000940948564843, 0.03470735249075659, 0.035761192938559644], [0.10914620691295635, 0.0681068573436399, -0.06704391198934014, -0.0058835924268194634, -0.0900478676267965], [-0.18273868602774165, -0.018173976405129886, -0.033806530873410706, -0.014822716241073402, 0.0011868321653409092], [0.08779567256832134, -0.0007543581153410836, -0.026161440641287743, -0.0008218318939369428, -0.05193780327764801], [0.0794895474049251, 0.013610288551551318, 0.00965207990980787, -0.09109264333425719, 0.07350332727372452], [0.11045172299216201, 0.10204791507802281, -0.01748638178864372, 0.0892068942348556, -0.11044337166665709], [0.2592527992617235, -0.059380721328938506, -0.36878016964982613, -0.08994677022091373, 0.536723089245246], [0.1481965010752139, 0.009199935103273497, 0.006816411716118778, 0.07245631603959318, -0.016350624468557635], [0.09861027451021276, 0.003809132398771833, 0.05702825577816732, 0.004509477844508769, -0.04145490208647068], [0.10552877411008127, 0.04774693387230193, -0.019785790434331273, -0.033777708832308954, 0.08008065558794654], [-0.009534395253850227, 0.2371943342468657, 0.05340427321482067, 0.054605243213691625, 0.22608070427496613], [0.5809629742447168, 0.03565806917804284, -0.2612010705948309, 0.28708726638020116, 0.062365224169860664], [9.396158363284712e-05, 0.00010777149932499175, 9.883206328784566e-05, 9.817530963461989e-05, 9.044142390143377e-05], [0.15581565021743365, 0.13103577835449093, -0.07876624531554803, -0.210951410006127, -0.0012785118547123297], [0.13182698748047716, 0.1762004260101664, 0.09210441306630922, 0.2060755656713025, 0.013773614352975106], [0.14984011485073506, 0.13626731231521377, -0.017230767623647506, 0.10871804112668679, 0.06346689814508452], [0.0001097756900344383, 9.740100575111105e-05, 0.00011760808818914617, 0.00010664388733340848, 8.993184159731601e-05]], "weights_FC1_layer_steer": [[0.38688127715207987, -0.3871187885731928, -0.3870036745961848, 0.38676281255272826, 0.38703086352404237, -0.3868947521837845, 0.3871124432809551, -0.3869712978151479, 0.38692919331713455, 0.38688800029402004]], "biases_FC1_layer_steer": [0.6827290688825456, -0.6837825068595632, -0.6832820008581203, 0.6822021789791652, 0.6833963624234348, -0.68279452477573, 0.6837620722967017, -0.6831272126615047, 0.682948780966369, 0.6827714454347539], "weights_FC2_layer_steer": [[2.0884597157646563], [-2.087764579846399], [-2.085872928095188], [2.089101087015271], [2.0872048698362446], [-2.087166620124699], [2.0860502186132233], [-2.0884596607292907], [2.0867457700638337], [2.085633475017681]], "biases_FC2_layer_steer": [-1.361573075727897], "lambda_forget_weight": [[0.995]], "max_Tyf_engine": 399.59648198652155, "max_Tyf_brake": 2317.418462624429, "max_Tyr_brake": 941.7535062079378, "max_vx": 22.441584159075212, "min_vx": 4.0, "max_ax": 2.870634750517449, "min_ax": -1.8177452194551227, "max_ay": 4.319774302260081, "max_steer": 7.41147685692138}
diff --git a/docs/_autodoc/tutorials/examples/equation_learner.ipynb b/docs/_autodoc/tutorials/examples/equation_learner.ipynb
index f2e52ea4..2ce52bfa 100644
--- a/docs/_autodoc/tutorials/examples/equation_learner.ipynb
+++ b/docs/_autodoc/tutorials/examples/equation_learner.ipynb
@@ -109,9 +109,9 @@
}
],
"source": [
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"equation_learner = EquationLearner(functions=[Tan, Sin, Cos])\n",
- "Output('out',equation_learner(x.last()))"
+ "Output(\"out\", equation_learner(x.last()))"
]
},
{
@@ -171,10 +171,10 @@
}
],
"source": [
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"input_layer = Linear(output_dimension=3)\n",
"equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=input_layer)\n",
- "Output('out', equation_learner(x.last()))"
+ "Output(\"out\", equation_learner(x.last()))"
]
},
{
@@ -235,11 +235,13 @@
}
],
"source": [
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"input_layer = Linear(output_dimension=3)\n",
"output_layer = Linear(output_dimension=1)\n",
- "equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=input_layer, linear_out=output_layer)\n",
- "Output('out', equation_learner(x.last()))"
+ "equation_learner = EquationLearner(\n",
+ " functions=[Tan, Sin, Cos], linear_in=input_layer, linear_out=output_layer\n",
+ ")\n",
+ "Output(\"out\", equation_learner(x.last()))"
]
},
{
@@ -307,10 +309,10 @@
}
],
"source": [
- "x = Input('x')\n",
- "F = Input('F')\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"equation_learner = EquationLearner(functions=[Tan, Sin, Cos])\n",
- "Output('out',equation_learner(inputs=(x.last(),F.last())))"
+ "Output(\"out\", equation_learner(inputs=(x.last(), F.last())))"
]
},
{
@@ -382,12 +384,14 @@
}
],
"source": [
- "x = Input('x')\n",
- "F = Input('F')\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"\n",
"linear_layer_in_1 = Linear(output_dimension=7)\n",
- "equation_learner_1 = EquationLearner(functions=[Tan, Add, Sin, Mul, Identity], linear_in=linear_layer_in_1)\n",
- "Output('out',equation_learner_1(x.last()))"
+ "equation_learner_1 = EquationLearner(\n",
+ " functions=[Tan, Add, Sin, Mul, Identity], linear_in=linear_layer_in_1\n",
+ ")\n",
+ "Output(\"out\", equation_learner_1(x.last()))"
]
},
{
@@ -461,17 +465,20 @@
"source": [
"import torch\n",
"\n",
+ "\n",
"def func1(K1):\n",
" return torch.sin(K1)\n",
"\n",
+ "\n",
"def func2(K2):\n",
" return torch.cos(K2)\n",
"\n",
- "x = Input('x')\n",
+ "\n",
+ "x = Input(\"x\")\n",
"parfun1 = ParamFun(func1)\n",
"parfun2 = ParamFun(func2)\n",
"equation_learner = EquationLearner([parfun1, parfun2])\n",
- "Output('out',equation_learner(x.last()))"
+ "Output(\"out\", equation_learner(x.last()))"
]
},
{
@@ -557,18 +564,19 @@
}
],
"source": [
- "def myFun(K1,K2,p1,p2):\n",
- " return K1*p1+K2*p2\n",
+ "def myFun(K1, K2, p1, p2):\n",
+ " return K1 * p1 + K2 * p2\n",
+ "\n",
"\n",
- "x = Input('x')\n",
- "F = Input('F')\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"\n",
- "K1 = Parameter('k1', dimensions = 1, sw = 1, values=[[2.0]])\n",
- "K2 = Parameter('k2', dimensions = 1, sw = 1, values=[[3.0]])\n",
- "parfun = ParamFun(myFun, parameters_and_constants=[K1,K2])\n",
+ "K1 = Parameter(\"k1\", dimensions=1, sw=1, values=[[2.0]])\n",
+ "K2 = Parameter(\"k2\", dimensions=1, sw=1, values=[[3.0]])\n",
+ "parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])\n",
"\n",
"equation_learner = EquationLearner([parfun, Sin, Add])\n",
- "Output('out',equation_learner((x.last(),F.last())))"
+ "Output(\"out\", equation_learner((x.last(), F.last())))"
]
},
{
@@ -645,18 +653,19 @@
}
],
"source": [
- "def myFun(K1,p1):\n",
- " return K1*p1\n",
+ "def myFun(K1, p1):\n",
+ " return K1 * p1\n",
"\n",
- "x = Input('x')\n",
- "F = Input('F')\n",
"\n",
- "K = Parameter('k', dimensions = 1, sw = 1,values=[[2.0]])\n",
- "parfun = ParamFun(myFun, parameters_and_constants = [K])\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"\n",
- "fuzzi = Fuzzify(centers=[0,1,2,3])\n",
+ "K = Parameter(\"k\", dimensions=1, sw=1, values=[[2.0]])\n",
+ "parfun = ParamFun(myFun, parameters_and_constants=[K])\n",
+ "\n",
+ "fuzzi = Fuzzify(centers=[0, 1, 2, 3])\n",
"equation_learner = EquationLearner([parfun, fuzzi])\n",
- "Output('out',equation_learner((x.last(),F.last())))"
+ "Output(\"out\", equation_learner((x.last(), F.last())))"
]
},
{
@@ -779,23 +788,43 @@
}
],
"source": [
- "x = Input('x')\n",
- "F = Input('F')\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
+ "\n",
+ "\n",
+ "def myFun(K1, K2, p1, p2):\n",
+ " return K1 * p1 + K2 * p2\n",
"\n",
- "def myFun(K1,K2,p1,p2):\n",
- " return K1*p1+K2*p2\n",
"\n",
- "K1 = Parameter('k1', dimensions = 1, sw = 1, values=[[2.0]])\n",
- "K2 = Parameter('k2', dimensions = 1, sw = 1, values=[[3.0]])\n",
- "parfun = ParamFun(myFun, parameters_and_constants = [K1,K2])\n",
+ "K1 = Parameter(\"k1\", dimensions=1, sw=1, values=[[2.0]])\n",
+ "K2 = Parameter(\"k2\", dimensions=1, sw=1, values=[[3.0]])\n",
+ "parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])\n",
"\n",
- "input_layer_1 = Linear(output_dimension=5, W_init='init_constant', W_init_params={'value':1}, b_init='init_constant', b_init_params={'value':0})\n",
- "input_layer_2 = Linear(output_dimension=7, W_init='init_constant', W_init_params={'value':1}, b_init='init_constant', b_init_params={'value':0})\n",
- "output_layer = Linear(output_dimension=1, W_init='init_constant', W_init_params={'value':1}, b=True)\n",
+ "input_layer_1 = Linear(\n",
+ " output_dimension=5,\n",
+ " W_init=\"init_constant\",\n",
+ " W_init_params={\"value\": 1},\n",
+ " b_init=\"init_constant\",\n",
+ " b_init_params={\"value\": 0},\n",
+ ")\n",
+ "input_layer_2 = Linear(\n",
+ " output_dimension=7,\n",
+ " W_init=\"init_constant\",\n",
+ " W_init_params={\"value\": 1},\n",
+ " b_init=\"init_constant\",\n",
+ " b_init_params={\"value\": 0},\n",
+ ")\n",
+ "output_layer = Linear(\n",
+ " output_dimension=1, W_init=\"init_constant\", W_init_params={\"value\": 1}, b=True\n",
+ ")\n",
"equation_learner = EquationLearner([parfun, Sin, Add], linear_in=input_layer_1)\n",
- "equation_learner_2 = EquationLearner(functions=[Tan, Add, Sin, Mul, Identity], linear_in=input_layer_2, linear_out=output_layer)\n",
+ "equation_learner_2 = EquationLearner(\n",
+ " functions=[Tan, Add, Sin, Mul, Identity],\n",
+ " linear_in=input_layer_2,\n",
+ " linear_out=output_layer,\n",
+ ")\n",
"\n",
- "Output('out',equation_learner_2(equation_learner((x.sw(1),F.sw(1)))))"
+ "Output(\"out\", equation_learner_2(equation_learner((x.sw(1), F.sw(1)))))"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/export.ipynb b/docs/_autodoc/tutorials/examples/export.ipynb
index 6cb1b98e..d2ee24ba 100644
--- a/docs/_autodoc/tutorials/examples/export.ipynb
+++ b/docs/_autodoc/tutorials/examples/export.ipynb
@@ -86,22 +86,24 @@
}
],
"source": [
- "x = Input('x')\n",
- "y = Input('y')\n",
- "z = Input('z')\n",
+ "x = Input(\"x\")\n",
+ "y = Input(\"y\")\n",
+ "z = Input(\"z\")\n",
"\n",
- "def myFun(K1,p1,p2):\n",
- " return K1*p1*p2\n",
"\n",
- "K_x = Parameter('k_x', dimensions=1, tw=1)\n",
- "c_v = Constant('c_v', tw=1, values=[[1],[2]])\n",
- "parfun = ParamFun(myFun, parameters_and_constants = [K_x,c_v])\n",
+ "def myFun(K1, p1, p2):\n",
+ " return K1 * p1 * p2\n",
"\n",
- "out = Output('out', Fir(parfun(x.tw(1)))+Fir(parfun(y.tw(1)))+Fir(parfun(z.tw(1))))\n",
"\n",
- "result_path = './results'\n",
+ "K_x = Parameter(\"k_x\", dimensions=1, tw=1)\n",
+ "c_v = Constant(\"c_v\", tw=1, values=[[1], [2]])\n",
+ "parfun = ParamFun(myFun, parameters_and_constants=[K_x, c_v])\n",
+ "\n",
+ "out = Output(\"out\", Fir(parfun(x.tw(1))) + Fir(parfun(y.tw(1))) + Fir(parfun(z.tw(1))))\n",
+ "\n",
+ "result_path = \"./results\"\n",
"model = Modely(workspace=result_path)\n",
- "model.addModel('model', out)"
+ "model.addModel(\"model\", out)"
]
},
{
@@ -163,7 +165,7 @@
}
],
"source": [
- "model.saveModel(name='model_definition', model_folder=result_path)"
+ "model.saveModel(name=\"model_definition\", model_folder=result_path)"
]
},
{
@@ -244,7 +246,7 @@
],
"source": [
"model.neuralizeModel(0.5)\n",
- "model.saveTorchModel(name='model_parameters', model_folder=result_path)"
+ "model.saveTorchModel(name=\"model_parameters\", model_folder=result_path)"
]
},
{
@@ -275,7 +277,7 @@
}
],
"source": [
- "model.loadTorchModel(name='model_parameters', model_folder=result_path)"
+ "model.loadTorchModel(name=\"model_parameters\", model_folder=result_path)"
]
},
{
@@ -311,7 +313,7 @@
}
],
"source": [
- "model.exportPythonModel(name='pytorch_model', model_folder=result_path)"
+ "model.exportPythonModel(name=\"pytorch_model\", model_folder=result_path)"
]
},
{
@@ -394,7 +396,7 @@
}
],
"source": [
- "model.importPythonModel(name='pytorch_model', model_folder=result_path)"
+ "model.importPythonModel(name=\"pytorch_model\", model_folder=result_path)"
]
},
{
@@ -485,7 +487,7 @@
],
"source": [
"model.neuralizeModel(0.5)\n",
- "model.exportONNX(name='model_onnx', model_folder=result_path)"
+ "model.exportONNX(name=\"model_onnx\", model_folder=result_path)"
]
},
{
@@ -522,7 +524,7 @@
}
],
"source": [
- "model.exportONNX(inputs_order=['x','y','z'], outputs_order=['out'])"
+ "model.exportONNX(inputs_order=[\"x\", \"y\", \"z\"], outputs_order=[\"out\"])"
]
},
{
@@ -558,12 +560,20 @@
"import numpy as np\n",
"import os\n",
"\n",
- "val = np.random.rand(1,2,1).astype(np.float32)\n",
+ "val = np.random.rand(1, 2, 1).astype(np.float32)\n",
"\n",
- "data = {'x':val, 'y':val, 'z':val}\n",
- "output_onnx = Modely().onnxInference(data, name = 'net', model_folder = os.path.join(result_path, 'onnx'))\n",
- "output = model({'x':val.squeeze(-1).tolist()[0], 'y':val.squeeze(-1).tolist()[0], 'z':val.squeeze(-1).tolist()[0]})\n",
- "print(f'model out : {output} | onnx out : {output_onnx}')"
+ "data = {\"x\": val, \"y\": val, \"z\": val}\n",
+ "output_onnx = Modely().onnxInference(\n",
+ " data, name=\"net\", model_folder=os.path.join(result_path, \"onnx\")\n",
+ ")\n",
+ "output = model(\n",
+ " {\n",
+ " \"x\": val.squeeze(-1).tolist()[0],\n",
+ " \"y\": val.squeeze(-1).tolist()[0],\n",
+ " \"z\": val.squeeze(-1).tolist()[0],\n",
+ " }\n",
+ ")\n",
+ "print(f\"model out : {output} | onnx out : {output_onnx}\")"
]
},
{
@@ -596,7 +606,7 @@
}
],
"source": [
- "model.exportReport(name='model_report', model_folder=result_path)"
+ "model.exportReport(name=\"model_report\", model_folder=result_path)"
]
},
{
@@ -939,38 +949,50 @@
],
"source": [
"clearNames()\n",
- "vehicle = nnodely(visualizer=TextVisualizer(), seed=2, workspace='results')\n",
+ "vehicle = nnodely(visualizer=TextVisualizer(), seed=2, workspace=\"results\")\n",
"\n",
"# Dimensions of the layers\n",
- "n = 25\n",
+ "n = 25\n",
"na = 21\n",
"\n",
- "#Create neural model inputs\n",
- "velocity = Input('vel')\n",
- "brake = Input('brk')\n",
- "gear = Input('gear')\n",
- "torque = Input('trq')\n",
- "altitude = Input('alt',dimensions=na)\n",
- "acc = Input('acc')\n",
+ "# Create neural model inputs\n",
+ "velocity = Input(\"vel\")\n",
+ "brake = Input(\"brk\")\n",
+ "gear = Input(\"gear\")\n",
+ "torque = Input(\"trq\")\n",
+ "altitude = Input(\"alt\", dimensions=na)\n",
+ "acc = Input(\"acc\")\n",
"\n",
"# Create neural network relations\n",
- "air_drag_force = Linear(b=True)(velocity.last()**2)\n",
- "breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))\n",
- "gravity_force = Linear(W_init = 'init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())\n",
- "fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())\n",
- "local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))\n",
+ "air_drag_force = Linear(b=True)(velocity.last() ** 2)\n",
+ "breaking_force = -Relu(\n",
+ " Fir(\n",
+ " W_init=\"init_negexp\",\n",
+ " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n",
+ " )(brake.sw(n))\n",
+ ")\n",
+ "gravity_force = Linear(\n",
+ " W_init=\"init_constant\", W_init_params={\"value\": 0}, dropout=0.1, W=\"gravity\"\n",
+ ")(altitude.last())\n",
+ "fuzzi_gear = Fuzzify(6, range=[2, 7], functions=\"Rectangular\")(gear.last())\n",
+ "local_model = LocalModel(\n",
+ " input_function=lambda: Fir(\n",
+ " W_init=\"init_negexp\",\n",
+ " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n",
+ " )\n",
+ ")\n",
"engine_force = local_model(torque.sw(n), fuzzi_gear)\n",
"\n",
"# Create neural network output\n",
- "out = Output('acc_hat', air_drag_force+breaking_force+gravity_force+engine_force)\n",
+ "out = Output(\"acc_hat\", air_drag_force + breaking_force + gravity_force + engine_force)\n",
"\n",
"# Add the neural model to the nnodely structure and neuralization of the model\n",
- "vehicle.addModel('vehicle',[out])\n",
- "vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse')\n",
+ "vehicle.addModel(\"vehicle\", [out])\n",
+ "vehicle.addMinimize(\"acc_error\", acc.last(), out, loss_function=\"rmse\")\n",
"vehicle.neuralizeModel(0.05)\n",
"\n",
"## Export the Onnx Model\n",
- "vehicle.exportONNX(['vel','brk','gear','trq','alt'],['acc_hat'],models='vehicle')"
+ "vehicle.exportONNX([\"vel\", \"brk\", \"gear\", \"trq\", \"alt\"], [\"acc_hat\"], models=\"vehicle\")"
]
},
{
@@ -1328,44 +1350,62 @@
],
"source": [
"clearNames()\n",
- "vehicle = nnodely(visualizer=MPLVisualizer(),seed=2, workspace=os.path.join(os.getcwd(), 'results'))\n",
+ "vehicle = nnodely(\n",
+ " visualizer=MPLVisualizer(), seed=2, workspace=os.path.join(os.getcwd(), \"results\")\n",
+ ")\n",
"# Dimensions of the layers\n",
- "n = 25\n",
+ "n = 25\n",
"na = 21\n",
"\n",
- "#Create neural model inputs\n",
- "velocity = Input('vel')\n",
- "brake = Input('brk')\n",
- "gear = Input('gear')\n",
- "torque = Input('trq')\n",
- "altitude = Input('alt',dimensions=na)\n",
- "acc = Input('acc')\n",
+ "# Create neural model inputs\n",
+ "velocity = Input(\"vel\")\n",
+ "brake = Input(\"brk\")\n",
+ "gear = Input(\"gear\")\n",
+ "torque = Input(\"trq\")\n",
+ "altitude = Input(\"alt\", dimensions=na)\n",
+ "acc = Input(\"acc\")\n",
"\n",
"# Create neural network relations\n",
- "air_drag_force = Linear(b=True)(velocity.last()**2)\n",
- "breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))\n",
- "gravity_force = Linear(W_init = 'init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())\n",
- "fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())\n",
- "local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))\n",
+ "air_drag_force = Linear(b=True)(velocity.last() ** 2)\n",
+ "breaking_force = -Relu(\n",
+ " Fir(\n",
+ " W_init=\"init_negexp\",\n",
+ " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n",
+ " )(brake.sw(n))\n",
+ ")\n",
+ "gravity_force = Linear(\n",
+ " W_init=\"init_constant\", W_init_params={\"value\": 0}, dropout=0.1, W=\"gravity\"\n",
+ ")(altitude.last())\n",
+ "fuzzi_gear = Fuzzify(6, range=[2, 7], functions=\"Rectangular\")(gear.last())\n",
+ "local_model = LocalModel(\n",
+ " input_function=lambda: Fir(\n",
+ " W_init=\"init_negexp\",\n",
+ " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n",
+ " )\n",
+ ")\n",
"engine_force = local_model(torque.sw(n), fuzzi_gear)\n",
"\n",
- "acc_hat = air_drag_force+breaking_force+gravity_force+engine_force\n",
- "vel_hat = acc_hat.s(-1,int_name='vel_init')\n",
+ "acc_hat = air_drag_force + breaking_force + gravity_force + engine_force\n",
+ "vel_hat = acc_hat.s(-1, int_name=\"vel_init\")\n",
"\n",
"# Closing the loop\n",
"vel_hat.closedLoop(velocity)\n",
"\n",
"# Create neural network output\n",
- "out1 = Output('acc_hat', acc_hat)\n",
- "out2 = Output('vel_hat', vel_hat)\n",
+ "out1 = Output(\"acc_hat\", acc_hat)\n",
+ "out2 = Output(\"vel_hat\", vel_hat)\n",
"\n",
"# Add the neural model to the nnodely structure and neuralization of the model\n",
- "vehicle.addModel('vehicle',[out1,out2])\n",
- "vehicle.addMinimize('acc_error', acc.last(), out1, loss_function='rmse')\n",
+ "vehicle.addModel(\"vehicle\", [out1, out2])\n",
+ "vehicle.addMinimize(\"acc_error\", acc.last(), out1, loss_function=\"rmse\")\n",
"vehicle.neuralizeModel(0.05)\n",
"\n",
"## Export the Onnx Model\n",
- "vehicle.exportONNX(['brk','gear','trq','alt','vel','vel_init'],['acc_hat','vel_hat'],models='vehicle')"
+ "vehicle.exportONNX(\n",
+ " [\"brk\", \"gear\", \"trq\", \"alt\", \"vel\", \"vel_init\"],\n",
+ " [\"acc_hat\", \"vel_hat\"],\n",
+ " models=\"vehicle\",\n",
+ ")"
]
},
{
@@ -1388,11 +1428,27 @@
],
"source": [
"## Make inference using the onnx model\n",
- "data = {'vel_init':np.random.rand(1,1,1).astype(np.float32),'vel':np.random.rand(1,1,1).astype(np.float32), 'brk':np.random.rand(1,1,25,1).astype(np.float32), 'gear':np.random.rand(1,1,1,1).astype(np.float32), 'trq':np.random.rand(1,1,25,1).astype(np.float32), 'alt':np.random.rand(1,1,1,21).astype(np.float32)}\n",
- "output_onnx = Modely().onnxInference(data,'net_vehicle','results/onnx')\n",
+ "data = {\n",
+ " \"vel_init\": np.random.rand(1, 1, 1).astype(np.float32),\n",
+ " \"vel\": np.random.rand(1, 1, 1).astype(np.float32),\n",
+ " \"brk\": np.random.rand(1, 1, 25, 1).astype(np.float32),\n",
+ " \"gear\": np.random.rand(1, 1, 1, 1).astype(np.float32),\n",
+ " \"trq\": np.random.rand(1, 1, 25, 1).astype(np.float32),\n",
+ " \"alt\": np.random.rand(1, 1, 1, 21).astype(np.float32),\n",
+ "}\n",
+ "output_onnx = Modely().onnxInference(data, \"net_vehicle\", \"results/onnx\")\n",
"\n",
- "output = vehicle({'vel_init':data['vel_init'].squeeze(-1).tolist()[0], 'vel':data['vel'].squeeze(-1).tolist()[0], 'brk':data['brk'].squeeze(-1).tolist()[0][0], 'gear':data['gear'].squeeze(-1).tolist()[0], 'trq':data['trq'].squeeze(-1).tolist()[0][0], 'alt':data['alt'].squeeze(1).tolist()[0]})\n",
- "print(f'model out : {output} | onnx out : {output_onnx}')"
+ "output = vehicle(\n",
+ " {\n",
+ " \"vel_init\": data[\"vel_init\"].squeeze(-1).tolist()[0],\n",
+ " \"vel\": data[\"vel\"].squeeze(-1).tolist()[0],\n",
+ " \"brk\": data[\"brk\"].squeeze(-1).tolist()[0][0],\n",
+ " \"gear\": data[\"gear\"].squeeze(-1).tolist()[0],\n",
+ " \"trq\": data[\"trq\"].squeeze(-1).tolist()[0][0],\n",
+ " \"alt\": data[\"alt\"].squeeze(1).tolist()[0],\n",
+ " }\n",
+ ")\n",
+ "print(f\"model out : {output} | onnx out : {output_onnx}\")"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/fir.ipynb b/docs/_autodoc/tutorials/examples/fir.ipynb
index b832c001..0b2397e6 100644
--- a/docs/_autodoc/tutorials/examples/fir.ipynb
+++ b/docs/_autodoc/tutorials/examples/fir.ipynb
@@ -74,11 +74,11 @@
"metadata": {},
"outputs": [],
"source": [
- "input = Input('x')\n",
+ "input = Input(\"x\")\n",
"fir_layer = Fir(b=True)\n",
- "output = Output('out', fir_layer(input.tw(0.05)))\n",
+ "output = Output(\"out\", fir_layer(input.tw(0.05)))\n",
"# Inplace creation with default initialization\n",
- "output = Output('out2', Fir(input.tw(0.05)))"
+ "output = Output(\"out2\", Fir(input.tw(0.05)))"
]
},
{
@@ -105,9 +105,9 @@
}
],
"source": [
- "input = Input('x')\n",
- "fir_layer = Fir(b='b', W='w')\n",
- "output = Output('out', fir_layer(input.tw(0.05)))"
+ "input = Input(\"x\")\n",
+ "fir_layer = Fir(b=\"b\", W=\"w\")\n",
+ "output = Output(\"out\", fir_layer(input.tw(0.05)))"
]
},
{
@@ -136,13 +136,13 @@
}
],
"source": [
- "input = Input('x')\n",
+ "input = Input(\"x\")\n",
"\n",
- "weight = Parameter('w', dimensions=3, sw=2, init='init_constant')\n",
- "bias = Parameter('b', dimensions=3, init='init_constant')\n",
+ "weight = Parameter(\"w\", dimensions=3, sw=2, init=\"init_constant\")\n",
+ "bias = Parameter(\"b\", dimensions=3, init=\"init_constant\")\n",
"\n",
"fir_layer = Fir(W=weight, b=bias)(input.sw(2))\n",
- "output = Output('out', fir_layer)"
+ "output = Output(\"out\", fir_layer)"
]
},
{
@@ -173,13 +173,18 @@
}
],
"source": [
- "x = Input('x')\n",
- "F = Input('F')\n",
- "\n",
- "fir_x = Fir(W_init=init_negexp, b_init=init_exp)(x.tw(0.2)) \n",
- "fir_F = Fir(W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0})(F.last())\n",
- "\n",
- "output = Output('out', fir_x + fir_F)"
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
+ "\n",
+ "fir_x = Fir(W_init=init_negexp, b_init=init_exp)(x.tw(0.2))\n",
+ "fir_F = Fir(\n",
+ " W_init=init_constant,\n",
+ " W_init_params={\"value\": 1},\n",
+ " b_init=init_constant,\n",
+ " b_init_params={\"value\": 0},\n",
+ ")(F.last())\n",
+ "\n",
+ "output = Output(\"out\", fir_x + fir_F)"
]
},
{
@@ -207,12 +212,12 @@
}
],
"source": [
- "input = Input('x')\n",
+ "input = Input(\"x\")\n",
"\n",
- "weight = Parameter('w', dimensions=3, sw=2, init=init_constant)\n",
+ "weight = Parameter(\"w\", dimensions=3, sw=2, init=init_constant)\n",
"\n",
"fir_layer = Fir(W=weight, b=False, dropout=0.2)(input.sw(2))\n",
- "output = Output('out', fir_layer)"
+ "output = Output(\"out\", fir_layer)"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/fuzzify.ipynb b/docs/_autodoc/tutorials/examples/fuzzify.ipynb
index 0b9fa8e8..70702921 100644
--- a/docs/_autodoc/tutorials/examples/fuzzify.ipynb
+++ b/docs/_autodoc/tutorials/examples/fuzzify.ipynb
@@ -95,14 +95,14 @@
}
],
"source": [
- "x = Input('x')\n",
- "fuz = Fuzzify(5,[1,5])\n",
- "out = Output('out',fuz(x.last()))\n",
+ "x = Input(\"x\")\n",
+ "fuz = Fuzzify(5, [1, 5])\n",
+ "out = Output(\"out\", fuz(x.last()))\n",
"\n",
"example = Modely(visualizer=MPLNotebookVisualizer())\n",
- "example.addModel('model',out)\n",
+ "example.addModel(\"model\", out)\n",
"example.neuralizeModel()\n",
- "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))"
+ "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))"
]
},
{
@@ -159,14 +159,14 @@
}
],
"source": [
- "x = Input('x')\n",
- "fuz = Fuzzify(6,[1,6], functions = 'Rectangular')\n",
- "out = Output('out',fuz(x.last()))\n",
+ "x = Input(\"x\")\n",
+ "fuz = Fuzzify(6, [1, 6], functions=\"Rectangular\")\n",
+ "out = Output(\"out\", fuz(x.last()))\n",
"\n",
"example = Modely(visualizer=MPLNotebookVisualizer())\n",
- "example.addModel('model',out)\n",
+ "example.addModel(\"model\", out)\n",
"example.neuralizeModel()\n",
- "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))"
+ "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))"
]
},
{
@@ -235,16 +235,18 @@
"source": [
"def fun(x):\n",
" import torch\n",
+ "\n",
" return torch.tanh(x)\n",
"\n",
- "x = Input('x')\n",
- "fuz = Fuzzify(output_dimension=11, range=[-5,5], functions=fun)\n",
- "out = Output('out',fuz(x.last()))\n",
+ "\n",
+ "x = Input(\"x\")\n",
+ "fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=fun)\n",
+ "out = Output(\"out\", fuz(x.last()))\n",
"\n",
"example = Modely(visualizer=MPLNotebookVisualizer())\n",
- "example.addModel('model',out)\n",
+ "example.addModel(\"model\", out)\n",
"example.neuralizeModel()\n",
- "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))"
+ "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))"
]
},
{
@@ -306,20 +308,24 @@
"source": [
"def fun1(x):\n",
" import torch\n",
+ "\n",
" return torch.sin(x)\n",
"\n",
+ "\n",
"def fun2(x):\n",
" import torch\n",
+ "\n",
" return torch.cos(x)\n",
"\n",
- "x = Input('x')\n",
- "fuz = Fuzzify(2,range=[-1,5],functions=[fun1,fun2])\n",
- "out = Output('out',fuz(x.last()))\n",
+ "\n",
+ "x = Input(\"x\")\n",
+ "fuz = Fuzzify(2, range=[-1, 5], functions=[fun1, fun2])\n",
+ "out = Output(\"out\", fuz(x.last()))\n",
"\n",
"example = Modely(visualizer=MPLNotebookVisualizer())\n",
- "example.addModel('model',out)\n",
+ "example.addModel(\"model\", out)\n",
"example.neuralizeModel()\n",
- "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))"
+ "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))"
]
},
{
@@ -386,21 +392,25 @@
"source": [
"import torch\n",
"\n",
+ "\n",
"def fun1(x):\n",
" return torch.sin(x)\n",
+ "\n",
+ "\n",
"def fun2(x):\n",
" return torch.cos(x)\n",
"\n",
- "x = Input('x')\n",
- "F = Input('F')\n",
"\n",
- "fuz = Fuzzify(centers=[-1,0,3,5],functions=[fun1,fun2,fun1,fun2])\n",
- "out = Output('out',fuz(x.last())+fuz(F.last()))\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
+ "\n",
+ "fuz = Fuzzify(centers=[-1, 0, 3, 5], functions=[fun1, fun2, fun1, fun2])\n",
+ "out = Output(\"out\", fuz(x.last()) + fuz(F.last()))\n",
"\n",
"example = Modely(visualizer=MPLNotebookVisualizer())\n",
- "example.addModel('model',out)\n",
+ "example.addModel(\"model\", out)\n",
"example.neuralizeModel()\n",
- "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))"
+ "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/inference.ipynb b/docs/_autodoc/tutorials/examples/inference.ipynb
index 0c7395e5..4f21d720 100644
--- a/docs/_autodoc/tutorials/examples/inference.ipynb
+++ b/docs/_autodoc/tutorials/examples/inference.ipynb
@@ -98,15 +98,15 @@
],
"source": [
"## Model definition\n",
- "x = Input('x')\n",
- "F = Input('F')\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"\n",
- "next_x = Fir()(x.tw(0.5))+Fir()(F.last())\n",
+ "next_x = Fir()(x.tw(0.5)) + Fir()(F.last())\n",
"\n",
- "out = Output('next_x', next_x)\n",
+ "out = Output(\"next_x\", next_x)\n",
"\n",
"model = Modely()\n",
- "model.addModel('model',[out])\n",
+ "model.addModel(\"model\", [out])\n",
"model.neuralizeModel(0.05)"
]
},
@@ -125,7 +125,9 @@
],
"source": [
"## Inference\n",
- "results = model(inputs={'F':[[9]],'x':[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]]})\n",
+ "results = model(\n",
+ " inputs={\"F\": [[9]], \"x\": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]]}\n",
+ ")\n",
"print(results)"
]
},
@@ -143,7 +145,12 @@
}
],
"source": [
- "results = model(inputs={'F':[[5],[4],[9]],'x':[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13]]})\n",
+ "results = model(\n",
+ " inputs={\n",
+ " \"F\": [[5], [4], [9]],\n",
+ " \"x\": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13]],\n",
+ " }\n",
+ ")\n",
"print(results)"
]
},
@@ -170,7 +177,16 @@
}
],
"source": [
- "results = model(inputs={'F':[[5],[2]],'x':[[1,2,3,4,5,6,7,8,9,10],[12,13,14,15,16,17,18,19,20,21]]}, sampled=True)\n",
+ "results = model(\n",
+ " inputs={\n",
+ " \"F\": [[5], [2]],\n",
+ " \"x\": [\n",
+ " [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n",
+ " [12, 13, 14, 15, 16, 17, 18, 19, 20, 21],\n",
+ " ],\n",
+ " },\n",
+ " sampled=True,\n",
+ ")\n",
"print(results)"
]
},
@@ -215,15 +231,15 @@
}
],
"source": [
- "input1 = Input('in1')\n",
- "W = Parameter('W', sw=3, values=[[1], [2], [3]])\n",
- "out = Output('out',Fir(W=W)(input1.sw(3)))\n",
+ "input1 = Input(\"in1\")\n",
+ "W = Parameter(\"W\", sw=3, values=[[1], [2], [3]])\n",
+ "out = Output(\"out\", Fir(W=W)(input1.sw(3)))\n",
"\n",
"model = Modely(visualizer=TextVisualizer(), seed=42)\n",
- "model.addModel('model', [out])\n",
+ "model.addModel(\"model\", [out])\n",
"model.neuralizeModel()\n",
"\n",
- "result = model({'in1': [1, 2, 3]}, closed_loop={'in1':'out'})\n",
+ "result = model({\"in1\": [1, 2, 3]}, closed_loop={\"in1\": \"out\"})\n",
"print(result)"
]
},
@@ -277,14 +293,14 @@
}
],
"source": [
- "input2 = Input('in2')\n",
- "K = Parameter('K', sw=3, values=[[1], [2], [3]])\n",
- "out2 = Output('out2',Fir(W=K)(input2.sw(3)))\n",
+ "input2 = Input(\"in2\")\n",
+ "K = Parameter(\"K\", sw=3, values=[[1], [2], [3]])\n",
+ "out2 = Output(\"out2\", Fir(W=K)(input2.sw(3)))\n",
"\n",
- "model.addModel('model2', [out2])\n",
+ "model.addModel(\"model2\", [out2])\n",
"model.neuralizeModel()\n",
"\n",
- "result = model(inputs={'in1': [1, 2, 3]}, connect={'in2':'out'})\n",
+ "result = model(inputs={\"in1\": [1, 2, 3]}, connect={\"in2\": \"out\"})\n",
"print(result)"
]
},
@@ -313,7 +329,9 @@
}
],
"source": [
- "result = model({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3)\n",
+ "result = model(\n",
+ " {\"in1\": [1, 2, 3, 4, 5]}, closed_loop={\"in1\": \"out\"}, prediction_samples=3\n",
+ ")\n",
"print(result)"
]
},
@@ -342,7 +360,12 @@
}
],
"source": [
- "result = model({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3, num_of_samples = 5)\n",
+ "result = model(\n",
+ " {\"in1\": [1, 2, 3, 4, 5]},\n",
+ " closed_loop={\"in1\": \"out\"},\n",
+ " prediction_samples=3,\n",
+ " num_of_samples=5,\n",
+ ")\n",
"print(result)"
]
}
diff --git a/docs/_autodoc/tutorials/examples/interpolation.ipynb b/docs/_autodoc/tutorials/examples/interpolation.ipynb
index 7a015245..b365bffc 100644
--- a/docs/_autodoc/tutorials/examples/interpolation.ipynb
+++ b/docs/_autodoc/tutorials/examples/interpolation.ipynb
@@ -46,9 +46,11 @@
"metadata": {},
"outputs": [],
"source": [
- "x = Input('x')\n",
- "interpolation = Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[1.0, 4.0, 9.0, 16.0])(x.last())\n",
- "out = Output('out',interpolation)"
+ "x = Input(\"x\")\n",
+ "interpolation = Interpolation(\n",
+ " x_points=[1.0, 2.0, 3.0, 4.0], y_points=[1.0, 4.0, 9.0, 16.0]\n",
+ ")(x.last())\n",
+ "out = Output(\"out\", interpolation)"
]
},
{
@@ -76,9 +78,11 @@
}
],
"source": [
- "y = Input('y')\n",
- "interpolation = Interpolation(x_points=[1.0, 4.0, 3.0, 2.0],y_points=[1.0, 16.0, 9.0, 4.0], mode='linear')(y.last())\n",
- "out = Output('out',interpolation)"
+ "y = Input(\"y\")\n",
+ "interpolation = Interpolation(\n",
+ " x_points=[1.0, 4.0, 3.0, 2.0], y_points=[1.0, 16.0, 9.0, 4.0], mode=\"linear\"\n",
+ ")(y.last())\n",
+ "out = Output(\"out\", interpolation)"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/linear.ipynb b/docs/_autodoc/tutorials/examples/linear.ipynb
index 065ba3e0..45f5c44d 100644
--- a/docs/_autodoc/tutorials/examples/linear.ipynb
+++ b/docs/_autodoc/tutorials/examples/linear.ipynb
@@ -69,7 +69,7 @@
"metadata": {},
"outputs": [],
"source": [
- "x = Input('x').tw(0.05)\n",
+ "x = Input(\"x\").tw(0.05)\n",
"linear = Linear(output_dimension=5)(x)"
]
},
@@ -96,10 +96,10 @@
}
],
"source": [
- "x = Input('x').last()\n",
+ "x = Input(\"x\").last()\n",
"\n",
- "weight = Parameter('W', values=[[1]])\n",
- "bias = Parameter('b', values=[1])\n",
+ "weight = Parameter(\"W\", values=[[1]])\n",
+ "bias = Parameter(\"b\", values=[1])\n",
"\n",
"linear = Linear(W=weight, b=bias)(x)"
]
@@ -131,8 +131,14 @@
}
],
"source": [
- "x = Input('x').last()\n",
- "linear = Linear(output_dimension=5, b=True, W_init=init_negexp, b_init=init_constant, b_init_params={'value':1})(x)"
+ "x = Input(\"x\").last()\n",
+ "linear = Linear(\n",
+ " output_dimension=5,\n",
+ " b=True,\n",
+ " W_init=init_negexp,\n",
+ " b_init=init_constant,\n",
+ " b_init_params={\"value\": 1},\n",
+ ")(x)"
]
},
{
@@ -158,9 +164,9 @@
}
],
"source": [
- "input = Input('x')\n",
+ "input = Input(\"x\")\n",
"linear = Linear(output_dimension=10, dropout=0.2)(input.sw(2))\n",
- "output = Output('out', linear)"
+ "output = Output(\"out\", linear)"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/localmodel.ipynb b/docs/_autodoc/tutorials/examples/localmodel.ipynb
index b62da3e0..9e6f94a5 100644
--- a/docs/_autodoc/tutorials/examples/localmodel.ipynb
+++ b/docs/_autodoc/tutorials/examples/localmodel.ipynb
@@ -59,11 +59,11 @@
"metadata": {},
"outputs": [],
"source": [
- "c = Input('c')\n",
- "x = Input('x')\n",
- "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n",
+ "c = Input(\"c\")\n",
+ "x = Input(\"x\")\n",
+ "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n",
"loc = LocalModel(input_function=Fir())\n",
- "out = Output('out', loc(x.tw(1), activation))"
+ "out = Output(\"out\", loc(x.tw(1), activation))"
]
},
{
@@ -93,11 +93,13 @@
}
],
"source": [
- "c = Input('c')\n",
- "x = Input('x')\n",
- "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n",
- "loc = LocalModel(input_function = lambda:Fir, output_function = lambda:Fir)(x.last(), activation)\n",
- "out = Output('out', loc)"
+ "c = Input(\"c\")\n",
+ "x = Input(\"x\")\n",
+ "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n",
+ "loc = LocalModel(input_function=lambda: Fir, output_function=lambda: Fir)(\n",
+ " x.last(), activation\n",
+ ")\n",
+ "out = Output(\"out\", loc)"
]
},
{
@@ -125,14 +127,17 @@
}
],
"source": [
- "def myFun(in1,p1,p2):\n",
- " return p1*in1+p2\n",
+ "def myFun(in1, p1, p2):\n",
+ " return p1 * in1 + p2\n",
"\n",
- "c = Input('c')\n",
- "x = Input('x')\n",
- "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n",
- "loc = LocalModel(input_function = lambda:ParamFun(myFun), output_function = lambda:Fir)(x.last(), activation)\n",
- "out = Output('out', loc)"
+ "\n",
+ "c = Input(\"c\")\n",
+ "x = Input(\"x\")\n",
+ "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n",
+ "loc = LocalModel(input_function=lambda: ParamFun(myFun), output_function=lambda: Fir)(\n",
+ " x.last(), activation\n",
+ ")\n",
+ "out = Output(\"out\", loc)"
]
},
{
@@ -160,17 +165,21 @@
}
],
"source": [
- "c = Input('c')\n",
- "d = Input('d')\n",
- "activationA = Fuzzify(2,[0,1],functions='Triangular')(c.tw(1))\n",
- "activationB = Fuzzify(2,[0,1],functions='Triangular')(d.tw(1))\n",
+ "c = Input(\"c\")\n",
+ "d = Input(\"d\")\n",
+ "activationA = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.tw(1))\n",
+ "activationB = Fuzzify(2, [0, 1], functions=\"Triangular\")(d.tw(1))\n",
+ "\n",
"\n",
- "def myFun(in1,p1,p2):\n",
- " return p1*in1+p2\n",
+ "def myFun(in1, p1, p2):\n",
+ " return p1 * in1 + p2\n",
"\n",
- "x = Input('x')\n",
- "loc = LocalModel(input_function = lambda:ParamFun(myFun), output_function = Fir(3))(x.tw(1),(activationA,activationB))\n",
- "out = Output('out', loc)"
+ "\n",
+ "x = Input(\"x\")\n",
+ "loc = LocalModel(input_function=lambda: ParamFun(myFun), output_function=Fir(3))(\n",
+ " x.tw(1), (activationA, activationB)\n",
+ ")\n",
+ "out = Output(\"out\", loc)"
]
},
{
@@ -203,32 +212,45 @@
}
],
"source": [
- "c = Input('c')\n",
- "d = Input('d')\n",
- "activationA = Fuzzify(2,[0,1],functions='Triangular')(c.tw(1))\n",
- "activationB = Fuzzify(2,[0,1],functions='Triangular')(d.tw(1))\n",
+ "c = Input(\"c\")\n",
+ "d = Input(\"d\")\n",
+ "activationA = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.tw(1))\n",
+ "activationB = Fuzzify(2, [0, 1], functions=\"Triangular\")(d.tw(1))\n",
+ "\n",
+ "\n",
+ "def myFun(in1, p1, p2):\n",
+ " return p1 * in1 + p2\n",
"\n",
- "def myFun(in1,p1,p2):\n",
- " return p1*in1+p2\n",
"\n",
"def input_function_gen(idx_list):\n",
- " if idx_list == [0,0]:\n",
- " p1, p2 = Parameter('p1_0',values=[[1]]), Parameter('p2_0',values=[[2]])\n",
- " if idx_list == [0,1]:\n",
- " p1, p2 = Parameter('p1_0',values=[[1]]), Parameter('p2_1',values=[[3]])\n",
- " if idx_list == [1,0]:\n",
- " p1, p2 = Parameter('p1_1',values=[[2]]), Parameter('p2_0',values=[[2]])\n",
+ " if idx_list == [0, 0]:\n",
+ " p1, p2 = Parameter(\"p1_0\", values=[[1]]), Parameter(\"p2_0\", values=[[2]])\n",
+ " if idx_list == [0, 1]:\n",
+ " p1, p2 = Parameter(\"p1_0\", values=[[1]]), Parameter(\"p2_1\", values=[[3]])\n",
+ " if idx_list == [1, 0]:\n",
+ " p1, p2 = Parameter(\"p1_1\", values=[[2]]), Parameter(\"p2_0\", values=[[2]])\n",
" if idx_list == [1, 1]:\n",
- " p1, p2 = Parameter('p1_1',values=[[2]]), Parameter('p2_1',values=[[3]])\n",
- " return ParamFun(myFun,parameters_and_constants=[p1,p2])\n",
+ " p1, p2 = Parameter(\"p1_1\", values=[[2]]), Parameter(\"p2_1\", values=[[3]])\n",
+ " return ParamFun(myFun, parameters_and_constants=[p1, p2])\n",
+ "\n",
"\n",
"def output_function_gen(idx_list):\n",
- " pfir = Parameter('pfir_'+str(idx_list),tw=1,dimensions=2,values=[[1+idx_list[0],2+idx_list[1]],[3+idx_list[0],4+idx_list[1]]])\n",
- " return Fir(2,W=pfir)\n",
+ " pfir = Parameter(\n",
+ " \"pfir_\" + str(idx_list),\n",
+ " tw=1,\n",
+ " dimensions=2,\n",
+ " values=[[1 + idx_list[0], 2 + idx_list[1]], [3 + idx_list[0], 4 + idx_list[1]]],\n",
+ " )\n",
+ " return Fir(2, W=pfir)\n",
+ "\n",
"\n",
- "x = Input('x')\n",
- "loc = LocalModel(input_function=input_function_gen, output_function= output_function_gen, pass_indexes = True)(x.tw(1),(activationA,activationB))\n",
- "out = Output('out', loc)"
+ "x = Input(\"x\")\n",
+ "loc = LocalModel(\n",
+ " input_function=input_function_gen,\n",
+ " output_function=output_function_gen,\n",
+ " pass_indexes=True,\n",
+ ")(x.tw(1), (activationA, activationB))\n",
+ "out = Output(\"out\", loc)"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/parameter.ipynb b/docs/_autodoc/tutorials/examples/parameter.ipynb
index 32f3ed14..ee3c001d 100644
--- a/docs/_autodoc/tutorials/examples/parameter.ipynb
+++ b/docs/_autodoc/tutorials/examples/parameter.ipynb
@@ -73,8 +73,8 @@
},
"outputs": [],
"source": [
- "g = Constant('g', values=[9.81])\n",
- "k = Parameter('k', tw=4)"
+ "g = Constant(\"g\", values=[9.81])\n",
+ "k = Parameter(\"k\", tw=4)"
]
},
{
@@ -107,14 +107,14 @@
}
],
"source": [
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"\n",
- "k = Parameter('k', dimensions=3, tw=4)\n",
+ "k = Parameter(\"k\", dimensions=3, tw=4)\n",
"\n",
"fir1 = Fir(W=k)\n",
"fir2 = Fir(3, W=k)\n",
"\n",
- "out = Output('out', fir1(x.tw(4))+fir2(x.tw(4)))"
+ "out = Output(\"out\", fir1(x.tw(4)) + fir2(x.tw(4)))"
]
},
{
@@ -147,17 +147,19 @@
}
],
"source": [
- "x= Input('x')\n",
+ "x = Input(\"x\")\n",
+ "\n",
+ "g = Parameter(\"g\", dimensions=3, values=[[4, 5, 6]])\n",
+ "t = Parameter(\"t\", dimensions=3, values=[[1, 2, 3]])\n",
"\n",
- "g = Parameter('g', dimensions=3, values=[[4,5,6]])\n",
- "t = Parameter('t', dimensions=3, values=[[1,2,3]])\n",
"\n",
"def fun(x, k, t):\n",
- " return x+(k+t)\n",
+ " return x + (k + t)\n",
+ "\n",
"\n",
- "p = ParamFun(fun, parameters_and_constants=[g,t])\n",
+ "p = ParamFun(fun, parameters_and_constants=[g, t])\n",
"\n",
- "out = Output('out', p(x.tw(1)))"
+ "out = Output(\"out\", p(x.tw(1)))"
]
},
{
@@ -190,10 +192,10 @@
}
],
"source": [
- "g = Parameter('g', sw=1, values=[[1,2,3,4]])\n",
- "o = Constant('o', sw=1, values=[[1,2,3,4]])\n",
- "x = Input('x', dimensions=4)\n",
- "out = Output('out', x.last()+g+o)"
+ "g = Parameter(\"g\", sw=1, values=[[1, 2, 3, 4]])\n",
+ "o = Constant(\"o\", sw=1, values=[[1, 2, 3, 4]])\n",
+ "x = Input(\"x\", dimensions=4)\n",
+ "out = Output(\"out\", x.last() + g + o)"
]
},
{
@@ -226,8 +228,8 @@
],
"source": [
"dt = SampleTime()\n",
- "x = Input('x')\n",
- "out = Output('out', x.last() + dt)"
+ "x = Input(\"x\")\n",
+ "out = Output(\"out\", x.last() + dt)"
]
},
{
@@ -250,7 +252,7 @@
},
"outputs": [],
"source": [
- "p = Parameter('p', dimensions=4, init=init_constant, init_params={'value':4})"
+ "p = Parameter(\"p\", dimensions=4, init=init_constant, init_params={\"value\": 4})"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/parametric_functions.ipynb b/docs/_autodoc/tutorials/examples/parametric_functions.ipynb
index 0ba8e250..aeeecda1 100644
--- a/docs/_autodoc/tutorials/examples/parametric_functions.ipynb
+++ b/docs/_autodoc/tutorials/examples/parametric_functions.ipynb
@@ -59,15 +59,17 @@
"metadata": {},
"outputs": [],
"source": [
- "def myFun(K1,K2,p1,p2):\n",
+ "def myFun(K1, K2, p1, p2):\n",
" import torch\n",
- " return p1*K1+p2*torch.sin(K2)\n",
"\n",
- "x = Input('x')\n",
- "F = Input('F')\n",
+ " return p1 * K1 + p2 * torch.sin(K2)\n",
+ "\n",
+ "\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
"\n",
"parfun = ParamFun(myFun)\n",
- "out = Output('out',parfun(x.last(),F.last()))"
+ "out = Output(\"out\", parfun(x.last(), F.last()))"
]
},
{
@@ -94,14 +96,16 @@
}
],
"source": [
- "def myFun(K1,K2,p1):\n",
+ "def myFun(K1, K2, p1):\n",
" import torch\n",
- " return torch.stack([K1,2*K1,3*K1,4*K1],dim=2).squeeze(-1)*p1+K2\n",
"\n",
- "x=Input('x')\n",
- "F=Input('F')\n",
- "parfun = ParamFun(myFun, parameters_and_constants = {'p1':(1,4)})\n",
- "out = Output('out',parfun(x.last(),F.last()))"
+ " return torch.stack([K1, 2 * K1, 3 * K1, 4 * K1], dim=2).squeeze(-1) * p1 + K2\n",
+ "\n",
+ "\n",
+ "x = Input(\"x\")\n",
+ "F = Input(\"F\")\n",
+ "parfun = ParamFun(myFun, parameters_and_constants={\"p1\": (1, 4)})\n",
+ "out = Output(\"out\", parfun(x.last(), F.last()))"
]
},
{
@@ -128,13 +132,14 @@
}
],
"source": [
- "def myFun(K1,p1):\n",
- " return K1*p1\n",
+ "def myFun(K1, p1):\n",
+ " return K1 * p1\n",
"\n",
- "x = Input('x')\n",
- "K = Parameter('k', dimensions = 1, sw = 1,values=[[2.0]])\n",
+ "\n",
+ "x = Input(\"x\")\n",
+ "K = Parameter(\"k\", dimensions=1, sw=1, values=[[2.0]])\n",
"parfun = ParamFun(myFun, parameters_and_constants=[K])\n",
- "out = Output('out',parfun(x.sw(1)))"
+ "out = Output(\"out\", parfun(x.sw(1)))"
]
},
{
@@ -161,15 +166,16 @@
}
],
"source": [
- "def myFun(K1,p1):\n",
- " return K1*p1\n",
+ "def myFun(K1, p1):\n",
+ " return K1 * p1\n",
+ "\n",
"\n",
- "K = Parameter('k1', dimensions = 1, tw = 1, values=[[2.0],[3.0],[4.0],[5.0]])\n",
- "R = Parameter('r1', dimensions = 1, tw = 1, values=[[5.0],[4.0],[3.0],[2.0]])\n",
+ "K = Parameter(\"k1\", dimensions=1, tw=1, values=[[2.0], [3.0], [4.0], [5.0]])\n",
+ "R = Parameter(\"r1\", dimensions=1, tw=1, values=[[5.0], [4.0], [3.0], [2.0]])\n",
"\n",
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"parfun = ParamFun(myFun)\n",
- "out = Output('out',parfun(x.tw(1),K)+parfun(x.tw(1),R))"
+ "out = Output(\"out\", parfun(x.tw(1), K) + parfun(x.tw(1), R))"
]
},
{
@@ -196,13 +202,14 @@
}
],
"source": [
- "def myFun(K1,p1):\n",
- " return K1*p1\n",
+ "def myFun(K1, p1):\n",
+ " return K1 * p1\n",
+ "\n",
"\n",
"parfun = ParamFun(myFun)\n",
- "x = Input('x')\n",
- "c = Constant('c',values=[[5.0],[4.0],[3.0],[2.0]])\n",
- "out = Output('out',parfun(x.sw(4),c))"
+ "x = Input(\"x\")\n",
+ "c = Constant(\"c\", values=[[5.0], [4.0], [3.0], [2.0]])\n",
+ "out = Output(\"out\", parfun(x.sw(4), c))"
]
},
{
@@ -232,13 +239,14 @@
],
"source": [
"def myFun(k, p1):\n",
- " print(f'k:{k.shape}')\n",
- " print(f'p1:{p1.shape}')\n",
+ " print(f\"k:{k.shape}\")\n",
+ " print(f\"p1:{p1.shape}\")\n",
" return k * p1\n",
"\n",
- "x = Input('x')\n",
+ "\n",
+ "x = Input(\"x\")\n",
"parfun = ParamFun(myFun, map_over_batch=True)\n",
- "out = Output('out',parfun(x.sw(4)))"
+ "out = Output(\"out\", parfun(x.sw(4)))"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/partitioning.ipynb b/docs/_autodoc/tutorials/examples/partitioning.ipynb
index e3a0708d..c916415b 100644
--- a/docs/_autodoc/tutorials/examples/partitioning.ipynb
+++ b/docs/_autodoc/tutorials/examples/partitioning.ipynb
@@ -80,13 +80,13 @@
}
],
"source": [
- "x = Input('x', dimensions=10).last()\n",
+ "x = Input(\"x\", dimensions=10).last()\n",
"sub = Part(x, 0, 2)\n",
- "out = Output('out', sub)\n",
+ "out = Output(\"out\", sub)\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out])\n",
+ "test.addModel(\"test\", [out])\n",
"test.neuralizeModel()\n",
- "test({'x': [[1,2,3,4,5,6,7,8,9,10]]})"
+ "test({\"x\": [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]})"
]
},
{
@@ -130,13 +130,13 @@
}
],
"source": [
- "x = Input('x', dimensions=3).last()\n",
+ "x = Input(\"x\", dimensions=3).last()\n",
"sub = Select(x, 1)\n",
- "out = Output('out', sub)\n",
+ "out = Output(\"out\", sub)\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out])\n",
+ "test.addModel(\"test\", [out])\n",
"test.neuralizeModel()\n",
- "test({'x': [[1,2,3]]})"
+ "test({\"x\": [[1, 2, 3]]})"
]
},
{
@@ -180,14 +180,14 @@
}
],
"source": [
- "x = Input('x', dimensions=3).last()\n",
- "y = Input('y', dimensions=5).last()\n",
+ "x = Input(\"x\", dimensions=3).last()\n",
+ "y = Input(\"y\", dimensions=5).last()\n",
"cat = Concatenate(x, y)\n",
- "out = Output('out', cat)\n",
+ "out = Output(\"out\", cat)\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out])\n",
+ "test.addModel(\"test\", [out])\n",
"test.neuralizeModel()\n",
- "test({'x': [[1,2,3]],'y': [[4,5,6,7,8]]})"
+ "test({\"x\": [[1, 2, 3]], \"y\": [[4, 5, 6, 7, 8]]})"
]
},
{
@@ -231,16 +231,16 @@
}
],
"source": [
- "x = Input('x')\n",
+ "x = Input(\"x\")\n",
"x_sw10 = x.sw(10)\n",
"relation = SamplePart(x_sw10, 0, 3)\n",
- "out = Output('out', relation)\n",
- "out2 = Output('out2', x.last())\n",
+ "out = Output(\"out\", relation)\n",
+ "out2 = Output(\"out2\", x.last())\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out,out2])\n",
+ "test.addModel(\"test\", [out, out2])\n",
"test.neuralizeModel()\n",
"# Test 1 input in time\n",
- "test({'x': [1,2,3,4,5,6,7,8,9,10]})"
+ "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})"
]
},
{
@@ -266,7 +266,7 @@
],
"source": [
"# Test 2 input in time\n",
- "test({'x': [1,2,3,4,5,6,7,8,9,10,11]})"
+ "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})"
]
},
{
@@ -310,13 +310,13 @@
}
],
"source": [
- "x = Input('x').sw(10)\n",
+ "x = Input(\"x\").sw(10)\n",
"relation = SampleSelect(x, 5)\n",
- "out = Output('out', relation)\n",
+ "out = Output(\"out\", relation)\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out])\n",
+ "test.addModel(\"test\", [out])\n",
"test.neuralizeModel()\n",
- "test({'x': [1,2,3,4,5,6,7,8,9,10]})"
+ "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})"
]
},
{
@@ -349,9 +349,9 @@
],
"source": [
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out, Output('out2', relation.sw(2))])\n",
+ "test.addModel(\"test\", [out, Output(\"out2\", relation.sw(2))])\n",
"test.neuralizeModel()\n",
- "test({'x': [1,2,3,4,5,6,7,8,9,10,11]})"
+ "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})"
]
},
{
@@ -396,16 +396,16 @@
}
],
"source": [
- "x = Input('x').sw(5)\n",
- "y = Input('y').sw(3)\n",
+ "x = Input(\"x\").sw(5)\n",
+ "y = Input(\"y\").sw(3)\n",
"cat = TimeConcatenate(x, y)\n",
"cat2 = TimeConcatenate(Fir(x), y)\n",
- "out = Output('out', cat)\n",
- "out2 = Output('out2', cat2)\n",
+ "out = Output(\"out\", cat)\n",
+ "out2 = Output(\"out2\", cat2)\n",
"test = Modely(visualizer=None)\n",
- "test.addModel('test', [out,out2])\n",
+ "test.addModel(\"test\", [out, out2])\n",
"test.neuralizeModel()\n",
- "test({'x': [1,2,3,4,5],'y': [1,2,3]})"
+ "test({\"x\": [1, 2, 3, 4, 5], \"y\": [1, 2, 3]})"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/states.ipynb b/docs/_autodoc/tutorials/examples/states.ipynb
index 2087b0ec..5b2f16b0 100644
--- a/docs/_autodoc/tutorials/examples/states.ipynb
+++ b/docs/_autodoc/tutorials/examples/states.ipynb
@@ -65,8 +65,8 @@
},
"outputs": [],
"source": [
- "clearNames('x_state')\n",
- "x_state = Input('x_state', dimensions=1)\n",
+ "clearNames(\"x_state\")\n",
+ "x_state = Input(\"x_state\", dimensions=1)\n",
"x_out = Fir(x_state.tw(0.5))"
]
},
@@ -90,9 +90,9 @@
},
"outputs": [],
"source": [
- "clearNames('out')\n",
+ "clearNames(\"out\")\n",
"x_out.closedLoop(x_state)\n",
- "out = Output('out',x_out)"
+ "out = Output(\"out\", x_out)"
]
},
{
@@ -113,9 +113,9 @@
},
"outputs": [],
"source": [
- "clearNames('out')\n",
+ "clearNames(\"out\")\n",
"x_out = ClosedLoop(x_out, x_state)\n",
- "out = Output('out',x_out)"
+ "out = Output(\"out\", x_out)"
]
},
{
@@ -143,7 +143,7 @@
"clearNames()\n",
"x_out = Fir(x_state.tw(0.5))\n",
"x_out.connect(x_state)\n",
- "out = Output('out',x_out)"
+ "out = Output(\"out\", x_out)"
]
},
{
@@ -164,9 +164,9 @@
},
"outputs": [],
"source": [
- "clearNames('out')\n",
+ "clearNames(\"out\")\n",
"x_out = Connect(x_out, x_state)\n",
- "out = Output('out',x_out)"
+ "out = Output(\"out\", x_out)"
]
},
{
@@ -366,52 +366,64 @@
}
],
"source": [
- "clearNames(['a','b_t','c','d_t','b_in','shared','b','A','B','C','D','d'])\n",
+ "clearNames([\"a\", \"b_t\", \"c\", \"d_t\", \"b_in\", \"shared\", \"b\", \"A\", \"B\", \"C\", \"D\", \"d\"])\n",
"import numpy as np\n",
"\n",
+ "\n",
"def linear_function(x, k1, k2):\n",
- " return x*k1 + k2\n",
+ " return x * k1 + k2\n",
+ "\n",
"\n",
- "data_a = np.arange(1,101, dtype=np.float32)\n",
+ "data_a = np.arange(1, 101, dtype=np.float32)\n",
"data_b_t = linear_function(data_a, 2, 3)\n",
"\n",
- "data_c = np.arange(1,101, dtype=np.float32)\n",
- "data_b_in = np.arange(5,105, dtype=np.float32)\n",
+ "data_c = np.arange(1, 101, dtype=np.float32)\n",
+ "data_b_in = np.arange(5, 105, dtype=np.float32)\n",
"data_d_t = linear_function(data_c, 5, 1)\n",
"\n",
- "dataset = {'a': data_a, 'b_t': data_b_t, 'c':data_c, 'b_in': data_b_in, 'd_t':data_d_t }\n",
+ "dataset = {\n",
+ " \"a\": data_a,\n",
+ " \"b_t\": data_b_t,\n",
+ " \"c\": data_c,\n",
+ " \"b_in\": data_b_in,\n",
+ " \"d_t\": data_d_t,\n",
+ "}\n",
"## Model a\n",
- "a = Input('a')\n",
- "b_t = Input('b_t')\n",
- "shared = Parameter('shared',dimensions=(1,1))\n",
- "output_relation = Linear(W=shared)(a.last())+Linear(W='A')(Fir(W='B')(a.tw(0.5)))\n",
- "b = Output('b',output_relation)\n",
+ "a = Input(\"a\")\n",
+ "b_t = Input(\"b_t\")\n",
+ "shared = Parameter(\"shared\", dimensions=(1, 1))\n",
+ "output_relation = Linear(W=shared)(a.last()) + Linear(W=\"A\")(Fir(W=\"B\")(a.tw(0.5)))\n",
+ "b = Output(\"b\", output_relation)\n",
"\n",
"model = Modely(seed=42)\n",
- "model.addModel('b_model', b)\n",
- "model.addMinimize('b_min', b, b_t.last())\n",
+ "model.addModel(\"b_model\", b)\n",
+ "model.addMinimize(\"b_min\", b, b_t.last())\n",
"model.neuralizeModel(0.1)\n",
"\n",
"# Model d\n",
- "c = Input('c')\n",
- "d_t = Input('d_t')\n",
- "b_in = Input('b_in')\n",
+ "c = Input(\"c\")\n",
+ "d_t = Input(\"d_t\")\n",
+ "b_in = Input(\"b_in\")\n",
"output_relation.connect(b_in)\n",
- "d = Output('d',Linear(W=shared)(c.last())+Fir(W='C')(c.tw(0.5))+Fir(W='D')(b_in.tw(0.3)))\n",
+ "d = Output(\n",
+ " \"d\", Linear(W=shared)(c.last()) + Fir(W=\"C\")(c.tw(0.5)) + Fir(W=\"D\")(b_in.tw(0.3))\n",
+ ")\n",
"\n",
- "model.addModel('d_model', [b,d])\n",
- "model.addMinimize('d_min', d, d_t.last())\n",
+ "model.addModel(\"d_model\", [b, d])\n",
+ "model.addMinimize(\"d_min\", d, d_t.last())\n",
"model.neuralizeModel(0.1)\n",
- "model.loadData('dataset', dataset)\n",
+ "model.loadData(\"dataset\", dataset)\n",
"\n",
- "params = {'num_of_epochs': 1,\n",
- " 'train_batch_size': 8,\n",
- " 'val_batch_size': 8,\n",
- " 'test_batch_size':1,\n",
- " 'lr':0.1}\n",
+ "params = {\n",
+ " \"num_of_epochs\": 1,\n",
+ " \"train_batch_size\": 8,\n",
+ " \"val_batch_size\": 8,\n",
+ " \"test_batch_size\": 1,\n",
+ " \"lr\": 0.1,\n",
+ "}\n",
"\n",
"## training dei parametri di tutti i modelli\n",
- "_ = model.trainModel(splits=[100,0,0], training_params=params, prediction_samples=4)"
+ "_ = model.trainModel(splits=[100, 0, 0], training_params=params, prediction_samples=4)"
]
},
{
@@ -551,29 +563,35 @@
],
"source": [
"import numpy as np\n",
- "clearNames(['x','x_state','y_state','out'])\n",
- "x = Input('x', dimensions=3)\n",
- "x_state = Input('x_state', dimensions=3)\n",
- "y_state = Input('y_state', dimensions=3)\n",
+ "\n",
+ "clearNames([\"x\", \"x_state\", \"y_state\", \"out\"])\n",
+ "x = Input(\"x\", dimensions=3)\n",
+ "x_state = Input(\"x_state\", dimensions=3)\n",
+ "y_state = Input(\"y_state\", dimensions=3)\n",
"x_out = Linear(output_dimension=3)(x_state.tw(0.5))\n",
"y_out = Linear(output_dimension=3)(y_state.tw(0.5))\n",
"x_out.closedLoop(x_state)\n",
"y_out.closedLoop(y_state)\n",
- "out = Output('out',x_out+y_out)\n",
+ "out = Output(\"out\", x_out + y_out)\n",
"\n",
"test = Modely(seed=42)\n",
- "test.addModel('model', out)\n",
- "test.addMinimize('error', out, x.tw(0.5))\n",
+ "test.addModel(\"model\", out)\n",
+ "test.addMinimize(\"error\", out, x.tw(0.5))\n",
"\n",
"test.neuralizeModel(0.1)\n",
"\n",
- "dataset = {'x':np.array([np.random.uniform(1,4,300)]).reshape(100,3).tolist()}\n",
- "test.loadData(name='dataset', source=dataset)\n",
+ "dataset = {\"x\": np.array([np.random.uniform(1, 4, 300)]).reshape(100, 3).tolist()}\n",
+ "test.loadData(name=\"dataset\", source=dataset)\n",
"\n",
"# Training non ricorrente\n",
- "params = {'num_of_epochs': 10, 'train_batch_size': 1, 'val_batch_size':1, 'lr':0.01}\n",
- "tp = test.trainModel(splits=[70,20,10], prediction_samples=5, shuffle_data=False, training_params=params)\n",
- "print('finale state: ', test._states)"
+ "params = {\"num_of_epochs\": 10, \"train_batch_size\": 1, \"val_batch_size\": 1, \"lr\": 0.01}\n",
+ "tp = test.trainModel(\n",
+ " splits=[70, 20, 10],\n",
+ " prediction_samples=5,\n",
+ " shuffle_data=False,\n",
+ " training_params=params,\n",
+ ")\n",
+ "print(\"finale state: \", test._states)"
]
},
{
@@ -605,7 +623,7 @@
],
"source": [
"test.resetStates()\n",
- "print('finale state: ', test.states)"
+ "print(\"finale state: \", test.states)"
]
},
{
@@ -728,29 +746,42 @@
],
"source": [
"import numpy as np\n",
+ "\n",
"clearNames()\n",
- "x = Input('x', dimensions=3)\n",
- "x_s = Input('x_s', dimensions=3)\n",
- "y_s = Input('y_s', dimensions=3)\n",
+ "x = Input(\"x\", dimensions=3)\n",
+ "x_s = Input(\"x_s\", dimensions=3)\n",
+ "y_s = Input(\"y_s\", dimensions=3)\n",
"x_out = Linear(output_dimension=3)(x_s.tw(0.5))\n",
"y_out = Linear(output_dimension=3)(y_s.tw(0.5))\n",
- "out = Output('out',x_out+y_out)\n",
- "out_x = Output('out_x',x_out)\n",
- "out_y = Output('out_y',y_out)\n",
+ "out = Output(\"out\", x_out + y_out)\n",
+ "out_x = Output(\"out_x\", x_out)\n",
+ "out_y = Output(\"out_y\", y_out)\n",
"\n",
"test = Modely(seed=42)\n",
- "test.addModel('model', [out,out_x,out_y])\n",
- "test.addMinimize('error', out, x.tw(0.5))\n",
+ "test.addModel(\"model\", [out, out_x, out_y])\n",
+ "test.addMinimize(\"error\", out, x.tw(0.5))\n",
"\n",
"test.neuralizeModel(0.1)\n",
"\n",
- "dataset = {'x':np.array([np.random.uniform(1,4,300)]).reshape(100,3).tolist()}\n",
- "test.loadData(name='dataset', source=dataset)\n",
+ "dataset = {\"x\": np.array([np.random.uniform(1, 4, 300)]).reshape(100, 3).tolist()}\n",
+ "test.loadData(name=\"dataset\", source=dataset)\n",
"\n",
"# Training non ricorrente\n",
- "params = {'num_of_epochs': 10, 'train_batch_size': 4, 'val_batch_size':4, 'test_batch_size':1, 'lr':0.01}\n",
- "test.trainModel(splits=[70,20,10], prediction_samples=3, shuffle_data=False, closed_loop={'x_s':'out_x','y_s':'out_y'}, training_params=params)\n",
- "print('finale state: ', test.states)"
+ "params = {\n",
+ " \"num_of_epochs\": 10,\n",
+ " \"train_batch_size\": 4,\n",
+ " \"val_batch_size\": 4,\n",
+ " \"test_batch_size\": 1,\n",
+ " \"lr\": 0.01,\n",
+ "}\n",
+ "test.trainModel(\n",
+ " splits=[70, 20, 10],\n",
+ " prediction_samples=3,\n",
+ " shuffle_data=False,\n",
+ " closed_loop={\"x_s\": \"out_x\", \"y_s\": \"out_y\"},\n",
+ " training_params=params,\n",
+ ")\n",
+ "print(\"finale state: \", test.states)"
]
}
],
diff --git a/docs/_autodoc/tutorials/examples/training.ipynb b/docs/_autodoc/tutorials/examples/training.ipynb
index 2a29fa7f..085eed9e 100644
--- a/docs/_autodoc/tutorials/examples/training.ipynb
+++ b/docs/_autodoc/tutorials/examples/training.ipynb
@@ -203,19 +203,19 @@
],
"source": [
"## Create a neural network model and Load a dataset\n",
- "in1 = Input('in1')\n",
- "target = Input('target')\n",
+ "in1 = Input(\"in1\")\n",
+ "target = Input(\"target\")\n",
"relation = Fir(in1.tw(0.05))\n",
- "output = Output('out', relation)\n",
+ "output = Output(\"out\", relation)\n",
"\n",
"model = Modely(visualizer=TextVisualizer())\n",
- "model.addMinimize('error', output, target.last())\n",
- "model.addModel('model', output)\n",
+ "model.addMinimize(\"error\", output, target.last())\n",
+ "model.addModel(\"model\", output)\n",
"model.neuralizeModel(0.01)\n",
"\n",
- "train_folder = 'data'\n",
- "data_struct = ['in1', '', 'target']\n",
- "model.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=1)"
+ "train_folder = \"data\"\n",
+ "data_struct = [\"in1\", \"\", \"target\"]\n",
+ "model.loadData(name=\"dataset\", source=train_folder, format=data_struct, skiplines=1)"
]
},
{
@@ -384,7 +384,14 @@
}
],
"source": [
- "training_parameters = model.trainModel(models='model', train_dataset='dataset', num_of_epochs=10, lr=0.01, shuffle_data=True, train_batch_size=4)"
+ "training_parameters = model.trainModel(\n",
+ " models=\"model\",\n",
+ " train_dataset=\"dataset\",\n",
+ " num_of_epochs=10,\n",
+ " lr=0.01,\n",
+ " shuffle_data=True,\n",
+ " train_batch_size=4,\n",
+ ")"
]
},
{
@@ -414,7 +421,14 @@
}
],
"source": [
- "training_parameters = model.trainModel(models='model', dataset='dataset', splits=[70, 20, 10], num_of_epochs=10, shuffle_data=True, train_batch_size=4)"
+ "training_parameters = model.trainModel(\n",
+ " models=\"model\",\n",
+ " dataset=\"dataset\",\n",
+ " splits=[70, 20, 10],\n",
+ " num_of_epochs=10,\n",
+ " shuffle_data=True,\n",
+ " train_batch_size=4,\n",
+ ")"
]
},
{
@@ -479,19 +493,43 @@
}
],
"source": [
- "target = Input('target')\n",
- "x = Input('x')\n",
+ "target = Input(\"target\")\n",
+ "x = Input(\"x\")\n",
"relation = Fir(x.last())\n",
"relation.closedLoop(x)\n",
- "output = Output('out', relation)\n",
+ "output = Output(\"out\", relation)\n",
"\n",
"test = Modely(visualizer=TextVisualizer(), seed=42)\n",
- "test.addModel('model', output)\n",
- "test.addMinimize('error', target.next(), relation)\n",
+ "test.addModel(\"model\", output)\n",
+ "test.addMinimize(\"error\", target.next(), relation)\n",
"test.neuralizeModel(0.01)\n",
"\n",
- "dataset = {'x': [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], 'target': [21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]}\n",
- "test.loadData(name='dataset', source=dataset)"
+ "dataset = {\n",
+ " \"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],\n",
+ " \"target\": [\n",
+ " 21,\n",
+ " 22,\n",
+ " 23,\n",
+ " 24,\n",
+ " 25,\n",
+ " 26,\n",
+ " 27,\n",
+ " 28,\n",
+ " 29,\n",
+ " 30,\n",
+ " 31,\n",
+ " 32,\n",
+ " 33,\n",
+ " 34,\n",
+ " 35,\n",
+ " 36,\n",
+ " 37,\n",
+ " 38,\n",
+ " 39,\n",
+ " 40,\n",
+ " ],\n",
+ "}\n",
+ "test.loadData(name=\"dataset\", source=dataset)"
]
},
{
@@ -548,7 +586,15 @@
}
],
"source": [
- "training_parameters = test.trainModel(train_dataset='dataset', lr=0.01, num_of_epochs=10, train_batch_size=4, prediction_samples=2, step=1, shuffle_data=True)"
+ "training_parameters = test.trainModel(\n",
+ " train_dataset=\"dataset\",\n",
+ " lr=0.01,\n",
+ " num_of_epochs=10,\n",
+ " train_batch_size=4,\n",
+ " prediction_samples=2,\n",
+ " step=1,\n",
+ " shuffle_data=True,\n",
+ ")"
]
},
{
@@ -615,7 +661,9 @@
}
],
"source": [
- "training_parameters = test.trainModel(train_dataset='dataset', minimize_gain={'error':0})"
+ "training_parameters = test.trainModel(\n",
+ " train_dataset=\"dataset\", minimize_gain={\"error\": 0}\n",
+ ")"
]
},
{
@@ -683,12 +731,14 @@
}
],
"source": [
- "optimizer_defaults = {\n",
- " 'lr': 0.1,\n",
- " 'betas': (0.5, 0.99)\n",
- " }\n",
+ "optimizer_defaults = {\"lr\": 0.1, \"betas\": (0.5, 0.99)}\n",
"\n",
- "training_parameters = test.trainModel(train_dataset='dataset', optimizer='Adam', optimizer_defaults=optimizer_defaults, num_of_epochs=10)"
+ "training_parameters = test.trainModel(\n",
+ " train_dataset=\"dataset\",\n",
+ " optimizer=\"Adam\",\n",
+ " optimizer_defaults=optimizer_defaults,\n",
+ " num_of_epochs=10,\n",
+ ")"
]
},
{
@@ -759,7 +809,13 @@
],
"source": [
"from nnodely.support.earlystopping import early_stop_patience, select_best_model\n",
- "training_parameters = test.trainModel(train_dataset='dataset', minimize_gain={'error':0}, early_stopping=early_stop_patience, select_model=select_best_model)"
+ "\n",
+ "training_parameters = test.trainModel(\n",
+ " train_dataset=\"dataset\",\n",
+ " minimize_gain={\"error\": 0},\n",
+ " early_stopping=early_stop_patience,\n",
+ " select_model=select_best_model,\n",
+ ")"
]
},
{
@@ -863,12 +919,22 @@
}
],
"source": [
- "test_folder = 'data'\n",
- "data_struct = ['in1', '', 'target']\n",
- "model.loadData(name='dataset_test', source=test_folder, format=data_struct, skiplines=1)\n",
- "model.loadData(name='dataset_val', source=test_folder, format=data_struct, skiplines=1)\n",
+ "test_folder = \"data\"\n",
+ "data_struct = [\"in1\", \"\", \"target\"]\n",
+ "model.loadData(name=\"dataset_test\", source=test_folder, format=data_struct, skiplines=1)\n",
+ "model.loadData(name=\"dataset_val\", source=test_folder, format=data_struct, skiplines=1)\n",
"\n",
- "training_parameters = model.trainAndAnalyze(models='model', train_dataset='dataset', validation_dataset='dataset_val', train_batch_size=4, test_dataset='dataset_test', test_batch_size=1, num_of_epochs=10, lr=0.01, shuffle_data=True)"
+ "training_parameters = model.trainAndAnalyze(\n",
+ " models=\"model\",\n",
+ " train_dataset=\"dataset\",\n",
+ " validation_dataset=\"dataset_val\",\n",
+ " train_batch_size=4,\n",
+ " test_dataset=\"dataset_test\",\n",
+ " test_batch_size=1,\n",
+ " num_of_epochs=10,\n",
+ " lr=0.01,\n",
+ " shuffle_data=True,\n",
+ ")"
]
},
{
@@ -924,6 +990,7 @@
],
"source": [
"import pprint\n",
+ "\n",
"print(\"Training completed with parameters:\", pprint.pprint(training_parameters))"
]
}
diff --git a/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv b/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv
index 5625a587..3d80578b 100644
--- a/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv
+++ b/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv
@@ -98,4 +98,4 @@
13.473333333333336,12.779200000000001,0.,5,-0.7460835566787409,-0.6602761975976819,-0.5746327099489577,-0.48922828574131927,-0.4061226600199461,-0.3259584416340431,-0.2506976788511679,-0.18109245144700026,-0.11566989947226602,-0.05551166846061051,0.,0.04959411345004128,0.09326429982348827,0.12985450593151882,0.16023045483507303,0.1832476556463689,0.20626485645772163,0.22928205726901751,0.2522992580803134,0.2753164588916661,0.298333659702962,0.09911902783658734
13.474444444444448,13.0928,0.,5,-0.7485232229184362,-0.6627158638373771,-0.5770452306900893,-0.4916408064824509,-0.4081821510060877,-0.32801793262018464,-0.25204581648097246,-0.18244058907686167,-0.11638159820341798,-0.05622336719181931,0.,0.04959411345004128,0.09414036583672214,0.13073057194475268,0.162020926674586,0.1850381274858819,0.2080553282972346,0.2310725291085305,0.25408972991982637,0.2771069307311791,0.30012413154247497,0.11169376341230414
13.496111111111112,13.014400000000002,0.,5,-0.7509649513013414,-0.6651575922202255,-0.5794597906294712,-0.4940553664218328,-0.41024338278987216,-0.3300791644039691,-0.2533950936325482,-0.1837898662284374,-0.11709389850244634,-0.05693566749079082,0.,0.04959411345004128,0.09501717235025353,0.13160737845828407,0.16381291192163872,0.1868301127329346,0.20984731354423047,0.2328645143555832,0.2558817151668791,0.2788989159782318,0.3019161167895277,0.1517397792930878
-13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794
\ No newline at end of file
+13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794
diff --git a/docs/_autodoc/validation/validator_module.rst b/docs/_autodoc/validation/validator_module.rst
index eb9284bb..0382b11a 100644
--- a/docs/_autodoc/validation/validator_module.rst
+++ b/docs/_autodoc/validation/validator_module.rst
@@ -5,4 +5,4 @@ Validation Module
:undoc-members:
:no-inherited-members:
-.. autofunction:: nnodely.operators.validator.Validator.analyzeModel
\ No newline at end of file
+.. autofunction:: nnodely.operators.validator.Validator.analyzeModel
diff --git a/docs/conf.py b/docs/conf.py
index 1d6e9dda..28c8aec6 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -4,29 +4,36 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
import os
+
+
def read_version():
- version_file = os.path.join(os.path.dirname(__file__), '..', 'nnodely', '__init__.py')
- with open(version_file, 'r') as f:
+ version_file = os.path.join(
+ os.path.dirname(__file__), "..", "nnodely", "__init__.py"
+ )
+ with open(version_file, "r") as f:
for line in f:
- if line.startswith('__version__'):
+ if line.startswith("__version__"):
delim = '"' if '"' in line else "'"
return line.split(delim)[1]
raise RuntimeError("Unable to find version string.")
+
def skip_nnodely(app, what, name, obj, skip, options):
# Skip the alias nnodely
if name == "nnodely":
return True # esclude questo membro dalla documentazione
return skip
+
def setup(app):
app.connect("autodoc-skip-member", skip_nnodely)
+
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = __package__
-author = 'tonegas'
+author = "tonegas"
release = read_version()
version = read_version()
@@ -34,12 +41,12 @@ def setup(app):
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
-# 'sphinx.ext.autodoc',
- 'sphinx.ext.napoleon',
- 'sphinx.ext.viewcode',
- 'sphinx.ext.mathjax',
- 'myst_parser',
- 'nbsphinx',
+ # 'sphinx.ext.autodoc',
+ "sphinx.ext.napoleon",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.mathjax",
+ "myst_parser",
+ "nbsphinx",
]
templates_path = []
@@ -60,8 +67,8 @@ def setup(app):
}
-html_static_path = ['_static']
-html_logo = '_static/logo.png'
+html_static_path = ["_static"]
+html_logo = "_static/logo.png"
# -- Options for EPUB output -------------------------------------------------
-epub_copyright = '2024, tonegas' # Add this line
+epub_copyright = "2024, tonegas" # Add this line
diff --git a/docs/examples_basics/compser_module_ex/addClosedLoop.rst b/docs/examples_basics/compser_module_ex/addClosedLoop.rst
index 17ed8fdf..a00e934d 100644
--- a/docs/examples_basics/compser_module_ex/addClosedLoop.rst
+++ b/docs/examples_basics/compser_module_ex/addClosedLoop.rst
@@ -5,4 +5,3 @@
y = Input('y')
relation = Fir(x.last())
model.addClosedLoop(relation, y)
-
\ No newline at end of file
diff --git a/docs/examples_basics/compser_module_ex/addConnect.rst b/docs/examples_basics/compser_module_ex/addConnect.rst
index f1a96826..89e0fab6 100644
--- a/docs/examples_basics/compser_module_ex/addConnect.rst
+++ b/docs/examples_basics/compser_module_ex/addConnect.rst
@@ -4,4 +4,4 @@
x = Input('x')
y = Input('y')
relation = Fir(x.last())
- model.addConnect(relation, y)
\ No newline at end of file
+ model.addConnect(relation, y)
diff --git a/docs/examples_basics/compser_module_ex/addModel.rst b/docs/examples_basics/compser_module_ex/addModel.rst
index ad9f410f..5cb910f5 100644
--- a/docs/examples_basics/compser_module_ex/addModel.rst
+++ b/docs/examples_basics/compser_module_ex/addModel.rst
@@ -3,4 +3,4 @@
model = Modely()
x = Input('x')
out = Output('out', Fir(x.last()))
- model.addModel('example_model', [out])
\ No newline at end of file
+ model.addModel('example_model', [out])
diff --git a/docs/examples_basics/compser_module_ex/neuralizeModel.rst b/docs/examples_basics/compser_module_ex/neuralizeModel.rst
index a0f66cc0..6795679d 100644
--- a/docs/examples_basics/compser_module_ex/neuralizeModel.rst
+++ b/docs/examples_basics/compser_module_ex/neuralizeModel.rst
@@ -1,4 +1,4 @@
.. code-block:: python
-
+
model = Modely(name='example_model')
- model.neuralizeModel(sample_time=0.1, clear_model=True)
\ No newline at end of file
+ model.neuralizeModel(sample_time=0.1, clear_model=True)
diff --git a/docs/examples_basics/compser_module_ex/removeConnection.rst b/docs/examples_basics/compser_module_ex/removeConnection.rst
index ca03e376..8d59d09b 100644
--- a/docs/examples_basics/compser_module_ex/removeConnection.rst
+++ b/docs/examples_basics/compser_module_ex/removeConnection.rst
@@ -5,4 +5,4 @@
y = Input('y')
relation = Fir(x.last())
model.addConnect(relation, y)
- model.removeConnection(y)
\ No newline at end of file
+ model.removeConnection(y)
diff --git a/docs/examples_basics/compser_module_ex/removeModel.rst b/docs/examples_basics/compser_module_ex/removeModel.rst
index 11e4652a..c9d853a2 100644
--- a/docs/examples_basics/compser_module_ex/removeModel.rst
+++ b/docs/examples_basics/compser_module_ex/removeModel.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- model.removeModel(['sub_model1', 'sub_model2'])
\ No newline at end of file
+ model.removeModel(['sub_model1', 'sub_model2'])
diff --git a/docs/examples_basics/export_module_ex/exportONNX.rst b/docs/examples_basics/export_module_ex/exportONNX.rst
index ba84c3c2..120879aa 100644
--- a/docs/examples_basics/export_module_ex/exportONNX.rst
+++ b/docs/examples_basics/export_module_ex/exportONNX.rst
@@ -6,4 +6,4 @@
model = Modely()
model.neuralizeModel()
- model.exportONNX(inputs_order=['input1', 'input2'], outputs_order=['output1'], name='example_model', model_folder='path/to/export')
\ No newline at end of file
+ model.exportONNX(inputs_order=['input1', 'input2'], outputs_order=['output1'], name='example_model', model_folder='path/to/export')
diff --git a/docs/examples_basics/export_module_ex/exportPythonModel.rst b/docs/examples_basics/export_module_ex/exportPythonModel.rst
index 172f2258..7ad98f22 100644
--- a/docs/examples_basics/export_module_ex/exportPythonModel.rst
+++ b/docs/examples_basics/export_module_ex/exportPythonModel.rst
@@ -2,4 +2,4 @@
model = Modely(name='example_model')
model.neuralizeModel()
- model.exportPythonModel(name='example_model', model_folder='folder/')
\ No newline at end of file
+ model.exportPythonModel(name='example_model', model_folder='folder/')
diff --git a/docs/examples_basics/export_module_ex/exportReport.rst b/docs/examples_basics/export_module_ex/exportReport.rst
index 9e5483ce..79de92ac 100644
--- a/docs/examples_basics/export_module_ex/exportReport.rst
+++ b/docs/examples_basics/export_module_ex/exportReport.rst
@@ -3,4 +3,4 @@
model = Modely()
model.neuralizeModel()
model.trainModel(train_dataset='train_dataset', validation_dataset='val_dataset', num_of_epochs=10)
- model.exportReport(name='example_model', model_folder='path/to/export')
\ No newline at end of file
+ model.exportReport(name='example_model', model_folder='path/to/export')
diff --git a/docs/examples_basics/export_module_ex/importPythonModel.rst b/docs/examples_basics/export_module_ex/importPythonModel.rst
index a85613f4..21d8ab85 100644
--- a/docs/examples_basics/export_module_ex/importPythonModel.rst
+++ b/docs/examples_basics/export_module_ex/importPythonModel.rst
@@ -1,4 +1,4 @@
.. code-block:: python
model = Modely()
- model.importPythonModel(name='example_model', model_folder='path/to/import')
\ No newline at end of file
+ model.importPythonModel(name='example_model', model_folder='path/to/import')
diff --git a/docs/examples_basics/export_module_ex/loadModel.rst b/docs/examples_basics/export_module_ex/loadModel.rst
index dfcddf2f..2b70c2db 100644
--- a/docs/examples_basics/export_module_ex/loadModel.rst
+++ b/docs/examples_basics/export_module_ex/loadModel.rst
@@ -1,4 +1,4 @@
.. code-block:: python
model = Modely()
- model.loadModel(name='example_model', model_folder='path/to/load')
\ No newline at end of file
+ model.loadModel(name='example_model', model_folder='path/to/load')
diff --git a/docs/examples_basics/export_module_ex/loadTorchModel.rst b/docs/examples_basics/export_module_ex/loadTorchModel.rst
index 5f6fff2d..bcbf93f1 100644
--- a/docs/examples_basics/export_module_ex/loadTorchModel.rst
+++ b/docs/examples_basics/export_module_ex/loadTorchModel.rst
@@ -2,4 +2,4 @@
model = Modely()
model.neuralizeModel()
- model.loadTorchModel(name='example_model', model_folder='path/to/load')
\ No newline at end of file
+ model.loadTorchModel(name='example_model', model_folder='path/to/load')
diff --git a/docs/examples_basics/export_module_ex/onnxInference.rst b/docs/examples_basics/export_module_ex/onnxInference.rst
index 3f9b4091..c627a2db 100644
--- a/docs/examples_basics/export_module_ex/onnxInference.rst
+++ b/docs/examples_basics/export_module_ex/onnxInference.rst
@@ -33,4 +33,4 @@ Example - Recurrent:
'y': np.ones(shape=(1, 1, 1)).astype(np.float32)
}
- predictions = Modely().onnxInference(dummy_input, model_folder)
\ No newline at end of file
+ predictions = Modely().onnxInference(dummy_input, model_folder)
diff --git a/docs/examples_basics/export_module_ex/saveModel.rst b/docs/examples_basics/export_module_ex/saveModel.rst
index bde2f989..74ab3185 100644
--- a/docs/examples_basics/export_module_ex/saveModel.rst
+++ b/docs/examples_basics/export_module_ex/saveModel.rst
@@ -2,4 +2,4 @@
model = Modely()
model.neuralizeModel()
- model.saveModel(name='example_model', model_folder='folder/')
\ No newline at end of file
+ model.saveModel(name='example_model', model_folder='folder/')
diff --git a/docs/examples_basics/export_module_ex/saveTorchModel.rst b/docs/examples_basics/export_module_ex/saveTorchModel.rst
index 9f2017ef..29188790 100644
--- a/docs/examples_basics/export_module_ex/saveTorchModel.rst
+++ b/docs/examples_basics/export_module_ex/saveTorchModel.rst
@@ -2,4 +2,4 @@
model = Modely()
model.neuralizeModel()
- model.saveTorchModel(name='example_model', model_folder='path/to/save')
\ No newline at end of file
+ model.saveTorchModel(name='example_model', model_folder='path/to/save')
diff --git a/docs/examples_basics/inference_module_ex/inference.rst b/docs/examples_basics/inference_module_ex/inference.rst
index c7cc4a89..81a34023 100644
--- a/docs/examples_basics/inference_module_ex/inference.rst
+++ b/docs/examples_basics/inference_module_ex/inference.rst
@@ -5,4 +5,4 @@
out = Output('out', Fir(x.last()))
model.addModel('example_model', [out])
model.neuralizeModel()
- predictions = model(inputs={'x': [1, 2, 3]})
\ No newline at end of file
+ predictions = model(inputs={'x': [1, 2, 3]})
diff --git a/docs/examples_basics/input_module_ex/z.rst b/docs/examples_basics/input_module_ex/z.rst
index a3509cda..5da83cc6 100644
--- a/docs/examples_basics/input_module_ex/z.rst
+++ b/docs/examples_basics/input_module_ex/z.rst
@@ -8,4 +8,4 @@ where the time vector 0 represents the last passed instant.
T.z(-1) # = 1
T.z(0) # = 0 # the last passed instant
- T.z(2) # = -2
\ No newline at end of file
+ T.z(2) # = -2
diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst
index 6ee67c07..eed00e15 100644
--- a/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst
+++ b/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- x = ELU(x)
\ No newline at end of file
+ x = ELU(x)
diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst
index f384ed96..82597e0b 100644
--- a/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst
+++ b/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- x = Identity(x)
\ No newline at end of file
+ x = Identity(x)
diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst
index 7c4d311f..e5ba89b8 100644
--- a/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst
+++ b/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- x = Relu(x)
\ No newline at end of file
+ x = Relu(x)
diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst
index 6810a2fc..64c9e56e 100644
--- a/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst
+++ b/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- x = Sigmoid(x)
\ No newline at end of file
+ x = Sigmoid(x)
diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst
index 1126dd3a..d3a14be8 100644
--- a/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst
+++ b/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- x = Softmax(x)
\ No newline at end of file
+ x = Softmax(x)
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst
index 227b1ec5..324d6e0a 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst
@@ -2,4 +2,4 @@
add = Add(relation1, relation2)
# or
- add = relation1 + relation2
\ No newline at end of file
+ add = relation1 + relation2
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst
index d3a20424..ebd84aac 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst
@@ -2,4 +2,4 @@
div = Div(relation1, relation2)
# or
- div = relation1 / relation2
\ No newline at end of file
+ div = relation1 / relation2
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst
index 547b9164..d5b556bf 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst
@@ -2,4 +2,4 @@
mul = Mul(relation1, relation2)
# or
- mul = relation1 * relation2
\ No newline at end of file
+ mul = relation1 * relation2
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst
index 68ce98a9..2326e9d8 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst
@@ -1,3 +1,3 @@
.. code-block :: python
- x = Neg(x)
\ No newline at end of file
+ x = Neg(x)
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst
index b7ae9085..c342a3e4 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst
@@ -2,4 +2,4 @@
pow = Pow(relation, exp)
# or
- pow = relation1 ** relation2
\ No newline at end of file
+ pow = relation1 ** relation2
diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst
index e10dcfad..ff47ab7c 100644
--- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst
+++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst
@@ -2,4 +2,4 @@
sub = Sub(relation1, relation2)
# or
- sub = relation1 - relation2
\ No newline at end of file
+ sub = relation1 - relation2
diff --git a/docs/examples_basics/layer_module_ex/fir.rst b/docs/examples_basics/layer_module_ex/fir.rst
index aab246cc..e140e08c 100644
--- a/docs/examples_basics/layer_module_ex/fir.rst
+++ b/docs/examples_basics/layer_module_ex/fir.rst
@@ -16,8 +16,8 @@ Passing a parameter:
Parameters initialization:
.. code-block:: python
-
+
x = Input('x')
F = Input('F')
fir_x = Fir(W_init='init_negexp')(x.tw(0.2))
- fir_F = Fir(W_init='init_constant', W_init_params={'value':1})(F.last())
\ No newline at end of file
+ fir_F = Fir(W_init='init_constant', W_init_params={'value':1})(F.last())
diff --git a/docs/examples_basics/layer_module_ex/part_module/part.rst b/docs/examples_basics/layer_module_ex/part_module/part.rst
index fb05be16..80b30327 100644
--- a/docs/examples_basics/layer_module_ex/part_module/part.rst
+++ b/docs/examples_basics/layer_module_ex/part_module/part.rst
@@ -1,4 +1,4 @@
.. code-block:: python
-
+
x = Input('x', dimensions=3).last()
- relation = Part(x, 0, 1)
\ No newline at end of file
+ relation = Part(x, 0, 1)
diff --git a/docs/examples_basics/layer_module_ex/part_module/sample_part.rst b/docs/examples_basics/layer_module_ex/part_module/sample_part.rst
index 496eeeae..3ce85bc8 100644
--- a/docs/examples_basics/layer_module_ex/part_module/sample_part.rst
+++ b/docs/examples_basics/layer_module_ex/part_module/sample_part.rst
@@ -1,4 +1,4 @@
.. code-block:: python
-
+
x = Input('x').sw(3)
- relation = SamplePart(x, 0, 1)
\ No newline at end of file
+ relation = SamplePart(x, 0, 1)
diff --git a/docs/examples_basics/layer_module_ex/part_module/sample_select.rst b/docs/examples_basics/layer_module_ex/part_module/sample_select.rst
index a531ba1b..e9e40a50 100644
--- a/docs/examples_basics/layer_module_ex/part_module/sample_select.rst
+++ b/docs/examples_basics/layer_module_ex/part_module/sample_select.rst
@@ -1,4 +1,4 @@
.. code-block:: python
x = Input('x').sw(3)
- relation = SampleSelect(x, 1)
\ No newline at end of file
+ relation = SampleSelect(x, 1)
diff --git a/docs/examples_basics/layer_module_ex/part_module/select.rst b/docs/examples_basics/layer_module_ex/part_module/select.rst
index 13cf44f4..8bcc809c 100644
--- a/docs/examples_basics/layer_module_ex/part_module/select.rst
+++ b/docs/examples_basics/layer_module_ex/part_module/select.rst
@@ -1,4 +1,4 @@
.. code-block:: python
x = Input('x', dimensions=3).last()
- relation = Select(x, 1)
\ No newline at end of file
+ relation = Select(x, 1)
diff --git a/docs/examples_basics/layer_module_ex/part_module/time_part.rst b/docs/examples_basics/layer_module_ex/part_module/time_part.rst
index 87a3297b..08d66d61 100644
--- a/docs/examples_basics/layer_module_ex/part_module/time_part.rst
+++ b/docs/examples_basics/layer_module_ex/part_module/time_part.rst
@@ -1,4 +1,4 @@
.. code-block:: python
x = Input('x').sw(10)
- time_part = TimePart(x, i=0, j=5)
\ No newline at end of file
+ time_part = TimePart(x, i=0, j=5)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst
index ba879cb6..304216ff 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- cos = Cos(relation)
\ No newline at end of file
+ cos = Cos(relation)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst
index 1f1852d4..975a677c 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- cosh = Cosh(relation)
\ No newline at end of file
+ cosh = Cosh(relation)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst
index c3445322..76c4e6fb 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- sech = Sech(relation)
\ No newline at end of file
+ sech = Sech(relation)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst
index 1ee6cb4e..1f64a417 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- sin = Sin(relation)
\ No newline at end of file
+ sin = Sin(relation)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst
index 420ff4c1..c816c5ac 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- tan = Tan(relation)
\ No newline at end of file
+ tan = Tan(relation)
diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst
index bc3285c6..4191bf64 100644
--- a/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst
+++ b/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- tanh = Tanh(relation)
\ No newline at end of file
+ tanh = Tanh(relation)
diff --git a/docs/examples_basics/parameter_module_ex/sample_time.rst b/docs/examples_basics/parameter_module_ex/sample_time.rst
index 994f1290..171b6e00 100644
--- a/docs/examples_basics/parameter_module_ex/sample_time.rst
+++ b/docs/examples_basics/parameter_module_ex/sample_time.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- dt = SampleTime()
\ No newline at end of file
+ dt = SampleTime()
diff --git a/docs/examples_basics/trainer_module_ex/addMinimize.rst b/docs/examples_basics/trainer_module_ex/addMinimize.rst
index a61c02a6..941f46b8 100644
--- a/docs/examples_basics/trainer_module_ex/addMinimize.rst
+++ b/docs/examples_basics/trainer_module_ex/addMinimize.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- model.addMinimize('minimize_op', streamA, streamB, loss_function='mse')
\ No newline at end of file
+ model.addMinimize('minimize_op', streamA, streamB, loss_function='mse')
diff --git a/docs/examples_basics/trainer_module_ex/removeMinimize.rst b/docs/examples_basics/trainer_module_ex/removeMinimize.rst
index 7999e263..3f66f480 100644
--- a/docs/examples_basics/trainer_module_ex/removeMinimize.rst
+++ b/docs/examples_basics/trainer_module_ex/removeMinimize.rst
@@ -1,3 +1,3 @@
.. code-block:: python
- model.removeMinimize(['minimize_op1', 'minimize_op2'])
\ No newline at end of file
+ model.removeMinimize(['minimize_op1', 'minimize_op2'])
diff --git a/docs/examples_basics/trainer_module_ex/trainModel.rst b/docs/examples_basics/trainer_module_ex/trainModel.rst
index a835dea3..57d55385 100644
--- a/docs/examples_basics/trainer_module_ex/trainModel.rst
+++ b/docs/examples_basics/trainer_module_ex/trainModel.rst
@@ -36,4 +36,4 @@ Example - recurrent training:
mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';')
params = {'num_of_epochs': 100, 'train_batch_size': 128, 'lr': 0.001}
- mass_spring_damper.trainModel(splits=[70, 20, 10], prediction_samples=10, training_params=params)
\ No newline at end of file
+ mass_spring_damper.trainModel(splits=[70, 20, 10], prediction_samples=10, training_params=params)
diff --git a/docs/index.rst b/docs/index.rst
index d0b78a50..1497c358 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -8,15 +8,15 @@ Welcome to nnodely's documentation!
.. image:: https://raw.githubusercontent.com/tonegas/nnodely/main/imgs/logo_white_info.png
:target: https://github.com/tonegas/nnodely
- :alt: Open
+ :alt: Open
-*nnodely* is a framework designed to facilitate the creation and deployment of **Model-Structured Neural Networks** (**MSNNs**).
-Modeling, control, and estimation of physical systems impose constraints that differ fundamentally from typical
-deep-learning tasks (e.g., images or text). In engineering applications, models often need to respect
-known physical laws or constraints, operate in real time, remain interpretable, and generalize reliably
-even when only limited experimental data are available. MS-NNs combine the learning capabilities of neural
-networks with structural priors grounded in physics, control and estimation theory, enabling:
+*nnodely* is a framework designed to facilitate the creation and deployment of **Model-Structured Neural Networks** (**MSNNs**).
+Modeling, control, and estimation of physical systems impose constraints that differ fundamentally from typical
+deep-learning tasks (e.g., images or text). In engineering applications, models often need to respect
+known physical laws or constraints, operate in real time, remain interpretable, and generalize reliably
+even when only limited experimental data are available. MS-NNs combine the learning capabilities of neural
+networks with structural priors grounded in physics, control and estimation theory, enabling:
- **Data Efficiency**: By embedding structural priors, MS-NNs can learn effectively from limited data, reducing the need for extensive datasets.
@@ -66,7 +66,7 @@ Overview
.. sidebar:: Overview
-
+
Overview of the *nnodely* development pipeline. It spans model design (:ref:`PH1 `), dataset construction aligned with the network architecture (:ref:`PH2 `), training (:ref:`PH3 `), domain-specific validation (:ref:`PH4 `), model export (:ref:`PH5 `), and composition of complex models (:ref:`PH6 `). Ellipses indicate the pipeline phases, while rectangles denote the artifacts produced at each phase.
@@ -99,4 +99,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
-
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 8e66700e..415be759 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -3,4 +3,4 @@ myst_parser
nbsphinx
pandoc
jupyter
-jupyterlab
\ No newline at end of file
+jupyterlab
diff --git a/mplplots/__init__.py b/mplplots/__init__.py
index eb1f48f8..9d18dea1 100644
--- a/mplplots/__init__.py
+++ b/mplplots/__init__.py
@@ -1 +1,7 @@
-from mplplots.plots import plot_training, plot_results, plot_fuzzy, plot_2d_function, plot_3d_function
\ No newline at end of file
+from mplplots.plots import (
+ plot_training,
+ plot_results,
+ plot_fuzzy,
+ plot_2d_function,
+ plot_3d_function,
+)
diff --git a/mplplots/plots.py b/mplplots/plots.py
index 9eea6db0..b5babfee 100644
--- a/mplplots/plots.py
+++ b/mplplots/plots.py
@@ -2,22 +2,30 @@
import matplotlib.colors as mcolors
-def plot_training(ax, title, key, data_train, data_val = None, last = None):
+
+def plot_training(ax, title, key, data_train, data_val=None, last=None):
# Plot data
if last is not None:
- ax.set_title(f'{title} - epochs last {last}')
+ ax.set_title(f"{title} - epochs last {last}")
else:
- ax.set_title(f'{title}')
+ ax.set_title(f"{title}")
- ax.plot([i + 1 for i in range(len(data_train))], data_train, label=f'Train loss {key}')
+ ax.plot(
+ [i + 1 for i in range(len(data_train))], data_train, label=f"Train loss {key}"
+ )
if data_val:
- ax.plot([i + 1 for i in range(len(data_val))], data_val, '-.', label=f'Validation loss {key}')
+ ax.plot(
+ [i + 1 for i in range(len(data_val))],
+ data_val,
+ "-.",
+ label=f"Validation loss {key}",
+ )
- ax.set_yscale('log')
+ ax.set_yscale("log")
ax.grid(True)
- ax.legend(loc='best')
- ax.set_xlabel('Epochs')
- ax.set_ylabel('Loss')
+ ax.legend(loc="best")
+ ax.set_xlabel("Epochs")
+ ax.set_ylabel("Loss")
# Set plot limits
data_train = np.nan_to_num(data_train, nan=np.nan, posinf=np.nan, neginf=np.nan)
if data_val:
@@ -32,13 +40,13 @@ def plot_training(ax, title, key, data_train, data_val = None, last = None):
def plot_results(ax, name_data, key, A, B, data_idxs, sample_time):
# Plot data
- ax.set_title(f'{key} on the dataset {name_data}')
+ ax.set_title(f"{key} on the dataset {name_data}")
A_t = np.transpose(np.array(A))
B_t = np.transpose(np.array(B))
idxs_t = np.transpose(np.array(data_idxs))
- color_A = 'tab:blue'
+ color_A = "tab:blue"
rgb_A = mcolors.to_rgb(color_A)
- color_B = 'tab:orange'
+ color_B = "tab:orange"
rgb_B = mcolors.to_rgb(color_B)
delta = 0.1
@@ -47,64 +55,136 @@ def plot_results(ax, name_data, key, A, B, data_idxs, sample_time):
time_array = []
num_samples = A_t.shape[2]
for o in range(A_t.shape[1]):
- time_array.append(np.linspace(0, (num_samples - 1) * sample_time, num_samples) + sample_time * o)
+ time_array.append(
+ np.linspace(0, (num_samples - 1) * sample_time, num_samples)
+ + sample_time * o
+ )
time_array = np.array(time_array)
for ind_dim in range(A_t.shape[0]):
# Print the marker only if the output have a time window
- ax.plot(time_array[0,:], A_t[ind_dim, 0, :],
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
- marker='s' if A_t.shape[1] > 1 else None, markersize=2,
- label=f'A_{ind_dim}')
- ax.plot(time_array[0,:], B_t[ind_dim, 0, :], '-.',
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
- marker='o' if A_t.shape[1] > 1 else None, markersize=2,
- label=f'B_{ind_dim}')
- if A_t.shape[1] > 1 :
- correlation = np.empty((A_t.shape[0],A_t.shape[2]))
+ ax.plot(
+ time_array[0, :],
+ A_t[ind_dim, 0, :],
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
+ marker="s" if A_t.shape[1] > 1 else None,
+ markersize=2,
+ label=f"A_{ind_dim}",
+ )
+ ax.plot(
+ time_array[0, :],
+ B_t[ind_dim, 0, :],
+ "-.",
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
+ marker="o" if A_t.shape[1] > 1 else None,
+ markersize=2,
+ label=f"B_{ind_dim}",
+ )
+ if A_t.shape[1] > 1:
+ correlation = np.empty((A_t.shape[0], A_t.shape[2]))
for ind_el in range(A_t.shape[2]):
- ax.plot(time_array[:,ind_el], A_t[ind_dim,:,ind_el], color=tuple((x + delta*ind_dim)%1.0001 for x in rgb_A))
- ax.plot(time_array[:,ind_el], B_t[ind_dim,:,ind_el], '-.', color=tuple((x + delta*ind_dim)%1.0001 for x in rgb_B))
- correlation[ind_dim, ind_el] = np.corrcoef(A_t[ind_dim,:,ind_el], B_t[ind_dim,:,ind_el])[0, 1]
- ax.text(0.05, 0.95 - 0.05 * ind_dim,
- f'Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=1)[ind_dim]:.2f}',
- transform=ax.transAxes, verticalalignment='top')
+ ax.plot(
+ time_array[:, ind_el],
+ A_t[ind_dim, :, ind_el],
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
+ )
+ ax.plot(
+ time_array[:, ind_el],
+ B_t[ind_dim, :, ind_el],
+ "-.",
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
+ )
+ correlation[ind_dim, ind_el] = np.corrcoef(
+ A_t[ind_dim, :, ind_el], B_t[ind_dim, :, ind_el]
+ )[0, 1]
+ ax.text(
+ 0.05,
+ 0.95 - 0.05 * ind_dim,
+ f"Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=1)[ind_dim]:.2f}",
+ transform=ax.transAxes,
+ verticalalignment="top",
+ )
else:
correlation = np.empty((A_t.shape[0],))
- correlation[ind_dim] = np.corrcoef(A_t[ind_dim, 0], B_t[ind_dim, 0])[0, 1]
- ax.text(0.05, 0.95-0.05*ind_dim, f'Correlation A_{ind_dim} - B_{ind_dim}: {correlation[ind_dim]:.2f}', transform=ax.transAxes, verticalalignment='top')
+ correlation[ind_dim] = np.corrcoef(A_t[ind_dim, 0], B_t[ind_dim, 0])[
+ 0, 1
+ ]
+ ax.text(
+ 0.05,
+ 0.95 - 0.05 * ind_dim,
+ f"Correlation A_{ind_dim} - B_{ind_dim}: {correlation[ind_dim]:.2f}",
+ transform=ax.transAxes,
+ verticalalignment="top",
+ )
else:
correlation = np.empty(A_t.shape)
for ind_dim in range(A_t.shape[0]):
first = True
- ax.scatter(idxs_t[:,0] * sample_time, A_t[ind_dim, 0, :, 0], marker='s', s=6,
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A))
- ax.scatter(idxs_t[:,0] * sample_time, B_t[ind_dim, 0, :, 0], marker='o', s=6,
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B))
+ ax.scatter(
+ idxs_t[:, 0] * sample_time,
+ A_t[ind_dim, 0, :, 0],
+ marker="s",
+ s=6,
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
+ )
+ ax.scatter(
+ idxs_t[:, 0] * sample_time,
+ B_t[ind_dim, 0, :, 0],
+ marker="o",
+ s=6,
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
+ )
for ind_el in range(A_t.shape[2]):
time_array = idxs_t[ind_el] * sample_time
# Print the marker only if the output have a time window
- ax.plot(time_array, A_t[ind_dim, 0, ind_el], marker = 's' if A_t.shape[1] > 1 else None, markersize=2,
+ ax.plot(
+ time_array,
+ A_t[ind_dim, 0, ind_el],
+ marker="s" if A_t.shape[1] > 1 else None,
+ markersize=2,
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
+ label=f"A_{ind_dim}" if first else None,
+ )
+ ax.plot(
+ time_array,
+ B_t[ind_dim, 0, ind_el],
+ "-.",
+ marker="o" if A_t.shape[1] > 1 else None,
+ markersize=2,
+ color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
+ label=f"B_{ind_dim}" if first else None,
+ )
+ for ind_pred in range(A_t.shape[3]):
+ time_array = idxs_t[ind_el, ind_pred] * sample_time + np.linspace(
+ 0, (A_t.shape[1] - 1) * sample_time, A_t.shape[1]
+ )
+ ax.plot(
+ time_array,
+ A_t[ind_dim, :, ind_el, ind_pred],
color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A),
- label=f'A_{ind_dim}' if first else None)
- ax.plot(time_array, B_t[ind_dim, 0, ind_el], '-.', marker = 'o' if A_t.shape[1] > 1 else None, markersize=2,
+ )
+ ax.plot(
+ time_array,
+ B_t[ind_dim, :, ind_el, ind_pred],
+ "-.",
color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B),
- label=f'B_{ind_dim}' if first else None)
- for ind_pred in range(A_t.shape[3]):
- time_array = idxs_t[ind_el,ind_pred] * sample_time + np.linspace(0, (A_t.shape[1] - 1) * sample_time, A_t.shape[1])
- ax.plot(time_array, A_t[ind_dim, :, ind_el,ind_pred],
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A))
- ax.plot(time_array, B_t[ind_dim, :, ind_el,ind_pred], '-.',
- color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B))
+ )
first = False
for ind_win in range(A_t.shape[1]):
- correlation[ind_dim,ind_win,ind_el] = np.corrcoef(A_t[ind_dim,ind_win,ind_el], B_t[ind_dim,ind_win,ind_el])[0, 1]
- ax.text(0.05, 0.95-0.05*ind_dim, f'Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=(1, 2, 3))[ind_dim]:.2f}', transform=ax.transAxes, verticalalignment='top')
-
+ correlation[ind_dim, ind_win, ind_el] = np.corrcoef(
+ A_t[ind_dim, ind_win, ind_el], B_t[ind_dim, ind_win, ind_el]
+ )[0, 1]
+ ax.text(
+ 0.05,
+ 0.95 - 0.05 * ind_dim,
+ f"Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=(1, 2, 3))[ind_dim]:.2f}",
+ transform=ax.transAxes,
+ verticalalignment="top",
+ )
ax.grid(True)
- ax.legend(loc='best')
- ax.set_xlabel('Time [s]')
- ax.set_ylabel(f'Value {key}')
+ ax.legend(loc="best")
+ ax.set_xlabel("Time [s]")
+ ax.set_ylabel(f"Value {key}")
# min_val = min([min(A), min(B)])
# max_val = max([max(A), max(B)])
@@ -152,28 +232,38 @@ def plot_fuzzy(ax, name, x, y, chan_centers):
tableau_colors = mcolors.TABLEAU_COLORS
num_of_colors = len(list(tableau_colors.keys()))
for ind in range(len(y)):
- ax.axvline(x=chan_centers[ind], color=tableau_colors[list(tableau_colors.keys())[ind % num_of_colors]],
- linestyle='--')
- ax.plot(x, y[ind], label=f'Channel {int(ind) + 1}', linewidth=2)
- ax.legend(loc='best')
- ax.set_xlabel('Input')
- ax.set_ylabel('Value')
- ax.set_title(f'Function {name}')
+ ax.axvline(
+ x=chan_centers[ind],
+ color=tableau_colors[list(tableau_colors.keys())[ind % num_of_colors]],
+ linestyle="--",
+ )
+ ax.plot(x, y[ind], label=f"Channel {int(ind) + 1}", linewidth=2)
+ ax.legend(loc="best")
+ ax.set_xlabel("Input")
+ ax.set_ylabel("Value")
+ ax.set_title(f"Function {name}")
def plot_3d_function(plt, name, x0, x1, params, output, input_names):
fig = plt.figure()
# Clear the current plot
plt.clf()
- ax = fig.add_subplot(111, projection='3d')
- ax.plot_surface(np.array(x0), np.array(x1), np.array(output), cmap='viridis')
+ ax = fig.add_subplot(111, projection="3d")
+ ax.plot_surface(np.array(x0), np.array(x1), np.array(output), cmap="viridis")
ax.set_xlabel(input_names[0])
ax.set_ylabel(input_names[1])
- ax.set_zlabel(f'{name} output')
+ ax.set_zlabel(f"{name} output")
for ind in range(len(input_names) - 2):
- fig.text(0.01, 0.9 - 0.05 * ind, f"{input_names[ind + 2]} ={params[ind]}", fontsize=10, color='blue',
- style='italic')
- plt.title(f'Function {name}')
+ fig.text(
+ 0.01,
+ 0.9 - 0.05 * ind,
+ f"{input_names[ind + 2]} ={params[ind]}",
+ fontsize=10,
+ color="blue",
+ style="italic",
+ )
+ plt.title(f"Function {name}")
+
def plot_2d_function(plt, name, x, params, output, input_names):
fig = plt.figure()
@@ -181,8 +271,14 @@ def plot_2d_function(plt, name, x, params, output, input_names):
plt.clf()
plt.plot(np.array(x), np.array(output), linewidth=2)
plt.xlabel(input_names[0])
- plt.ylabel(f'{name} output')
+ plt.ylabel(f"{name} output")
for ind in range(len(input_names) - 1):
- fig.text(0.01, 0.9 - 0.05 * ind, f"{input_names[ind + 1]} ={params[ind]}", fontsize=10, color='blue',
- style='italic')
- plt.title(f'Function {name}')
\ No newline at end of file
+ fig.text(
+ 0.01,
+ 0.9 - 0.05 * ind,
+ f"{input_names[ind + 1]} ={params[ind]}",
+ fontsize=10,
+ color="blue",
+ style="italic",
+ )
+ plt.title(f"Function {name}")
diff --git a/nnodely/basic/loss.py b/nnodely/basic/loss.py
deleted file mode 100644
index 11f46a35..00000000
--- a/nnodely/basic/loss.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import torch.nn as nn
-import torch
-from nnodely.support.utils import check
-
-available_losses = ['mse', 'rmse', 'mae', 'cross_entropy']
-
-class CustomLoss(nn.Module):
- def __init__(self, loss_type='mse', **kwargs):
- super(CustomLoss, self).__init__()
- check(loss_type in available_losses, TypeError, f'The \"{loss_type}\" loss is not available. Possible losses are: {available_losses}.')
- self.loss_type = loss_type
- self.loss = nn.MSELoss(**kwargs)
- if callable(loss_type):
- self.loss = loss_type
- elif self.loss_type == 'mae':
- self.loss = nn.L1Loss(**kwargs)
- elif self.loss_type == 'cross_entropy':
- self.loss = nn.CrossEntropyLoss(**kwargs)
-
- def forward(self, inA, inB):
- if self.loss_type == 'cross_entropy':
- inB = inB.squeeze().float() if inA.shape == inB.shape else inB.squeeze().long()
- inA = inA.squeeze()
- res = self.loss(inA,inB)
- if self.loss_type == 'rmse':
- res = torch.sqrt(res)
- return res
\ No newline at end of file
diff --git a/nnodely/basic/model.py b/nnodely/basic/model.py
deleted file mode 100644
index 81f66bd1..00000000
--- a/nnodely/basic/model.py
+++ /dev/null
@@ -1,220 +0,0 @@
-import torch
-import copy
-
-import torch.nn as nn
-import numpy as np
-
-from itertools import product
-
-from nnodely.support.utils import TORCH_DTYPE
-from nnodely.support import initializer
-
-@torch.fx.wrap
-def update_state(data_in, rel):
- #virtual = torch.roll(data_in, shifts=-1, dims=1)
- max_dim = min(rel.size(1), data_in.size(1))
- data_out = data_in.clone()
- data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]
- return data_out
-
-class Model(nn.Module):
- def __init__(self, model_def):
- super(Model, self).__init__()
- model_def = copy.deepcopy(model_def)
-
- self.states = {key: value for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())}
-
- self.inputs = model_def['Inputs']
- self.outputs = model_def['Outputs']
- self.relations = model_def['Relations']
- self.params = model_def['Parameters']
- self.constants = model_def['Constants']
- self.sample_time = model_def['Info']['SampleTime']
- self.functions = model_def['Functions']
-
- self.minimizers = model_def['Minimizers'] if 'Minimizers' in model_def else {}
- self.minimizers_keys = [self.minimizers[key]['A'] for key in self.minimizers] + [self.minimizers[key]['B'] for key in self.minimizers]
-
- self.input_ns_backward = {key:value['ns'][0] for key, value in model_def['Inputs'].items()}
- self.input_n_samples = {key:value['ntot'] for key, value in model_def['Inputs'].items()}
-
- ## Build the network
- self.all_parameters = {}
- self.all_constants = {}
- self.relation_forward = {}
- self.relation_inputs = {}
- self.closed_loop_update = {}
- self.connect_update = {}
-
- ## Update the connect_update and closed_loop_update
- self.update()
-
- ## Define the correct slicing
- for _, items in self.relations.items():
- if items[0] == 'SamplePart':
- if items[1][0] in self.inputs.keys():
- items[3][0] = self.input_ns_backward[items[1][0]] + items[3][0]
- items[3][1] = self.input_ns_backward[items[1][0]] + items[3][1]
- if len(items) > 4: ## Offset
- items[4] = self.input_ns_backward[items[1][0]] + items[4]
- if items[0] == 'TimePart':
- if items[1][0] in self.inputs.keys():
- items[3][0] = self.input_ns_backward[items[1][0]] + round(items[3][0]/self.sample_time)
- items[3][1] = self.input_ns_backward[items[1][0]] + round(items[3][1]/self.sample_time)
- if len(items) > 4: ## Offset
- items[4] = self.input_ns_backward[items[1][0]] + round(items[4]/self.sample_time)
- else:
- items[3][0] = round(items[3][0]/self.sample_time)
- items[3][1] = round(items[3][1]/self.sample_time)
- if len(items) > 4: ## Offset
- items[4] = round(items[4]/self.sample_time)
-
- ## Create all the parameters
- for name, param_data in self.params.items():
- window = 'tw' if 'tw' in param_data.keys() else ('sw' if 'sw' in param_data.keys() else None)
- aux_sample_time = self.sample_time if 'tw' == window else 1
- sample_window = round(param_data[window] / aux_sample_time) if window else None
- if sample_window is None:
- param_size = tuple(param_data['dim']) if type(param_data['dim']) is list else (param_data['dim'],)
- else:
- param_size = (sample_window,)+tuple(param_data['dim']) if type(param_data['dim']) is list else (sample_window, param_data['dim'])
- if 'values' in param_data:
- self.all_parameters[name] = nn.Parameter(torch.tensor(param_data['values'], dtype=TORCH_DTYPE), requires_grad=True)
- # TODO clean code
- elif 'init_fun' in param_data:
- if 'code' in param_data['init_fun'].keys():
- exec(param_data['init_fun']['code'], globals())
- function_to_call = globals()[param_data['init_fun']['name']]
- else:
- function_to_call = getattr(initializer, param_data['init_fun']['name'])
- values = np.zeros(param_size)
- for indexes in product(*(range(v) for v in param_size)):
- if 'params' in param_data['init_fun']:
- values[indexes] = function_to_call(indexes, param_size, param_data['init_fun']['params'])
- else:
- values[indexes] = function_to_call(indexes, param_size)
- self.all_parameters[name] = nn.Parameter(torch.tensor(values.tolist(), dtype=TORCH_DTYPE), requires_grad=True)
- else:
- self.all_parameters[name] = nn.Parameter(torch.rand(size=param_size, dtype=TORCH_DTYPE), requires_grad=True)
-
- ## Create all the constants
- for name, param_data in self.constants.items():
- self.all_constants[name] = nn.Parameter(torch.tensor(param_data['values'], dtype=TORCH_DTYPE), requires_grad=False)
- all_params_and_consts = self.all_parameters | self.all_constants
-
- ## Create all the relations
- for relation, inputs in self.relations.items():
- ## Take the relation name and the inputs needed to solve the relation
- rel_name, input_var = inputs[0], inputs[1]
- ## Create All the Relations
- func = getattr(self,rel_name)
- if func:
- layer_inputs = []
- for item in inputs[2:]:
- if item in list(self.params.keys()): ## the relation takes parameters
- layer_inputs.append(self.all_parameters[item])
- elif item in list(self.constants.keys()): ## the relation takes a constant
- layer_inputs.append(self.all_constants[item])
- elif item in list(self.functions.keys()): ## the relation takes a custom function
- layer_inputs.append(self.functions[item])
- if 'params_and_consts' in self.functions[item].keys() and len(self.functions[item]['params_and_consts']) >= 0: ## Parametric function that takes parameters
- layer_inputs.append([all_params_and_consts[par] for par in self.functions[item]['params_and_consts']])
- if 'map_over_dim' in self.functions[item].keys():
- layer_inputs.append(self.functions[item]['map_over_dim'])
- else:
- layer_inputs.append(item)
-
- if rel_name == 'SamplePart':
- if layer_inputs[0] == -1:
- layer_inputs[0] = self.input_n_samples[input_var[0]]
- elif rel_name == 'TimePart':
- if layer_inputs[0] == -1:
- layer_inputs[0] = self.input_n_samples[input_var[0]]
- else:
- layer_inputs[0] = round(layer_inputs[0] / self.sample_time)
- ## Initialize the relation
- self.relation_forward[relation] = func(*layer_inputs)
- ## Save the inputs needed for the relative relation
- self.relation_inputs[relation] = input_var
-
- ## Add the gradient to all the relations and parameters that requires it
- self.relation_forward = nn.ParameterDict(self.relation_forward)
- self.all_constants = nn.ParameterDict(self.all_constants)
- self.all_parameters = nn.ParameterDict(self.all_parameters)
- ## list of network outputs
- self.network_output_predictions = set(self.outputs.values())
- ## list of network minimization outputs
- self.network_output_minimizers = []
- for _,value in self.minimizers.items():
- self.network_output_minimizers.append(self.outputs[value['A']]) if value['A'] in self.outputs.keys() else self.network_output_minimizers.append(value['A'])
- self.network_output_minimizers.append(self.outputs[value['B']]) if value['B'] in self.outputs.keys() else self.network_output_minimizers.append(value['B'])
- self.network_output_minimizers = set(self.network_output_minimizers)
- ## list of all the network Outputs
- self.network_outputs = self.network_output_predictions.union(self.network_output_minimizers)
-
- def forward(self, kwargs):
- result_dict = {}
-
- ## Initially i have only the inputs from the dataset, the parameters, and the constants
- available_inputs = [key for key in self.inputs.keys() if key not in self.connect_update.keys()] ## remove connected inputs
- available_keys = set(available_inputs + list(self.all_parameters.keys()) + list(self.all_constants.keys()))
-
- ## Forward pass through the relations
- while not self.network_outputs.issubset(available_keys): ## i need to climb the relation tree until i get all the outputs
- for relation in self.relations.keys():
- ## if i have all the variables i can calculate the relation
- if set(self.relation_inputs[relation]).issubset(available_keys) and (relation not in available_keys):
- ## Collect all the necessary inputs for the relation
- layer_inputs = []
- for key in self.relation_inputs[relation]:
- if key in self.all_constants.keys(): ## relation that takes a constant
- layer_inputs.append(self.all_constants[key])
- elif key in available_inputs: ## relation that takes inputs
- layer_inputs.append(kwargs[key])
- elif key in self.all_parameters.keys(): ## relation that takes parameters
- layer_inputs.append(self.all_parameters[key])
- else: ## relation than takes another relation or a connect variable
- layer_inputs.append(result_dict[key])
-
- ## Execute the current relation
- result_dict[relation] = self.relation_forward[relation](*layer_inputs)
- available_keys.add(relation)
-
- ## Check if the relation is inside the connect
- for connect_input, connect_rel in self.connect_update.items():
- if relation == connect_rel:
- result_dict[connect_input] = update_state(kwargs[connect_input], result_dict[relation])
- available_keys.add(connect_input)
-
- ## Return a dictionary with all the connected inputs
- connect_update_dict = {key: result_dict[key] for key in self.connect_update.keys()}
- ## Return a dictionary with all the relations that updates the state variables
- closed_loop_update_dict = {key: result_dict[value] for key, value in self.closed_loop_update.items()}
- ## Return a dictionary with all the outputs final values
- output_dict = {key: result_dict[value] for key, value in self.outputs.items()}
- ## Return a dictionary with the minimization relations
- minimize_dict = {}
- for key in self.minimizers_keys:
- minimize_dict[key] = result_dict[self.outputs[key]] if key in self.outputs.keys() else result_dict[key]
- return output_dict, minimize_dict, closed_loop_update_dict, connect_update_dict
-
- def update(self, *, closed_loop = {}, connect = {}, disconnect = False):
- self.closed_loop_update = {}
- self.connect_update = {}
-
- if disconnect:
- return
-
- for key, state in self.states.items():
- if 'connect' in state.keys():
- self.connect_update[key] = state['connect']
- elif 'closedLoop' in state.keys():
- self.closed_loop_update[key] = state['closedLoop']
-
- # Get relation from outputs
- for connect_in, connect_rel in connect.items():
- set_relation = self.outputs[connect_rel] if connect_rel in self.outputs.keys() else connect_rel
- self.connect_update[connect_in] = set_relation
- for close_in, close_rel in closed_loop.items():
- set_relation = self.outputs[close_rel] if close_rel in self.outputs.keys() else close_rel
- self.closed_loop_update[close_in] = set_relation
diff --git a/nnodely/basic/modeldef.py b/nnodely/basic/modeldef.py
deleted file mode 100644
index 438771c1..00000000
--- a/nnodely/basic/modeldef.py
+++ /dev/null
@@ -1,230 +0,0 @@
-import copy
-
-import numpy as np
-
-from nnodely.support.utils import check, check_and_get_list
-from nnodely.support.jsonutils import merge, subjson_from_model, subjson_from_minimize, check_model, get_models_json
-from nnodely.basic.relation import MAIN_JSON, Stream, check_names
-from nnodely.layers.output import Output
-from nnodely.layers.input import Input
-
-from nnodely.support.logger import logging, nnLogger
-log = nnLogger(__name__, logging.INFO)
-
-
-class ModelDef:
- def __init__(self, model_def = MAIN_JSON):
- # Models definition
- self.__json_base = copy.deepcopy(model_def)
-
- # Initialize the model definition
- self.__json = copy.deepcopy(self.__json_base)
- if "SampleTime" in self.__json['Info']:
- self.__sample_time = self.__json['Info']["SampleTime"]
- else:
- self.__sample_time = None
-
- def __contains__(self, key):
- return key in self.__json
-
- def __getitem__(self, key):
- if key in self.__json:
- return self.__json[key]
- else:
- return None
-
- def __setitem__(self, key, value):
- self.__json[key] = value
-
- def __rebuild_json(self, models_names, minimizers):
- models_json = subjson_from_model(self.__json, list(models_names))
- if 'Minimizers' in self.__json and len(minimizers) > 0:
- minimizers_json = subjson_from_minimize(self.__json, list(minimizers))
- models_json = merge(models_json, minimizers_json)
- return copy.deepcopy(models_json)
-
- def recurrentInputs(self):
- return {key:value for key, value in self.__json['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())}
-
- def getJson(self, models:list|str|None = None) -> dict:
- if models is None:
- return copy.deepcopy(self.__json)
- else:
- json = subjson_from_model(self.__json, models)
- check_model(json)
- return copy.deepcopy(json)
-
- def getSampleTime(self):
- check(self.__sample_time is not None, AttributeError, "Sample time is not defined the model is not neuralized!")
- return self.__sample_time
-
- def isDefined(self):
- return self.__json is not None
-
- def addConnection(self, stream_out:str|Output|Stream, input_in:str|Input, type:str, local:bool = False):
- outputs = self.__json['Outputs']
-
- if isinstance(stream_out, (Output, Stream)):
- stream_name = outputs[stream_out.name] if stream_out.name in outputs.keys() else stream_out.name
- else:
- output_name = check_and_get_list(stream_out, set(outputs.keys()),
- lambda name: f"The name {name} is not part of the available Outputs")[0]
- stream_name = outputs[output_name]
-
- if isinstance(input_in, Input):
- input_name = input_in.name
- else:
- input_name = input_in #TODO Add tests
-
- input_name = check_and_get_list(input_name, set(self.__json['Inputs'].keys()),
- lambda name: f"The name {name} is not part of the available Inputs")[0]
- stream_name = check_and_get_list(stream_name, set(self.__json['Relations'].keys()),
- lambda name: f"The name {name} is not part of the available Relations")[0]
- self.__json['Inputs'][input_name][type] = stream_name
- self.__json['Inputs'][input_name]['local'] = int(local)
-
- def removeConnection(self, name_list:str|list[str]):
- name_list = check_and_get_list(name_list, set(self.__json['Inputs'].keys()), lambda name: f"The name {name} is not part of the available Inputs")
- for input_in in name_list:
- if 'closedLoop' in self.__json['Inputs'][input_in].keys():
- del self.__json['Inputs'][input_in]['closedLoop']
- del self.__json['Inputs'][input_in]['local']
- elif 'connect' in self.__json['Inputs'][input_in].keys():
- del self.__json['Inputs'][input_in]['connect']
- del self.__json['Inputs'][input_in]['local']
- else:
- raise ValueError(f"The input '{input_in}' has no connection or closed loop defined")
-
- def addModel(self, name:str, stream_list):
- if isinstance(stream_list, Output):
- stream_list = [stream_list]
-
- json = MAIN_JSON
- for stream in stream_list:
- json = merge(json, stream.json)
- check_model(json)
-
- if 'Models' not in self.__json:
- self.__json = merge(self.__json, json)
- self.__json['Models'] = name
- else:
- models_names = set((self.__json['Models'],)) if type(self.__json['Models']) is str else set(self.__json['Models'].keys())
- check_names(name, models_names, 'Models')
- if type(self.__json['Models']) is str:
- self.__json['Models'] = {self.__json['Models']: get_models_json(self.__json)}
- self.__json = merge(self.__json, json)
- self.__json['Models'][name] = get_models_json(json)
-
- def removeModel(self, name_list):
- if 'Models' not in self.__json:
- raise ValueError("No Models are defined")
- models_names = {self.__json['Models']} if type(self.__json['Models']) is str else set(self.__json['Models'].keys())
- name_list = check_and_get_list(name_list, models_names, lambda name: f"The name {name} is not part of the available models")
- models_names -= set(name_list)
- minimizers = set(self.__json['Minimizers'].keys()) if 'Minimizers' in self.__json else None
- self.__json = self.__rebuild_json(models_names, minimizers)
-
- def addMinimize(self, name, streamA, streamB, loss_function='mse'):
- if 'Minimizers' not in self.__json:
- self.__json['Minimizers'] = {}
- check_names(name, set(self.__json['Minimizers'].keys()), 'Minimizers')
-
- if isinstance(streamA, str):
- streamA_name = streamA
- else:
- check(isinstance(streamA, (Output, Stream)), TypeError, 'streamA must be an instance of Output or Stream')
- streamA_name = streamA.json['Outputs'][streamA.name] if isinstance(streamA, Output) else streamA.name
- self.__json = merge(self.__json, streamA.json)
-
- if isinstance(streamB, str):
- streamB_name = streamB
- else:
- check(isinstance(streamB, (Output, Stream)), TypeError, 'streamA must be an instance of Output or Stream')
- streamB_name = streamB.json['Outputs'][streamB.name] if isinstance(streamB, Output) else streamB.name
- self.__json = merge(self.__json, streamB.json)
- #check(streamA.dim == streamB.dim, ValueError, f'Dimension of streamA={streamA.dim} and streamB={streamB.dim} are not equal.')
-
- self.__json['Minimizers'][name] = {}
- self.__json['Minimizers'][name]['A'] = streamA_name
- self.__json['Minimizers'][name]['B'] = streamB_name
- self.__json['Minimizers'][name]['loss'] = loss_function
-
- def removeMinimize(self, name_list):
- if 'Minimizers' not in self.__json:
- raise ValueError("No Minimizers are defined")
- name_list = check_and_get_list(name_list, self.__json['Minimizers'].keys(), lambda name: f"The name {name} is not part of the available minimizers")
- models_names = {self.__json['Models']} if type(self.__json['Models']) is str else set(self.__json['Models'].keys())
- remaining_minimizers = set(self.__json['Minimizers'].keys()) - set(name_list) if 'Minimizers' in self.__json else None
- self.__json = self.__rebuild_json(models_names, remaining_minimizers)
-
- def setBuildWindow(self, sample_time = None):
- check(self.__json is not None, RuntimeError, "No model is defined!")
- if sample_time is not None:
- check(sample_time > 0, RuntimeError, 'Sample time must be strictly positive!')
- self.__sample_time = sample_time
- else:
- if self.__sample_time is None:
- self.__sample_time = 1
-
- self.__json['Info'] = {"SampleTime": self.__sample_time}
- if 'SampleTime' in self.__json['Constants']:
- self.__json['Constants']['SampleTime'] = {'dim': 1, 'values': self.__sample_time}
-
- check(self.__json['Inputs'] != {}, RuntimeError, "No model is defined!")
- json_inputs = self.__json['Inputs']
-
- input_ns_backward, input_ns_forward = {}, {}
- for key, value in json_inputs.items():
- if 'sw' not in value and 'tw' not in value:
- assert False, f"Input '{key}' has no time window or sample window"
- if 'sw' not in value and self.__sample_time is not None:
- ## check if value['tw'] is a multiple of sample_time
- absolute_tw = abs(value['tw'][0]) + abs(value['tw'][1])
- check(round(absolute_tw % self.__sample_time) == 0, ValueError,
- f"Time window of input '{key}' is not a multiple of sample time. This network cannot be neuralized")
- input_ns_backward[key] = round(-value['tw'][0] / self.__sample_time)
- input_ns_forward[key] = round(value['tw'][1] / self.__sample_time)
- elif self.__sample_time is not None:
- if 'tw' in value:
- input_ns_backward[key] = max(round(-value['tw'][0] / self.__sample_time), -value['sw'][0])
- input_ns_forward[key] = max(round(value['tw'][1] / self.__sample_time), value['sw'][1])
- else:
- input_ns_backward[key] = -value['sw'][0]
- input_ns_forward[key] = value['sw'][1]
- else:
- check(value['tw'] == [0,0], RuntimeError, f"Sample time is not defined for input '{key}'")
- input_ns_backward[key] = -value['sw'][0]
- input_ns_forward[key] = value['sw'][1]
- value['ns'] = [input_ns_backward[key], input_ns_forward[key]]
- value['ntot'] = sum(value['ns'])
-
- self.__json['Info']['ns'] = [max(input_ns_backward.values()), max(input_ns_forward.values())]
- self.__json['Info']['ntot'] = sum(self.__json['Info']['ns'])
- if self.__json['Info']['ns'][0] < 0:
- log.warning(
- f"The input is only in the far past the max_samples_backward is: {self.__json['Info']['ns'][0]}")
- if self.__json['Info']['ns'][1] < 0:
- log.warning(
- f"The input is only in the far future the max_sample_forward is: {self.__json['Info']['ns'][1]}")
-
- for k, v in (self.__json['Parameters'] | self.__json['Constants']).items():
- if 'values' in v:
- window = 'tw' if 'tw' in v.keys() else ('sw' if 'sw' in v.keys() else None)
- if window == 'tw':
- check(np.array(v['values']).shape[0] == v['tw'] / self.__sample_time, ValueError,
- f"{k} has a different number of values for this sample time.")
- if v['values'] == "SampleTime":
- v['values'] = self.__sample_time
-
- def updateParameters(self, model = None, *, clear_model = False):
- if clear_model:
- for key in self.__json['Parameters'].keys():
- if 'init_values' in self.__json['Parameters'][key]:
- self.__json['Parameters'][key]['values'] = self.__json['Parameters'][key]['init_values']
- elif 'values' in self.__json['Parameters'][key]:
- del self.__json['Parameters'][key]['values']
- elif model is not None:
- for key in self.__json['Parameters'].keys():
- if key in model.all_parameters:
- self.__json['Parameters'][key]['values'] = model.all_parameters[key].tolist()
-
diff --git a/nnodely/exporter/export.py b/nnodely/exporter/export.py
deleted file mode 100644
index fbfa1ce2..00000000
--- a/nnodely/exporter/export.py
+++ /dev/null
@@ -1,406 +0,0 @@
-import sys, os, torch, importlib, json
-
-from torch.fx import symbolic_trace
-
-from pprint import PrettyPrinter
-
-class JsonPrettyPrinter(PrettyPrinter):
- def _format(self, object, *args):
- if isinstance(object, str):
- width = self._width
- self._width = sys.maxsize
- try:
- super()._format(object.replace('\'','_"_'), *args)
- finally:
- self._width = width
- else:
- super()._format(object, *args)
-
-def save_model(model, model_path):
- # Export the dictionary as a JSON file
- with open(model_path, 'w') as json_file:
- # json.dump(self.model_def, json_file, indent=4)
- json_file.write(JsonPrettyPrinter().pformat(model)
- .replace('\'', '\"')
- .replace('_"_', '\'')
- .replace('None', 'null')
- .replace('False', 'false')
- .replace('True', 'true'))
- # json_file.write(JsonPrettyPrinter().pformat(model).replace('None','null'))
- # data = json.dumps(self.model_def)
- # json_file.write(pformat(data).replace('\\\\n', '\\n').replace('\'', '').replace('(','').replace(')',''))
- # json_file.write(pformat(data).replace('\'', '\"'))
-
-def load_model(model_path):
- import json
- with open(model_path, 'r', encoding='UTF-8') as file:
- model_def = json.load(file)
- return model_def
-
-def export_python_model(model_def, model, model_path):
- package_name = __package__.split('.')[0]
-
- # Get the symbolic tracer
- with torch.no_grad():
- trace = symbolic_trace(model)
-
- recurrent_inputs = {key:value for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())}
- inputs = {key: value for key, value in model_def['Inputs'].items() if ('closedLoop' not in value.keys() and 'connect' not in value.keys())}
- attributes = sorted(set([line for line in trace.code.split() if 'self.' in line]))
-
- saved_functions = []
-
- with open(model_path, 'w') as file:
- file.write("import torch\n\n")
-
- ## write the connect wrap function
- # file.write(f"def {package_name}_basic_model_update_state(data_in, rel):\n")
- # file.write(" virtual = torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n")
- # file.write(" max_dim = min(rel.size(1), data_in.size(1))\n")
- # file.write(" virtual[:, -max_dim:, :] = rel[:, -max_dim:, :]\n")
- # file.write(" return virtual\n\n")
- file.write(f"def {package_name}_basic_model_update_state(data_in, rel):\n")
- file.write(" data_out = data_in.clone()\n")
- file.write(" max_dim = min(rel.size(1), data_in.size(1))\n")
- file.write(" data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]\n")
- file.write(" return data_out\n\n")
-
- file.write(f"def {package_name}_basic_model_timeshift(data_in):\n")
- file.write(" return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n\n")
-
- for name in model_def['Functions'].keys():
- if 'Fuzzify' in name:
- if 'slicing' not in saved_functions:
- #file.write("@torch.fx.wrap\n")
- file.write(f"def {package_name}_layers_fuzzify_slicing(res, i, x):\n")
- file.write(" res[:, :, i:i+1] = x\n\n")
- saved_functions.append('slicing')
-
- function_name = model_def['Functions'][name]['names']
- function_code = model_def['Functions'][name]['functions']
- if isinstance(function_code, list):
- for i, fun_code in enumerate(function_code):
- if fun_code != 'Rectangular' and fun_code != 'Triangular':
- if function_name[i] not in saved_functions:
- fun_code = fun_code.replace(f'def {function_name[i]}',
- f'def {package_name}_layers_fuzzify_{function_name[i]}')
- #file.write("@torch.fx.wrap\n")
- file.write(fun_code)
- file.write("\n")
- saved_functions.append(function_name[i])
- else:
- if (function_name != 'Rectangular') and (function_name != 'Triangular') and (function_name not in saved_functions):
- function_code = function_code.replace(f'def {function_name}',
- f'def {package_name}_layers_fuzzify_{function_name}')
- #file.write("@torch.fx.wrap\n")
- file.write(function_code)
- file.write("\n")
- saved_functions.append(function_name)
-
-
- elif 'ParamFun' in name:
- function_name = model_def['Functions'][name]['name']
- if function_name not in saved_functions:
- code = model_def['Functions'][name]['code']
- code = code.replace(f'def {function_name}', f'def {package_name}_layers_parametricfunction_{function_name}')
- file.write(code)
- file.write("\n")
- saved_functions.append(function_name)
-
- elif 'NeuralODE' in name:
- function_name = model_def['Functions'][name]['name']
- if function_name not in saved_functions:
- code = model_def['Functions'][name]['code']
- code = code.replace(f'def {function_name}', f'def {package_name}_layers_neuralODE_{function_name}')
- file.write(code)
- file.write("\n")
- saved_functions.append(function_name)
-
-
- file.write("class TracerModel(torch.nn.Module):\n")
- file.write(" def __init__(self):\n")
- file.write(" super().__init__()\n")
- file.write(" self.all_parameters = {}\n")
- file.write(" self.all_constants = {}\n")
- for attr in attributes:
- if 'all_constant' in attr:
- key = attr.split('.')[-1]
- file.write(f" self.all_constants[\"{key}\"] = torch.tensor({model.all_constants[key].tolist()}, requires_grad=False)\n")
- elif 'relation_forward' in attr:
- key = attr.split('.')[2]
- if 'Fir' in key or 'Linear' in key:
- if 'weights' in attr.split('.')[3]:
- param = model_def['Relations'][key][2]
- value = model.all_parameters[param]
- file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.tensor({value.tolist()}), requires_grad=True)\n")
- elif 'bias' in attr.split('.')[3]:
- param = model_def['Relations'][key][3]
- file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.tensor({model.all_parameters[param].tolist()}), requires_grad=True)\n")
- elif 'dropout' in attr.split('.')[3]:
- param = model_def['Relations'][key][4]
- file.write(f" self.{key} = torch.nn.Dropout(p={param})\n")
- # param = model_def['Relations'][key][2] if 'weights' in attr.split('.')[3] else model_def['Relations'][key][3]
- # value = model.all_parameters[param].data.squeeze(0) if 'Linear' in key else model.all_parameters[param].data
- # file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.{value}, requires_grad=True)\n")
- elif 'Part' in key or 'Select' in key: # any(element in key for element in ['Part', 'Select']):
- value = model.relation_forward[key].W
- temp_value = json.dumps(value.tolist())
- file.write(f" self.all_constants[\"{key}\"] = torch.tensor({temp_value}, requires_grad=True)\n")
- elif 'all_parameters' in attr:
- key = attr.split('.')[-1]
- file.write(f" self.all_parameters[\"{key}\"] = torch.nn.Parameter(torch.tensor({model.all_parameters[key].tolist()}), requires_grad=True)\n")
- elif '_tensor_constant' in attr:
- key = attr.split('.')[-1]
- file.write(f" {attr} = torch.tensor({getattr(model,key).item()})\n")
-
- file.write(" self.all_parameters = torch.nn.ParameterDict(self.all_parameters)\n")
- file.write(" self.all_constants = torch.nn.ParameterDict(self.all_constants)\n\n")
- file.write(" def update(self, closed_loop={}, connect={}, disconnect=False):\n")
- file.write(" pass\n")
-
- for line in trace.code.split("\n")[len(saved_functions) + 2:]:
- if 'self.relation_forward' in line:
- if 'Part' in line or 'Select' in line:
- attribute = [x for x in line.split() if 'self.relation_forward' in x][0].split('.')[2]
- old_line = f"self.relation_forward.{attribute}.W"
- new_line = f"self.all_constants.{attribute}"
- file.write(f" {line.replace(old_line, new_line)}\n")
- elif 'dropout' in line:
- attribute = line.split()[0]
- layer = attribute.split('_')[2].capitalize()
- old_line = f"self.relation_forward.{layer}.dropout"
- new_line = f"self.{layer}"
- file.write(f" {line.replace(old_line, new_line)}\n")
- else:
- attribute = line.split()[-1]
- relation = attribute.split('.')[2]
- relation_type = attribute.split('.')[3]
- param = model_def['Relations'][relation][2] if 'weights' == relation_type else \
- model_def['Relations'][relation][3]
- new_attribute = f'self.all_parameters.{param}'
- file.write(f" {line.replace(attribute, new_attribute)}\n")
- else:
- file.write(f" {line}\n")
-
- if len(recurrent_inputs) > 0:
- file.write("class RecurrentModel(torch.nn.Module):\n")
- file.write(" def __init__(self):\n")
- file.write(" super().__init__()\n")
- file.write(" self.Cell = TracerModel()\n")
- list_inputs = " self.inputs = ["
- for key in inputs.keys():
- list_inputs += f"'{key}', "
- list_inputs += "]\n"
- file.write(list_inputs)
- file.write(" self.states = dict()\n")
- file.write("\n")
- file.write(" def forward(self, kwargs, n_samples = None):\n")
- file.write(" n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs])\n")
- for key in recurrent_inputs.keys():
- file.write(f" self.states['{key}'] = kwargs['{key}']\n")
- result_str = ""
- for key, value in model_def['Outputs'].items():
- result_str += f"'{key}':[], "
- file.write(f" results = {{{result_str}}}\n")
- file.write(" X = dict()\n")
- file.write(" for idx in range(n_samples):\n")
- file.write(f" for key in self.inputs:\n")
- file.write(f" X[key] = kwargs[key][idx]\n")
- file.write(f" for key, value in self.states.items():\n")
- file.write(f" X[key] = value\n")
- file.write(" out, _, closed_loop, connect = self.Cell(X)\n")
- file.write(" for key, value in results.items():\n")
- file.write(" results[key].append(out[key])\n")
- file.write(" for key, val in closed_loop.items():\n")
- file.write(" self.states[key] = nnodely_basic_model_timeshift(self.states[key])\n")
- file.write(" self.states[key] = nnodely_basic_model_update_state(self.states[key], val)\n")
- file.write(" for key, val in connect.items():\n")
- file.write(" self.states[key] = nnodely_basic_model_timeshift(val)\n")
- file.write(" return results\n")
-
-def export_pythononnx_model(model_def, model_path, model_onnx_path, input_order=None, outputs_order=None):
- closed_loop_states, connect_states = [], []
- for key, value in model_def['Inputs'].items():
- if 'closedLoop' in value.keys():
- closed_loop_states.append(key)
- if 'connect' in value.keys():
- connect_states.append(key)
-
- model_inputs = input_order if input_order else list(model_def['Inputs'].keys())
- model_outputs = outputs_order if outputs_order else list(model_def['Outputs'].keys())
- model_losses = []
- if model_def['Minimizers']:
- model_losses = [loss_dict['A'] for loss_dict in model_def['Minimizers'].values() if 'A' in loss_dict.keys()] + [loss_dict['B'] for loss_dict in model_def['Minimizers'].values() if 'A' in loss_dict.keys()]
- recurrent_inputs = [key for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())]
- inputs = [key for key in model_inputs if key not in recurrent_inputs]
-
- # Define the mapping dictionary input
- trace_mapping_input = {}
- forward = 'def forward(self,'
- for i, key in enumerate(model_inputs):
- value = f'kwargs[\'{key}\']'
- trace_mapping_input[value] = key
- forward = forward + f' {key}' + (',' if i < len(model_inputs) - 1 else '')
- forward = forward + '):'
- # Define the mapping dictionary output
- outputs = ' return ('
- for i, key in enumerate(model_outputs):
- outputs += f'outputs[0][\'{key}\']' + (',' if i < len(model_outputs) - 1 else ',)')
- outputs += ', ('
- for i, key in enumerate(model_losses):
- outputs += f'outputs[1][\'{key}\'], '# + (',' if i < len(model_outputs) - 1 else ',)')
- outputs += '), ('
- for key in closed_loop_states:
- outputs += f'outputs[2][\'{key}\'], '
- outputs += '), ('
- for key in connect_states:
- outputs += f'outputs[3][\'{key}\'], '
- outputs += ')\n'
-
- # Open and read the file
- file_content = []
- with open(model_path, 'r') as file:
- for line in file:
- if 'return ({' in line:
- file_content.append(line)
- break
- file_content.append(line)
- file_content = ''.join(file_content)
-
- # Replace the forward header
- file_content = file_content.replace('def forward(self, kwargs):', forward)
- # Perform the substitution
- for key, value in trace_mapping_input.items():
- file_content = file_content.replace(key, value)
- # Write the modified content back to a new file
- # Replace the return statement
- last_return_index = file_content.rfind('return')
- if last_return_index != -1:
- file_content = file_content[:last_return_index] + 'outputs =' + file_content[last_return_index + len('return'):]
- file_content += outputs
- with open(model_onnx_path, 'w') as file:
- file.write(file_content)
-
- if len(recurrent_inputs) > 0:
- file.write('\n')
- file.write("class RecurrentModel(torch.nn.Module):\n")
- file.write(" def __init__(self):\n")
- file.write(" super().__init__()\n")
- file.write(" self.Cell = TracerModel()\n")
-
- forward_str = " def forward(self, "
- for key in model_inputs:
- forward_str += f"{key}, "
- forward_str += "):\n"
- file.write(forward_str)
-
- if model_inputs:
- file.write(" n_samples = min([" + ", ".join([f"{key}.size(0)" for key in inputs]) + "])\n")
- else:
- file.write(" n_samples = 1\n")
-
- for key in model_outputs:
- file.write(f" results_{key} = []\n")
- file.write(" for idx in range(n_samples):\n")
- call_str = " out, losses, closed_loop, connect = self.Cell("
- for key in model_inputs:
- call_str += f"{key}[idx], " if key in inputs else f"{key}, "
- call_str += ")\n"
- file.write(call_str)
- for idx, key in enumerate(model_outputs):
- file.write(f" results_{key}.append(out[{idx}])\n")
- for idx, key in enumerate(closed_loop_states):
- file.write(f" {key} = nnodely_basic_model_timeshift({key})\n")
- file.write(f" {key} = nnodely_basic_model_update_state({key}, closed_loop[{idx}])\n")
- for idx, key in enumerate(connect_states):
- file.write(f" {key} = nnodely_basic_model_timeshift(connect[{idx}])\n")
- #file.write(f" {key} = connect[{idx}]\n")
- for idx, key in enumerate(model_outputs):
- file.write(f" results_{key} = torch.stack(results_{key}, dim=0)\n")
- return_str = " return "
- for key in model_outputs:
- return_str += f"results_{key}, "
- file.write(return_str)
-
-def import_python_model(name, model_folder):
- sys.path.insert(0, model_folder)
- module_name = os.path.basename(name)
- if module_name in sys.modules:
- # Reload the module if it is already loaded
- module = importlib.reload(sys.modules[module_name])
- else:
- # Import the module if it is not loaded
- module = importlib.import_module(module_name)
- return module.TracerModel()
-
-def export_onnx_model(model_def, model, model_path, input_order=None, output_order=None, name='net_onnx'):
- sys.path.insert(0, model_path)
- module_name = os.path.basename(name)
-
- recurrent_inputs = {key:value for key, value in model_def['Inputs'].items() if
- ('closedLoop' in value.keys() or 'connect' in value.keys())}
- inputs = {key:value for key, value in model_def['Inputs'].items() if
- ('closedLoop' not in value.keys() and 'connect' not in value.keys())}
-
- if module_name in sys.modules:
- # Reload the module if it is already loaded
- module = importlib.reload(sys.modules[module_name])
- else:
- # Import the module if it is not loaded
- module = importlib.import_module(module_name)
- model = torch.jit.script(module.RecurrentModel()) if len(recurrent_inputs) > 0 else module.TracerModel()
- model.eval()
- dummy_inputs = []
- input_names = []
- dynamic_axes = {}
- onnx_inputs = input_order if input_order else model_def['Inputs'].keys()
- for key in onnx_inputs:
- input_names.append(key)
- window_size = model_def['Inputs'][key]['ntot']
- dim = model_def['Inputs'][key]['dim']
- if len(recurrent_inputs) > 0 != {}:
- if key in inputs.keys():
- dummy_inputs.append(torch.randn(size=(1, 1, window_size, dim)))
- dynamic_axes[key] = {0: 'horizon', 1: 'batch_size'}
- elif key in recurrent_inputs.keys():
- dummy_inputs.append(torch.randn(size=(1, window_size, dim)))
- dynamic_axes[key] = {0: 'batch_size'}
- else:
- dummy_inputs.append(torch.randn(size=(1, window_size, dim)))
- dynamic_axes[key] = {0: 'batch_size'}
- output_names = output_order if output_order else list(model_def['Outputs'].keys())
- dummy_inputs = tuple(dummy_inputs)
-
- torch.onnx.export(
- model, # The model to be exported
- dummy_inputs, # Tuple of inputs to match the forward signature
- model_path, # File path to save the ONNX model
- export_params = True, # Store the trained parameters in the model file
- opset_version = 17, # ONNX version to export to (you can use 11 or higher)
- do_constant_folding=False, # Optimize constant folding for inference
- input_names = input_names, # Name each input as they will appear in ONNX
- output_names = output_names, # Name the output
- dynamic_axes = dynamic_axes,
- )
-
-def onnx_inference(inputs, path, optimize_graph=False):
- import onnxruntime as ort
- # Create an ONNX Runtime session
- # Define session options
- ## TODO: Warning when using constant folding in inference CanUpdateImplicitInputNameInSubgraphs]
- ## TODO: Implicit input name Cell.all_constants.Constant75 cannot be safely updated to Cell.all_constants.Constant76 in one of the subgraphs.
- if optimize_graph == False:
- session_options = ort.SessionOptions()
- # Set graph optimization level to disable all optimizations
- session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
- session = ort.InferenceSession(path, sess_options=session_options)
- else:
- session = ort.InferenceSession(path)
- output_data = []
- for item in session.get_outputs():
- output_data.append(item.name)
- input_data = {}
- for item in session.get_inputs():
- input_data[item.name] = inputs[item.name]
- # Run inference
- return session.run(output_data, input_data)
\ No newline at end of file
diff --git a/nnodely/exporter/reporter.py b/nnodely/exporter/reporter.py
deleted file mode 100644
index 31f1e35a..00000000
--- a/nnodely/exporter/reporter.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import io
-
-import matplotlib.pyplot as plt
-from reportlab.lib.pagesizes import letter
-from reportlab.pdfgen import canvas
-from reportlab.lib.utils import ImageReader
-
-from mplplots import plots
-
-
-class Reporter:
- def __init__(self, modely):
- self.modely = modely
-
- def exportReport(self, report_path):
- c = canvas.Canvas(report_path, pagesize=letter)
- width, height = letter
-
- if 'Minimizers' in self.modely._model_def:
- for key, value in self.modely._model_def['Minimizers'].items():
- fig = plt.figure(figsize=(10, 5))
- ax = fig.add_subplot(111)
- if 'val' in self.modely._training[key]:
- plots.plot_training(ax, f"Training Loss of {key}", key, self.modely._training[key]['train'], self.modely._training[key]['val'])
- else:
- plots.plot_training(ax, f"Training Loss of {key}", key, self.modely._training[key]['train'])
- training = io.BytesIO()
- plt.savefig(training, format='png')
- training.seek(0)
- plt.close()
- c.drawString(100, height - 30, f"Training Loss of {key}")
- c.drawImage(ImageReader(training), 50, height - 290, width=500, height=250)
- c.showPage()
-
- if len(self.modely.prediction) > 0:
- for key in self.modely._model_def['Minimizers'].keys():
- c.drawString(100, height - 30, f"Prediction of {key}")
- for ind, name_data in enumerate(self.modely.prediction.keys()):
- fig = plt.figure(figsize=(10, 5))
- ax = fig.add_subplot(111)
- idxs = None
- if 'idxs' in self.modely.prediction[name_data]:
- idxs = self.modely.prediction[name_data]['idxs']
- plots.plot_results(ax, name_data, key, self.modely.prediction[name_data][key]['A'],
- self.modely.prediction[name_data][key]['B'], idxs, self.modely._model_def['Info']["SampleTime"])
- # Add a text box with correlation coefficient
- results = io.BytesIO()
- plt.savefig(results, format='png')
- results.seek(0)
- plt.close()
- c.drawImage(ImageReader(results), 50, height - 290 - 245*ind, width=500, height=250)
- c.showPage()
- else:
- c.drawString(100, height - 30, f"No Minimize")
- c.save()
diff --git a/nnodely/exporter/standardexporter.py b/nnodely/exporter/standardexporter.py
deleted file mode 100644
index 7db7ba59..00000000
--- a/nnodely/exporter/standardexporter.py
+++ /dev/null
@@ -1,117 +0,0 @@
-import os, torch
-
-from nnodely.visualizer import EmptyVisualizer
-from nnodely.exporter.emptyexporter import EmptyExporter
-from nnodely.exporter.reporter import Reporter
-from nnodely.exporter.export import save_model, load_model, export_python_model, export_pythononnx_model, export_onnx_model, import_python_model, onnx_inference
-from nnodely.support.utils import check, enforce_types
-
-from nnodely.support.logger import logging, nnLogger
-log = nnLogger(__name__, logging.INFO)
-
-class StandardExporter(EmptyExporter):
- @enforce_types
- def __init__(self, workspace:str|None=None, visualizer:EmptyVisualizer|None=None, *, save_history:bool=False):
- super().__init__(workspace, visualizer, save_history)
-
- def getWorkspace(self):
- return self.workspace_folder if hasattr(self,'workspace_folder') else '.'
-
- def saveTorchModel(self, model, name = 'net', model_folder = None):
- file_name = name + ".pt"
- model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder,file_name)
- #TODO check if the folder exist
- torch.save(model.state_dict(), model_path)
- self.visualizer.saveModel('Torch Model', model_path)
-
- def loadTorchModel(self, model, name = 'net', model_folder = None):
- file_name = name + ".pt"
- model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder,file_name)
- check(os.path.exists(model_path), FileNotFoundError, f"The model {name} it is not found in the folder {model_folder}")
- model.load_state_dict(torch.load(model_path, weights_only=True))
- self.visualizer.loadModel('Torch Model',model_path)
- #TODO Update the model parameters....
-
- def saveModel(self, model_def, name = 'net', model_folder = None):
- # Combine the folder path and file name to form the complete file path
- model_folder = self.getWorkspace() if model_folder is None else model_folder
- # Specify the JSON file name
- file_name = name + ".json"
- # Combine the folder path and file name to form the complete file path
- model_path = os.path.join(model_folder, file_name)
- save_model(model_def, model_path)
- self.visualizer.saveModel('JSON Model', model_path)
-
- def loadModel(self, name = 'net', model_folder = None):
- # Combine the folder path and file name to form the complete file path
- model_folder = self.getWorkspace() if model_folder is None else model_folder
- model_def = None
- try:
- file_name = name + ".json"
- model_path = os.path.join(model_folder, file_name)
- model_def = load_model(model_path)
- self.visualizer.loadModel('JSON Model', model_path)
- except Exception as e:
- check(False, FileNotFoundError, f"The file {model_path} it is not found or not conformed.\n Error: {e}")
- return model_def
-
- def exportPythonModel(self, model_def, model, name = 'net', model_folder = None):
- file_name = name + ".py"
- model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder, file_name)
- ## Export to python file
- export_python_model(model_def.getJson(), model, model_path)
- self.visualizer.exportModel('Python Torch Model', model_path)
-
- def importPythonModel(self, name = 'net', model_folder = None):
- try:
- model_folder = self.getWorkspace() if model_folder is None else model_folder
- model = import_python_model(name, model_folder)
- self.visualizer.importModel('Python Torch Model', os.path.join(model_folder,name+'.py'))
- except Exception as e:
- model = None
- check(False, FileNotFoundError, f"The model {name} it is not found in the folder {model_folder}.\nError: {e}")
- return model
-
- def exportONNX(self, model_def, model, inputs_order=None, outputs_order=None, name = 'net', model_folder = None):
- if inputs_order is None:
- log.info(f"The inputs order for the export is not specified, the order will set equal to {set(model_def['Inputs'].keys())}.")
- elif set(inputs_order) != set(model_def['Inputs'].keys()):
- raise ValueError(f'The inputs are not the same as the model inputs {set(model_def["Inputs"].keys())}.')
- if outputs_order is None:
- log.info(f"The outputs order for the export is not specified, the order will set equal to {set(model_def['Outputs'].keys())}")
- elif set(outputs_order) != set(model_def['Outputs'].keys()):
- log.info(f'The outputs are not the same as the model outputs {set(model_def["Outputs"].keys())}.')
- file_name = name + ".py"
- model_folder = os.path.join(self.getWorkspace(), 'onnx') if model_folder is None else model_folder
- os.makedirs(model_folder, exist_ok=True)
- model_path = os.path.join(model_folder, file_name)
- onnx_python_model_path = model_path.replace('.py', '_onnx.py')
- onnx_model_path = model_path.replace('.py', '.onnx')
- ## Export to python file (onnx compatible)
- export_python_model(model_def, model, model_path)
- self.visualizer.exportModel('Python Torch Model', model_path)
- export_pythononnx_model(model_def, model_path, onnx_python_model_path, inputs_order, outputs_order)
- self.visualizer.exportModel('Python Onnx Torch Model', onnx_python_model_path)
- ## Export to onnx file (onnx compatible)
- model = import_python_model(file_name.replace('.py', '_onnx'), model_folder)
- export_onnx_model(model_def, model, onnx_model_path, inputs_order, outputs_order, name=name+'_onnx')
- self.visualizer.exportModel('Onnx Model', onnx_model_path)
-
- def onnxInference(self, inputs, name:str='net', model_folder:str|None=None):
- model_folder = os.path.join(self.getWorkspace(), 'onnx') if model_folder is None else model_folder
- file_name = name + ".onnx"
- onnx_model_path = os.path.join(model_folder, file_name)
- check(os.path.exists(onnx_model_path), FileNotFoundError, f"The model {file_name} it is not found in the folder {model_folder}")
- return onnx_inference(inputs, onnx_model_path)
-
- def exportReport(self, n4m, name = 'net', model_folder = None):
- # Combine the folder path and file name to form the complete file path
- model_folder = self.getWorkspace() if model_folder is None else model_folder
- # Specify the JSON file name
- file_name = name + ".pdf"
- # Combine the folder path and file name to form the complete file path
- report_path = os.path.join(model_folder, file_name)
- reporter = Reporter(n4m)
- reporter.exportReport(report_path)
- self.visualizer.exportReport('Training Results', report_path)
-
diff --git a/nnodely/layers/arithmetic.py b/nnodely/layers/arithmetic.py
deleted file mode 100644
index 0b2f5877..00000000
--- a/nnodely/layers/arithmetic.py
+++ /dev/null
@@ -1,332 +0,0 @@
-import torch.nn as nn
-import torch
-
-from nnodely.basic.relation import ToStream, Stream, toStream
-from nnodely.basic.model import Model
-from nnodely.support.utils import check, enforce_types
-from nnodely.layers.parameter import Parameter, Constant
-from nnodely.support.jsonutils import merge, binary_cheks
-
-
-# Binary operators
-add_relation_name = 'Add'
-sub_relation_name = 'Sub'
-mul_relation_name = 'Mul'
-div_relation_name = 'Div'
-pow_relation_name = 'Pow'
-
-# Unary operators
-neg_relation_name = 'Neg'
-sign_relation_name = 'Sign'
-
-# Merge operator
-sum_relation_name = 'Sum'
-
-class Add(Stream, ToStream):
- """
- Implement the addition function between two tensors.
- (it is also possible to use the classical math operator '+')
-
- See also:
- Official PyTorch Add documentation:
- `torch.add `_
-
- :param input1: the first element of the addition
- :type obj: Tensor
- :param input2: the second element of the addition
- :type obj: Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/add.rst
- """
- @enforce_types
- def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream:
- obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'addition operators (+)')
- super().__init__(add_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [add_relation_name,[obj1.name,obj2.name]]
-
-## TODO: check the scalar dimension, helpful for the offset
-class Sub(Stream, ToStream):
- """
- Implement the subtraction function between two tensors.
- (it is also possible to use the classical math operator '-')
-
- :param input1: the first element of the subtraction
- :type obj: Tensor
- :param input2: the second element of the subtraction
- :type obj: Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst
- """
- @enforce_types
- def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream:
- obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'subtraction operators (-)')
- super().__init__(sub_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [sub_relation_name,[obj1.name,obj2.name]]
-
-class Mul(Stream, ToStream):
- """
- Implement the multiplication function between two tensors.
- (it is also possible to use the classical math operator '*')
-
- :param input1: the first element of the multiplication
- :type obj: Tensor
- :param input2: the second element of the multiplication
- :type obj: Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst
- """
- @enforce_types
- def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream:
- obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'multiplication operators (*)')
- super().__init__(mul_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [mul_relation_name,[obj1.name,obj2.name]]
-
-class Div(Stream, ToStream):
- """
- Implement the division function between two tensors.
- (it is also possible to use the classical math operator '/')
-
- :param input1: the numerator of the division
- :type obj: Tensor
- :param input2: the denominator of the division
- :type obj: Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/div.rst
- """
- @enforce_types
- def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream:
- obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'division operators (/) ')
- super().__init__(div_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [div_relation_name,[obj1.name,obj2.name]]
-
-class Pow(Stream, ToStream):
- """
- Implement the power function given an input and an exponent.
- (it is also possible to use the classical math operator '**')
-
- See also:
- Official PyTorch pow documentation:
- `torch.pow `_
-
- :param input: the base of the power function
- :type obj: Tensor
- :param exp: the exponent of the power function
- :type obj: float or Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst
- """
- @enforce_types
- def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream:
- obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'pow operators (**)')
- super().__init__(pow_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [pow_relation_name,[obj1.name,obj2.name]]
-
-class Neg(Stream, ToStream):
- """
- Implement the negate function given an input.
-
- :param input: the input to negate
- :type obj: Tensor
-
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst
- """
- @enforce_types
- def __init__(self, obj:Stream|Parameter|Constant) -> Stream:
- obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for neg operation.")
- super().__init__(neg_relation_name+str(Stream.count), obj.json, obj.dim)
- self.json['Relations'][self.name] = [neg_relation_name,[obj.name]]
-
-class Sign(Stream, ToStream):
- """
- Implement the sign function given an input.
-
- :param input: the input for the sign function
- :type obj: Tensor
-
- Example:
- >>> x = Sign(x)
- """
- @enforce_types
- def __init__(self, obj:Stream|Parameter|Constant) -> Stream:
- obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for sign operation.")
- super().__init__(sign_relation_name+str(Stream.count), obj.json, obj.dim)
- self.json['Relations'][self.name] = [sign_relation_name,[obj.name]]
-
-class Sum(Stream, ToStream):
- @enforce_types
- def __init__(self, obj:Stream|Parameter|Constant) -> Stream:
- obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for sum operation.")
- obj.dim['dim'] = 1
- super().__init__(sum_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [sum_relation_name,[obj.name]]
-
-class Add_Layer(nn.Module):
- #: :noindex:
- def __init__(self):
- super(Add_Layer, self).__init__()
-
- def forward(self, *inputs):
- results = inputs[0]
- for input in inputs[1:]:
- results = results + input
- return results
-
-def createAdd(name, *inputs):
- """
- :noindex:
- """
- return Add_Layer()
-
-class Sub_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Sub_Layer, self).__init__()
-
- def forward(self, *inputs):
- # Perform element-wise subtraction
- results = inputs[0]
- for input in inputs[1:]:
- results = results - input
- return results
-
-def createSub(self, *inputs):
- """
- :noindex:
- """
- return Sub_Layer()
-
-
-class Mul_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Mul_Layer, self).__init__()
-
- def forward(self, *inputs):
- results = inputs[0]
- for input in inputs[1:]:
- results = results * input
- return results
-
-def createMul(name, *inputs):
- """
- :noindex:
- """
- return Mul_Layer()
-
-class Div_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Div_Layer, self).__init__()
-
- def forward(self, *inputs):
- results = inputs[0]
- for input in inputs[1:]:
- results = results / input
- return results
-
-def createDiv(name, *inputs):
- """
- :noindex:
- """
- return Div_Layer()
-
-class Pow_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Pow_Layer, self).__init__()
-
- def forward(self, *inputs):
- return torch.pow(inputs[0], inputs[1])
-
-def createPow(name, *inputs):
- """
- :noindex:
- """
- return Pow_Layer()
-
-class Neg_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Neg_Layer, self).__init__()
-
- def forward(self, x):
- return -x
-
-def createNeg(self, *inputs):
- """
- :noindex:
- """
- return Neg_Layer()
-
-class Sign_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Sign_Layer, self).__init__()
-
- def forward(self, x):
- return torch.sign(x)
-
-def createSign(self, *inputs):
- """
- :noindex:
- """
- return Sign_Layer()
-
-class Sum_Layer(nn.Module):
- """
- :noindex:
- """
- def __init__(self):
- super(Sum_Layer, self).__init__()
-
- def forward(self, inputs):
- return torch.sum(inputs, dim = 2, keepdim = True)
-
-def createSum(name, *inputs):
- """
- :noindex:
- """
- return Sum_Layer()
-
-setattr(Model, add_relation_name, createAdd)
-setattr(Model, sub_relation_name, createSub)
-setattr(Model, mul_relation_name, createMul)
-setattr(Model, div_relation_name, createDiv)
-setattr(Model, pow_relation_name, createPow)
-
-setattr(Model, neg_relation_name, createNeg)
-setattr(Model, sign_relation_name, createSign)
-
-setattr(Model, sum_relation_name, createSum)
-
-
diff --git a/nnodely/operators/network.py b/nnodely/operators/network.py
deleted file mode 100644
index 2d60497c..00000000
--- a/nnodely/operators/network.py
+++ /dev/null
@@ -1,426 +0,0 @@
-import copy
-from collections import defaultdict
-
-import numpy as np
-import torch, random
-
-from nnodely.support.utils import TORCH_DTYPE, NP_DTYPE, check, enforce_types, tensor_to_list
-from nnodely.basic.modeldef import ModelDef
-
-from nnodely.support.logger import logging, nnLogger
-log = nnLogger(__name__, logging.WARNING)
-
-class Network:
- @enforce_types
- def __init__(self):
- check(type(self) is not Network, TypeError, "Loader class cannot be instantiated directly")
-
- # Models definition
- self._model_def = ModelDef()
- self._model = None
- self._neuralized = False
- self._traced = False
-
- # Model components
- self._states = {}
- self._input_n_samples = {}
- self._input_ns_backward = {}
- self._input_ns_forward = {}
- self._max_samples_backward = None
- self._max_samples_forward = None
- self._max_n_samples = 0
-
- # Dataset information
- self._data_loaded = False
- self._file_count = 0
- self._num_of_samples = {}
- self._data = {}
- self._multifile = {}
-
- # Training information
- self._standard_train_parameters = {
- 'models': None,
- 'train_dataset': None, 'validation_dataset': None,
- 'dataset': None, 'splits': [100, 0, 0],
- 'closed_loop': {}, 'connect': {}, 'step': 0, 'prediction_samples': 0,
- 'shuffle_data': True,
- 'early_stopping': None, 'early_stopping_params': {},
- 'select_model': 'last', 'select_model_params': {},
- 'minimize_gain': {},
- 'num_of_epochs': 100,
- 'train_batch_size': 128, 'val_batch_size': 128,
- 'optimizer': 'Adam',
- 'lr': 0.001, 'lr_param': {},
- 'optimizer_params': [], 'add_optimizer_params': [],
- 'optimizer_defaults': {}, 'add_optimizer_defaults': {}
- }
- self._training = {}
-
- # Save internal
- self._log_internal = False
- self._internals = {}
-
- def _save_internal(self, key, value):
- self._internals[key] = tensor_to_list(value)
-
- def _set_log_internal(self, log_internal:bool):
- self._log_internal = log_internal
-
- def _clean_log_internal(self):
- self._internals = {}
-
- def _remove_virtual_states(self, connect, closed_loop):
- if connect or closed_loop:
- for key in (connect.keys() | closed_loop.keys()):
- if key in self._states.keys():
- del self._states[key]
-
- def _update_state(self, X, out_closed_loop, out_connect):
- for key, value in out_connect.items():
- X[key] = torch.roll(value, shifts=-1, dims=1) ## Roll the time window
- X[key][:, -1, :] = float('inf') ## inf value to make clear that the last state value
- self._states[key] = X[key].clone().detach()
- for key, val in out_closed_loop.items():
- shift = val.shape[1] #+ self._input_ns_forward[key] ## take the output time dimension + forward samples
- X[key] = torch.roll(X[key], shifts=-1, dims=1) ## Roll the time window
- X[key][:, -shift:, :] = val ## substitute with the predicted value
- self._states[key] = X[key].clone().detach()
-
- def _get_gradient_on_inference(self):
- for key, value in self._model_def['Inputs'].items():
- if 'type' in value.keys():
- return True
- return False
-
- def _get_mandatory_inputs(self, connect, closed_loop):
- model_inputs = list(self._model_def['Inputs'].keys())
- non_mandatory_inputs = list(closed_loop.keys()) + list(connect.keys()) + list(self._model_def.recurrentInputs().keys())
- mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
- return mandatory_inputs, non_mandatory_inputs
-
- def _get_batch_indexes(self, datasets:str|list|dict|None, n_samples:int=0, prediction_samples:int=0):
- if datasets is None:
- return []
- batch_indexes = list(range(n_samples))
- if prediction_samples > 0 and not isinstance(datasets, dict):
- datasets = [datasets] if type(datasets) is str else datasets
- forbidden_idxs = []
- n_samples_count = 0
- for dataset in datasets:
- if dataset in self._multifile.keys(): ## i have some forbidden indexes
- for i in self._multifile[dataset]:
- if i+n_samples_count < batch_indexes[-1]:
- forbidden_idxs.extend(range((i+n_samples_count) - prediction_samples, (i+n_samples_count), 1))
- n_samples_count += self._num_of_samples[dataset]
- batch_indexes = [idx for idx in batch_indexes if idx not in forbidden_idxs]
- batch_indexes = batch_indexes[:-prediction_samples]
- return batch_indexes
-
- def _get_data(self, dataset:str|list|dict|None):
- if dataset is None:
- return {}
- if isinstance(dataset, dict):
- self.__check_data_integrity(dataset)
- return dataset
- dataset = [dataset] if type(dataset) is str else dataset
- loaded_datasets = list(self._data.keys())
- check(len([data for data in dataset if data in loaded_datasets]) > 0, KeyError, f'the datasets: {dataset} are not loaded!')
- total_data = defaultdict(list)
- for data in dataset:
- if data not in loaded_datasets:
- log.warning(f'{data} is not loaded. Ignoring this dataset...')
- dataset.remove(data)
- continue
- for k, v in self._data[data].items():
- total_data[k].append(v)
- total_data = {key: np.concatenate(arrays) for key, arrays in total_data.items()}
- total_data = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in total_data.items()}
- return total_data
-
- def _clip_step(self, step, batch_indexes, batch_size):
- clipped_step = copy.deepcopy(step)
- if clipped_step < 0: ## clip the step to zero
- log.warning(f"The step is negative ({clipped_step}). The step is set to zero.", stacklevel=5)
- clipped_step = 0
- if clipped_step > (len(batch_indexes) - batch_size): ## Clip the step to the maximum number of samples
- log.warning(f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes) - batch_size}). The step is set to the maximum number.", stacklevel=5)
- clipped_step = len(batch_indexes) - batch_size
- check((batch_size + clipped_step) > 0, ValueError, f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.")
- return clipped_step
-
- def _clip_batch_size(self, n_samples, batch_size=None):
- batch_size = batch_size if batch_size <= n_samples else max(0, n_samples)
- check((n_samples - batch_size + 1) > 0, ValueError, f"The number of available sample are {n_samples - batch_size + 1}")
- check(batch_size > 0, ValueError, f'The batch_size must be greater than 0.')
- return batch_size
-
- def __split_dataset(self, dataset:str|list|dict, splits:list):
- check(len(splits) == 3, ValueError, '3 elements must be inserted for the dataset split in training, validation and test')
- check(sum(splits) == 100, ValueError, 'Training, Validation and Test splits must sum up to 100.')
- check(splits[0] > 0, ValueError, 'The training split cannot be zero.')
- train_size, val_size, test_size = splits[0] / 100, splits[1] / 100, splits[2] / 100
- XY_train, XY_val, XY_test = {}, {}, {}
- if isinstance(dataset, dict):
- self.__check_data_integrity(dataset)
- num_of_samples = next(iter(dataset.values())).size(0)
- XY_train = {key: value[:round(num_of_samples*train_size), :, :] for key, value in dataset.items()}
- XY_val = {key: value[round(num_of_samples*train_size):round(num_of_samples*(train_size + val_size)), :, :] for key, value in dataset.items()}
- XY_test = {key: value[round(num_of_samples*(train_size + val_size)):, :, :] for key, value in dataset.items()}
- else:
- dataset = [dataset] if type(dataset) is str else dataset
- check(len([data for data in dataset if data in self._data.keys()]) > 0, KeyError, f'the datasets: {dataset} are not loaded!')
- for data in dataset:
- if data not in self._data.keys():
- log.warning(f'{data} is not loaded. The training will continue without this dataset.')
- dataset.remove(data)
-
- num_of_samples = sum([self._num_of_samples[data] for data in dataset])
- n_samples_train, n_samples_val = round(num_of_samples * train_size), round(num_of_samples * val_size)
- n_samples_test = num_of_samples - n_samples_train - n_samples_val
- check(n_samples_train > 0, ValueError, f'The number of train samples {n_samples_train} must be greater than 0.')
- total_data = defaultdict(list)
- for data in dataset:
- for k, v in self._data[data].items():
- total_data[k].append(v)
- total_data = {key: np.concatenate(arrays, dtype=NP_DTYPE) for key, arrays in total_data.items()}
- for key, samples in total_data.items():
- if val_size == 0.0 and test_size == 0.0: ## we have only training set
- XY_train[key] = torch.from_numpy(samples).to(TORCH_DTYPE)
- elif val_size == 0.0 and test_size != 0.0: ## we have only training and test set
- XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
- XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
- elif val_size != 0.0 and test_size == 0.0: ## we have only training and validation set
- XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
- XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
- else: ## we have training, validation and test set
- XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
- XY_val[key] = torch.from_numpy(samples[n_samples_train:-n_samples_test]).to(TORCH_DTYPE)
- XY_test[key] = torch.from_numpy(samples[n_samples_train + n_samples_val:]).to(TORCH_DTYPE)
- return XY_train, XY_val, XY_test
-
- def _get_tag(self, dataset: str | list | dict | None) -> str:
- """
- Helper function to get the tag for a dataset.
- """
- if isinstance(dataset, str):
- return dataset
- elif isinstance(dataset, list):
- return f"{dataset[0]}_{len(dataset)}" if len(dataset) > 1 else f"{dataset[0]}"
- elif isinstance(dataset, dict):
- return "custom_dataset"
- return dataset
-
- def _setup_dataset(self, train_dataset:str|list|dict, validation_dataset:str|list|dict, test_dataset:str|list|dict, dataset:str|list|dict, splits:list):
- if train_dataset is None: ## use the splits
- train_dataset = list(self._data.keys()) if dataset is None else dataset
- return self.__split_dataset(train_dataset, splits)
- else: ## use each dataset
- return self._get_data(train_dataset), self._get_data(validation_dataset), self._get_data(test_dataset)
-
- def __check_data_integrity(self, dataset:dict):
- if bool(dataset):
- check(len(set([t.size(0) for t in dataset.values()])) == 1, ValueError, "All the tensors in the dataset must have the same number of samples.")
- #TODO check why is wrong
- #check(len([t for t in self._model_def['Inputs'].keys() if t in dataset.keys()]) == len(list(self._model_def['Inputs'].keys())), ValueError, "Some inputs are missing.")
- for key, value in dataset.items():
- if key not in self._model_def['Inputs']:
- log.warning(f"The key '{key}' is not an input of the network. It will be ignored.")
- else:
- check(isinstance(value, torch.Tensor), TypeError, f"The value of the input '{key}' must be a torch.Tensor.")
- check(value.size(1) == self._model_def['Inputs'][key]['ntot'], ValueError, f"The time size of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['ntot']}, got {value.size(1)}.")
- check(value.size(2) == self._model_def['Inputs'][key]['dim'], ValueError, f"The dimension of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['dim']}, got {value.size(2)}.")
-
- def _get_not_mandatory_inputs(self, data, X, non_mandatory_inputs, remaning_indexes, batch_size, step, shuffle = False):
- related_indexes = random.sample(remaning_indexes, batch_size) if shuffle else remaning_indexes[:batch_size]
- for num in related_indexes:
- remaning_indexes.remove(num)
- if step > 0:
- if len(remaning_indexes) >= step:
- step_idxs = random.sample(remaning_indexes, step) if shuffle else remaning_indexes[:step]
- for num in step_idxs:
- remaning_indexes.remove(num)
- else:
- remaning_indexes.clear()
- for key in non_mandatory_inputs:
- if key in data.keys(): ## with data
- X[key] = data[key][related_indexes]
- else: ## with zeros
- window_size = self._input_n_samples[key]
- dim = self._model_def['Inputs'][key]['dim']
- if 'type' in self._model_def['Inputs'][key]:
- X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True)
- else:
- X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
- self._states[key] = X[key]
- return related_indexes
-
- def _inference(self, data, n_samples, batch_size, loss_gains, loss_functions,
- shuffle = False, optimizer = None,
- total_losses = None, A = None, B = None):
- if shuffle:
- randomize = torch.randperm(n_samples)
- data = {key: val[randomize] for key, val in data.items()}
-
- ## Initialize the train losses vector
- aux_losses = torch.zeros([len(self._model_def['Minimizers']), n_samples // batch_size])
- for idx in range(0, (n_samples - batch_size + 1), batch_size):
- ## Build the input tensor
- XY = {}
- for key, val in data.items():
- if self._model_def['Inputs'].get(key, None):
- if self._model_def['Inputs'][key].get('type', None) == 'derivate':
- XY[key] = val[idx:idx + batch_size].detach().requires_grad_(True)
- else:
- XY[key] = val[idx:idx + batch_size]
- #XY = {key: val[idx:idx + batch_size].detach().requires_grad_(True) for key, val in data.items()}
- for key in self._model_def.recurrentInputs().keys():
- if key not in XY.keys():
- window_size = self._input_n_samples[key]
- dim = self._model_def['Inputs'][key]['dim']
- XY[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True)
- ## Reset gradient
- if optimizer:
- optimizer.zero_grad()
- ## Model Forward
- out, minimize_out, _, _ = self._model(XY) ## Forward pass
-
- if self._log_internal:
- internals_dict = {'XY': tensor_to_list(XY), 'out': out, 'param': self._model.all_parameters}
-
- ## Loss Calculation
- total_loss = 0
- for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()):
- if A is not None:
- A[key].append(minimize_out[value['A']].detach().numpy())
- if B is not None:
- B[key].append(minimize_out[value['B']].detach().numpy())
- loss = loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
- loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss
- if total_losses is not None:
- total_losses[key].append(loss.detach().numpy())
- aux_losses[ind][idx // batch_size] = loss.item()
- total_loss += loss
-
- if self._log_internal:
- self._save_internal('inout_' + str(idx), internals_dict)
-
- ## Gradient step
- if optimizer:
- total_loss.backward() ## Backpropagate the error
- optimizer.step()
- self.visualizer.showWeightsInTrain(batch=idx // batch_size)
-
- ## return the losses
- return aux_losses
-
- def _recurrent_inference(self, data, batch_indexes, batch_size, loss_gains, prediction_samples,
- step, non_mandatory_inputs, mandatory_inputs, loss_functions,
- shuffle = False, optimizer = None,
- total_losses = None, A = None, B = None, idxs = None):
- indexes = copy.deepcopy(batch_indexes)
- aux_losses = torch.zeros([len(self._model_def['Minimizers']), round((len(indexes) + step) / (batch_size + step))])
- X = {}
- batch_idx = 0
- while len(indexes) >= batch_size:
- selected_indexes = self._get_not_mandatory_inputs(data, X, non_mandatory_inputs, indexes, batch_size, step, shuffle)
- horizon_losses = {ind: [] for ind in range(len(self._model_def['Minimizers']))}
- if optimizer:
- optimizer.zero_grad() ## Reset the gradient
-
- for horizon_idx in range(prediction_samples + 1):
- # Save the indexes
- if idxs is not None:
- idxs[horizon_idx].append([idx + horizon_idx for idx in selected_indexes])
- ## Get data
- for key in mandatory_inputs:
- X[key] = data[key][[idx + horizon_idx for idx in selected_indexes]]
- ## Forward pass
- out, minimize_out, out_closed_loop, out_connect = self._model(X)
-
- if self._log_internal:
- internals_dict = {'XY': tensor_to_list(X), 'out': out, 'param': self._model.all_parameters,
- 'closedLoop': self._model.closed_loop_update, 'connect': self._model.connect_update}
-
- ## Loss Calculation
- for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()):
- if A is not None:
- A[key][horizon_idx].append(minimize_out[value['A']].detach().numpy())
- if B is not None:
- B[key][horizon_idx].append(minimize_out[value['B']].detach().numpy())
- loss = loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
- loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss
- horizon_losses[ind].append(loss)
-
- ## Update
- self._update_state(X, out_closed_loop, out_connect)
-
- if self._log_internal:
- internals_dict['state'] = self._states
- self._save_internal('inout_' + str(batch_idx) + '_' + str(horizon_idx), internals_dict)
-
- ## Calculate the total loss
- total_loss = 0
- for ind, key in enumerate(self._model_def['Minimizers'].keys()):
- loss = sum(horizon_losses[ind]) / (prediction_samples + 1)
- aux_losses[ind][batch_idx] = loss.item()
- if total_losses is not None:
- total_losses[key].append(loss.detach().numpy())
- total_loss += loss
-
- ## Gradient Step
- if optimizer:
- total_loss.backward() ## Backpropagate the error
- optimizer.step()
- self.visualizer.showWeightsInTrain(batch=batch_idx)
- batch_idx += 1
-
- ## return the losses
- return aux_losses
-
- def _setup_recurrent_variables(self, prediction_samples, closed_loop, connect):
- ## Prediction samples
- check(prediction_samples == 'auto' or prediction_samples >= -1, KeyError, "The sample horizon must be positive, -1, 'auto', for disconnect connection!")
- ## Close loop information
- for input, output in closed_loop.items():
- check(input in self._model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.')
- check(output in self._model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network')
- log.info(f'Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples')
- if self._input_ns_forward[input] > 0:
- log.warning(f"Closed loop on variable '{input}' with sample in the future.")
- ## Connect information
- for input, output in connect.items():
- check(input in self._model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.')
- check(output in self._model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network')
- log.info(f'Recurrent train: connecting the input ports {input} with output ports {output} for {prediction_samples} samples')
- if self._input_ns_forward[input] > 0:
- log.warning(f"Connect on variable '{input}' with sample in the future.")
- ## Disable recurrent training if there are no recurrent variables
- if len(connect|closed_loop|self._model_def.recurrentInputs()) == 0:
- if type(prediction_samples) is not str and prediction_samples >= 0:
- log.warning(f"The value of the prediction_samples={prediction_samples} but the network has no recurrent variables.")
- prediction_samples = -1
- return prediction_samples
-
- @enforce_types
- def resetStates(self, states:set={}, *, batch:int=1) -> None:
- """
- Resets the state of all the recurrent inputs of the network to zero.
- Parameters
- ----------
- states : set, optional
- A set of recurrent inputs names to reset. If provided, only those inputs will be resetted.
- batch : int, optional
- The batch size for the reset states. Default is 1.
- """
- if states: ## reset only specific states
- for key in states:
- window_size = self._input_n_samples[key]
- dim = self._model_def['Inputs'][key]['dim']
- self._states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
- else: ## reset all states
- self._states = {}
- for key, state in self._model_def.recurrentInputs().items():
- window_size = self._input_n_samples[key]
- dim = state['dim']
- self._states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
-
diff --git a/nnodely/support/fixstepsolver.py b/nnodely/support/fixstepsolver.py
deleted file mode 100644
index 01158271..00000000
--- a/nnodely/support/fixstepsolver.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from nnodely.layers.parameter import SampleTime
-
-class FixedStepSolver():
- def __init__(self, int_name:str|None = None, der_name:str|None = None):
- self.dt = SampleTime()
- self.int_name = int_name
- self.der_name = der_name
-
-class Euler(FixedStepSolver):
- def __init__(self, int_name:str|None = None, der_name:str|None = None):
- super().__init__(int_name, der_name)
- def integrate(self, obj):
- from nnodely.layers.input import Input
- integral = Input(self.int_name, dimensions=obj.dim['dim'])
- return (integral.last() + obj * self.dt).closedLoop(integral)
-
- def derivate(self, obj):
- from nnodely.layers.input import Input
- obj = Input(self.int_name, dimensions=obj.dim['dim']).connect(obj)
- return (obj.last() - obj.sw([-2, -1])) / self.dt
-
-class Trapezoidal(FixedStepSolver):
- def __init__(self, int_name:str|None = None, der_name:str|None = None):
- super().__init__(int_name, der_name)
- def integrate(self, obj):
- from nnodely.layers.input import Input
- integral = Input(self.int_name, dimensions=obj.dim['dim'])
- obj = Input(self.der_name, dimensions=obj.dim['dim']).connect(obj)
- return (integral.last() + (obj.last() + obj.sw([-2,-1])) * 0.5 * self.dt).closedLoop(integral)
-
- def derivate(self, obj):
- from nnodely.layers.input import Input
- obj = Input(self.int_name, dimensions=obj.dim['dim']).connect(obj)
- derivative = Input(self.der_name, dimensions=obj.dim['dim'])
- return (((obj.last() - obj.sw([-2, -1])) * 2.0) / self.dt - derivative.last()).closedLoop(derivative)
\ No newline at end of file
diff --git a/nnodely/support/jsonutils.py b/nnodely/support/jsonutils.py
deleted file mode 100644
index ec294fae..00000000
--- a/nnodely/support/jsonutils.py
+++ /dev/null
@@ -1,459 +0,0 @@
-import copy
-from pprint import pformat
-
-
-from nnodely.support.utils import check
-
-from nnodely.support.logger import logging, nnLogger
-log = nnLogger(__name__, logging.WARNING)
-
-def get_window(obj):
- return 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None)
-
-# Codice per comprimere le relazioni
- #print(self.json['Relations'])
- # used_rel = {string for values in self.json['Relations'].values() for string in values[1]}
- # if obj1.name not in used_rel and obj1.name in self.json['Relations'].keys() and self.json['Relations'][obj1.name][0] == add_relation_name:
- # self.json['Relations'][self.name] = [add_relation_name, self.json['Relations'][obj1.name][1]+[obj2.name]]
- # del self.json['Relations'][obj1.name]
- # else:
- # Devo aggiungere un operazione che rimuove un operazione di Add,Sub,Mul,Div se può essere unita ad un'altra operazione dello stesso tipo
- #
-def merge(source, destination, main = True):
- if main:
- for key, value in destination["Functions"].items():
- if key in source["Functions"].keys() and 'n_input' in value.keys() and 'n_input' in source["Functions"][key].keys():
- check(value == {} or source["Functions"][key] == {} or value['n_input'] == source["Functions"][key]['n_input'],
- TypeError,
- f"The ParamFun {key} is present multiple times, with different number of inputs. "
- f"The ParamFun {key} is called with {value['n_input']} parameters and with {source['Functions'][key]['n_input']} parameters.")
- for key, value in destination["Parameters"].items():
- if key in source["Parameters"].keys():
- if 'dim' in value.keys() and 'dim' in source["Parameters"][key].keys():
- check(value['dim'] == source["Parameters"][key]['dim'],
- TypeError,
- f"The Parameter {key} is present multiple times, with different dimensions. "
- f"The Parameter {key} is called with {value['dim']} dimension and with {source['Parameters'][key]['dim']} dimension.")
- window_dest = 'tw' if 'tw' in value else ('sw' if 'sw' in value else None)
- window_source = 'tw' if 'tw' in source["Parameters"][key] else ('sw' if 'sw' in source["Parameters"][key] else None)
- if window_dest is not None:
- check(window_dest == window_source and value[window_dest] == source["Parameters"][key][window_source] ,
- TypeError,
- f"The Parameter {key} is present multiple times, with different window. "
- f"The Parameter {key} is called with {window_dest}={value[window_dest]} dimension and with {window_source}={source['Parameters'][key][window_source]} dimension.")
-
- log.debug("Merge Source")
- log.debug("\n"+pformat(source))
- log.debug("Merge Destination")
- log.debug("\n"+pformat(destination))
- result = copy.deepcopy(destination)
- else:
- result = destination
- for key, value in source.items():
- if isinstance(value, dict):
- # get node or create one
- node = result.setdefault(key, {})
- merge(value, node, False)
- else:
- if key in result and type(result[key]) is list:
- if key == 'tw' or key == 'sw':
- if result[key][0] > value[0]:
- result[key][0] = value[0]
- if result[key][1] < value[1]:
- result[key][1] = value[1]
- else:
- result[key] = value
- if main == True:
- log.debug("Merge Result")
- log.debug("\n" + pformat(result))
- return result
-
-def get_models_json(json):
- model_json = {}
- model_json['Parameters'] = list(json['Parameters'].keys())
- model_json['Constants'] = list(json['Constants'].keys())
- model_json['Inputs'] = list(json['Inputs'].keys())
- model_json['Outputs'] = list(json['Outputs'].keys())
- model_json['Functions'] = list(json['Functions'].keys())
- model_json['Relations'] = list(json['Relations'].keys())
- return model_json
-
-def check_model(json):
- all_inputs = json['Inputs'].keys()
- all_outputs = json['Outputs'].keys()
-
- from nnodely.basic.relation import MAIN_JSON
- subjson = MAIN_JSON
- for name in all_outputs:
- subjson = merge(subjson, subjson_from_output(json, name))
- needed_inputs = subjson['Inputs'].keys()
- extenal_inputs = set(all_inputs) - set(needed_inputs)
-
- check(all_inputs == needed_inputs, RuntimeError,
- f'Connect or close loop operation on the inputs {list(extenal_inputs)}, that are not used in the model.')
- return json
-
-def binary_cheks(self, obj1, obj2, name):
- from nnodely.basic.relation import Stream, toStream
- obj1,obj2 = toStream(obj1),toStream(obj2)
- check(type(obj1) is Stream,TypeError,
- f"The type of {obj1} is {type(obj1)} and is not supported for add operation.")
- check(type(obj2) is Stream,TypeError,
- f"The type of {obj2} is {type(obj2)} and is not supported for add operation.")
- window_obj1 = get_window(obj1)
- window_obj2 = get_window(obj2)
- if window_obj1 is not None and window_obj2 is not None:
- check(window_obj1==window_obj2, TypeError,
- f"For {name} the time window type must match or None but they were {window_obj1} and {window_obj2}.")
- check(obj1.dim[window_obj1] == obj2.dim[window_obj2], ValueError,
- f"For {name} the time window must match or None but they were {window_obj1}={obj1.dim[window_obj1]} and {window_obj2}={obj2.dim[window_obj2]}.")
- check(obj1.dim['dim'] == obj2.dim['dim'] or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError,
- f"For {name} the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.")
- dim = obj1.dim | obj2.dim
- dim['dim'] = max(obj1.dim['dim'], obj2.dim['dim'])
- return obj1, obj2, dim
-
-def subjson_from_relation(json, relation):
- json = copy.deepcopy(json)
- # Get all the inputs needed to compute a specific relation from the json graph
- inputs = set()
- relations = set()
- constants = set()
- parameters = set()
- functions = set()
-
- def search(rel):
- if rel in json['Inputs']: # Found an input
- inputs.add(rel)
- if rel in json['Inputs']:
- if 'connect' in json['Inputs'][rel] and json['Inputs'][rel]['local'] == 1:
- search(json['Inputs'][rel]['connect'])
- if 'closed_loop' in json['Inputs'][rel] and json['Inputs'][rel]['local'] == 1:
- search(json['Inputs'][rel]['closed_loop'])
- # if 'init' in json['Inputs'][rel]:
- # search(json['Inputs'][rel]['init'])
- elif rel in json['Constants']: # Found a constant or parameter
- constants.add(rel)
- elif rel in json['Parameters']:
- parameters.add(rel)
- elif rel in json['Functions']:
- functions.add(rel)
- if 'params_and_consts' in json['Functions'][rel]:
- for sub_rel in json['Functions'][rel]['params_and_consts']:
- search(sub_rel)
- elif rel in json['Relations']: # Another relation
- relations.add(rel)
- for sub_rel in json['Relations'][rel][1]:
- search(sub_rel)
- for sub_rel in json['Relations'][rel][2:]:
- if json['Relations'][rel][0] in ('Fir', 'Linear'):
- search(sub_rel)
- if json['Relations'][rel][0] in ('Fuzzify'):
- search(sub_rel)
- if json['Relations'][rel][0] in ('ParamFun'):
- search(sub_rel)
-
- search(relation)
- from nnodely.basic.relation import MAIN_JSON
- sub_json = copy.deepcopy(MAIN_JSON)
- sub_json['Relations'] = {key: value for key, value in json['Relations'].items() if key in relations}
- sub_json['Inputs'] = {key: value for key, value in json['Inputs'].items() if key in inputs}
- sub_json['Constants'] = {key: value for key, value in json['Constants'].items() if key in constants}
- sub_json['Parameters'] = {key: value for key, value in json['Parameters'].items() if key in parameters}
- sub_json['Functions'] = {key: value for key, value in json['Functions'].items() if key in functions}
- sub_json['Outputs'] = {}
- sub_json['Info'] = {}
- return sub_json
-
-
-def subjson_from_output(json, outputs:str|list):
- json = copy.deepcopy(json)
- from nnodely.basic.relation import MAIN_JSON
- sub_json = copy.deepcopy(MAIN_JSON)
- if type(outputs) is str:
- outputs = [outputs]
- for output in outputs:
- sub_json = merge(sub_json, subjson_from_relation(json,json['Outputs'][output]))
- sub_json['Outputs'][output] = json['Outputs'][output]
- return sub_json
-
-def subjson_from_model(json, models:str|list):
- from nnodely.basic.relation import MAIN_JSON
- json = copy.deepcopy(json)
- sub_json = copy.deepcopy(MAIN_JSON)
- models_names = set([json['Models']]) if type(json['Models']) is str else set(json['Models'].keys())
- if type(models) is str or len(models) == 1:
- if len(models) == 1:
- models = models[0]
- check(models in models_names, AttributeError, f"Model [{models}] not found!")
- if type(json['Models']) is str:
- outputs = set(json['Outputs'].keys())
- else:
- outputs = set(json['Models'][models]['Outputs'])
- sub_json['Models'] = models
- else:
- outputs = set()
- sub_json['Models'] = {}
- for model in models:
- check(model in models_names, AttributeError, f"Model [{model}] not found!")
- outputs |= set(json['Models'][model]['Outputs'])
- sub_json['Models'][model] = {key: value for key, value in json['Models'][model].items()}
-
- # Remove the extern connections not keys in the graph
- final_json = merge(sub_json, subjson_from_output(json, outputs))
- for key, value in final_json['Inputs'].items():
- if 'connect' in value and (value['local'] == 0 and value['connect'] not in final_json['Relations'].keys()):
- del final_json['Inputs'][key]['connect']
- del final_json['Inputs'][key]['local']
- log.warning(f'The input {key} is "connect" outside the model connection removed for subjson')
- if 'closedLoop' in value and (value['local'] == 0 and value['closedLoop'] not in final_json['Relations'].keys()):
- del final_json['Inputs'][key]['closedLoop']
- del final_json['Inputs'][key]['local']
- log.warning(f'The input {key} is "closedLoop" outside the model connection removed for subjson')
- return final_json
-
-def subjson_from_minimize(json, minimizers:str|list):
- from nnodely.basic.relation import MAIN_JSON
- json = copy.deepcopy(json)
- sub_json = copy.deepcopy(MAIN_JSON)
-
- if 'Minimizers' in json:
- rel_A = [json['Minimizers'][key]['A'] for key in minimizers]
- rel_B = [json['Minimizers'][key]['B'] for key in minimizers]
- relations_name = set(rel_A) | set(rel_B)
- for rel_name in relations_name:
- minimizers_json = subjson_from_relation(json, rel_name)
- sub_json = merge(sub_json, minimizers_json)
- sub_json['Minimizers'] = { key : json['Minimizers'][key] for key in minimizers }
-
- return sub_json
-
-def stream_to_str(obj, type = 'Stream'):
- from nnodely.visualizer.emptyvisualizer import color, GREEN
- from pprint import pformat
- stream = f" {type} "
- stream_name = f" {obj.name} {obj.dim} "
-
- title = color((stream).center(80, '='), GREEN, True)
- json = color(pformat(obj.json), GREEN)
- stream = color((stream_name).center(80, '-'), GREEN, True)
- return title + '\n' + json + '\n' + stream
-
-def plot_structure(json, filename='nnodely_graph', library='matplotlib', view=True):
- #json = self.modely.json if json is None else json
- # if json is None:
- # raise ValueError("No JSON model definition provided. Please provide a valid JSON model definition.")
- if library not in ['matplotlib', 'graphviz']:
- raise ValueError("Invalid library specified. Use 'matplotlib' or 'graphviz'.")
- if library == 'matplotlib':
- plot_matplotlib_structure(json, filename, view=view)
- elif library == 'graphviz':
- plot_graphviz_structure(json, filename, view=view)
-
-def plot_matplotlib_structure(json, filename='nnodely_graph', view=True):
- import matplotlib.pyplot as plt
- from matplotlib import patches
- from matplotlib.lines import Line2D
- layer_positions = {}
- x, y = 0, 0 # Initial position
- dy, dx = 1.5, 2.5 # Spacing
-
- ## Layer Inputs:
- for input_name, input_type in json['Inputs'].items():
- layer_positions[input_name] = (x, y)
- y -= dy
- for constant_name in json['Constants'].keys():
- layer_positions[constant_name] = (x, y)
- y -= dy
- y_limit = abs(y)
-
- # Layers Relations:
- available_inputs = list(json['Inputs'].keys() | json['Constants'].keys())
- available_outputs = list(set(json['Outputs'].values()))
- while available_outputs:
- x += dx
- y = 0
- inputs_to_add, outputs_to_remove = [], []
- for relation_name, (relation_type, dependencies, *_) in json['Relations'].items():
- if all(dep in available_inputs for dep in dependencies) and (relation_name not in available_inputs):
- inputs_to_add.append(relation_name)
- if relation_name in available_outputs:
- outputs_to_remove.append(relation_name)
- layer_positions[relation_name] = (x, y)
- y -= dy
- y_limit = max(y_limit, abs(y))
- available_inputs.extend(inputs_to_add)
- available_outputs = [out for out in available_outputs if out not in outputs_to_remove]
-
- ## Layer Outputs:
- x += dx
- y = 0
- for idx, output_name in enumerate(json['Outputs'].keys()):
- layer_positions[output_name] = (x, y)
- y -= dy # Move down for the next input
- x_limit = abs(x)
- y_limit = max(y_limit, abs(y))
-
- # Create the plot
- fig, ax = plt.subplots(figsize=(x_limit, y_limit))
- #fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
-
- # Plot rectangles for each layer
- colors, labels = ['lightgreen', 'lightblue', 'orange', 'lightgray'], ['Inputs', 'Relations', 'Outputs', 'Constants']
- legend_info = [patches.Patch(facecolor=color, edgecolor='black', label=label) for color, label in zip(colors, labels)]
- for layer in (json['Inputs'].keys() | json['Outputs'].keys() | json['Relations'].keys() | json['Constants'].keys()):
- x1, y1 = layer_positions[layer]
- if layer in json['Inputs'].keys():
- color = 'lightgreen'
- tag = f'{layer}\ndim: {json["Inputs"][layer]["dim"]}\nWindow: {json["Inputs"][layer]["ntot"]}'
- elif layer in json['Outputs'].keys():
- color = 'orange'
- tag = layer
- elif layer in json['Constants'].keys():
- color = 'lightgray'
- tag = f'{layer}\ndim: {json["Constants"][layer]["dim"]}'
- else:
- color = 'lightblue'
- tag = f'{json["Relations"][layer][0]}\n({layer})'
- rect = patches.Rectangle((x1, y1), 2, 1, edgecolor='black', facecolor=color)
- ax.add_patch(rect)
- ax.text(x1 + 1, y1 + 0.5, f"{tag}", ha='center', va='center', fontsize=8, fontweight='bold')
-
- # Draw arrows for dependencies
- for layer, (_, dependencies, *_) in json['Relations'].items():
- x1, y1 = layer_positions[layer] # Get position of the current layer
- for dep in dependencies:
- if dep in layer_positions:
- x2, y2 = layer_positions[dep] # Get position of the dependent layer
- ax.annotate("", xy=(x1, y1), xytext=(x2 + 2, y2 + 0.5), arrowprops=dict(arrowstyle="->", color='black', lw=1))
- for out_name, rel_name in json['Outputs'].items():
- x1, y1 = layer_positions[out_name]
- x2, y2 = layer_positions[rel_name]
- ax.annotate("", xy=(x1, y1 + 0.5), xytext=(x2 + 2, y2 + 0.5),
- arrowprops=dict(arrowstyle="->", color='black', lw=1))
- for key, state in json['Inputs'].items():
- if 'closedLoop' in state.keys():
- x1, y1 = layer_positions[key]
- x2, y2 = layer_positions[state['closedLoop']]
- #ax.annotate("", xy=(x2+1, y2), xytext=(x2+1, y_limit), arrowprops=dict(arrowstyle="-", color='red', lw=1, linestyle='dashed'))
- ax.add_patch(patches.FancyArrowPatch((x2+1, y2), (x2+1, -y_limit), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed'))
- ax.add_patch(patches.FancyArrowPatch((x2+1, -y_limit), (x1-1, -y_limit), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed'))
- ax.add_patch(patches.FancyArrowPatch((x1-1, -y_limit), (x1-1, y1+0.5), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed'))
- ax.add_patch(patches.FancyArrowPatch((x1-1, y1+0.5), (x1, y1+0.5), arrowstyle='->', mutation_scale=15, color='red', linestyle='dashed'))
- elif 'connect' in state.keys():
- x1, y1 = layer_positions[key]
- x2, y2 = layer_positions[state['connect']]
- ax.add_patch(patches.FancyArrowPatch((x1, y1), (x2, y2), arrowstyle='->', mutation_scale=15, color='green', linestyle='dashed'))
-
- legend_info.extend([Line2D([0], [0], color='black', lw=2, label='Dependency'),
- Line2D([0], [0], color='red', lw=2, linestyle='dashed', label='Closed Loop'),
- Line2D([0], [0], color='green', lw=2, linestyle='dashed', label='Connect')])
-
- # Adjust the plot limits
- ax.set_xlim(-dx, x_limit+dx)
- ax.set_ylim(-y_limit, dy)
- ax.set_aspect('equal')
- ax.legend(handles=legend_info, loc='lower right')
- ax.axis('off') # Hide axes
-
- plt.title(f"Neural Network Diagram - Sampling [{json['Info']['SampleTime']}]", fontsize=12, fontweight='bold')
- ## Save the figure
- plt.savefig(filename, format="png", bbox_inches='tight')
- if view:
- plt.show()
-
-def plot_graphviz_structure(json, filename='nnodely_graph', view=True): # pragma: no cover
- import shutil
- from graphviz import view
- from graphviz import Digraph
-
- # Check if Graphviz is installed
- if shutil.which('dot') is None:
- # raise RuntimeError(
- # "Graphviz does not appear to be installed on your system. "
- # "Please install it from https://graphviz.org/download/"
- # )
- log.warning(
- "Graphviz does not appear to be installed on your system. "
- "Please install it from https://graphviz.org/download/"
- )
- return
-
- dot = Digraph(comment='Structured Neural Network')
-
- # Set graph attributes for top-down layout and style
- dot.attr(rankdir='LR', size='21')
- dot.attr('node', shape='box', style='filled', color='lightgray', fontname='Helvetica')
-
- # Add metadata/info box
- if 'Info' in json:
- info = json['Info']
- info_text = '\n'.join([f"{k}: {v}" for k, v in info.items()])
- dot.node('INFO_BOX', label=f"Model Info\n{info_text}", shape='note', fillcolor='white', fontsize='10')
-
- # Add input nodes
- for inp, data in json['Inputs'].items():
- dim = data['dim']
- window = data['sw'] if 'sw' in data else data['tw']
- window_tag = 'sw' if 'sw' in data else 'tw'
- label = f"{inp}\nDim: {dim}\nWindow({window_tag}): {window}"
- dot.node(inp, label=label, fillcolor='lightgreen')
- if 'connect' in data.keys():
- dot.edge(data['connect'], inp, label='connect', color='blue', fontcolor='blue')
- if 'closedLoop' in data.keys():
- dot.edge(data['closedLoop'], inp, label='closedLoop', color='red', fontcolor='red')
-
- # Add constant nodes
- if 'Constants' in json:
- for const, data in json['Constants'].items():
- dim = data['dim']
- label = f"{const}\nDim: {dim}"
- dot.node(const, label=label, fillcolor='lightgray')
-
- # Add relation nodes
- for name, rel in json['Relations'].items():
- op_type = rel[0]
- parents = rel[1]
- param1 = rel[2] if len(rel) > 2 else None
- param2 = rel[3] if len(rel) > 3 else None
- label = f"{name}\nType: {op_type}"
- dot.node(name, label=label, fillcolor='lightblue')
- for i in [param1,param2]:
- if isinstance(i, str):
- if i in json['Parameters']:
- param_dim = json['Parameters'][i]['dim']
- dot.node(i, label=f"{i}\nDim: {param_dim}", shape='ellipse', fillcolor='orange')
- dot.edge(i, name, label='Parameter', color='orange', fontcolor='orange')
- elif i in json['Functions']:
- dot.node(i, label=f"{param1}", shape='ellipse', fillcolor='darkorange')
- dot.edge(i, name, label='function', color='darkorange', fontcolor='darkorange')
- for parent in parents:
- dot.edge(parent, name)
-
- # Add output nodes
- for out, rel in json['Outputs'].items():
- dot.node(out, fillcolor='lightcoral')
- dot.edge(rel, out)
-
- # Add Minimize nodes if present
- if 'Minimizers' in json:
- for name, rel in json['Minimizers'].items():
- rel_a, rel_b = rel['A'], rel['B']
- loss = rel['loss']
- dot.node(name, label=f"{name}\nLoss:{loss}", shape='ellipse', fillcolor='purple')
- dot.edge(rel_a, name, label='Minimize', color='purple', fontcolor='purple')
- dot.edge(rel_b, name, label='Minimize', color='purple', fontcolor='purple')
-
- # Add a legend as a subgraph
- # with dot.subgraph(name='cluster_legend') as legend:
- # legend.attr(label='Legend', style='dashed')
- # legend.node('LegendInput', 'Inputs', shape='box', fillcolor='lightgreen', style='filled')
- # legend.node('LegendRel', 'Relation', shape='box', fillcolor='lightblue', style='filled')
- # legend.node('LegendOutput', 'Outputs', shape='box', fillcolor='lightcoral', style='filled')
- # # Hide the edges inside the legend box
- # legend.attr('edge', style='invis')
- # legend.edge('LegendInput', 'LegendRel')
- # legend.edge('LegendRel', 'LegendOutput')
-
- # Render the graph
- dot.render(filename=filename, view=view, format='svg') # opens in default viewer and saves as SVG
\ No newline at end of file
diff --git a/nnodely/support/odeint/dopri5.py b/nnodely/support/odeint/dopri5.py
deleted file mode 100644
index 9d6e3024..00000000
--- a/nnodely/support/odeint/dopri5.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import torch
-from nnodely.support.odeint.rk_solvers import _ButcherTableau, RKAdaptiveStepsizeODESolver
-
-_DORMAND_PRINCE_SHAMPINE_TABLEAU = _ButcherTableau(
- alpha=torch.tensor([1 / 5, 3 / 10, 4 / 5, 8 / 9, 1., 1.], dtype=torch.float32),
- beta=[
- torch.tensor([1 / 5], dtype=torch.float32),
- torch.tensor([3 / 40, 9 / 40], dtype=torch.float32),
- torch.tensor([44 / 45, -56 / 15, 32 / 9], dtype=torch.float32),
- torch.tensor([19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729], dtype=torch.float32),
- torch.tensor([9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656], dtype=torch.float32),
- torch.tensor([35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84], dtype=torch.float32),
- ],
- c_sol=torch.tensor([35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0], dtype=torch.float32),
- c_error=torch.tensor([
- 35 / 384 - 1951 / 21600,
- 0,
- 500 / 1113 - 22642 / 50085,
- 125 / 192 - 451 / 720,
- -2187 / 6784 - -12231 / 42400,
- 11 / 84 - 649 / 6300,
- -1. / 60.,
- ], dtype=torch.float32),
-)
-
-DPS_C_MID = torch.tensor([
- 6025192743 / 30085553152 / 2, 0, 51252292925 / 65400821598 / 2, -2691868925 / 45128329728 / 2,
- 187940372067 / 1594534317056 / 2, -1776094331 / 19743644256 / 2, 11237099 / 235043384 / 2
-], dtype=torch.float32)
-
-
-class Dopri5Solver(RKAdaptiveStepsizeODESolver):
- order = 5
- tableau = _DORMAND_PRINCE_SHAMPINE_TABLEAU
- mid = DPS_C_MID
\ No newline at end of file
diff --git a/nnodely/visualizer/textvisualizer.py b/nnodely/visualizer/textvisualizer.py
deleted file mode 100644
index f574abee..00000000
--- a/nnodely/visualizer/textvisualizer.py
+++ /dev/null
@@ -1,319 +0,0 @@
-import numpy as np
-from pprint import pformat
-
-from nnodely.support.utils import is_notebook
-from nnodely.visualizer.emptyvisualizer import EmptyVisualizer, color, GREEN, RED, BLUE
-
-class TextVisualizer(EmptyVisualizer):
- def __init__(self, verbose=1):
- self.verbose = verbose
-
- def __title(self,msg, lenght = 80):
- print(color((msg).center(lenght, '='), GREEN, True))
-
- def __subtitle(self,msg, lenght = 80):
- print(color((msg).center(lenght, '-'), GREEN, True))
-
- def __line(self):
- print(color('='.center(80, '='),GREEN))
-
- def __singleline(self):
- print(color('-'.center(80, '-'),GREEN))
-
- def __info(self,name, dim =30):
- print(color((name).ljust(dim),BLUE))
-
- def __paramjson(self,name, value, dim =30):
- lines = pformat(value, width=80 - dim).strip().splitlines()
- vai = ('\n' + (' ' * dim)).join(x for x in lines)
- # pformat(value).strip().splitlines().rjust(40)
- print(color((name).ljust(dim) + vai,GREEN))
-
- def __param(self,name, value, dim =30):
- print(color((name).ljust(dim) + value,GREEN))
-
- def showModel(self, model):
- if self.verbose >= 1:
- self.__title(" nnodely Model ")
- print(color(pformat(model),GREEN))
- self.__line()
-
- def showMinimize(self,variable_name):
- if self.verbose >= 2:
- self.__title(f" Minimize Error of {variable_name} between"
- f" {self.modely._model_def['Minimizers'][variable_name]['A']} and"
- f" {self.modely._model_def['Minimizers'][variable_name]['B']} with {self.modely._model_def['Minimizers'][variable_name]['loss']} ")
- self.__line()
-
- def showModelInputWindow(self):
- if self.verbose >= 2:
- input_ns_backward = {key: value['ns'][0] for key, value in self.modely._model_def['Inputs'].items()}
- input_ns_forward = {key: value['ns'][1] for key, value in self.modely._model_def['Inputs'].items()}
- self.__title(" nnodely Model Input Windows ")
- #self.__paramjson("time_window_backward:",self.modely.input_tw_backward)
- #self.__paramjson("time_window_forward:",self.modely.input_tw_forward)
- self.__paramjson("sample_window_backward:", input_ns_backward)
- self.__paramjson("sample_window_forward:", input_ns_forward)
- self.__paramjson("input_n_samples:", self.modely._input_n_samples)
- self.__param("max_samples [backw, forw]:", f"[{self.modely._model_def['Info']['ns'][0]},{self.modely._model_def['Info']['ns'][1]}]")
- self.__param("max_samples total:",f"{self.modely._max_n_samples}")
- self.__line()
-
- def showModelRelationSamples(self):
- if self.verbose >= 2:
- self.__title(" nnodely Model Relation Samples ")
- self.__paramjson("Relation_samples:", self.modely.relation_samples)
- self.__line()
-
- def showBuiltModel(self):
- if self.verbose >= 2:
- self.__title(" nnodely Built Model ")
- print(color(pformat(self.modely._model),GREEN))
- self.__line()
-
- def showWeights(self, weights = None):
- self.__title(" nnodely Models Weights ")
- for key, param in self.modely.parameters.items():
- if weights is None or key in weights:
- self.__paramjson(key,param)
- self.__line()
-
- def showWeightsInTrain(self, batch = None, epoch = None, weights = None):
- if self.verbose >= 2:
- par = self.modely.running_parameters
- dim = len(self.modely._model_def['Minimizers'])
- COLOR = BLUE
- if epoch is not None:
- print(color('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|',COLOR), end='')
- print(color((f' Params end epochs {epoch + 1} ').center(20 * (dim + 1) - 1, '-') + '|',COLOR))
-
- if batch is not None:
- print(color('|' + (f"{batch + 1}").center(10, ' ') + '|', COLOR), end='')
- print(color((f' Params end batch {batch + 1} ').center(20 * (dim + 1) - 1, '-') + '|', COLOR))
-
- for key, param in self.modely.parameters.items():
- if weights is None or key in weights:
- print(color('|' + (f"{key}").center(10, ' ') + '|', COLOR), end='')
- print(color((f'{param}').center(20 * (dim + 1) - 1, ' ') + '|', COLOR))
-
- if epoch is not None:
- print(color('|'+(f'').center(10+20*(dim+1), '-') + '|'))
-
- def showDataset(self, name):
- if self.verbose >= 1:
- self.__title(" nnodely Model Dataset ")
- self.__param("Dataset Name:", name)
- self.__param("Number of files:", f'{self.modely._file_count}')
- self.__param("Total number of samples:", f'{self.modely._num_of_samples[name]}')
- for key in self.modely._model_def['Inputs'].keys():
- if key in self.modely._data[name].keys():
- self.__param(f"Shape of {key}:", f'{self.modely._data[name][key].shape}')
- self.__line()
-
- def showStartTraining(self):
- if self.verbose >= 1:
- par = self.modely.running_parameters
- dim = len(self.modely._model_def['Minimizers'])
- self.__title(" nnodely Training ", 12+(len(self.modely._model_def['Minimizers'])+1)*20)
- print(color('|'+(f'Epoch').center(10,' ')+'|'),end='')
- for key in self.modely._model_def['Minimizers'].keys():
- print(color((f'{key}').center(19, ' ') + '|'), end='')
- print(color((f'Total').center(19, ' ') + '|'))
-
- print(color('|' + (f' ').center(10, ' ') + '|'), end='')
- for key in self.modely._model_def['Minimizers'].keys():
- print(color((f'Loss').center(19, ' ') + '|'),end='')
- print(color((f'Loss').center(19, ' ') + '|'))
-
- print(color('|' + (f' ').center(10, ' ') + '|'), end='')
- for key in self.modely._model_def['Minimizers'].keys():
- if par['n_samples_val']:
- print(color((f'train').center(9, ' ') + '|'),end='')
- print(color((f'val').center(9, ' ') + '|'),end='')
- else:
- print(color((f'train').center(19, ' ') + '|'), end='')
- if par['n_samples_val']:
- print(color((f'train').center(9, ' ') + '|'), end='')
- print(color((f'val').center(9, ' ') + '|'))
- else:
- print(color((f'train').center(19, ' ') + '|'))
-
- print(color('|'+(f'').center(10+20*(dim+1), '-') + '|'))
-
- def showTraining(self, epoch, train_losses, val_losses):
- if self.verbose >= 1:
- eng = lambda val: np.format_float_scientific(val, precision=3)
- par = self.modely.running_parameters
- show_epoch = 1 if par['num_of_epochs'] <= 100 else int(par['num_of_epochs']/100)
- dim = len(self.modely._model_def['Minimizers'])
- if epoch < par['num_of_epochs']:
- print('', end='\r')
- print('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|', end='')
- train_loss = []
- val_loss = []
- for key in self.modely._model_def['Minimizers'].keys():
- train_loss.append(train_losses[key][epoch])
- if val_losses:
- val_loss.append(val_losses[key][epoch])
- print((f'{eng(train_losses[key][epoch])}').center(9, ' ') + '|', end='')
- print((f'{eng(val_losses[key][epoch])}').center(9, ' ') + '|', end='')
- else:
- print((f'{eng(train_losses[key][epoch])}').center(19, ' ') + '|', end='')
-
- if val_losses:
- print((f'{eng(np.mean(train_loss))}').center(9, ' ') + '|', end='')
- print((f'{eng(np.mean(val_loss))}').center(9, ' ') + '|', end='')
- else:
- print((f'{eng(np.mean(train_loss))}').center(19, ' ') + '|', end='')
-
- if (epoch + 1) % show_epoch == 0:
- print('', end='\r')
- print(color('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|'), end='')
- for key in self.modely._model_def['Minimizers'].keys():
- if val_losses:
- print(color((f'{eng(train_losses[key][epoch])}').center(9, ' ') + '|'), end='')
- print(color((f'{eng(val_losses[key][epoch])}').center(9, ' ') + '|'), end='')
- else:
- print(color((f'{eng(train_losses[key][epoch])}').center(19, ' ') + '|'), end='')
- if val_losses:
- print(color((f'{eng(np.mean(train_loss))}').center(9, ' ') + '|'), end='')
- print(color((f'{eng(np.mean(val_loss))}').center(9, ' ') + '|'))
- else:
- print(color((f'{eng(np.mean(train_loss))}').center(19, ' ') + '|'))
-
- if epoch+1 == par['num_of_epochs']:
- print(color('|'+(f'').center(10+20*(dim+1), '-') + '|'))
-
- def showTrainingTime(self, time):
- if self.verbose >= 1:
- self.__title(" nnodely Training Time ")
- self.__param("Total time of Training:", f'{time}')
- self.__line()
-
- def showTrainParams(self):
- if self.verbose >= 1:
- self.__title(" nnodely Model Train Parameters ")
- par = self.modely.getTrainingInfo()
-
- self.__paramjson("models:", par['models'])
- self.__param("num of epochs:", str(par['num_of_epochs']))
- self.__param("update per epochs:", str(par['update_per_epochs']))
- if par['prediction_samples'] >= 0:
- self.__info("â””>len(train_indexes)//(batch_size+step)")
- else:
- self.__info("â””>(n_samples-batch_size)/batch_size+1")
-
- if par['shuffle_data']:
- self.__param('shuffle data:', str(par['shuffle_data']))
-
- if 'early_stopping' in par and par['early_stopping']:
- self.__param('early stopping:', par['early_stopping'])
- self.__paramjson('early stopping params:', par['early_stopping_params'])
-
- if par['prediction_samples'] >= 0:
- self.__param("prediction samples:", f"{par['prediction_samples']}")
- self.__param("step:", f"{par['train_step']}")
- self.__paramjson("closed loop:", par['closed_loop'])
- self.__paramjson("connect:", par['connect'])
-
- self.__param("train dataset:", f"{par['train_tag']}")
- self.__param("\t- batch size:", f"{par['train_batch_size']}")
- self.__param("\t- num of samples:", f"{par['n_samples_train']}")
- if par['prediction_samples'] >= 0:
- self.__param("\t- num of first samples:", f"{par['n_first_samples_train']}")
-
- if par['n_samples_val'] > 0:
- self.__param("validation dataset:", f"{par['val_tag']}")
- self.__param("\t- batch size:", f"{par['val_batch_size']}")
- self.__param("\t- num of samples:", f"{par['n_samples_val']}")
- if par['prediction_samples'] >= 0:
- self.__param("\t- num of first samples:", f"{par['n_first_samples_val']}")
-
- if par['n_samples_test'] > 0:
- self.__param("test dataset:", f"{par['test_tag']}")
- self.__param("\t- num of samples:", f"{par['n_samples_test']}")
- if 'test_batch_size' in par:
- self.__param("\t- batch size:", f"{par['test_batch_size']}")
- if par['prediction_samples'] >= 0:
- self.__param("\t- num of first samples:", f"{par['n_first_samples_test']}")
-
- self.__paramjson('minimizers:', par['minimizers'])
-
- self.__param("optimizer:", par['optimizer'])
- self.__paramjson("optimizer defaults:", par['optimizer_defaults'])
- if par['optimizer_params'] is not None:
- self.__paramjson("optimizer params:", par['optimizer_params'])
-
- self.__line()
-
- def showResult(self, name_data):
- eng = lambda val: np.format_float_scientific(val, precision=3)
- if self.verbose >= 1:
- dim_loss = max(5,len(max(self.modely._model_def['Minimizers'].keys(),key=len)))
- loss_type_list = set([value["loss"] for ind, (key, value) in enumerate(self.modely._model_def['Minimizers'].items())])
- self.__title(f" nnodely Model Results for {name_data} ", dim_loss + 2 + (len(loss_type_list) + 2) * 20)
- print(color('|' + (f'Loss').center(dim_loss, ' ') + '|'), end='')
- for loss in loss_type_list:
- print(color((f'{loss}').center(19, ' ') + '|'), end='')
- print(color((f'FVU').center(19, ' ') + '|'), end='')
- print(color((f'AIC').center(19, ' ') + '|'))
-
- print(color('|' + (f'').center(dim_loss, ' ') + '|'), end='')
- for i in range(len(loss_type_list)):
- print(color((f'small better').center(19, ' ') + '|'), end='')
- print(color((f'small better').center(19, ' ') + '|'), end='')
- print(color((f'lower better').center(19, ' ') + '|'))
-
- print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|'))
- for ind, (key, value) in enumerate(self.modely._model_def['Minimizers'].items()):
- print(color('|'+(f'{key}').center(dim_loss, ' ') + '|'), end='')
- for loss in list(loss_type_list):
- if value["loss"] == loss:
- print(color((f'{eng(self.modely.performance[name_data][key][value["loss"]])}').center(19, ' ') + '|'), end='')
- else:
- print(color((f' ').center(19, ' ') + '|'), end='')
- print(color((f'{eng(self.modely.performance[name_data][key]["fvu"]["total"])}').center(19, ' ') + '|'), end='')
- print(color((f'{eng(self.modely.performance[name_data][key]["aic"]["value"])}').center(19, ' ') + '|'))
-
- print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|'))
- print(color('|'+(f'Total').center(dim_loss, ' ') + '|'), end='')
- print(color((f'{eng(self.modely.performance[name_data]["total"]["mean_error"])}').center(len(loss_type_list)*20-1, ' ') + '|'), end='')
- print(color((f'{eng(self.modely.performance[name_data]["total"]["fvu"])}').center(19, ' ') + '|'), end='')
- print(color((f'{eng(self.modely.performance[name_data]["total"]["aic"])}').center(19, ' ') + '|'))
-
- print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|'))
-
- if self.verbose >= 2:
- self.__title(" Detalied Results ")
- print(color(pformat(self.modely.performance), GREEN))
- self.__line()
-
- def saveModel(self, name, path):
- if self.verbose >= 1:
- self.__title(f" Save {name} ")
- self.__param("Model saved in:", path)
- self.__line()
-
- def loadModel(self, name, path):
- if self.verbose >= 1:
- self.__title(f" Load {name} ")
- self.__param("Model loaded from:", path)
- self.__line()
-
- def exportModel(self, name, path):
- if self.verbose >= 1:
- self.__title(f" Export {name} ")
- self.__param("Model exported in:", path)
- self.__line()
-
- def importModel(self, name, path):
- if self.verbose >= 1:
- self.__title(f" Import {name} ")
- self.__param("Model imported from:", path)
- self.__line()
-
- def exportReport(self, name, path):
- if self.verbose >= 1:
- self.__title(f" Export {name} Report ")
- self.__param("Report exported in:", path)
- self.__line()
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 74e01be3..9a13b455 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,20 +1,21 @@
[build-system]
-requires = ["setuptools>=61", "wheel"]
-build-backend = "setuptools.build_meta"
+requires = ["uv_build>=0.11.6,<0.12.0"]
+build-backend = "uv_build"
[project]
name = "nnodely"
+version = "1.5.5.dev1"
description = "Model-structured neural network framework for the modeling and control of physical systems"
readme = "README.md"
-requires-python = ">=3.10, <3.13"
-license = {file = "LICENSE"}
+requires-python = ">=3.10,<3.14"
+license = "MIT"
+license-files = ["LICENSE"]
authors = [
- {name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com"}
+ { name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com" },
]
classifiers = [
"Programming Language :: Python :: 3",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent"
+ "Operating System :: OS Independent",
]
dependencies = [
"numpy == 1.26.4; platform_machine == 'x86_64' and python_version == '3.10'",
@@ -26,24 +27,18 @@ dependencies = [
"reportlab",
"matplotlib",
"onnxruntime",
- "graphviz"
+ "graphviz",
]
-dynamic = ["version"]
[project.urls]
"Homepage" = "https://github.com/tonegas/nnodely"
+"Repository" = "https://github.com/tonegas/nnodely"
-[tool.setuptools]
-packages = ["nnodely",
- "nnodely.basic",
- "nnodely.exporter",
- "nnodely.layers",
- "nnodely.operators",
- "nnodely.support",
- "nnodely.visualizer",
- "nnodely.visualizer.dynamicmpl",
- "mplplots"]
+[dependency-groups]
+dev = ["pre-commit>=4.5.1", "pytest-cov>=7.1.0", "ruff>=0.15.10"]
-#[tool.setuptools.data-files]
-#"imgs" = ["imgs/*"]
-#"data" = ["tests/data/*","tests/test_data/*","tests/val_data/*","tests/vector_data/*"]
\ No newline at end of file
+[[tool.uv.index]]
+name = "testpypi"
+url = "https://test.pypi.org/simple/"
+publish-url = "https://test.pypi.org/legacy/"
+explicit = true
diff --git a/setup.py b/setup.py
index 0ceefd2c..7f68bd37 100644
--- a/setup.py
+++ b/setup.py
@@ -10,18 +10,20 @@
# with open(version_file, 'w') as f:
# f.write(content_new)
+
def read_version():
- version_file = os.path.join(os.path.dirname(__file__), 'nnodely', '__init__.py')
- with open(version_file, 'r') as f:
+ version_file = os.path.join(os.path.dirname(__file__), "nnodely", "__init__.py")
+ with open(version_file, "r") as f:
for line in f:
- if line.startswith('__version__'):
+ if line.startswith("__version__"):
delim = '"' if '"' in line else "'"
return line.split(delim)[1]
raise RuntimeError("Unable to find version string.")
+
setup(
- name='nnodely',
+ name="nnodely",
version=read_version(),
packages=find_packages(exclude=["docs*", "tests*", "imgs*"]),
- include_package_data=True
-)
\ No newline at end of file
+ include_package_data=True,
+)
diff --git a/nnodely/__init__.py b/src/nnodely/__init__.py
similarity index 51%
rename from nnodely/__init__.py
rename to src/nnodely/__init__.py
index 26201190..a08714a0 100644
--- a/nnodely/__init__.py
+++ b/src/nnodely/__init__.py
@@ -14,7 +14,15 @@
from nnodely.layers.trigonometric import Sin, Cos, Tan, Cosh, Tanh, Sech
from nnodely.layers.parametricfunction import ParamFun
from nnodely.layers.fuzzify import Fuzzify
-from nnodely.layers.part import Part, Select, Concatenate, SamplePart, SampleSelect, TimePart, TimeConcatenate
+from nnodely.layers.part import (
+ Part,
+ Select,
+ Concatenate,
+ SamplePart,
+ SampleSelect,
+ TimePart,
+ TimeConcatenate,
+)
from nnodely.layers.localmodel import LocalModel
from nnodely.layers.equationlearner import EquationLearner
from nnodely.layers.timeoperation import Integrate, Differentiate
@@ -37,43 +45,85 @@
major, minor = sys.version_info.major, sys.version_info.minor
logger.LOG_LEVEL = logging.INFO
-__version__ = '1.5.4'
+__version__ = "1.5.4"
if major < 3:
- sys.exit("Sorry, Python 2 is not supported. You need Python >= 3.10 for "+__package__+".")
+ sys.exit(
+ "Sorry, Python 2 is not supported. You need Python >= 3.10 for "
+ + __package__
+ + "."
+ )
elif minor < 9:
- sys.exit("Sorry, You need Python >= 3.10 for "+__package__+".")
+ sys.exit("Sorry, You need Python >= 3.10 for " + __package__ + ".")
else:
- print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>' +
- f' {__package__}_v{__version__} '.center(20, '-') +
- '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')
+ print(
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+ + f" {__package__}_v{__version__} ".center(20, "-")
+ + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ )
__all__ = [
- 'nnodely', 'Modely', 'clearNames',
- 'Input', 'Connect', 'ClosedLoop',
- 'Parameter', 'Constant', 'SampleTime',
- 'Output',
- 'Relu', 'ELU', 'Softmax', 'Sigmoid', 'Identity',
- 'Fir',
- 'Linear',
- 'NeuralODE',
- 'Add', 'Sum', 'Sub', 'Mul', 'Div', 'Pow', 'Neg', 'Sign',
- 'Sin', 'Cos', 'Tan', 'Cosh', 'Tanh', 'Sech',
- 'ParamFun',
- 'Fuzzify',
- 'Part', 'Select', 'Concatenate',
- 'SamplePart', 'SampleSelect',
- 'TimePart', 'TimeConcatenate',
- 'LocalModel',
- 'EquationLearner',
- 'Integrate', 'Differentiate',
- 'Interpolation',
- 'ForwardEuler', 'RK2', 'RK4',
- 'TextVisualizer', 'MPLVisualizer', 'MPLNotebookVisualizer',
- 'StandardExporter',
- 'SGD', 'Adam', 'Optimizer',
- 'init_negexp', 'init_lin', 'init_constant', 'init_exp',
+ "nnodely",
+ "Modely",
+ "clearNames",
+ "Input",
+ "Connect",
+ "ClosedLoop",
+ "Parameter",
+ "Constant",
+ "SampleTime",
+ "Output",
+ "Relu",
+ "ELU",
+ "Softmax",
+ "Sigmoid",
+ "Identity",
+ "Fir",
+ "Linear",
+ "NeuralODE",
+ "Add",
+ "Sum",
+ "Sub",
+ "Mul",
+ "Div",
+ "Pow",
+ "Neg",
+ "Sign",
+ "Sin",
+ "Cos",
+ "Tan",
+ "Cosh",
+ "Tanh",
+ "Sech",
+ "ParamFun",
+ "Fuzzify",
+ "Part",
+ "Select",
+ "Concatenate",
+ "SamplePart",
+ "SampleSelect",
+ "TimePart",
+ "TimeConcatenate",
+ "LocalModel",
+ "EquationLearner",
+ "Integrate",
+ "Differentiate",
+ "Interpolation",
+ "ForwardEuler",
+ "RK2",
+ "RK4",
+ "TextVisualizer",
+ "MPLVisualizer",
+ "MPLNotebookVisualizer",
+ "StandardExporter",
+ "SGD",
+ "Adam",
+ "Optimizer",
+ "init_negexp",
+ "init_lin",
+ "init_constant",
+ "init_exp",
# Main nnodely classes
- '__version__'
+ "__version__",
]
diff --git a/nnodely/basic/__init__.py b/src/nnodely/basic/__init__.py
similarity index 100%
rename from nnodely/basic/__init__.py
rename to src/nnodely/basic/__init__.py
diff --git a/src/nnodely/basic/loss.py b/src/nnodely/basic/loss.py
new file mode 100644
index 00000000..2ec49f3b
--- /dev/null
+++ b/src/nnodely/basic/loss.py
@@ -0,0 +1,36 @@
+import torch.nn as nn
+import torch
+from nnodely.support.utils import check
+
+available_losses = ["mse", "rmse", "mae", "cross_entropy"]
+
+
+class CustomLoss(nn.Module):
+ def __init__(self, loss_type="mse", **kwargs):
+ super(CustomLoss, self).__init__()
+ check(
+ loss_type in available_losses,
+ TypeError,
+ f'The "{loss_type}" loss is not available. Possible losses are: {available_losses}.',
+ )
+ self.loss_type = loss_type
+ self.loss = nn.MSELoss(**kwargs)
+ if callable(loss_type):
+ self.loss = loss_type
+ elif self.loss_type == "mae":
+ self.loss = nn.L1Loss(**kwargs)
+ elif self.loss_type == "cross_entropy":
+ self.loss = nn.CrossEntropyLoss(**kwargs)
+
+ def forward(self, inA, inB):
+ if self.loss_type == "cross_entropy":
+ inB = (
+ inB.squeeze().float()
+ if inA.shape == inB.shape
+ else inB.squeeze().long()
+ )
+ inA = inA.squeeze()
+ res = self.loss(inA, inB)
+ if self.loss_type == "rmse":
+ res = torch.sqrt(res)
+ return res
diff --git a/src/nnodely/basic/model.py b/src/nnodely/basic/model.py
new file mode 100644
index 00000000..4937bd9e
--- /dev/null
+++ b/src/nnodely/basic/model.py
@@ -0,0 +1,324 @@
+import torch
+import copy
+
+import torch.nn as nn
+import numpy as np
+
+from itertools import product
+
+from nnodely.support.utils import TORCH_DTYPE
+from nnodely.support import initializer
+
+
+@torch.fx.wrap
+def update_state(data_in, rel):
+ # virtual = torch.roll(data_in, shifts=-1, dims=1)
+ max_dim = min(rel.size(1), data_in.size(1))
+ data_out = data_in.clone()
+ data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]
+ return data_out
+
+
+class Model(nn.Module):
+ def __init__(self, model_def):
+ super(Model, self).__init__()
+ model_def = copy.deepcopy(model_def)
+
+ self.states = {
+ key: value
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" in value.keys() or "connect" in value.keys())
+ }
+
+ self.inputs = model_def["Inputs"]
+ self.outputs = model_def["Outputs"]
+ self.relations = model_def["Relations"]
+ self.params = model_def["Parameters"]
+ self.constants = model_def["Constants"]
+ self.sample_time = model_def["Info"]["SampleTime"]
+ self.functions = model_def["Functions"]
+
+ self.minimizers = model_def["Minimizers"] if "Minimizers" in model_def else {}
+ self.minimizers_keys = [
+ self.minimizers[key]["A"] for key in self.minimizers
+ ] + [self.minimizers[key]["B"] for key in self.minimizers]
+
+ self.input_ns_backward = {
+ key: value["ns"][0] for key, value in model_def["Inputs"].items()
+ }
+ self.input_n_samples = {
+ key: value["ntot"] for key, value in model_def["Inputs"].items()
+ }
+
+ ## Build the network
+ self.all_parameters = {}
+ self.all_constants = {}
+ self.relation_forward = {}
+ self.relation_inputs = {}
+ self.closed_loop_update = {}
+ self.connect_update = {}
+
+ ## Update the connect_update and closed_loop_update
+ self.update()
+
+ ## Define the correct slicing
+ for _, items in self.relations.items():
+ if items[0] == "SamplePart":
+ if items[1][0] in self.inputs.keys():
+ items[3][0] = self.input_ns_backward[items[1][0]] + items[3][0]
+ items[3][1] = self.input_ns_backward[items[1][0]] + items[3][1]
+ if len(items) > 4: ## Offset
+ items[4] = self.input_ns_backward[items[1][0]] + items[4]
+ if items[0] == "TimePart":
+ if items[1][0] in self.inputs.keys():
+ items[3][0] = self.input_ns_backward[items[1][0]] + round(
+ items[3][0] / self.sample_time
+ )
+ items[3][1] = self.input_ns_backward[items[1][0]] + round(
+ items[3][1] / self.sample_time
+ )
+ if len(items) > 4: ## Offset
+ items[4] = self.input_ns_backward[items[1][0]] + round(
+ items[4] / self.sample_time
+ )
+ else:
+ items[3][0] = round(items[3][0] / self.sample_time)
+ items[3][1] = round(items[3][1] / self.sample_time)
+ if len(items) > 4: ## Offset
+ items[4] = round(items[4] / self.sample_time)
+
+ ## Create all the parameters
+ for name, param_data in self.params.items():
+ window = (
+ "tw"
+ if "tw" in param_data.keys()
+ else ("sw" if "sw" in param_data.keys() else None)
+ )
+ aux_sample_time = self.sample_time if "tw" == window else 1
+ sample_window = (
+ round(param_data[window] / aux_sample_time) if window else None
+ )
+ if sample_window is None:
+ param_size = (
+ tuple(param_data["dim"])
+ if type(param_data["dim"]) is list
+ else (param_data["dim"],)
+ )
+ else:
+ param_size = (
+ (sample_window,) + tuple(param_data["dim"])
+ if type(param_data["dim"]) is list
+ else (sample_window, param_data["dim"])
+ )
+ if "values" in param_data:
+ self.all_parameters[name] = nn.Parameter(
+ torch.tensor(param_data["values"], dtype=TORCH_DTYPE),
+ requires_grad=True,
+ )
+ # TODO clean code
+ elif "init_fun" in param_data:
+ if "code" in param_data["init_fun"].keys():
+ exec(param_data["init_fun"]["code"], globals())
+ function_to_call = globals()[param_data["init_fun"]["name"]]
+ else:
+ function_to_call = getattr(
+ initializer, param_data["init_fun"]["name"]
+ )
+ values = np.zeros(param_size)
+ for indexes in product(*(range(v) for v in param_size)):
+ if "params" in param_data["init_fun"]:
+ values[indexes] = function_to_call(
+ indexes, param_size, param_data["init_fun"]["params"]
+ )
+ else:
+ values[indexes] = function_to_call(indexes, param_size)
+ self.all_parameters[name] = nn.Parameter(
+ torch.tensor(values.tolist(), dtype=TORCH_DTYPE), requires_grad=True
+ )
+ else:
+ self.all_parameters[name] = nn.Parameter(
+ torch.rand(size=param_size, dtype=TORCH_DTYPE), requires_grad=True
+ )
+
+ ## Create all the constants
+ for name, param_data in self.constants.items():
+ self.all_constants[name] = nn.Parameter(
+ torch.tensor(param_data["values"], dtype=TORCH_DTYPE),
+ requires_grad=False,
+ )
+ all_params_and_consts = self.all_parameters | self.all_constants
+
+ ## Create all the relations
+ for relation, inputs in self.relations.items():
+ ## Take the relation name and the inputs needed to solve the relation
+ rel_name, input_var = inputs[0], inputs[1]
+ ## Create All the Relations
+ func = getattr(self, rel_name)
+ if func:
+ layer_inputs = []
+ for item in inputs[2:]:
+ if item in list(
+ self.params.keys()
+ ): ## the relation takes parameters
+ layer_inputs.append(self.all_parameters[item])
+ elif item in list(
+ self.constants.keys()
+ ): ## the relation takes a constant
+ layer_inputs.append(self.all_constants[item])
+ elif item in list(
+ self.functions.keys()
+ ): ## the relation takes a custom function
+ layer_inputs.append(self.functions[item])
+ if (
+ "params_and_consts" in self.functions[item].keys()
+ and len(self.functions[item]["params_and_consts"]) >= 0
+ ): ## Parametric function that takes parameters
+ layer_inputs.append(
+ [
+ all_params_and_consts[par]
+ for par in self.functions[item]["params_and_consts"]
+ ]
+ )
+ if "map_over_dim" in self.functions[item].keys():
+ layer_inputs.append(self.functions[item]["map_over_dim"])
+ else:
+ layer_inputs.append(item)
+
+ if rel_name == "SamplePart":
+ if layer_inputs[0] == -1:
+ layer_inputs[0] = self.input_n_samples[input_var[0]]
+ elif rel_name == "TimePart":
+ if layer_inputs[0] == -1:
+ layer_inputs[0] = self.input_n_samples[input_var[0]]
+ else:
+ layer_inputs[0] = round(layer_inputs[0] / self.sample_time)
+ ## Initialize the relation
+ self.relation_forward[relation] = func(*layer_inputs)
+ ## Save the inputs needed for the relative relation
+ self.relation_inputs[relation] = input_var
+
+ ## Add the gradient to all the relations and parameters that requires it
+ self.relation_forward = nn.ParameterDict(self.relation_forward)
+ self.all_constants = nn.ParameterDict(self.all_constants)
+ self.all_parameters = nn.ParameterDict(self.all_parameters)
+ ## list of network outputs
+ self.network_output_predictions = set(self.outputs.values())
+ ## list of network minimization outputs
+ self.network_output_minimizers = []
+ for _, value in self.minimizers.items():
+ self.network_output_minimizers.append(self.outputs[value["A"]]) if value[
+ "A"
+ ] in self.outputs.keys() else self.network_output_minimizers.append(
+ value["A"]
+ )
+ self.network_output_minimizers.append(self.outputs[value["B"]]) if value[
+ "B"
+ ] in self.outputs.keys() else self.network_output_minimizers.append(
+ value["B"]
+ )
+ self.network_output_minimizers = set(self.network_output_minimizers)
+ ## list of all the network Outputs
+ self.network_outputs = self.network_output_predictions.union(
+ self.network_output_minimizers
+ )
+
+ def forward(self, kwargs):
+ result_dict = {}
+
+ ## Initially i have only the inputs from the dataset, the parameters, and the constants
+ available_inputs = [
+ key for key in self.inputs.keys() if key not in self.connect_update.keys()
+ ] ## remove connected inputs
+ available_keys = set(
+ available_inputs
+ + list(self.all_parameters.keys())
+ + list(self.all_constants.keys())
+ )
+
+ ## Forward pass through the relations
+ while not self.network_outputs.issubset(
+ available_keys
+ ): ## i need to climb the relation tree until i get all the outputs
+ for relation in self.relations.keys():
+ ## if i have all the variables i can calculate the relation
+ if set(self.relation_inputs[relation]).issubset(available_keys) and (
+ relation not in available_keys
+ ):
+ ## Collect all the necessary inputs for the relation
+ layer_inputs = []
+ for key in self.relation_inputs[relation]:
+ if (
+ key in self.all_constants.keys()
+ ): ## relation that takes a constant
+ layer_inputs.append(self.all_constants[key])
+ elif key in available_inputs: ## relation that takes inputs
+ layer_inputs.append(kwargs[key])
+ elif (
+ key in self.all_parameters.keys()
+ ): ## relation that takes parameters
+ layer_inputs.append(self.all_parameters[key])
+ else: ## relation than takes another relation or a connect variable
+ layer_inputs.append(result_dict[key])
+
+ ## Execute the current relation
+ result_dict[relation] = self.relation_forward[relation](
+ *layer_inputs
+ )
+ available_keys.add(relation)
+
+ ## Check if the relation is inside the connect
+ for connect_input, connect_rel in self.connect_update.items():
+ if relation == connect_rel:
+ result_dict[connect_input] = update_state(
+ kwargs[connect_input], result_dict[relation]
+ )
+ available_keys.add(connect_input)
+
+ ## Return a dictionary with all the connected inputs
+ connect_update_dict = {
+ key: result_dict[key] for key in self.connect_update.keys()
+ }
+ ## Return a dictionary with all the relations that updates the state variables
+ closed_loop_update_dict = {
+ key: result_dict[value] for key, value in self.closed_loop_update.items()
+ }
+ ## Return a dictionary with all the outputs final values
+ output_dict = {key: result_dict[value] for key, value in self.outputs.items()}
+ ## Return a dictionary with the minimization relations
+ minimize_dict = {}
+ for key in self.minimizers_keys:
+ minimize_dict[key] = (
+ result_dict[self.outputs[key]]
+ if key in self.outputs.keys()
+ else result_dict[key]
+ )
+ return output_dict, minimize_dict, closed_loop_update_dict, connect_update_dict
+
+ def update(self, *, closed_loop={}, connect={}, disconnect=False):
+ self.closed_loop_update = {}
+ self.connect_update = {}
+
+ if disconnect:
+ return
+
+ for key, state in self.states.items():
+ if "connect" in state.keys():
+ self.connect_update[key] = state["connect"]
+ elif "closedLoop" in state.keys():
+ self.closed_loop_update[key] = state["closedLoop"]
+
+ # Get relation from outputs
+ for connect_in, connect_rel in connect.items():
+ set_relation = (
+ self.outputs[connect_rel]
+ if connect_rel in self.outputs.keys()
+ else connect_rel
+ )
+ self.connect_update[connect_in] = set_relation
+ for close_in, close_rel in closed_loop.items():
+ set_relation = (
+ self.outputs[close_rel]
+ if close_rel in self.outputs.keys()
+ else close_rel
+ )
+ self.closed_loop_update[close_in] = set_relation
diff --git a/src/nnodely/basic/modeldef.py b/src/nnodely/basic/modeldef.py
new file mode 100644
index 00000000..9c9e4956
--- /dev/null
+++ b/src/nnodely/basic/modeldef.py
@@ -0,0 +1,345 @@
+import copy
+
+import numpy as np
+
+from nnodely.support.utils import check, check_and_get_list
+from nnodely.support.jsonutils import (
+ merge,
+ subjson_from_model,
+ subjson_from_minimize,
+ check_model,
+ get_models_json,
+)
+from nnodely.basic.relation import MAIN_JSON, Stream, check_names
+from nnodely.layers.output import Output
+from nnodely.layers.input import Input
+
+from nnodely.support.logger import logging, nnLogger
+
+log = nnLogger(__name__, logging.INFO)
+
+
+class ModelDef:
+ def __init__(self, model_def=MAIN_JSON):
+ # Models definition
+ self.__json_base = copy.deepcopy(model_def)
+
+ # Initialize the model definition
+ self.__json = copy.deepcopy(self.__json_base)
+ if "SampleTime" in self.__json["Info"]:
+ self.__sample_time = self.__json["Info"]["SampleTime"]
+ else:
+ self.__sample_time = None
+
+ def __contains__(self, key):
+ return key in self.__json
+
+ def __getitem__(self, key):
+ if key in self.__json:
+ return self.__json[key]
+ else:
+ return None
+
+ def __setitem__(self, key, value):
+ self.__json[key] = value
+
+ def __rebuild_json(self, models_names, minimizers):
+ models_json = subjson_from_model(self.__json, list(models_names))
+ if "Minimizers" in self.__json and len(minimizers) > 0:
+ minimizers_json = subjson_from_minimize(self.__json, list(minimizers))
+ models_json = merge(models_json, minimizers_json)
+ return copy.deepcopy(models_json)
+
+ def recurrentInputs(self):
+ return {
+ key: value
+ for key, value in self.__json["Inputs"].items()
+ if ("closedLoop" in value.keys() or "connect" in value.keys())
+ }
+
+ def getJson(self, models: list | str | None = None) -> dict:
+ if models is None:
+ return copy.deepcopy(self.__json)
+ else:
+ json = subjson_from_model(self.__json, models)
+ check_model(json)
+ return copy.deepcopy(json)
+
+ def getSampleTime(self):
+ check(
+ self.__sample_time is not None,
+ AttributeError,
+ "Sample time is not defined the model is not neuralized!",
+ )
+ return self.__sample_time
+
+ def isDefined(self):
+ return self.__json is not None
+
+ def addConnection(
+ self,
+ stream_out: str | Output | Stream,
+ input_in: str | Input,
+ type: str,
+ local: bool = False,
+ ):
+ outputs = self.__json["Outputs"]
+
+ if isinstance(stream_out, (Output, Stream)):
+ stream_name = (
+ outputs[stream_out.name]
+ if stream_out.name in outputs.keys()
+ else stream_out.name
+ )
+ else:
+ output_name = check_and_get_list(
+ stream_out,
+ set(outputs.keys()),
+ lambda name: f"The name {name} is not part of the available Outputs",
+ )[0]
+ stream_name = outputs[output_name]
+
+ if isinstance(input_in, Input):
+ input_name = input_in.name
+ else:
+ input_name = input_in # TODO Add tests
+
+ input_name = check_and_get_list(
+ input_name,
+ set(self.__json["Inputs"].keys()),
+ lambda name: f"The name {name} is not part of the available Inputs",
+ )[0]
+ stream_name = check_and_get_list(
+ stream_name,
+ set(self.__json["Relations"].keys()),
+ lambda name: f"The name {name} is not part of the available Relations",
+ )[0]
+ self.__json["Inputs"][input_name][type] = stream_name
+ self.__json["Inputs"][input_name]["local"] = int(local)
+
+ def removeConnection(self, name_list: str | list[str]):
+ name_list = check_and_get_list(
+ name_list,
+ set(self.__json["Inputs"].keys()),
+ lambda name: f"The name {name} is not part of the available Inputs",
+ )
+ for input_in in name_list:
+ if "closedLoop" in self.__json["Inputs"][input_in].keys():
+ del self.__json["Inputs"][input_in]["closedLoop"]
+ del self.__json["Inputs"][input_in]["local"]
+ elif "connect" in self.__json["Inputs"][input_in].keys():
+ del self.__json["Inputs"][input_in]["connect"]
+ del self.__json["Inputs"][input_in]["local"]
+ else:
+ raise ValueError(
+ f"The input '{input_in}' has no connection or closed loop defined"
+ )
+
+ def addModel(self, name: str, stream_list):
+ if isinstance(stream_list, Output):
+ stream_list = [stream_list]
+
+ json = MAIN_JSON
+ for stream in stream_list:
+ json = merge(json, stream.json)
+ check_model(json)
+
+ if "Models" not in self.__json:
+ self.__json = merge(self.__json, json)
+ self.__json["Models"] = name
+ else:
+ models_names = (
+ set((self.__json["Models"],))
+ if type(self.__json["Models"]) is str
+ else set(self.__json["Models"].keys())
+ )
+ check_names(name, models_names, "Models")
+ if type(self.__json["Models"]) is str:
+ self.__json["Models"] = {
+ self.__json["Models"]: get_models_json(self.__json)
+ }
+ self.__json = merge(self.__json, json)
+ self.__json["Models"][name] = get_models_json(json)
+
+ def removeModel(self, name_list):
+ if "Models" not in self.__json:
+ raise ValueError("No Models are defined")
+ models_names = (
+ {self.__json["Models"]}
+ if type(self.__json["Models"]) is str
+ else set(self.__json["Models"].keys())
+ )
+ name_list = check_and_get_list(
+ name_list,
+ models_names,
+ lambda name: f"The name {name} is not part of the available models",
+ )
+ models_names -= set(name_list)
+ minimizers = (
+ set(self.__json["Minimizers"].keys())
+ if "Minimizers" in self.__json
+ else None
+ )
+ self.__json = self.__rebuild_json(models_names, minimizers)
+
+ def addMinimize(self, name, streamA, streamB, loss_function="mse"):
+ if "Minimizers" not in self.__json:
+ self.__json["Minimizers"] = {}
+ check_names(name, set(self.__json["Minimizers"].keys()), "Minimizers")
+
+ if isinstance(streamA, str):
+ streamA_name = streamA
+ else:
+ check(
+ isinstance(streamA, (Output, Stream)),
+ TypeError,
+ "streamA must be an instance of Output or Stream",
+ )
+ streamA_name = (
+ streamA.json["Outputs"][streamA.name]
+ if isinstance(streamA, Output)
+ else streamA.name
+ )
+ self.__json = merge(self.__json, streamA.json)
+
+ if isinstance(streamB, str):
+ streamB_name = streamB
+ else:
+ check(
+ isinstance(streamB, (Output, Stream)),
+ TypeError,
+ "streamA must be an instance of Output or Stream",
+ )
+ streamB_name = (
+ streamB.json["Outputs"][streamB.name]
+ if isinstance(streamB, Output)
+ else streamB.name
+ )
+ self.__json = merge(self.__json, streamB.json)
+ # check(streamA.dim == streamB.dim, ValueError, f'Dimension of streamA={streamA.dim} and streamB={streamB.dim} are not equal.')
+
+ self.__json["Minimizers"][name] = {}
+ self.__json["Minimizers"][name]["A"] = streamA_name
+ self.__json["Minimizers"][name]["B"] = streamB_name
+ self.__json["Minimizers"][name]["loss"] = loss_function
+
+ def removeMinimize(self, name_list):
+ if "Minimizers" not in self.__json:
+ raise ValueError("No Minimizers are defined")
+ name_list = check_and_get_list(
+ name_list,
+ self.__json["Minimizers"].keys(),
+ lambda name: f"The name {name} is not part of the available minimizers",
+ )
+ models_names = (
+ {self.__json["Models"]}
+ if type(self.__json["Models"]) is str
+ else set(self.__json["Models"].keys())
+ )
+ remaining_minimizers = (
+ set(self.__json["Minimizers"].keys()) - set(name_list)
+ if "Minimizers" in self.__json
+ else None
+ )
+ self.__json = self.__rebuild_json(models_names, remaining_minimizers)
+
+ def setBuildWindow(self, sample_time=None):
+ check(self.__json is not None, RuntimeError, "No model is defined!")
+ if sample_time is not None:
+ check(
+ sample_time > 0, RuntimeError, "Sample time must be strictly positive!"
+ )
+ self.__sample_time = sample_time
+ else:
+ if self.__sample_time is None:
+ self.__sample_time = 1
+
+ self.__json["Info"] = {"SampleTime": self.__sample_time}
+ if "SampleTime" in self.__json["Constants"]:
+ self.__json["Constants"]["SampleTime"] = {
+ "dim": 1,
+ "values": self.__sample_time,
+ }
+
+ check(self.__json["Inputs"] != {}, RuntimeError, "No model is defined!")
+ json_inputs = self.__json["Inputs"]
+
+ input_ns_backward, input_ns_forward = {}, {}
+ for key, value in json_inputs.items():
+ if "sw" not in value and "tw" not in value:
+ assert False, f"Input '{key}' has no time window or sample window"
+ if "sw" not in value and self.__sample_time is not None:
+ ## check if value['tw'] is a multiple of sample_time
+ absolute_tw = abs(value["tw"][0]) + abs(value["tw"][1])
+ check(
+ round(absolute_tw % self.__sample_time) == 0,
+ ValueError,
+ f"Time window of input '{key}' is not a multiple of sample time. This network cannot be neuralized",
+ )
+ input_ns_backward[key] = round(-value["tw"][0] / self.__sample_time)
+ input_ns_forward[key] = round(value["tw"][1] / self.__sample_time)
+ elif self.__sample_time is not None:
+ if "tw" in value:
+ input_ns_backward[key] = max(
+ round(-value["tw"][0] / self.__sample_time), -value["sw"][0]
+ )
+ input_ns_forward[key] = max(
+ round(value["tw"][1] / self.__sample_time), value["sw"][1]
+ )
+ else:
+ input_ns_backward[key] = -value["sw"][0]
+ input_ns_forward[key] = value["sw"][1]
+ else:
+ check(
+ value["tw"] == [0, 0],
+ RuntimeError,
+ f"Sample time is not defined for input '{key}'",
+ )
+ input_ns_backward[key] = -value["sw"][0]
+ input_ns_forward[key] = value["sw"][1]
+ value["ns"] = [input_ns_backward[key], input_ns_forward[key]]
+ value["ntot"] = sum(value["ns"])
+
+ self.__json["Info"]["ns"] = [
+ max(input_ns_backward.values()),
+ max(input_ns_forward.values()),
+ ]
+ self.__json["Info"]["ntot"] = sum(self.__json["Info"]["ns"])
+ if self.__json["Info"]["ns"][0] < 0:
+ log.warning(
+ f"The input is only in the far past the max_samples_backward is: {self.__json['Info']['ns'][0]}"
+ )
+ if self.__json["Info"]["ns"][1] < 0:
+ log.warning(
+ f"The input is only in the far future the max_sample_forward is: {self.__json['Info']['ns'][1]}"
+ )
+
+ for k, v in (self.__json["Parameters"] | self.__json["Constants"]).items():
+ if "values" in v:
+ window = (
+ "tw" if "tw" in v.keys() else ("sw" if "sw" in v.keys() else None)
+ )
+ if window == "tw":
+ check(
+ np.array(v["values"]).shape[0] == v["tw"] / self.__sample_time,
+ ValueError,
+ f"{k} has a different number of values for this sample time.",
+ )
+ if v["values"] == "SampleTime":
+ v["values"] = self.__sample_time
+
+ def updateParameters(self, model=None, *, clear_model=False):
+ if clear_model:
+ for key in self.__json["Parameters"].keys():
+ if "init_values" in self.__json["Parameters"][key]:
+ self.__json["Parameters"][key]["values"] = self.__json[
+ "Parameters"
+ ][key]["init_values"]
+ elif "values" in self.__json["Parameters"][key]:
+ del self.__json["Parameters"][key]["values"]
+ elif model is not None:
+ for key in self.__json["Parameters"].keys():
+ if key in model.all_parameters:
+ self.__json["Parameters"][key]["values"] = model.all_parameters[
+ key
+ ].tolist()
diff --git a/nnodely/basic/optimizer.py b/src/nnodely/basic/optimizer.py
similarity index 78%
rename from nnodely/basic/optimizer.py
rename to src/nnodely/basic/optimizer.py
index 15889697..9bde7f47 100644
--- a/nnodely/basic/optimizer.py
+++ b/src/nnodely/basic/optimizer.py
@@ -3,6 +3,7 @@
from nnodely.support.utils import check
+
class Optimizer:
"""
Represents an optimizer for training neural network models.
@@ -29,7 +30,8 @@ class Optimizer:
params_to_train : list or None
A list of parameters to be trained.
"""
- def __init__(self, name, optimizer_defaults = {}, optimizer_params = []):
+
+ def __init__(self, name, optimizer_defaults={}, optimizer_params=[]):
"""
Initializes the Optimizer object.
@@ -64,9 +66,9 @@ def set_params_to_train(self, all_params, params_to_train):
if self.optimizer_params == []:
for param_name in self.all_params.keys():
if param_name in self.params_to_train:
- self.optimizer_params.append({'params': param_name})
+ self.optimizer_params.append({"params": param_name})
else:
- self.optimizer_params.append({'params': param_name, 'lr': 0.0})
+ self.optimizer_params.append({"params": param_name, "lr": 0.0})
def set_defaults(self, optimizer_defaults):
"""
@@ -110,18 +112,18 @@ def unfold(self, params):
If the params argument is not a list.
"""
optimizer_params = []
- check(type(params) is list, KeyError, f'The params {params} must be a list')
+ check(type(params) is list, KeyError, f"The params {params} must be a list")
for param in params:
- if type(param['params']) is list:
+ if type(param["params"]) is list:
par_copy = copy.deepcopy(param)
- del par_copy['params']
- for par in param['params']:
- optimizer_params.append({'params':par}|par_copy)
+ del par_copy["params"]
+ for par in param["params"]:
+ optimizer_params.append({"params": par} | par_copy)
else:
optimizer_params.append(param)
return optimizer_params
- def add_defaults(self, option_name, params, overwrite = True):
+ def add_defaults(self, option_name, params, overwrite=True):
"""
Adds default settings to the optimizer.
@@ -140,41 +142,48 @@ def add_defaults(self, option_name, params, overwrite = True):
elif option_name not in self.optimizer_defaults:
self.optimizer_defaults[option_name] = params
- def add_option_to_params(self, option_name, params, overwrite = True):
+ def add_option_to_params(self, option_name, params, overwrite=True):
if params is None:
return
for key, value in params.items():
- check(self.all_params is not None, RuntimeError, "Call set_params before add_option_to_params")
+ check(
+ self.all_params is not None,
+ RuntimeError,
+ "Call set_params before add_option_to_params",
+ )
old_key = False
for param in self.optimizer_params:
- if param['params'] == key:
+ if param["params"] == key:
old_key = True
if overwrite:
param[option_name] = value
elif option_name not in param:
param[option_name] = value
if old_key == False:
- self.optimizer_params.append({'params': key, option_name: value})
+ self.optimizer_params.append({"params": key, option_name: value})
def replace_key_with_params(self):
params = copy.deepcopy(self.optimizer_params)
for param in params:
- if type(param['params']) is list:
- for ind, par in enumerate(param['params']):
- param['params'][ind] = self.all_params[par]
+ if type(param["params"]) is list:
+ for ind, par in enumerate(param["params"]):
+ param["params"][ind] = self.all_params[par]
else:
- param['params'] = self.all_params[param['params']]
+ param["params"] = self.all_params[param["params"]]
return params
def get_torch_optimizer(self):
- raise NotImplemented('The function get_torch_optimizer must be implemented.')
+ raise NotImplementedError(
+ "The function get_torch_optimizer must be implemented."
+ )
+
class SGD(Optimizer):
"""
Stochastic Gradient Descent (SGD) optimizer.
See also:
- Official PyTorch SGD documentation:
+ Official PyTorch SGD documentation:
`torch.optim.SGD `_
Parameters
@@ -201,18 +210,22 @@ class SGD(Optimizer):
nesterov : bool, optional
Enables Nesterov momentum. Default is False.
"""
- def __init__(self, optimizer_defaults = {}, optimizer_params = []):
- super(SGD, self).__init__('SGD', optimizer_defaults, optimizer_params)
+
+ def __init__(self, optimizer_defaults={}, optimizer_params=[]):
+ super(SGD, self).__init__("SGD", optimizer_defaults, optimizer_params)
def get_torch_optimizer(self):
- return torch.optim.SGD(self.replace_key_with_params(), **self.optimizer_defaults)
+ return torch.optim.SGD(
+ self.replace_key_with_params(), **self.optimizer_defaults
+ )
+
class Adam(Optimizer):
"""
Stochastic Gradient Descent (SGD) optimizer.
See also:
- Official PyTorch Adam documentation:
+ Official PyTorch Adam documentation:
`torch.optim.Adam `_
Parameters
@@ -239,8 +252,11 @@ class Adam(Optimizer):
amsgrad : bool, optional
Whether to use the AMSGrad variant of this algorithm. Default is False.
"""
- def __init__(self, optimizer_defaults = {}, optimizer_params = []):
- super(Adam, self).__init__('Adam', optimizer_defaults, optimizer_params)
+
+ def __init__(self, optimizer_defaults={}, optimizer_params=[]):
+ super(Adam, self).__init__("Adam", optimizer_defaults, optimizer_params)
def get_torch_optimizer(self):
- return torch.optim.Adam(self.replace_key_with_params(), **self.optimizer_defaults)
\ No newline at end of file
+ return torch.optim.Adam(
+ self.replace_key_with_params(), **self.optimizer_defaults
+ )
diff --git a/nnodely/basic/relation.py b/src/nnodely/basic/relation.py
similarity index 62%
rename from nnodely/basic/relation.py
rename to src/nnodely/basic/relation.py
index b99a7d50..8e4e2011 100644
--- a/nnodely/basic/relation.py
+++ b/src/nnodely/basic/relation.py
@@ -6,42 +6,56 @@
from nnodely.support.jsonutils import merge, stream_to_str
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
MAIN_JSON = {
- 'Info' : {},
- 'Inputs' : {},
- 'Constants': {},
- 'Parameters' : {},
- 'Functions' : {},
- 'Relations': {},
- 'Outputs': {}
- }
+ "Info": {},
+ "Inputs": {},
+ "Constants": {},
+ "Parameters": {},
+ "Functions": {},
+ "Relations": {},
+ "Outputs": {},
+}
CHECK_NAMES = False if is_notebook() else True
+
def toStream(obj):
from nnodely.layers.parameter import Parameter, Constant
- if type(obj) in (int,float,list,np.ndarray):
- obj = Constant('Constant'+str(NeuObj.count), obj)
- #obj = Stream(obj, MAIN_JSON, {'dim': 1}) if type(obj) in (int, float) else obj
+
+ if type(obj) in (int, float, list, np.ndarray):
+ obj = Constant("Constant" + str(NeuObj.count), obj)
+ # obj = Stream(obj, MAIN_JSON, {'dim': 1}) if type(obj) in (int, float) else obj
if type(obj) is Parameter or type(obj) is Constant:
obj = Stream(obj.name, obj.json, obj.dim)
return obj
-def check_names(name:str, name_list, list_type):
- check(name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag.")
+
+def check_names(name: str, name_list, list_type):
+ check(
+ name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag."
+ )
if CHECK_NAMES == True:
- check(name not in name_list, NameError, f"The name '{name}' is already used as {list_type}.")
+ check(
+ name not in name_list,
+ NameError,
+ f"The name '{name}' is already used as {list_type}.",
+ )
elif name in name_list:
- log.warning(f"The name '{name}' is already in defined as {list_type} but it is overwritten.")
+ log.warning(
+ f"The name '{name}' is already in defined as {list_type} but it is overwritten."
+ )
-class NeuObj():
+
+class NeuObj:
count = 0
names = []
+
@classmethod
@enforce_types
- def clearNames(cls, names:str|list|None=None):
+ def clearNames(cls, names: str | list | None = None):
if names is None:
NeuObj.count = 0
NeuObj.names = []
@@ -54,10 +68,10 @@ def clearNames(cls, names:str|list|None=None):
if names in NeuObj.names:
NeuObj.names.remove(names)
- def __init__(self, name='', json={}, dim=0):
+ def __init__(self, name="", json={}, dim=0):
NeuObj.count += 1
- if name == '':
- name = 'Auto'+str(NeuObj.count)
+ if name == "":
+ name = "Auto" + str(NeuObj.count)
check_names(name, NeuObj.names, "NeuObj")
NeuObj.names.append(name)
self.name = name
@@ -67,63 +81,82 @@ def __init__(self, name='', json={}, dim=0):
else:
self.json = copy.deepcopy(MAIN_JSON)
-class Relation():
+
+class Relation:
def __add__(self, obj):
from nnodely.layers.arithmetic import Add
+
return Add(self, obj)
def __radd__(self, obj):
from nnodely.layers.arithmetic import Add
+
return Add(obj, self)
def __sub__(self, obj):
from nnodely.layers.arithmetic import Sub
+
return Sub(self, obj)
def __rsub__(self, obj):
from nnodely.layers.arithmetic import Sub
+
return Sub(obj, self)
def __truediv__(self, obj):
from nnodely.layers.arithmetic import Div
+
return Div(self, obj)
def __rtruediv__(self, obj):
from nnodely.layers.arithmetic import Div
+
return Div(obj, self)
def __mul__(self, obj):
from nnodely.layers.arithmetic import Mul
+
return Mul(self, obj)
def __rmul__(self, obj):
from nnodely.layers.arithmetic import Mul
+
return Mul(obj, self)
def __pow__(self, obj):
from nnodely.layers.arithmetic import Pow
+
return Pow(self, obj)
def __rpow__(self, obj):
from nnodely.layers.arithmetic import Pow
+
return Pow(obj, self)
def __neg__(self):
from nnodely.layers.arithmetic import Neg
+
return Neg(self)
+
class Stream(Relation):
"""
Represents a stream of data inside the neural network. A Stream is automatically create when you operate over a Input, Parameter, or Constant object.
"""
+
count = 0
+
@classmethod
def resetCount(cls):
Stream.count = 0
- def __init__(self, name, json, dim, count = 1):
+ def __init__(self, name, json, dim, count=1):
Stream.count += count
- check(name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag.")
+ check(
+ name not in ForbiddenTags,
+ NameError,
+ f"The name '{name}' is a forbidden tag.",
+ )
self.name = name
self.json = copy.deepcopy(json)
self.dim = dim
@@ -135,7 +168,13 @@ def __repr__(self):
return self.__str__()
@enforce_types
- def tw(self, tw:float|int|list, offset:float|int|None = None, *, name:str|None = None) -> "Stream":
+ def tw(
+ self,
+ tw: float | int | list,
+ offset: float | int | None = None,
+ *,
+ name: str | None = None,
+ ) -> "Stream":
"""
Selects a time window on Stream. It is possible to create a smaller or bigger time window on the stream.
The Time Window must be in the past not in the future.
@@ -157,19 +196,28 @@ def tw(self, tw:float|int|list, offset:float|int|None = None, *, name:str|None =
"""
from nnodely.layers.input import Input
from nnodely.layers.part import TimePart
+
if name is None:
- name = self.name+"_tw"+str(NeuObj.count)
+ name = self.name + "_tw" + str(NeuObj.count)
if type(tw) is list:
- check(0 >= tw[1] > tw[0] and tw[0] < 0, ValueError, "The dimension of the sample window must be in the past.")
- if 'tw' not in self.dim:
- self.dim['tw'] = 0
+ check(
+ 0 >= tw[1] > tw[0] and tw[0] < 0,
+ ValueError,
+ "The dimension of the sample window must be in the past.",
+ )
+ if "tw" not in self.dim:
+ self.dim["tw"] = 0
if type(tw) is not list:
- tw = [-tw,0]
- delayed_input = Input(name, dimensions=self.dim['dim']).connect(self).tw([tw[0],0], offset)
- return TimePart(delayed_input,tw[0]-tw[0],tw[1]-tw[0])
+ tw = [-tw, 0]
+ delayed_input = (
+ Input(name, dimensions=self.dim["dim"]).connect(self).tw([tw[0], 0], offset)
+ )
+ return TimePart(delayed_input, tw[0] - tw[0], tw[1] - tw[0])
@enforce_types
- def sw(self, sw:int|list, offset:int|None = None, *, name:str|None = None) -> "Stream":
+ def sw(
+ self, sw: int | list, offset: int | None = None, *, name: str | None = None
+ ) -> "Stream":
"""
Selects a sample window on Stream. It is possible to create a smaller or bigger window on the stream.
The Sample Window must be in the past not in the future.
@@ -191,19 +239,26 @@ def sw(self, sw:int|list, offset:int|None = None, *, name:str|None = None) -> "S
"""
from nnodely.layers.input import Input
from nnodely.layers.part import SamplePart
+
if name is None:
- name = self.name+"_sw"+str(NeuObj.count)
+ name = self.name + "_sw" + str(NeuObj.count)
if type(sw) is list:
- check(0 >= sw[1] > sw[0] and sw[0] < 0, ValueError, "The dimension of the sample window must be in the past.")
- if 'sw' not in self.dim:
- self.dim['sw'] = 0
+ check(
+ 0 >= sw[1] > sw[0] and sw[0] < 0,
+ ValueError,
+ "The dimension of the sample window must be in the past.",
+ )
+ if "sw" not in self.dim:
+ self.dim["sw"] = 0
if type(sw) is not list:
- sw = [-sw,0]
- delayed_input = Input(name, dimensions=self.dim['dim']).connect(self).sw([sw[0],0], offset)
- return SamplePart(delayed_input,sw[0]-sw[0],sw[1]-sw[0])
+ sw = [-sw, 0]
+ delayed_input = (
+ Input(name, dimensions=self.dim["dim"]).connect(self).sw([sw[0], 0], offset)
+ )
+ return SamplePart(delayed_input, sw[0] - sw[0], sw[1] - sw[0])
@enforce_types
- def z(self, delay:int|float, *, name:str|None = None) -> "Stream":
+ def z(self, delay: int | float, *, name: str | None = None) -> "Stream":
# TODO fix the convetion z-1 means a dealy z+1 means unitary advance
"""
Considering the Zeta transform notation. The function is used to delay a Stream.
@@ -220,11 +275,15 @@ def z(self, delay:int|float, *, name:str|None = None) -> "Stream":
A Stream representing the delayed Stream
"""
check(delay > 0, ValueError, "The delay must be a positive integer")
- check('sw' in self.dim, TypeError, "The stream is not defined in samples but in time")
- return self.sw([-self.dim['sw']-delay,-delay], name = name)
+ check(
+ "sw" in self.dim,
+ TypeError,
+ "The stream is not defined in samples but in time",
+ )
+ return self.sw([-self.dim["sw"] - delay, -delay], name=name)
@enforce_types
- def delay(self, delay:int|float, *, name:str|None = None) -> "Stream":
+ def delay(self, delay: int | float, *, name: str | None = None) -> "Stream":
"""
The function is used to delay a Stream.
The value of the delay can be only positive.
@@ -240,11 +299,22 @@ def delay(self, delay:int|float, *, name:str|None = None) -> "Stream":
A Stream representing the delayed Stream
"""
check(delay > 0, ValueError, "The delay must be a positive integer")
- check('tw' in self.dim, TypeError, "The stream is not defined in time but in sample")
- return self.tw([-self.dim['tw']-delay,-delay], name = name)
+ check(
+ "tw" in self.dim,
+ TypeError,
+ "The stream is not defined in time but in sample",
+ )
+ return self.tw([-self.dim["tw"] - delay, -delay], name=name)
@enforce_types
- def s(self, order:int, *, int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> "Stream":
+ def s(
+ self,
+ order: int,
+ *,
+ int_name: str | None = None,
+ der_name: str | None = None,
+ method: str = "euler",
+ ) -> "Stream":
"""
Considering the Laplace transform notation. The function is used to operate an integral or derivate operation on a Stream.
The order of the integral or the derivative operation is indicated by the order parameter.
@@ -262,13 +332,20 @@ def s(self, order:int, *, int_name:str|None = None, der_name:str|None = None, me
A Stream of the signal represents the integral or derivation operation.
"""
from nnodely.layers.timeoperation import Differentiate, Integrate
- check(order != 0, ValueError, "The order must be a positive or negative integer not a zero")
+
+ check(
+ order != 0,
+ ValueError,
+ "The order must be a positive or negative integer not a zero",
+ )
if order > 0:
for i in range(order):
- o = Differentiate(self, der_name = der_name, int_name = int_name, method = method)
+ o = Differentiate(
+ self, der_name=der_name, int_name=int_name, method=method
+ )
elif order < 0:
for i in range(-order):
- o = Integrate(self, der_name = der_name, int_name = int_name, method = method)
+ o = Integrate(self, der_name=der_name, int_name=int_name, method=method)
return o
def connect(self, obj) -> "Stream":
@@ -293,13 +370,21 @@ def connect(self, obj) -> "Stream":
If the input variable is already connected.
"""
from nnodely.layers.input import Input
- check(type(obj) is Input, TypeError,
- f"The {obj} must be a Input and not a {type(obj)}.")
+
+ check(
+ type(obj) is Input,
+ TypeError,
+ f"The {obj} must be a Input and not a {type(obj)}.",
+ )
self.json = merge(self.json, obj.json)
- check('closedLoop' not in self.json['Inputs'][obj.name] or 'connect' not in self.json['Inputs'][obj.name], KeyError,
- f"The input variable {obj.name} is already connected.")
- self.json['Inputs'][obj.name]['connect'] = self.name
- self.json['Inputs'][obj.name]['local'] = 1
+ check(
+ "closedLoop" not in self.json["Inputs"][obj.name]
+ or "connect" not in self.json["Inputs"][obj.name],
+ KeyError,
+ f"The input variable {obj.name} is already connected.",
+ )
+ self.json["Inputs"][obj.name]["connect"] = self.name
+ self.json["Inputs"][obj.name]["local"] = 1
return self
def closedLoop(self, obj) -> "Stream":
@@ -324,27 +409,38 @@ def closedLoop(self, obj) -> "Stream":
If the input variable is already connected.
"""
from nnodely.layers.input import Input
- check(type(obj) is Input, TypeError,
- f"The {obj} must be a Input and not a {type(obj)}.")
+
+ check(
+ type(obj) is Input,
+ TypeError,
+ f"The {obj} must be a Input and not a {type(obj)}.",
+ )
self.json = merge(self.json, obj.json)
- check('closedLoop' not in self.json['Inputs'][obj.name] or 'connect' not in self.json['Inputs'][obj.name],
- KeyError,
- f"The input variable {obj.name} is already connected.")
- self.json['Inputs'][obj.name]['closedLoop'] = self.name
- self.json['Inputs'][obj.name]['local'] = 1
+ check(
+ "closedLoop" not in self.json["Inputs"][obj.name]
+ or "connect" not in self.json["Inputs"][obj.name],
+ KeyError,
+ f"The input variable {obj.name} is already connected.",
+ )
+ self.json["Inputs"][obj.name]["closedLoop"] = self.name
+ self.json["Inputs"][obj.name]["local"] = 1
return self
-class ToStream():
+
+class ToStream:
def __new__(cls, *args, **kwargs):
- out = super(ToStream,cls).__new__(cls)
+ out = super(ToStream, cls).__new__(cls)
out.__init__(*args, **kwargs)
- return Stream(out.name,out.json,out.dim,0)
+ return Stream(out.name, out.json, out.dim, 0)
+
-class AutoToStream():
- def __new__(cls, *args, **kwargs):
- if len(args) > 0 and (issubclass(type(args[0]),NeuObj) or type(args[0]) is Stream):
+class AutoToStream:
+ def __new__(cls, *args, **kwargs):
+ if len(args) > 0 and (
+ issubclass(type(args[0]), NeuObj) or type(args[0]) is Stream
+ ):
instance = super().__new__(cls)
- #instance.__init__(**kwargs)
+ # instance.__init__(**kwargs)
instance.__init__()
return instance(args[0])
instance = super().__new__(cls)
diff --git a/nnodely/exporter/__init__.py b/src/nnodely/exporter/__init__.py
similarity index 100%
rename from nnodely/exporter/__init__.py
rename to src/nnodely/exporter/__init__.py
diff --git a/nnodely/exporter/emptyexporter.py b/src/nnodely/exporter/emptyexporter.py
similarity index 56%
rename from nnodely/exporter/emptyexporter.py
rename to src/nnodely/exporter/emptyexporter.py
index 6c6dbcf6..57406d2e 100644
--- a/nnodely/exporter/emptyexporter.py
+++ b/src/nnodely/exporter/emptyexporter.py
@@ -3,9 +3,9 @@
from datetime import datetime
from nnodely.visualizer import EmptyVisualizer
-class EmptyExporter:
- def __init__(self, workspace = None, visualizer = None, save_history = False):
+class EmptyExporter:
+ def __init__(self, workspace=None, visualizer=None, save_history=False):
# Export parameters
if workspace is not None:
self.workspace = workspace
@@ -22,26 +22,28 @@ def __init__(self, workspace = None, visualizer = None, save_history = False):
else:
self.visualizer = EmptyVisualizer()
- def saveTorchModel(self, model, name = 'net', model_folder = None):
+ def saveTorchModel(self, model, name="net", model_folder=None):
pass
- def loadTorchModel(self, name = 'net', model_folder = None):
+ def loadTorchModel(self, name="net", model_folder=None):
pass
- def saveModel(self, model, name = 'net', model_folder = None):
+ def saveModel(self, model, name="net", model_folder=None):
pass
- def loadModel(self, name = 'net', model_folder = None):
+ def loadModel(self, name="net", model_folder=None):
pass
- def exportPythonModel(self, name = 'net', model_folder = None):
+ def exportPythonModel(self, name="net", model_folder=None):
pass
- def importPythonModel(self, name = 'net', model_folder = None):
+ def importPythonModel(self, name="net", model_folder=None):
pass
- def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None):
+ def onnxInference(
+ self, inputs: dict, name: str = "net", model_folder: str | None = None
+ ):
pass
- def exportReport(self, name = 'net', model_folder = None):
- pass
\ No newline at end of file
+ def exportReport(self, name="net", model_folder=None):
+ pass
diff --git a/src/nnodely/exporter/export.py b/src/nnodely/exporter/export.py
new file mode 100644
index 00000000..a5575e10
--- /dev/null
+++ b/src/nnodely/exporter/export.py
@@ -0,0 +1,532 @@
+import sys
+import os
+import torch
+import importlib
+import json
+
+from torch.fx import symbolic_trace
+
+from pprint import PrettyPrinter
+
+
+class JsonPrettyPrinter(PrettyPrinter):
+ def _format(self, object, *args):
+ if isinstance(object, str):
+ width = self._width
+ self._width = sys.maxsize
+ try:
+ super()._format(object.replace("'", '_"_'), *args)
+ finally:
+ self._width = width
+ else:
+ super()._format(object, *args)
+
+
+def save_model(model, model_path):
+ # Export the dictionary as a JSON file
+ with open(model_path, "w") as json_file:
+ # json.dump(self.model_def, json_file, indent=4)
+ json_file.write(
+ JsonPrettyPrinter()
+ .pformat(model)
+ .replace("'", '"')
+ .replace('_"_', "'")
+ .replace("None", "null")
+ .replace("False", "false")
+ .replace("True", "true")
+ )
+ # json_file.write(JsonPrettyPrinter().pformat(model).replace('None','null'))
+ # data = json.dumps(self.model_def)
+ # json_file.write(pformat(data).replace('\\\\n', '\\n').replace('\'', '').replace('(','').replace(')',''))
+ # json_file.write(pformat(data).replace('\'', '\"'))
+
+
+def load_model(model_path):
+ import json
+
+ with open(model_path, "r", encoding="UTF-8") as file:
+ model_def = json.load(file)
+ return model_def
+
+
+def export_python_model(model_def, model, model_path):
+ package_name = __package__.split(".")[0]
+
+ # Get the symbolic tracer
+ with torch.no_grad():
+ trace = symbolic_trace(model)
+
+ recurrent_inputs = {
+ key: value
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" in value.keys() or "connect" in value.keys())
+ }
+ inputs = {
+ key: value
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" not in value.keys() and "connect" not in value.keys())
+ }
+ attributes = sorted(set([line for line in trace.code.split() if "self." in line]))
+
+ saved_functions = []
+
+ with open(model_path, "w") as file:
+ file.write("import torch\n\n")
+
+ ## write the connect wrap function
+ # file.write(f"def {package_name}_basic_model_update_state(data_in, rel):\n")
+ # file.write(" virtual = torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n")
+ # file.write(" max_dim = min(rel.size(1), data_in.size(1))\n")
+ # file.write(" virtual[:, -max_dim:, :] = rel[:, -max_dim:, :]\n")
+ # file.write(" return virtual\n\n")
+ file.write(f"def {package_name}_basic_model_update_state(data_in, rel):\n")
+ file.write(" data_out = data_in.clone()\n")
+ file.write(" max_dim = min(rel.size(1), data_in.size(1))\n")
+ file.write(" data_out[:, -max_dim:, :] = rel[:, -max_dim:, :]\n")
+ file.write(" return data_out\n\n")
+
+ file.write(f"def {package_name}_basic_model_timeshift(data_in):\n")
+ file.write(
+ " return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n\n"
+ )
+
+ for name in model_def["Functions"].keys():
+ if "Fuzzify" in name:
+ if "slicing" not in saved_functions:
+ # file.write("@torch.fx.wrap\n")
+ file.write(
+ f"def {package_name}_layers_fuzzify_slicing(res, i, x):\n"
+ )
+ file.write(" res[:, :, i:i+1] = x\n\n")
+ saved_functions.append("slicing")
+
+ function_name = model_def["Functions"][name]["names"]
+ function_code = model_def["Functions"][name]["functions"]
+ if isinstance(function_code, list):
+ for i, fun_code in enumerate(function_code):
+ if fun_code != "Rectangular" and fun_code != "Triangular":
+ if function_name[i] not in saved_functions:
+ fun_code = fun_code.replace(
+ f"def {function_name[i]}",
+ f"def {package_name}_layers_fuzzify_{function_name[i]}",
+ )
+ # file.write("@torch.fx.wrap\n")
+ file.write(fun_code)
+ file.write("\n")
+ saved_functions.append(function_name[i])
+ else:
+ if (
+ (function_name != "Rectangular")
+ and (function_name != "Triangular")
+ and (function_name not in saved_functions)
+ ):
+ function_code = function_code.replace(
+ f"def {function_name}",
+ f"def {package_name}_layers_fuzzify_{function_name}",
+ )
+ # file.write("@torch.fx.wrap\n")
+ file.write(function_code)
+ file.write("\n")
+ saved_functions.append(function_name)
+
+ elif "ParamFun" in name:
+ function_name = model_def["Functions"][name]["name"]
+ if function_name not in saved_functions:
+ code = model_def["Functions"][name]["code"]
+ code = code.replace(
+ f"def {function_name}",
+ f"def {package_name}_layers_parametricfunction_{function_name}",
+ )
+ file.write(code)
+ file.write("\n")
+ saved_functions.append(function_name)
+
+ elif "NeuralODE" in name:
+ function_name = model_def["Functions"][name]["name"]
+ if function_name not in saved_functions:
+ code = model_def["Functions"][name]["code"]
+ code = code.replace(
+ f"def {function_name}",
+ f"def {package_name}_layers_neuralODE_{function_name}",
+ )
+ file.write(code)
+ file.write("\n")
+ saved_functions.append(function_name)
+
+ file.write("class TracerModel(torch.nn.Module):\n")
+ file.write(" def __init__(self):\n")
+ file.write(" super().__init__()\n")
+ file.write(" self.all_parameters = {}\n")
+ file.write(" self.all_constants = {}\n")
+ for attr in attributes:
+ if "all_constant" in attr:
+ key = attr.split(".")[-1]
+ file.write(
+ f' self.all_constants["{key}"] = torch.tensor({model.all_constants[key].tolist()}, requires_grad=False)\n'
+ )
+ elif "relation_forward" in attr:
+ key = attr.split(".")[2]
+ if "Fir" in key or "Linear" in key:
+ if "weights" in attr.split(".")[3]:
+ param = model_def["Relations"][key][2]
+ value = model.all_parameters[param]
+ file.write(
+ f' self.all_parameters["{param}"] = torch.nn.Parameter(torch.tensor({value.tolist()}), requires_grad=True)\n'
+ )
+ elif "bias" in attr.split(".")[3]:
+ param = model_def["Relations"][key][3]
+ file.write(
+ f' self.all_parameters["{param}"] = torch.nn.Parameter(torch.tensor({model.all_parameters[param].tolist()}), requires_grad=True)\n'
+ )
+ elif "dropout" in attr.split(".")[3]:
+ param = model_def["Relations"][key][4]
+ file.write(
+ f" self.{key} = torch.nn.Dropout(p={param})\n"
+ )
+ # param = model_def['Relations'][key][2] if 'weights' in attr.split('.')[3] else model_def['Relations'][key][3]
+ # value = model.all_parameters[param].data.squeeze(0) if 'Linear' in key else model.all_parameters[param].data
+ # file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.{value}, requires_grad=True)\n")
+ elif (
+ "Part" in key or "Select" in key
+ ): # any(element in key for element in ['Part', 'Select']):
+ value = model.relation_forward[key].W
+ temp_value = json.dumps(value.tolist())
+ file.write(
+ f' self.all_constants["{key}"] = torch.tensor({temp_value}, requires_grad=True)\n'
+ )
+ elif "all_parameters" in attr:
+ key = attr.split(".")[-1]
+ file.write(
+ f' self.all_parameters["{key}"] = torch.nn.Parameter(torch.tensor({model.all_parameters[key].tolist()}), requires_grad=True)\n'
+ )
+ elif "_tensor_constant" in attr:
+ key = attr.split(".")[-1]
+ file.write(
+ f" {attr} = torch.tensor({getattr(model, key).item()})\n"
+ )
+
+ file.write(
+ " self.all_parameters = torch.nn.ParameterDict(self.all_parameters)\n"
+ )
+ file.write(
+ " self.all_constants = torch.nn.ParameterDict(self.all_constants)\n\n"
+ )
+ file.write(
+ " def update(self, closed_loop={}, connect={}, disconnect=False):\n"
+ )
+ file.write(" pass\n")
+
+ for line in trace.code.split("\n")[len(saved_functions) + 2 :]:
+ if "self.relation_forward" in line:
+ if "Part" in line or "Select" in line:
+ attribute = [
+ x for x in line.split() if "self.relation_forward" in x
+ ][0].split(".")[2]
+ old_line = f"self.relation_forward.{attribute}.W"
+ new_line = f"self.all_constants.{attribute}"
+ file.write(f" {line.replace(old_line, new_line)}\n")
+ elif "dropout" in line:
+ attribute = line.split()[0]
+ layer = attribute.split("_")[2].capitalize()
+ old_line = f"self.relation_forward.{layer}.dropout"
+ new_line = f"self.{layer}"
+ file.write(f" {line.replace(old_line, new_line)}\n")
+ else:
+ attribute = line.split()[-1]
+ relation = attribute.split(".")[2]
+ relation_type = attribute.split(".")[3]
+ param = (
+ model_def["Relations"][relation][2]
+ if "weights" == relation_type
+ else model_def["Relations"][relation][3]
+ )
+ new_attribute = f"self.all_parameters.{param}"
+ file.write(f" {line.replace(attribute, new_attribute)}\n")
+ else:
+ file.write(f" {line}\n")
+
+ if len(recurrent_inputs) > 0:
+ file.write("class RecurrentModel(torch.nn.Module):\n")
+ file.write(" def __init__(self):\n")
+ file.write(" super().__init__()\n")
+ file.write(" self.Cell = TracerModel()\n")
+ list_inputs = " self.inputs = ["
+ for key in inputs.keys():
+ list_inputs += f"'{key}', "
+ list_inputs += "]\n"
+ file.write(list_inputs)
+ file.write(" self.states = dict()\n")
+ file.write("\n")
+ file.write(" def forward(self, kwargs, n_samples = None):\n")
+ file.write(
+ " n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs])\n"
+ )
+ for key in recurrent_inputs.keys():
+ file.write(f" self.states['{key}'] = kwargs['{key}']\n")
+ result_str = ""
+ for key, value in model_def["Outputs"].items():
+ result_str += f"'{key}':[], "
+ file.write(f" results = {{{result_str}}}\n")
+ file.write(" X = dict()\n")
+ file.write(" for idx in range(n_samples):\n")
+ file.write(" for key in self.inputs:\n")
+ file.write(" X[key] = kwargs[key][idx]\n")
+ file.write(" for key, value in self.states.items():\n")
+ file.write(" X[key] = value\n")
+ file.write(" out, _, closed_loop, connect = self.Cell(X)\n")
+ file.write(" for key, value in results.items():\n")
+ file.write(" results[key].append(out[key])\n")
+ file.write(" for key, val in closed_loop.items():\n")
+ file.write(
+ " self.states[key] = nnodely_basic_model_timeshift(self.states[key])\n"
+ )
+ file.write(
+ " self.states[key] = nnodely_basic_model_update_state(self.states[key], val)\n"
+ )
+ file.write(" for key, val in connect.items():\n")
+ file.write(
+ " self.states[key] = nnodely_basic_model_timeshift(val)\n"
+ )
+ file.write(" return results\n")
+
+
+def export_pythononnx_model(
+ model_def, model_path, model_onnx_path, input_order=None, outputs_order=None
+):
+ closed_loop_states, connect_states = [], []
+ for key, value in model_def["Inputs"].items():
+ if "closedLoop" in value.keys():
+ closed_loop_states.append(key)
+ if "connect" in value.keys():
+ connect_states.append(key)
+
+ model_inputs = input_order if input_order else list(model_def["Inputs"].keys())
+ model_outputs = (
+ outputs_order if outputs_order else list(model_def["Outputs"].keys())
+ )
+ model_losses = []
+ if model_def["Minimizers"]:
+ model_losses = [
+ loss_dict["A"]
+ for loss_dict in model_def["Minimizers"].values()
+ if "A" in loss_dict.keys()
+ ] + [
+ loss_dict["B"]
+ for loss_dict in model_def["Minimizers"].values()
+ if "A" in loss_dict.keys()
+ ]
+ recurrent_inputs = [
+ key
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" in value.keys() or "connect" in value.keys())
+ ]
+ inputs = [key for key in model_inputs if key not in recurrent_inputs]
+
+ # Define the mapping dictionary input
+ trace_mapping_input = {}
+ forward = "def forward(self,"
+ for i, key in enumerate(model_inputs):
+ value = f"kwargs['{key}']"
+ trace_mapping_input[value] = key
+ forward = forward + f" {key}" + ("," if i < len(model_inputs) - 1 else "")
+ forward = forward + "):"
+ # Define the mapping dictionary output
+ outputs = " return ("
+ for i, key in enumerate(model_outputs):
+ outputs += f"outputs[0]['{key}']" + (
+ "," if i < len(model_outputs) - 1 else ",)"
+ )
+ outputs += ", ("
+ for i, key in enumerate(model_losses):
+ outputs += (
+ f"outputs[1]['{key}'], " # + (',' if i < len(model_outputs) - 1 else ',)')
+ )
+ outputs += "), ("
+ for key in closed_loop_states:
+ outputs += f"outputs[2]['{key}'], "
+ outputs += "), ("
+ for key in connect_states:
+ outputs += f"outputs[3]['{key}'], "
+ outputs += ")\n"
+
+ # Open and read the file
+ file_content = []
+ with open(model_path, "r") as file:
+ for line in file:
+ if "return ({" in line:
+ file_content.append(line)
+ break
+ file_content.append(line)
+ file_content = "".join(file_content)
+
+ # Replace the forward header
+ file_content = file_content.replace("def forward(self, kwargs):", forward)
+ # Perform the substitution
+ for key, value in trace_mapping_input.items():
+ file_content = file_content.replace(key, value)
+ # Write the modified content back to a new file
+ # Replace the return statement
+ last_return_index = file_content.rfind("return")
+ if last_return_index != -1:
+ file_content = (
+ file_content[:last_return_index]
+ + "outputs ="
+ + file_content[last_return_index + len("return") :]
+ )
+ file_content += outputs
+ with open(model_onnx_path, "w") as file:
+ file.write(file_content)
+
+ if len(recurrent_inputs) > 0:
+ file.write("\n")
+ file.write("class RecurrentModel(torch.nn.Module):\n")
+ file.write(" def __init__(self):\n")
+ file.write(" super().__init__()\n")
+ file.write(" self.Cell = TracerModel()\n")
+
+ forward_str = " def forward(self, "
+ for key in model_inputs:
+ forward_str += f"{key}, "
+ forward_str += "):\n"
+ file.write(forward_str)
+
+ if model_inputs:
+ file.write(
+ " n_samples = min(["
+ + ", ".join([f"{key}.size(0)" for key in inputs])
+ + "])\n"
+ )
+ else:
+ file.write(" n_samples = 1\n")
+
+ for key in model_outputs:
+ file.write(f" results_{key} = []\n")
+ file.write(" for idx in range(n_samples):\n")
+ call_str = " out, losses, closed_loop, connect = self.Cell("
+ for key in model_inputs:
+ call_str += f"{key}[idx], " if key in inputs else f"{key}, "
+ call_str += ")\n"
+ file.write(call_str)
+ for idx, key in enumerate(model_outputs):
+ file.write(f" results_{key}.append(out[{idx}])\n")
+ for idx, key in enumerate(closed_loop_states):
+ file.write(
+ f" {key} = nnodely_basic_model_timeshift({key})\n"
+ )
+ file.write(
+ f" {key} = nnodely_basic_model_update_state({key}, closed_loop[{idx}])\n"
+ )
+ for idx, key in enumerate(connect_states):
+ file.write(
+ f" {key} = nnodely_basic_model_timeshift(connect[{idx}])\n"
+ )
+ # file.write(f" {key} = connect[{idx}]\n")
+ for idx, key in enumerate(model_outputs):
+ file.write(
+ f" results_{key} = torch.stack(results_{key}, dim=0)\n"
+ )
+ return_str = " return "
+ for key in model_outputs:
+ return_str += f"results_{key}, "
+ file.write(return_str)
+
+
+def import_python_model(name, model_folder):
+ sys.path.insert(0, model_folder)
+ module_name = os.path.basename(name)
+ if module_name in sys.modules:
+ # Reload the module if it is already loaded
+ module = importlib.reload(sys.modules[module_name])
+ else:
+ # Import the module if it is not loaded
+ module = importlib.import_module(module_name)
+ return module.TracerModel()
+
+
+def export_onnx_model(
+ model_def, model, model_path, input_order=None, output_order=None, name="net_onnx"
+):
+ sys.path.insert(0, model_path)
+ module_name = os.path.basename(name)
+
+ recurrent_inputs = {
+ key: value
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" in value.keys() or "connect" in value.keys())
+ }
+ inputs = {
+ key: value
+ for key, value in model_def["Inputs"].items()
+ if ("closedLoop" not in value.keys() and "connect" not in value.keys())
+ }
+
+ if module_name in sys.modules:
+ # Reload the module if it is already loaded
+ module = importlib.reload(sys.modules[module_name])
+ else:
+ # Import the module if it is not loaded
+ module = importlib.import_module(module_name)
+ model = (
+ torch.jit.script(module.RecurrentModel())
+ if len(recurrent_inputs) > 0
+ else module.TracerModel()
+ )
+ model.eval()
+ dummy_inputs = []
+ input_names = []
+ dynamic_axes = {}
+ onnx_inputs = input_order if input_order else model_def["Inputs"].keys()
+ for key in onnx_inputs:
+ input_names.append(key)
+ window_size = model_def["Inputs"][key]["ntot"]
+ dim = model_def["Inputs"][key]["dim"]
+ if len(recurrent_inputs) > 0 != {}:
+ if key in inputs.keys():
+ dummy_inputs.append(torch.randn(size=(1, 1, window_size, dim)))
+ dynamic_axes[key] = {0: "horizon", 1: "batch_size"}
+ elif key in recurrent_inputs.keys():
+ dummy_inputs.append(torch.randn(size=(1, window_size, dim)))
+ dynamic_axes[key] = {0: "batch_size"}
+ else:
+ dummy_inputs.append(torch.randn(size=(1, window_size, dim)))
+ dynamic_axes[key] = {0: "batch_size"}
+ output_names = output_order if output_order else list(model_def["Outputs"].keys())
+ dummy_inputs = tuple(dummy_inputs)
+
+ torch.onnx.export(
+ model, # The model to be exported
+ dummy_inputs, # Tuple of inputs to match the forward signature
+ model_path, # File path to save the ONNX model
+ export_params=True, # Store the trained parameters in the model file
+ opset_version=17, # ONNX version to export to (you can use 11 or higher)
+ do_constant_folding=False, # Optimize constant folding for inference
+ input_names=input_names, # Name each input as they will appear in ONNX
+ output_names=output_names, # Name the output
+ dynamic_axes=dynamic_axes,
+ )
+
+
+def onnx_inference(inputs, path, optimize_graph=False):
+ import onnxruntime as ort
+
+ # Create an ONNX Runtime session
+ # Define session options
+ ## TODO: Warning when using constant folding in inference CanUpdateImplicitInputNameInSubgraphs]
+ ## TODO: Implicit input name Cell.all_constants.Constant75 cannot be safely updated to Cell.all_constants.Constant76 in one of the subgraphs.
+ if optimize_graph == False:
+ session_options = ort.SessionOptions()
+ # Set graph optimization level to disable all optimizations
+ session_options.graph_optimization_level = (
+ ort.GraphOptimizationLevel.ORT_DISABLE_ALL
+ )
+ session = ort.InferenceSession(path, sess_options=session_options)
+ else:
+ session = ort.InferenceSession(path)
+ output_data = []
+ for item in session.get_outputs():
+ output_data.append(item.name)
+ input_data = {}
+ for item in session.get_inputs():
+ input_data[item.name] = inputs[item.name]
+ # Run inference
+ return session.run(output_data, input_data)
diff --git a/src/nnodely/exporter/reporter.py b/src/nnodely/exporter/reporter.py
new file mode 100644
index 00000000..8259fa12
--- /dev/null
+++ b/src/nnodely/exporter/reporter.py
@@ -0,0 +1,81 @@
+import io
+
+import matplotlib.pyplot as plt
+from reportlab.lib.pagesizes import letter
+from reportlab.pdfgen import canvas
+from reportlab.lib.utils import ImageReader
+
+from mplplots import plots
+
+
+class Reporter:
+ def __init__(self, modely):
+ self.modely = modely
+
+ def exportReport(self, report_path):
+ c = canvas.Canvas(report_path, pagesize=letter)
+ width, height = letter
+
+ if "Minimizers" in self.modely._model_def:
+ for key, value in self.modely._model_def["Minimizers"].items():
+ fig = plt.figure(figsize=(10, 5))
+ ax = fig.add_subplot(111)
+ if "val" in self.modely._training[key]:
+ plots.plot_training(
+ ax,
+ f"Training Loss of {key}",
+ key,
+ self.modely._training[key]["train"],
+ self.modely._training[key]["val"],
+ )
+ else:
+ plots.plot_training(
+ ax,
+ f"Training Loss of {key}",
+ key,
+ self.modely._training[key]["train"],
+ )
+ training = io.BytesIO()
+ plt.savefig(training, format="png")
+ training.seek(0)
+ plt.close()
+ c.drawString(100, height - 30, f"Training Loss of {key}")
+ c.drawImage(
+ ImageReader(training), 50, height - 290, width=500, height=250
+ )
+ c.showPage()
+
+ if len(self.modely.prediction) > 0:
+ for key in self.modely._model_def["Minimizers"].keys():
+ c.drawString(100, height - 30, f"Prediction of {key}")
+ for ind, name_data in enumerate(self.modely.prediction.keys()):
+ fig = plt.figure(figsize=(10, 5))
+ ax = fig.add_subplot(111)
+ idxs = None
+ if "idxs" in self.modely.prediction[name_data]:
+ idxs = self.modely.prediction[name_data]["idxs"]
+ plots.plot_results(
+ ax,
+ name_data,
+ key,
+ self.modely.prediction[name_data][key]["A"],
+ self.modely.prediction[name_data][key]["B"],
+ idxs,
+ self.modely._model_def["Info"]["SampleTime"],
+ )
+ # Add a text box with correlation coefficient
+ results = io.BytesIO()
+ plt.savefig(results, format="png")
+ results.seek(0)
+ plt.close()
+ c.drawImage(
+ ImageReader(results),
+ 50,
+ height - 290 - 245 * ind,
+ width=500,
+ height=250,
+ )
+ c.showPage()
+ else:
+ c.drawString(100, height - 30, "No Minimize")
+ c.save()
diff --git a/src/nnodely/exporter/standardexporter.py b/src/nnodely/exporter/standardexporter.py
new file mode 100644
index 00000000..6e17073d
--- /dev/null
+++ b/src/nnodely/exporter/standardexporter.py
@@ -0,0 +1,196 @@
+import os
+import torch
+
+from nnodely.visualizer import EmptyVisualizer
+from nnodely.exporter.emptyexporter import EmptyExporter
+from nnodely.exporter.reporter import Reporter
+from nnodely.exporter.export import (
+ save_model,
+ load_model,
+ export_python_model,
+ export_pythononnx_model,
+ export_onnx_model,
+ import_python_model,
+ onnx_inference,
+)
+from nnodely.support.utils import check, enforce_types
+
+from nnodely.support.logger import logging, nnLogger
+
+log = nnLogger(__name__, logging.INFO)
+
+
+class StandardExporter(EmptyExporter):
+ @enforce_types
+ def __init__(
+ self,
+ workspace: str | None = None,
+ visualizer: EmptyVisualizer | None = None,
+ *,
+ save_history: bool = False,
+ ):
+ super().__init__(workspace, visualizer, save_history)
+
+ def getWorkspace(self):
+ return self.workspace_folder if hasattr(self, "workspace_folder") else "."
+
+ def saveTorchModel(self, model, name="net", model_folder=None):
+ file_name = name + ".pt"
+ model_path = (
+ os.path.join(self.getWorkspace(), file_name)
+ if model_folder is None
+ else os.path.join(model_folder, file_name)
+ )
+ # TODO check if the folder exist
+ torch.save(model.state_dict(), model_path)
+ self.visualizer.saveModel("Torch Model", model_path)
+
+ def loadTorchModel(self, model, name="net", model_folder=None):
+ file_name = name + ".pt"
+ model_path = (
+ os.path.join(self.getWorkspace(), file_name)
+ if model_folder is None
+ else os.path.join(model_folder, file_name)
+ )
+ check(
+ os.path.exists(model_path),
+ FileNotFoundError,
+ f"The model {name} it is not found in the folder {model_folder}",
+ )
+ model.load_state_dict(torch.load(model_path, weights_only=True))
+ self.visualizer.loadModel("Torch Model", model_path)
+ # TODO Update the model parameters....
+
+ def saveModel(self, model_def, name="net", model_folder=None):
+ # Combine the folder path and file name to form the complete file path
+ model_folder = self.getWorkspace() if model_folder is None else model_folder
+ # Specify the JSON file name
+ file_name = name + ".json"
+ # Combine the folder path and file name to form the complete file path
+ model_path = os.path.join(model_folder, file_name)
+ save_model(model_def, model_path)
+ self.visualizer.saveModel("JSON Model", model_path)
+
+ def loadModel(self, name="net", model_folder=None):
+ # Combine the folder path and file name to form the complete file path
+ model_folder = self.getWorkspace() if model_folder is None else model_folder
+ model_def = None
+ try:
+ file_name = name + ".json"
+ model_path = os.path.join(model_folder, file_name)
+ model_def = load_model(model_path)
+ self.visualizer.loadModel("JSON Model", model_path)
+ except Exception as e:
+ check(
+ False,
+ FileNotFoundError,
+ f"The file {model_path} it is not found or not conformed.\n Error: {e}",
+ )
+ return model_def
+
+ def exportPythonModel(self, model_def, model, name="net", model_folder=None):
+ file_name = name + ".py"
+ model_path = (
+ os.path.join(self.getWorkspace(), file_name)
+ if model_folder is None
+ else os.path.join(model_folder, file_name)
+ )
+ ## Export to python file
+ export_python_model(model_def.getJson(), model, model_path)
+ self.visualizer.exportModel("Python Torch Model", model_path)
+
+ def importPythonModel(self, name="net", model_folder=None):
+ try:
+ model_folder = self.getWorkspace() if model_folder is None else model_folder
+ model = import_python_model(name, model_folder)
+ self.visualizer.importModel(
+ "Python Torch Model", os.path.join(model_folder, name + ".py")
+ )
+ except Exception as e:
+ model = None
+ check(
+ False,
+ FileNotFoundError,
+ f"The model {name} it is not found in the folder {model_folder}.\nError: {e}",
+ )
+ return model
+
+ def exportONNX(
+ self,
+ model_def,
+ model,
+ inputs_order=None,
+ outputs_order=None,
+ name="net",
+ model_folder=None,
+ ):
+ if inputs_order is None:
+ log.info(
+ f"The inputs order for the export is not specified, the order will set equal to {set(model_def['Inputs'].keys())}."
+ )
+ elif set(inputs_order) != set(model_def["Inputs"].keys()):
+ raise ValueError(
+ f"The inputs are not the same as the model inputs {set(model_def['Inputs'].keys())}."
+ )
+ if outputs_order is None:
+ log.info(
+ f"The outputs order for the export is not specified, the order will set equal to {set(model_def['Outputs'].keys())}"
+ )
+ elif set(outputs_order) != set(model_def["Outputs"].keys()):
+ log.info(
+ f"The outputs are not the same as the model outputs {set(model_def['Outputs'].keys())}."
+ )
+ file_name = name + ".py"
+ model_folder = (
+ os.path.join(self.getWorkspace(), "onnx")
+ if model_folder is None
+ else model_folder
+ )
+ os.makedirs(model_folder, exist_ok=True)
+ model_path = os.path.join(model_folder, file_name)
+ onnx_python_model_path = model_path.replace(".py", "_onnx.py")
+ onnx_model_path = model_path.replace(".py", ".onnx")
+ ## Export to python file (onnx compatible)
+ export_python_model(model_def, model, model_path)
+ self.visualizer.exportModel("Python Torch Model", model_path)
+ export_pythononnx_model(
+ model_def, model_path, onnx_python_model_path, inputs_order, outputs_order
+ )
+ self.visualizer.exportModel("Python Onnx Torch Model", onnx_python_model_path)
+ ## Export to onnx file (onnx compatible)
+ model = import_python_model(file_name.replace(".py", "_onnx"), model_folder)
+ export_onnx_model(
+ model_def,
+ model,
+ onnx_model_path,
+ inputs_order,
+ outputs_order,
+ name=name + "_onnx",
+ )
+ self.visualizer.exportModel("Onnx Model", onnx_model_path)
+
+ def onnxInference(self, inputs, name: str = "net", model_folder: str | None = None):
+ model_folder = (
+ os.path.join(self.getWorkspace(), "onnx")
+ if model_folder is None
+ else model_folder
+ )
+ file_name = name + ".onnx"
+ onnx_model_path = os.path.join(model_folder, file_name)
+ check(
+ os.path.exists(onnx_model_path),
+ FileNotFoundError,
+ f"The model {file_name} it is not found in the folder {model_folder}",
+ )
+ return onnx_inference(inputs, onnx_model_path)
+
+ def exportReport(self, n4m, name="net", model_folder=None):
+ # Combine the folder path and file name to form the complete file path
+ model_folder = self.getWorkspace() if model_folder is None else model_folder
+ # Specify the JSON file name
+ file_name = name + ".pdf"
+ # Combine the folder path and file name to form the complete file path
+ report_path = os.path.join(model_folder, file_name)
+ reporter = Reporter(n4m)
+ reporter.exportReport(report_path)
+ self.visualizer.exportReport("Training Results", report_path)
diff --git a/nnodely/layers/__init__.py b/src/nnodely/layers/__init__.py
similarity index 100%
rename from nnodely/layers/__init__.py
rename to src/nnodely/layers/__init__.py
diff --git a/nnodely/layers/activation.py b/src/nnodely/layers/activation.py
similarity index 53%
rename from nnodely/layers/activation.py
rename to src/nnodely/layers/activation.py
index a6c38482..4c432fa0 100644
--- a/nnodely/layers/activation.py
+++ b/src/nnodely/layers/activation.py
@@ -7,80 +7,95 @@
from nnodely.support.utils import check, enforce_types
-relu_relation_name = 'Relu'
-elu_relation_name = 'ELU'
-sigmoid_relation_name = 'Sigmoid'
-identity_relation_name = 'Identity'
-softmax_relation_name = 'Softmax'
+relu_relation_name = "Relu"
+elu_relation_name = "ELU"
+sigmoid_relation_name = "Sigmoid"
+identity_relation_name = "Identity"
+softmax_relation_name = "Softmax"
+
class Relu(Stream, ToStream):
"""
- Implement the Rectified-Linear Unit (ReLU) relation function.
+ Implement the Rectified-Linear Unit (ReLU) relation function.
- See also:
- Official PyTorch ReLU documentation:
- `torch.nn.ReLU `_
+ See also:
+ Official PyTorch ReLU documentation:
+ `torch.nn.ReLU `_
- :param obj: The relation stream.
- :type obj: Stream
+ :param obj: The relation stream.
+ :type obj: Stream
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/activation_module_ex/relu.rst
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/activation_module_ex/relu.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Relu operation.")
- super().__init__(relu_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [relu_relation_name,[obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Relu operation.",
+ )
+ super().__init__(relu_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [relu_relation_name, [obj.name]]
+
class ELU(Stream, ToStream):
"""
- Implement the Exponential-Linear Unit (ELU) relation function.
+ Implement the Exponential-Linear Unit (ELU) relation function.
- See also:
- Official PyTorch ReLU documentation:
- `torch.nn.ELU `_
+ See also:
+ Official PyTorch ReLU documentation:
+ `torch.nn.ELU `_
- :param obj: The relation stream.
- :type obj: Stream
+ :param obj: The relation stream.
+ :type obj: Stream
- Example:
- ---------
- .. include:: /examples_basics/layer_module_ex/activation_module_ex/elu.rst
+ Example:
+ ---------
+ .. include:: /examples_basics/layer_module_ex/activation_module_ex/elu.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream,TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.")
- super().__init__(elu_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [elu_relation_name,[obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.",
+ )
+ super().__init__(elu_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [elu_relation_name, [obj.name]]
+
class Identity(Stream, ToStream):
"""
Implement the Identity relation function that simply returns the input vector x.
See also:
- Official PyTorch Identity documentation:
+ Official PyTorch Identity documentation:
`torch.nn.Identity `_
:param obj: The relation stream.
- :type obj: Stream
+ :type obj: Stream
Example:
---------
.. include:: /examples_basics/layer_module_ex/activation_module_ex/identity.rst
"""
+
@enforce_types
- def __init__(self, obj: Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Identity operation.")
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Identity operation.",
+ )
super().__init__(identity_relation_name + str(Stream.count), obj.json, obj.dim)
- self.json['Relations'][self.name] = [identity_relation_name, [obj.name]]
+ self.json["Relations"][self.name] = [identity_relation_name, [obj.name]]
class Softmax(Stream, ToStream):
@@ -98,13 +113,18 @@ class Softmax(Stream, ToStream):
---------
.. include:: /examples_basics/layer_module_ex/activation_module_ex/softmax.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Softmax operation.")
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Softmax operation.",
+ )
super().__init__(softmax_relation_name + str(Stream.count), obj.json, obj.dim)
- self.json['Relations'][self.name] = [softmax_relation_name, [obj.name]]
+ self.json["Relations"][self.name] = [softmax_relation_name, [obj.name]]
+
class Sigmoid(Stream, ToStream):
r"""
@@ -125,74 +145,94 @@ class Sigmoid(Stream, ToStream):
---------
.. include:: /examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for {sigmoid_relation_name} operation.")
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for {sigmoid_relation_name} operation.",
+ )
super().__init__(sigmoid_relation_name + str(Stream.count), obj.json, obj.dim)
- self.json['Relations'][self.name] = [sigmoid_relation_name, [obj.name]]
+ self.json["Relations"][self.name] = [sigmoid_relation_name, [obj.name]]
+
class Relu_Layer(nn.Module):
"""
- :noindex:
+ :noindex:
"""
- def __init__(self,):
+
+ def __init__(
+ self,
+ ):
super(Relu_Layer, self).__init__()
+
def forward(self, x):
return torch.relu(x)
-
+
+
def createRelu(self, *input):
"""
- :noindex:
+ :noindex:
"""
return Relu_Layer()
-
+
def createELU(self, *input):
"""
- :noindex:
+ :noindex:
"""
return nn.ELU()
+
class Identity_Layer(nn.Module):
"""
- :noindex:
+ :noindex:
"""
+
def __init__(self, *args):
super(Identity_Layer, self).__init__()
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x
-
+
+
def createIdentity(self, *input):
"""
- :noindex:
+ :noindex:
"""
return Identity_Layer()
class Sigmoid_Layer(nn.Module):
"""
- :noindex:
+ :noindex:
"""
- def __init__(self,):
+
+ def __init__(
+ self,
+ ):
super(Sigmoid_Layer, self).__init__()
+
def forward(self, x):
- return 1/(1+torch.exp(-x))
-
+ return 1 / (1 + torch.exp(-x))
+
+
def createSigmoid(self, *input):
"""
- :noindex:
+ :noindex:
"""
return Sigmoid_Layer()
+
def createSoftmax(self, *input):
"""
- :noindex:
+ :noindex:
"""
return nn.Softmax(dim=-1)
+
setattr(Model, relu_relation_name, createRelu)
setattr(Model, elu_relation_name, createELU)
setattr(Model, sigmoid_relation_name, createSigmoid)
diff --git a/src/nnodely/layers/arithmetic.py b/src/nnodely/layers/arithmetic.py
new file mode 100644
index 00000000..240116e2
--- /dev/null
+++ b/src/nnodely/layers/arithmetic.py
@@ -0,0 +1,407 @@
+import torch.nn as nn
+import torch
+
+from nnodely.basic.relation import ToStream, Stream, toStream
+from nnodely.basic.model import Model
+from nnodely.support.utils import check, enforce_types
+from nnodely.layers.parameter import Parameter, Constant
+from nnodely.support.jsonutils import merge, binary_cheks
+
+
+# Binary operators
+add_relation_name = "Add"
+sub_relation_name = "Sub"
+mul_relation_name = "Mul"
+div_relation_name = "Div"
+pow_relation_name = "Pow"
+
+# Unary operators
+neg_relation_name = "Neg"
+sign_relation_name = "Sign"
+
+# Merge operator
+sum_relation_name = "Sum"
+
+
+class Add(Stream, ToStream):
+ """
+ Implement the addition function between two tensors.
+ (it is also possible to use the classical math operator '+')
+
+ See also:
+ Official PyTorch Add documentation:
+ `torch.add `_
+
+ :param input1: the first element of the addition
+ :type obj: Tensor
+ :param input2: the second element of the addition
+ :type obj: Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/add.rst
+ """
+
+ @enforce_types
+ def __init__(
+ self,
+ obj1: Stream | Parameter | Constant | int | float,
+ obj2: Stream | Parameter | Constant | int | float,
+ ) -> Stream:
+ obj1, obj2, dim = binary_cheks(self, obj1, obj2, "addition operators (+)")
+ super().__init__(
+ add_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim
+ )
+ self.json["Relations"][self.name] = [add_relation_name, [obj1.name, obj2.name]]
+
+
+## TODO: check the scalar dimension, helpful for the offset
+class Sub(Stream, ToStream):
+ """
+ Implement the subtraction function between two tensors.
+ (it is also possible to use the classical math operator '-')
+
+ :param input1: the first element of the subtraction
+ :type obj: Tensor
+ :param input2: the second element of the subtraction
+ :type obj: Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst
+ """
+
+ @enforce_types
+ def __init__(
+ self,
+ obj1: Stream | Parameter | Constant | int | float,
+ obj2: Stream | Parameter | Constant | int | float,
+ ) -> Stream:
+ obj1, obj2, dim = binary_cheks(self, obj1, obj2, "subtraction operators (-)")
+ super().__init__(
+ sub_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim
+ )
+ self.json["Relations"][self.name] = [sub_relation_name, [obj1.name, obj2.name]]
+
+
+class Mul(Stream, ToStream):
+ """
+ Implement the multiplication function between two tensors.
+ (it is also possible to use the classical math operator '*')
+
+ :param input1: the first element of the multiplication
+ :type obj: Tensor
+ :param input2: the second element of the multiplication
+ :type obj: Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst
+ """
+
+ @enforce_types
+ def __init__(
+ self,
+ obj1: Stream | Parameter | Constant | int | float,
+ obj2: Stream | Parameter | Constant | int | float,
+ ) -> Stream:
+ obj1, obj2, dim = binary_cheks(self, obj1, obj2, "multiplication operators (*)")
+ super().__init__(
+ mul_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim
+ )
+ self.json["Relations"][self.name] = [mul_relation_name, [obj1.name, obj2.name]]
+
+
+class Div(Stream, ToStream):
+ """
+ Implement the division function between two tensors.
+ (it is also possible to use the classical math operator '/')
+
+ :param input1: the numerator of the division
+ :type obj: Tensor
+ :param input2: the denominator of the division
+ :type obj: Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/div.rst
+ """
+
+ @enforce_types
+ def __init__(
+ self,
+ obj1: Stream | Parameter | Constant | int | float,
+ obj2: Stream | Parameter | Constant | int | float,
+ ) -> Stream:
+ obj1, obj2, dim = binary_cheks(self, obj1, obj2, "division operators (/) ")
+ super().__init__(
+ div_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim
+ )
+ self.json["Relations"][self.name] = [div_relation_name, [obj1.name, obj2.name]]
+
+
+class Pow(Stream, ToStream):
+ """
+ Implement the power function given an input and an exponent.
+ (it is also possible to use the classical math operator '**')
+
+ See also:
+ Official PyTorch pow documentation:
+ `torch.pow `_
+
+ :param input: the base of the power function
+ :type obj: Tensor
+ :param exp: the exponent of the power function
+ :type obj: float or Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst
+ """
+
+ @enforce_types
+ def __init__(
+ self,
+ obj1: Stream | Parameter | Constant | int | float,
+ obj2: Stream | Parameter | Constant | int | float,
+ ) -> Stream:
+ obj1, obj2, dim = binary_cheks(self, obj1, obj2, "pow operators (**)")
+ super().__init__(
+ pow_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim
+ )
+ self.json["Relations"][self.name] = [pow_relation_name, [obj1.name, obj2.name]]
+
+
+class Neg(Stream, ToStream):
+ """
+ Implement the negate function given an input.
+
+ :param input: the input to negate
+ :type obj: Tensor
+
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst
+ """
+
+ @enforce_types
+ def __init__(self, obj: Stream | Parameter | Constant) -> Stream:
+ obj = toStream(obj)
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for neg operation.",
+ )
+ super().__init__(neg_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [neg_relation_name, [obj.name]]
+
+
+class Sign(Stream, ToStream):
+ """
+ Implement the sign function given an input.
+
+ :param input: the input for the sign function
+ :type obj: Tensor
+
+ Example:
+ >>> x = Sign(x)
+ """
+
+ @enforce_types
+ def __init__(self, obj: Stream | Parameter | Constant) -> Stream:
+ obj = toStream(obj)
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for sign operation.",
+ )
+ super().__init__(sign_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [sign_relation_name, [obj.name]]
+
+
+class Sum(Stream, ToStream):
+ @enforce_types
+ def __init__(self, obj: Stream | Parameter | Constant) -> Stream:
+ obj = toStream(obj)
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for sum operation.",
+ )
+ obj.dim["dim"] = 1
+ super().__init__(sum_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [sum_relation_name, [obj.name]]
+
+
+class Add_Layer(nn.Module):
+ #: :noindex:
+ def __init__(self):
+ super(Add_Layer, self).__init__()
+
+ def forward(self, *inputs):
+ results = inputs[0]
+ for input in inputs[1:]:
+ results = results + input
+ return results
+
+
+def createAdd(name, *inputs):
+ """
+ :noindex:
+ """
+ return Add_Layer()
+
+
+class Sub_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Sub_Layer, self).__init__()
+
+ def forward(self, *inputs):
+ # Perform element-wise subtraction
+ results = inputs[0]
+ for input in inputs[1:]:
+ results = results - input
+ return results
+
+
+def createSub(self, *inputs):
+ """
+ :noindex:
+ """
+ return Sub_Layer()
+
+
+class Mul_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Mul_Layer, self).__init__()
+
+ def forward(self, *inputs):
+ results = inputs[0]
+ for input in inputs[1:]:
+ results = results * input
+ return results
+
+
+def createMul(name, *inputs):
+ """
+ :noindex:
+ """
+ return Mul_Layer()
+
+
+class Div_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Div_Layer, self).__init__()
+
+ def forward(self, *inputs):
+ results = inputs[0]
+ for input in inputs[1:]:
+ results = results / input
+ return results
+
+
+def createDiv(name, *inputs):
+ """
+ :noindex:
+ """
+ return Div_Layer()
+
+
+class Pow_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Pow_Layer, self).__init__()
+
+ def forward(self, *inputs):
+ return torch.pow(inputs[0], inputs[1])
+
+
+def createPow(name, *inputs):
+ """
+ :noindex:
+ """
+ return Pow_Layer()
+
+
+class Neg_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Neg_Layer, self).__init__()
+
+ def forward(self, x):
+ return -x
+
+
+def createNeg(self, *inputs):
+ """
+ :noindex:
+ """
+ return Neg_Layer()
+
+
+class Sign_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Sign_Layer, self).__init__()
+
+ def forward(self, x):
+ return torch.sign(x)
+
+
+def createSign(self, *inputs):
+ """
+ :noindex:
+ """
+ return Sign_Layer()
+
+
+class Sum_Layer(nn.Module):
+ """
+ :noindex:
+ """
+
+ def __init__(self):
+ super(Sum_Layer, self).__init__()
+
+ def forward(self, inputs):
+ return torch.sum(inputs, dim=2, keepdim=True)
+
+
+def createSum(name, *inputs):
+ """
+ :noindex:
+ """
+ return Sum_Layer()
+
+
+setattr(Model, add_relation_name, createAdd)
+setattr(Model, sub_relation_name, createSub)
+setattr(Model, mul_relation_name, createMul)
+setattr(Model, div_relation_name, createDiv)
+setattr(Model, pow_relation_name, createPow)
+
+setattr(Model, neg_relation_name, createNeg)
+setattr(Model, sign_relation_name, createSign)
+
+setattr(Model, sum_relation_name, createSum)
diff --git a/nnodely/layers/equationlearner.py b/src/nnodely/layers/equationlearner.py
similarity index 60%
rename from nnodely/layers/equationlearner.py
rename to src/nnodely/layers/equationlearner.py
index fa6d1dbf..b3db8ed8 100644
--- a/nnodely/layers/equationlearner.py
+++ b/src/nnodely/layers/equationlearner.py
@@ -11,16 +11,35 @@
from nnodely.layers.trigonometric import Sin, Cos, Tan, Tanh, Cosh, Sech
from nnodely.layers.arithmetic import Add, Mul, Sub, Neg, Pow, Sum
-equationlearner_relation_name = 'EquationLearner'
-Available_functions = [Sin, Cos, Tan, Cosh, Tanh, Sech, Add, Mul, Sub, Neg, Pow, Sum, Concatenate, Relu, ELU, Identity, Sigmoid]
+equationlearner_relation_name = "EquationLearner"
+Available_functions = [
+ Sin,
+ Cos,
+ Tan,
+ Cosh,
+ Tanh,
+ Sech,
+ Add,
+ Mul,
+ Sub,
+ Neg,
+ Pow,
+ Sum,
+ Concatenate,
+ Relu,
+ ELU,
+ Identity,
+ Sigmoid,
+]
Initialized_functions = [ParamFun, Fuzzify]
+
class EquationLearner(NeuObj):
"""
Represents a nnodely implementation of the Task-Parametrized Equation Learner block.
See also:
- Task-Parametrized Equation Learner official paper:
+ Task-Parametrized Equation Learner official paper:
`Equation Learner `_
Parameters
@@ -52,8 +71,15 @@ class EquationLearner(NeuObj):
.. include:: /examples_basics/layer_module_ex/eql.rst
"""
+
@enforce_types
- def __init__(self, functions:list, *, linear_in:Linear|None = None, linear_out:Linear|None = None) -> Stream:
+ def __init__(
+ self,
+ functions: list,
+ *,
+ linear_in: Linear | None = None,
+ linear_out: Linear | None = None,
+ ) -> Stream:
self.relation_name = equationlearner_relation_name
self.linear_in = linear_in
self.linear_out = linear_out
@@ -64,38 +90,77 @@ def __init__(self, functions:list, *, linear_in:Linear|None = None, linear_out:L
self.func_parameters = {}
for func_idx, func in enumerate(self.functions):
- check(callable(func), TypeError, 'The activation functions must be callable')
+ check(
+ callable(func), TypeError, "The activation functions must be callable"
+ )
if type(func) in Initialized_functions:
if type(func) == ParamFun:
funinfo = inspect.getfullargspec(func.param_fun)
- num_args = len(funinfo.args) - len(func.parameters_and_constants) if func.parameters_and_constants else len(funinfo.args)
+ num_args = (
+ len(funinfo.args) - len(func.parameters_and_constants)
+ if func.parameters_and_constants
+ else len(funinfo.args)
+ )
elif type(func) == Fuzzify:
- init_signature = inspect.signature(func.__call__)
+ init_signature = inspect.signature(func.__call__)
parameters = list(init_signature.parameters.values())
- num_args = len([param for param in parameters if param.name != "self"])
+ num_args = len(
+ [param for param in parameters if param.name != "self"]
+ )
else:
- check(func in Available_functions, ValueError, f'The function {func} is not available for the EquationLearner operation')
- init_signature = inspect.signature(func.__init__)
+ check(
+ func in Available_functions,
+ ValueError,
+ f"The function {func} is not available for the EquationLearner operation",
+ )
+ init_signature = inspect.signature(func.__init__)
parameters = list(init_signature.parameters.values())
num_args = len([param for param in parameters if param.name != "self"])
self.func_parameters[func_idx] = num_args
self.n_activations = sum(self.func_parameters.values())
- check(self.n_activations > 0, ValueError, 'At least one activation function must be provided')
+ check(
+ self.n_activations > 0,
+ ValueError,
+ "At least one activation function must be provided",
+ )
def __call__(self, inputs):
if type(inputs) is not tuple:
inputs = (inputs,)
- check(len(set([x.dim['sw'] if 'sw' in x.dim.keys() else x.dim['tw'] for x in inputs])) == 1, ValueError, 'All inputs must have the same time dimension')
+ check(
+ len(
+ set(
+ [
+ x.dim["sw"] if "sw" in x.dim.keys() else x.dim["tw"]
+ for x in inputs
+ ]
+ )
+ )
+ == 1,
+ ValueError,
+ "All inputs must have the same time dimension",
+ )
concatenated_input = inputs[0]
for inp in inputs[1:]:
concatenated_input = Concatenate(concatenated_input, inp)
- linear_layer = self.linear_in(concatenated_input) if self.linear_in else Linear(output_dimension=self.n_activations, b=True)(concatenated_input)
+ linear_layer = (
+ self.linear_in(concatenated_input)
+ if self.linear_in
+ else Linear(output_dimension=self.n_activations, b=True)(concatenated_input)
+ )
idx, out = 0, None
for func_idx, func in enumerate(self.functions):
- arguments = [Select(linear_layer,idx+arg_idx) for arg_idx in range(self.func_parameters[func_idx])]
+ arguments = [
+ Select(linear_layer, idx + arg_idx)
+ for arg_idx in range(self.func_parameters[func_idx])
+ ]
idx += self.func_parameters[func_idx]
- out = func(*arguments) if func_idx == 0 else Concatenate(out, func(*arguments))
+ out = (
+ func(*arguments)
+ if func_idx == 0
+ else Concatenate(out, func(*arguments))
+ )
if self.linear_out:
out = self.linear_out(out)
return out
diff --git a/nnodely/layers/fir.py b/src/nnodely/layers/fir.py
similarity index 52%
rename from nnodely/layers/fir.py
rename to src/nnodely/layers/fir.py
index b340dbe0..38066de9 100644
--- a/nnodely/layers/fir.py
+++ b/src/nnodely/layers/fir.py
@@ -1,4 +1,5 @@
-import copy, torch
+import copy
+import torch
import torch.nn as nn
@@ -11,9 +12,11 @@
from nnodely.support.jsonutils import merge
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
-fir_relation_name = 'Fir'
+fir_relation_name = "Fir"
+
class Fir(NeuObj, AutoToStream):
"""
@@ -71,21 +74,26 @@ class Fir(NeuObj, AutoToStream):
output_dimension : int
The output dimension of the FIR relation.
- Examples
+ Examples
--------
.. include:: /examples_basics/layer_module_ex/fir.rst
"""
+
@enforce_types
- def __init__(self, output_dimension:int|None = None, *,
- W_init:Callable|str|None = None,
- W_init_params:dict|None = None,
- b_init:Callable|str|None = None,
- b_init_params:dict|None = None,
- W:Parameter|str|None = None,
- b:bool|str|Parameter|None = None,
- dropout:int|float = 0):
+ def __init__(
+ self,
+ output_dimension: int | None = None,
+ *,
+ W_init: Callable | str | None = None,
+ W_init_params: dict | None = None,
+ b_init: Callable | str | None = None,
+ b_init_params: dict | None = None,
+ W: Parameter | str | None = None,
+ b: bool | str | Parameter | None = None,
+ dropout: int | float = 0,
+ ):
self.W = W
self.b = b
@@ -93,72 +101,124 @@ def __init__(self, output_dimension:int|None = None, *,
self.bname = None
self.dropout = dropout
- super().__init__('P'+fir_relation_name + str(NeuObj.count))
+ super().__init__("P" + fir_relation_name + str(NeuObj.count))
if type(self.W) is Parameter:
- check('tw' in self.W.dim or 'sw' in self.W.dim, TypeError, f'The "W" Parameter must have a time dimension or a sample dimension but got {self.W.dim}.')
- #check(len(self.W.dim) == 2,ValueError,f"The values of the parameters must have two dimensions [tw/sample_rate,output_dimension] or [sw,output_dimension].")
+ check(
+ "tw" in self.W.dim or "sw" in self.W.dim,
+ TypeError,
+ f'The "W" Parameter must have a time dimension or a sample dimension but got {self.W.dim}.',
+ )
+ # check(len(self.W.dim) == 2,ValueError,f"The values of the parameters must have two dimensions [tw/sample_rate,output_dimension] or [sw,output_dimension].")
if output_dimension is None:
- check(type(self.W.dim['dim']) is int, TypeError, 'Dimension of the parameter must be an integer for the Fir')
- self.output_dimension = self.W.dim['dim']
+ check(
+ type(self.W.dim["dim"]) is int,
+ TypeError,
+ "Dimension of the parameter must be an integer for the Fir",
+ )
+ self.output_dimension = self.W.dim["dim"]
else:
self.output_dimension = output_dimension
- check(self.W.dim['dim'] == self.output_dimension,
- ValueError,
- 'output_dimension must be equal to dim of the Parameter')
+ check(
+ self.W.dim["dim"] == self.output_dimension,
+ ValueError,
+ "output_dimension must be equal to dim of the Parameter",
+ )
self.Wname = self.W.name
W_json = self.W.json
else: ## Create a new default parameter
self.output_dimension = 1 if output_dimension is None else output_dimension
- self.Wname = W if type(W) is str else self.name + 'W'
- W_json = Parameter(name=self.Wname, dimensions=self.output_dimension, init=W_init, init_params=W_init_params).json
- self.json = merge(self.json,W_json)
+ self.Wname = W if type(W) is str else self.name + "W"
+ W_json = Parameter(
+ name=self.Wname,
+ dimensions=self.output_dimension,
+ init=W_init,
+ init_params=W_init_params,
+ ).json
+ self.json = merge(self.json, W_json)
if self.b is not None and self.b is not False:
if type(self.b) is Parameter:
- check('tw' not in self.b.dim and 'sw' not in self.b.dim, TypeError, f'The "bias" must no have a time dimensions but got {self.b.dim}.')
- check(type(self.b.dim['dim']) is int, ValueError, 'The "bias" dimensions must be an integer.')
- check(self.b.dim['dim'] == self.output_dimension, ValueError, 'output_dimension must be equal to the dim of the "bias".')
+ check(
+ "tw" not in self.b.dim and "sw" not in self.b.dim,
+ TypeError,
+ f'The "bias" must no have a time dimensions but got {self.b.dim}.',
+ )
+ check(
+ type(self.b.dim["dim"]) is int,
+ ValueError,
+ 'The "bias" dimensions must be an integer.',
+ )
+ check(
+ self.b.dim["dim"] == self.output_dimension,
+ ValueError,
+ 'output_dimension must be equal to the dim of the "bias".',
+ )
self.bname = self.b.name
b_json = self.b.json
else:
- self.bname = b if type(self.b) is str else self.name + 'b'
- b_json = Parameter(name=self.bname, dimensions=self.output_dimension, init=b_init, init_params=b_init_params).json
- self.json = merge(self.json,b_json)
+ self.bname = b if type(self.b) is str else self.name + "b"
+ b_json = Parameter(
+ name=self.bname,
+ dimensions=self.output_dimension,
+ init=b_init,
+ init_params=b_init_params,
+ ).json
+ self.json = merge(self.json, b_json)
self.json_stream = {}
@enforce_types
- def __call__(self, obj:Stream) -> Stream:
+ def __call__(self, obj: Stream) -> Stream:
stream_name = fir_relation_name + str(Stream.count)
- check('dim' in obj.dim and obj.dim['dim'] == 1,
- ValueError,
- f"Input dimension is {obj.dim['dim']} and not scalar")
- window = 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None)
+ check(
+ "dim" in obj.dim and obj.dim["dim"] == 1,
+ ValueError,
+ f"Input dimension is {obj.dim['dim']} and not scalar",
+ )
+ window = "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None)
json_stream_name = window + str(obj.dim[window])
if json_stream_name not in self.json_stream:
if len(self.json_stream) > 0:
- log.warning(f"The Fir {self.name} was called with inputs with different dimensions. If both Fir enter in the model an error will be raised.")
+ log.warning(
+ f"The Fir {self.name} was called with inputs with different dimensions. If both Fir enter in the model an error will be raised."
+ )
self.json_stream[json_stream_name] = copy.deepcopy(self.json)
- self.json_stream[json_stream_name]['Parameters'][self.Wname][window] = obj.dim[window]
+ self.json_stream[json_stream_name]["Parameters"][self.Wname][window] = obj.dim[
+ window
+ ]
if window:
if type(self.W) is Parameter:
- check(window in self.json['Parameters'][self.Wname],
- TypeError,
- f"The window \'{window}\' of the input is not in the W")
- check(self.json['Parameters'][self.Wname][window] == obj.dim[window],
- ValueError,
- f"The window \'{window}\' of the input must be the same of the W")
+ check(
+ window in self.json["Parameters"][self.Wname],
+ TypeError,
+ f"The window '{window}' of the input is not in the W",
+ )
+ check(
+ self.json["Parameters"][self.Wname][window] == obj.dim[window],
+ ValueError,
+ f"The window '{window}' of the input must be the same of the W",
+ )
else:
if type(self.W) is Parameter:
- cond = 'sw' not in self.json_stream[json_stream_name]['Parameters'][self.Wname] and 'tw' not in \
- self.json_stream[json_stream_name]['Parameters'][self.Wname]
- check(cond, KeyError, 'The W have a time window and the input no')
-
- stream_json = merge(self.json_stream[json_stream_name],obj.json)
- stream_json['Relations'][stream_name] = [fir_relation_name, [obj.name], self.Wname, self.bname, self.dropout]
- return Stream(stream_name, stream_json,{'dim':self.output_dimension, 'sw': 1})
+ cond = (
+ "sw"
+ not in self.json_stream[json_stream_name]["Parameters"][self.Wname]
+ and "tw"
+ not in self.json_stream[json_stream_name]["Parameters"][self.Wname]
+ )
+ check(cond, KeyError, "The W have a time window and the input no")
+
+ stream_json = merge(self.json_stream[json_stream_name], obj.json)
+ stream_json["Relations"][stream_name] = [
+ fir_relation_name,
+ [obj.name],
+ self.Wname,
+ self.bname,
+ self.dropout,
+ ]
+ return Stream(stream_name, stream_json, {"dim": self.output_dimension, "sw": 1})
class Fir_Layer(nn.Module):
@@ -186,7 +246,9 @@ def forward(self, x):
x = self.dropout(x)
return x
+
def createFir(self, *inputs):
return Fir_Layer(weights=inputs[0], bias=inputs[1], dropout=inputs[2])
+
setattr(Model, fir_relation_name, createFir)
diff --git a/nnodely/layers/fuzzify.py b/src/nnodely/layers/fuzzify.py
similarity index 53%
rename from nnodely/layers/fuzzify.py
rename to src/nnodely/layers/fuzzify.py
index 2e65ad8f..2e1063dc 100644
--- a/nnodely/layers/fuzzify.py
+++ b/src/nnodely/layers/fuzzify.py
@@ -1,4 +1,7 @@
-import inspect, copy, textwrap, torch
+import inspect
+import copy
+import textwrap
+import torch
import numpy as np
import torch.nn as nn
@@ -10,7 +13,8 @@
from nnodely.support.utils import check, enforce_types
from nnodely.support.jsonutils import merge
-fuzzify_relation_name = 'Fuzzify'
+fuzzify_relation_name = "Fuzzify"
+
class Fuzzify(NeuObj):
"""
@@ -27,7 +31,7 @@ class Fuzzify(NeuObj):
The `output_dimension` will be inferred from the number of centers provided.
functions : str, list, or Callable, optional
The fuzzy functions to use. Can be a string specifying a predefined function type, a custom function, or a list of callable functions. Default is 'Triangular'.
-
+
Notes
-----
.. note::
@@ -48,85 +52,122 @@ class Fuzzify(NeuObj):
.. include:: /examples_basics/layer_module_ex/fuzzy.rst
"""
+
@enforce_types
- def __init__(self, output_dimension: int | None = None,
- range: list | None = None, *,
- centers: list | None = None,
- functions: str | list | Callable = 'Triangular'):
+ def __init__(
+ self,
+ output_dimension: int | None = None,
+ range: list | None = None,
+ *,
+ centers: list | None = None,
+ functions: str | list | Callable = "Triangular",
+ ):
self.relation_name = fuzzify_relation_name
- super().__init__('F' + fuzzify_relation_name + str(NeuObj.count))
- self.json['Functions'][self.name] = {}
+ super().__init__("F" + fuzzify_relation_name + str(NeuObj.count))
+ self.json["Functions"][self.name] = {}
if output_dimension is not None:
- check(range is not None, ValueError, 'if "output_dimension" is not None, "range" must be not setted')
- check(centers is None, ValueError,
- 'if "output_dimension" and "range" are not None, then "centers" must be None')
- self.output_dimension = {'dim': output_dimension}
- interval = ((range[1] - range[0]) / (output_dimension - 1))
- self.json['Functions'][self.name]['centers'] = np.arange(range[0], range[1] + interval, interval).tolist()
+ check(
+ range is not None,
+ ValueError,
+ 'if "output_dimension" is not None, "range" must be not setted',
+ )
+ check(
+ centers is None,
+ ValueError,
+ 'if "output_dimension" and "range" are not None, then "centers" must be None',
+ )
+ self.output_dimension = {"dim": output_dimension}
+ interval = (range[1] - range[0]) / (output_dimension - 1)
+ self.json["Functions"][self.name]["centers"] = np.arange(
+ range[0], range[1] + interval, interval
+ ).tolist()
else:
- check(centers is not None, ValueError, 'if "output_dimension" is None and "centers" must be setted')
- self.output_dimension = {'dim': len(centers)}
- self.json['Functions'][self.name]['centers'] = np.array(centers).tolist()
- self.json['Functions'][self.name]['dim_out'] = copy.deepcopy(self.output_dimension)
+ check(
+ centers is not None,
+ ValueError,
+ 'if "output_dimension" is None and "centers" must be setted',
+ )
+ self.output_dimension = {"dim": len(centers)}
+ self.json["Functions"][self.name]["centers"] = np.array(centers).tolist()
+ self.json["Functions"][self.name]["dim_out"] = copy.deepcopy(
+ self.output_dimension
+ )
if type(functions) is str:
- self.json['Functions'][self.name]['functions'] = functions
- self.json['Functions'][self.name]['names'] = functions
+ self.json["Functions"][self.name]["functions"] = functions
+ self.json["Functions"][self.name]["names"] = functions
elif type(functions) is list:
- self.json['Functions'][self.name]['functions'] = []
- self.json['Functions'][self.name]['names'] = []
+ self.json["Functions"][self.name]["functions"] = []
+ self.json["Functions"][self.name]["names"] = []
for func in functions:
- code = textwrap.dedent(inspect.getsource(func)).replace('\"', '\'')
- self.json['Functions'][self.name]['functions'].append(code)
- self.json['Functions'][self.name]['names'].append(func.__name__)
+ code = textwrap.dedent(inspect.getsource(func)).replace('"', "'")
+ self.json["Functions"][self.name]["functions"].append(code)
+ self.json["Functions"][self.name]["names"].append(func.__name__)
else:
- code = textwrap.dedent(inspect.getsource(functions)).replace('\"', '\'')
- self.json['Functions'][self.name]['functions'] = code
- self.json['Functions'][self.name]['names'] = functions.__name__
+ code = textwrap.dedent(inspect.getsource(functions)).replace('"', "'")
+ self.json["Functions"][self.name]["functions"] = code
+ self.json["Functions"][self.name]["names"] = functions.__name__
@enforce_types
def __call__(self, obj: Stream) -> Stream:
stream_name = fuzzify_relation_name + str(Stream.count)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Fuzzify operation.")
- check('dim' in obj.dim and obj.dim['dim'] == 1, ValueError, 'Input dimension must be scalar')
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Fuzzify operation.",
+ )
+ check(
+ "dim" in obj.dim and obj.dim["dim"] == 1,
+ ValueError,
+ "Input dimension must be scalar",
+ )
output_dimension = copy.deepcopy(obj.dim)
output_dimension.update(self.output_dimension)
stream_json = merge(self.json, obj.json)
- stream_json['Relations'][stream_name] = [fuzzify_relation_name, [obj.name], self.name]
+ stream_json["Relations"][stream_name] = [
+ fuzzify_relation_name,
+ [obj.name],
+ self.name,
+ ]
return Stream(stream_name, stream_json, output_dimension)
+
def return_fuzzify(json, xlim=None, num_points=1000):
if xlim is not None:
x = torch.from_numpy(np.linspace(xlim[0], xlim[1], num=num_points))
else:
- x = torch.from_numpy(np.linspace(json['centers'][0] - 2, json['centers'][-1] + 2, num=num_points))
- chan_centers = np.array(json['centers'])
+ x = torch.from_numpy(
+ np.linspace(json["centers"][0] - 2, json["centers"][-1] + 2, num=num_points)
+ )
+ chan_centers = np.array(json["centers"])
activ_fun = {}
- if isinstance(json['names'], list):
- n_func = len(json['names'])
+ if isinstance(json["names"], list):
+ n_func = len(json["names"])
else:
n_func = 1
for i in range(len(chan_centers)):
- if json['functions'] == 'Triangular':
+ if json["functions"] == "Triangular":
activ_fun[i] = triangular(x, i, chan_centers).tolist()
- elif json['functions'] == 'Rectangular':
+ elif json["functions"] == "Rectangular":
activ_fun[i] = rectangular(x, i, chan_centers).tolist()
else:
- if isinstance(json['names'], list):
+ if isinstance(json["names"], list):
if i >= n_func:
func_idx = i - round(n_func * (i // n_func))
else:
func_idx = i
- exec(json['functions'][func_idx], globals())
- function_to_call = globals()[json['names'][func_idx]]
+ exec(json["functions"][func_idx], globals())
+ function_to_call = globals()[json["names"][func_idx]]
else:
- exec(json['functions'], globals())
- function_to_call = globals()[json['names']]
- activ_fun[i] = custom_function(function_to_call, x, i, chan_centers).tolist()
+ exec(json["functions"], globals())
+ function_to_call = globals()[json["names"]]
+ activ_fun[i] = custom_function(
+ function_to_call, x, i, chan_centers
+ ).tolist()
return x.tolist(), activ_fun
+
def triangular(x, idx_channel, chan_centers):
# Compute the number of channels
num_channels = len(chan_centers)
@@ -134,19 +175,33 @@ def triangular(x, idx_channel, chan_centers):
if idx_channel == 0:
if num_channels != 1:
ampl = chan_centers[1] - chan_centers[0]
- act_fcn = torch.minimum(torch.maximum(-(x - chan_centers[0]) / ampl + 1, torch.tensor(0.0)), torch.tensor(1.0))
+ act_fcn = torch.minimum(
+ torch.maximum(-(x - chan_centers[0]) / ampl + 1, torch.tensor(0.0)),
+ torch.tensor(1.0),
+ )
else:
# In case the user only wants one channel
act_fcn = 1
elif idx_channel != 0 and idx_channel == (num_channels - 1):
ampl = chan_centers[-1] - chan_centers[-2]
- act_fcn = torch.minimum(torch.maximum((x - chan_centers[-2]) / ampl, torch.tensor(0.0)), torch.tensor(1.0))
+ act_fcn = torch.minimum(
+ torch.maximum((x - chan_centers[-2]) / ampl, torch.tensor(0.0)),
+ torch.tensor(1.0),
+ )
else:
ampl_1 = chan_centers[idx_channel] - chan_centers[idx_channel - 1]
ampl_2 = chan_centers[idx_channel + 1] - chan_centers[idx_channel]
- act_fcn = torch.minimum(torch.maximum((x - chan_centers[idx_channel - 1]) / ampl_1, torch.tensor(0.0)), torch.maximum(-(x - chan_centers[idx_channel]) / ampl_2 + 1, torch.tensor(0.0)))
+ act_fcn = torch.minimum(
+ torch.maximum(
+ (x - chan_centers[idx_channel - 1]) / ampl_1, torch.tensor(0.0)
+ ),
+ torch.maximum(
+ -(x - chan_centers[idx_channel]) / ampl_2 + 1, torch.tensor(0.0)
+ ),
+ )
return act_fcn
+
def rectangular(x, idx_channel, chan_centers):
## compute number of channels
num_channels = len(chan_centers)
@@ -162,9 +217,18 @@ def rectangular(x, idx_channel, chan_centers):
width = abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2
act_fcn = torch.where(x >= chan_centers[idx_channel] - width, 1.0, 0.0)
else:
- width_forward = abs(chan_centers[idx_channel + 1] - chan_centers[idx_channel]) / 2
- width_backward = abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2
- act_fcn = torch.where((x >= chan_centers[idx_channel] - width_backward) & (x < chan_centers[idx_channel] + width_forward), 1.0, 0.0)
+ width_forward = (
+ abs(chan_centers[idx_channel + 1] - chan_centers[idx_channel]) / 2
+ )
+ width_backward = (
+ abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2
+ )
+ act_fcn = torch.where(
+ (x >= chan_centers[idx_channel] - width_backward)
+ & (x < chan_centers[idx_channel] + width_forward),
+ 1.0,
+ 0.0,
+ )
return act_fcn
@@ -172,39 +236,48 @@ def custom_function(func, x, idx_channel, chan_centers):
act_fcn = func(x - chan_centers[idx_channel])
return act_fcn
+
class Fuzzify_Layer(nn.Module):
def __init__(self, params):
super().__init__()
- self.centers = params['centers']
- self.function = params['functions']
- self.dimension = params['dim_out']['dim']
- self.name = params['names']
+ self.centers = params["centers"]
+ self.function = params["functions"]
+ self.dimension = params["dim_out"]["dim"]
+ self.name = params["names"]
if type(self.name) is list:
self.n_func = len(self.name)
for func, name in zip(self.function, self.name):
## Add the function to the globals
try:
- code = 'import torch\n@torch.fx.wrap\n' + func
+ code = "import torch\n@torch.fx.wrap\n" + func
exec(code, globals())
except Exception as e:
- check(False, RuntimeError, f"An error occurred when running the function '{name}':\n {e}")
+ check(
+ False,
+ RuntimeError,
+ f"An error occurred when running the function '{name}':\n {e}",
+ )
else:
self.n_func = 1
- if self.name not in ['Triangular', 'Rectangular']: ## custom function
+ if self.name not in ["Triangular", "Rectangular"]: ## custom function
## Add the function to the globals
try:
- code = 'import torch\n@torch.fx.wrap\n' + self.function
+ code = "import torch\n@torch.fx.wrap\n" + self.function
exec(code, globals())
except Exception as e:
- check(False, RuntimeError, f"An error occurred when running the function '{self.name}':\n {e}")
+ check(
+ False,
+ RuntimeError,
+ f"An error occurred when running the function '{self.name}':\n {e}",
+ )
def forward(self, x):
res = torch.zeros_like(x).repeat(1, 1, self.dimension)
- if self.function == 'Triangular':
+ if self.function == "Triangular":
for i in range(len(self.centers)):
slicing(res, torch.tensor(i), triangular(x, i, self.centers))
- elif self.function == 'Rectangular':
+ elif self.function == "Rectangular":
for i in range(len(self.centers)):
slicing(res, torch.tensor(i), rectangular(x, i, self.centers))
else: ## Custom_function
@@ -212,7 +285,11 @@ def forward(self, x):
# Retrieve the function object from the globals dictionary
function_to_call = globals()[self.name]
for i in range(len(self.centers)):
- slicing(res, torch.tensor(i), custom_function(function_to_call, x, i, self.centers))
+ slicing(
+ res,
+ torch.tensor(i),
+ custom_function(function_to_call, x, i, self.centers),
+ )
else: ## we have multiple functions
for i in range(len(self.centers)):
if i >= self.n_func:
@@ -220,14 +297,21 @@ def forward(self, x):
else:
func_idx = i
function_to_call = globals()[self.name[func_idx]]
- slicing(res, torch.tensor(i), custom_function(function_to_call, x, i, self.centers))
+ slicing(
+ res,
+ torch.tensor(i),
+ custom_function(function_to_call, x, i, self.centers),
+ )
return res
+
@torch.fx.wrap
def slicing(res, i, x):
- res[:, :, i:i + 1] = x
+ res[:, :, i : i + 1] = x
+
def createFuzzify(self, *params):
return Fuzzify_Layer(params[0])
+
setattr(Model, fuzzify_relation_name, createFuzzify)
diff --git a/nnodely/layers/input.py b/src/nnodely/layers/input.py
similarity index 55%
rename from nnodely/layers/input.py
rename to src/nnodely/layers/input.py
index 061f955d..1bf48f0e 100644
--- a/nnodely/layers/input.py
+++ b/src/nnodely/layers/input.py
@@ -6,6 +6,7 @@
from nnodely.layers.part import SamplePart, TimePart
from nnodely.layers.timeoperation import Differentiate, Integrate
+
class Input(NeuObj):
"""
Represents an Input in the neural network model.
@@ -30,7 +31,8 @@ class Input(NeuObj):
json : dict
A dictionary containing the configuration of the Input.
"""
- def __init__(self, name:str, *, dimensions:int = 1):
+
+ def __init__(self, name: str, *, dimensions: int = 1):
"""
Initializes the Input object.
@@ -44,12 +46,12 @@ def __init__(self, name:str, *, dimensions:int = 1):
The number of dimensions for the Input. Default is 1.
"""
NeuObj.__init__(self, name)
- check(type(dimensions) == int, TypeError,"The dimensions must be a integer")
- self.json['Inputs'][self.name] = {'dim': dimensions }
- self.dim = {'dim': dimensions}
+ check(type(dimensions) == int, TypeError, "The dimensions must be a integer")
+ self.json["Inputs"][self.name] = {"dim": dimensions}
+ self.dim = {"dim": dimensions}
@enforce_types
- def tw(self, tw:int|float|list, offset:int|float|None = None) -> Stream:
+ def tw(self, tw: int | float | list, offset: int | float | None = None) -> Stream:
"""
Selects a time window for the Input.
@@ -75,23 +77,39 @@ def tw(self, tw:int|float|list, offset:int|float|None = None) -> Stream:
dim = copy.deepcopy(self.dim)
json = copy.deepcopy(self.json)
if type(tw) is list:
- check(len(tw) == 2, TypeError, "The time window must be a list of two elements.")
- check(tw[1] > tw[0], ValueError, "The dimension of the sample window must be positive")
- json['Inputs'][self.name]['tw'] = tw
+ check(
+ len(tw) == 2,
+ TypeError,
+ "The time window must be a list of two elements.",
+ )
+ check(
+ tw[1] > tw[0],
+ ValueError,
+ "The dimension of the sample window must be positive",
+ )
+ json["Inputs"][self.name]["tw"] = tw
tw = tw[1] - tw[0]
else:
- json['Inputs'][self.name]['tw'] = [-tw, 0]
+ json["Inputs"][self.name]["tw"] = [-tw, 0]
check(tw > 0, ValueError, "The time window must be positive")
- dim['tw'] = tw
+ dim["tw"] = tw
if offset is not None:
- check(json['Inputs'][self.name]['tw'][0] <= offset < json['Inputs'][self.name]['tw'][1],
- IndexError,
- "The offset must be inside the time window")
- return TimePart(Stream(self.name, json, dim), json['Inputs'][self.name]['tw'][0], json['Inputs'][self.name]['tw'][1], offset)
-
+ check(
+ json["Inputs"][self.name]["tw"][0]
+ <= offset
+ < json["Inputs"][self.name]["tw"][1],
+ IndexError,
+ "The offset must be inside the time window",
+ )
+ return TimePart(
+ Stream(self.name, json, dim),
+ json["Inputs"][self.name]["tw"][0],
+ json["Inputs"][self.name]["tw"][1],
+ offset,
+ )
@enforce_types
- def sw(self, sw:int|list, offset:int|None = None) -> Stream:
+ def sw(self, sw: int | list, offset: int | None = None) -> Stream:
"""
Selects a sample window for the Input.
@@ -119,24 +137,45 @@ def sw(self, sw:int|list, offset:int|None = None) -> Stream:
dim = copy.deepcopy(self.dim)
json = copy.deepcopy(self.json)
if type(sw) is list:
- check(len(sw) == 2, TypeError, "The sample window must be a list of two elements.")
- check(type(sw[0]) == int and type(sw[1]) == int, TypeError, "The sample window must be integer")
- check(sw[1] > sw[0], ValueError, "The dimension of the sample window must be positive")
- json['Inputs'][self.name]['sw'] = sw
+ check(
+ len(sw) == 2,
+ TypeError,
+ "The sample window must be a list of two elements.",
+ )
+ check(
+ type(sw[0]) == int and type(sw[1]) == int,
+ TypeError,
+ "The sample window must be integer",
+ )
+ check(
+ sw[1] > sw[0],
+ ValueError,
+ "The dimension of the sample window must be positive",
+ )
+ json["Inputs"][self.name]["sw"] = sw
sw = sw[1] - sw[0]
else:
check(type(sw) == int, TypeError, "The sample window must be integer")
- json['Inputs'][self.name]['sw'] = [-sw, 0]
+ json["Inputs"][self.name]["sw"] = [-sw, 0]
check(sw > 0, ValueError, "The sample window must be positive")
- dim['sw'] = sw
+ dim["sw"] = sw
if offset is not None:
- check(json['Inputs'][self.name]['sw'][0] <= offset < json['Inputs'][self.name]['sw'][1],
- IndexError,
- "The offset must be inside the sample window")
- return SamplePart(Stream(self.name, json, dim), json['Inputs'][self.name]['sw'][0], json['Inputs'][self.name]['sw'][1], offset)
+ check(
+ json["Inputs"][self.name]["sw"][0]
+ <= offset
+ < json["Inputs"][self.name]["sw"][1],
+ IndexError,
+ "The offset must be inside the sample window",
+ )
+ return SamplePart(
+ Stream(self.name, json, dim),
+ json["Inputs"][self.name]["sw"][0],
+ json["Inputs"][self.name]["sw"][1],
+ offset,
+ )
@enforce_types
- def z(self, delay:int) -> Stream:
+ def z(self, delay: int) -> Stream:
"""
Considering the Zeta transform notation. The function is used to selects a unitary delay from the Input.
@@ -157,9 +196,14 @@ def z(self, delay:int) -> Stream:
dim = copy.deepcopy(self.dim)
json = copy.deepcopy(self.json)
sw = [(-delay) - 1, (-delay)]
- json['Inputs'][self.name]['sw'] = sw
- dim['sw'] = sw[1] - sw[0]
- return SamplePart(Stream(self.name, json, dim), json['Inputs'][self.name]['sw'][0], json['Inputs'][self.name]['sw'][1], None)
+ json["Inputs"][self.name]["sw"] = sw
+ dim["sw"] = sw[1] - sw[0]
+ return SamplePart(
+ Stream(self.name, json, dim),
+ json["Inputs"][self.name]["sw"][0],
+ json["Inputs"][self.name]["sw"][1],
+ None,
+ )
@enforce_types
def last(self) -> Stream:
@@ -186,7 +230,14 @@ def next(self) -> Stream:
return self.z(-1)
@enforce_types
- def s(self, order:int, *, der_name:str|None = None, int_name:str|None = None, method:str = 'euler') -> Stream:
+ def s(
+ self,
+ order: int,
+ *,
+ der_name: str | None = None,
+ int_name: str | None = None,
+ method: str = "euler",
+ ) -> Stream:
"""
Considering the Laplace transform notation. The function is used to operate an integral or derivate operation on the input.
The order of the integral or the derivative operation is indicated by the order parameter.
@@ -203,19 +254,25 @@ def s(self, order:int, *, der_name:str|None = None, int_name:str|None = None, me
Stream
A Stream of the signal represents the integral or derivation operation.
"""
- check(order != 0, ValueError, "The order must be a positive or negative integer not a zero")
+ check(
+ order != 0,
+ ValueError,
+ "The order must be a positive or negative integer not a zero",
+ )
if order > 0:
o = self.last()
for i in range(order):
- o = Differentiate(o, der_name = der_name, int_name = int_name, method = method)
+ o = Differentiate(
+ o, der_name=der_name, int_name=int_name, method=method
+ )
elif order < 0:
o = self.last()
for i in range(-order):
- o = Integrate(o, der_name = der_name, int_name = int_name, method = method)
+ o = Integrate(o, der_name=der_name, int_name=int_name, method=method)
return o
@enforce_types
- def connect(self, obj:Stream) -> "Input":
+ def connect(self, obj: Stream) -> "Input":
"""
Update and return the current Input with a given Stream object.
@@ -236,17 +293,24 @@ def connect(self, obj:Stream) -> "Input":
KeyError
If the Input variable is already connected.
"""
- check(type(obj) is Stream, TypeError,
- f"The {obj} must be a Stream and not a {type(obj)}.")
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The {obj} must be a Stream and not a {type(obj)}.",
+ )
self.json = merge(self.json, obj.json)
- check('closedLoop' not in self.json['Inputs'][self.name] or 'connect' not in self.json['Inputs'][self.name], KeyError,
- f"The Input variable {self.name} is already connected.")
- self.json['Inputs'][self.name]['connect'] = obj.name
- self.json['Inputs'][self.name]['local'] = 1
+ check(
+ "closedLoop" not in self.json["Inputs"][self.name]
+ or "connect" not in self.json["Inputs"][self.name],
+ KeyError,
+ f"The Input variable {self.name} is already connected.",
+ )
+ self.json["Inputs"][self.name]["connect"] = obj.name
+ self.json["Inputs"][self.name]["local"] = 1
return self
@enforce_types
- def closedLoop(self, obj:Stream) -> "Input":
+ def closedLoop(self, obj: Stream) -> "Input":
"""
Update and return the current Input in a closed loop with a given Stream object.
@@ -267,41 +331,58 @@ def closedLoop(self, obj:Stream) -> "Input":
KeyError
If the Input variable is already connected.
"""
- from nnodely.layers.input import Input
- check(type(obj) is Stream, TypeError,
- f"The {obj} must be a Stream and not a {type(obj)}.")
+
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The {obj} must be a Stream and not a {type(obj)}.",
+ )
self.json = merge(self.json, obj.json)
- check('closedLoop' not in self.json['Inputs'][self.name] or 'connect' not in self.json['Inputs'][self.name],
- KeyError,
- f"The Input variable {self.name} is already connected.")
- self.json['Inputs'][self.name]['closedLoop'] = self.name
- self.json['Inputs'][self.name]['local'] = 1
+ check(
+ "closedLoop" not in self.json["Inputs"][self.name]
+ or "connect" not in self.json["Inputs"][self.name],
+ KeyError,
+ f"The Input variable {self.name} is already connected.",
+ )
+ self.json["Inputs"][self.name]["closedLoop"] = self.name
+ self.json["Inputs"][self.name]["local"] = 1
return self
def __str__(self):
- return stream_to_str(self, 'Input')
+ return stream_to_str(self, "Input")
def __repr__(self):
return self.__str__()
+
# connect operation
-connect_name = 'connect'
-closedloop_name = 'closedLoop'
+connect_name = "connect"
+closedloop_name = "closedLoop"
+
class Connect(Stream, ToStream):
@enforce_types
- def __init__(self, obj1:Stream, obj2:Input, *, local:bool=False) -> Stream:
- super().__init__(obj1.name,merge(obj1.json, obj2.json),obj1.dim)
- check(closedloop_name not in self.json['Inputs'][obj2.name] or connect_name not in self.json['Inputs'][obj2.name],
- KeyError,f"The input variable {obj2.name} is already connected.")
- self.json['Inputs'][obj2.name][connect_name] = obj1.name
- self.json['Inputs'][obj2.name]['local'] = int(local)
+ def __init__(self, obj1: Stream, obj2: Input, *, local: bool = False) -> Stream:
+ super().__init__(obj1.name, merge(obj1.json, obj2.json), obj1.dim)
+ check(
+ closedloop_name not in self.json["Inputs"][obj2.name]
+ or connect_name not in self.json["Inputs"][obj2.name],
+ KeyError,
+ f"The input variable {obj2.name} is already connected.",
+ )
+ self.json["Inputs"][obj2.name][connect_name] = obj1.name
+ self.json["Inputs"][obj2.name]["local"] = int(local)
+
class ClosedLoop(Stream, ToStream):
@enforce_types
- def __init__(self, obj1:Stream, obj2:Input, *, local:bool=False) -> Stream:
+ def __init__(self, obj1: Stream, obj2: Input, *, local: bool = False) -> Stream:
super().__init__(obj1.name, merge(obj1.json, obj2.json), obj1.dim)
- check(closedloop_name not in self.json['Inputs'][obj2.name] or connect_name not in self.json['Inputs'][obj2.name],
- KeyError, f"The input variable {obj2.name} is already connected.")
- self.json['Inputs'][obj2.name][closedloop_name] = obj1.name
- self.json['Inputs'][obj2.name]['local'] = int(local)
\ No newline at end of file
+ check(
+ closedloop_name not in self.json["Inputs"][obj2.name]
+ or connect_name not in self.json["Inputs"][obj2.name],
+ KeyError,
+ f"The input variable {obj2.name} is already connected.",
+ )
+ self.json["Inputs"][obj2.name][closedloop_name] = obj1.name
+ self.json["Inputs"][obj2.name]["local"] = int(local)
diff --git a/nnodely/layers/interpolation.py b/src/nnodely/layers/interpolation.py
similarity index 63%
rename from nnodely/layers/interpolation.py
rename to src/nnodely/layers/interpolation.py
index 8f1e90c0..7214df22 100644
--- a/nnodely/layers/interpolation.py
+++ b/src/nnodely/layers/interpolation.py
@@ -7,7 +7,9 @@
from nnodely.support.utils import check, enforce_types
from nnodely.support.jsonutils import merge
-interpolation_relation_name = 'Interpolation'
+interpolation_relation_name = "Interpolation"
+
+
class Interpolation(NeuObj):
"""
Represents an Interpolation relation in the neural network model.
@@ -35,40 +37,64 @@ class Interpolation(NeuObj):
>>> x = Input('x')
>>> rel1 = Interpolation(x_points=x_points,y_points=y_points, mode='linear')(x.last())
-
+
>>> out = Output('out',rel1)
"""
@enforce_types
- def __init__(self, x_points:list,
- y_points:list, *,
- mode:str|None = 'linear'):
+ def __init__(self, x_points: list, y_points: list, *, mode: str | None = "linear"):
self.relation_name = interpolation_relation_name
self.x_points = x_points
self.y_points = y_points
self.mode = mode
- self.available_modes = ['linear', 'polynomial']
-
- super().__init__('P' + interpolation_relation_name + str(NeuObj.count))
- check(len(x_points) == len(y_points), ValueError, 'The x_points and y_points must have the same length.')
- check(mode in self.available_modes, ValueError, f'The mode must be one of {self.available_modes}.')
- check(len(torch.tensor(x_points).shape) == 1, ValueError, 'The x_points must be a 1D tensor.')
- check(len(torch.tensor(y_points).shape) == 1, ValueError, 'The y_points must be a 1D tensor.')
+ self.available_modes = ["linear", "polynomial"]
+
+ super().__init__("P" + interpolation_relation_name + str(NeuObj.count))
+ check(
+ len(x_points) == len(y_points),
+ ValueError,
+ "The x_points and y_points must have the same length.",
+ )
+ check(
+ mode in self.available_modes,
+ ValueError,
+ f"The mode must be one of {self.available_modes}.",
+ )
+ check(
+ len(torch.tensor(x_points).shape) == 1,
+ ValueError,
+ "The x_points must be a 1D tensor.",
+ )
+ check(
+ len(torch.tensor(y_points).shape) == 1,
+ ValueError,
+ "The y_points must be a 1D tensor.",
+ )
@enforce_types
- def __call__(self, obj:Stream) -> Stream:
+ def __call__(self, obj: Stream) -> Stream:
stream_name = interpolation_relation_name + str(Stream.count)
- check(type(obj) is Stream, TypeError, f"The type of {obj} is {type(obj)} and is not supported for Interpolation operation.")
-
- stream_json = merge(self.json,obj.json)
- stream_json['Relations'][stream_name] = [interpolation_relation_name, [obj.name], self.x_points, self.y_points, self.mode]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Interpolation operation.",
+ )
+
+ stream_json = merge(self.json, obj.json)
+ stream_json["Relations"][stream_name] = [
+ interpolation_relation_name,
+ [obj.name],
+ self.x_points,
+ self.y_points,
+ self.mode,
+ ]
return Stream(stream_name, stream_json, obj.dim)
class Interpolation_Layer(nn.Module):
- def __init__(self, x_points, y_points, mode='linear'):
+ def __init__(self, x_points, y_points, mode="linear"):
super(Interpolation_Layer, self).__init__()
self.mode = mode
## Sort the points
@@ -83,13 +109,13 @@ def __init__(self, x_points, y_points, mode='linear'):
self.y_points = self.y_points.unsqueeze(-1)
def forward(self, x):
- if self.mode == 'linear':
+ if self.mode == "linear":
return self.linear_interpolation(x)
else:
raise NotImplementedError
-
+
def linear_interpolation(self, x):
- # Inputs:
+ # Inputs:
# x: query point, a tensor of shape torch.Size([N, 1, 1])
# x_data: map of x values, sorted in ascending order, a tensor of shape torch.Size([Q, 1])
# y_data: map of y values, a tensor of shape torch.Size([Q, 1])
@@ -97,14 +123,18 @@ def linear_interpolation(self, x):
# y: interpolated value at x, a tensor of shape torch.Size([N, 1, 1])
# Saturate x to the range of x_data
- x = torch.min(torch.max(x,self.x_points[0]),self.x_points[-1])
+ x = torch.min(torch.max(x, self.x_points[0]), self.x_points[-1])
# Find the index of the closest value in x_data
- idx = torch.argmin(torch.abs(self.x_points[:-1] - x),dim=1)
+ idx = torch.argmin(torch.abs(self.x_points[:-1] - x), dim=1)
# Linear interpolation
- y = self.y_points[idx] + (self.y_points[idx+1] - self.y_points[idx])/(self.x_points[idx+1] - self.x_points[idx])*(x - self.x_points[idx])
+ y = self.y_points[idx] + (self.y_points[idx + 1] - self.y_points[idx]) / (
+ self.x_points[idx + 1] - self.x_points[idx]
+ ) * (x - self.x_points[idx])
return y
+
def createInterpolation(self, *inputs):
return Interpolation_Layer(x_points=inputs[0], y_points=inputs[1], mode=inputs[2])
-setattr(Model, interpolation_relation_name, createInterpolation)
\ No newline at end of file
+
+setattr(Model, interpolation_relation_name, createInterpolation)
diff --git a/nnodely/layers/linear.py b/src/nnodely/layers/linear.py
similarity index 51%
rename from nnodely/layers/linear.py
rename to src/nnodely/layers/linear.py
index 0a6ac3ed..d7511107 100644
--- a/nnodely/layers/linear.py
+++ b/src/nnodely/layers/linear.py
@@ -12,9 +12,11 @@
from nnodely.support.jsonutils import merge
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
-linear_relation_name = 'Linear'
+linear_relation_name = "Linear"
+
class Linear(NeuObj, AutoToStream):
"""
@@ -77,14 +79,18 @@ class Linear(NeuObj, AutoToStream):
"""
@enforce_types
- def __init__(self, output_dimension:int|None = None, *,
- W_init:Callable|str|None = None,
- W_init_params:dict|None = None,
- b_init:Callable|str|None = None,
- b_init_params:dict|None = None,
- W:Parameter|str|None = None,
- b:bool|str|Parameter|None = None,
- dropout:int|float = 0):
+ def __init__(
+ self,
+ output_dimension: int | None = None,
+ *,
+ W_init: Callable | str | None = None,
+ W_init_params: dict | None = None,
+ b_init: Callable | str | None = None,
+ b_init_params: dict | None = None,
+ W: Parameter | str | None = None,
+ b: bool | str | Parameter | None = None,
+ dropout: int | float = 0,
+ ):
self.W = W
self.b = b
@@ -92,57 +98,113 @@ def __init__(self, output_dimension:int|None = None, *,
self.Wname = None
self.dropout = dropout
- super().__init__('P' + linear_relation_name + str(NeuObj.count))
+ super().__init__("P" + linear_relation_name + str(NeuObj.count))
if type(self.W) is Parameter:
- check('tw' not in self.W.dim.keys() and 'sw' not in self.W.dim.keys(), TypeError, f'The "W" must no have time dimension but was {W.dim}.')
- check(len(self.W.dim['dim']) == 2, ValueError,'The "W" dimensions must be a list of 2.')
- self.output_dimension = self.W.dim['dim'][1]
+ check(
+ "tw" not in self.W.dim.keys() and "sw" not in self.W.dim.keys(),
+ TypeError,
+ f'The "W" must no have time dimension but was {W.dim}.',
+ )
+ check(
+ len(self.W.dim["dim"]) == 2,
+ ValueError,
+ 'The "W" dimensions must be a list of 2.',
+ )
+ self.output_dimension = self.W.dim["dim"][1]
if output_dimension is not None:
- check(self.W.dim['dim'][1] == output_dimension, ValueError, 'output_dimension must be equal to the second dim of "W".')
+ check(
+ self.W.dim["dim"][1] == output_dimension,
+ ValueError,
+ 'output_dimension must be equal to the second dim of "W".',
+ )
self.Wname = self.W.name
W_json = W.json
else:
self.output_dimension = 1 if output_dimension is None else output_dimension
- self.Wname = W if type(W) is str else self.name + 'W'
- W_json = Parameter(name=self.Wname, dimensions=self.output_dimension, init=W_init, init_params=W_init_params).json
- self.json = merge(self.json,W_json)
+ self.Wname = W if type(W) is str else self.name + "W"
+ W_json = Parameter(
+ name=self.Wname,
+ dimensions=self.output_dimension,
+ init=W_init,
+ init_params=W_init_params,
+ ).json
+ self.json = merge(self.json, W_json)
if self.b is not None and self.b is not False:
if type(self.b) is Parameter:
- check('tw' not in self.b.dim and 'sw' not in self.b.dim, TypeError, f'The "bias" must no have a time dimensions but got {self.b.dim}.')
- check(type(self.b.dim['dim']) is int, TypeError, 'The "b" dimensions must be an integer.')
- check(self.b.dim['dim'] == self.output_dimension, ValueError,'output_dimension must be equal to the dim of the "b".')
+ check(
+ "tw" not in self.b.dim and "sw" not in self.b.dim,
+ TypeError,
+ f'The "bias" must no have a time dimensions but got {self.b.dim}.',
+ )
+ check(
+ type(self.b.dim["dim"]) is int,
+ TypeError,
+ 'The "b" dimensions must be an integer.',
+ )
+ check(
+ self.b.dim["dim"] == self.output_dimension,
+ ValueError,
+ 'output_dimension must be equal to the dim of the "b".',
+ )
self.bname = self.b.name
b_json = self.b.json
else:
- self.bname = b if type(self.b) is str else self.name + 'b'
- b_json = Parameter(name=self.bname, dimensions=self.output_dimension, init=b_init, init_params=b_init_params).json
- self.json = merge(self.json,b_json)
+ self.bname = b if type(self.b) is str else self.name + "b"
+ b_json = Parameter(
+ name=self.bname,
+ dimensions=self.output_dimension,
+ init=b_init,
+ init_params=b_init_params,
+ ).json
+ self.json = merge(self.json, b_json)
self.json_stream = {}
@enforce_types
- def __call__(self, obj:Stream) -> Stream:
+ def __call__(self, obj: Stream) -> Stream:
stream_name = linear_relation_name + str(Stream.count)
- check(type(obj) is Stream, TypeError,f"The type of {obj} is {type(obj)} and is not supported for Linear operation.")
- window = 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None)
-
- json_stream_name = obj.dim['dim']
- if obj.dim['dim'] not in self.json_stream:
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Linear operation.",
+ )
+ window = "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None)
+
+ json_stream_name = obj.dim["dim"]
+ if obj.dim["dim"] not in self.json_stream:
if len(self.json_stream) > 0:
- log.warning(f"The Linear {self.name} was called with inputs with different dimensions. If both Linear enter in the model an error will be raised.")
+ log.warning(
+ f"The Linear {self.name} was called with inputs with different dimensions. If both Linear enter in the model an error will be raised."
+ )
self.json_stream[json_stream_name] = copy.deepcopy(self.json)
- self.json_stream[json_stream_name]['Parameters'][self.Wname]['dim'] = [obj.dim['dim'],self.output_dimension,]
+ self.json_stream[json_stream_name]["Parameters"][self.Wname]["dim"] = [
+ obj.dim["dim"],
+ self.output_dimension,
+ ]
if type(self.W) is Parameter:
- check(self.json['Parameters'][self.Wname]['dim'][0] == obj.dim['dim'], ValueError,
- 'the input dimension must be equal to the first dim of the parameter')
-
- stream_json = merge(self.json_stream[json_stream_name],obj.json)
- stream_json['Relations'][stream_name] = [linear_relation_name, [obj.name], self.Wname, self.bname, self.dropout]
- return Stream(stream_name, stream_json,{'dim': self.output_dimension, window:obj.dim[window]})
+ check(
+ self.json["Parameters"][self.Wname]["dim"][0] == obj.dim["dim"],
+ ValueError,
+ "the input dimension must be equal to the first dim of the parameter",
+ )
+
+ stream_json = merge(self.json_stream[json_stream_name], obj.json)
+ stream_json["Relations"][stream_name] = [
+ linear_relation_name,
+ [obj.name],
+ self.Wname,
+ self.bname,
+ self.dropout,
+ ]
+ return Stream(
+ stream_name,
+ stream_json,
+ {"dim": self.output_dimension, window: obj.dim[window]},
+ )
class Linear_Layer(nn.Module):
@@ -155,15 +217,19 @@ def __init__(self, weights, bias=None, dropout=0):
def forward(self, x):
# x is expected to be of shape [batch, window, input_dimension]
# Using torch.einsum for batch matrix multiplication
- y = torch.einsum('bwi,io->bwo', x, self.weights) # y will have shape [batch, window, output_features]
+ y = torch.einsum(
+ "bwi,io->bwo", x, self.weights
+ ) # y will have shape [batch, window, output_features]
if self.bias is not None:
- y += self.bias
+ y += self.bias
# Add dropout if necessary
if self.dropout is not None:
y = self.dropout(y)
return y
+
def createLinear(self, *inputs):
return Linear_Layer(weights=inputs[0], bias=inputs[1], dropout=inputs[2])
+
setattr(Model, linear_relation_name, createLinear)
diff --git a/nnodely/layers/localmodel.py b/src/nnodely/layers/localmodel.py
similarity index 69%
rename from nnodely/layers/localmodel.py
rename to src/nnodely/layers/localmodel.py
index 095cd0b9..adab79bd 100644
--- a/nnodely/layers/localmodel.py
+++ b/src/nnodely/layers/localmodel.py
@@ -6,7 +6,8 @@
from nnodely.layers.part import Select
from nnodely.support.utils import check, enforce_types
-localmodel_relation_name = 'LocalModel'
+localmodel_relation_name = "LocalModel"
+
class LocalModel(NeuObj):
"""
@@ -15,9 +16,9 @@ class LocalModel(NeuObj):
Parameters
----------
input_function : Callable, optional
- A callable function to process the inputs.
+ A callable function to process the inputs.
output_function : Callable, optional
- A callable function to process the outputs.
+ A callable function to process the outputs.
pass_indexes : bool, optional
A boolean indicating whether to pass indexes to the functions. Default is False.
@@ -37,39 +38,54 @@ class LocalModel(NeuObj):
.. include:: /examples_basics/layer_module_ex/localmodel.rst
"""
+
@enforce_types
- def __init__(self, input_function:Callable|None = None,
- output_function:Callable|None = None, *,
- pass_indexes:bool = False):
+ def __init__(
+ self,
+ input_function: Callable | None = None,
+ output_function: Callable | None = None,
+ *,
+ pass_indexes: bool = False,
+ ):
self.relation_name = localmodel_relation_name
self.pass_indexes = pass_indexes
super().__init__(localmodel_relation_name + str(NeuObj.count))
- self.json['Functions'][self.name] = {}
+ self.json["Functions"][self.name] = {}
if input_function is not None:
- check(callable(input_function), TypeError, 'The input_function must be callable')
+ check(
+ callable(input_function),
+ TypeError,
+ "The input_function must be callable",
+ )
self.input_function = input_function
if output_function is not None:
- check(callable(output_function), TypeError, 'The output_function must be callable')
+ check(
+ callable(output_function),
+ TypeError,
+ "The output_function must be callable",
+ )
self.output_function = output_function
@enforce_types
- def __call__(self, inputs:Stream|tuple, activations:Stream|tuple= None):
+ def __call__(self, inputs: Stream | tuple, activations: Stream | tuple = None):
out_sum = []
if type(activations) is not tuple:
activations = (activations,)
- self.___activations_matrix(activations,inputs,out_sum)
+ self.___activations_matrix(activations, inputs, out_sum)
out = out_sum[0]
- for ind in range(1,len(out_sum)):
+ for ind in range(1, len(out_sum)):
out = out + out_sum[ind]
return out
# Definisci una funzione ricorsiva per annidare i cicli for
def ___activations_matrix(self, activations, inputs, out, idx=0, idx_list=[]):
if idx != len(activations):
- for i in range(activations[idx].dim['dim']):
- self.___activations_matrix(activations, inputs, out, idx+1, idx_list+[i])
+ for i in range(activations[idx].dim["dim"]):
+ self.___activations_matrix(
+ activations, inputs, out, idx + 1, idx_list + [i]
+ )
else:
if self.input_function is not None:
if len(inspect.signature(self.input_function).parameters) == 0:
@@ -89,12 +105,16 @@ def ___activations_matrix(self, activations, inputs, out, idx=0, idx_list=[]):
else:
out_in = self.input_function(inputs)
else:
- check(type(inputs) is not tuple, TypeError, 'The input cannot be a tuple without input_function')
+ check(
+ type(inputs) is not tuple,
+ TypeError,
+ "The input cannot be a tuple without input_function",
+ )
out_in = inputs
act = Select(activations[0], idx_list[0])
- for ind, i in enumerate(idx_list[1:]):
- act = act * Select(activations[ind+1], i)
+ for ind, i in enumerate(idx_list[1:]):
+ act = act * Select(activations[ind + 1], i)
prod = out_in * act
diff --git a/nnodely/layers/neuralODE.py b/src/nnodely/layers/neuralODE.py
similarity index 54%
rename from nnodely/layers/neuralODE.py
rename to src/nnodely/layers/neuralODE.py
index 787498ff..c41017a1 100644
--- a/nnodely/layers/neuralODE.py
+++ b/src/nnodely/layers/neuralODE.py
@@ -1,4 +1,6 @@
-import inspect, copy, textwrap, torch, math
+import inspect
+import copy
+import textwrap
import torch.nn as nn
@@ -9,9 +11,11 @@
from nnodely.support.jsonutils import merge
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
-ode_relation_name = 'NeuralODE'
+ode_relation_name = "NeuralODE"
+
class NeuralODE(NeuObj):
"""Neural ODE layer implementing continuous-depth models.
@@ -24,13 +28,15 @@ class NeuralODE(NeuObj):
"""
@enforce_types
- def __init__(self,
- func: ParamFun,
- dt: float,
- rtol: float = 1e-7,
- atol: float = 1e-9,
- method: str = 'dopri5') -> Stream:
- super().__init__('F'+ode_relation_name + str(NeuObj.count))
+ def __init__(
+ self,
+ func: ParamFun,
+ dt: float,
+ rtol: float = 1e-7,
+ atol: float = 1e-9,
+ method: str = "dopri5",
+ ) -> Stream:
+ super().__init__("F" + ode_relation_name + str(NeuObj.count))
self.func = func
self.dt = dt
@@ -38,21 +44,25 @@ def __init__(self,
self.atol = atol
self.method = method
- code = textwrap.dedent(inspect.getsource(func.param_fun)).replace('\"', '\'')
- code = 'def ' + f'{self.name}' + '(state, *weights):\n' + \
- ' from nnodely.support.odeint.adjoint import odeint_adjoint as odeint\n ' + \
- code.replace('\n', '\n ') + \
- '\n' + \
- f' ans = odeint(lambda t, y: {func.param_fun.__name__}(t, y, *weights), state, t=torch.tensor([0.0, {self.dt}]), rtol={self.rtol}, atol={self.atol}, method=\'{self.method}\', adjoint_params=list(weights))' + \
- f'\n return ans[-1]\n'
-
- self.json['Functions'][self.name] = {
- 'code' : code,
- 'name' : f'{self.name}',
- 'rtol' : rtol,
- 'atol' : atol,
- 'method' : method,
- 'dt' : dt
+ code = textwrap.dedent(inspect.getsource(func.param_fun)).replace('"', "'")
+ code = (
+ "def "
+ + f"{self.name}"
+ + "(state, *weights):\n"
+ + " from nnodely.support.odeint.adjoint import odeint_adjoint as odeint\n "
+ + code.replace("\n", "\n ")
+ + "\n"
+ + f" ans = odeint(lambda t, y: {func.param_fun.__name__}(t, y, *weights), state, t=torch.tensor([0.0, {self.dt}]), rtol={self.rtol}, atol={self.atol}, method='{self.method}', adjoint_params=list(weights))"
+ + "\n return ans[-1]\n"
+ )
+
+ self.json["Functions"][self.name] = {
+ "code": code,
+ "name": f"{self.name}",
+ "rtol": rtol,
+ "atol": atol,
+ "method": method,
+ "dt": dt,
}
self.json_stream = {}
@@ -63,28 +73,34 @@ def __call__(self, *obj: Stream) -> Stream:
input_names = []
for ind, o in enumerate(obj):
o = toStream(o)
- check(type(o) is Stream, TypeError,
- f"The type of {o} is {type(o)} and is not supported for ParamFun operation.")
+ check(
+ type(o) is Stream,
+ TypeError,
+ f"The type of {o} is {type(o)} and is not supported for ParamFun operation.",
+ )
stream_json = merge(stream_json, o.json)
input_names.append(o.name)
- stream_json['Relations'][stream_name] = [ode_relation_name, input_names, self.name]
+ stream_json["Relations"][stream_name] = [
+ ode_relation_name,
+ input_names,
+ self.name,
+ ]
return Stream(stream_name, stream_json, obj[0].dim)
class ODE_Layer(nn.Module):
-
def __init__(self, func):
super().__init__()
- self.name = func['name']
- self.dt = func['dt']
- self.rtol = func['rtol']
- self.atol = func['atol']
- self.method = func['method']
+ self.name = func["name"]
+ self.dt = func["dt"]
+ self.rtol = func["rtol"]
+ self.atol = func["atol"]
+ self.method = func["method"]
## Add the function to the globals
try:
- code = 'import torch\n@torch.fx.wrap\n' + func['code']
- #print(f"Defining ODE function:\n{code}")
+ code = "import torch\n@torch.fx.wrap\n" + func["code"]
+ # print(f"Defining ODE function:\n{code}")
exec(code, globals())
except Exception as e:
print(f"An error occurred: {e}")
@@ -95,10 +111,12 @@ def forward(self, *inputs):
weights = list(inputs)[1:]
return function_to_call(list(inputs)[0], *weights) # Return the last state
+
def createODE(self, *func_params):
# for key, value in func_params[0].items():
# print(f"{key}: {value}")
-
+
return ODE_Layer(func_params[0])
-setattr(Model, ode_relation_name, createODE)
\ No newline at end of file
+
+setattr(Model, ode_relation_name, createODE)
diff --git a/nnodely/layers/output.py b/src/nnodely/layers/output.py
similarity index 82%
rename from nnodely/layers/output.py
rename to src/nnodely/layers/output.py
index f80f3670..9a28295a 100644
--- a/nnodely/layers/output.py
+++ b/src/nnodely/layers/output.py
@@ -5,8 +5,10 @@
from nnodely.support.jsonutils import stream_to_str
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.INFO)
+
class Output(NeuObj):
"""
Represents an output in the neural network model. This relation is what the network will give as output during inference.
@@ -27,8 +29,9 @@ class Output(NeuObj):
dim : dict
A dictionary containing the dimensions of the output.
"""
+
@enforce_types
- def __init__(self, name:str, relation:Stream):
+ def __init__(self, name: str, relation: Stream):
"""
Initializes the Output object.
@@ -41,12 +44,12 @@ def __init__(self, name:str, relation:Stream):
"""
super().__init__(name, relation.json, relation.dim)
log.debug(f"Output {name}")
- self.json['Outputs'][name] = {}
- self.json['Outputs'][name] = relation.name
- log.debug("\n"+pformat(self.json))
+ self.json["Outputs"][name] = {}
+ self.json["Outputs"][name] = relation.name
+ log.debug("\n" + pformat(self.json))
def __str__(self):
- return stream_to_str(self, 'Output')
+ return stream_to_str(self, "Output")
def __repr__(self):
- return self.__str__()
\ No newline at end of file
+ return self.__str__()
diff --git a/nnodely/layers/parameter.py b/src/nnodely/layers/parameter.py
similarity index 54%
rename from nnodely/layers/parameter.py
rename to src/nnodely/layers/parameter.py
index c3bc129f..af99160d 100644
--- a/nnodely/layers/parameter.py
+++ b/src/nnodely/layers/parameter.py
@@ -1,4 +1,6 @@
-import copy, inspect, textwrap
+import copy
+import inspect
+import textwrap
import numpy as np
from collections.abc import Callable
@@ -10,6 +12,7 @@
def is_numpy_float(var):
return isinstance(var, (np.float16, np.float32, np.float64))
+
class Constant(NeuObj, Relation):
"""
Represents a constant value in the neural network model.
@@ -39,11 +42,16 @@ class Constant(NeuObj, Relation):
.. include:: /examples_basics/parameter_module_ex/constant.rst
"""
+
@enforce_types
- def __init__(self, name:str,
- values:list|float|int|np.ndarray, *,
- tw:float|int|None = None,
- sw:int|None = None):
+ def __init__(
+ self,
+ name: str,
+ values: list | float | int | np.ndarray,
+ *,
+ tw: float | int | None = None,
+ sw: int | None = None,
+ ):
NeuObj.__init__(self, name)
values = np.array(values, dtype=NP_DTYPE)
@@ -52,26 +60,45 @@ def __init__(self, name:str,
self.dim = {}
if tw is not None:
- check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if tw is set.")
+ check(
+ len(shape) >= 2,
+ ValueError,
+ "The dimension must be at least 2 if tw is set.",
+ )
check(sw is None, ValueError, "If tw is set sw must be None")
dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:])
- self.dim['tw'] = tw
+ self.dim["tw"] = tw
elif sw is not None:
- check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if sw is set.")
- self.dim['sw'] = sw
- check(shape[0] == self.dim['sw'],ValueError, f"The sw = {sw} is different from sw = {shape[0]} of the values.")
+ check(
+ len(shape) >= 2,
+ ValueError,
+ "The dimension must be at least 2 if sw is set.",
+ )
+ self.dim["sw"] = sw
+ check(
+ shape[0] == self.dim["sw"],
+ ValueError,
+ f"The sw = {sw} is different from sw = {shape[0]} of the values.",
+ )
dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:])
else:
- dimensions = 1 if len(shape[0:]) == 0 else shape[0] if len(shape[0:]) == 1 else list(shape[0:])
+ dimensions = (
+ 1
+ if len(shape[0:]) == 0
+ else shape[0]
+ if len(shape[0:]) == 1
+ else list(shape[0:])
+ )
- self.dim['dim'] = dimensions
+ self.dim["dim"] = dimensions
# deepcopy dimention information inside Parameters
- self.json['Constants'][self.name] = copy.deepcopy(self.dim)
- if type(values) in (float,int):
- self.json['Constants'][self.name]['values'] = [values]
+ self.json["Constants"][self.name] = copy.deepcopy(self.dim)
+ if type(values) in (float, int):
+ self.json["Constants"][self.name]["values"] = [values]
else:
- self.json['Constants'][self.name]['values'] = values
+ self.json["Constants"][self.name]["values"] = values
+
class Parameter(NeuObj, Relation):
"""
@@ -113,29 +140,34 @@ class Parameter(NeuObj, Relation):
.. include:: /examples_basics/parameter_module_ex/parameter.rst
"""
+
@enforce_types
- def __init__(self, name:str,
- dimensions:int|list|tuple|None = None, *,
- tw:float|int|None = None,
- sw:int|None = None,
- values:list|float|int|np.ndarray|None = None,
- init:Callable|str|None = None,
- init_params:dict|None = None):
+ def __init__(
+ self,
+ name: str,
+ dimensions: int | list | tuple | None = None,
+ *,
+ tw: float | int | None = None,
+ sw: int | None = None,
+ values: list | float | int | np.ndarray | None = None,
+ init: Callable | str | None = None,
+ init_params: dict | None = None,
+ ):
NeuObj.__init__(self, name)
dimensions = list(dimensions) if type(dimensions) is tuple else dimensions
if values is None:
if dimensions is None:
dimensions = 1
- self.dim = {'dim': dimensions}
+ self.dim = {"dim": dimensions}
if tw is not None:
check(sw is None, ValueError, "If tw is set sw must be None")
- self.dim['tw'] = tw
+ self.dim["tw"] = tw
elif sw is not None:
- self.dim['sw'] = sw
+ self.dim["sw"] = sw
# deepcopy dimention information inside Parameters
- self.json['Parameters'][self.name] = copy.deepcopy(self.dim)
+ self.json["Parameters"][self.name] = copy.deepcopy(self.dim)
else:
values = np.array(values, dtype=NP_DTYPE)
shape = values.shape
@@ -143,41 +175,68 @@ def __init__(self, name:str,
self.dim = {}
if tw is not None:
- check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if tw is set.")
+ check(
+ len(shape) >= 2,
+ ValueError,
+ "The dimension must be at least 2 if tw is set.",
+ )
check(sw is None, ValueError, "If tw is set sw must be None")
dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:])
- self.dim['tw'] = tw
+ self.dim["tw"] = tw
elif sw is not None:
- check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if sw is set.")
- self.dim['sw'] = sw
- check(shape[0] == self.dim['sw'], ValueError,
- f"The sw = {sw} is different from sw = {shape[0]} of the values.")
+ check(
+ len(shape) >= 2,
+ ValueError,
+ "The dimension must be at least 2 if sw is set.",
+ )
+ self.dim["sw"] = sw
+ check(
+ shape[0] == self.dim["sw"],
+ ValueError,
+ f"The sw = {sw} is different from sw = {shape[0]} of the values.",
+ )
dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:])
else:
- dimensions = 1 if len(shape[0:]) == 0 else shape[0] if len(shape[0:]) == 1 else list(shape[0:])
+ dimensions = (
+ 1
+ if len(shape[0:]) == 0
+ else shape[0]
+ if len(shape[0:]) == 1
+ else list(shape[0:])
+ )
- self.dim['dim'] = dimensions
+ self.dim["dim"] = dimensions
# deepcopy dimention information inside Parameters
- self.json['Parameters'][self.name] = copy.deepcopy(self.dim)
+ self.json["Parameters"][self.name] = copy.deepcopy(self.dim)
if type(values) in (int, float):
- self.json['Parameters'][self.name]['init_values'] = [values]
+ self.json["Parameters"][self.name]["init_values"] = [values]
else:
- self.json['Parameters'][self.name]['init_values'] = values
- self.json['Parameters'][self.name]['values'] = self.json['Parameters'][self.name]['init_values']
+ self.json["Parameters"][self.name]["init_values"] = values
+ self.json["Parameters"][self.name]["values"] = self.json["Parameters"][
+ self.name
+ ]["init_values"]
if init is not None:
- check('values' not in self.json['Parameters'][self.name], ValueError, f"The parameter {self.name} is already initialized.")
- #check(inspect.isfunction(init), ValueError,f"The init parameter must be a function.")
+ check(
+ "values" not in self.json["Parameters"][self.name],
+ ValueError,
+ f"The parameter {self.name} is already initialized.",
+ )
+ # check(inspect.isfunction(init), ValueError,f"The init parameter must be a function.")
if inspect.isfunction(init):
- code = textwrap.dedent(inspect.getsource(init)).replace('\"', '\'')
- self.json['Parameters'][self.name]['init_fun'] = { 'code' : code, 'name' : init.__name__}
+ code = textwrap.dedent(inspect.getsource(init)).replace('"', "'")
+ self.json["Parameters"][self.name]["init_fun"] = {
+ "code": code,
+ "name": init.__name__,
+ }
elif type(init) is str:
- self.json['Parameters'][self.name]['init_fun'] = { 'name' : init }
+ self.json["Parameters"][self.name]["init_fun"] = {"name": init}
if init_params is not None:
- self.json['Parameters'][self.name]['init_fun']['params'] = init_params
-
-class SampleTime():
+ self.json["Parameters"][self.name]["init_fun"]["params"] = init_params
+
+
+class SampleTime:
"""
Represents a constant value that is equal to the sample time.
@@ -194,10 +253,14 @@ class SampleTime():
-------
.. include:: /examples_basics/parameter_module_ex/sample_time.rst
"""
- name = 'SampleTime'
+
+ name = "SampleTime"
g = Constant(name, values=0)
+
def __new__(cls):
- SampleTime.g.dim = {'dim': 1}
- SampleTime.g.json['Constants'][SampleTime.name] = copy.deepcopy(SampleTime.g.dim)
- SampleTime.g.json['Constants'][SampleTime.name]['values'] = SampleTime.name
+ SampleTime.g.dim = {"dim": 1}
+ SampleTime.g.json["Constants"][SampleTime.name] = copy.deepcopy(
+ SampleTime.g.dim
+ )
+ SampleTime.g.json["Constants"][SampleTime.name]["values"] = SampleTime.name
return SampleTime.g
diff --git a/nnodely/layers/parametricfunction.py b/src/nnodely/layers/parametricfunction.py
similarity index 55%
rename from nnodely/layers/parametricfunction.py
rename to src/nnodely/layers/parametricfunction.py
index 8cc6d714..75335361 100644
--- a/nnodely/layers/parametricfunction.py
+++ b/src/nnodely/layers/parametricfunction.py
@@ -1,4 +1,8 @@
-import inspect, copy, textwrap, torch, math
+import inspect
+import copy
+import textwrap
+import torch
+import math
import torch.nn as nn
import numpy as np
@@ -13,10 +17,12 @@
from nnodely.support.jsonutils import merge
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
-paramfun_relation_name = 'ParamFun'
+paramfun_relation_name = "ParamFun"
+
class ParamFun(NeuObj):
"""
@@ -56,13 +62,18 @@ class ParamFun(NeuObj):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/paramfun.rst
"""
+
@enforce_types
- def __init__(self, param_fun:Callable,
- parameters_and_constants:list|dict|None = None, *,
- map_over_batch:bool = False) -> Stream:
+ def __init__(
+ self,
+ param_fun: Callable,
+ parameters_and_constants: list | dict | None = None,
+ *,
+ map_over_batch: bool = False,
+ ) -> Stream:
self.relation_name = paramfun_relation_name
@@ -72,13 +83,13 @@ def __init__(self, param_fun:Callable,
self.map_over_batch = map_over_batch
self.output_dimension = {}
- super().__init__('F'+paramfun_relation_name + str(NeuObj.count))
- code = textwrap.dedent(inspect.getsource(param_fun)).replace('\"', '\'')
- self.json['Functions'][self.name] = {
- 'code' : code,
- 'name' : param_fun.__name__,
+ super().__init__("F" + paramfun_relation_name + str(NeuObj.count))
+ code = textwrap.dedent(inspect.getsource(param_fun)).replace('"', "'")
+ self.json["Functions"][self.name] = {
+ "code": code,
+ "name": param_fun.__name__,
}
- self.json['Functions'][self.name]['params_and_consts'] = []
+ self.json["Functions"][self.name]["params_and_consts"] = []
funinfo = inspect.getfullargspec(self.param_fun)
@@ -86,7 +97,9 @@ def __init__(self, param_fun:Callable,
if type(self.parameters_and_constants) is list:
n_pc = len(self.parameters_and_constants)
n_input = len(funinfo.args)
- for pc, pc_name in zip(self.parameters_and_constants,funinfo.args[n_input-n_pc:]):
+ for pc, pc_name in zip(
+ self.parameters_and_constants, funinfo.args[n_input - n_pc :]
+ ):
self.__create_parameter(pc, pc_name)
# Create the parameters and constants from list
@@ -99,13 +112,17 @@ def __init__(self, param_fun:Callable,
self.__create_parameter(pc, key)
elif first == True:
p = Parameter(name=self.name + key, dimensions=1)
- self.json['Functions'][self.name]['params_and_consts'].append(p.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(
+ p.name
+ )
self.json = merge(self.json, p.json)
self.json_stream = {}
@enforce_types
- def __call__(self, *obj:Union[Stream|Parameter|Constant|float|int]) -> Stream:
+ def __call__(
+ self, *obj: Union[Stream | Parameter | Constant | float | int]
+ ) -> Stream:
stream_name = paramfun_relation_name + str(Stream.count)
funinfo = inspect.getfullargspec(self.param_fun)
@@ -121,78 +138,124 @@ def __call__(self, *obj:Union[Stream|Parameter|Constant|float|int]) -> Stream:
else:
obj_type = type(o)
o = toStream(o)
- check(type(o) is Stream, TypeError,
- f"The type of {o} is {type(o)} and is not supported for ParamFun operation.")
+ check(
+ type(o) is Stream,
+ TypeError,
+ f"The type of {o} is {type(o)} and is not supported for ParamFun operation.",
+ )
input_types.append(obj_type)
input_dimensions.append(o.dim)
if n_call_input not in self.json_stream:
if len(self.json_stream) > 0:
- log.warning(f"The function {self.name} was called with a different number of inputs. If both functions enter in the model an error will be raised.")
+ log.warning(
+ f"The function {self.name} was called with a different number of inputs. If both functions enter in the model an error will be raised."
+ )
self.json_stream[n_call_input] = copy.deepcopy(self.json)
- self.json_stream[n_call_input]['Functions'][self.name]['n_input'] = n_call_input
+ self.json_stream[n_call_input]["Functions"][self.name]["n_input"] = (
+ n_call_input
+ )
# Create the missing parameters
- n_created_parameters = len(self.json_stream[n_call_input]['Functions'][self.name]['params_and_consts'])
+ n_created_parameters = len(
+ self.json_stream[n_call_input]["Functions"][self.name][
+ "params_and_consts"
+ ]
+ )
n_missing_parameters = n_parameters - n_created_parameters
- check(n_missing_parameters >= 0, ValueError, f"The function is called with too many parameter and inputs.")
- self.__create_missing_parameters(self.json_stream[n_call_input], n_call_input, n_missing_parameters)
-
- self.json_stream[n_call_input]['Functions'][self.name]['in_dim'] = copy.deepcopy(input_dimensions)
- self.json_stream[n_call_input]['Functions'][self.name]['map_over_dim'] = self.__infer_map_over_batch(input_types, n_parameters)
- output_dimension = self.__infer_output_dimensions(self.json_stream[n_call_input], input_types, input_dimensions)
+ check(
+ n_missing_parameters >= 0,
+ ValueError,
+ "The function is called with too many parameter and inputs.",
+ )
+ self.__create_missing_parameters(
+ self.json_stream[n_call_input], n_call_input, n_missing_parameters
+ )
+
+ self.json_stream[n_call_input]["Functions"][self.name]["in_dim"] = (
+ copy.deepcopy(input_dimensions)
+ )
+ self.json_stream[n_call_input]["Functions"][self.name]["map_over_dim"] = (
+ self.__infer_map_over_batch(input_types, n_parameters)
+ )
+ output_dimension = self.__infer_output_dimensions(
+ self.json_stream[n_call_input], input_types, input_dimensions
+ )
else:
map_over_batch = self.__infer_map_over_batch(input_types, n_parameters)
- check(map_over_batch == self.json_stream[n_call_input]['Functions'][self.name]['map_over_dim'], ValueError, f"The function {self.name} was called with different type of input using map_over_batch=True.")
- output_dimension = self.__infer_output_dimensions(self.json_stream[n_call_input], input_types, input_dimensions)
+ check(
+ map_over_batch
+ == self.json_stream[n_call_input]["Functions"][self.name][
+ "map_over_dim"
+ ],
+ ValueError,
+ f"The function {self.name} was called with different type of input using map_over_batch=True.",
+ )
+ output_dimension = self.__infer_output_dimensions(
+ self.json_stream[n_call_input], input_types, input_dimensions
+ )
# Save the all the input dimension used for call the parametric function
- in_dim = self.json_stream[n_call_input]['Functions'][self.name]['in_dim']
+ in_dim = self.json_stream[n_call_input]["Functions"][self.name]["in_dim"]
if type(in_dim[0]) is dict:
if in_dim != input_dimensions:
in_dim = [in_dim, input_dimensions]
- log.warning(f"The function {self.name} was called with inputs with different dimensions.")
+ log.warning(
+ f"The function {self.name} was called with inputs with different dimensions."
+ )
elif input_dimensions not in in_dim:
in_dim.append(input_dimensions)
- log.warning(f"The function {self.name} was called with inputs with different dimensions.")
- self.json_stream[n_call_input]['Functions'][self.name]['in_dim'] = in_dim
+ log.warning(
+ f"The function {self.name} was called with inputs with different dimensions."
+ )
+ self.json_stream[n_call_input]["Functions"][self.name]["in_dim"] = in_dim
stream_json = copy.deepcopy(self.json_stream[n_call_input])
input_names = []
for ind, o in enumerate(obj):
o = toStream(o)
- check(type(o) is Stream, TypeError,
- f"The type of {o} is {type(o)} and is not supported for ParamFun operation.")
+ check(
+ type(o) is Stream,
+ TypeError,
+ f"The type of {o} is {type(o)} and is not supported for ParamFun operation.",
+ )
stream_json = merge(stream_json, o.json)
input_names.append(o.name)
- stream_json['Relations'][stream_name] = [paramfun_relation_name, input_names, self.name]
+ stream_json["Relations"][stream_name] = [
+ paramfun_relation_name,
+ input_names,
+ self.name,
+ ]
return Stream(stream_name, stream_json, output_dimension)
def __create_parameter(self, pc, pc_name):
if type(pc) is Parameter:
- self.json['Functions'][self.name]['params_and_consts'].append(pc.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(pc.name)
self.json = merge(self.json, pc.json)
elif type(pc) is str:
# TODO to remove! there is no reason to give a name to the parameter. The name of the parameter is the name of the function parameter
p = Parameter(name=pc, dimensions=1)
- self.json['Functions'][self.name]['params_and_consts'].append(p.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(p.name)
self.json = merge(self.json, p.json)
elif type(pc) is tuple:
p = Parameter(name=self.name + pc_name, dimensions=list(pc))
- self.json['Functions'][self.name]['params_and_consts'].append(p.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(p.name)
self.json = merge(self.json, p.json)
elif type(pc) is Constant:
- self.json['Functions'][self.name]['params_and_consts'].append(pc.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(pc.name)
self.json = merge(self.json, pc.json)
elif type(pc) in (float, int, list):
c = Constant(name=self.name + pc_name, values=pc)
- self.json['Functions'][self.name]['params_and_consts'].append(c.name)
+ self.json["Functions"][self.name]["params_and_consts"].append(c.name)
self.json = merge(self.json, c.json)
else:
- check(type(pc) in (Parameter, str, tuple, Constant, float, int, list), TypeError,
- f'The element inside the \"parameters_and_constants\" list or dict must be a Parameter, str, tuple to build a Parameter or Constant, int, float or list to build a Constant but was {type(pc)}.')
+ check(
+ type(pc) in (Parameter, str, tuple, Constant, float, int, list),
+ TypeError,
+ f'The element inside the "parameters_and_constants" list or dict must be a Parameter, str, tuple to build a Parameter or Constant, int, float or list to build a Constant but was {type(pc)}.',
+ )
def __infer_map_over_batch(self, input_types, n_constants_and_params):
input_map_dim = ()
@@ -211,21 +274,24 @@ def __infer_map_over_batch(self, input_types, n_constants_and_params):
else:
return False
- def __create_missing_parameters(self, stream_json, n_call_input, n_missing_parameters):
+ def __create_missing_parameters(
+ self, stream_json, n_call_input, n_missing_parameters
+ ):
funinfo = inspect.getfullargspec(self.param_fun)
for i in range(n_missing_parameters):
- p_name = self.name + funinfo.args[n_call_input+i]
- stream_json['Functions'][self.name]['params_and_consts'].insert(i, p_name)
- stream_json['Parameters'][p_name] = {'dim': 1}
+ p_name = self.name + funinfo.args[n_call_input + i]
+ stream_json["Functions"][self.name]["params_and_consts"].insert(i, p_name)
+ stream_json["Parameters"][p_name] = {"dim": 1}
def __infer_output_dimensions(self, stream_json, input_types, input_dimensions):
import torch
+
batch_dim = 5
all_inputs_dim = copy.deepcopy(input_dimensions)
all_inputs_type = copy.deepcopy(input_types)
- params_and_consts = stream_json['Constants'] | stream_json['Parameters']
- for name in stream_json['Functions'][self.name]['params_and_consts']:
+ params_and_consts = stream_json["Constants"] | stream_json["Parameters"]
+ for name in stream_json["Functions"][self.name]["params_and_consts"]:
all_inputs_dim.append(params_and_consts[name])
all_inputs_type.append(Constant)
@@ -233,7 +299,11 @@ def __infer_output_dimensions(self, stream_json, input_types, input_dimensions):
is_int = False
while is_int == False:
n_samples_sec *= 10
- vect_input_time = [math.isclose(d['tw']*n_samples_sec,round(d['tw']*n_samples_sec)) for d in all_inputs_dim if 'tw' in d]
+ vect_input_time = [
+ math.isclose(d["tw"] * n_samples_sec, round(d["tw"] * n_samples_sec))
+ for d in all_inputs_dim
+ if "tw" in d
+ ]
if len(vect_input_time) == 0:
is_int = True
else:
@@ -244,67 +314,96 @@ def __infer_output_dimensions(self, stream_json, input_types, input_dimensions):
inputs_win_type = []
inputs_win = []
- for t, dim in zip(all_inputs_type,all_inputs_dim):
- window = 'tw' if 'tw' in dim else ('sw' if 'sw' in dim else None)
- if window == 'tw':
+ for t, dim in zip(all_inputs_type, all_inputs_dim):
+ window = "tw" if "tw" in dim else ("sw" if "sw" in dim else None)
+ if window == "tw":
dim_win = round(dim[window] * n_samples_sec)
- elif window == 'sw':
+ elif window == "sw":
dim_win = dim[window]
else:
dim_win = None if t in (Parameter, Constant) else 1
if t in (Parameter, Constant):
- if type(dim['dim']) is list:
+ if type(dim["dim"]) is list:
if dim_win is not None:
- inputs.append(torch.rand(size=(dim_win,) + tuple(dim['dim'])))
+ inputs.append(torch.rand(size=(dim_win,) + tuple(dim["dim"])))
else:
- inputs.append(torch.rand(size=tuple(dim['dim'])))
+ inputs.append(torch.rand(size=tuple(dim["dim"])))
else:
if dim_win is not None:
- inputs.append(torch.rand(size=(dim_win, dim['dim'])))
+ inputs.append(torch.rand(size=(dim_win, dim["dim"])))
else:
- inputs.append(torch.rand(size=(dim['dim'],)))
+ inputs.append(torch.rand(size=(dim["dim"],)))
else:
- inputs.append(torch.rand(size=(batch_dim, dim_win, dim['dim'])))
+ inputs.append(torch.rand(size=(batch_dim, dim_win, dim["dim"])))
inputs_win_type.append(window)
inputs_win.append(dim_win)
if self.map_over_batch:
- function_to_call = torch.func.vmap(self.param_fun,in_dims=tuple(stream_json['Functions'][self.name]['map_over_dim']))
+ function_to_call = torch.func.vmap(
+ self.param_fun,
+ in_dims=tuple(stream_json["Functions"][self.name]["map_over_dim"]),
+ )
else:
function_to_call = self.param_fun
out = function_to_call(*inputs)
out_shape = out.shape
- check(out_shape[0] == batch_dim, ValueError, "The batch output dimension it is not correct.")
+ check(
+ out_shape[0] == batch_dim,
+ ValueError,
+ "The batch output dimension it is not correct.",
+ )
out_dim = list(out_shape[2:])
- check(len(out_dim) == 1, ValueError, "The output dimension of the function is bigger than a vector.")
- out_win_type = 'sw'
+ check(
+ len(out_dim) == 1,
+ ValueError,
+ "The output dimension of the function is bigger than a vector.",
+ )
+ out_win_type = "sw"
out_win = out_shape[1]
for idx, win in enumerate(inputs_win):
- if out_shape[1] == win and all_inputs_type[idx] not in (Parameter, Constant):
+ if out_shape[1] == win and all_inputs_type[idx] not in (
+ Parameter,
+ Constant,
+ ):
out_win_type = inputs_win_type[idx]
out_win = all_inputs_dim[idx][out_win_type]
- return { 'dim': out_dim[0], out_win_type : out_win }
+ return {"dim": out_dim[0], out_win_type: out_win}
-def return_standard_inputs(json, model_def, xlim = None, num_points = 1000):
- check(json['n_input'] == 1 or json['n_input'] == 2, ValueError, "The function must have only one or two inputs.")
+
+def return_standard_inputs(json, model_def, xlim=None, num_points=1000):
+ check(
+ json["n_input"] == 1 or json["n_input"] == 2,
+ ValueError,
+ "The function must have only one or two inputs.",
+ )
fun_inputs = tuple()
- for i in range(json['n_input']):
- dim = json['in_dim'][i]
- check(dim['dim'] == 1, ValueError, "The input dimension must be 1.")
- if 'tw' in dim:
- check(dim['tw'] == model_def['Info']['SampleTime'], ValueError, f"The input window must be 1 but was {dim['tw']}.")
- elif 'sw' in dim:
- check(dim['sw'] == 1, ValueError, "The input window must be 1.")
+ for i in range(json["n_input"]):
+ dim = json["in_dim"][i]
+ check(dim["dim"] == 1, ValueError, "The input dimension must be 1.")
+ if "tw" in dim:
+ check(
+ dim["tw"] == model_def["Info"]["SampleTime"],
+ ValueError,
+ f"The input window must be 1 but was {dim['tw']}.",
+ )
+ elif "sw" in dim:
+ check(dim["sw"] == 1, ValueError, "The input window must be 1.")
if xlim is not None:
- if json['n_input'] == 2:
- check(np.array(xlim).shape == (json['n_input'], 2), ValueError,
- "The xlim must have the same shape as the number of inputs.")
+ if json["n_input"] == 2:
+ check(
+ np.array(xlim).shape == (json["n_input"], 2),
+ ValueError,
+ "The xlim must have the same shape as the number of inputs.",
+ )
x_value = np.linspace(xlim[i][0], xlim[i][1], num=num_points)
else:
- check(np.array(xlim).shape == (2,), ValueError,
- "The xlim must have the same shape as the number of inputs.")
+ check(
+ np.array(xlim).shape == (2,),
+ ValueError,
+ "The xlim must have the same shape as the number of inputs.",
+ )
x_value = np.linspace(xlim[0], xlim[1], num=num_points)
else:
x_value = np.linspace(0, 1, num=num_points)
@@ -313,34 +412,45 @@ def return_standard_inputs(json, model_def, xlim = None, num_points = 1000):
else:
x1_value = torch.from_numpy(x_value)
- if json['n_input'] == 2:
- x0_value, x1_value = torch.meshgrid(x0_value,x1_value,indexing="xy")
+ if json["n_input"] == 2:
+ x0_value, x1_value = torch.meshgrid(x0_value, x1_value, indexing="xy")
x0_value = x0_value.flatten().unsqueeze(1).unsqueeze(1)
x1_value = x1_value.flatten().unsqueeze(1).unsqueeze(1)
- fun_inputs += (x0_value,x1_value,)
+ fun_inputs += (
+ x0_value,
+ x1_value,
+ )
else:
x0_value = x0_value.unsqueeze(1).unsqueeze(1)
fun_inputs += (x0_value,)
- for key in json['params_and_consts']:
- val = model_def['Parameters'][key] if key in model_def['Parameters'] else model_def['Constants'][key]
- fun_inputs += tuple([torch.from_numpy(np.array(val['values']))]) # The vector is transform in a tuple
+ for key in json["params_and_consts"]:
+ val = (
+ model_def["Parameters"][key]
+ if key in model_def["Parameters"]
+ else model_def["Constants"][key]
+ )
+ fun_inputs += tuple(
+ [torch.from_numpy(np.array(val["values"]))]
+ ) # The vector is transform in a tuple
return fun_inputs
+
def return_function(json, fun_inputs):
- exec(json['code'], globals())
- function_to_call = globals()[json['name']]
+ exec(json["code"], globals())
+ function_to_call = globals()[json["name"]]
output = function_to_call(*fun_inputs)
check(output.shape[1] == 1, ValueError, "The output dimension must be 1.")
check(output.shape[2] == 1, ValueError, "The output window must be 1.")
funinfo = inspect.getfullargspec(function_to_call)
return output, funinfo.args
+
class Parametric_Layer(nn.Module):
def __init__(self, func, params_and_consts, map_over_batch):
super().__init__()
- self.name = func['name']
+ self.name = func["name"]
self.params_and_consts = params_and_consts
if type(map_over_batch) is list:
self.map_over_batch = True
@@ -349,7 +459,7 @@ def __init__(self, func, params_and_consts, map_over_batch):
self.map_over_batch = False
## Add the function to the globals
try:
- code = 'import torch\n@torch.fx.wrap\n' + func['code']
+ code = "import torch\n@torch.fx.wrap\n" + func["code"]
exec(code, globals())
except Exception as e:
print(f"An error occurred: {e}")
@@ -360,11 +470,19 @@ def forward(self, *inputs):
function_to_call = globals()[self.name]
# Call the function using the retrieved function object
if self.map_over_batch:
- function_to_call = torch.func.vmap(function_to_call,in_dims=self.input_map_dim)
+ function_to_call = torch.func.vmap(
+ function_to_call, in_dims=self.input_map_dim
+ )
result = function_to_call(*args)
return result
+
def createParamFun(self, *func_params):
- return Parametric_Layer(func=func_params[0], params_and_consts=func_params[1], map_over_batch=func_params[2])
+ return Parametric_Layer(
+ func=func_params[0],
+ params_and_consts=func_params[1],
+ map_over_batch=func_params[2],
+ )
+
setattr(Model, paramfun_relation_name, createParamFun)
diff --git a/nnodely/layers/part.py b/src/nnodely/layers/part.py
similarity index 55%
rename from nnodely/layers/part.py
rename to src/nnodely/layers/part.py
index b86bd4cd..b9ee48c2 100644
--- a/nnodely/layers/part.py
+++ b/src/nnodely/layers/part.py
@@ -8,17 +8,16 @@
from nnodely.support.utils import check, enforce_types
from nnodely.support.jsonutils import merge
-part_relation_name = 'Part'
-select_relation_name = 'Select'
-concatenate_relation_name = 'Concatenate'
+part_relation_name = "Part"
+select_relation_name = "Select"
+concatenate_relation_name = "Concatenate"
-timepart_relation_name = 'TimePart'
-timeselect_relation_name = 'TimeSelect'
-timeconcatenate_relation_name = 'TimeConcatenate'
-
-samplepart_relation_name = 'SamplePart'
-sampleselect_relation_name = 'SampleSelect'
+timepart_relation_name = "TimePart"
+timeselect_relation_name = "TimeSelect"
+timeconcatenate_relation_name = "TimeConcatenate"
+samplepart_relation_name = "SamplePart"
+sampleselect_relation_name = "SampleSelect"
class Part(Stream, ToStream):
@@ -50,7 +49,7 @@ class Part(Stream, ToStream):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/part_module/part.rst
Raises
@@ -58,17 +57,26 @@ class Part(Stream, ToStream):
IndexError
If the indices i and j are out of range.
"""
+
@enforce_types
- def __init__(self, obj:Stream, i:int, j:int):
+ def __init__(self, obj: Stream, i: int, j: int):
# check(type(obj) is Stream, TypeError,
# f"The type of {obj} is {type(obj)} and is not supported for Part operation.")
- check(i >= 0 and j > 0 and i < obj.dim['dim'] and j <= obj.dim['dim'],
- IndexError,
- f"i={i} or j={j} are not in the range [0,{obj.dim['dim']}]")
+ check(
+ i >= 0 and j > 0 and i < obj.dim["dim"] and j <= obj.dim["dim"],
+ IndexError,
+ f"i={i} or j={j} are not in the range [0,{obj.dim['dim']}]",
+ )
dim = copy.deepcopy(obj.dim)
- dim['dim'] = j - i
- super().__init__(part_relation_name + str(Stream.count),obj.json,dim)
- self.json['Relations'][self.name] = [part_relation_name,[obj.name],obj.dim['dim'],[i,j]]
+ dim["dim"] = j - i
+ super().__init__(part_relation_name + str(Stream.count), obj.json, dim)
+ self.json["Relations"][self.name] = [
+ part_relation_name,
+ [obj.name],
+ obj.dim["dim"],
+ [i, j],
+ ]
+
class Select(Stream, ToStream):
"""
@@ -97,67 +105,101 @@ class Select(Stream, ToStream):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/part_module/select.rst
-
+
Raises
------
IndexError
If the index i is out of range.
"""
+
@enforce_types
- def __init__(self, obj:Stream, i:int):
+ def __init__(self, obj: Stream, i: int):
# check(type(obj) is Stream, TypeError,
# f"The type of {obj} is {type(obj)} and is not supported for Select operation.")
- check(i >= 0 and i < obj.dim['dim'],
- IndexError,
- f"i={i} are not in the range [0,{obj.dim['dim']}]")
+ check(
+ i >= 0 and i < obj.dim["dim"],
+ IndexError,
+ f"i={i} are not in the range [0,{obj.dim['dim']}]",
+ )
dim = copy.deepcopy(obj.dim)
- dim['dim'] = 1
- super().__init__(select_relation_name + str(Stream.count),obj.json,dim)
- self.json['Relations'][self.name] = [select_relation_name,[obj.name],obj.dim['dim'],i]
+ dim["dim"] = 1
+ super().__init__(select_relation_name + str(Stream.count), obj.json, dim)
+ self.json["Relations"][self.name] = [
+ select_relation_name,
+ [obj.name],
+ obj.dim["dim"],
+ i,
+ ]
+
class Concatenate(Stream, ToStream):
"""
- Implement the concatenate function between two tensors.
+ Implement the concatenate function between two tensors.
- See also:
- Official PyTorch Cat documentation:
- `torch.cat `_
+ See also:
+ Official PyTorch Cat documentation:
+ `torch.cat `_
- :param input1: the first relation to concatenate
- :type obj: Tensor
- :param input2: the second relation to concatenate
- :type obj: Tensor
+ :param input1: the first relation to concatenate
+ :type obj: Tensor
+ :param input2: the second relation to concatenate
+ :type obj: Tensor
- Examples
- --------
- .. image:: https://colab.research.google.com/assets/colab-badge.svg
- :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb
- :alt: Open in Colab
+ Examples
+ --------
+ .. image:: https://colab.research.google.com/assets/colab-badge.svg
+ :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb
+ :alt: Open in Colab
- Example:
- >>> cat = Concatenate(relation1, relation2)
+ Example:
+ >>> cat = Concatenate(relation1, relation2)
"""
+
@enforce_types
- def __init__(self, obj1:Stream, obj2:Stream) -> Stream:
- obj1,obj2 = toStream(obj1),toStream(obj2)
- check(type(obj1) is Stream,TypeError,
- f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.")
- check(type(obj2) is Stream,TypeError,
- f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.")
- #check(obj1.dim == obj2.dim or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError,
+ def __init__(self, obj1: Stream, obj2: Stream) -> Stream:
+ obj1, obj2 = toStream(obj1), toStream(obj2)
+ check(
+ type(obj1) is Stream,
+ TypeError,
+ f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.",
+ )
+ check(
+ type(obj2) is Stream,
+ TypeError,
+ f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.",
+ )
+ # check(obj1.dim == obj2.dim or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError,
# f"For addition operators (+) the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.")
dim = copy.deepcopy(obj1.dim)
- dim['dim'] = obj1.dim['dim']+obj2.dim['dim']
- if 'tw' in obj1.dim.keys() and 'tw' in obj2.dim.keys():
- check(obj1.dim['tw'] == obj2.dim['tw'], ValueError, 'The time window of the two inputs must be the same')
- elif 'sw' in obj1.dim.keys() and 'sw' in obj2.dim.keys():
- check(obj1.dim['sw'] == obj2.dim['sw'], ValueError, 'The sample window of the two inputs must be the same')
+ dim["dim"] = obj1.dim["dim"] + obj2.dim["dim"]
+ if "tw" in obj1.dim.keys() and "tw" in obj2.dim.keys():
+ check(
+ obj1.dim["tw"] == obj2.dim["tw"],
+ ValueError,
+ "The time window of the two inputs must be the same",
+ )
+ elif "sw" in obj1.dim.keys() and "sw" in obj2.dim.keys():
+ check(
+ obj1.dim["sw"] == obj2.dim["sw"],
+ ValueError,
+ "The sample window of the two inputs must be the same",
+ )
else:
- raise(ValueError('The two inputs have different time or sample dimensions'))
- super().__init__(concatenate_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [concatenate_relation_name,[obj1.name,obj2.name]]
+ raise (
+ ValueError("The two inputs have different time or sample dimensions")
+ )
+ super().__init__(
+ concatenate_relation_name + str(Stream.count),
+ merge(obj1.json, obj2.json),
+ dim,
+ )
+ self.json["Relations"][self.name] = [
+ concatenate_relation_name,
+ [obj1.name, obj2.name],
+ ]
+
class SamplePart(Stream, ToStream):
"""
@@ -190,7 +232,7 @@ class SamplePart(Stream, ToStream):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/part_module/sample_part.rst
Raises
@@ -202,34 +244,48 @@ class SamplePart(Stream, ToStream):
IndexError
If the offset is not within the sample window.
"""
+
@enforce_types
- def __init__(self, obj:Stream, i:int, j:int, offset:int|None = None):
+ def __init__(self, obj: Stream, i: int, j: int, offset: int | None = None):
# check(type(obj) is Stream, TypeError,
# f"The type of {obj} is {type(obj)} and is not supported for SamplePart operation.")
- check('sw' in obj.dim, KeyError, 'Input must have a sample window')
- check(i < j, ValueError, 'i must be smaller than j')
- all_inputs = obj.json['Inputs']
+ check("sw" in obj.dim, KeyError, "Input must have a sample window")
+ check(i < j, ValueError, "i must be smaller than j")
+ all_inputs = obj.json["Inputs"]
if obj.name in all_inputs:
- backward_idx = all_inputs[obj.name]['sw'][0]
- forward_idx = all_inputs[obj.name]['sw'][1]
+ backward_idx = all_inputs[obj.name]["sw"][0]
+ forward_idx = all_inputs[obj.name]["sw"][1]
else:
backward_idx = 0
- forward_idx = obj.dim['sw']
- check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the sample window of the input')
- check(j > backward_idx and j <= forward_idx, ValueError, 'j must be in the sample window of the input')
+ forward_idx = obj.dim["sw"]
+ check(
+ i >= backward_idx and i < forward_idx,
+ ValueError,
+ "i must be in the sample window of the input",
+ )
+ check(
+ j > backward_idx and j <= forward_idx,
+ ValueError,
+ "j must be in the sample window of the input",
+ )
dim = copy.deepcopy(obj.dim)
- dim['sw'] = j - i
+ dim["sw"] = j - i
name = samplepart_relation_name + str(Stream.count)
- super().__init__(name,obj.json,dim)
+ super().__init__(name, obj.json, dim)
if obj.name in all_inputs:
- rel = [samplepart_relation_name,[obj.name],-1,[i,j]]
+ rel = [samplepart_relation_name, [obj.name], -1, [i, j]]
else:
- rel = [samplepart_relation_name,[obj.name],obj.dim['sw'],[i,j]]
- #rel = [samplepart_relation_name,[obj.name],[i,j]]
+ rel = [samplepart_relation_name, [obj.name], obj.dim["sw"], [i, j]]
+ # rel = [samplepart_relation_name,[obj.name],[i,j]]
if offset is not None:
- check(i <= offset < j, IndexError,"The offset must be inside the sample window")
+ check(
+ i <= offset < j,
+ IndexError,
+ "The offset must be inside the sample window",
+ )
rel.append(offset)
- self.json['Relations'][self.name] = rel
+ self.json["Relations"][self.name] = rel
+
class SampleSelect(Stream, ToStream):
"""
@@ -258,9 +314,9 @@ class SampleSelect(Stream, ToStream):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/part_module/sample_select.rst
-
+
Raises
------
IndexError
@@ -270,18 +326,29 @@ class SampleSelect(Stream, ToStream):
IndexError
If the offset is not within the sample window.
"""
+
@enforce_types
- def __init__(self, obj:Stream, i:int):
+ def __init__(self, obj: Stream, i: int):
# check(type(obj) is Stream, TypeError,
# f"The type of {obj} is {type(obj)} and is not supported for SampleSelect operation.")
- check('sw' in obj.dim, KeyError, 'Input must have a sample window')
+ check("sw" in obj.dim, KeyError, "Input must have a sample window")
backward_idx = 0
- forward_idx = obj.dim['sw']
- check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the sample window of the input')
+ forward_idx = obj.dim["sw"]
+ check(
+ i >= backward_idx and i < forward_idx,
+ ValueError,
+ "i must be in the sample window of the input",
+ )
dim = copy.deepcopy(obj.dim)
- dim['sw'] = 1
- super().__init__(sampleselect_relation_name + str(Stream.count),obj.json,dim)
- self.json['Relations'][self.name] = [sampleselect_relation_name,[obj.name],obj.dim['sw'],i]
+ dim["sw"] = 1
+ super().__init__(sampleselect_relation_name + str(Stream.count), obj.json, dim)
+ self.json["Relations"][self.name] = [
+ sampleselect_relation_name,
+ [obj.name],
+ obj.dim["sw"],
+ i,
+ ]
+
class TimePart(Stream, ToStream):
"""
@@ -309,7 +376,7 @@ class TimePart(Stream, ToStream):
Examples
--------
-
+
.. include:: /examples_basics/layer_module_ex/part_module/time_part.rst
Raises
@@ -321,79 +388,113 @@ class TimePart(Stream, ToStream):
IndexError
If the offset is not within the time window.
"""
+
@enforce_types
- def __init__(self, obj:Stream, i:int|float, j:int|float, offset:int|float|None = None):
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for TimePart operation.")
- check('tw' in obj.dim, KeyError, 'Input must have a time window')
- check(i < j, ValueError, 'i must be smaller than j')
- all_inputs = obj.json['Inputs']
+ def __init__(
+ self,
+ obj: Stream,
+ i: int | float,
+ j: int | float,
+ offset: int | float | None = None,
+ ):
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for TimePart operation.",
+ )
+ check("tw" in obj.dim, KeyError, "Input must have a time window")
+ check(i < j, ValueError, "i must be smaller than j")
+ all_inputs = obj.json["Inputs"]
if obj.name in all_inputs:
- backward_idx = all_inputs[obj.name]['tw'][0]
- forward_idx = all_inputs[obj.name]['tw'][1]
+ backward_idx = all_inputs[obj.name]["tw"][0]
+ forward_idx = all_inputs[obj.name]["tw"][1]
else:
backward_idx = 0
- forward_idx = obj.dim['tw']
- check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the time window of the input')
- check(j > backward_idx and j <= forward_idx, ValueError, 'j must be in the time window of the input')
+ forward_idx = obj.dim["tw"]
+ check(
+ i >= backward_idx and i < forward_idx,
+ ValueError,
+ "i must be in the time window of the input",
+ )
+ check(
+ j > backward_idx and j <= forward_idx,
+ ValueError,
+ "j must be in the time window of the input",
+ )
dim = copy.deepcopy(obj.dim)
- dim['tw'] = j - i
- super().__init__(timepart_relation_name + str(Stream.count),obj.json,dim)
+ dim["tw"] = j - i
+ super().__init__(timepart_relation_name + str(Stream.count), obj.json, dim)
if obj.name in all_inputs:
- rel = [timepart_relation_name,[obj.name],-1,[i,j]]
+ rel = [timepart_relation_name, [obj.name], -1, [i, j]]
else:
- rel = [timepart_relation_name,[obj.name],obj.dim['tw'],[i,j]]
- #rel = [timepart_relation_name,[obj.name],[i,j]]
+ rel = [timepart_relation_name, [obj.name], obj.dim["tw"], [i, j]]
+ # rel = [timepart_relation_name,[obj.name],[i,j]]
if offset is not None:
- check(i <= offset < j, IndexError,"The offset must be inside the time window")
+ check(
+ i <= offset < j, IndexError, "The offset must be inside the time window"
+ )
rel.append(offset)
- self.json['Relations'][self.name] = rel
+ self.json["Relations"][self.name] = rel
+
class TimeConcatenate(Stream, ToStream):
"""
- Implement the concatenate function between two tensors along the time dimension (second dimension).
+ Implement the concatenate function between two tensors along the time dimension (second dimension).
- See also:
- Official PyTorch Cat documentation:
- `torch.cat `_
+ See also:
+ Official PyTorch Cat documentation:
+ `torch.cat `_
- :param input1: the first relation to concatenate
- :type obj: Tensor
- :param input2: the second relation to concatenate
- :type obj: Tensor
+ :param input1: the first relation to concatenate
+ :type obj: Tensor
+ :param input2: the second relation to concatenate
+ :type obj: Tensor
- Examples
- --------
- .. image:: https://colab.research.google.com/assets/colab-badge.svg
- :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb
- :alt: Open in Colab
+ Examples
+ --------
+ .. image:: https://colab.research.google.com/assets/colab-badge.svg
+ :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb
+ :alt: Open in Colab
- Example:
- >>> cat = TimeConcatenate(relation1, relation2)
+ Example:
+ >>> cat = TimeConcatenate(relation1, relation2)
"""
+
@enforce_types
- def __init__(self, obj1:Stream, obj2:Stream) -> Stream:
- obj1,obj2 = toStream(obj1),toStream(obj2)
- check(type(obj1) is Stream,TypeError,
- f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.")
- check(type(obj2) is Stream,TypeError,
- f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.")
-
- #check('tw' in obj1.dim, KeyError, 'Input1 must have a time window')
- #check('tw' in obj2.dim, KeyError, 'Input2 must have a time window')
+ def __init__(self, obj1: Stream, obj2: Stream) -> Stream:
+ obj1, obj2 = toStream(obj1), toStream(obj2)
+ check(
+ type(obj1) is Stream,
+ TypeError,
+ f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.",
+ )
+ check(
+ type(obj2) is Stream,
+ TypeError,
+ f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.",
+ )
+
+ # check('tw' in obj1.dim, KeyError, 'Input1 must have a time window')
+ # check('tw' in obj2.dim, KeyError, 'Input2 must have a time window')
dim = copy.deepcopy(obj1.dim)
- if 'tw' in obj1.dim and 'tw' in obj2.dim:
- dim['tw'] = obj1.dim['tw'] + obj2.dim['tw']
- elif 'sw' in obj1.dim and 'sw' in obj2.dim:
- dim['sw'] = obj1.dim['sw'] + obj2.dim['sw']
- super().__init__(timeconcatenate_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim)
- self.json['Relations'][self.name] = [timeconcatenate_relation_name,[obj1.name,obj2.name]]
-
+ if "tw" in obj1.dim and "tw" in obj2.dim:
+ dim["tw"] = obj1.dim["tw"] + obj2.dim["tw"]
+ elif "sw" in obj1.dim and "sw" in obj2.dim:
+ dim["sw"] = obj1.dim["sw"] + obj2.dim["sw"]
+ super().__init__(
+ timeconcatenate_relation_name + str(Stream.count),
+ merge(obj1.json, obj2.json),
+ dim,
+ )
+ self.json["Relations"][self.name] = [
+ timeconcatenate_relation_name,
+ [obj1.name, obj2.name],
+ ]
class Part_Layer(nn.Module):
#: :noindex:
- def __init__(self, dim:int, i:int, j:int):
+ def __init__(self, dim: int, i: int, j: int):
super(Part_Layer, self).__init__()
self.i, self.j = i, j
@@ -404,12 +505,14 @@ def __init__(self, dim:int, i:int, j:int):
def forward(self, x):
## assert x.ndim >= 3, 'The Part Relation Works only for 3D inputs'
- return torch.einsum('bij,kj->bik', x, self.W)
+ return torch.einsum("bij,kj->bik", x, self.W)
+
## Select elements on the third dimension in the range [i,j]
def createPart(self, *inputs):
return Part_Layer(dim=inputs[0], i=inputs[1][0], j=inputs[1][1])
+
class Select_Layer(nn.Module):
#: :noindex:
def __init__(self, dim, idx):
@@ -419,12 +522,14 @@ def __init__(self, dim, idx):
def forward(self, x):
## assert x.ndim >= 3, 'The Part Relation Works only for 3D inputs'
- return torch.einsum('ijk,k->ij', x, self.W).unsqueeze(2)
+ return torch.einsum("ijk,k->ij", x, self.W).unsqueeze(2)
+
## Select an element i on the third dimension
def createSelect(self, *inputs):
return Select_Layer(dim=inputs[0], idx=inputs[1])
+
class SamplePart_Layer(nn.Module):
#: :noindex:
def __init__(self, dim, part, offset):
@@ -440,9 +545,10 @@ def __init__(self, dim, part, offset):
def forward(self, x):
if self.offset is not None:
x = x - x[:, self.offset].unsqueeze(1)
- result = torch.einsum('bij,ki->bkj', x, self.W)
+ result = torch.einsum("bij,ki->bkj", x, self.W)
return result
+
class Concatenate_Layer(nn.Module):
#: :noindex:
def __init__(self):
@@ -451,16 +557,19 @@ def __init__(self):
def forward(self, *inputs):
return torch.cat((inputs[0], inputs[1]), dim=2)
+
def createConcatenate(name, *inputs):
#: :noindex:
return Concatenate_Layer()
+
def createSamplePart(self, *inputs):
if len(inputs) > 2: ## offset
return SamplePart_Layer(dim=inputs[0], part=inputs[1], offset=inputs[2])
else:
return SamplePart_Layer(dim=inputs[0], part=inputs[1], offset=None)
+
class SampleSelect_Layer(nn.Module):
#: :noindex:
def __init__(self, dim, idx):
@@ -469,11 +578,13 @@ def __init__(self, dim, idx):
self.W[idx] = 1
def forward(self, x):
- return torch.einsum('ijk,j->ik', x, self.W).unsqueeze(1)
+ return torch.einsum("ijk,j->ik", x, self.W).unsqueeze(1)
+
def createSampleSelect(self, *inputs):
return SampleSelect_Layer(dim=inputs[0], idx=inputs[1])
+
class TimePart_Layer(nn.Module):
#: :noindex:
def __init__(self, dim, part, offset):
@@ -489,15 +600,17 @@ def __init__(self, dim, part, offset):
def forward(self, x):
if self.offset is not None:
x = x - x[:, self.offset].unsqueeze(1)
- result = torch.einsum('bij,ki->bkj', x, self.W)
+ result = torch.einsum("bij,ki->bkj", x, self.W)
return result
+
def createTimePart(self, *inputs):
- if len(inputs) > 2: ## offset
+ if len(inputs) > 2: ## offset
return TimePart_Layer(dim=inputs[0], part=inputs[1], offset=inputs[2])
else:
return TimePart_Layer(dim=inputs[0], part=inputs[1], offset=None)
+
class TimeConcatenate_Layer(nn.Module):
#: :noindex:
def __init__(self):
@@ -506,10 +619,12 @@ def __init__(self):
def forward(self, *inputs):
return torch.cat((inputs[0], inputs[1]), dim=1)
+
def createTimeConcatenate(name, *inputs):
#: :noindex:
return TimeConcatenate_Layer()
+
setattr(Model, part_relation_name, createPart)
setattr(Model, select_relation_name, createSelect)
setattr(Model, concatenate_relation_name, createConcatenate)
diff --git a/nnodely/layers/rungekutta.py b/src/nnodely/layers/rungekutta.py
similarity index 74%
rename from nnodely/layers/rungekutta.py
rename to src/nnodely/layers/rungekutta.py
index 35255cfc..bc1281b4 100644
--- a/nnodely/layers/rungekutta.py
+++ b/src/nnodely/layers/rungekutta.py
@@ -1,19 +1,13 @@
-import torch.nn as nn
-import torch
-
from nnodely.layers.parametricfunction import ParamFun
from nnodely.layers.parameter import SampleTime
-from nnodely.basic.relation import Stream, NeuObj, ToStream
-from nnodely.support.utils import enforce_types, check
-from nnodely.support.jsonutils import merge, subjson_from_relation
-from nnodely.basic.model import Model
-import textwrap, inspect
+from nnodely.basic.relation import Stream, NeuObj
+from nnodely.support.utils import enforce_types
from collections.abc import Callable
-fe_relation_name = 'ForwardEuler'
-rk2_relation_name = 'RK2'
-rk4_relation_name = 'RK4'
+fe_relation_name = "ForwardEuler"
+rk2_relation_name = "RK2"
+rk4_relation_name = "RK4"
# class ForwardEuler(NeuObj):
# """
@@ -30,6 +24,7 @@
# 'name' : f.__name__,
# }
+
# @enforce_types
# def __call__(self, obj:Stream) -> Stream:
# stream_name = fe_relation_name + str(Stream.count)
@@ -42,55 +37,62 @@ class ForwardEuler(NeuObj):
"""
This operation perform Forward Euler Integration on a Stream
"""
+
@enforce_types
- def __init__(self, f:Callable|ParamFun) -> Stream:
- super().__init__('F' + fe_relation_name + str(NeuObj.count))
+ def __init__(self, f: Callable | ParamFun) -> Stream:
+ super().__init__("F" + fe_relation_name + str(NeuObj.count))
self.f = f if isinstance(f, ParamFun) else ParamFun(f)
self.dt = SampleTime()
+
@enforce_types
- def __call__(self, obj:Stream) -> Stream:
+ def __call__(self, obj: Stream) -> Stream:
return obj + self.dt * self.f(obj)
+
class RK2(NeuObj):
"""
This operation perform RK2 Integration on a Stream
"""
+
@enforce_types
- def __init__(self, f:Callable|ParamFun) -> Stream:
+ def __init__(self, f: Callable | ParamFun) -> Stream:
super().__init__(rk2_relation_name + str(NeuObj.count))
self.f = f if isinstance(f, ParamFun) else ParamFun(f)
- #self.fe = ForwardEuler(self.f)
+ # self.fe = ForwardEuler(self.f)
self.dt = SampleTime()
@enforce_types
- def __call__(self, obj:Stream) -> Stream:
+ def __call__(self, obj: Stream) -> Stream:
f1 = self.f(obj)
- f2 = self.f(obj + (self.dt/2) * f1)
+ f2 = self.f(obj + (self.dt / 2) * f1)
return obj + self.dt * f2
+
class RK4(NeuObj):
"""
This operation perform RK4 Integration on a Stream
"""
+
@enforce_types
- def __init__(self, f:Callable|ParamFun) -> Stream:
+ def __init__(self, f: Callable | ParamFun) -> Stream:
super().__init__(rk4_relation_name + str(NeuObj.count))
self.f = f if isinstance(f, ParamFun) else ParamFun(f)
self.dt = SampleTime()
@enforce_types
- def __call__(self, obj:Stream, t:Stream|None = None) -> Stream:
- if t: ## Partial differential equation
+ def __call__(self, obj: Stream, t: Stream | None = None) -> Stream:
+ if t: ## Partial differential equation
f1 = self.f(obj, t)
- f2 = self.f(obj + (self.dt/2) * f1, t + (self.dt/2))
- f3 = self.f(obj + (self.dt/2) * f2, t + (self.dt/2))
+ f2 = self.f(obj + (self.dt / 2) * f1, t + (self.dt / 2))
+ f3 = self.f(obj + (self.dt / 2) * f2, t + (self.dt / 2))
f4 = self.f(obj + self.dt * f3, t + self.dt)
- else: ## Ordinary differential equation
+ else: ## Ordinary differential equation
f1 = self.f(obj)
- f2 = self.f(obj + (self.dt/2) * f1)
- f3 = self.f(obj + (self.dt/2) * f2)
+ f2 = self.f(obj + (self.dt / 2) * f1)
+ f3 = self.f(obj + (self.dt / 2) * f2)
f4 = self.f(obj + self.dt * f3)
- return obj + (self.dt/6) * (f1 + 2*f2 + 2*f3 + f4)
+ return obj + (self.dt / 6) * (f1 + 2 * f2 + 2 * f3 + f4)
+
# class ForwardEuler_Layer(nn.Module):
# #: :noindex:
@@ -108,7 +110,7 @@ def __call__(self, obj:Stream, t:Stream|None = None) -> Stream:
# func = globals()[self.name]
# print(f'ForwardEuler executing function name {self.name} which is {func}')
# return x + self.dt * func(x)
-
+
# def createForwardEuler(name, *inputs):
# #: :noindex:
# return ForwardEuler_Layer(inputs[0])
diff --git a/nnodely/layers/timeoperation.py b/src/nnodely/layers/timeoperation.py
similarity index 53%
rename from nnodely/layers/timeoperation.py
rename to src/nnodely/layers/timeoperation.py
index d85172db..ac985f43 100644
--- a/nnodely/layers/timeoperation.py
+++ b/src/nnodely/layers/timeoperation.py
@@ -7,14 +7,12 @@
from nnodely.basic.model import Model
from nnodely.support.fixstepsolver import Euler, Trapezoidal
-SOLVERS = {
- 'euler': Euler,
- 'trapezoidal': Trapezoidal
-}
+SOLVERS = {"euler": Euler, "trapezoidal": Trapezoidal}
# Binary operators
-int_relation_name = 'Integrate'
-der_relation_name = 'Differentiate'
+int_relation_name = "Integrate"
+der_relation_name = "Differentiate"
+
class Integrate(Stream, ToStream):
"""
@@ -24,18 +22,28 @@ class Integrate(Stream, ToStream):
----------
method : is the integration method
"""
+
@enforce_types
- def __init__(self, output:Stream, *,
- int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> Stream:
+ def __init__(
+ self,
+ output: Stream,
+ *,
+ int_name: str | None = None,
+ der_name: str | None = None,
+ method: str = "euler",
+ ) -> Stream:
if int_name is None:
int_name = output.name + "_int" + str(NeuObj.count)
if der_name is None:
der_name = output.name + "_der" + str(NeuObj.count)
- check(method in SOLVERS, ValueError, f"The method '{method}' is not supported yet")
- solver = SOLVERS[method](int_name,der_name)
+ check(
+ method in SOLVERS, ValueError, f"The method '{method}' is not supported yet"
+ )
+ solver = SOLVERS[method](int_name, der_name)
output_int = solver.integrate(output)
super().__init__(output_int.name, output_int.json, output_int.dim)
+
class Differentiate(Stream, ToStream):
"""
This operation Differentiate a Stream with respect to time or another Stream
@@ -44,25 +52,44 @@ class Differentiate(Stream, ToStream):
----------
method : is the derivative method
"""
+
@enforce_types
- def __init__(self, output:Stream, input:Stream = None, *,
- int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> Stream:
+ def __init__(
+ self,
+ output: Stream,
+ input: Stream = None,
+ *,
+ int_name: str | None = None,
+ der_name: str | None = None,
+ method: str = "euler",
+ ) -> Stream:
if input is None:
if int_name is None:
int_name = output.name + "_int" + str(NeuObj.count)
if der_name is None:
der_name = output.name + "_der" + str(NeuObj.count)
- check(method in SOLVERS, ValueError, f"The method '{method}' is not supported yet")
- solver = SOLVERS[method](int_name,der_name)
+ check(
+ method in SOLVERS,
+ ValueError,
+ f"The method '{method}' is not supported yet",
+ )
+ solver = SOLVERS[method](int_name, der_name)
output_der = solver.derivate(output)
super().__init__(output_der.name, output_der.json, output_der.dim)
else:
- super().__init__(der_relation_name + str(Stream.count), merge(output.json,input.json), input.dim)
- self.json['Relations'][self.name] = [der_relation_name, [output.name, input.name]]
+ super().__init__(
+ der_relation_name + str(Stream.count),
+ merge(output.json, input.json),
+ input.dim,
+ )
+ self.json["Relations"][self.name] = [
+ der_relation_name,
+ [output.name, input.name],
+ ]
subjson = subjson_from_relation(self.json, input.name)
- grad_inputs = subjson['Inputs'].keys()
+ grad_inputs = subjson["Inputs"].keys()
for i in grad_inputs:
- self.json['Inputs'][i]['type'] = 'derivate'
+ self.json["Inputs"][i]["type"] = "derivate"
class Differentiate_Layer(nn.Module):
@@ -71,10 +98,19 @@ def __init__(self):
super(Differentiate_Layer, self).__init__()
def forward(self, *inputs):
- return torch.autograd.grad(inputs[0], inputs[1], grad_outputs=torch.ones_like(inputs[0]), create_graph=True, retain_graph=True, allow_unused=False)[0]
+ return torch.autograd.grad(
+ inputs[0],
+ inputs[1],
+ grad_outputs=torch.ones_like(inputs[0]),
+ create_graph=True,
+ retain_graph=True,
+ allow_unused=False,
+ )[0]
+
def createAdd(name, *inputs):
#: :noindex:
return Differentiate_Layer()
+
setattr(Model, der_relation_name, createAdd)
diff --git a/nnodely/layers/trigonometric.py b/src/nnodely/layers/trigonometric.py
similarity index 54%
rename from nnodely/layers/trigonometric.py
rename to src/nnodely/layers/trigonometric.py
index 932cabad..9ebba516 100644
--- a/nnodely/layers/trigonometric.py
+++ b/src/nnodely/layers/trigonometric.py
@@ -6,19 +6,20 @@
from nnodely.support.utils import check, enforce_types
from nnodely.layers.parameter import Parameter, Constant
-sin_relation_name = 'Sin'
-cos_relation_name = 'Cos'
-tan_relation_name = 'Tan'
-tanh_relation_name = 'Tanh'
-cosh_relation_name = 'Cosh'
-sech_relation_name = 'Sech'
+sin_relation_name = "Sin"
+cos_relation_name = "Cos"
+tan_relation_name = "Tan"
+tanh_relation_name = "Tanh"
+cosh_relation_name = "Cosh"
+sech_relation_name = "Sech"
+
class Sin(Stream, ToStream):
"""
Implement the sine function given an input relation.
See also:
- Official PyTorch Sin documentation:
+ Official PyTorch Sin documentation:
`torch.sin `_
:param obj: the input relation stream
@@ -28,20 +29,25 @@ class Sin(Stream, ToStream):
--------
.. include:: /examples_basics/layer_module_ex/trig_module_ex/sin.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Sin operation.")
- super().__init__(sin_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [sin_relation_name, [obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Sin operation.",
+ )
+ super().__init__(sin_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [sin_relation_name, [obj.name]]
+
class Cos(Stream, ToStream):
"""
Implement the cosine function given an input relation.
See also:
- Official PyTorch Cos documentation:
+ Official PyTorch Cos documentation:
`torch.cos `_
:param obj: the input relation stream
@@ -51,20 +57,25 @@ class Cos(Stream, ToStream):
--------
.. include:: /examples_basics/layer_module_ex/trig_module_ex/cos.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Cos operation.")
- super().__init__(cos_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [cos_relation_name, [obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Cos operation.",
+ )
+ super().__init__(cos_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [cos_relation_name, [obj.name]]
+
class Tan(Stream, ToStream):
"""
Implement the tangent function given an input relation.
See also:
- Official PyTorch Tan documentation:
+ Official PyTorch Tan documentation:
`torch.tan `_
:param obj: the input relation stream
@@ -74,20 +85,25 @@ class Tan(Stream, ToStream):
--------
.. include:: /examples_basics/layer_module_ex/trig_module_ex/tan.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Tan operation.")
- super().__init__(tan_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [tan_relation_name, [obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Tan operation.",
+ )
+ super().__init__(tan_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [tan_relation_name, [obj.name]]
+
class Cosh(Stream, ToStream):
"""
Returns a new tensor with the hyperbolic cosine of the elements of input.
See also:
- Official PyTorch Cosh documentation:
+ Official PyTorch Cosh documentation:
`torch.cosh `_
:param obj: the input relation stream
@@ -97,12 +113,17 @@ class Cosh(Stream, ToStream):
--------
.. include:: /examples_basics/layer_module_ex/trig_module_ex/cosh.rst
"""
- def __init__(self, obj:Stream) -> Stream:
+
+ def __init__(self, obj: Stream) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Cosh operation.")
- super().__init__(cosh_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [cosh_relation_name, [obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Cosh operation.",
+ )
+ super().__init__(cosh_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [cosh_relation_name, [obj.name]]
+
class Sech(Stream, ToStream):
"""
@@ -115,99 +136,140 @@ class Sech(Stream, ToStream):
--------
.. include:: /examples_basics/layer_module_ex/trig_module_ex/sech.rst
"""
- def __init__(self, obj:Stream) -> Stream:
+
+ def __init__(self, obj: Stream) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream, TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Sech operation.")
- super().__init__(sech_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [sech_relation_name, [obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Sech operation.",
+ )
+ super().__init__(sech_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [sech_relation_name, [obj.name]]
+
class Tanh(Stream, ToStream):
"""
- Implement the Hyperbolic Tangent (Tanh) relation function.
+ Implement the Hyperbolic Tangent (Tanh) relation function.
- See also:
- Official PyTorch tanh documentation:
- `torch.nn.Tanh `_
+ See also:
+ Official PyTorch tanh documentation:
+ `torch.nn.Tanh `_
- :param obj: The relation stream.
- :type obj: Stream
+ :param obj: The relation stream.
+ :type obj: Stream
- Example:
- --------
- .. include:: /examples_basics/layer_module_ex/trig_module_ex/tanh.rst
+ Example:
+ --------
+ .. include:: /examples_basics/layer_module_ex/trig_module_ex/tanh.rst
"""
+
@enforce_types
- def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream:
+ def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream:
obj = toStream(obj)
- check(type(obj) is Stream,TypeError,
- f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.")
- super().__init__(tanh_relation_name + str(Stream.count),obj.json,obj.dim)
- self.json['Relations'][self.name] = [tanh_relation_name,[obj.name]]
+ check(
+ type(obj) is Stream,
+ TypeError,
+ f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.",
+ )
+ super().__init__(tanh_relation_name + str(Stream.count), obj.json, obj.dim)
+ self.json["Relations"][self.name] = [tanh_relation_name, [obj.name]]
+
class Sin_Layer(nn.Module):
- def __init__(self,):
+ def __init__(
+ self,
+ ):
super(Sin_Layer, self).__init__()
+
def forward(self, x):
return torch.sin(x)
+
def createSin(self, *inputs):
return Sin_Layer()
+
class Cos_Layer(nn.Module):
- def __init__(self,):
+ def __init__(
+ self,
+ ):
super(Cos_Layer, self).__init__()
+
def forward(self, x):
return torch.cos(x)
+
def createCos(self, *inputs):
return Cos_Layer()
+
class Tan_Layer(nn.Module):
- def __init__(self,):
+ def __init__(
+ self,
+ ):
super(Tan_Layer, self).__init__()
+
def forward(self, x):
return torch.tan(x)
+
def createTan(self, *inputs):
return Tan_Layer()
+
class Cosh_Layer(nn.Module):
- def __init__(self,):
+ def __init__(
+ self,
+ ):
super(Cosh_Layer, self).__init__()
+
def forward(self, x):
return torch.cosh(x)
+
def createCosh(self, *inputs):
return Cosh_Layer()
+
class Tanh_Layer(nn.Module):
"""
- :noindex:
+ :noindex:
"""
- def __init__(self,):
+
+ def __init__(
+ self,
+ ):
super(Tanh_Layer, self).__init__()
+
def forward(self, x):
return torch.tanh(x)
+
def createTanh(self, *input):
"""
- :noindex:
+ :noindex:
"""
return Tanh_Layer()
+
class Sech_Layer(nn.Module):
- def __init__(self,):
+ def __init__(
+ self,
+ ):
super(Sech_Layer, self).__init__()
+
def forward(self, x):
- return 1/torch.cosh(x)
+ return 1 / torch.cosh(x)
+
def createSech(self, *inputs):
return Sech_Layer()
+
setattr(Model, sin_relation_name, createSin)
setattr(Model, cos_relation_name, createCos)
setattr(Model, tan_relation_name, createTan)
setattr(Model, cosh_relation_name, createCosh)
setattr(Model, tanh_relation_name, createTanh)
-setattr(Model, sech_relation_name, createSech)
\ No newline at end of file
+setattr(Model, sech_relation_name, createSech)
diff --git a/nnodely/nnodely.py b/src/nnodely/nnodely.py
similarity index 67%
rename from nnodely/nnodely.py
rename to src/nnodely/nnodely.py
index 083f2e4c..417089e7 100644
--- a/nnodely/nnodely.py
+++ b/src/nnodely/nnodely.py
@@ -1,5 +1,7 @@
# Extern packages
-import random, torch, copy
+import random
+import torch
+import copy
import numpy as np
# Main operators
@@ -16,13 +18,15 @@
from nnodely.support.utils import ReadOnlyDict, ParamDict, enforce_types, check
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.INFO)
@enforce_types
-def clearNames(names:str|list|None = None):
+def clearNames(names: str | list | None = None):
NeuObj.clearNames(names)
+
class Modely(Composer, Trainer, Loader, Validator, Exporter):
"""
Create the main object, the nnodely object, that will be used to create the network, train and export it.
@@ -46,21 +50,25 @@ class Modely(Composer, Trainer, Loader, Validator, Exporter):
-------
>>> model = Modely()
"""
+
@enforce_types
- def __init__(self, *,
- visualizer:str|EmptyVisualizer|None = 'Standard',
- exporter:str|EmptyExporter|None = 'Standard',
- seed:int|None = None,
- workspace:str|None = None,
- log_internal:bool = False,
- save_history:bool = False):
+ def __init__(
+ self,
+ *,
+ visualizer: str | EmptyVisualizer | None = "Standard",
+ exporter: str | EmptyExporter | None = "Standard",
+ seed: int | None = None,
+ workspace: str | None = None,
+ log_internal: bool = False,
+ save_history: bool = False,
+ ):
## Set the random seed for reproducibility
if seed is not None:
self.resetSeed(seed)
# Visualizer
- if visualizer == 'Standard':
+ if visualizer == "Standard":
self.visualizer = TextVisualizer(1)
elif visualizer != None:
self.visualizer = visualizer
@@ -87,7 +95,9 @@ def neuralized(self):
@neuralized.setter
def neuralized(self, value):
- raise AttributeError("Cannot modify read-only property 'neuralized' use neuralizeModel() instead.")
+ raise AttributeError(
+ "Cannot modify read-only property 'neuralized' use neuralizeModel() instead."
+ )
@property
def traced(self):
@@ -100,24 +110,31 @@ def traced(self, value):
@property
def parameters(self):
if self._neuralized:
- return ParamDict(self._model_def['Parameters'], self._model.all_parameters)
+ return ParamDict(self._model_def["Parameters"], self._model.all_parameters)
else:
- return ParamDict(self._model_def['Parameters'])
+ return ParamDict(self._model_def["Parameters"])
@property
def constants(self):
- return ReadOnlyDict({key:value.detach().numpy().tolist() for key,value in self._model.all_constants})
+ return ReadOnlyDict(
+ {
+ key: value.detach().numpy().tolist()
+ for key, value in self._model.all_constants
+ }
+ )
@property
def states(self):
- return {key:value.detach().numpy().tolist() for key,value in self._states.items()}
+ return {
+ key: value.detach().numpy().tolist() for key, value in self._states.items()
+ }
@property
def json(self):
return copy.deepcopy(self._model_def._ModelDef__json)
@enforce_types
- def resetSeed(self, seed:int) -> None:
+ def resetSeed(self, seed: int) -> None:
"""
Resets the random seed for reproducibility.
@@ -135,7 +152,13 @@ def resetSeed(self, seed:int) -> None:
random.seed(seed) ## set the random module seed
np.random.seed(seed) ## set the numpy seed
- def trainAndAnalyze(self, *, test_dataset: str | list | dict | None = None, test_batch_size: int = 128, **kwargs):
+ def trainAndAnalyze(
+ self,
+ *,
+ test_dataset: str | list | dict | None = None,
+ test_batch_size: int = 128,
+ **kwargs,
+ ):
"""
Trains the model using the provided datasets and parameters. After training, it analyzes the results on the training, validation, and test datasets.
@@ -212,38 +235,83 @@ def trainAndAnalyze(self, *, test_dataset: str | list | dict | None = None, test
self.trainModel(**kwargs)
params = self.running_parameters
- minimize_gain = params['minimize_gain']
- closed_loop, connect, prediction_samples = params['closed_loop'], params['connect'], params['prediction_samples']
-
- if kwargs.get('train_dataset', None) is None:
- check(test_dataset is None, ValueError, 'If train_dataset is None, test_dataset must also be None.')
+ minimize_gain = params["minimize_gain"]
+ closed_loop, connect, prediction_samples = (
+ params["closed_loop"],
+ params["connect"],
+ params["prediction_samples"],
+ )
+
+ if kwargs.get("train_dataset", None) is None:
+ check(
+ test_dataset is None,
+ ValueError,
+ "If train_dataset is None, test_dataset must also be None.",
+ )
else:
- params['test_tag'] = self._get_tag(test_dataset)
- params['XY_test'] = self._get_data(test_dataset)
- params['n_samples_test'] = next(iter(params['XY_test'].values())).size(0) if params['XY_test'] else 0
- params['test_indexes'] = self._get_batch_indexes(test_dataset, params['n_samples_test'], prediction_samples)
+ params["test_tag"] = self._get_tag(test_dataset)
+ params["XY_test"] = self._get_data(test_dataset)
+ params["n_samples_test"] = (
+ next(iter(params["XY_test"].values())).size(0)
+ if params["XY_test"]
+ else 0
+ )
+ params["test_indexes"] = self._get_batch_indexes(
+ test_dataset, params["n_samples_test"], prediction_samples
+ )
## Training set Results
- self._analyze(params['XY_train'], dataset_tag=params['train_tag'], indexes=params['train_indexes'], minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=params['train_step'], batch_size=params['train_batch_size'])
-
+ self._analyze(
+ params["XY_train"],
+ dataset_tag=params["train_tag"],
+ indexes=params["train_indexes"],
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=params["train_step"],
+ batch_size=params["train_batch_size"],
+ )
+
## Validation set Results
- if params['n_samples_val'] > 0:
- self._analyze(params['XY_val'], dataset_tag=params['val_tag'], indexes=params['val_indexes'], minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=params['val_step'], batch_size=params['val_batch_size'])
+ if params["n_samples_val"] > 0:
+ self._analyze(
+ params["XY_val"],
+ dataset_tag=params["val_tag"],
+ indexes=params["val_indexes"],
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=params["val_step"],
+ batch_size=params["val_batch_size"],
+ )
else:
- log.warning("Validation dataset is empty. Skipping validation results analysis.")
+ log.warning(
+ "Validation dataset is empty. Skipping validation results analysis."
+ )
## Test set Results
- if params['n_samples_test'] > 0:
- params['test_batch_size'] = self._clip_batch_size(len(params['test_indexes']), test_batch_size)
- params['test_step'] = self._clip_step(params['step'], params['test_indexes'], params['test_batch_size'])
- self._analyze(params['XY_test'], dataset_tag=params['test_tag'], indexes=params['test_indexes'], minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=params['test_step'], batch_size=test_batch_size)
+ if params["n_samples_test"] > 0:
+ params["test_batch_size"] = self._clip_batch_size(
+ len(params["test_indexes"]), test_batch_size
+ )
+ params["test_step"] = self._clip_step(
+ params["step"], params["test_indexes"], params["test_batch_size"]
+ )
+ self._analyze(
+ params["XY_test"],
+ dataset_tag=params["test_tag"],
+ indexes=params["test_indexes"],
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=params["test_step"],
+ batch_size=test_batch_size,
+ )
else:
log.warning("Test dataset is empty. Skipping test results analysis.")
-nnodely = Modely
\ No newline at end of file
+
+nnodely = Modely
diff --git a/nnodely/operators/__init__.py b/src/nnodely/operators/__init__.py
similarity index 100%
rename from nnodely/operators/__init__.py
rename to src/nnodely/operators/__init__.py
diff --git a/nnodely/operators/composer.py b/src/nnodely/operators/composer.py
similarity index 58%
rename from nnodely/operators/composer.py
rename to src/nnodely/operators/composer.py
index 563777f6..b8d1e61c 100644
--- a/nnodely/operators/composer.py
+++ b/src/nnodely/operators/composer.py
@@ -1,4 +1,5 @@
-import copy, torch
+import copy
+import torch
import numpy as np
@@ -6,29 +7,44 @@
from nnodely.basic.modeldef import ModelDef
from nnodely.basic.model import Model
-from nnodely.support.utils import check, TORCH_DTYPE, NP_DTYPE, enforce_types, tensor_to_list
+from nnodely.support.utils import (
+ check,
+ TORCH_DTYPE,
+ NP_DTYPE,
+ enforce_types,
+ tensor_to_list,
+)
from nnodely.support.mathutils import argmax_dict, argmin_dict
from nnodely.basic.relation import Stream
from nnodely.layers.input import Input
from nnodely.layers.output import Output
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
+
class Composer(Network):
@enforce_types
def __init__(self):
- check(type(self) is not Composer, TypeError, "Composer class cannot be instantiated directly")
+ check(
+ type(self) is not Composer,
+ TypeError,
+ "Composer class cannot be instantiated directly",
+ )
super().__init__()
def __addInfo(self) -> None:
- total_params = sum(p.numel() for p in self._model.parameters() if p.requires_grad)
- self._model_def['Info']['num_parameters'] = total_params
+ total_params = sum(
+ p.numel() for p in self._model.parameters() if p.requires_grad
+ )
+ self._model_def["Info"]["num_parameters"] = total_params
from nnodely import __version__
- self._model_def['Info']['nnodely_version'] = __version__
+
+ self._model_def["Info"]["nnodely_version"] = __version__
@enforce_types
- def addModel(self, name:str, stream_list:list|Output) -> None:
+ def addModel(self, name: str, stream_list: list | Output) -> None:
"""
Adds a new model with the given name along with a list of Outputs.
@@ -47,7 +63,7 @@ def addModel(self, name:str, stream_list:list|Output) -> None:
self._neuralized = False
@enforce_types
- def removeModel(self, name_list:list|str) -> None:
+ def removeModel(self, name_list: list | str) -> None:
"""
Removes models with the given list of names.
@@ -64,7 +80,13 @@ def removeModel(self, name_list:list|str) -> None:
self._neuralized = False
@enforce_types
- def addConnect(self, stream_out:str|Output|Stream, input_in:str|Input, *, local:bool=False) -> None:
+ def addConnect(
+ self,
+ stream_out: str | Output | Stream,
+ input_in: str | Input,
+ *,
+ local: bool = False,
+ ) -> None:
"""
Adds a connection from a relation stream to an input.
@@ -80,11 +102,17 @@ def addConnect(self, stream_out:str|Output|Stream, input_in:str|Input, *, local:
.. include:: /examples_basics/compser_module_ex/addConnect.rst
"""
- self._model_def.addConnection(stream_out, input_in,'connect', local)
+ self._model_def.addConnection(stream_out, input_in, "connect", local)
self._neuralized = False
@enforce_types
- def addClosedLoop(self, stream_out:str|Output|Stream, input_in:str|Input, *, local:bool=False) -> None:
+ def addClosedLoop(
+ self,
+ stream_out: str | Output | Stream,
+ input_in: str | Input,
+ *,
+ local: bool = False,
+ ) -> None:
"""
Adds a closed loop connection from a relation stream to an input.
@@ -100,11 +128,11 @@ def addClosedLoop(self, stream_out:str|Output|Stream, input_in:str|Input, *, loc
.. include:: /examples_basics/compser_module_ex/addClosedLoop.rst
"""
- self._model_def.addConnection(stream_out, input_in,'closedLoop', local)
+ self._model_def.addConnection(stream_out, input_in, "closedLoop", local)
self._neuralized = False
@enforce_types
- def removeConnection(self, input_in:str|Input) -> None:
+ def removeConnection(self, input_in: str | Input) -> None:
"""
Remove a closed loop or connect connection from an input.
@@ -126,7 +154,13 @@ def removeConnection(self, input_in:str|Input) -> None:
self._neuralized = False
@enforce_types
- def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool = False, model_def:dict|None = None) -> None:
+ def neuralizeModel(
+ self,
+ sample_time: float | int | None = None,
+ *,
+ clear_model: bool = False,
+ model_def: dict | None = None,
+ ) -> None:
"""
Neuralizes the model, preparing it for inference and training. This method creates a neural network model starting from the model definition.
It will also create all the time windows and correct slicing for all the inputs defined.
@@ -151,29 +185,45 @@ def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool
.. include:: /examples_basics/compser_module_ex/neuralizeModel.rst
"""
if model_def is not None:
- check(sample_time == None, ValueError, 'The sample_time must be None if a model_def is provided')
- check(clear_model == False, ValueError, 'The clear_model must be False if a model_def is provided')
+ check(
+ sample_time == None,
+ ValueError,
+ "The sample_time must be None if a model_def is provided",
+ )
+ check(
+ clear_model == False,
+ ValueError,
+ "The clear_model must be False if a model_def is provided",
+ )
self._model_def = ModelDef(model_def)
else:
- self._model_def.updateParameters(model = None, clear_model = clear_model)
+ self._model_def.updateParameters(model=None, clear_model=clear_model)
self._model_def.setBuildWindow(sample_time)
self._model = Model(self._model_def.getJson())
self.__addInfo()
- self._input_ns_backward = {key:value['ns'][0] for key, value in self._model_def['Inputs'].items()}
- self._input_ns_forward = {key:value['ns'][1] for key, value in self._model_def['Inputs'].items()}
+ self._input_ns_backward = {
+ key: value["ns"][0] for key, value in self._model_def["Inputs"].items()
+ }
+ self._input_ns_forward = {
+ key: value["ns"][1] for key, value in self._model_def["Inputs"].items()
+ }
self._max_samples_backward = max(self._input_ns_backward.values())
self._max_samples_forward = max(self._input_ns_forward.values())
self._input_n_samples = {}
- for key, value in self._model_def['Inputs'].items():
+ for key, value in self._model_def["Inputs"].items():
if self._input_ns_forward[key] >= 0:
- if 'closedLoop' in value:
+ if "closedLoop" in value:
log.warning(f"Closed loop on {key} with sample in the future.")
- if 'connect' in value:
+ if "connect" in value:
log.warning(f"Connect on {key} with sample in the future.")
- self._input_n_samples[key] = self._input_ns_backward[key] + self._input_ns_forward[key]
- self._max_n_samples = max(self._input_ns_backward.values()) + max(self._input_ns_forward.values())
+ self._input_n_samples[key] = (
+ self._input_ns_backward[key] + self._input_ns_forward[key]
+ )
+ self._max_n_samples = max(self._input_ns_backward.values()) + max(
+ self._input_ns_forward.values()
+ )
## Initialize States
self.resetStates()
@@ -186,7 +236,17 @@ def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool
self.visualizer.showBuiltModel()
@enforce_types
- def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, connect:dict={}, prediction_samples:str|int='auto', num_of_samples:int|None=None, log_internal:bool=False) -> dict:
+ def __call__(
+ self,
+ inputs: dict = {},
+ *,
+ sampled: bool = False,
+ closed_loop: dict = {},
+ connect: dict = {},
+ prediction_samples: str | int = "auto",
+ num_of_samples: int | None = None,
+ log_internal: bool = False,
+ ) -> dict:
"""
Performs inference on the model.
@@ -219,99 +279,149 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c
Examples
--------
-
+
.. include:: /examples_basics/inference_module_ex/inference.rst
"""
## Copy dict for avoid python bug
inputs = copy.deepcopy(inputs)
- all_closed_loop = copy.deepcopy(closed_loop) #| self._model_def._input_closed_loop
- all_connect = copy.deepcopy(connect) #| self._model_def._input_connect
+ all_closed_loop = copy.deepcopy(
+ closed_loop
+ ) # | self._model_def._input_closed_loop
+ all_connect = copy.deepcopy(connect) # | self._model_def._input_connect
## Check neuralize
check(self.neuralized, RuntimeError, "The network is not neuralized.")
## Check closed loop integrity
- prediction_samples = self._setup_recurrent_variables(prediction_samples, all_closed_loop, all_connect)
+ prediction_samples = self._setup_recurrent_variables(
+ prediction_samples, all_closed_loop, all_connect
+ )
## List of keys
- model_inputs = list(self._model_def['Inputs'].keys())
- json_inputs = self._model_def['Inputs']
+ model_inputs = list(self._model_def["Inputs"].keys())
+ json_inputs = self._model_def["Inputs"]
extra_inputs = list(set(list(inputs.keys())) - set(model_inputs))
- non_mandatory_inputs = list(all_closed_loop.keys()) + list(all_connect.keys()) + list(self._model_def.recurrentInputs().keys())
+ non_mandatory_inputs = (
+ list(all_closed_loop.keys())
+ + list(all_connect.keys())
+ + list(self._model_def.recurrentInputs().keys())
+ )
mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
## Remove extra inputs
for key in extra_inputs:
log.warning(
- f'The provided input {key} is not used inside the network. the inference will continue without using it')
+ f"The provided input {key} is not used inside the network. the inference will continue without using it"
+ )
del inputs[key]
## Get the number of data windows for each input
- num_of_windows = {key: len(value) for key, value in inputs.items()} if sampled else {
- key: len(value) - self._input_n_samples[key] + 1 for key, value in inputs.items()}
+ num_of_windows = (
+ {key: len(value) for key, value in inputs.items()}
+ if sampled
+ else {
+ key: len(value) - self._input_n_samples[key] + 1
+ for key, value in inputs.items()
+ }
+ )
if num_of_samples is not None and sampled == True:
- log.warning(f'num_of_samples is ignored if sampled is equal to True')
+ log.warning("num_of_samples is ignored if sampled is equal to True")
## Get the maximum inference window
if num_of_samples and not sampled:
window_dim = num_of_samples
for key in inputs.keys():
- input_dim = self._model_def['Inputs'][key]['dim']
- new_samples = num_of_samples - (len(inputs[key]) - self._input_n_samples[key] + 1)
+ input_dim = self._model_def["Inputs"][key]["dim"]
+ new_samples = num_of_samples - (
+ len(inputs[key]) - self._input_n_samples[key] + 1
+ )
if input_dim > 1:
- log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.')
- inputs[key] += [[0 for _ in range(input_dim)] for _ in range(new_samples)]
+ log.warning(
+ f"The variable {key} is filled with {new_samples} samples equal to zeros."
+ )
+ inputs[key] += [
+ [0 for _ in range(input_dim)] for _ in range(new_samples)
+ ]
else:
- log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.')
+ log.warning(
+ f"The variable {key} is filled with {new_samples} samples equal to zeros."
+ )
inputs[key] += [0 for _ in range(new_samples)]
elif inputs:
windows = []
for key in inputs.keys():
if key in mandatory_inputs:
- n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self._model_def['Inputs'][key]['ntot'] + 1
+ n_samples = (
+ len(inputs[key])
+ if sampled
+ else len(inputs[key])
+ - self._model_def["Inputs"][key]["ntot"]
+ + 1
+ )
windows.append(n_samples)
if not windows:
for key in inputs.keys():
if key in non_mandatory_inputs:
if key in model_inputs:
- n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self._model_def['Inputs'][key]['ntot'] + 1
+ n_samples = (
+ len(inputs[key])
+ if sampled
+ else len(inputs[key])
+ - self._model_def["Inputs"][key]["ntot"]
+ + 1
+ )
windows.append(n_samples)
window_dim = min(windows) if windows else 0
else: ## No inputs
window_dim = 1 if non_mandatory_inputs else 0
- check(window_dim > 0, StopIteration, f'Missing samples in the input window')
+ check(window_dim > 0, StopIteration, "Missing samples in the input window")
if len(set(num_of_windows.values())) > 1:
max_ind_key, max_dim = argmax_dict(num_of_windows)
min_ind_key, min_dim = argmin_dict(num_of_windows)
log.warning(
- f'Different number of samples between inputs [MAX {num_of_windows[max_ind_key]} = {max_dim}; MIN {num_of_windows[min_ind_key]} = {min_dim}]')
+ f"Different number of samples between inputs [MAX {num_of_windows[max_ind_key]} = {max_dim}; MIN {num_of_windows[min_ind_key]} = {min_dim}]"
+ )
## Autofill the missing inputs
provided_inputs = list(inputs.keys())
missing_inputs = list(set(mandatory_inputs) - set(provided_inputs))
if missing_inputs:
- log.warning(f'Inputs not provided: {missing_inputs}. Autofilling with zeros..')
+ log.warning(
+ f"Inputs not provided: {missing_inputs}. Autofilling with zeros.."
+ )
for key in missing_inputs:
inputs[key] = np.zeros(
- shape=(self._input_n_samples[key] + window_dim - 1, self._model_def['Inputs'][key]['dim']),
- dtype=NP_DTYPE).tolist()
+ shape=(
+ self._input_n_samples[key] + window_dim - 1,
+ self._model_def["Inputs"][key]["dim"],
+ ),
+ dtype=NP_DTYPE,
+ ).tolist()
## Transform inputs in 3D Tensors
for key in inputs.keys():
- input_dim = json_inputs[key]['dim']
+ input_dim = json_inputs[key]["dim"]
inputs[key] = torch.from_numpy(np.array(inputs[key])).to(TORCH_DTYPE)
if input_dim > 1:
correct_dim = 3 if sampled else 2
- check(len(inputs[key].shape) == correct_dim, ValueError,
- f'The input {key} must have {correct_dim} dimensions')
- check(inputs[key].shape[correct_dim - 1] == input_dim, ValueError,
- f'The second dimension of the input "{key}" must be equal to {input_dim}')
-
- if input_dim == 1 and inputs[key].shape[-1] != 1: ## add the input dimension
+ check(
+ len(inputs[key].shape) == correct_dim,
+ ValueError,
+ f"The input {key} must have {correct_dim} dimensions",
+ )
+ check(
+ inputs[key].shape[correct_dim - 1] == input_dim,
+ ValueError,
+ f'The second dimension of the input "{key}" must be equal to {input_dim}',
+ )
+
+ if (
+ input_dim == 1 and inputs[key].shape[-1] != 1
+ ): ## add the input dimension
inputs[key] = inputs[key].unsqueeze(-1)
if inputs[key].ndim <= 1: ## add the batch dimension
inputs[key] = inputs[key].unsqueeze(0)
@@ -320,15 +430,24 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c
## initialize the resulting dictionary
result_dict = {}
- for key in self._model_def['Outputs'].keys():
+ for key in self._model_def["Outputs"].keys():
result_dict[key] = []
if log_internal:
- internals_dict = {'ingress': [], 'state': [], 'closedLoop': [], 'connect': []}
+ internals_dict = {
+ "ingress": [],
+ "state": [],
+ "closedLoop": [],
+ "connect": [],
+ }
## Inference
- with (torch.enable_grad() if self._get_gradient_on_inference() else torch.inference_mode()):
+ with (
+ torch.enable_grad()
+ if self._get_gradient_on_inference()
+ else torch.inference_mode()
+ ):
## Update with virtual states
- if prediction_samples == 'auto' or prediction_samples >= 0:
+ if prediction_samples == "auto" or prediction_samples >= 0:
self._model.update(closed_loop=all_closed_loop, connect=all_connect)
else:
self._model.update(disconnect=True)
@@ -339,21 +458,31 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c
for idx in range(window_dim):
## Get mandatory data inputs
for key in mandatory_inputs:
- X[key] = inputs[key][idx:idx + 1] if sampled else inputs[key][:,idx:idx + self._input_n_samples[key]]
- if 'type' in json_inputs[key].keys():
+ X[key] = (
+ inputs[key][idx : idx + 1]
+ if sampled
+ else inputs[key][:, idx : idx + self._input_n_samples[key]]
+ )
+ if "type" in json_inputs[key].keys():
X[key] = X[key].requires_grad_(True)
## reset states
- if count == 0 or prediction_samples == 'auto':
+ if count == 0 or prediction_samples == "auto":
count = prediction_samples
for key in non_mandatory_inputs: ## Get non mandatory data (from inputs, from states, or with zeros)
## If it is given as input AND
## if prediction_samples is 'auto' and there are enough samples OR
## if prediction_samples is NOT 'auto'
if key in inputs.keys() and (
- (prediction_samples == 'auto' and idx < num_of_windows[key]) or \
- (prediction_samples != 'auto')
+ (prediction_samples == "auto" and idx < num_of_windows[key])
+ or (prediction_samples != "auto")
):
- X[key] = inputs[key][idx:idx + 1] if sampled else inputs[key][:,idx:idx + self._input_n_samples[key]]
+ X[key] = (
+ inputs[key][idx : idx + 1]
+ if sampled
+ else inputs[key][
+ :, idx : idx + self._input_n_samples[key]
+ ]
+ )
# if 0 in X[key].shape:
# window_size = self._input_n_samples[key]
# dim = json_inputs[key]['dim']
@@ -362,47 +491,51 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c
## if prediction_samples = 'auto' and there are not enough samples OR
## it is the first iteration with prediction_samples = None
elif key in self._states.keys() and (
- prediction_samples == 'auto' or
- (first and prediction_samples == None)
+ prediction_samples == "auto"
+ or (first and prediction_samples == None)
):
X[key] = self._states[key]
else:
- ## if there are no samples
+ ## if there are no samples
window_size = self._input_n_samples[key]
- dim = json_inputs[key]['dim']
- X[key] = torch.zeros(size=(1, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False)
-
- if 'type' in json_inputs[key].keys():
+ dim = json_inputs[key]["dim"]
+ X[key] = torch.zeros(
+ size=(1, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=False,
+ )
+
+ if "type" in json_inputs[key].keys():
X[key] = X[key].requires_grad_(True)
first = False
else:
# Remove the gradient of the previous forward
for key in X.keys():
- if 'type' in json_inputs[key].keys():
+ if "type" in json_inputs[key].keys():
X[key] = X[key].detach().requires_grad_(True)
count -= 1
## Forward pass
result, _, out_closed_loop, out_connect = self._model(X)
if log_internal:
- internals_dict['ingress'].append(tensor_to_list(X))
- internals_dict['closedLoop'].append(out_closed_loop)
- internals_dict['connect'].append(out_connect)
+ internals_dict["ingress"].append(tensor_to_list(X))
+ internals_dict["closedLoop"].append(out_closed_loop)
+ internals_dict["connect"].append(out_connect)
## Append the prediction of the current sample to the result dictionary
- for key in self._model_def['Outputs'].keys():
+ for key in self._model_def["Outputs"].keys():
if result[key].shape[-1] == 1:
result[key] = result[key].squeeze(-1)
if result[key].shape[-1] == 1:
result[key] = result[key].squeeze(-1)
- result_dict[key].append(result[key].detach().squeeze(dim=0).tolist())
+ result_dict[key].append(
+ result[key].detach().squeeze(dim=0).tolist()
+ )
## Update closed_loop and connect
if prediction_samples:
self._update_state(X, out_closed_loop, out_connect)
-
+
## Remove virtual states
self._remove_virtual_states(connect, closed_loop)
return result_dict if not log_internal else (result_dict, internals_dict)
-
-
diff --git a/nnodely/operators/exporter.py b/src/nnodely/operators/exporter.py
similarity index 69%
rename from nnodely/operators/exporter.py
rename to src/nnodely/operators/exporter.py
index 79cbed3e..7baaebb5 100644
--- a/nnodely/operators/exporter.py
+++ b/src/nnodely/operators/exporter.py
@@ -9,13 +9,25 @@
class Exporter(Network):
@enforce_types
- def __init__(self, exporter:EmptyExporter|str|None=None, workspace:str|None=None, *, save_history:bool=False):
- check(type(self) is not Exporter, TypeError, "Exporter class cannot be instantiated directly")
+ def __init__(
+ self,
+ exporter: EmptyExporter | str | None = None,
+ workspace: str | None = None,
+ *,
+ save_history: bool = False,
+ ):
+ check(
+ type(self) is not Exporter,
+ TypeError,
+ "Exporter class cannot be instantiated directly",
+ )
super().__init__()
# Exporter
- if exporter == 'Standard':
- self.__exporter = StandardExporter(workspace, self.visualizer, save_history=save_history)
+ if exporter == "Standard":
+ self.__exporter = StandardExporter(
+ workspace, self.visualizer, save_history=save_history
+ )
elif exporter != None:
self.__exporter = exporter
else:
@@ -26,7 +38,13 @@ def getWorkspace(self) -> str:
return self.__exporter.getWorkspace()
@enforce_types
- def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:str|None=None) -> None:
+ def saveTorchModel(
+ self,
+ name: str = "net",
+ model_folder: str | None = None,
+ *,
+ models: str | None = None,
+ ) -> None:
"""
Saves the neural network model in PyTorch format.
@@ -49,15 +67,21 @@ def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:s
.. include:: /examples_basics/export_module_ex/saveTorchModel.rst
"""
- check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.")
- check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet!')
+ check(
+ self._model_def.isDefined(),
+ RuntimeError,
+ "The network has not been defined.",
+ )
+ check(
+ self._neuralized == True, RuntimeError, "The model is not neuralized yet!"
+ )
if models is not None:
if type(models) is str:
models = [models]
- if name == 'net':
- name += '_' + '_'.join(models)
+ if name == "net":
+ name += "_" + "_".join(models)
model_def = ModelDef(self._model_def.getJson(models))
- model_def.setBuildWindow(self._model_def['Info']['SampleTime'])
+ model_def.setBuildWindow(self._model_def["Info"]["SampleTime"])
model_def.updateParameters(self._model)
model = Model(model_def.getJson())
else:
@@ -65,7 +89,9 @@ def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:s
self.__exporter.saveTorchModel(model, name, model_folder)
@enforce_types
- def loadTorchModel(self, name:str='net', model_folder:str|None=None) -> None:
+ def loadTorchModel(
+ self, name: str = "net", model_folder: str | None = None
+ ) -> None:
"""
Loads a neural network model from a PyTorch format file.
@@ -86,11 +112,17 @@ def loadTorchModel(self, name:str='net', model_folder:str|None=None) -> None:
.. include:: /examples_basics/export_module_ex/loadTorchModel.rst
"""
- check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
+ check(self.neuralized == True, RuntimeError, "The model is not neuralized yet.")
self.__exporter.loadTorchModel(self._model, name, model_folder)
@enforce_types
- def saveModel(self, name:str='net', model_folder:str|None=None, *, models:str|list|None=None) -> None:
+ def saveModel(
+ self,
+ name: str = "net",
+ model_folder: str | None = None,
+ *,
+ models: str | list | None = None,
+ ) -> None:
"""
Saves the neural network model definition in a json file.
@@ -113,21 +145,25 @@ def saveModel(self, name:str='net', model_folder:str|None=None, *, models:str|li
.. include:: /examples_basics/export_module_ex/saveModel.rst
"""
- check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.")
+ check(
+ self._model_def.isDefined(),
+ RuntimeError,
+ "The network has not been defined.",
+ )
if models is not None:
if type(models) is str:
models = [models]
- if name == 'net':
- name += '_' + '_'.join(models)
+ if name == "net":
+ name += "_" + "_".join(models)
model_def = ModelDef(self._model_def.getJson(models))
- model_def.setBuildWindow(self._model_def['Info']['SampleTime'])
+ model_def.setBuildWindow(self._model_def["Info"]["SampleTime"])
model_def.updateParameters(self._model)
else:
model_def = self._model_def
self.__exporter.saveModel(model_def.getJson(), name, model_folder)
@enforce_types
- def loadModel(self, name:str='net', model_folder:str|None=None) -> None:
+ def loadModel(self, name: str = "net", model_folder: str | None = None) -> None:
"""
Loads a neural network model from a json file containing the model definition.
@@ -150,17 +186,19 @@ def loadModel(self, name:str='net', model_folder:str|None=None) -> None:
"""
model_def = self.__exporter.loadModel(name, model_folder)
check(model_def, RuntimeError, "Error to load the network.")
- new_tags = (list(model_def['Inputs'].keys()) +
- list(model_def['Functions'].keys()) +
- list(model_def['Relations'].keys()) +
- list(model_def['Parameters'].keys())+
- list(model_def['Constants'].keys()))
+ new_tags = (
+ list(model_def["Inputs"].keys())
+ + list(model_def["Functions"].keys())
+ + list(model_def["Relations"].keys())
+ + list(model_def["Parameters"].keys())
+ + list(model_def["Constants"].keys())
+ )
## TODO: setting the Stream.count is not enough, we need a global tag manager
# old_tags = list(self._model_def['Functions'].keys()) + list(self._model_def['Relations'].keys()) + list(self._model_def['Parameters'].keys()) if self._model_def is not None else []
# check that there are no common tags
# common_tags = set(new_tags).intersection(set(old_tags))
# check(len(common_tags) == 0, RuntimeError, f"The model contains some tags that are already present in the current model: {common_tags}.\n Please rename them before loading the model.")
- #check(Stream.count == 0, RuntimeError, "There are some defined Stream, loadModel can be called only at the beginning, when the neural graph is empty.")
+ # check(Stream.count == 0, RuntimeError, "There are some defined Stream, loadModel can be called only at the beginning, when the neural graph is empty.")
Stream.count = Stream.count + len(new_tags) + 1
NeuObj.count = NeuObj.count + len(new_tags) + 1
self._model_def = ModelDef(model_def)
@@ -169,7 +207,13 @@ def loadModel(self, name:str='net', model_folder:str|None=None) -> None:
self._traced = False
@enforce_types
- def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, models:str|None=None) -> None:
+ def exportPythonModel(
+ self,
+ name: str = "net",
+ model_folder: str | None = None,
+ *,
+ models: str | None = None,
+ ) -> None:
"""
Exports the neural network model as a standalone PyTorch Module class.
@@ -194,20 +238,31 @@ def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, model
.. include:: /examples_basics/export_module_ex/exportPythonModel.rst
"""
- check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.")
- check(self._traced == False, RuntimeError,
- 'The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.')
+ check(
+ self._model_def.isDefined(),
+ RuntimeError,
+ "The network has not been defined.",
+ )
+ check(
+ self._traced == False,
+ RuntimeError,
+ "The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.",
+ )
if models is not None:
if type(models) is str:
models = [models]
- if name == 'net':
- name += '_' + '_'.join(models)
+ if name == "net":
+ name += "_" + "_".join(models)
model_def = ModelDef(self._model_def.getJson(models))
- model_def.setBuildWindow(self._model_def['Info']['SampleTime'])
+ model_def.setBuildWindow(self._model_def["Info"]["SampleTime"])
model_def.updateParameters(self._model)
model = Model(model_def.getJson())
else:
- check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet.')
+ check(
+ self._neuralized == True,
+ RuntimeError,
+ "The model is not neuralized yet.",
+ )
model_def = self._model_def
model = self._model
model.update()
@@ -215,7 +270,9 @@ def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, model
self.__exporter.exportPythonModel(model_def, model, name, model_folder)
@enforce_types
- def importPythonModel(self, name:str='net', model_folder:str|None=None) -> None:
+ def importPythonModel(
+ self, name: str = "net", model_folder: str | None = None
+ ) -> None:
"""
Imports a neural network model from a standalone PyTorch Module class.
@@ -244,7 +301,15 @@ def importPythonModel(self, name:str='net', model_folder:str|None=None) -> None:
self._model_def.updateParameters(self._model)
@enforce_types
- def exportONNX(self, inputs_order:list|None=None, outputs_order:list|None=None, name:str='net', model_folder:str|None=None, *, models:str|list|None=None) -> None:
+ def exportONNX(
+ self,
+ inputs_order: list | None = None,
+ outputs_order: list | None = None,
+ name: str = "net",
+ model_folder: str | None = None,
+ *,
+ models: str | list | None = None,
+ ) -> None:
"""
Exports the neural network model to an ONNX file.
@@ -277,32 +342,52 @@ def exportONNX(self, inputs_order:list|None=None, outputs_order:list|None=None,
.. include:: /examples_basics/export_module_ex/exportONNX.rst
"""
- check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.")
- check(self._traced == False, RuntimeError,
- 'The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.')
- check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet.')
+ check(
+ self._model_def.isDefined(),
+ RuntimeError,
+ "The network has not been defined.",
+ )
+ check(
+ self._traced == False,
+ RuntimeError,
+ "The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.",
+ )
+ check(
+ self._neuralized == True, RuntimeError, "The model is not neuralized yet."
+ )
# From here --------------
if models is not None:
if type(models) is str:
models = [models]
- if name == 'net':
- name += '_' + '_'.join(models)
+ if name == "net":
+ name += "_" + "_".join(models)
model_def = ModelDef(self._model_def.getJson(models))
- check(len(model_def.recurrentInputs().keys()) < len(model_def['Inputs'].keys()), TypeError,
- "The network has only recurrent inputs.")
- model_def.setBuildWindow(self._model_def['Info']['SampleTime'])
+ check(
+ len(model_def.recurrentInputs().keys())
+ < len(model_def["Inputs"].keys()),
+ TypeError,
+ "The network has only recurrent inputs.",
+ )
+ model_def.setBuildWindow(self._model_def["Info"]["SampleTime"])
model_def.updateParameters(self._model)
model = Model(model_def.getJson())
else:
model_def = self._model_def
model = self._model
model.update()
- check(len(model_def.recurrentInputs().keys()) < len(model_def['Inputs'].keys()), TypeError,
- "The network is autonomous because only recurrent variables are present.")
- self.__exporter.exportONNX(model_def, model, inputs_order, outputs_order, name, model_folder)
+ check(
+ len(model_def.recurrentInputs().keys()) < len(model_def["Inputs"].keys()),
+ TypeError,
+ "The network is autonomous because only recurrent variables are present.",
+ )
+ self.__exporter.exportONNX(
+ model_def, model, inputs_order, outputs_order, name, model_folder
+ )
@enforce_types
- def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None) -> dict:
+ def onnxInference(
+ self, inputs: dict, name: str = "net", model_folder: str | None = None
+ ) -> dict:
"""
Run an inference session using an onnx model previously exported using the nnodely framework.
@@ -328,13 +413,13 @@ def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None)
Examples
--------
-
+
.. include:: /examples_basics/export_module_ex/onnxInference.rst
"""
return self.__exporter.onnxInference(inputs, name, model_folder)
@enforce_types
- def exportReport(self, name:str='net', model_folder:str|None=None) -> None:
+ def exportReport(self, name: str = "net", model_folder: str | None = None) -> None:
"""
Generates a PDF report with plots containing the results of the training and validation of the neural network.
diff --git a/nnodely/operators/loader.py b/src/nnodely/operators/loader.py
similarity index 67%
rename from nnodely/operators/loader.py
rename to src/nnodely/operators/loader.py
index 8b2d0960..6b56278a 100644
--- a/nnodely/operators/loader.py
+++ b/src/nnodely/operators/loader.py
@@ -1,4 +1,5 @@
-import os, random
+import os
+import random
import pandas as pd
import numpy as np
@@ -10,12 +11,18 @@
from nnodely.support.utils import check, enforce_types
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.WARNING)
+
class Loader(Network):
@enforce_types
def __init__(self):
- check(type(self) is not Loader, TypeError, "Loader class cannot be instantiated directly")
+ check(
+ type(self) is not Loader,
+ TypeError,
+ "Loader class cannot be instantiated directly",
+ )
super().__init__()
# Dataaset Parameters
@@ -23,7 +30,9 @@ def __init__(self):
self.__datasets_loaded = set()
@enforce_types
- def getSamples(self, dataset:str, *, index:int|None = None, window:int=1) -> dict:
+ def getSamples(
+ self, dataset: str, *, index: int | None = None, window: int = 1
+ ) -> dict:
"""
Retrieves a window of samples from a given dataset.
@@ -53,19 +62,25 @@ def getSamples(self, dataset:str, *, index:int|None = None, window:int=1) -> dic
"""
if index is None:
index = random.randint(0, self._num_of_samples[dataset] - window)
- check(self._data_loaded, ValueError, 'The Dataset must first be loaded using function!')
+ check(
+ self._data_loaded,
+ ValueError,
+ "The Dataset must first be loaded using function!",
+ )
if self._data_loaded:
result_dict = {}
- for key in self._model_def['Inputs'].keys():
+ for key in self._model_def["Inputs"].keys():
result_dict[key] = []
for idx in range(window):
- for key ,samples in self._data[dataset].items():
- if key in self._model_def['Inputs'].keys():
- result_dict[key].append(samples[index+idx])
+ for key, samples in self._data[dataset].items():
+ if key in self._model_def["Inputs"].keys():
+ result_dict[key].append(samples[index + idx])
return result_dict
@enforce_types
- def filterData(self, filter_function:Callable, dataset_name:str|None = None) -> None:
+ def filterData(
+ self, filter_function: Callable, dataset_name: str | None = None
+ ) -> None:
"""
Filters the data in the dataset using the provided filter function.
@@ -97,7 +112,9 @@ def filterData(self, filter_function:Callable, dataset_name:str|None = None) ->
idx_to_remove.append(idx)
for key in self._data[name].keys():
- self._data[name][key] = np.delete(self._data[name][key], idx_to_remove, axis=0)
+ self._data[name][key] = np.delete(
+ self._data[name][key], idx_to_remove, axis=0
+ )
self._num_of_samples[name] = self._data[name][key].shape[0]
self.visualizer.showDataset(name=name)
@@ -115,12 +132,16 @@ def filterData(self, filter_function:Callable, dataset_name:str|None = None) ->
idx_to_remove.append(idx)
for key in self._data[dataset_name].keys():
- self._data[dataset_name][key] = np.delete(self._data[dataset_name][key], idx_to_remove, axis=0)
- self._num_of_samples[dataset_name] = self._data[dataset_name][key].shape[0]
+ self._data[dataset_name][key] = np.delete(
+ self._data[dataset_name][key], idx_to_remove, axis=0
+ )
+ self._num_of_samples[dataset_name] = self._data[dataset_name][
+ key
+ ].shape[0]
self.visualizer.showDataset(name=dataset_name)
@enforce_types
- def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None:
+ def resamplingData(self, df: pd.DataFrame, *, scale: float = 1e9) -> None:
"""
Resamples the DataFrame to a specified sample time.
@@ -149,7 +170,7 @@ def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None:
sample_time_ns = int(self._model_def.getSampleTime() * scale)
if sample_time_ns <= 0:
raise ValueError(f"Invalid resampling step: {sample_time_ns}ns")
- method = 'linear'
+ method = "linear"
if isinstance(df.index, pd.DatetimeIndex):
# FORZA risoluzione ns (evita unit='s' interno)
if df.index.dtype != "datetime64[ns]":
@@ -177,12 +198,14 @@ def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None:
df = df.resample(f"{sample_time_ns}ns").interpolate(method=method)
else:
- raise TypeError("No time column found in the DataFrame. Please provide a time column for resampling.")
+ raise TypeError(
+ "No time column found in the DataFrame. Please provide a time column for resampling."
+ )
return df
-
+
@enforce_types
def __get_format_idxs(self, format: list | None = None) -> dict:
- model_inputs = self._model_def['Inputs']
+ model_inputs = self._model_def["Inputs"]
format_idx = {}
idx = 0
for item in format:
@@ -190,11 +213,17 @@ def __get_format_idxs(self, format: list | None = None) -> dict:
n_cols = None
for key in item:
if key in model_inputs.keys():
- if n_cols is None or n_cols == model_inputs[key]['dim']:
- n_cols = model_inputs[key]['dim']
+ if n_cols is None or n_cols == model_inputs[key]["dim"]:
+ n_cols = model_inputs[key]["dim"]
else:
- raise ValueError(f'The variables {item} have different dimensionality.')
- check(key not in format_idx, ValueError, f"The format '{format}' in not correct some variables appears more than once.")
+ raise ValueError(
+ f"The variables {item} have different dimensionality."
+ )
+ check(
+ key not in format_idx,
+ ValueError,
+ f"The format '{format}' in not correct some variables appears more than once.",
+ )
format_idx[key] = (idx, idx + n_cols)
if n_cols is not None:
idx += n_cols
@@ -204,30 +233,33 @@ def __get_format_idxs(self, format: list | None = None) -> dict:
if item not in model_inputs.keys():
idx += 1
continue
- n_cols = model_inputs[item]['dim']
- check(item not in format_idx, ValueError,
- f"The format '{format}' in not correct some variables appears more than once.")
+ n_cols = model_inputs[item]["dim"]
+ check(
+ item not in format_idx,
+ ValueError,
+ f"The format '{format}' in not correct some variables appears more than once.",
+ )
format_idx[item] = (idx, idx + n_cols)
idx += n_cols
return format_idx
-
+
@enforce_types
- def __get_files(self, folder:str) -> list:
+ def __get_files(self, folder: str) -> list:
try:
_, _, files = next(os.walk(folder))
files.sort()
- except StopIteration as e:
+ except StopIteration:
check(False, StopIteration, f'ERROR: The path "{folder}" does not exist!')
return []
return files
-
+
@enforce_types
def __stack_arrays(self, data: dict) -> tuple:
## Convert lists to numpy arrays
num_of_samples = {}
for key in data:
data[key] = np.stack(data[key])
- if self._model_def['Inputs'][key]['dim'] > 1:
+ if self._model_def["Inputs"][key]["dim"] > 1:
data[key] = np.array(data[key].tolist(), dtype=np.float64)
if data[key].ndim == 2: ## Add the sample dimension
data[key] = np.expand_dims(data[key], axis=-1)
@@ -237,14 +269,17 @@ def __stack_arrays(self, data: dict) -> tuple:
return num_of_samples
@enforce_types
- def loadData(self, name:str,
- source: str | dict | pd.DataFrame, *,
- format: list | None = None,
- skiplines: int = 0,
- delimiter: str = ',',
- header: int | str | Sequence | None = None,
- resampling: bool = False
- ) -> None:
+ def loadData(
+ self,
+ name: str,
+ source: str | dict | pd.DataFrame,
+ *,
+ format: list | None = None,
+ skiplines: int = 0,
+ delimiter: str = ",",
+ header: int | str | Sequence | None = None,
+ resampling: bool = False,
+ ) -> None:
"""
Loads data into the model. The data can be loaded from a directory path containing the csv files or from a crafted dataset.
@@ -271,15 +306,17 @@ def loadData(self, name:str,
Examples
--------
-
+
.. include:: /examples_basics/data_loader_module_ex/loadData.rst
"""
check(self.neuralized, ValueError, "The network is not neuralized.")
- check(delimiter in ['\t', '\n', ';', ',', ' '], ValueError, 'delimiter not valid!')
+ check(
+ delimiter in ["\t", "\n", ";", ",", " "], ValueError, "delimiter not valid!"
+ )
- json_inputs = self._model_def['Inputs']
+ json_inputs = self._model_def["Inputs"]
## Initialize the dictionary containing the data
- check_names(name, self._data.keys(), f"Dataset")
+ check_names(name, self._data.keys(), "Dataset")
if type(source) is str: ## we have a directory path containing the files
## collect column indexes
@@ -299,23 +336,46 @@ def loadData(self, name:str,
for file in files:
try:
## read the csv
- df = pd.read_csv(os.path.join(source, file), skiprows=skiplines, delimiter=delimiter, header=header)
+ df = pd.read_csv(
+ os.path.join(source, file),
+ skiprows=skiplines,
+ delimiter=delimiter,
+ header=header,
+ )
if not all(df.iloc[0].apply(lambda x: isinstance(x, (int, float)))):
- log.warning(f"The file {file} does not contain a numerical column.")
+ log.warning(
+ f"The file {file} does not contain a numerical column."
+ )
## Resampling if the time column is provided (must be a Datetime object)
if resampling:
self.resamplingData(df)
except:
- log.warning(f'Cannot read file {os.path.join(source, file)}')
+ log.warning(f"Cannot read file {os.path.join(source, file)}")
continue
if self._file_count > 1:
- self._multifile[name].append((self._multifile[name][-1] + (len(df) - self._max_n_samples + 1)) if self._multifile[name] else len(df) - self._max_n_samples + 1)
+ self._multifile[name].append(
+ (
+ self._multifile[name][-1]
+ + (len(df) - self._max_n_samples + 1)
+ )
+ if self._multifile[name]
+ else len(df) - self._max_n_samples + 1
+ )
## Cycle through all the windows
for key, idxs in format_idx.items():
- back, forw = self._input_ns_backward[key], self._input_ns_forward[key]
+ back, forw = (
+ self._input_ns_backward[key],
+ self._input_ns_forward[key],
+ )
## Save as numpy array the data
- data = df.iloc[:, idxs[0]:idxs[1]].to_numpy()
- self._data[name][key] += [data[i - back:i + forw] for i in range(self._max_samples_backward, len(df) - self._max_samples_forward + 1)]
+ data = df.iloc[:, idxs[0] : idxs[1]].to_numpy()
+ self._data[name][key] += [
+ data[i - back : i + forw]
+ for i in range(
+ self._max_samples_backward,
+ len(df) - self._max_samples_forward + 1,
+ )
+ ]
else: ## we have a crafted dataset
## add the dataset
self._data[name] = {}
@@ -326,9 +386,17 @@ def loadData(self, name:str,
if key not in source.keys():
continue
self._data[name][key] = [] ## Initialize the dataset
- back, forw = self._input_ns_backward[key], self._input_ns_forward[key]
+ back, forw = (
+ self._input_ns_backward[key],
+ self._input_ns_forward[key],
+ )
for idx in range(len(source[key]) - self._max_n_samples + 1):
- self._data[name][key].append(source[key][idx + (self._max_samples_backward - back):idx + (self._max_samples_backward + forw)])
+ self._data[name][key].append(
+ source[key][
+ idx + (self._max_samples_backward - back) : idx
+ + (self._max_samples_backward + forw)
+ ]
+ )
else:
if resampling:
source = self.resamplingData(source)
@@ -336,15 +404,25 @@ def loadData(self, name:str,
if key not in source.columns:
continue
self._data[name][key] = [] ## Initialize the dataset
- back, forw = self._input_ns_backward[key], self._input_ns_forward[key]
+ back, forw = (
+ self._input_ns_backward[key],
+ self._input_ns_forward[key],
+ )
for idx in range(len(source) - self._max_n_samples + 1):
- window = source[key].iloc[idx + (self._max_samples_backward - back):idx + (self._max_samples_backward + forw)]
+ window = source[key].iloc[
+ idx + (self._max_samples_backward - back) : idx
+ + (self._max_samples_backward + forw)
+ ]
self._data[name][key].append(window.to_numpy())
## Convert lists to numpy arrays
num_of_samples = self.__stack_arrays(self._data[name])
# Check dim of the samples
- check(len(set(num_of_samples.values())) == 1, ValueError, f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}")
+ check(
+ len(set(num_of_samples.values())) == 1,
+ ValueError,
+ f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}",
+ )
self._num_of_samples[name] = num_of_samples[list(num_of_samples.keys())[0]]
## Set the Loaded flag to True
self._data_loaded = True
@@ -352,4 +430,4 @@ def loadData(self, name:str,
self.__n_datasets = len(self._data.keys())
self.__datasets_loaded.add(name)
## Show the dataset
- self.visualizer.showDataset(name=name)
\ No newline at end of file
+ self.visualizer.showDataset(name=name)
diff --git a/src/nnodely/operators/network.py b/src/nnodely/operators/network.py
new file mode 100644
index 00000000..fcf97ec7
--- /dev/null
+++ b/src/nnodely/operators/network.py
@@ -0,0 +1,711 @@
+import copy
+from collections import defaultdict
+
+import numpy as np
+import torch
+import random
+
+from nnodely.support.utils import (
+ TORCH_DTYPE,
+ NP_DTYPE,
+ check,
+ enforce_types,
+ tensor_to_list,
+)
+from nnodely.basic.modeldef import ModelDef
+
+from nnodely.support.logger import logging, nnLogger
+
+log = nnLogger(__name__, logging.WARNING)
+
+
+class Network:
+ @enforce_types
+ def __init__(self):
+ check(
+ type(self) is not Network,
+ TypeError,
+ "Loader class cannot be instantiated directly",
+ )
+
+ # Models definition
+ self._model_def = ModelDef()
+ self._model = None
+ self._neuralized = False
+ self._traced = False
+
+ # Model components
+ self._states = {}
+ self._input_n_samples = {}
+ self._input_ns_backward = {}
+ self._input_ns_forward = {}
+ self._max_samples_backward = None
+ self._max_samples_forward = None
+ self._max_n_samples = 0
+
+ # Dataset information
+ self._data_loaded = False
+ self._file_count = 0
+ self._num_of_samples = {}
+ self._data = {}
+ self._multifile = {}
+
+ # Training information
+ self._standard_train_parameters = {
+ "models": None,
+ "train_dataset": None,
+ "validation_dataset": None,
+ "dataset": None,
+ "splits": [100, 0, 0],
+ "closed_loop": {},
+ "connect": {},
+ "step": 0,
+ "prediction_samples": 0,
+ "shuffle_data": True,
+ "early_stopping": None,
+ "early_stopping_params": {},
+ "select_model": "last",
+ "select_model_params": {},
+ "minimize_gain": {},
+ "num_of_epochs": 100,
+ "train_batch_size": 128,
+ "val_batch_size": 128,
+ "optimizer": "Adam",
+ "lr": 0.001,
+ "lr_param": {},
+ "optimizer_params": [],
+ "add_optimizer_params": [],
+ "optimizer_defaults": {},
+ "add_optimizer_defaults": {},
+ }
+ self._training = {}
+
+ # Save internal
+ self._log_internal = False
+ self._internals = {}
+
+ def _save_internal(self, key, value):
+ self._internals[key] = tensor_to_list(value)
+
+ def _set_log_internal(self, log_internal: bool):
+ self._log_internal = log_internal
+
+ def _clean_log_internal(self):
+ self._internals = {}
+
+ def _remove_virtual_states(self, connect, closed_loop):
+ if connect or closed_loop:
+ for key in connect.keys() | closed_loop.keys():
+ if key in self._states.keys():
+ del self._states[key]
+
+ def _update_state(self, X, out_closed_loop, out_connect):
+ for key, value in out_connect.items():
+ X[key] = torch.roll(value, shifts=-1, dims=1) ## Roll the time window
+ X[key][:, -1, :] = float(
+ "inf"
+ ) ## inf value to make clear that the last state value
+ self._states[key] = X[key].clone().detach()
+ for key, val in out_closed_loop.items():
+ shift = val.shape[
+ 1
+ ] # + self._input_ns_forward[key] ## take the output time dimension + forward samples
+ X[key] = torch.roll(X[key], shifts=-1, dims=1) ## Roll the time window
+ X[key][:, -shift:, :] = val ## substitute with the predicted value
+ self._states[key] = X[key].clone().detach()
+
+ def _get_gradient_on_inference(self):
+ for key, value in self._model_def["Inputs"].items():
+ if "type" in value.keys():
+ return True
+ return False
+
+ def _get_mandatory_inputs(self, connect, closed_loop):
+ model_inputs = list(self._model_def["Inputs"].keys())
+ non_mandatory_inputs = (
+ list(closed_loop.keys())
+ + list(connect.keys())
+ + list(self._model_def.recurrentInputs().keys())
+ )
+ mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
+ return mandatory_inputs, non_mandatory_inputs
+
+ def _get_batch_indexes(
+ self,
+ datasets: str | list | dict | None,
+ n_samples: int = 0,
+ prediction_samples: int = 0,
+ ):
+ if datasets is None:
+ return []
+ batch_indexes = list(range(n_samples))
+ if prediction_samples > 0 and not isinstance(datasets, dict):
+ datasets = [datasets] if type(datasets) is str else datasets
+ forbidden_idxs = []
+ n_samples_count = 0
+ for dataset in datasets:
+ if dataset in self._multifile.keys(): ## i have some forbidden indexes
+ for i in self._multifile[dataset]:
+ if i + n_samples_count < batch_indexes[-1]:
+ forbidden_idxs.extend(
+ range(
+ (i + n_samples_count) - prediction_samples,
+ (i + n_samples_count),
+ 1,
+ )
+ )
+ n_samples_count += self._num_of_samples[dataset]
+ batch_indexes = [idx for idx in batch_indexes if idx not in forbidden_idxs]
+ batch_indexes = batch_indexes[:-prediction_samples]
+ return batch_indexes
+
+ def _get_data(self, dataset: str | list | dict | None):
+ if dataset is None:
+ return {}
+ if isinstance(dataset, dict):
+ self.__check_data_integrity(dataset)
+ return dataset
+ dataset = [dataset] if type(dataset) is str else dataset
+ loaded_datasets = list(self._data.keys())
+ check(
+ len([data for data in dataset if data in loaded_datasets]) > 0,
+ KeyError,
+ f"the datasets: {dataset} are not loaded!",
+ )
+ total_data = defaultdict(list)
+ for data in dataset:
+ if data not in loaded_datasets:
+ log.warning(f"{data} is not loaded. Ignoring this dataset...")
+ dataset.remove(data)
+ continue
+ for k, v in self._data[data].items():
+ total_data[k].append(v)
+ total_data = {key: np.concatenate(arrays) for key, arrays in total_data.items()}
+ total_data = {
+ key: torch.from_numpy(val).to(TORCH_DTYPE)
+ for key, val in total_data.items()
+ }
+ return total_data
+
+ def _clip_step(self, step, batch_indexes, batch_size):
+ clipped_step = copy.deepcopy(step)
+ if clipped_step < 0: ## clip the step to zero
+ log.warning(
+ f"The step is negative ({clipped_step}). The step is set to zero.",
+ stacklevel=5,
+ )
+ clipped_step = 0
+ if clipped_step > (
+ len(batch_indexes) - batch_size
+ ): ## Clip the step to the maximum number of samples
+ log.warning(
+ f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes) - batch_size}). The step is set to the maximum number.",
+ stacklevel=5,
+ )
+ clipped_step = len(batch_indexes) - batch_size
+ check(
+ (batch_size + clipped_step) > 0,
+ ValueError,
+ f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.",
+ )
+ return clipped_step
+
+ def _clip_batch_size(self, n_samples, batch_size=None):
+ batch_size = batch_size if batch_size <= n_samples else max(0, n_samples)
+ check(
+ (n_samples - batch_size + 1) > 0,
+ ValueError,
+ f"The number of available sample are {n_samples - batch_size + 1}",
+ )
+ check(batch_size > 0, ValueError, "The batch_size must be greater than 0.")
+ return batch_size
+
+ def __split_dataset(self, dataset: str | list | dict, splits: list):
+ check(
+ len(splits) == 3,
+ ValueError,
+ "3 elements must be inserted for the dataset split in training, validation and test",
+ )
+ check(
+ sum(splits) == 100,
+ ValueError,
+ "Training, Validation and Test splits must sum up to 100.",
+ )
+ check(splits[0] > 0, ValueError, "The training split cannot be zero.")
+ train_size, val_size, test_size = (
+ splits[0] / 100,
+ splits[1] / 100,
+ splits[2] / 100,
+ )
+ XY_train, XY_val, XY_test = {}, {}, {}
+ if isinstance(dataset, dict):
+ self.__check_data_integrity(dataset)
+ num_of_samples = next(iter(dataset.values())).size(0)
+ XY_train = {
+ key: value[: round(num_of_samples * train_size), :, :]
+ for key, value in dataset.items()
+ }
+ XY_val = {
+ key: value[
+ round(num_of_samples * train_size) : round(
+ num_of_samples * (train_size + val_size)
+ ),
+ :,
+ :,
+ ]
+ for key, value in dataset.items()
+ }
+ XY_test = {
+ key: value[round(num_of_samples * (train_size + val_size)) :, :, :]
+ for key, value in dataset.items()
+ }
+ else:
+ dataset = [dataset] if type(dataset) is str else dataset
+ check(
+ len([data for data in dataset if data in self._data.keys()]) > 0,
+ KeyError,
+ f"the datasets: {dataset} are not loaded!",
+ )
+ for data in dataset:
+ if data not in self._data.keys():
+ log.warning(
+ f"{data} is not loaded. The training will continue without this dataset."
+ )
+ dataset.remove(data)
+
+ num_of_samples = sum([self._num_of_samples[data] for data in dataset])
+ n_samples_train, n_samples_val = (
+ round(num_of_samples * train_size),
+ round(num_of_samples * val_size),
+ )
+ n_samples_test = num_of_samples - n_samples_train - n_samples_val
+ check(
+ n_samples_train > 0,
+ ValueError,
+ f"The number of train samples {n_samples_train} must be greater than 0.",
+ )
+ total_data = defaultdict(list)
+ for data in dataset:
+ for k, v in self._data[data].items():
+ total_data[k].append(v)
+ total_data = {
+ key: np.concatenate(arrays, dtype=NP_DTYPE)
+ for key, arrays in total_data.items()
+ }
+ for key, samples in total_data.items():
+ if val_size == 0.0 and test_size == 0.0: ## we have only training set
+ XY_train[key] = torch.from_numpy(samples).to(TORCH_DTYPE)
+ elif (
+ val_size == 0.0 and test_size != 0.0
+ ): ## we have only training and test set
+ XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(
+ TORCH_DTYPE
+ )
+ XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(
+ TORCH_DTYPE
+ )
+ elif (
+ val_size != 0.0 and test_size == 0.0
+ ): ## we have only training and validation set
+ XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(
+ TORCH_DTYPE
+ )
+ XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(
+ TORCH_DTYPE
+ )
+ else: ## we have training, validation and test set
+ XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(
+ TORCH_DTYPE
+ )
+ XY_val[key] = torch.from_numpy(
+ samples[n_samples_train:-n_samples_test]
+ ).to(TORCH_DTYPE)
+ XY_test[key] = torch.from_numpy(
+ samples[n_samples_train + n_samples_val :]
+ ).to(TORCH_DTYPE)
+ return XY_train, XY_val, XY_test
+
+ def _get_tag(self, dataset: str | list | dict | None) -> str:
+ """
+ Helper function to get the tag for a dataset.
+ """
+ if isinstance(dataset, str):
+ return dataset
+ elif isinstance(dataset, list):
+ return (
+ f"{dataset[0]}_{len(dataset)}" if len(dataset) > 1 else f"{dataset[0]}"
+ )
+ elif isinstance(dataset, dict):
+ return "custom_dataset"
+ return dataset
+
+ def _setup_dataset(
+ self,
+ train_dataset: str | list | dict,
+ validation_dataset: str | list | dict,
+ test_dataset: str | list | dict,
+ dataset: str | list | dict,
+ splits: list,
+ ):
+ if train_dataset is None: ## use the splits
+ train_dataset = list(self._data.keys()) if dataset is None else dataset
+ return self.__split_dataset(train_dataset, splits)
+ else: ## use each dataset
+ return (
+ self._get_data(train_dataset),
+ self._get_data(validation_dataset),
+ self._get_data(test_dataset),
+ )
+
+ def __check_data_integrity(self, dataset: dict):
+ if bool(dataset):
+ check(
+ len(set([t.size(0) for t in dataset.values()])) == 1,
+ ValueError,
+ "All the tensors in the dataset must have the same number of samples.",
+ )
+ # TODO check why is wrong
+ # check(len([t for t in self._model_def['Inputs'].keys() if t in dataset.keys()]) == len(list(self._model_def['Inputs'].keys())), ValueError, "Some inputs are missing.")
+ for key, value in dataset.items():
+ if key not in self._model_def["Inputs"]:
+ log.warning(
+ f"The key '{key}' is not an input of the network. It will be ignored."
+ )
+ else:
+ check(
+ isinstance(value, torch.Tensor),
+ TypeError,
+ f"The value of the input '{key}' must be a torch.Tensor.",
+ )
+ check(
+ value.size(1) == self._model_def["Inputs"][key]["ntot"],
+ ValueError,
+ f"The time size of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['ntot']}, got {value.size(1)}.",
+ )
+ check(
+ value.size(2) == self._model_def["Inputs"][key]["dim"],
+ ValueError,
+ f"The dimension of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['dim']}, got {value.size(2)}.",
+ )
+
+ def _get_not_mandatory_inputs(
+ self,
+ data,
+ X,
+ non_mandatory_inputs,
+ remaning_indexes,
+ batch_size,
+ step,
+ shuffle=False,
+ ):
+ related_indexes = (
+ random.sample(remaning_indexes, batch_size)
+ if shuffle
+ else remaning_indexes[:batch_size]
+ )
+ for num in related_indexes:
+ remaning_indexes.remove(num)
+ if step > 0:
+ if len(remaning_indexes) >= step:
+ step_idxs = (
+ random.sample(remaning_indexes, step)
+ if shuffle
+ else remaning_indexes[:step]
+ )
+ for num in step_idxs:
+ remaning_indexes.remove(num)
+ else:
+ remaning_indexes.clear()
+ for key in non_mandatory_inputs:
+ if key in data.keys(): ## with data
+ X[key] = data[key][related_indexes]
+ else: ## with zeros
+ window_size = self._input_n_samples[key]
+ dim = self._model_def["Inputs"][key]["dim"]
+ if "type" in self._model_def["Inputs"][key]:
+ X[key] = torch.zeros(
+ size=(batch_size, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=True,
+ )
+ else:
+ X[key] = torch.zeros(
+ size=(batch_size, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=False,
+ )
+ self._states[key] = X[key]
+ return related_indexes
+
+ def _inference(
+ self,
+ data,
+ n_samples,
+ batch_size,
+ loss_gains,
+ loss_functions,
+ shuffle=False,
+ optimizer=None,
+ total_losses=None,
+ A=None,
+ B=None,
+ ):
+ if shuffle:
+ randomize = torch.randperm(n_samples)
+ data = {key: val[randomize] for key, val in data.items()}
+
+ ## Initialize the train losses vector
+ aux_losses = torch.zeros(
+ [len(self._model_def["Minimizers"]), n_samples // batch_size]
+ )
+ for idx in range(0, (n_samples - batch_size + 1), batch_size):
+ ## Build the input tensor
+ XY = {}
+ for key, val in data.items():
+ if self._model_def["Inputs"].get(key, None):
+ if self._model_def["Inputs"][key].get("type", None) == "derivate":
+ XY[key] = (
+ val[idx : idx + batch_size].detach().requires_grad_(True)
+ )
+ else:
+ XY[key] = val[idx : idx + batch_size]
+ # XY = {key: val[idx:idx + batch_size].detach().requires_grad_(True) for key, val in data.items()}
+ for key in self._model_def.recurrentInputs().keys():
+ if key not in XY.keys():
+ window_size = self._input_n_samples[key]
+ dim = self._model_def["Inputs"][key]["dim"]
+ XY[key] = torch.zeros(
+ size=(batch_size, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=True,
+ )
+ ## Reset gradient
+ if optimizer:
+ optimizer.zero_grad()
+ ## Model Forward
+ out, minimize_out, _, _ = self._model(XY) ## Forward pass
+
+ if self._log_internal:
+ internals_dict = {
+ "XY": tensor_to_list(XY),
+ "out": out,
+ "param": self._model.all_parameters,
+ }
+
+ ## Loss Calculation
+ total_loss = 0
+ for ind, (key, value) in enumerate(self._model_def["Minimizers"].items()):
+ if A is not None:
+ A[key].append(minimize_out[value["A"]].detach().numpy())
+ if B is not None:
+ B[key].append(minimize_out[value["B"]].detach().numpy())
+ loss = loss_functions[key](
+ minimize_out[value["A"]], minimize_out[value["B"]]
+ )
+ loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss
+ if total_losses is not None:
+ total_losses[key].append(loss.detach().numpy())
+ aux_losses[ind][idx // batch_size] = loss.item()
+ total_loss += loss
+
+ if self._log_internal:
+ self._save_internal("inout_" + str(idx), internals_dict)
+
+ ## Gradient step
+ if optimizer:
+ total_loss.backward() ## Backpropagate the error
+ optimizer.step()
+ self.visualizer.showWeightsInTrain(batch=idx // batch_size)
+
+ ## return the losses
+ return aux_losses
+
+ def _recurrent_inference(
+ self,
+ data,
+ batch_indexes,
+ batch_size,
+ loss_gains,
+ prediction_samples,
+ step,
+ non_mandatory_inputs,
+ mandatory_inputs,
+ loss_functions,
+ shuffle=False,
+ optimizer=None,
+ total_losses=None,
+ A=None,
+ B=None,
+ idxs=None,
+ ):
+ indexes = copy.deepcopy(batch_indexes)
+ aux_losses = torch.zeros(
+ [
+ len(self._model_def["Minimizers"]),
+ round((len(indexes) + step) / (batch_size + step)),
+ ]
+ )
+ X = {}
+ batch_idx = 0
+ while len(indexes) >= batch_size:
+ selected_indexes = self._get_not_mandatory_inputs(
+ data, X, non_mandatory_inputs, indexes, batch_size, step, shuffle
+ )
+ horizon_losses = {
+ ind: [] for ind in range(len(self._model_def["Minimizers"]))
+ }
+ if optimizer:
+ optimizer.zero_grad() ## Reset the gradient
+
+ for horizon_idx in range(prediction_samples + 1):
+ # Save the indexes
+ if idxs is not None:
+ idxs[horizon_idx].append(
+ [idx + horizon_idx for idx in selected_indexes]
+ )
+ ## Get data
+ for key in mandatory_inputs:
+ X[key] = data[key][[idx + horizon_idx for idx in selected_indexes]]
+ ## Forward pass
+ out, minimize_out, out_closed_loop, out_connect = self._model(X)
+
+ if self._log_internal:
+ internals_dict = {
+ "XY": tensor_to_list(X),
+ "out": out,
+ "param": self._model.all_parameters,
+ "closedLoop": self._model.closed_loop_update,
+ "connect": self._model.connect_update,
+ }
+
+ ## Loss Calculation
+ for ind, (key, value) in enumerate(
+ self._model_def["Minimizers"].items()
+ ):
+ if A is not None:
+ A[key][horizon_idx].append(
+ minimize_out[value["A"]].detach().numpy()
+ )
+ if B is not None:
+ B[key][horizon_idx].append(
+ minimize_out[value["B"]].detach().numpy()
+ )
+ loss = loss_functions[key](
+ minimize_out[value["A"]], minimize_out[value["B"]]
+ )
+ loss = (
+ (loss * loss_gains[key]) if key in loss_gains.keys() else loss
+ )
+ horizon_losses[ind].append(loss)
+
+ ## Update
+ self._update_state(X, out_closed_loop, out_connect)
+
+ if self._log_internal:
+ internals_dict["state"] = self._states
+ self._save_internal(
+ "inout_" + str(batch_idx) + "_" + str(horizon_idx),
+ internals_dict,
+ )
+
+ ## Calculate the total loss
+ total_loss = 0
+ for ind, key in enumerate(self._model_def["Minimizers"].keys()):
+ loss = sum(horizon_losses[ind]) / (prediction_samples + 1)
+ aux_losses[ind][batch_idx] = loss.item()
+ if total_losses is not None:
+ total_losses[key].append(loss.detach().numpy())
+ total_loss += loss
+
+ ## Gradient Step
+ if optimizer:
+ total_loss.backward() ## Backpropagate the error
+ optimizer.step()
+ self.visualizer.showWeightsInTrain(batch=batch_idx)
+ batch_idx += 1
+
+ ## return the losses
+ return aux_losses
+
+ def _setup_recurrent_variables(self, prediction_samples, closed_loop, connect):
+ ## Prediction samples
+ check(
+ prediction_samples == "auto" or prediction_samples >= -1,
+ KeyError,
+ "The sample horizon must be positive, -1, 'auto', for disconnect connection!",
+ )
+ ## Close loop information
+ for input, output in closed_loop.items():
+ check(
+ input in self._model_def["Inputs"],
+ ValueError,
+ f"the tag {input} is not an input variable.",
+ )
+ check(
+ output in self._model_def["Outputs"],
+ ValueError,
+ f"the tag {output} is not an output of the network",
+ )
+ log.info(
+ f"Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples"
+ )
+ if self._input_ns_forward[input] > 0:
+ log.warning(
+ f"Closed loop on variable '{input}' with sample in the future."
+ )
+ ## Connect information
+ for input, output in connect.items():
+ check(
+ input in self._model_def["Inputs"],
+ ValueError,
+ f"the tag {input} is not an input variable.",
+ )
+ check(
+ output in self._model_def["Outputs"],
+ ValueError,
+ f"the tag {output} is not an output of the network",
+ )
+ log.info(
+ f"Recurrent train: connecting the input ports {input} with output ports {output} for {prediction_samples} samples"
+ )
+ if self._input_ns_forward[input] > 0:
+ log.warning(f"Connect on variable '{input}' with sample in the future.")
+ ## Disable recurrent training if there are no recurrent variables
+ if len(connect | closed_loop | self._model_def.recurrentInputs()) == 0:
+ if type(prediction_samples) is not str and prediction_samples >= 0:
+ log.warning(
+ f"The value of the prediction_samples={prediction_samples} but the network has no recurrent variables."
+ )
+ prediction_samples = -1
+ return prediction_samples
+
+ @enforce_types
+ def resetStates(self, states: set = {}, *, batch: int = 1) -> None:
+ """
+ Resets the state of all the recurrent inputs of the network to zero.
+ Parameters
+ ----------
+ states : set, optional
+ A set of recurrent inputs names to reset. If provided, only those inputs will be resetted.
+ batch : int, optional
+ The batch size for the reset states. Default is 1.
+ """
+ if states: ## reset only specific states
+ for key in states:
+ window_size = self._input_n_samples[key]
+ dim = self._model_def["Inputs"][key]["dim"]
+ self._states[key] = torch.zeros(
+ size=(batch, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=False,
+ )
+ else: ## reset all states
+ self._states = {}
+ for key, state in self._model_def.recurrentInputs().items():
+ window_size = self._input_n_samples[key]
+ dim = state["dim"]
+ self._states[key] = torch.zeros(
+ size=(batch, window_size, dim),
+ dtype=TORCH_DTYPE,
+ requires_grad=False,
+ )
diff --git a/nnodely/operators/trainer.py b/src/nnodely/operators/trainer.py
similarity index 53%
rename from nnodely/operators/trainer.py
rename to src/nnodely/operators/trainer.py
index 2fec15e2..95f3e52b 100644
--- a/nnodely/operators/trainer.py
+++ b/src/nnodely/operators/trainer.py
@@ -1,4 +1,7 @@
-import copy, torch, time, inspect
+import copy
+import torch
+import time
+import inspect
from collections.abc import Callable
from functools import wraps
@@ -13,12 +16,17 @@
from nnodely.layers.output import Output
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.INFO)
class Trainer(Network):
def __init__(self):
- check(type(self) is not Trainer, TypeError, "Trainer class cannot be instantiated directly")
+ check(
+ type(self) is not Trainer,
+ TypeError,
+ "Trainer class cannot be instantiated directly",
+ )
super().__init__()
## User Parameters
@@ -31,7 +39,13 @@ def __init__(self):
self.__optimizer = None
@enforce_types
- def addMinimize(self, name:str, streamA:str|Stream|Output, streamB:str|Stream|Output, loss_function:str='mse') -> None:
+ def addMinimize(
+ self,
+ name: str,
+ streamA: str | Stream | Output,
+ streamB: str | Stream | Output,
+ loss_function: str = "mse",
+ ) -> None:
"""
Adds a minimize loss function to the model.
@@ -55,7 +69,7 @@ def addMinimize(self, name:str, streamA:str|Stream|Output, streamB:str|Stream|Ou
self._neuralized = False
@enforce_types
- def removeMinimize(self, name_list:list|str) -> None:
+ def removeMinimize(self, name_list: list | str) -> None:
"""
Removes minimize loss functions using the given list of names.
@@ -72,13 +86,33 @@ def removeMinimize(self, name_list:list|str) -> None:
self._neuralized = False
def __preliminary_checks(self, **kwargs):
- check(self._data_loaded, RuntimeError, 'There is no data loaded! The Training will stop.')
- check('Models' in self._model_def.getJson(), RuntimeError, 'There are no models to train. Load a model using the addModel function.')
- check(list(self._model.parameters()), RuntimeError, 'There are no modules with learnable parameters! The Training will stop.')
- if kwargs.get('train_dataset', None) is None:
- check(kwargs.get('validation_dataset', None) is None, ValueError, 'If train_dataset is None, validation_dataset must also be None.')
- for model in kwargs['models']:
- check(model in kwargs['all_models'], ValueError, f'The model {model} is not in the model definition')
+ check(
+ self._data_loaded,
+ RuntimeError,
+ "There is no data loaded! The Training will stop.",
+ )
+ check(
+ "Models" in self._model_def.getJson(),
+ RuntimeError,
+ "There are no models to train. Load a model using the addModel function.",
+ )
+ check(
+ list(self._model.parameters()),
+ RuntimeError,
+ "There are no modules with learnable parameters! The Training will stop.",
+ )
+ if kwargs.get("train_dataset", None) is None:
+ check(
+ kwargs.get("validation_dataset", None) is None,
+ ValueError,
+ "If train_dataset is None, validation_dataset must also be None.",
+ )
+ for model in kwargs["models"]:
+ check(
+ model in kwargs["all_models"],
+ ValueError,
+ f"The model {model} is not in the model definition",
+ )
def __fill_parameters(func):
@wraps(func)
@@ -89,10 +123,14 @@ def wrapper(self, *args, **kwargs):
# Get standard parameters
standard = self._standard_train_parameters
# Get user_parameters
- users = bound.arguments.get('training_params', None)
+ users = bound.arguments.get("training_params", None)
# Fill missing (None) arguments
for param in sig.parameters.values():
- if param.name == 'self' or param.name == 'lr' or param.name == 'lr_param':
+ if (
+ param.name == "self"
+ or param.name == "lr"
+ or param.name == "lr_param"
+ ):
continue
if bound.arguments.get(param.name, None) is None:
if param.name in users.keys():
@@ -100,35 +138,51 @@ def wrapper(self, *args, **kwargs):
else:
bound.arguments[param.name] = standard.get(param.name, None)
return func(**bound.arguments)
+
return wrapper
- def __initialize_optimizer(self, models, optimizer, training_params, optimizer_params, optimizer_defaults, add_optimizer_defaults, add_optimizer_params, lr, lr_param):
+ def __initialize_optimizer(
+ self,
+ models,
+ optimizer,
+ training_params,
+ optimizer_params,
+ optimizer_defaults,
+ add_optimizer_defaults,
+ add_optimizer_params,
+ lr,
+ lr_param,
+ ):
## Get models
params_to_train = set()
for model in models:
- if type(self._model_def['Models']) is dict:
- params_to_train |= set(self._model_def['Models'][model]['Parameters'])
+ if type(self._model_def["Models"]) is dict:
+ params_to_train |= set(self._model_def["Models"][model]["Parameters"])
else:
- params_to_train |= set(self._model_def['Parameters'].keys())
+ params_to_train |= set(self._model_def["Parameters"].keys())
# Get the optimizer
if type(optimizer) is str:
- if optimizer == 'SGD':
+ if optimizer == "SGD":
optimizer = SGD({}, [])
- elif optimizer == 'Adam':
+ elif optimizer == "Adam":
optimizer = Adam({}, [])
else:
optimizer = copy.deepcopy(optimizer)
- check(issubclass(type(optimizer), Optimizer), TypeError, "The optimizer must be an Optimizer or str")
+ check(
+ issubclass(type(optimizer), Optimizer),
+ TypeError,
+ "The optimizer must be an Optimizer or str",
+ )
optimizer.set_params_to_train(self._model.all_parameters, params_to_train)
- optimizer.add_defaults('lr', self._standard_train_parameters['lr'])
+ optimizer.add_defaults("lr", self._standard_train_parameters["lr"])
- if training_params and 'lr' in training_params:
- optimizer.add_defaults('lr', training_params['lr'])
- if training_params and 'lr_param' in training_params:
- optimizer.add_option_to_params('lr', training_params['lr_param'])
+ if training_params and "lr" in training_params:
+ optimizer.add_defaults("lr", training_params["lr"])
+ if training_params and "lr_param" in training_params:
+ optimizer.add_option_to_params("lr", training_params["lr_param"])
if optimizer_defaults != {}:
optimizer.set_defaults(optimizer_defaults)
@@ -140,21 +194,21 @@ def __initialize_optimizer(self, models, optimizer, training_params, optimizer_p
add_optimizer_params = optimizer.unfold(add_optimizer_params)
for param in add_optimizer_params:
- par = param['params']
- del param['params']
+ par = param["params"]
+ del param["params"]
for key, value in param.items():
optimizer.add_option_to_params(key, {par: value})
# Modify the parameter
- optimizer.add_defaults('lr', lr)
+ optimizer.add_defaults("lr", lr)
if lr_param:
- optimizer.add_option_to_params('lr', lr_param)
+ optimizer.add_option_to_params("lr", lr_param)
self.__optimizer = optimizer
def __initialize_loss(self):
- for name, values in self._model_def['Minimizers'].items():
- self.__loss_functions[name] = CustomLoss(values['loss'])
+ for name, values in self._model_def["Minimizers"].items():
+ self.__loss_functions[name] = CustomLoss(values["loss"])
def getTrainingInfo(self):
"""
@@ -168,72 +222,109 @@ def getTrainingInfo(self):
dict
A dictionary containing the training parameters and information.
"""
- to_remove = ['XY_train','XY_val','XY_test','train_indexes','val_indexes','test_indexes']
- tp = copy.deepcopy({key:value for key, value in self.running_parameters.items() if key not in to_remove})
+ to_remove = [
+ "XY_train",
+ "XY_val",
+ "XY_test",
+ "train_indexes",
+ "val_indexes",
+ "test_indexes",
+ ]
+ tp = copy.deepcopy(
+ {
+ key: value
+ for key, value in self.running_parameters.items()
+ if key not in to_remove
+ }
+ )
## training
- tp['update_per_epochs'] = len(self.running_parameters['train_indexes']) // (tp['train_batch_size'] + tp['step'])
- if tp['prediction_samples'] >= 0: # TODO
- tp['n_first_samples_train'] = len(self.running_parameters['train_indexes'])
- if tp['n_samples_val'] > 0:
- tp['n_first_samples_val'] = len(self.running_parameters['val_indexes'])
- if tp['n_samples_test'] > 0:
- tp['n_first_samples_test'] = len(self.running_parameters['test_indexes'])
-
+ tp["update_per_epochs"] = len(self.running_parameters["train_indexes"]) // (
+ tp["train_batch_size"] + tp["step"]
+ )
+ if tp["prediction_samples"] >= 0: # TODO
+ tp["n_first_samples_train"] = len(self.running_parameters["train_indexes"])
+ if tp["n_samples_val"] > 0:
+ tp["n_first_samples_val"] = len(self.running_parameters["val_indexes"])
+ if tp["n_samples_test"] > 0:
+ tp["n_first_samples_test"] = len(
+ self.running_parameters["test_indexes"]
+ )
## optimizer
- tp['optimizer'] = self.__optimizer.name
- tp['optimizer_defaults'] = self.__optimizer.optimizer_defaults
- tp['optimizer_params'] = self.__optimizer.optimizer_params
+ tp["optimizer"] = self.__optimizer.name
+ tp["optimizer_defaults"] = self.__optimizer.optimizer_defaults
+ tp["optimizer_params"] = self.__optimizer.optimizer_params
## early stopping
- early_stopping = tp['early_stopping']
+ early_stopping = tp["early_stopping"]
if early_stopping:
- tp['early_stopping'] = early_stopping.__name__
+ tp["early_stopping"] = early_stopping.__name__
## Loss functions
- tp['minimizers'] = {}
- for name, values in self._model_def['Minimizers'].items():
- tp['minimizers'][name] = {}
- tp['minimizers'][name]['A'] = values['A']
- tp['minimizers'][name]['B'] = values['B']
- tp['minimizers'][name]['loss'] = values['loss']
- if name in tp['minimize_gain']:
- tp['minimizers'][name]['gain'] = tp['minimize_gain'][name]
+ tp["minimizers"] = {}
+ for name, values in self._model_def["Minimizers"].items():
+ tp["minimizers"][name] = {}
+ tp["minimizers"][name]["A"] = values["A"]
+ tp["minimizers"][name]["B"] = values["B"]
+ tp["minimizers"][name]["loss"] = values["loss"]
+ if name in tp["minimize_gain"]:
+ tp["minimizers"][name]["gain"] = tp["minimize_gain"][name]
return tp
def __check_needed_keys(self, train_data, connect, closed_loop):
# Needed keys
- keys = set(self._model_def['Inputs'].keys())
- keys |= ({value['A'] for value in self._model_def['Minimizers'].values()} | {value['B'] for value in self._model_def['Minimizers'].values()})
+ keys = set(self._model_def["Inputs"].keys())
+ keys |= {value["A"] for value in self._model_def["Minimizers"].values()} | {
+ value["B"] for value in self._model_def["Minimizers"].values()
+ }
# Available keys
- keys -= set(self._model_def['Outputs'].keys()|self._model_def['Relations'].keys())
+ keys -= set(
+ self._model_def["Outputs"].keys() | self._model_def["Relations"].keys()
+ )
keys -= set(self._model_def.recurrentInputs().keys())
- keys -= (set(connect.keys()|closed_loop.keys()))
+ keys -= set(connect.keys() | closed_loop.keys())
# Check if the keys are in the dataset
- check(set(keys).issubset(set(train_data.keys())), KeyError, f"Not all the mandatory keys {keys} are present in the training dataset {set(train_data.keys())}.")
+ check(
+ set(keys).issubset(set(train_data.keys())),
+ KeyError,
+ f"Not all the mandatory keys {keys} are present in the training dataset {set(train_data.keys())}.",
+ )
@enforce_types
@__fill_parameters
- def trainModel(self, *,
- name: str | None = None,
- models: str | list | None = None,
- train_dataset: str | list | dict | None = None, validation_dataset: str | list | dict | None = None,
- dataset: str | list | None = None, splits: list | None = None,
- closed_loop: dict | None = None, connect: dict | None = None, step: int | None = None, prediction_samples: int | None = None,
- shuffle_data: bool | None = None,
- early_stopping: Callable | None = None, early_stopping_params: dict | None = None,
- select_model: Callable | None = None, select_model_params: dict | None = None,
- minimize_gain: dict | None = None,
- num_of_epochs: int = None,
- train_batch_size: int = None, val_batch_size: int = None,
- optimizer: str | Optimizer | None = None,
- lr: int | float | None = None, lr_param: dict | None = None,
- optimizer_params: list | None = None, optimizer_defaults: dict | None = None,
- add_optimizer_params: list | None = None, add_optimizer_defaults: dict | None = None,
- training_params: dict | None = {}
- ) -> None:
+ def trainModel(
+ self,
+ *,
+ name: str | None = None,
+ models: str | list | None = None,
+ train_dataset: str | list | dict | None = None,
+ validation_dataset: str | list | dict | None = None,
+ dataset: str | list | None = None,
+ splits: list | None = None,
+ closed_loop: dict | None = None,
+ connect: dict | None = None,
+ step: int | None = None,
+ prediction_samples: int | None = None,
+ shuffle_data: bool | None = None,
+ early_stopping: Callable | None = None,
+ early_stopping_params: dict | None = None,
+ select_model: Callable | None = None,
+ select_model_params: dict | None = None,
+ minimize_gain: dict | None = None,
+ num_of_epochs: int = None,
+ train_batch_size: int = None,
+ val_batch_size: int = None,
+ optimizer: str | Optimizer | None = None,
+ lr: int | float | None = None,
+ lr_param: dict | None = None,
+ optimizer_params: list | None = None,
+ optimizer_defaults: dict | None = None,
+ add_optimizer_params: list | None = None,
+ add_optimizer_defaults: dict | None = None,
+ training_params: dict | None = {},
+ ) -> None:
"""
Trains the model using the provided datasets and parameters.
@@ -307,21 +398,36 @@ def trainModel(self, *,
.. include:: /examples_basics/trainer_module_ex/trainModel.rst
"""
## Get model for train
- all_models = list(self._model_def['Models'].keys()) if type(self._model_def['Models']) is dict else [self._model_def['Models']]
+ all_models = (
+ list(self._model_def["Models"].keys())
+ if type(self._model_def["Models"]) is dict
+ else [self._model_def["Models"]]
+ )
if models is None:
models = all_models
if isinstance(models, str):
models = [models]
## Preliminary Checks
- self.__preliminary_checks(models = models, all_models = all_models, train_dataset = train_dataset, validation_dataset = validation_dataset)
+ self.__preliminary_checks(
+ models=models,
+ all_models=all_models,
+ train_dataset=train_dataset,
+ validation_dataset=validation_dataset,
+ )
## Recurret variables
- prediction_samples = self._setup_recurrent_variables(prediction_samples, closed_loop, connect)
+ prediction_samples = self._setup_recurrent_variables(
+ prediction_samples, closed_loop, connect
+ )
## Get the dataset
- XY_train, XY_val, XY_test = self._setup_dataset(train_dataset, validation_dataset, None, dataset, splits)
- self.__check_needed_keys(train_data=XY_train, connect=connect, closed_loop=closed_loop)
+ XY_train, XY_val, XY_test = self._setup_dataset(
+ train_dataset, validation_dataset, None, dataset, splits
+ )
+ self.__check_needed_keys(
+ train_data=XY_train, connect=connect, closed_loop=closed_loop
+ )
n_samples_train = next(iter(XY_train.values())).size(0)
n_samples_val = next(iter(XY_val.values())).size(0) if XY_val else 0
@@ -330,7 +436,7 @@ def trainModel(self, *,
if train_dataset is not None:
train_tag = self._get_tag(train_dataset)
val_tag = self._get_tag(validation_dataset)
- else: ## splits is used
+ else: ## splits is used
if dataset is None:
dataset = list(self._data.keys())
tag = self._get_tag(dataset)
@@ -340,22 +446,50 @@ def trainModel(self, *,
train_indexes, val_indexes = [], []
if train_dataset is not None:
- train_indexes, val_indexes = self._get_batch_indexes(train_dataset, n_samples_train, prediction_samples), self._get_batch_indexes(validation_dataset, n_samples_val, prediction_samples)
+ train_indexes, val_indexes = (
+ self._get_batch_indexes(
+ train_dataset, n_samples_train, prediction_samples
+ ),
+ self._get_batch_indexes(
+ validation_dataset, n_samples_val, prediction_samples
+ ),
+ )
else:
dataset = list(self._data.keys()) if dataset is None else dataset
- train_indexes = self._get_batch_indexes(dataset, n_samples_train, prediction_samples)
- check(len(train_indexes) > 0, ValueError,
- 'The number of valid train samples is less than the number of prediction samples.')
+ train_indexes = self._get_batch_indexes(
+ dataset, n_samples_train, prediction_samples
+ )
+ check(
+ len(train_indexes) > 0,
+ ValueError,
+ "The number of valid train samples is less than the number of prediction samples.",
+ )
if n_samples_val > 0:
- val_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val, prediction_samples)
- val_indexes = [i - n_samples_train for i in val_indexes if i >= n_samples_train]
+ val_indexes = self._get_batch_indexes(
+ dataset, n_samples_train + n_samples_val, prediction_samples
+ )
+ val_indexes = [
+ i - n_samples_train for i in val_indexes if i >= n_samples_train
+ ]
if len(val_indexes) < 0:
- log.warning('The number of valid validation samples is less than the number of prediction samples.')
+ log.warning(
+ "The number of valid validation samples is less than the number of prediction samples."
+ )
if n_samples_test > 0:
- test_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val + n_samples_test, prediction_samples)
- test_indexes = [i - (n_samples_train+n_samples_val)for i in test_indexes if i >= (n_samples_train+n_samples_val)]
+ test_indexes = self._get_batch_indexes(
+ dataset,
+ n_samples_train + n_samples_val + n_samples_test,
+ prediction_samples,
+ )
+ test_indexes = [
+ i - (n_samples_train + n_samples_val)
+ for i in test_indexes
+ if i >= (n_samples_train + n_samples_val)
+ ]
if len(test_indexes) < 0:
- log.warning('The number of valid test samples is less than the number of prediction samples.')
+ log.warning(
+ "The number of valid test samples is less than the number of prediction samples."
+ )
## clip batch size and step
train_batch_size = self._clip_batch_size(len(train_indexes), train_batch_size)
@@ -365,32 +499,48 @@ def trainModel(self, *,
val_step = self._clip_step(step, val_indexes, val_batch_size)
## Save the training parameters
- self.running_parameters = {key:value for key,value in locals().items() if key not in ['self', 'kwargs', 'training_params', 'lr', 'lr_param']}
+ self.running_parameters = {
+ key: value
+ for key, value in locals().items()
+ if key not in ["self", "kwargs", "training_params", "lr", "lr_param"]
+ }
## Define the optimizer
- self.__initialize_optimizer(models, optimizer, training_params, optimizer_params, optimizer_defaults, add_optimizer_defaults, add_optimizer_params, lr, lr_param)
+ self.__initialize_optimizer(
+ models,
+ optimizer,
+ training_params,
+ optimizer_params,
+ optimizer_defaults,
+ add_optimizer_defaults,
+ add_optimizer_params,
+ lr,
+ lr_param,
+ )
torch_optimizer = self.__optimizer.get_torch_optimizer()
## Define the loss functions
self.__initialize_loss()
## Define mandatory inputs
- mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(connect, closed_loop)
+ mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(
+ connect, closed_loop
+ )
## Check close loop and connect
self._clean_log_internal()
## Create the train, validation and test loss dictionaries
train_losses, val_losses = {}, {}
- for key in self._model_def['Minimizers'].keys():
+ for key in self._model_def["Minimizers"].keys():
train_losses[key] = []
if n_samples_val > 0:
val_losses[key] = []
## Set the gradient to true if necessary
- model_inputs = self._model_def['Inputs']
+ model_inputs = self._model_def["Inputs"]
for key in model_inputs.keys():
- if 'type' in model_inputs[key]:
+ if "type" in model_inputs[key]:
if key in XY_train:
XY_train[key].requires_grad_(True)
if key in XY_val:
@@ -415,27 +565,64 @@ def trainModel(self, *,
## TRAIN
self._model.train()
if prediction_samples >= 0:
- losses = self._recurrent_inference(XY_train, train_indexes, train_batch_size, minimize_gain, prediction_samples, train_step, non_mandatory_inputs, mandatory_inputs, self.__loss_functions, shuffle=shuffle_data, optimizer=torch_optimizer)
+ losses = self._recurrent_inference(
+ XY_train,
+ train_indexes,
+ train_batch_size,
+ minimize_gain,
+ prediction_samples,
+ train_step,
+ non_mandatory_inputs,
+ mandatory_inputs,
+ self.__loss_functions,
+ shuffle=shuffle_data,
+ optimizer=torch_optimizer,
+ )
else:
- losses = self._inference(XY_train, n_samples_train, train_batch_size, minimize_gain, self.__loss_functions, shuffle=shuffle_data, optimizer=torch_optimizer)
+ losses = self._inference(
+ XY_train,
+ n_samples_train,
+ train_batch_size,
+ minimize_gain,
+ self.__loss_functions,
+ shuffle=shuffle_data,
+ optimizer=torch_optimizer,
+ )
## save the losses
- for ind, key in enumerate(self._model_def['Minimizers'].keys()):
+ for ind, key in enumerate(self._model_def["Minimizers"].keys()):
train_losses[key].append(torch.mean(losses[ind]).tolist())
if n_samples_val > 0:
## VALIDATION
self._model.eval()
setted_log_internal = self._log_internal
- self._set_log_internal(False) # TODO To remove when the function is moved outside the train
+ self._set_log_internal(
+ False
+ ) # TODO To remove when the function is moved outside the train
if prediction_samples >= 0:
- losses = self._recurrent_inference(XY_val, val_indexes, val_batch_size, minimize_gain, prediction_samples, val_step,
- non_mandatory_inputs, mandatory_inputs, self.__loss_functions)
+ losses = self._recurrent_inference(
+ XY_val,
+ val_indexes,
+ val_batch_size,
+ minimize_gain,
+ prediction_samples,
+ val_step,
+ non_mandatory_inputs,
+ mandatory_inputs,
+ self.__loss_functions,
+ )
else:
- losses = self._inference(XY_val, n_samples_val, val_batch_size, minimize_gain, self.__loss_functions)
+ losses = self._inference(
+ XY_val,
+ n_samples_val,
+ val_batch_size,
+ minimize_gain,
+ self.__loss_functions,
+ )
self._set_log_internal(setted_log_internal)
## save the losses
- for ind, key in enumerate(self._model_def['Minimizers'].keys()):
+ for ind, key in enumerate(self._model_def["Minimizers"].keys()):
val_losses[key].append(torch.mean(losses[ind]).tolist())
if callable(select_model):
@@ -446,7 +633,9 @@ def trainModel(self, *,
## Early-stopping
if callable(early_stopping):
if early_stopping(train_losses, val_losses, early_stopping_params):
- log.info(f'Stopping the training at epoch {epoch} due to early stopping.')
+ log.info(
+ f"Stopping the training at epoch {epoch} due to early stopping."
+ )
break
## Visualize the training...
@@ -457,10 +646,10 @@ def trainModel(self, *,
end = time.time()
self.visualizer.showTrainingTime(end - start)
- for key in self._model_def['Minimizers'].keys():
- self._training[key] = {'train': train_losses[key]}
+ for key in self._model_def["Minimizers"].keys():
+ self._training[key] = {"train": train_losses[key]}
if n_samples_val > 0:
- self._training[key]['val'] = val_losses[key]
+ self._training[key]["val"] = val_losses[key]
self.visualizer.showEndTraining(num_of_epochs - 1, train_losses, val_losses)
## Select the model
@@ -469,11 +658,13 @@ def trainModel(self, *,
# The model selected is updated for the last time by the final batch;
# so the minimum loss (selected model) is referred to the model before the last update.
# If the batch is small compared to the dataset dimension the differences in the model are small.
- log.warning('If not validation set is provided the selected model can differ from the optimal.')
- log.info(f'Selected the model at the epoch {best_model_epoch + 1}.')
+ log.warning(
+ "If not validation set is provided the selected model can differ from the optimal."
+ )
+ log.info(f"Selected the model at the epoch {best_model_epoch + 1}.")
self._model = Model(selected_model_def)
else:
- log.info('The selected model is the LAST model of the training.')
+ log.info("The selected model is the LAST model of the training.")
## Remove virtual states
self._remove_virtual_states(connect, closed_loop)
@@ -481,5 +672,6 @@ def trainModel(self, *,
## Get trained model from torch and set the model_def
self._model_def.updateParameters(self._model)
-#from 685
-#from 840
\ No newline at end of file
+
+# from 685
+# from 840
diff --git a/nnodely/operators/validator.py b/src/nnodely/operators/validator.py
similarity index 52%
rename from nnodely/operators/validator.py
rename to src/nnodely/operators/validator.py
index a2bdc583..a8e38b49 100644
--- a/nnodely/operators/validator.py
+++ b/src/nnodely/operators/validator.py
@@ -1,16 +1,22 @@
-import torch, warnings
+import torch
+import warnings
import numpy as np
from nnodely.support.utils import ReadOnlyDict, get_batch_size
from nnodely.basic.loss import CustomLoss
from nnodely.operators.network import Network
-from nnodely.support.utils import check, TORCH_DTYPE, enforce_types
+from nnodely.support.utils import check, enforce_types
+
class Validator(Network):
@enforce_types
def __init__(self):
- check(type(self) is not Validator, TypeError, "Validator class cannot be instantiated directly")
+ check(
+ type(self) is not Validator,
+ TypeError,
+ "Validator class cannot be instantiated directly",
+ )
super().__init__()
# Validation Parameters
@@ -26,18 +32,23 @@ def prediction(self):
return ReadOnlyDict(self.__prediction)
@enforce_types
- def _analyze(self,
- dataset: dict,
- dataset_tag: str,
- indexes: list = None,
- minimize_gain: dict = {},
- closed_loop: dict = {},
- connect: dict = {},
- prediction_samples: int | str = 0,
- step: int = 0,
- batch_size: int | None = None
- ) -> None:
- with torch.enable_grad() if self._get_gradient_on_inference() else torch.inference_mode():
+ def _analyze(
+ self,
+ dataset: dict,
+ dataset_tag: str,
+ indexes: list = None,
+ minimize_gain: dict = {},
+ closed_loop: dict = {},
+ connect: dict = {},
+ prediction_samples: int | str = 0,
+ step: int = 0,
+ batch_size: int | None = None,
+ ) -> None:
+ with (
+ torch.enable_grad()
+ if self._get_gradient_on_inference()
+ else torch.inference_mode()
+ ):
self._model.eval()
self.__performance[dataset_tag] = {}
self.__prediction[dataset_tag] = {}
@@ -48,36 +59,52 @@ def _analyze(self,
# Create the losses
losses = {}
- for name, values in self._model_def['Minimizers'].items():
- losses[name] = CustomLoss(values['loss'])
+ for name, values in self._model_def["Minimizers"].items():
+ losses[name] = CustomLoss(values["loss"])
- #data = self._get_data(dataset)
+ # data = self._get_data(dataset)
n_samples = len(dataset[list(dataset.keys())[0]])
batch_size = get_batch_size(n_samples, batch_size, prediction_samples)
- prediction_samples = self._setup_recurrent_variables(prediction_samples, closed_loop, connect)
+ prediction_samples = self._setup_recurrent_variables(
+ prediction_samples, closed_loop, connect
+ )
if prediction_samples >= 0:
- mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(connect,closed_loop)
+ mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(
+ connect, closed_loop
+ )
idxs = []
for horizon_idx in range(prediction_samples + 1):
idxs.append([])
- for key, value in self._model_def['Minimizers'].items():
+ for key, value in self._model_def["Minimizers"].items():
total_losses[key], A[key], B[key] = [], [], []
for horizon_idx in range(prediction_samples + 1):
A[key].append([])
B[key].append([])
## Update with virtual states
- self._model.update(closed_loop = closed_loop, connect = connect)
- self._recurrent_inference(dataset, indexes, batch_size, minimize_gain, prediction_samples,
- step, non_mandatory_inputs, mandatory_inputs, losses,
- total_losses = total_losses, A = A, B = B, idxs = idxs)
+ self._model.update(closed_loop=closed_loop, connect=connect)
+ self._recurrent_inference(
+ dataset,
+ indexes,
+ batch_size,
+ minimize_gain,
+ prediction_samples,
+ step,
+ non_mandatory_inputs,
+ mandatory_inputs,
+ losses,
+ total_losses=total_losses,
+ A=A,
+ B=B,
+ idxs=idxs,
+ )
for horizon_idx in range(prediction_samples + 1):
idxs[horizon_idx] = np.concatenate(idxs[horizon_idx])
- for key, value in self._model_def['Minimizers'].items():
+ for key, value in self._model_def["Minimizers"].items():
for horizon_idx in range(prediction_samples + 1):
if A is not None:
A[key][horizon_idx] = np.concatenate(A[key][horizon_idx])
@@ -86,65 +113,102 @@ def _analyze(self,
if total_losses is not None:
total_losses[key] = np.mean(total_losses[key])
else:
- for key, value in self._model_def['Minimizers'].items():
+ for key, value in self._model_def["Minimizers"].items():
total_losses[key], A[key], B[key] = [], [], []
self._model.update(disconnect=True)
- self._inference(dataset, n_samples, batch_size, minimize_gain, losses,
- total_losses = total_losses, A = A, B = B)
-
- for key, value in self._model_def['Minimizers'].items():
+ self._inference(
+ dataset,
+ n_samples,
+ batch_size,
+ minimize_gain,
+ losses,
+ total_losses=total_losses,
+ A=A,
+ B=B,
+ )
+
+ for key, value in self._model_def["Minimizers"].items():
A[key] = np.concatenate(A[key])
B[key] = np.concatenate(B[key])
total_losses[key] = np.mean(total_losses[key])
- for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()):
+ for ind, (key, value) in enumerate(self._model_def["Minimizers"].items()):
A_np = np.array(A[key])
B_np = np.array(B[key])
self.__performance[dataset_tag][key] = {}
- self.__performance[dataset_tag][key][value['loss']] = np.mean(total_losses[key]).item()
- self.__performance[dataset_tag][key]['fvu'] = {}
+ self.__performance[dataset_tag][key][value["loss"]] = np.mean(
+ total_losses[key]
+ ).item()
+ self.__performance[dataset_tag][key]["fvu"] = {}
# Compute FVU
residual = A_np - B_np
error_var = np.var(residual)
error_mean = np.mean(residual)
- #error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.__prediction['B'][ind]) - 0)
- #print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}")
+ # error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.__prediction['B'][ind]) - 0)
+ # print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}")
with warnings.catch_warnings(record=True) as w:
- self.__performance[dataset_tag][key]['fvu']['A'] = (error_var / np.var(A_np)).item()
- self.__performance[dataset_tag][key]['fvu']['B'] = (error_var / np.var(B_np)).item()
- if w and np.var(A_np) == 0.0 and np.var(B_np) == 0.0:
- self.__performance[dataset_tag][key]['fvu']['A'] = np.nan
- self.__performance[dataset_tag][key]['fvu']['B'] = np.nan
- self.__performance[dataset_tag][key]['fvu']['total'] = np.mean([self.__performance[dataset_tag][key]['fvu']['A'],self.__performance[dataset_tag][key]['fvu']['B']]).item()
+ self.__performance[dataset_tag][key]["fvu"]["A"] = (
+ error_var / np.var(A_np)
+ ).item()
+ self.__performance[dataset_tag][key]["fvu"]["B"] = (
+ error_var / np.var(B_np)
+ ).item()
+ if w and np.var(A_np) == 0.0 and np.var(B_np) == 0.0:
+ self.__performance[dataset_tag][key]["fvu"]["A"] = np.nan
+ self.__performance[dataset_tag][key]["fvu"]["B"] = np.nan
+ self.__performance[dataset_tag][key]["fvu"]["total"] = np.mean(
+ [
+ self.__performance[dataset_tag][key]["fvu"]["A"],
+ self.__performance[dataset_tag][key]["fvu"]["B"],
+ ]
+ ).item()
# Compute AIC
- #normal_dist = norm(0, error_var ** 0.5)
- #probability_of_residual = normal_dist.pdf(residual)
- #log_likelihood_first = sum(np.log(probability_of_residual))
- p1 = -len(residual)/2.0*np.log(2*np.pi)
+ # normal_dist = norm(0, error_var ** 0.5)
+ # probability_of_residual = normal_dist.pdf(residual)
+ # log_likelihood_first = sum(np.log(probability_of_residual))
+ p1 = -len(residual) / 2.0 * np.log(2 * np.pi)
with warnings.catch_warnings(record=True) as w:
- p2 = -len(residual)/2.0*np.log(error_var)
- p3 = -1 / (2.0 * error_var) * np.sum(residual ** 2)
+ p2 = -len(residual) / 2.0 * np.log(error_var)
+ p3 = -1 / (2.0 * error_var) * np.sum(residual**2)
if w and p2 == np.float32(np.inf) and p3 == np.float32(-np.inf):
p2 = p3 = 0.0
- log_likelihood = p1+p2+p3
- #print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}")
- total_params = sum(p.numel() for p in self._model.parameters() if p.requires_grad)
- #print(f"{key} total_params:{total_params}")
- aic = - 2 * log_likelihood + 2 * total_params
- #print(f"{key} aic:{aic}")
- self.__performance[dataset_tag][key]['aic'] = {'value':aic,'total_params':total_params,'log_likelihood':log_likelihood}
+ log_likelihood = p1 + p2 + p3
+ # print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}")
+ total_params = sum(
+ p.numel() for p in self._model.parameters() if p.requires_grad
+ )
+ # print(f"{key} total_params:{total_params}")
+ aic = -2 * log_likelihood + 2 * total_params
+ # print(f"{key} aic:{aic}")
+ self.__performance[dataset_tag][key]["aic"] = {
+ "value": aic,
+ "total_params": total_params,
+ "log_likelihood": log_likelihood,
+ }
# Prediction and target
self.__prediction[dataset_tag][key] = {}
- self.__prediction[dataset_tag][key]['A'] = A_np.tolist()
- self.__prediction[dataset_tag][key]['B'] = B_np.tolist()
+ self.__prediction[dataset_tag][key]["A"] = A_np.tolist()
+ self.__prediction[dataset_tag][key]["B"] = B_np.tolist()
if idxs is not None:
- self.__prediction[dataset_tag]['idxs'] = np.array(idxs).tolist()
- self.__performance[dataset_tag]['total'] = {}
- self.__performance[dataset_tag]['total']['mean_error'] = np.mean([value for key,value in total_losses.items()])
- self.__performance[dataset_tag]['total']['fvu'] = np.mean([self.__performance[dataset_tag][key]['fvu']['total'] for key in self._model_def['Minimizers'].keys()])
- self.__performance[dataset_tag]['total']['aic'] = np.mean([self.__performance[dataset_tag][key]['aic']['value']for key in self._model_def['Minimizers'].keys()])
+ self.__prediction[dataset_tag]["idxs"] = np.array(idxs).tolist()
+ self.__performance[dataset_tag]["total"] = {}
+ self.__performance[dataset_tag]["total"]["mean_error"] = np.mean(
+ [value for key, value in total_losses.items()]
+ )
+ self.__performance[dataset_tag]["total"]["fvu"] = np.mean(
+ [
+ self.__performance[dataset_tag][key]["fvu"]["total"]
+ for key in self._model_def["Minimizers"].keys()
+ ]
+ )
+ self.__performance[dataset_tag]["total"]["aic"] = np.mean(
+ [
+ self.__performance[dataset_tag][key]["aic"]["value"]
+ for key in self._model_def["Minimizers"].keys()
+ ]
+ )
self.visualizer.showResult(dataset_tag)
@@ -185,13 +249,12 @@ def _analyze(self,
# batch_size :
# The batch size use for analyse the performance of the model on the provided dataset.
-
# """
# # Get the dataset if is None take all datasets
# if dataset is None:
# dataset = list(self._data.keys())
- # data = self._get_data(dataset)
+ # data = self._get_data(dataset)
# n_samples = len(data[list(data.keys())[0]])
# data_tag = self._get_tag(dataset) if name is None else name
# indexes = list(range(n_samples))
@@ -219,19 +282,20 @@ def _analyze(self,
# if n_samples_test > 0:
# self._analyze(data_test, f"{data_tag}_test", indexes, minimize_gain, closed_loop, connect, prediction_samples, step, batch_size)
-
@enforce_types
- def analyzeModel(self,
- dataset: str | list | dict | None = None, *,
- tag: str | None = None,
- splits: list | None = None,
- minimize_gain: dict = {},
- closed_loop: dict = {},
- connect: dict = {},
- prediction_samples: int | str = 0,
- step: int = 0,
- batch_size: int | None = None
- ) -> None:
+ def analyzeModel(
+ self,
+ dataset: str | list | dict | None = None,
+ *,
+ tag: str | None = None,
+ splits: list | None = None,
+ minimize_gain: dict = {},
+ closed_loop: dict = {},
+ connect: dict = {},
+ prediction_samples: int | str = 0,
+ step: int = 0,
+ batch_size: int | None = None,
+ ) -> None:
"""
The function is used to analyze the performance of the model on the provided dataset.
@@ -258,13 +322,15 @@ def analyzeModel(self,
"""
# Get the dataset if is None take all datasets
if tag is None:
- tag = dataset if isinstance(dataset, str) else 'default'
+ tag = dataset if isinstance(dataset, str) else "default"
if dataset is None:
dataset = list(self._data.keys())
- if splits: ## splits is used
+ if splits: ## splits is used
## Get the dataset
- XY_train, XY_val, XY_test = self._setup_dataset(None, None, None, dataset, splits)
+ XY_train, XY_val, XY_test = self._setup_dataset(
+ None, None, None, dataset, splits
+ )
n_samples_train = next(iter(XY_train.values())).size(0)
n_samples_val = next(iter(XY_val.values())).size(0) if XY_val else 0
n_samples_test = next(iter(XY_test.values())).size(0) if XY_test else 0
@@ -274,31 +340,82 @@ def analyzeModel(self,
test_tag = f"{tag}_test" if n_samples_test > 0 else None
train_indexes, val_indexes = [], []
- train_indexes = self._get_batch_indexes(dataset, n_samples_train, prediction_samples)
- check(len(train_indexes) > 0, ValueError,
- 'The number of valid train samples is less than the number of prediction samples.')
+ train_indexes = self._get_batch_indexes(
+ dataset, n_samples_train, prediction_samples
+ )
+ check(
+ len(train_indexes) > 0,
+ ValueError,
+ "The number of valid train samples is less than the number of prediction samples.",
+ )
if n_samples_val > 0:
- val_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val, prediction_samples)
- val_indexes = [i - n_samples_train for i in val_indexes if i >= n_samples_train]
+ val_indexes = self._get_batch_indexes(
+ dataset, n_samples_train + n_samples_val, prediction_samples
+ )
+ val_indexes = [
+ i - n_samples_train for i in val_indexes if i >= n_samples_train
+ ]
if n_samples_test > 0:
- test_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val + n_samples_test, prediction_samples)
- test_indexes = [i - (n_samples_train+n_samples_val)for i in test_indexes if i >= (n_samples_train+n_samples_val)]
+ test_indexes = self._get_batch_indexes(
+ dataset,
+ n_samples_train + n_samples_val + n_samples_test,
+ prediction_samples,
+ )
+ test_indexes = [
+ i - (n_samples_train + n_samples_val)
+ for i in test_indexes
+ if i >= (n_samples_train + n_samples_val)
+ ]
## Training set Results
- self._analyze(XY_train, dataset_tag=train_tag, indexes=train_indexes, minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=step, batch_size=batch_size)
+ self._analyze(
+ XY_train,
+ dataset_tag=train_tag,
+ indexes=train_indexes,
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=step,
+ batch_size=batch_size,
+ )
## Validation set Results
if n_samples_val > 0:
- self._analyze(XY_val, dataset_tag=val_tag, indexes=val_indexes, minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=step, batch_size=batch_size)
+ self._analyze(
+ XY_val,
+ dataset_tag=val_tag,
+ indexes=val_indexes,
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=step,
+ batch_size=batch_size,
+ )
## Test set Results
if n_samples_test > 0:
- self._analyze(XY_test, dataset_tag=test_tag, indexes=test_indexes, minimize_gain=minimize_gain,
- closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples,
- step=step, batch_size=batch_size)
+ self._analyze(
+ XY_test,
+ dataset_tag=test_tag,
+ indexes=test_indexes,
+ minimize_gain=minimize_gain,
+ closed_loop=closed_loop,
+ connect=connect,
+ prediction_samples=prediction_samples,
+ step=step,
+ batch_size=batch_size,
+ )
else:
- data = self._get_data(dataset)
+ data = self._get_data(dataset)
n_samples = next(iter(data.values())).size(0)
indexes = self._get_batch_indexes(dataset, n_samples, prediction_samples)
- self._analyze(data, tag, indexes, minimize_gain, closed_loop, connect, prediction_samples, step, batch_size)
\ No newline at end of file
+ self._analyze(
+ data,
+ tag,
+ indexes,
+ minimize_gain,
+ closed_loop,
+ connect,
+ prediction_samples,
+ step,
+ batch_size,
+ )
diff --git a/nnodely/support/__init__.py b/src/nnodely/support/__init__.py
similarity index 100%
rename from nnodely/support/__init__.py
rename to src/nnodely/support/__init__.py
diff --git a/nnodely/support/earlystopping.py b/src/nnodely/support/earlystopping.py
similarity index 83%
rename from nnodely/support/earlystopping.py
rename to src/nnodely/support/earlystopping.py
index dae6cd2e..b65c6a12 100644
--- a/nnodely/support/earlystopping.py
+++ b/src/nnodely/support/earlystopping.py
@@ -22,20 +22,24 @@ def early_stop_patience(train_losses, val_losses, params):
bool
True if training should be stopped early, False otherwise.
"""
- patience = params['patience'] if 'patience' in params.keys() else 50
+ patience = params["patience"] if "patience" in params.keys() else 50
if val_losses:
losses = val_losses
else:
# if there is no validation set, use the training losses
losses = train_losses
- if 'error' in params.keys():
+ if "error" in params.keys():
# if the type of loss to be used is provided by the user
- losses_use = losses[params['error']]
+ losses_use = losses[params["error"]]
else:
# take the mean of all the losses for all the keys of the dictionary
import numpy as np
- losses_use = [np.mean([losses[key][index] for key in losses.keys()]) for index in range(len(losses[list(losses.keys())[0]]))]
+
+ losses_use = [
+ np.mean([losses[key][index] for key in losses.keys()])
+ for index in range(len(losses[list(losses.keys())[0]]))
+ ]
if len(losses_use) > patience:
# index of the minimum validation loss
min_val_loss_index = losses_use.index(min(losses_use))
@@ -69,8 +73,12 @@ def select_best_model(train_losses, val_losses, params):
# if there is no validation set, use the training losses
losses = train_losses
import numpy as np
- losses_use = [np.mean([losses[key][index] for key in losses.keys()]) for index in range(len(losses[list(losses.keys())[0]]))]
- if len(losses_use)-1 == losses_use.index(min(losses_use)):
+
+ losses_use = [
+ np.mean([losses[key][index] for key in losses.keys()])
+ for index in range(len(losses[list(losses.keys())[0]]))
+ ]
+ if len(losses_use) - 1 == losses_use.index(min(losses_use)):
return True
else:
return False
@@ -94,9 +102,11 @@ def mean_stopping(train_losses, val_losses, params):
bool
True if training should be stopped early, False otherwise.
"""
- tol = params['tol'] if 'tol' in params.keys() else 0.001
+ tol = params["tol"] if "tol" in params.keys() else 0.001
if val_losses:
- for (train_loss_name, train_loss_value), (val_loss_name, val_loss_value) in zip(train_losses.items(), val_losses.items()):
+ for (train_loss_name, train_loss_value), (val_loss_name, val_loss_value) in zip(
+ train_losses.items(), val_losses.items()
+ ):
if abs(train_loss_value[-1] - val_loss_value[-1]) < tol:
return True
else:
@@ -105,6 +115,7 @@ def mean_stopping(train_losses, val_losses, params):
return True
return False
+
def standard_early_stopping(train_losses, val_losses, params):
"""
Determines whether to stop training early based on training and validation losses.
@@ -123,12 +134,14 @@ def standard_early_stopping(train_losses, val_losses, params):
bool
True if training should be stopped early, False otherwise.
"""
- n = params['tol'] if 'tol' in params.keys() else 10
+ n = params["tol"] if "tol" in params.keys() else 10
if val_losses:
- for (_, train_loss_value), (_, val_loss_value) in zip(train_losses.items(), val_losses.items()):
+ for (_, train_loss_value), (_, val_loss_value) in zip(
+ train_losses.items(), val_losses.items()
+ ):
if (len(train_loss_value) <= n) and (len(val_loss_value) <= n):
return False
-
+
tol = 0.0
for train_loss, val_loss in zip(train_loss_value[-n:], val_loss_value[-n:]):
if abs(train_loss - val_loss) > tol:
@@ -137,13 +150,12 @@ def standard_early_stopping(train_losses, val_losses, params):
return False
else:
for _, loss_value in train_losses.items():
- if (len(loss_value) <= n):
+ if len(loss_value) <= n:
return False
-
+
tol = loss_value[-n]
- for loss in loss_value[-n+1:]:
+ for loss in loss_value[-n + 1 :]:
if loss < tol:
return False
-
+
return True
-
\ No newline at end of file
diff --git a/src/nnodely/support/fixstepsolver.py b/src/nnodely/support/fixstepsolver.py
new file mode 100644
index 00000000..7f45a1b4
--- /dev/null
+++ b/src/nnodely/support/fixstepsolver.py
@@ -0,0 +1,48 @@
+from nnodely.layers.parameter import SampleTime
+
+
+class FixedStepSolver:
+ def __init__(self, int_name: str | None = None, der_name: str | None = None):
+ self.dt = SampleTime()
+ self.int_name = int_name
+ self.der_name = der_name
+
+
+class Euler(FixedStepSolver):
+ def __init__(self, int_name: str | None = None, der_name: str | None = None):
+ super().__init__(int_name, der_name)
+
+ def integrate(self, obj):
+ from nnodely.layers.input import Input
+
+ integral = Input(self.int_name, dimensions=obj.dim["dim"])
+ return (integral.last() + obj * self.dt).closedLoop(integral)
+
+ def derivate(self, obj):
+ from nnodely.layers.input import Input
+
+ obj = Input(self.int_name, dimensions=obj.dim["dim"]).connect(obj)
+ return (obj.last() - obj.sw([-2, -1])) / self.dt
+
+
+class Trapezoidal(FixedStepSolver):
+ def __init__(self, int_name: str | None = None, der_name: str | None = None):
+ super().__init__(int_name, der_name)
+
+ def integrate(self, obj):
+ from nnodely.layers.input import Input
+
+ integral = Input(self.int_name, dimensions=obj.dim["dim"])
+ obj = Input(self.der_name, dimensions=obj.dim["dim"]).connect(obj)
+ return (
+ integral.last() + (obj.last() + obj.sw([-2, -1])) * 0.5 * self.dt
+ ).closedLoop(integral)
+
+ def derivate(self, obj):
+ from nnodely.layers.input import Input
+
+ obj = Input(self.int_name, dimensions=obj.dim["dim"]).connect(obj)
+ derivative = Input(self.der_name, dimensions=obj.dim["dim"])
+ return (
+ ((obj.last() - obj.sw([-2, -1])) * 2.0) / self.dt - derivative.last()
+ ).closedLoop(derivative)
diff --git a/nnodely/support/initializer.py b/src/nnodely/support/initializer.py
similarity index 63%
rename from nnodely/support/initializer.py
rename to src/nnodely/support/initializer.py
index 133d495e..10704e0b 100644
--- a/nnodely/support/initializer.py
+++ b/src/nnodely/support/initializer.py
@@ -1,5 +1,4 @@
-
-def init_constant(indexes, params_size, dict_param = {'value':1}):
+def init_constant(indexes, params_size, dict_param={"value": 1}):
"""
Initializes parameters to a constant value.
@@ -10,9 +9,12 @@ def init_constant(indexes, params_size, dict_param = {'value':1}):
value : int or float
The constant value to initialize the parameters with.
"""
- return dict_param['value']
+ return dict_param["value"]
+
-def init_negexp(indexes, params_size, dict_param = {'size_index':0, 'first_value':1, 'lambda':3}):
+def init_negexp(
+ indexes, params_size, dict_param={"size_index": 0, "first_value": 1, "lambda": 3}
+):
"""
Initializes parameters using a negative decay exponential function.
@@ -32,12 +34,27 @@ def init_negexp(indexes, params_size, dict_param = {'size_index':0, 'first_value
The decay rate parameter of the exponential function.
"""
import numpy as np
- size_index = dict_param['size_index']
+
+ size_index = dict_param["size_index"]
# check if the size of the list of parameters is 1, to avoid a division by zero
- x = 1 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1)
- return dict_param['first_value']*np.exp(-dict_param['lambda']*(1-x))
+ x = (
+ 1
+ if params_size[size_index] - 1 == 0
+ else indexes[size_index] / (params_size[size_index] - 1)
+ )
+ return dict_param["first_value"] * np.exp(-dict_param["lambda"] * (1 - x))
-def init_exp(indexes, params_size, dict_param = {'size_index':0, 'max_value':1, 'lambda':3, 'monotonicity':'decreasing'}):
+
+def init_exp(
+ indexes,
+ params_size,
+ dict_param={
+ "size_index": 0,
+ "max_value": 1,
+ "lambda": 3,
+ "monotonicity": "decreasing",
+ },
+):
"""
Initializes parameters using an increasing or decreasing exponential function.
@@ -64,21 +81,37 @@ def init_exp(indexes, params_size, dict_param = {'size_index':0, 'max_value':1,
If the monotonicity is not 'increasing' or 'decreasing'.
"""
import numpy as np
- size_index = dict_param['size_index']
- monotonicity = dict_param['monotonicity']
- if monotonicity == 'increasing':
+
+ size_index = dict_param["size_index"]
+ monotonicity = dict_param["monotonicity"]
+ if monotonicity == "increasing":
# increasing exponential, the 'max_value' is the value at x=1, i.e, at the end of the range
- x = 1 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1)
- out = dict_param['max_value']*np.exp(dict_param['lambda']*(x-1))
- elif monotonicity == 'decreasing':
+ x = (
+ 1
+ if params_size[size_index] - 1 == 0
+ else indexes[size_index] / (params_size[size_index] - 1)
+ )
+ out = dict_param["max_value"] * np.exp(dict_param["lambda"] * (x - 1))
+ elif monotonicity == "decreasing":
# decreasing exponential, the 'max_value' is the value at x=0, i.e, at the beginning of the range
- x = 0 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1)
- out = dict_param['max_value']*np.exp(-dict_param['lambda']*x)
+ x = (
+ 0
+ if params_size[size_index] - 1 == 0
+ else indexes[size_index] / (params_size[size_index] - 1)
+ )
+ out = dict_param["max_value"] * np.exp(-dict_param["lambda"] * x)
else:
- raise ValueError('The parameter monotonicity must be either increasing or decreasing.')
+ raise ValueError(
+ "The parameter monotonicity must be either increasing or decreasing."
+ )
return out
-def init_lin(indexes, params_size, dict_param = {'size_index':0, 'first_value':1, 'last_value':0}):
+
+def init_lin(
+ indexes,
+ params_size,
+ dict_param={"size_index": 0, "first_value": 1, "last_value": 0},
+):
"""
Initializes parameters using a linear function.
@@ -97,6 +130,12 @@ def init_lin(indexes, params_size, dict_param = {'size_index':0, 'first_value':1
last_value : int or float
The value at the end of the range.
"""
- size_index = dict_param['size_index']
- x = 0 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1)
- return (dict_param['last_value'] - dict_param['first_value']) * x + dict_param['first_value']
+ size_index = dict_param["size_index"]
+ x = (
+ 0
+ if params_size[size_index] - 1 == 0
+ else indexes[size_index] / (params_size[size_index] - 1)
+ )
+ return (dict_param["last_value"] - dict_param["first_value"]) * x + dict_param[
+ "first_value"
+ ]
diff --git a/src/nnodely/support/jsonutils.py b/src/nnodely/support/jsonutils.py
new file mode 100644
index 00000000..f331a939
--- /dev/null
+++ b/src/nnodely/support/jsonutils.py
@@ -0,0 +1,678 @@
+import copy
+from pprint import pformat
+
+
+from nnodely.support.utils import check
+
+from nnodely.support.logger import logging, nnLogger
+
+log = nnLogger(__name__, logging.WARNING)
+
+
+def get_window(obj):
+ return "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None)
+
+
+# Codice per comprimere le relazioni
+# print(self.json['Relations'])
+# used_rel = {string for values in self.json['Relations'].values() for string in values[1]}
+# if obj1.name not in used_rel and obj1.name in self.json['Relations'].keys() and self.json['Relations'][obj1.name][0] == add_relation_name:
+# self.json['Relations'][self.name] = [add_relation_name, self.json['Relations'][obj1.name][1]+[obj2.name]]
+# del self.json['Relations'][obj1.name]
+# else:
+# Devo aggiungere un operazione che rimuove un operazione di Add,Sub,Mul,Div se può essere unita ad un'altra operazione dello stesso tipo
+#
+def merge(source, destination, main=True):
+ if main:
+ for key, value in destination["Functions"].items():
+ if (
+ key in source["Functions"].keys()
+ and "n_input" in value.keys()
+ and "n_input" in source["Functions"][key].keys()
+ ):
+ check(
+ value == {}
+ or source["Functions"][key] == {}
+ or value["n_input"] == source["Functions"][key]["n_input"],
+ TypeError,
+ f"The ParamFun {key} is present multiple times, with different number of inputs. "
+ f"The ParamFun {key} is called with {value['n_input']} parameters and with {source['Functions'][key]['n_input']} parameters.",
+ )
+ for key, value in destination["Parameters"].items():
+ if key in source["Parameters"].keys():
+ if "dim" in value.keys() and "dim" in source["Parameters"][key].keys():
+ check(
+ value["dim"] == source["Parameters"][key]["dim"],
+ TypeError,
+ f"The Parameter {key} is present multiple times, with different dimensions. "
+ f"The Parameter {key} is called with {value['dim']} dimension and with {source['Parameters'][key]['dim']} dimension.",
+ )
+ window_dest = (
+ "tw" if "tw" in value else ("sw" if "sw" in value else None)
+ )
+ window_source = (
+ "tw"
+ if "tw" in source["Parameters"][key]
+ else ("sw" if "sw" in source["Parameters"][key] else None)
+ )
+ if window_dest is not None:
+ check(
+ window_dest == window_source
+ and value[window_dest]
+ == source["Parameters"][key][window_source],
+ TypeError,
+ f"The Parameter {key} is present multiple times, with different window. "
+ f"The Parameter {key} is called with {window_dest}={value[window_dest]} dimension and with {window_source}={source['Parameters'][key][window_source]} dimension.",
+ )
+
+ log.debug("Merge Source")
+ log.debug("\n" + pformat(source))
+ log.debug("Merge Destination")
+ log.debug("\n" + pformat(destination))
+ result = copy.deepcopy(destination)
+ else:
+ result = destination
+ for key, value in source.items():
+ if isinstance(value, dict):
+ # get node or create one
+ node = result.setdefault(key, {})
+ merge(value, node, False)
+ else:
+ if key in result and type(result[key]) is list:
+ if key == "tw" or key == "sw":
+ if result[key][0] > value[0]:
+ result[key][0] = value[0]
+ if result[key][1] < value[1]:
+ result[key][1] = value[1]
+ else:
+ result[key] = value
+ if main == True:
+ log.debug("Merge Result")
+ log.debug("\n" + pformat(result))
+ return result
+
+
+def get_models_json(json):
+ model_json = {}
+ model_json["Parameters"] = list(json["Parameters"].keys())
+ model_json["Constants"] = list(json["Constants"].keys())
+ model_json["Inputs"] = list(json["Inputs"].keys())
+ model_json["Outputs"] = list(json["Outputs"].keys())
+ model_json["Functions"] = list(json["Functions"].keys())
+ model_json["Relations"] = list(json["Relations"].keys())
+ return model_json
+
+
+def check_model(json):
+ all_inputs = json["Inputs"].keys()
+ all_outputs = json["Outputs"].keys()
+
+ from nnodely.basic.relation import MAIN_JSON
+
+ subjson = MAIN_JSON
+ for name in all_outputs:
+ subjson = merge(subjson, subjson_from_output(json, name))
+ needed_inputs = subjson["Inputs"].keys()
+ extenal_inputs = set(all_inputs) - set(needed_inputs)
+
+ check(
+ all_inputs == needed_inputs,
+ RuntimeError,
+ f"Connect or close loop operation on the inputs {list(extenal_inputs)}, that are not used in the model.",
+ )
+ return json
+
+
+def binary_cheks(self, obj1, obj2, name):
+ from nnodely.basic.relation import Stream, toStream
+
+ obj1, obj2 = toStream(obj1), toStream(obj2)
+ check(
+ type(obj1) is Stream,
+ TypeError,
+ f"The type of {obj1} is {type(obj1)} and is not supported for add operation.",
+ )
+ check(
+ type(obj2) is Stream,
+ TypeError,
+ f"The type of {obj2} is {type(obj2)} and is not supported for add operation.",
+ )
+ window_obj1 = get_window(obj1)
+ window_obj2 = get_window(obj2)
+ if window_obj1 is not None and window_obj2 is not None:
+ check(
+ window_obj1 == window_obj2,
+ TypeError,
+ f"For {name} the time window type must match or None but they were {window_obj1} and {window_obj2}.",
+ )
+ check(
+ obj1.dim[window_obj1] == obj2.dim[window_obj2],
+ ValueError,
+ f"For {name} the time window must match or None but they were {window_obj1}={obj1.dim[window_obj1]} and {window_obj2}={obj2.dim[window_obj2]}.",
+ )
+ check(
+ obj1.dim["dim"] == obj2.dim["dim"]
+ or obj1.dim == {"dim": 1}
+ or obj2.dim == {"dim": 1},
+ ValueError,
+ f"For {name} the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.",
+ )
+ dim = obj1.dim | obj2.dim
+ dim["dim"] = max(obj1.dim["dim"], obj2.dim["dim"])
+ return obj1, obj2, dim
+
+
+def subjson_from_relation(json, relation):
+ json = copy.deepcopy(json)
+ # Get all the inputs needed to compute a specific relation from the json graph
+ inputs = set()
+ relations = set()
+ constants = set()
+ parameters = set()
+ functions = set()
+
+ def search(rel):
+ if rel in json["Inputs"]: # Found an input
+ inputs.add(rel)
+ if rel in json["Inputs"]:
+ if (
+ "connect" in json["Inputs"][rel]
+ and json["Inputs"][rel]["local"] == 1
+ ):
+ search(json["Inputs"][rel]["connect"])
+ if (
+ "closed_loop" in json["Inputs"][rel]
+ and json["Inputs"][rel]["local"] == 1
+ ):
+ search(json["Inputs"][rel]["closed_loop"])
+ # if 'init' in json['Inputs'][rel]:
+ # search(json['Inputs'][rel]['init'])
+ elif rel in json["Constants"]: # Found a constant or parameter
+ constants.add(rel)
+ elif rel in json["Parameters"]:
+ parameters.add(rel)
+ elif rel in json["Functions"]:
+ functions.add(rel)
+ if "params_and_consts" in json["Functions"][rel]:
+ for sub_rel in json["Functions"][rel]["params_and_consts"]:
+ search(sub_rel)
+ elif rel in json["Relations"]: # Another relation
+ relations.add(rel)
+ for sub_rel in json["Relations"][rel][1]:
+ search(sub_rel)
+ for sub_rel in json["Relations"][rel][2:]:
+ if json["Relations"][rel][0] in ("Fir", "Linear"):
+ search(sub_rel)
+ if json["Relations"][rel][0] in ("Fuzzify"):
+ search(sub_rel)
+ if json["Relations"][rel][0] in ("ParamFun"):
+ search(sub_rel)
+
+ search(relation)
+ from nnodely.basic.relation import MAIN_JSON
+
+ sub_json = copy.deepcopy(MAIN_JSON)
+ sub_json["Relations"] = {
+ key: value for key, value in json["Relations"].items() if key in relations
+ }
+ sub_json["Inputs"] = {
+ key: value for key, value in json["Inputs"].items() if key in inputs
+ }
+ sub_json["Constants"] = {
+ key: value for key, value in json["Constants"].items() if key in constants
+ }
+ sub_json["Parameters"] = {
+ key: value for key, value in json["Parameters"].items() if key in parameters
+ }
+ sub_json["Functions"] = {
+ key: value for key, value in json["Functions"].items() if key in functions
+ }
+ sub_json["Outputs"] = {}
+ sub_json["Info"] = {}
+ return sub_json
+
+
+def subjson_from_output(json, outputs: str | list):
+ json = copy.deepcopy(json)
+ from nnodely.basic.relation import MAIN_JSON
+
+ sub_json = copy.deepcopy(MAIN_JSON)
+ if type(outputs) is str:
+ outputs = [outputs]
+ for output in outputs:
+ sub_json = merge(sub_json, subjson_from_relation(json, json["Outputs"][output]))
+ sub_json["Outputs"][output] = json["Outputs"][output]
+ return sub_json
+
+
+def subjson_from_model(json, models: str | list):
+ from nnodely.basic.relation import MAIN_JSON
+
+ json = copy.deepcopy(json)
+ sub_json = copy.deepcopy(MAIN_JSON)
+ models_names = (
+ set([json["Models"]])
+ if type(json["Models"]) is str
+ else set(json["Models"].keys())
+ )
+ if type(models) is str or len(models) == 1:
+ if len(models) == 1:
+ models = models[0]
+ check(models in models_names, AttributeError, f"Model [{models}] not found!")
+ if type(json["Models"]) is str:
+ outputs = set(json["Outputs"].keys())
+ else:
+ outputs = set(json["Models"][models]["Outputs"])
+ sub_json["Models"] = models
+ else:
+ outputs = set()
+ sub_json["Models"] = {}
+ for model in models:
+ check(model in models_names, AttributeError, f"Model [{model}] not found!")
+ outputs |= set(json["Models"][model]["Outputs"])
+ sub_json["Models"][model] = {
+ key: value for key, value in json["Models"][model].items()
+ }
+
+ # Remove the extern connections not keys in the graph
+ final_json = merge(sub_json, subjson_from_output(json, outputs))
+ for key, value in final_json["Inputs"].items():
+ if "connect" in value and (
+ value["local"] == 0
+ and value["connect"] not in final_json["Relations"].keys()
+ ):
+ del final_json["Inputs"][key]["connect"]
+ del final_json["Inputs"][key]["local"]
+ log.warning(
+ f'The input {key} is "connect" outside the model connection removed for subjson'
+ )
+ if "closedLoop" in value and (
+ value["local"] == 0
+ and value["closedLoop"] not in final_json["Relations"].keys()
+ ):
+ del final_json["Inputs"][key]["closedLoop"]
+ del final_json["Inputs"][key]["local"]
+ log.warning(
+ f'The input {key} is "closedLoop" outside the model connection removed for subjson'
+ )
+ return final_json
+
+
+def subjson_from_minimize(json, minimizers: str | list):
+ from nnodely.basic.relation import MAIN_JSON
+
+ json = copy.deepcopy(json)
+ sub_json = copy.deepcopy(MAIN_JSON)
+
+ if "Minimizers" in json:
+ rel_A = [json["Minimizers"][key]["A"] for key in minimizers]
+ rel_B = [json["Minimizers"][key]["B"] for key in minimizers]
+ relations_name = set(rel_A) | set(rel_B)
+ for rel_name in relations_name:
+ minimizers_json = subjson_from_relation(json, rel_name)
+ sub_json = merge(sub_json, minimizers_json)
+ sub_json["Minimizers"] = {key: json["Minimizers"][key] for key in minimizers}
+
+ return sub_json
+
+
+def stream_to_str(obj, type="Stream"):
+ from nnodely.visualizer.emptyvisualizer import color, GREEN
+ from pprint import pformat
+
+ stream = f" {type} "
+ stream_name = f" {obj.name} {obj.dim} "
+
+ title = color((stream).center(80, "="), GREEN, True)
+ json = color(pformat(obj.json), GREEN)
+ stream = color((stream_name).center(80, "-"), GREEN, True)
+ return title + "\n" + json + "\n" + stream
+
+
+def plot_structure(json, filename="nnodely_graph", library="matplotlib", view=True):
+ # json = self.modely.json if json is None else json
+ # if json is None:
+ # raise ValueError("No JSON model definition provided. Please provide a valid JSON model definition.")
+ if library not in ["matplotlib", "graphviz"]:
+ raise ValueError("Invalid library specified. Use 'matplotlib' or 'graphviz'.")
+ if library == "matplotlib":
+ plot_matplotlib_structure(json, filename, view=view)
+ elif library == "graphviz":
+ plot_graphviz_structure(json, filename, view=view)
+
+
+def plot_matplotlib_structure(json, filename="nnodely_graph", view=True):
+ import matplotlib.pyplot as plt
+ from matplotlib import patches
+ from matplotlib.lines import Line2D
+
+ layer_positions = {}
+ x, y = 0, 0 # Initial position
+ dy, dx = 1.5, 2.5 # Spacing
+
+ ## Layer Inputs:
+ for input_name, input_type in json["Inputs"].items():
+ layer_positions[input_name] = (x, y)
+ y -= dy
+ for constant_name in json["Constants"].keys():
+ layer_positions[constant_name] = (x, y)
+ y -= dy
+ y_limit = abs(y)
+
+ # Layers Relations:
+ available_inputs = list(json["Inputs"].keys() | json["Constants"].keys())
+ available_outputs = list(set(json["Outputs"].values()))
+ while available_outputs:
+ x += dx
+ y = 0
+ inputs_to_add, outputs_to_remove = [], []
+ for relation_name, (relation_type, dependencies, *_) in json[
+ "Relations"
+ ].items():
+ if all(dep in available_inputs for dep in dependencies) and (
+ relation_name not in available_inputs
+ ):
+ inputs_to_add.append(relation_name)
+ if relation_name in available_outputs:
+ outputs_to_remove.append(relation_name)
+ layer_positions[relation_name] = (x, y)
+ y -= dy
+ y_limit = max(y_limit, abs(y))
+ available_inputs.extend(inputs_to_add)
+ available_outputs = [
+ out for out in available_outputs if out not in outputs_to_remove
+ ]
+
+ ## Layer Outputs:
+ x += dx
+ y = 0
+ for idx, output_name in enumerate(json["Outputs"].keys()):
+ layer_positions[output_name] = (x, y)
+ y -= dy # Move down for the next input
+ x_limit = abs(x)
+ y_limit = max(y_limit, abs(y))
+
+ # Create the plot
+ fig, ax = plt.subplots(figsize=(x_limit, y_limit))
+ # fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
+
+ # Plot rectangles for each layer
+ colors, labels = (
+ ["lightgreen", "lightblue", "orange", "lightgray"],
+ ["Inputs", "Relations", "Outputs", "Constants"],
+ )
+ legend_info = [
+ patches.Patch(facecolor=color, edgecolor="black", label=label)
+ for color, label in zip(colors, labels)
+ ]
+ for layer in (
+ json["Inputs"].keys()
+ | json["Outputs"].keys()
+ | json["Relations"].keys()
+ | json["Constants"].keys()
+ ):
+ x1, y1 = layer_positions[layer]
+ if layer in json["Inputs"].keys():
+ color = "lightgreen"
+ tag = f"{layer}\ndim: {json['Inputs'][layer]['dim']}\nWindow: {json['Inputs'][layer]['ntot']}"
+ elif layer in json["Outputs"].keys():
+ color = "orange"
+ tag = layer
+ elif layer in json["Constants"].keys():
+ color = "lightgray"
+ tag = f"{layer}\ndim: {json['Constants'][layer]['dim']}"
+ else:
+ color = "lightblue"
+ tag = f"{json['Relations'][layer][0]}\n({layer})"
+ rect = patches.Rectangle((x1, y1), 2, 1, edgecolor="black", facecolor=color)
+ ax.add_patch(rect)
+ ax.text(
+ x1 + 1,
+ y1 + 0.5,
+ f"{tag}",
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Draw arrows for dependencies
+ for layer, (_, dependencies, *_) in json["Relations"].items():
+ x1, y1 = layer_positions[layer] # Get position of the current layer
+ for dep in dependencies:
+ if dep in layer_positions:
+ x2, y2 = layer_positions[dep] # Get position of the dependent layer
+ ax.annotate(
+ "",
+ xy=(x1, y1),
+ xytext=(x2 + 2, y2 + 0.5),
+ arrowprops=dict(arrowstyle="->", color="black", lw=1),
+ )
+ for out_name, rel_name in json["Outputs"].items():
+ x1, y1 = layer_positions[out_name]
+ x2, y2 = layer_positions[rel_name]
+ ax.annotate(
+ "",
+ xy=(x1, y1 + 0.5),
+ xytext=(x2 + 2, y2 + 0.5),
+ arrowprops=dict(arrowstyle="->", color="black", lw=1),
+ )
+ for key, state in json["Inputs"].items():
+ if "closedLoop" in state.keys():
+ x1, y1 = layer_positions[key]
+ x2, y2 = layer_positions[state["closedLoop"]]
+ # ax.annotate("", xy=(x2+1, y2), xytext=(x2+1, y_limit), arrowprops=dict(arrowstyle="-", color='red', lw=1, linestyle='dashed'))
+ ax.add_patch(
+ patches.FancyArrowPatch(
+ (x2 + 1, y2),
+ (x2 + 1, -y_limit),
+ arrowstyle="-",
+ mutation_scale=15,
+ color="red",
+ linestyle="dashed",
+ )
+ )
+ ax.add_patch(
+ patches.FancyArrowPatch(
+ (x2 + 1, -y_limit),
+ (x1 - 1, -y_limit),
+ arrowstyle="-",
+ mutation_scale=15,
+ color="red",
+ linestyle="dashed",
+ )
+ )
+ ax.add_patch(
+ patches.FancyArrowPatch(
+ (x1 - 1, -y_limit),
+ (x1 - 1, y1 + 0.5),
+ arrowstyle="-",
+ mutation_scale=15,
+ color="red",
+ linestyle="dashed",
+ )
+ )
+ ax.add_patch(
+ patches.FancyArrowPatch(
+ (x1 - 1, y1 + 0.5),
+ (x1, y1 + 0.5),
+ arrowstyle="->",
+ mutation_scale=15,
+ color="red",
+ linestyle="dashed",
+ )
+ )
+ elif "connect" in state.keys():
+ x1, y1 = layer_positions[key]
+ x2, y2 = layer_positions[state["connect"]]
+ ax.add_patch(
+ patches.FancyArrowPatch(
+ (x1, y1),
+ (x2, y2),
+ arrowstyle="->",
+ mutation_scale=15,
+ color="green",
+ linestyle="dashed",
+ )
+ )
+
+ legend_info.extend(
+ [
+ Line2D([0], [0], color="black", lw=2, label="Dependency"),
+ Line2D(
+ [0], [0], color="red", lw=2, linestyle="dashed", label="Closed Loop"
+ ),
+ Line2D([0], [0], color="green", lw=2, linestyle="dashed", label="Connect"),
+ ]
+ )
+
+ # Adjust the plot limits
+ ax.set_xlim(-dx, x_limit + dx)
+ ax.set_ylim(-y_limit, dy)
+ ax.set_aspect("equal")
+ ax.legend(handles=legend_info, loc="lower right")
+ ax.axis("off") # Hide axes
+
+ plt.title(
+ f"Neural Network Diagram - Sampling [{json['Info']['SampleTime']}]",
+ fontsize=12,
+ fontweight="bold",
+ )
+ ## Save the figure
+ plt.savefig(filename, format="png", bbox_inches="tight")
+ if view:
+ plt.show()
+
+
+def plot_graphviz_structure(
+ json, filename="nnodely_graph", view=True
+): # pragma: no cover
+ import shutil
+ from graphviz import view
+ from graphviz import Digraph
+
+ # Check if Graphviz is installed
+ if shutil.which("dot") is None:
+ # raise RuntimeError(
+ # "Graphviz does not appear to be installed on your system. "
+ # "Please install it from https://graphviz.org/download/"
+ # )
+ log.warning(
+ "Graphviz does not appear to be installed on your system. "
+ "Please install it from https://graphviz.org/download/"
+ )
+ return
+
+ dot = Digraph(comment="Structured Neural Network")
+
+ # Set graph attributes for top-down layout and style
+ dot.attr(rankdir="LR", size="21")
+ dot.attr(
+ "node", shape="box", style="filled", color="lightgray", fontname="Helvetica"
+ )
+
+ # Add metadata/info box
+ if "Info" in json:
+ info = json["Info"]
+ info_text = "\n".join([f"{k}: {v}" for k, v in info.items()])
+ dot.node(
+ "INFO_BOX",
+ label=f"Model Info\n{info_text}",
+ shape="note",
+ fillcolor="white",
+ fontsize="10",
+ )
+
+ # Add input nodes
+ for inp, data in json["Inputs"].items():
+ dim = data["dim"]
+ window = data["sw"] if "sw" in data else data["tw"]
+ window_tag = "sw" if "sw" in data else "tw"
+ label = f"{inp}\nDim: {dim}\nWindow({window_tag}): {window}"
+ dot.node(inp, label=label, fillcolor="lightgreen")
+ if "connect" in data.keys():
+ dot.edge(
+ data["connect"], inp, label="connect", color="blue", fontcolor="blue"
+ )
+ if "closedLoop" in data.keys():
+ dot.edge(
+ data["closedLoop"],
+ inp,
+ label="closedLoop",
+ color="red",
+ fontcolor="red",
+ )
+
+ # Add constant nodes
+ if "Constants" in json:
+ for const, data in json["Constants"].items():
+ dim = data["dim"]
+ label = f"{const}\nDim: {dim}"
+ dot.node(const, label=label, fillcolor="lightgray")
+
+ # Add relation nodes
+ for name, rel in json["Relations"].items():
+ op_type = rel[0]
+ parents = rel[1]
+ param1 = rel[2] if len(rel) > 2 else None
+ param2 = rel[3] if len(rel) > 3 else None
+ label = f"{name}\nType: {op_type}"
+ dot.node(name, label=label, fillcolor="lightblue")
+ for i in [param1, param2]:
+ if isinstance(i, str):
+ if i in json["Parameters"]:
+ param_dim = json["Parameters"][i]["dim"]
+ dot.node(
+ i,
+ label=f"{i}\nDim: {param_dim}",
+ shape="ellipse",
+ fillcolor="orange",
+ )
+ dot.edge(
+ i, name, label="Parameter", color="orange", fontcolor="orange"
+ )
+ elif i in json["Functions"]:
+ dot.node(
+ i, label=f"{param1}", shape="ellipse", fillcolor="darkorange"
+ )
+ dot.edge(
+ i,
+ name,
+ label="function",
+ color="darkorange",
+ fontcolor="darkorange",
+ )
+ for parent in parents:
+ dot.edge(parent, name)
+
+ # Add output nodes
+ for out, rel in json["Outputs"].items():
+ dot.node(out, fillcolor="lightcoral")
+ dot.edge(rel, out)
+
+ # Add Minimize nodes if present
+ if "Minimizers" in json:
+ for name, rel in json["Minimizers"].items():
+ rel_a, rel_b = rel["A"], rel["B"]
+ loss = rel["loss"]
+ dot.node(
+ name, label=f"{name}\nLoss:{loss}", shape="ellipse", fillcolor="purple"
+ )
+ dot.edge(rel_a, name, label="Minimize", color="purple", fontcolor="purple")
+ dot.edge(rel_b, name, label="Minimize", color="purple", fontcolor="purple")
+
+ # Add a legend as a subgraph
+ # with dot.subgraph(name='cluster_legend') as legend:
+ # legend.attr(label='Legend', style='dashed')
+ # legend.node('LegendInput', 'Inputs', shape='box', fillcolor='lightgreen', style='filled')
+ # legend.node('LegendRel', 'Relation', shape='box', fillcolor='lightblue', style='filled')
+ # legend.node('LegendOutput', 'Outputs', shape='box', fillcolor='lightcoral', style='filled')
+ # # Hide the edges inside the legend box
+ # legend.attr('edge', style='invis')
+ # legend.edge('LegendInput', 'LegendRel')
+ # legend.edge('LegendRel', 'LegendOutput')
+
+ # Render the graph
+ dot.render(
+ filename=filename, view=view, format="svg"
+ ) # opens in default viewer and saves as SVG
diff --git a/nnodely/support/logger.py b/src/nnodely/support/logger.py
similarity index 64%
rename from nnodely/support/logger.py
rename to src/nnodely/support/logger.py
index a94f3f21..f519fbe9 100644
--- a/nnodely/support/logger.py
+++ b/src/nnodely/support/logger.py
@@ -3,9 +3,9 @@
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
-#The background is set with 40 plus the number of the color, and the foreground with 30
+# The background is set with 40 plus the number of the color, and the foreground with 30
-#These are the sequences need to get colored ouput
+# These are the sequences need to get colored ouput
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[%dm"
COLOR_BOLD_SEQ = "\033[1;%dm"
@@ -18,7 +18,7 @@
logging.INFO: BLUE,
logging.WARNING: YELLOW,
logging.CRITICAL: RED,
- logging.ERROR: RED
+ logging.ERROR: RED,
}
LEVEL_STRING = {
logging.DEBUG: "DEBUG",
@@ -26,15 +26,16 @@
logging.WARNING: "WARNING",
logging.CRITICAL: "CRITICAL",
logging.ERROR: "ERROR",
- SUPPRESS: "SUPPRESS"
+ SUPPRESS: "SUPPRESS",
}
LOG_LEVEL = logging.INFO
class JsonFormatter(logging.Formatter):
- FORMAT = "[%(levelname)s][%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s" # + ""
+ FORMAT = "[%(levelname)s][%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s" # + ""
FORMAT_WARNING = "[%(funcName)s] %(message)s"
FORMAT_INFO = "%(message)s"
+
def __init__(self):
logging.Formatter.__init__(self, self.FORMAT)
@@ -54,39 +55,49 @@ def format(self, record):
class nnLogger(logging.Logger):
levels = []
loggers = []
- params = {'level':None}
+ params = {"level": None}
+
def __init__(self, name, level):
logging.Logger.__init__(self, name)
self.setLevel(max(level, LOG_LEVEL))
- #file = logging.FileHandler('example.log')
- #color_formatter = ColoredFormatter(self.COLOR_FORMAT)
+ # file = logging.FileHandler('example.log')
+ # color_formatter = ColoredFormatter(self.COLOR_FORMAT)
self.console = logging.StreamHandler(sys.stdout)
color_formatter = JsonFormatter()
self.console.setFormatter(color_formatter)
- #self.console.setLevel(logging.CRITICAL)
+ # self.console.setLevel(logging.CRITICAL)
- #logging.getLogger().addHandler(self.console)
+ # logging.getLogger().addHandler(self.console)
self.addHandler(self.console)
self.loggers.append(self)
self.levels.append(level)
- #self.addHandler(file)
+ # self.addHandler(file)
def setAllLevel(self, level):
- if self.params['level'] is None or self.params['level'] != level:
- self._log(logging.INFO,
- COLOR_SEQ % (30 + BLUE) + (f" Loggers to {LEVEL_STRING[level]} ").center(80, '=') + RESET_SEQ, None)
- self.params['level'] = level
+ if self.params["level"] is None or self.params["level"] != level:
+ self._log(
+ logging.INFO,
+ COLOR_SEQ % (30 + BLUE)
+ + (f" Loggers to {LEVEL_STRING[level]} ").center(80, "=")
+ + RESET_SEQ,
+ None,
+ )
+ self.params["level"] = level
for ind, logger in enumerate(self.loggers):
logger.setLevel(level)
def resetAllLevel(self):
- if self.params['level'] != 0:
- self._log(logging.INFO, COLOR_SEQ % (30 + BLUE) + (" Standard Level Log ").center(80, '=') + RESET_SEQ, None)
- self.params['level'] = None
+ if self.params["level"] != 0:
+ self._log(
+ logging.INFO,
+ COLOR_SEQ % (30 + BLUE)
+ + (" Standard Level Log ").center(80, "=")
+ + RESET_SEQ,
+ None,
+ )
+ self.params["level"] = None
for ind, logger in enumerate(self.loggers):
logger.setLevel(self.levels[ind])
-
-
diff --git a/nnodely/support/mathutils.py b/src/nnodely/support/mathutils.py
similarity index 88%
rename from nnodely/support/mathutils.py
rename to src/nnodely/support/mathutils.py
index 6e6dca45..0663b1de 100644
--- a/nnodely/support/mathutils.py
+++ b/src/nnodely/support/mathutils.py
@@ -1,17 +1,22 @@
import torch
+
def argmax_max(iterable):
return max(enumerate(iterable), key=lambda x: x[1])
+
def argmin_min(iterable):
return min(enumerate(iterable), key=lambda x: x[1])
+
def argmax_dict(iterable: dict):
return max(iterable.items(), key=lambda x: x[1])
+
def argmin_dict(iterable: dict):
return min(iterable.items(), key=lambda x: x[1])
+
# Linear interpolation function, operating on batches of input data and returning batches of output data
def linear_interp(x, x_data, y_data):
# Inputs:
@@ -28,5 +33,7 @@ def linear_interp(x, x_data, y_data):
idx = torch.argmin(torch.abs(x_data[:-1] - x), dim=1)
# Linear interpolation
- y = y_data[idx] + (y_data[idx + 1] - y_data[idx]) / (x_data[idx + 1] - x_data[idx]) * (x - x_data[idx])
- return y
\ No newline at end of file
+ y = y_data[idx] + (y_data[idx + 1] - y_data[idx]) / (
+ x_data[idx + 1] - x_data[idx]
+ ) * (x - x_data[idx])
+ return y
diff --git a/nnodely/support/odeint/__init__.py b/src/nnodely/support/odeint/__init__.py
similarity index 100%
rename from nnodely/support/odeint/__init__.py
rename to src/nnodely/support/odeint/__init__.py
diff --git a/nnodely/support/odeint/adjoint.py b/src/nnodely/support/odeint/adjoint.py
similarity index 66%
rename from nnodely/support/odeint/adjoint.py
rename to src/nnodely/support/odeint/adjoint.py
index b68b7f14..f3525704 100644
--- a/nnodely/support/odeint/adjoint.py
+++ b/src/nnodely/support/odeint/adjoint.py
@@ -2,14 +2,35 @@
import torch
import torch.nn as nn
from nnodely.support.odeint.my_odeint import SOLVERS, odeint
-from nnodely.support.odeint.utils import _check_inputs, _flat_to_shape, _mixed_norm, _all_callback_names, _all_adjoint_callback_names
+from nnodely.support.odeint.utils import (
+ _check_inputs,
+ _flat_to_shape,
+ _mixed_norm,
+ _all_callback_names,
+ _all_adjoint_callback_names,
+)
class OdeintAdjointMethod(torch.autograd.Function):
-
@staticmethod
- def forward(ctx, shapes, func, y0, t, rtol, atol, method, options, event_fn, adjoint_rtol, adjoint_atol, adjoint_method,
- adjoint_options, t_requires_grad, *adjoint_params):
+ def forward(
+ ctx,
+ shapes,
+ func,
+ y0,
+ t,
+ rtol,
+ atol,
+ method,
+ options,
+ event_fn,
+ adjoint_rtol,
+ adjoint_atol,
+ adjoint_method,
+ adjoint_options,
+ t_requires_grad,
+ *adjoint_params,
+ ):
ctx.shapes = shapes
ctx.func = func
@@ -21,7 +42,16 @@ def forward(ctx, shapes, func, y0, t, rtol, atol, method, options, event_fn, adj
ctx.event_mode = event_fn is not None
with torch.no_grad():
- ans = odeint(func, y0, t, rtol=rtol, atol=atol, method=method, options=options, event_fn=event_fn)
+ ans = odeint(
+ func,
+ y0,
+ t,
+ rtol=rtol,
+ atol=atol,
+ method=method,
+ options=options,
+ event_fn=event_fn,
+ )
if event_fn is None:
y = ans
@@ -61,8 +91,14 @@ def backward(ctx, *grad_y):
##################################
# [-1] because y and grad_y are both of shape (len(t), *y0.shape)
- aug_state = [torch.zeros((), dtype=y.dtype, device=y.device), y[-1], grad_y[-1]] # vjp_t, y, vjp_y
- aug_state.extend([torch.zeros_like(param) for param in adjoint_params]) # vjp_params
+ aug_state = [
+ torch.zeros((), dtype=y.dtype, device=y.device),
+ y[-1],
+ grad_y[-1],
+ ] # vjp_t, y, vjp_y
+ aug_state.extend(
+ [torch.zeros_like(param) for param in adjoint_params]
+ ) # vjp_params
##################################
# Set up backward ODE func #
@@ -89,23 +125,32 @@ def augmented_dynamics(t, y_aug):
# Workaround for PyTorch bug #39784
_t = torch.as_strided(t, (), ()) # noqa
_y = torch.as_strided(y, (), ()) # noqa
- _params = tuple(torch.as_strided(param, (), ()) for param in adjoint_params) # noqa
+ _params = tuple(
+ torch.as_strided(param, (), ()) for param in adjoint_params
+ ) # noqa
vjp_t, vjp_y, *vjp_params = torch.autograd.grad(
- func_eval, (t, y) + adjoint_params, -adj_y,
- allow_unused=True, retain_graph=True
+ func_eval,
+ (t, y) + adjoint_params,
+ -adj_y,
+ allow_unused=True,
+ retain_graph=True,
)
# autograd.grad returns None if no gradient, set to zero.
vjp_t = torch.zeros_like(t) if vjp_t is None else vjp_t
vjp_y = torch.zeros_like(y) if vjp_y is None else vjp_y
- vjp_params = [torch.zeros_like(param) if vjp_param is None else vjp_param
- for param, vjp_param in zip(adjoint_params, vjp_params)]
+ vjp_params = [
+ torch.zeros_like(param) if vjp_param is None else vjp_param
+ for param, vjp_param in zip(adjoint_params, vjp_params)
+ ]
return (vjp_t, func_eval, vjp_y, *vjp_params)
# Add adjoint callbacks
- for callback_name, adjoint_callback_name in zip(_all_callback_names, _all_adjoint_callback_names):
+ for callback_name, adjoint_callback_name in zip(
+ _all_callback_names, _all_adjoint_callback_names
+ ):
try:
callback = getattr(func, adjoint_callback_name)
except AttributeError:
@@ -132,36 +177,78 @@ def augmented_dynamics(t, y_aug):
# Run the augmented system backwards in time.
aug_state = odeint(
- augmented_dynamics, tuple(aug_state),
- t[i - 1:i + 1].flip(0),
- rtol=adjoint_rtol, atol=adjoint_atol, method=adjoint_method, options=adjoint_options
+ augmented_dynamics,
+ tuple(aug_state),
+ t[i - 1 : i + 1].flip(0),
+ rtol=adjoint_rtol,
+ atol=adjoint_atol,
+ method=adjoint_method,
+ options=adjoint_options,
)
aug_state = [a[1] for a in aug_state] # extract just the t[i - 1] value
- aug_state[1] = y[i - 1] # update to use our forward-pass estimate of the state
- aug_state[2] += grad_y[i - 1] # update any gradients wrt state at this time point
+ aug_state[1] = y[
+ i - 1
+ ] # update to use our forward-pass estimate of the state
+ aug_state[2] += grad_y[
+ i - 1
+ ] # update any gradients wrt state at this time point
if t_requires_grad:
time_vjps[0] = aug_state[0]
# Only compute gradient wrt initial time when in event handling mode.
if event_mode and t_requires_grad:
- time_vjps = torch.cat([time_vjps[0].reshape(-1), torch.zeros_like(_t[1:])])
+ time_vjps = torch.cat(
+ [time_vjps[0].reshape(-1), torch.zeros_like(_t[1:])]
+ )
adj_y = aug_state[2]
adj_params = aug_state[3:]
- return (None, None, adj_y, time_vjps, None, None, None, None, None, None, None, None, None, None, *adj_params)
-
-
-def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None,
- adjoint_rtol=None, adjoint_atol=None, adjoint_method=None, adjoint_options=None, adjoint_params=None):
+ return (
+ None,
+ None,
+ adj_y,
+ time_vjps,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ *adj_params,
+ )
+
+
+def odeint_adjoint(
+ func,
+ y0,
+ t,
+ *,
+ rtol=1e-7,
+ atol=1e-9,
+ method=None,
+ options=None,
+ event_fn=None,
+ adjoint_rtol=None,
+ adjoint_atol=None,
+ adjoint_method=None,
+ adjoint_options=None,
+ adjoint_params=None,
+):
# We need this in order to access the variables inside this module,
# since we have no other way of getting variables along the execution path.
if adjoint_params is None and not isinstance(func, nn.Module):
- raise ValueError('func must be an instance of nn.Module to specify the adjoint parameters; alternatively they '
- 'can be specified explicitly via the `adjoint_params` argument. If there are no parameters '
- 'then it is allowable to set `adjoint_params=()`.')
+ raise ValueError(
+ "func must be an instance of nn.Module to specify the adjoint parameters; alternatively they "
+ "can be specified explicitly via the `adjoint_params` argument. If there are no parameters "
+ "then it is allowable to set `adjoint_params=()`."
+ )
# Must come before _check_inputs as we don't want to use normalised input (in particular any changes to options)
if adjoint_rtol is None:
@@ -172,11 +259,17 @@ def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=No
adjoint_method = method
if adjoint_method != method and options is not None and adjoint_options is None:
- raise ValueError("If `adjoint_method != method` then we cannot infer `adjoint_options` from `options`. So as "
- "`options` has been passed then `adjoint_options` must be passed as well.")
+ raise ValueError(
+ "If `adjoint_method != method` then we cannot infer `adjoint_options` from `options`. So as "
+ "`options` has been passed then `adjoint_options` must be passed as well."
+ )
if adjoint_options is None:
- adjoint_options = {k: v for k, v in options.items() if k != "norm"} if options is not None else {}
+ adjoint_options = (
+ {k: v for k, v in options.items() if k != "norm"}
+ if options is not None
+ else {}
+ )
else:
# Avoid in-place modifying a user-specified dict.
adjoint_options = adjoint_options.copy()
@@ -192,19 +285,38 @@ def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=No
if len(adjoint_params) != oldlen_:
# Some params were excluded.
# Issue a warning if a user-specified norm is specified.
- if 'norm' in adjoint_options and callable(adjoint_options['norm']):
- warnings.warn("An adjoint parameter was passed without requiring gradient. For efficiency this will be "
- "excluded from the adjoint pass, and will not appear as a tensor in the adjoint norm.")
+ if "norm" in adjoint_options and callable(adjoint_options["norm"]):
+ warnings.warn(
+ "An adjoint parameter was passed without requiring gradient. For efficiency this will be "
+ "excluded from the adjoint pass, and will not appear as a tensor in the adjoint norm."
+ )
# Convert to flattened state.
- shapes, func, y0, t, rtol, atol, method, options, event_fn, decreasing_time = _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS)
+ shapes, func, y0, t, rtol, atol, method, options, event_fn, decreasing_time = (
+ _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS)
+ )
# Handle the adjoint norm function.
state_norm = options["norm"]
handle_adjoint_norm_(adjoint_options, shapes, state_norm)
- ans = OdeintAdjointMethod.apply(shapes, func, y0, t, rtol, atol, method, options, event_fn, adjoint_rtol, adjoint_atol,
- adjoint_method, adjoint_options, t.requires_grad, *adjoint_params)
+ ans = OdeintAdjointMethod.apply(
+ shapes,
+ func,
+ y0,
+ t,
+ rtol,
+ atol,
+ method,
+ options,
+ event_fn,
+ adjoint_rtol,
+ adjoint_atol,
+ adjoint_method,
+ adjoint_options,
+ t.requires_grad,
+ *adjoint_params,
+ )
if event_fn is None:
solution = ans
@@ -228,10 +340,14 @@ def find_parameters(module):
assert isinstance(module, nn.Module)
# If called within DataParallel, parameters won't appear in module.parameters().
- if getattr(module, '_is_replica', False):
+ if getattr(module, "_is_replica", False):
def find_tensor_attributes(module):
- tuples = [(k, v) for k, v in module.__dict__.items() if torch.is_tensor(v) and v.requires_grad]
+ tuples = [
+ (k, v)
+ for k, v in module.__dict__.items()
+ if torch.is_tensor(v) and v.requires_grad
+ ]
return tuples
gen = module._named_members(get_members_fn=find_tensor_attributes)
@@ -255,20 +371,21 @@ def default_adjoint_norm(tensor_tuple):
else:
# `adjoint_options` was explicitly specified by the user...
try:
- adjoint_norm = adjoint_options['norm']
+ adjoint_norm = adjoint_options["norm"]
except KeyError:
# ...but they did not specify the norm argument. Back to plan A: use the default norm.
- adjoint_options['norm'] = default_adjoint_norm
+ adjoint_options["norm"] = default_adjoint_norm
else:
# ...and they did specify the norm argument.
- if adjoint_norm == 'seminorm':
+ if adjoint_norm == "seminorm":
# They told us they want to use seminorms. Slight modification to plan A: use the default norm,
# but ignore the parameter state
def adjoint_seminorm(tensor_tuple):
t, y, adj_y, *adj_params = tensor_tuple
# (If the state is actually a flattened tuple then this will be unpacked again in state_norm.)
return max(t.abs(), state_norm(y), state_norm(adj_y))
- adjoint_options['norm'] = adjoint_seminorm
+
+ adjoint_options["norm"] = adjoint_seminorm
else:
# And they're using their own custom norm.
if shapes is None:
@@ -285,4 +402,5 @@ def _adjoint_norm(tensor_tuple):
y = _flat_to_shape(y, (), shapes)
adj_y = _flat_to_shape(adj_y, (), shapes)
return adjoint_norm((t, *y, *adj_y, *adj_params))
- adjoint_options['norm'] = _adjoint_norm
\ No newline at end of file
+
+ adjoint_options["norm"] = _adjoint_norm
diff --git a/src/nnodely/support/odeint/dopri5.py b/src/nnodely/support/odeint/dopri5.py
new file mode 100644
index 00000000..6506e280
--- /dev/null
+++ b/src/nnodely/support/odeint/dopri5.py
@@ -0,0 +1,60 @@
+import torch
+from nnodely.support.odeint.rk_solvers import (
+ _ButcherTableau,
+ RKAdaptiveStepsizeODESolver,
+)
+
+_DORMAND_PRINCE_SHAMPINE_TABLEAU = _ButcherTableau(
+ alpha=torch.tensor([1 / 5, 3 / 10, 4 / 5, 8 / 9, 1.0, 1.0], dtype=torch.float32),
+ beta=[
+ torch.tensor([1 / 5], dtype=torch.float32),
+ torch.tensor([3 / 40, 9 / 40], dtype=torch.float32),
+ torch.tensor([44 / 45, -56 / 15, 32 / 9], dtype=torch.float32),
+ torch.tensor(
+ [19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729], dtype=torch.float32
+ ),
+ torch.tensor(
+ [9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656],
+ dtype=torch.float32,
+ ),
+ torch.tensor(
+ [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84],
+ dtype=torch.float32,
+ ),
+ ],
+ c_sol=torch.tensor(
+ [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0],
+ dtype=torch.float32,
+ ),
+ c_error=torch.tensor(
+ [
+ 35 / 384 - 1951 / 21600,
+ 0,
+ 500 / 1113 - 22642 / 50085,
+ 125 / 192 - 451 / 720,
+ -2187 / 6784 - -12231 / 42400,
+ 11 / 84 - 649 / 6300,
+ -1.0 / 60.0,
+ ],
+ dtype=torch.float32,
+ ),
+)
+
+DPS_C_MID = torch.tensor(
+ [
+ 6025192743 / 30085553152 / 2,
+ 0,
+ 51252292925 / 65400821598 / 2,
+ -2691868925 / 45128329728 / 2,
+ 187940372067 / 1594534317056 / 2,
+ -1776094331 / 19743644256 / 2,
+ 11237099 / 235043384 / 2,
+ ],
+ dtype=torch.float32,
+)
+
+
+class Dopri5Solver(RKAdaptiveStepsizeODESolver):
+ order = 5
+ tableau = _DORMAND_PRINCE_SHAMPINE_TABLEAU
+ mid = DPS_C_MID
diff --git a/nnodely/support/odeint/fixed_grid.py b/src/nnodely/support/odeint/fixed_grid.py
similarity index 86%
rename from nnodely/support/odeint/fixed_grid.py
rename to src/nnodely/support/odeint/fixed_grid.py
index 28bbf28d..e17c88bc 100644
--- a/nnodely/support/odeint/fixed_grid.py
+++ b/src/nnodely/support/odeint/fixed_grid.py
@@ -15,4 +15,4 @@ class RK4(FixedGridODESolver):
def _step_func(self, func, t0, dt, t1, y0):
f0 = func(t0, y0)
- return rk4_step_func(func, t0, dt, t1, y0, f0=f0), f0
\ No newline at end of file
+ return rk4_step_func(func, t0, dt, t1, y0, f0=f0), f0
diff --git a/nnodely/support/odeint/my_odeint.py b/src/nnodely/support/odeint/my_odeint.py
similarity index 86%
rename from nnodely/support/odeint/my_odeint.py
rename to src/nnodely/support/odeint/my_odeint.py
index 46c9bdb6..9a9b7f7c 100644
--- a/nnodely/support/odeint/my_odeint.py
+++ b/src/nnodely/support/odeint/my_odeint.py
@@ -5,13 +5,15 @@
from nnodely.support.odeint.fixed_grid import Euler, RK4
SOLVERS = {
- 'dopri5': Dopri5Solver,
- 'euler': Euler,
- 'rk4': RK4,
+ "dopri5": Dopri5Solver,
+ "euler": Euler,
+ "rk4": RK4,
}
-def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None):
+def odeint(
+ func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None
+):
"""Integrate a system of ordinary differential equations.
Solves the initial value problem for a non-stiff system of first order ODEs:
@@ -52,7 +54,9 @@ def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, even
ValueError: if an invalid `method` is provided.
"""
- shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed = _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS)
+ shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed = (
+ _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS)
+ )
solver = SOLVERS[method](func=func, y0=y0, rtol=rtol, atol=atol, **options)
@@ -73,7 +77,9 @@ def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, even
return event_t, solution
-def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface=odeint, **kwargs):
+def odeint_event(
+ func, y0, t0, *, event_fn, reverse_time=False, odeint_interface=odeint, **kwargs
+):
"""Automatically links up the gradient from the event time."""
if reverse_time:
@@ -84,7 +90,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface
event_t, solution = odeint_interface(func, y0, t, event_fn=event_fn, **kwargs)
# Dummy values for rtol, atol, method, and options.
- shapes, _func, _, t, _, _, _, _, event_fn, _ = _check_inputs(func, y0, t, 0.0, 0.0, None, None, event_fn, SOLVERS)
+ shapes, _func, _, t, _, _, _, _, event_fn, _ = _check_inputs(
+ func, y0, t, 0.0, 0.0, None, None, event_fn, SOLVERS
+ )
if shapes is not None:
state_t = torch.cat([s[-1].reshape(-1) for s in solution])
@@ -95,7 +103,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface
if reverse_time:
event_t = -event_t
- event_t, state_t = ImplicitFnGradientRerouting.apply(_func, event_fn, event_t, state_t)
+ event_t, state_t = ImplicitFnGradientRerouting.apply(
+ _func, event_fn, event_t, state_t
+ )
# Return the user expected time value.
if reverse_time:
@@ -103,7 +113,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface
if shapes is not None:
state_t = _flat_to_shape(state_t, (), shapes)
- solution = tuple(torch.cat([s[:-1], s_t[None]], dim=0) for s, s_t in zip(solution, state_t))
+ solution = tuple(
+ torch.cat([s[:-1], s_t[None]], dim=0) for s, s_t in zip(solution, state_t)
+ )
else:
solution = torch.cat([solution[:-1], state_t[None]], dim=0)
@@ -111,10 +123,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface
class ImplicitFnGradientRerouting(torch.autograd.Function):
-
@staticmethod
def forward(ctx, func, event_fn, event_t, state_t):
- """ event_t is the solution to event_fn """
+ """event_t is the solution to event_fn"""
ctx.func = func
ctx.event_fn = event_fn
ctx.save_for_backward(event_t, state_t)
@@ -144,4 +155,4 @@ def backward(ctx, grad_t, grad_state):
grad_state = grad_state + dstate
- return None, None, None, grad_state
\ No newline at end of file
+ return None, None, None, grad_state
diff --git a/nnodely/support/odeint/rk_solvers.py b/src/nnodely/support/odeint/rk_solvers.py
similarity index 73%
rename from nnodely/support/odeint/rk_solvers.py
rename to src/nnodely/support/odeint/rk_solvers.py
index 134a4183..69ad93e4 100644
--- a/nnodely/support/odeint/rk_solvers.py
+++ b/src/nnodely/support/odeint/rk_solvers.py
@@ -1,12 +1,21 @@
import collections
import warnings
import torch
-from nnodely.support.odeint.solvers import _handle_unused_kwargs, find_event, AdaptiveStepsizeEventODESolver, FixedGridODESolver
+from nnodely.support.odeint.solvers import (
+ _handle_unused_kwargs,
+ find_event,
+ AdaptiveStepsizeEventODESolver,
+ FixedGridODESolver,
+)
-_ButcherTableau = collections.namedtuple('_ButcherTableau', 'alpha, beta, c_sol, c_error')
+_ButcherTableau = collections.namedtuple(
+ "_ButcherTableau", "alpha, beta, c_sol, c_error"
+)
-_RungeKuttaState = collections.namedtuple('_RungeKuttaState', 'y1, f1, t0, t1, dt, interp_coeff')
+_RungeKuttaState = collections.namedtuple(
+ "_RungeKuttaState", "y1, f1, t0, t1, dt, interp_coeff"
+)
# Saved state of the Runge Kutta solver.
#
# Attributes:
@@ -18,6 +27,7 @@
# interp_coeff: list of Tensors giving coefficients for polynomial
# interpolation between `t0` and `t1`.
+
def _interp_fit(y0, y1, y_mid, f0, f1, dt):
"""Fit coefficients for 4th order polynomial interpolation.
@@ -41,6 +51,7 @@ def _interp_fit(y0, y1, y_mid, f0, f1, dt):
e = y0
return [e, d, c, b, a]
+
def _interp_evaluate(coefficients, t0, t1, t):
"""Evaluate polynomial interpolation at the given time point.
@@ -54,7 +65,9 @@ def _interp_evaluate(coefficients, t0, t1, t):
Polynomial interpolation of the coefficients at time `t`.
"""
- assert (t0 <= t) & (t <= t1), 'invalid interpolation, fails `t0 <= t <= t1`: {}, {}, {}'.format(t0, t, t1)
+ assert (t0 <= t) & (t <= t1), (
+ "invalid interpolation, fails `t0 <= t <= t1`: {}, {}, {}".format(t0, t, t1)
+ )
x = (t - t0) / (t1 - t0)
x = x.to(coefficients[0].dtype)
@@ -66,6 +79,7 @@ def _interp_evaluate(coefficients, t0, t1, t):
return total
+
def _select_initial_step(func, t0, y0, order, rtol, atol, norm, f0=None):
"""Empirically select a good initial step.
@@ -104,15 +118,17 @@ def _select_initial_step(func, t0, y0, order, rtol, atol, norm, f0=None):
if d1 <= 1e-15 and d2 <= 1e-15:
h1 = torch.max(torch.tensor(1e-6, dtype=dtype, device=device), h0 * 1e-3)
else:
- h1 = (0.01 / max(d1, d2)) ** (1. / float(order + 1))
+ h1 = (0.01 / max(d1, d2)) ** (1.0 / float(order + 1))
h1 = h1.abs()
return torch.min(100 * h0, h1).to(t_dtype)
+
def _compute_error_ratio(error_estimate, rtol, atol, y0, y1, norm):
error_tol = atol + rtol * torch.max(y0.abs(), y1.abs())
return norm(error_estimate / error_tol).abs()
+
@torch.no_grad()
def _optimal_step_size(last_step, error_ratio, safety, ifactor, dfactor, order):
"""Calculate the optimal size for the next step."""
@@ -121,10 +137,13 @@ def _optimal_step_size(last_step, error_ratio, safety, ifactor, dfactor, order):
if error_ratio < 1:
dfactor = torch.ones((), dtype=last_step.dtype, device=last_step.device)
error_ratio = error_ratio.type_as(last_step)
- exponent = torch.tensor(order, dtype=last_step.dtype, device=last_step.device).reciprocal()
- factor = torch.min(ifactor, torch.max(safety / error_ratio ** exponent, dfactor))
+ exponent = torch.tensor(
+ order, dtype=last_step.dtype, device=last_step.device
+ ).reciprocal()
+ factor = torch.min(ifactor, torch.max(safety / error_ratio**exponent, dfactor))
return last_step * factor
+
class _UncheckedAssign(torch.autograd.Function):
@staticmethod
def forward(ctx, scratch, value, index):
@@ -136,6 +155,7 @@ def forward(ctx, scratch, value, index):
def backward(ctx, grad_scratch):
return grad_scratch, grad_scratch[ctx.index], None
+
def _runge_kutta_step(func, y0, f0, t0, dt, t1, tableau):
"""Take an arbitrary Runge-Kutta step and estimate error.
Args:
@@ -165,12 +185,12 @@ def _runge_kutta_step(func, y0, f0, t0, dt, t1, tableau):
k = torch.empty(*f0.shape, len(tableau.alpha) + 1, dtype=y0.dtype, device=y0.device)
k = _UncheckedAssign.apply(k, f0, (..., 0))
for i, (alpha_i, beta_i) in enumerate(zip(tableau.alpha, tableau.beta)):
- if alpha_i == 1.:
+ if alpha_i == 1.0:
# Always step to perturbing just before the end time, in case of discontinuities.
ti = t1
else:
ti = t0 + alpha_i * dt
- yi = y0 + torch.sum(k[..., :i + 1] * (beta_i * dt), dim=-1).view_as(f0)
+ yi = y0 + torch.sum(k[..., : i + 1] * (beta_i * dt), dim=-1).view_as(f0)
f = func(ti, yi)
k = _UncheckedAssign.apply(k, f, (..., i + 1))
@@ -200,18 +220,24 @@ class RKAdaptiveStepsizeODESolver(AdaptiveStepsizeEventODESolver):
tableau: _ButcherTableau
mid: torch.Tensor
- def __init__(self, func, y0, rtol, atol,
- min_step=1e-8,
- max_step=float('inf'),
- first_step=None,
- step_t=None,
- jump_t=None,
- safety=0.9,
- ifactor=10.0,
- dfactor=0.2,
- max_num_steps=2**20,
- dtype=torch.float32,
- **kwargs):
+ def __init__(
+ self,
+ func,
+ y0,
+ rtol,
+ atol,
+ min_step=1e-8,
+ max_step=float("inf"),
+ first_step=None,
+ step_t=None,
+ jump_t=None,
+ safety=0.9,
+ ifactor=10.0,
+ dfactor=0.2,
+ max_num_steps=2**20,
+ dtype=torch.float32,
+ **kwargs,
+ ):
super(RKAdaptiveStepsizeODESolver, self).__init__(dtype=dtype, y0=y0, **kwargs)
# We use mixed precision. y has its original dtype (probably float32), whilst all 'time'-like objects use
@@ -224,47 +250,79 @@ def __init__(self, func, y0, rtol, atol,
self.atol = torch.as_tensor(atol, dtype=dtype, device=device)
self.min_step = torch.as_tensor(min_step, dtype=dtype, device=device)
self.max_step = torch.as_tensor(max_step, dtype=dtype, device=device)
- self.first_step = None if first_step is None else torch.as_tensor(first_step, dtype=dtype, device=device)
+ self.first_step = (
+ None
+ if first_step is None
+ else torch.as_tensor(first_step, dtype=dtype, device=device)
+ )
self.safety = torch.as_tensor(safety, dtype=dtype, device=device)
self.ifactor = torch.as_tensor(ifactor, dtype=dtype, device=device)
self.dfactor = torch.as_tensor(dfactor, dtype=dtype, device=device)
- self.max_num_steps = torch.as_tensor(max_num_steps, dtype=torch.int32, device=device)
+ self.max_num_steps = torch.as_tensor(
+ max_num_steps, dtype=torch.int32, device=device
+ )
self.dtype = dtype
- self.step_t = None if step_t is None else torch.as_tensor(step_t, dtype=dtype, device=device)
- self.jump_t = None if jump_t is None else torch.as_tensor(jump_t, dtype=dtype, device=device)
+ self.step_t = (
+ None
+ if step_t is None
+ else torch.as_tensor(step_t, dtype=dtype, device=device)
+ )
+ self.jump_t = (
+ None
+ if jump_t is None
+ else torch.as_tensor(jump_t, dtype=dtype, device=device)
+ )
# Copy from class to instance to set device
- self.tableau = _ButcherTableau(alpha=self.tableau.alpha.to(device=device, dtype=y0.dtype),
- beta=[b.to(device=device, dtype=y0.dtype) for b in self.tableau.beta],
- c_sol=self.tableau.c_sol.to(device=device, dtype=y0.dtype),
- c_error=self.tableau.c_error.to(device=device, dtype=y0.dtype))
+ self.tableau = _ButcherTableau(
+ alpha=self.tableau.alpha.to(device=device, dtype=y0.dtype),
+ beta=[b.to(device=device, dtype=y0.dtype) for b in self.tableau.beta],
+ c_sol=self.tableau.c_sol.to(device=device, dtype=y0.dtype),
+ c_error=self.tableau.c_error.to(device=device, dtype=y0.dtype),
+ )
self.mid = self.mid.to(device=device, dtype=y0.dtype)
@classmethod
def valid_callbacks(cls):
- return super(RKAdaptiveStepsizeODESolver, cls).valid_callbacks() | {'callback_step',
- 'callback_accept_step',
- 'callback_reject_step'}
+ return super(RKAdaptiveStepsizeODESolver, cls).valid_callbacks() | {
+ "callback_step",
+ "callback_accept_step",
+ "callback_reject_step",
+ }
def _before_integrate(self, t):
t0 = t[0]
f0 = self.func(t[0], self.y0)
if self.first_step is None:
- first_step = _select_initial_step(self.func, t[0], self.y0, self.order - 1, self.rtol, self.atol,
- self.norm, f0=f0)
+ first_step = _select_initial_step(
+ self.func,
+ t[0],
+ self.y0,
+ self.order - 1,
+ self.rtol,
+ self.atol,
+ self.norm,
+ f0=f0,
+ )
else:
first_step = self.first_step
- self.rk_state = _RungeKuttaState(self.y0, f0, t[0], t[0], first_step, [self.y0] * 5)
+ self.rk_state = _RungeKuttaState(
+ self.y0, f0, t[0], t[0], first_step, [self.y0] * 5
+ )
def _advance(self, next_t):
"""Interpolate through the next time point, integrating as necessary."""
n_steps = 0
while next_t > self.rk_state.t1:
- assert n_steps < self.max_num_steps, 'max_num_steps exceeded ({}>={})'.format(n_steps, self.max_num_steps)
+ assert n_steps < self.max_num_steps, (
+ "max_num_steps exceeded ({}>={})".format(n_steps, self.max_num_steps)
+ )
self.rk_state = self._adaptive_step(self.rk_state)
n_steps += 1
- return _interp_evaluate(self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, next_t)
+ return _interp_evaluate(
+ self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, next_t
+ )
def _advance_until_event(self, event_fn):
"""Returns t, state(t) such that event_fn(t, state(t)) == 0."""
@@ -274,11 +332,17 @@ def _advance_until_event(self, event_fn):
n_steps = 0
sign0 = torch.sign(event_fn(self.rk_state.t1, self.rk_state.y1))
while sign0 == torch.sign(event_fn(self.rk_state.t1, self.rk_state.y1)):
- assert n_steps < self.max_num_steps, 'max_num_steps exceeded ({}>={})'.format(n_steps, self.max_num_steps)
+ assert n_steps < self.max_num_steps, (
+ "max_num_steps exceeded ({}>={})".format(n_steps, self.max_num_steps)
+ )
self.rk_state = self._adaptive_step(self.rk_state)
n_steps += 1
- interp_fn = lambda t: _interp_evaluate(self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, t)
- return find_event(interp_fn, sign0, self.rk_state.t0, self.rk_state.t1, event_fn, self.atol)
+ interp_fn = lambda t: _interp_evaluate(
+ self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, t
+ )
+ return find_event(
+ interp_fn, sign0, self.rk_state.t0, self.rk_state.t1, event_fn, self.atol
+ )
def _adaptive_step(self, rk_state):
"""Take an adaptive Runge-Kutta step to integrate the ODE."""
@@ -300,10 +364,12 @@ def _adaptive_step(self, rk_state):
########################################################
# Assertions #
########################################################
- assert t0 + dt > t0, 'underflow in dt {}'.format(dt.item())
- assert torch.isfinite(y0).all(), 'non-finite values in state `y`: {}'.format(y0)
+ assert t0 + dt > t0, "underflow in dt {}".format(dt.item())
+ assert torch.isfinite(y0).all(), "non-finite values in state `y`: {}".format(y0)
- y1, f1, y1_error, k = _runge_kutta_step(self.func, y0, f0, t0, dt, t1, tableau=self.tableau)
+ y1, f1, y1_error, k = _runge_kutta_step(
+ self.func, y0, f0, t0, dt, t1, tableau=self.tableau
+ )
# dtypes:
# y1.dtype == self.y0.dtype
# f1.dtype == self.y0.dtype
@@ -313,7 +379,9 @@ def _adaptive_step(self, rk_state):
########################################################
# Error Ratio #
########################################################
- error_ratio = _compute_error_ratio(y1_error, self.rtol, self.atol, y0, y1, self.norm)
+ error_ratio = _compute_error_ratio(
+ y1_error, self.rtol, self.atol, y0, y1, self.norm
+ )
accept_step = error_ratio <= 1
# Handle min max stepping
@@ -339,7 +407,9 @@ def _adaptive_step(self, rk_state):
t_next = t0
y_next = y0
f_next = f0
- dt_next = _optimal_step_size(dt, error_ratio, self.safety, self.ifactor, self.dfactor, self.order)
+ dt_next = _optimal_step_size(
+ dt, error_ratio, self.safety, self.ifactor, self.dfactor, self.order
+ )
dt_next = dt_next.clamp(self.min_step, self.max_step)
rk_state = _RungeKuttaState(y_next, f_next, t0, t_next, dt_next, interp_coeff)
return rk_state
@@ -351,17 +421,28 @@ def _interp_fit(self, y0, y1, k, dt):
f0 = k[..., 0]
f1 = k[..., -1]
return _interp_fit(y0, y1, y_mid, f0, f1, dt)
-
+
+
class FixedGridFIRKODESolver(FixedGridODESolver):
order: int
tableau: _ButcherTableau
- def __init__(self, func, y0, step_size=None, grid_constructor=None, interp='linear', perturb=False, max_iters=100, **unused_kwargs):
+ def __init__(
+ self,
+ func,
+ y0,
+ step_size=None,
+ grid_constructor=None,
+ interp="linear",
+ perturb=False,
+ max_iters=100,
+ **unused_kwargs,
+ ):
self.max_iters = max_iters
- self.atol = unused_kwargs.pop('atol')
- unused_kwargs.pop('rtol', None)
- unused_kwargs.pop('norm', None)
+ self.atol = unused_kwargs.pop("atol")
+ unused_kwargs.pop("rtol", None)
+ unused_kwargs.pop("norm", None)
_handle_unused_kwargs(self, unused_kwargs)
del unused_kwargs
@@ -382,13 +463,17 @@ def __init__(self, func, y0, step_size=None, grid_constructor=None, interp='line
if grid_constructor is None:
self.grid_constructor = self._grid_constructor_from_step_size(step_size)
else:
- raise ValueError("step_size and grid_constructor are mutually exclusive arguments.")
-
- self.tableau = _ButcherTableau(alpha=self.tableau.alpha.to(device=self.device, dtype=y0.dtype),
- beta=[b.to(device=self.device, dtype=y0.dtype) for b in self.tableau.beta],
- c_sol=self.tableau.c_sol.to(device=self.device, dtype=y0.dtype),
- c_error=self.tableau.c_error.to(device=self.device, dtype=y0.dtype))
-
+ raise ValueError(
+ "step_size and grid_constructor are mutually exclusive arguments."
+ )
+
+ self.tableau = _ButcherTableau(
+ alpha=self.tableau.alpha.to(device=self.device, dtype=y0.dtype),
+ beta=[b.to(device=self.device, dtype=y0.dtype) for b in self.tableau.beta],
+ c_sol=self.tableau.c_sol.to(device=self.device, dtype=y0.dtype),
+ c_error=self.tableau.c_error.to(device=self.device, dtype=y0.dtype),
+ )
+
def _step_func(self, func, t0, dt, t1, y0):
if not isinstance(t0, torch.Tensor):
t0 = torch.tensor(t0)
@@ -397,7 +482,7 @@ def _step_func(self, func, t0, dt, t1, y0):
if not isinstance(t1, torch.Tensor):
t1 = torch.tensor(t1)
f0 = func(t0, y0)
-
+
t_dtype = y0.abs().dtype
tol = 1e-8
if t_dtype == torch.float32:
@@ -433,26 +518,30 @@ def _step_func(self, func, t0, dt, t1, y0):
newf = self._residual(func, k, y, t0, dt, t1)
z = newf - f
f = newf
- J = J + (torch.outer ((z - torch.linalg.vecdot(J,s)),s)) / (torch.dot(s,s))
+ J = J + (torch.outer((z - torch.linalg.vecdot(J, s)), s)) / (
+ torch.dot(s, s)
+ )
if not converged:
- warnings.warn('Functional iteration did not converge. Solution may be incorrect.')
+ warnings.warn(
+ "Functional iteration did not converge. Solution may be incorrect."
+ )
dy = torch.matmul(k, dt * self.tableau.c_sol)
return dy, f0
-
+
def _residual(self, func, K, y, t0, dt, t1):
res = torch.zeros_like(K)
for i, (y_i, alpha_i) in enumerate(zip(y, self.tableau.alpha)):
- if alpha_i == 1.:
+ if alpha_i == 1.0:
ti = t1
- elif alpha_i == 0.:
+ elif alpha_i == 0.0:
if not torch.all(self.tableau.beta[i]):
# Same slope as stored so skip
continue
ti = t0
else:
ti = t0 + alpha_i * dt
- res[...,i] = K[...,i] - func(ti, y_i)
- return res.flatten()
\ No newline at end of file
+ res[..., i] = K[..., i] - func(ti, y_i)
+ return res.flatten()
diff --git a/nnodely/support/odeint/solvers.py b/src/nnodely/support/odeint/solvers.py
similarity index 77%
rename from nnodely/support/odeint/solvers.py
rename to src/nnodely/support/odeint/solvers.py
index 39ba70c3..6fb6b9e5 100644
--- a/nnodely/support/odeint/solvers.py
+++ b/src/nnodely/support/odeint/solvers.py
@@ -3,13 +3,18 @@
import warnings
import torch
+
def _handle_unused_kwargs(solver, unused_kwargs):
if len(unused_kwargs) > 0:
- warnings.warn('{}: Unexpected arguments {}'.format(solver.__class__.__name__, unused_kwargs))
+ warnings.warn(
+ "{}: Unexpected arguments {}".format(
+ solver.__class__.__name__, unused_kwargs
+ )
+ )
+
def find_event(interp_fn, sign0, t0, t1, event_fn, tol):
with torch.no_grad():
-
# Num iterations for the secant method until tolerance is within target.
nitrs = torch.ceil(torch.log((t1 - t0) / tol) / math.log(2.0))
@@ -17,13 +22,14 @@ def find_event(interp_fn, sign0, t0, t1, event_fn, tol):
t_mid = (t1 + t0) / 2.0
y_mid = interp_fn(t_mid)
sign_mid = torch.sign(event_fn(t_mid, y_mid))
- same_as_sign0 = (sign0 == sign_mid)
+ same_as_sign0 = sign0 == sign_mid
t0 = torch.where(same_as_sign0, t_mid, t0)
t1 = torch.where(same_as_sign0, t1, t_mid)
event_t = (t0 + t1) / 2.0
return event_t, interp_fn(event_t)
+
class AdaptiveStepsizeODESolver(metaclass=abc.ABCMeta):
def __init__(self, dtype, y0, norm, **unused_kwargs):
_handle_unused_kwargs(self, unused_kwargs)
@@ -46,7 +52,9 @@ def valid_callbacks(cls):
return set()
def integrate(self, t):
- solution = torch.empty(len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device)
+ solution = torch.empty(
+ len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device
+ )
solution[0] = self.y0
t = t.to(self.dtype)
self._before_integrate(t)
@@ -56,7 +64,6 @@ def integrate(self, t):
class AdaptiveStepsizeEventODESolver(AdaptiveStepsizeODESolver, metaclass=abc.ABCMeta):
-
@abc.abstractmethod
def _advance_until_event(self, event_fn):
raise NotImplementedError
@@ -72,10 +79,19 @@ def integrate_until_event(self, t0, event_fn):
class FixedGridODESolver(metaclass=abc.ABCMeta):
order: int
- def __init__(self, func, y0, step_size=None, grid_constructor=None, interp="linear", perturb=False, **unused_kwargs):
- self.atol = unused_kwargs.pop('atol')
- unused_kwargs.pop('rtol', None)
- unused_kwargs.pop('norm', None)
+ def __init__(
+ self,
+ func,
+ y0,
+ step_size=None,
+ grid_constructor=None,
+ interp="linear",
+ perturb=False,
+ **unused_kwargs,
+ ):
+ self.atol = unused_kwargs.pop("atol")
+ unused_kwargs.pop("rtol", None)
+ unused_kwargs.pop("norm", None)
_handle_unused_kwargs(self, unused_kwargs)
del unused_kwargs
@@ -96,11 +112,13 @@ def __init__(self, func, y0, step_size=None, grid_constructor=None, interp="line
if grid_constructor is None:
self.grid_constructor = self._grid_constructor_from_step_size(step_size)
else:
- raise ValueError("step_size and grid_constructor are mutually exclusive arguments.")
+ raise ValueError(
+ "step_size and grid_constructor are mutually exclusive arguments."
+ )
@classmethod
def valid_callbacks(cls):
- return {'callback_step'}
+ return {"callback_step"}
@staticmethod
def _grid_constructor_from_step_size(step_size):
@@ -109,10 +127,14 @@ def _grid_constructor(func, y0, t):
end_time = t[-1]
niters = torch.ceil((end_time - start_time) / step_size + 1).item()
- t_infer = torch.arange(0, niters, dtype=t.dtype, device=t.device) * step_size + start_time
+ t_infer = (
+ torch.arange(0, niters, dtype=t.dtype, device=t.device) * step_size
+ + start_time
+ )
t_infer[-1] = t[-1]
return t_infer
+
return _grid_constructor
@abc.abstractmethod
@@ -123,7 +145,9 @@ def integrate(self, t):
time_grid = self.grid_constructor(self.func, self.y0, t)
assert time_grid[0] == t[0] and time_grid[-1] == t[-1]
- solution = torch.empty(len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device)
+ solution = torch.empty(
+ len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device
+ )
solution[0] = self.y0
j = 1
@@ -139,7 +163,9 @@ def integrate(self, t):
solution[j] = self._linear_interp(t0, t1, y0, y1, t[j])
elif self.interp == "cubic":
f1 = self.func(t1, y1)
- solution[j] = self._cubic_hermite_interp(t0, y0, f0, t1, y1, f1, t[j])
+ solution[j] = self._cubic_hermite_interp(
+ t0, y0, f0, t1, y1, f1, t[j]
+ )
else:
raise ValueError(f"Unknown interpolation method {self.interp}")
j += 1
@@ -148,7 +174,9 @@ def integrate(self, t):
return solution
def integrate_until_event(self, t0, event_fn):
- assert self.step_size is not None, "Event handling for fixed step solvers currently requires `step_size` to be provided in options."
+ assert self.step_size is not None, (
+ "Event handling for fixed step solvers currently requires `step_size` to be provided in options."
+ )
t0 = t0.type_as(self.y0.abs())
y0 = self.y0
@@ -170,10 +198,14 @@ def integrate_until_event(self, t0, event_fn):
interp_fn = lambda t: self._linear_interp(t0, t1, y0, y1, t)
elif self.interp == "cubic":
f1 = self.func(t1, y1)
- interp_fn = lambda t: self._cubic_hermite_interp(t0, y0, f0, t1, y1, f1, t)
+ interp_fn = lambda t: self._cubic_hermite_interp(
+ t0, y0, f0, t1, y1, f1, t
+ )
else:
raise ValueError(f"Unknown interpolation method {self.interp}")
- event_time, y1 = find_event(interp_fn, sign0, t0, t1, event_fn, float(self.atol))
+ event_time, y1 = find_event(
+ interp_fn, sign0, t0, t1, event_fn, float(self.atol)
+ )
break
else:
t0, y0 = t1, y1
@@ -182,14 +214,14 @@ def integrate_until_event(self, t0, event_fn):
raise RuntimeError(f"Reached maximum number of iterations {max_itrs}.")
solution = torch.stack([self.y0, y1], dim=0)
return event_time, solution
-
+
def _cubic_hermite_interp(self, t0, y0, f0, t1, y1, f1, t):
h = (t - t0) / (t1 - t0)
h00 = (1 + 2 * h) * (1 - h) * (1 - h)
h10 = h * (1 - h) * (1 - h)
h01 = h * h * (3 - 2 * h)
h11 = h * h * (h - 1)
- dt = (t1 - t0)
+ dt = t1 - t0
return h00 * y0 + h10 * dt * f0 + h01 * y1 + h11 * dt * f1
def _linear_interp(self, t0, t1, y0, y1, t):
@@ -199,4 +231,3 @@ def _linear_interp(self, t0, t1, y0, y1, t):
return y1
slope = (t - t0) / (t1 - t0)
return y0 + slope * (y1 - y0)
-
diff --git a/nnodely/support/odeint/utils.py b/src/nnodely/support/odeint/utils.py
similarity index 78%
rename from nnodely/support/odeint/utils.py
rename to src/nnodely/support/odeint/utils.py
index 5ec8d6c5..1d4ab3b4 100644
--- a/nnodely/support/odeint/utils.py
+++ b/src/nnodely/support/odeint/utils.py
@@ -1,22 +1,26 @@
import torch
import warnings
-_all_callback_names = ['callback_step', 'callback_accept_step', 'callback_reject_step']
-_all_adjoint_callback_names = [name + '_adjoint' for name in _all_callback_names]
+_all_callback_names = ["callback_step", "callback_accept_step", "callback_reject_step"]
+_all_adjoint_callback_names = [name + "_adjoint" for name in _all_callback_names]
_null_callback = lambda *args, **kwargs: None
+
def _linf_norm(tensor):
return tensor.abs().max()
+
def _rms_norm(tensor):
return tensor.abs().pow(2).mean().sqrt()
+
def _zero_norm(tensor):
- return 0.
+ return 0.0
+
def _mixed_norm(tensor_tuple):
if len(tensor_tuple) == 0:
- return 0.
+ return 0.0
return max([_rms_norm(tensor) for tensor in tensor_tuple])
@@ -41,8 +45,12 @@ def _tuple_tol(name, tol, shapes):
except TypeError:
return tol
tol = tuple(tol)
- assert len(tol) == len(shapes), "If using tupled {} it must have the same length as the tuple y0".format(name)
- tol = [torch.as_tensor(tol_).expand(shape.numel()) for tol_, shape in zip(tol, shapes)]
+ assert len(tol) == len(shapes), (
+ "If using tupled {} it must have the same length as the tuple y0".format(name)
+ )
+ tol = [
+ torch.as_tensor(tol_).expand(shape.numel()) for tol_, shape in zip(tol, shapes)
+ ]
return torch.cat(tol)
@@ -59,17 +67,21 @@ def _flat_to_shape(tensor, length, shapes):
def _assert_floating(name, t):
if not torch.is_floating_point(t):
- raise TypeError('`{}` must be a floating point Tensor but is a {}'.format(name, t.type()))
+ raise TypeError(
+ "`{}` must be a floating point Tensor but is a {}".format(name, t.type())
+ )
def _check_timelike(name, timelike, can_grad):
- assert isinstance(timelike, torch.Tensor), '{} must be a torch.Tensor'.format(name)
+ assert isinstance(timelike, torch.Tensor), "{} must be a torch.Tensor".format(name)
_assert_floating(name, timelike)
assert timelike.ndimension() == 1, "{} must be one dimensional".format(name)
if not can_grad:
assert not timelike.requires_grad, "{} cannot require gradient".format(name)
diff = timelike[1:] > timelike[:-1]
- assert diff.all() or (~diff).all(), '{} must be strictly increasing or decreasing'.format(name)
+ assert diff.all() or (~diff).all(), (
+ "{} must be strictly increasing or decreasing".format(name)
+ )
class _TupleFunc(torch.nn.Module):
@@ -107,7 +119,9 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS):
if event_fn is not None:
if len(t) != 2:
- raise ValueError(f"We require len(t) == 2 when in event handling mode, but got len(t)={len(t)}.")
+ raise ValueError(
+ f"We require len(t) == 2 when in event handling mode, but got len(t)={len(t)}."
+ )
# Combine event functions if the output is multivariate.
event_fn = combine_event_functions(event_fn, t[0], y0)
@@ -119,10 +133,10 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS):
shapes = None
is_tuple = not isinstance(y0, torch.Tensor)
if is_tuple:
- assert isinstance(y0, tuple), 'y0 must be either a torch.Tensor or a tuple'
+ assert isinstance(y0, tuple), "y0 must be either a torch.Tensor or a tuple"
shapes = [y0_.shape for y0_ in y0]
- rtol = _tuple_tol('rtol', rtol, shapes)
- atol = _tuple_tol('atol', atol, shapes)
+ rtol = _tuple_tol("rtol", rtol, shapes)
+ atol = _tuple_tol("atol", atol, shapes)
y0 = torch.cat([y0_.reshape(-1) for y0_ in y0])
func = _TupleFunc(func, shapes)
if event_fn is not None:
@@ -134,18 +148,21 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS):
else:
options = options.copy()
if method is None:
- method = 'dopri5'
+ method = "dopri5"
if method not in SOLVERS:
- raise ValueError('Invalid method "{}". Must be one of {}'.format(method,
- '{"' + '", "'.join(SOLVERS.keys()) + '"}.'))
+ raise ValueError(
+ 'Invalid method "{}". Must be one of {}'.format(
+ method, '{"' + '", "'.join(SOLVERS.keys()) + '"}.'
+ )
+ )
if is_tuple:
# We accept tupled input. This is an abstraction that is hidden from the rest of odeint (exception when
# returning values), so here we need to maintain the abstraction by wrapping norm functions.
- if 'norm' in options:
+ if "norm" in options:
# If the user passed a norm then get that...
- norm = options['norm']
+ norm = options["norm"]
else:
# ...otherwise we default to a mixed Linf/L2 norm over tupled input.
norm = _mixed_norm
@@ -157,10 +174,11 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS):
def _norm(tensor):
y = _flat_to_shape(tensor, (), shapes)
return norm(y)
- options['norm'] = _norm
+
+ options["norm"] = _norm
else:
- if 'norm' in options:
+ if "norm" in options:
# No need to change the norm function.
pass
else:
@@ -168,10 +186,10 @@ def _norm(tensor):
# Technically we don't need to set that here (RKAdaptiveStepsizeODESolver has it as a default), but it
# makes it easier to reason about, in the adjoint norm logic, if we know that options['norm'] is
# definitely set to something.
- options['norm'] = _rms_norm
+ options["norm"] = _rms_norm
# Normalise time
- _check_timelike('t', t, True)
+ _check_timelike("t", t, True)
t_is_reversed = False
if len(t) > 1 and t[0] > t[1]:
t_is_reversed = True
@@ -188,18 +206,20 @@ def _norm(tensor):
# For fixed step solvers.
try:
- _grid_constructor = options['grid_constructor']
+ _grid_constructor = options["grid_constructor"]
except KeyError:
pass
else:
- options['grid_constructor'] = lambda func, y0, t: -_grid_constructor(func, y0, -t)
+ options["grid_constructor"] = lambda func, y0, t: (
+ -_grid_constructor(func, y0, -t)
+ )
# For RK solvers.
- #_flip_option(options, 'step_t')
- #_flip_option(options, 'jump_t')
+ # _flip_option(options, 'step_t')
+ # _flip_option(options, 'jump_t')
# Can only do after having normalised time
- assert (t[1:] > t[:-1]).all(), 't must be strictly increasing or decreasing'
+ assert (t[1:] > t[:-1]).all(), "t must be strictly increasing or decreasing"
# Tol checking
if torch.is_tensor(rtol):
@@ -214,7 +234,7 @@ def _norm(tensor):
# ~Backward compatibility
# Add perturb argument to func.
- #func = _PerturbFunc(func)
+ # func = _PerturbFunc(func)
# Add callbacks to wrapped_func
callback_names = set()
@@ -229,12 +249,16 @@ def _norm(tensor):
# At the moment all callbacks have the arguments (t0, y0, dt).
# These will need adjusting on a per-callback basis if that changes in the future.
if is_tuple:
+
def callback(t0, y0, dt, _callback=callback):
y0 = _flat_to_shape(y0, (), shapes)
return _callback(t0, y0, dt)
+
if t_is_reversed:
+
def callback(t0, y0, dt, _callback=callback):
return _callback(-t0, y0, dt)
+
setattr(func, callback_name, callback)
for callback_name in _all_adjoint_callback_names:
try:
@@ -246,6 +270,10 @@ def callback(t0, y0, dt, _callback=callback):
invalid_callbacks = callback_names - SOLVERS[method].valid_callbacks()
if len(invalid_callbacks) > 0:
- warnings.warn("Solver '{}' does not support callbacks {}".format(method, invalid_callbacks))
+ warnings.warn(
+ "Solver '{}' does not support callbacks {}".format(
+ method, invalid_callbacks
+ )
+ )
- return shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed
\ No newline at end of file
+ return shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed
diff --git a/nnodely/support/utils.py b/src/nnodely/support/utils.py
similarity index 77%
rename from nnodely/support/utils.py
rename to src/nnodely/support/utils.py
index 0aff8e20..c50255e5 100644
--- a/nnodely/support/utils.py
+++ b/src/nnodely/support/utils.py
@@ -1,4 +1,5 @@
-import torch, inspect
+import torch
+import inspect
import types
from collections import OrderedDict
@@ -13,6 +14,7 @@
ForbiddenTags = keyword.kwlist
+
class ReadOnlyDict:
def __init__(self, data):
self._data = data
@@ -40,6 +42,7 @@ def values(self):
def __repr__(self):
from pprint import pformat
+
return pformat(self._data)
def __or__(self, other):
@@ -51,6 +54,7 @@ def __or__(self, other):
def __str__(self):
from nnodely.visualizer.emptyvisualizer import color, GREEN
from pprint import pformat
+
return color(pformat(self._data), GREEN)
def __eq__(self, other):
@@ -58,19 +62,21 @@ def __eq__(self, other):
return self._data == other
return self._data == other._data
+
class ParamDict(ReadOnlyDict):
- def __init__(self, data, internal_data = None):
+ def __init__(self, data, internal_data=None):
super().__init__(data)
self._internal_data = internal_data if internal_data is not None else {}
def __setitem__(self, key, value):
- self._data[key]['values'] = value
+ self._data[key]["values"] = value
self._internal_data[key] = self._internal_data[key].new_tensor(value)
def __getitem__(self, key):
- value = self._data[key]['values'] if 'values' in self._data[key] else None
+ value = self._data[key]["values"] if "values" in self._data[key] else None
return value
+
def enforce_types(func):
@wraps(func)
def wrapper(*args, **kwargs):
@@ -81,27 +87,35 @@ def wrapper(*args, **kwargs):
if len(sig) != len(args):
var_type = None
for ind, arg in enumerate(args):
- if ind < len(list(sig.values())) and list(sig.values())[ind].kind == inspect.Parameter.VAR_POSITIONAL:
+ if (
+ ind < len(list(sig.values()))
+ and list(sig.values())[ind].kind == inspect.Parameter.VAR_POSITIONAL
+ ):
var_name = list(sig.keys())[ind]
var_type = sig.pop(var_name)
if var_type:
- sig[var_name+str(ind)] = var_type
+ sig[var_name + str(ind)] = var_type
all_args.update(dict(zip(sig, args)))
- if 'self' in sig.keys():
- sig.pop('self')
- if 'cls' in sig.keys():
- sig.pop('cls')
+ if "self" in sig.keys():
+ sig.pop("self")
+ if "cls" in sig.keys():
+ sig.pop("cls")
for arg_name, arg in all_args.items():
- if (arg_name in hints.keys() or arg_name in sig.keys()) and not isinstance(arg,sig[arg_name].annotation):
- class_name = func.__qualname__.split('.')[0]
+ if (arg_name in hints.keys() or arg_name in sig.keys()) and not isinstance(
+ arg, sig[arg_name].annotation
+ ):
+ class_name = func.__qualname__.split(".")[0]
if isinstance(sig[arg_name].annotation, types.UnionType):
- type_list = [val.__name__ for val in sig[arg_name].annotation.__args__]
+ type_list = [
+ val.__name__ for val in sig[arg_name].annotation.__args__
+ ]
else:
type_list = sig[arg_name].annotation.__name__
raise TypeError(
- f"In Function or Class {class_name} the argument '{arg_name}' to be of type {type_list}, but got {type(arg).__name__}")
+ f"In Function or Class {class_name} the argument '{arg_name}' to be of type {type_list}, but got {type(arg).__name__}"
+ )
# for arg, arg_type in hints.items():
# if arg in all_args and not isinstance(all_args[arg], arg_type):
@@ -112,15 +126,18 @@ def wrapper(*args, **kwargs):
return wrapper
+
def is_notebook():
try:
from IPython import get_ipython
- if 'IPKernelApp' in get_ipython().config:
+
+ if "IPKernelApp" in get_ipython().config:
return True # È un notebook
except Exception:
pass
return False # È uno script
+
def tensor_to_list(data):
if isinstance(data, torch.Tensor):
# Converte il tensore in una lista
@@ -141,25 +158,35 @@ def tensor_to_list(data):
# Altri tipi di dati rimangono invariati
return data
-def get_batch_size(n_samples, batch_size = None, predicion_samples = 0):
+
+def get_batch_size(n_samples, batch_size=None, predicion_samples=0):
batch_size = batch_size if batch_size is not None else n_samples
- predicion_samples = 0 if predicion_samples == -1 else predicion_samples #This value is used to disconnect the connect
- batch_size = batch_size if batch_size <= n_samples - predicion_samples else max(0, n_samples - predicion_samples)
- check(batch_size > 0, ValueError, f'The batch_size must be greater than 0.')
+ predicion_samples = (
+ 0 if predicion_samples == -1 else predicion_samples
+ ) # This value is used to disconnect the connect
+ batch_size = (
+ batch_size
+ if batch_size <= n_samples - predicion_samples
+ else max(0, n_samples - predicion_samples)
+ )
+ check(batch_size > 0, ValueError, "The batch_size must be greater than 0.")
return batch_size
+
def check_and_get_list(name_list, available_names, error_fun):
if type(name_list) is str:
name_list = [name_list]
if type(name_list) is list:
for name in name_list:
- check(name in available_names, IndexError, error_fun(name))
+ check(name in available_names, IndexError, error_fun(name))
return name_list
+
def check(condition, exception, string):
if not condition:
raise exception(string)
+
# Function used to verified the number of gradient operations in the graph
# def count_gradient_operations(grad_fn):
# count = 0
@@ -176,4 +203,4 @@ def check(condition, exception, string):
# count = 0
# for key in X.keys():
# count += count_gradient_operations(X[key].grad_fn)
-# return count
\ No newline at end of file
+# return count
diff --git a/nnodely/visualizer/__init__.py b/src/nnodely/visualizer/__init__.py
similarity index 95%
rename from nnodely/visualizer/__init__.py
rename to src/nnodely/visualizer/__init__.py
index 43263770..1f668dc4 100644
--- a/nnodely/visualizer/__init__.py
+++ b/src/nnodely/visualizer/__init__.py
@@ -1,4 +1,4 @@
from nnodely.visualizer.emptyvisualizer import EmptyVisualizer
from nnodely.visualizer.textvisualizer import TextVisualizer
from nnodely.visualizer.mplvisualizer import MPLVisualizer
-from nnodely.visualizer.mplnotebookvisualizer import MPLNotebookVisualizer
\ No newline at end of file
+from nnodely.visualizer.mplnotebookvisualizer import MPLNotebookVisualizer
diff --git a/nnodely/visualizer/dynamicmpl/functionplot.py b/src/nnodely/visualizer/dynamicmpl/functionplot.py
similarity index 61%
rename from nnodely/visualizer/dynamicmpl/functionplot.py
rename to src/nnodely/visualizer/dynamicmpl/functionplot.py
index c640814a..74fe533f 100644
--- a/nnodely/visualizer/dynamicmpl/functionplot.py
+++ b/src/nnodely/visualizer/dynamicmpl/functionplot.py
@@ -1,4 +1,5 @@
-import sys, json
+import sys
+import json
import matplotlib.pyplot as plt
@@ -13,17 +14,17 @@
try:
# Convert to float and append to buffer
data_point = json.loads(line)
- name = data_point['name']
- if 'x1' in data_point.keys():
- x0 = data_point['x0']
- x1 = data_point['x1']
+ name = data_point["name"]
+ if "x1" in data_point.keys():
+ x0 = data_point["x0"]
+ x1 = data_point["x1"]
else:
- x = data_point['x0']
- params = data_point['params']
- input_names = data_point['input_names']
- output = data_point['output']
+ x = data_point["x0"]
+ params = data_point["params"]
+ input_names = data_point["input_names"]
+ output = data_point["output"]
- if 'x1' in data_point.keys():
+ if "x1" in data_point.keys():
plots.plot_3d_function(plt, name, x0, x1, params, output, input_names)
else:
plots.plot_2d_function(plt, name, x, params, output, input_names)
@@ -31,4 +32,3 @@
except ValueError:
pass
-
diff --git a/nnodely/visualizer/dynamicmpl/fuzzyplot.py b/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py
similarity index 68%
rename from nnodely/visualizer/dynamicmpl/fuzzyplot.py
rename to src/nnodely/visualizer/dynamicmpl/fuzzyplot.py
index 6717f8cc..3b49f196 100644
--- a/nnodely/visualizer/dynamicmpl/fuzzyplot.py
+++ b/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py
@@ -1,4 +1,5 @@
-import sys, json
+import sys
+import json
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
@@ -14,13 +15,13 @@
try:
# Convert to float and append to buffer
data_point = json.loads(line)
- name = data_point['name']
- x = data_point['x']
- chan_centers = data_point['chan_centers']
+ name = data_point["name"]
+ x = data_point["x"]
+ chan_centers = data_point["chan_centers"]
tableau_colors = mcolors.TABLEAU_COLORS
num_of_colors = len(list(tableau_colors.keys()))
- for ind, key in enumerate(data_point['y'].keys()):
- y.append(data_point['y'][key])
+ for ind, key in enumerate(data_point["y"].keys()):
+ y.append(data_point["y"][key])
fig, ax = plt.subplots()
ax.cla()
@@ -28,4 +29,4 @@
plt.show()
except ValueError:
- pass
\ No newline at end of file
+ pass
diff --git a/nnodely/visualizer/dynamicmpl/resultsplot.py b/src/nnodely/visualizer/dynamicmpl/resultsplot.py
similarity index 68%
rename from nnodely/visualizer/dynamicmpl/resultsplot.py
rename to src/nnodely/visualizer/dynamicmpl/resultsplot.py
index c94a167f..9db0fbc5 100644
--- a/nnodely/visualizer/dynamicmpl/resultsplot.py
+++ b/src/nnodely/visualizer/dynamicmpl/resultsplot.py
@@ -1,6 +1,7 @@
import json
import sys
import os
+
# append a new directory to sys.path
sys.path.append(os.getcwd())
@@ -17,12 +18,12 @@
try:
# Convert to float and append to buffer
data_point = json.loads(line)
- name_data = data_point['name_data']
- key = data_point['key']
- A = data_point['prediction_A']
- B = data_point['prediction_B']
- data_idxs = data_point['data_idxs']
- sample_time = data_point['sample_time']
+ name_data = data_point["name_data"]
+ key = data_point["key"]
+ A = data_point["prediction_A"]
+ B = data_point["prediction_B"]
+ data_idxs = data_point["data_idxs"]
+ sample_time = data_point["sample_time"]
fig, ax = plt.subplots()
ax.cla()
@@ -30,4 +31,4 @@
plt.show()
except ValueError:
- pass
\ No newline at end of file
+ pass
diff --git a/nnodely/visualizer/dynamicmpl/trainingplot.py b/src/nnodely/visualizer/dynamicmpl/trainingplot.py
similarity index 78%
rename from nnodely/visualizer/dynamicmpl/trainingplot.py
rename to src/nnodely/visualizer/dynamicmpl/trainingplot.py
index 0b3b87e4..cd153d30 100644
--- a/nnodely/visualizer/dynamicmpl/trainingplot.py
+++ b/src/nnodely/visualizer/dynamicmpl/trainingplot.py
@@ -1,5 +1,6 @@
import sys
import os
+
# append a new directory to sys.path
sys.path.append(os.getcwd())
@@ -19,6 +20,7 @@
# Set up the plot
fig, ax = plt.subplots()
+
def update_graph(frame):
global last, title, epoch
if last > 0:
@@ -28,13 +30,13 @@ def update_graph(frame):
try:
# Convert to float and append to buffer
data = json.loads(line)
- data_train.append(data['train_losses'])
- if data['val_losses']:
- data_val.append(data['val_losses'])
- title = data['title']
- key = data['key']
- last = data['last']
- epoch = data['epoch']
+ data_train.append(data["train_losses"])
+ if data["val_losses"]:
+ data_val.append(data["val_losses"])
+ title = data["title"]
+ key = data["key"]
+ last = data["last"]
+ epoch = data["epoch"]
# Clear the current plot
ax.cla()
# Clear the current plot
@@ -44,6 +46,7 @@ def update_graph(frame):
else:
pass
+
# Use FuncAnimation to update the plot dynamically
ani = animation.FuncAnimation(fig, update_graph, interval=10, save_count=20)
diff --git a/nnodely/visualizer/emptyvisualizer.py b/src/nnodely/visualizer/emptyvisualizer.py
similarity index 84%
rename from nnodely/visualizer/emptyvisualizer.py
rename to src/nnodely/visualizer/emptyvisualizer.py
index d71abb8c..ac72115a 100644
--- a/nnodely/visualizer/emptyvisualizer.py
+++ b/src/nnodely/visualizer/emptyvisualizer.py
@@ -8,11 +8,13 @@
BOLD_SEQ = "\033[1m"
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
-def color(msg, color_val = GREEN, bold = False):
+
+def color(msg, color_val=GREEN, bold=False):
if bold:
return COLOR_BOLD_SEQ % (30 + color_val) + msg + RESET_SEQ
return COLOR_SEQ % (30 + color_val) + msg + RESET_SEQ
+
class EmptyVisualizer:
def __init__(self):
pass
@@ -23,7 +25,7 @@ def setModely(self, modely):
def showModel(self, model):
pass
- def showaddMinimize(self,variable_name):
+ def showaddMinimize(self, variable_name):
pass
def showModelInputWindow(self):
@@ -35,13 +37,13 @@ def showModelRelationSamples(self):
def showBuiltModel(self):
pass
- def showWeights(self, weights = None):
+ def showWeights(self, weights=None):
pass
- def showFunctions(self, functions = None):
+ def showFunctions(self, functions=None):
pass
- def showWeightsInTrain(self, batch = None, epoch = None, weights = None):
+ def showWeightsInTrain(self, batch=None, epoch=None, weights=None):
pass
def showDataset(self, name):
diff --git a/nnodely/visualizer/mplnotebookvisualizer.py b/src/nnodely/visualizer/mplnotebookvisualizer.py
similarity index 50%
rename from nnodely/visualizer/mplnotebookvisualizer.py
rename to src/nnodely/visualizer/mplnotebookvisualizer.py
index 4a9d4428..3d66ac53 100644
--- a/nnodely/visualizer/mplnotebookvisualizer.py
+++ b/src/nnodely/visualizer/mplnotebookvisualizer.py
@@ -7,80 +7,109 @@
from nnodely.support.utils import check
from mplplots import plots
+
class MPLNotebookVisualizer(TextVisualizer):
- def __init__(self, verbose = 1, *, test = False):
+ def __init__(self, verbose=1, *, test=False):
super().__init__(verbose)
self.test = test
if self.test:
plt.ion()
def showEndTraining(self, epoch, train_losses, val_losses):
- train_tag = self.modely.running_parameters['train_tag']
- val_tag = self.modely.running_parameters['val_tag']
- for key in self.modely.json['Minimizers'].keys():
+ train_tag = self.modely.running_parameters["train_tag"]
+ val_tag = self.modely.running_parameters["val_tag"]
+ for key in self.modely.json["Minimizers"].keys():
fig = plt.figure()
ax = fig.add_subplot(111)
if val_losses:
- plots.plot_training(ax, f"Training on {train_tag} and {val_tag}", key, train_losses[key], val_losses[key])
+ plots.plot_training(
+ ax,
+ f"Training on {train_tag} and {val_tag}",
+ key,
+ train_losses[key],
+ val_losses[key],
+ )
else:
- plots.plot_training(ax, f"Training on {train_tag}", key, train_losses[key])
+ plots.plot_training(
+ ax, f"Training on {train_tag}", key, train_losses[key]
+ )
plt.show()
def showResult(self, name_data):
super().showResult(name_data)
- for key in self.modely.json['Minimizers'].keys():
+ for key in self.modely.json["Minimizers"].keys():
fig = plt.figure()
ax = fig.add_subplot(111)
- np_data_A = np.array(self.modely.prediction[name_data][key]['A'])
+ np_data_A = np.array(self.modely.prediction[name_data][key]["A"])
if len(np_data_A.shape) > 3 and np_data_A.shape[1] > 30:
- np_data_B = np.array(self.modely.prediction[name_data][key]['B'])
+ np_data_B = np.array(self.modely.prediction[name_data][key]["B"])
indices = np.linspace(0, np_data_A.shape[1] - 1, 30, dtype=int)
data_A = np_data_A[:, indices, :, :].tolist()
data_B = np_data_B[:, indices, :, :].tolist()
- data_idxs = np.array(self.modely.prediction[name_data]['idxs'])[:,indices].tolist()
+ data_idxs = np.array(self.modely.prediction[name_data]["idxs"])[
+ :, indices
+ ].tolist()
else:
- data_A = self.modely.prediction[name_data][key]['A']
- data_B = self.modely.prediction[name_data][key]['B']
- data_idxs = self.modely.prediction[name_data]['idxs'] if len(np_data_A.shape) > 3 else None
+ data_A = self.modely.prediction[name_data][key]["A"]
+ data_B = self.modely.prediction[name_data][key]["B"]
+ data_idxs = (
+ self.modely.prediction[name_data]["idxs"]
+ if len(np_data_A.shape) > 3
+ else None
+ )
- plots.plot_results(ax, name_data, key, data_A,
- data_B, data_idxs, self.modely._model_def['Info']["SampleTime"])
+ plots.plot_results(
+ ax,
+ name_data,
+ key,
+ data_A,
+ data_B,
+ data_idxs,
+ self.modely._model_def["Info"]["SampleTime"],
+ )
plt.show()
- def showWeights(self, weights = None):
+ def showWeights(self, weights=None):
pass
- def showFunctions(self, functions = None, xlim = None, num_points = 1000):
+ def showFunctions(self, functions=None, xlim=None, num_points=1000):
check(self.modely.neuralized, ValueError, "The model has not been neuralized.")
- for fun, value in self.modely._model_def['Functions'].items():
+ for fun, value in self.modely._model_def["Functions"].items():
if fun in functions:
- if 'functions' in self.modely._model_def['Functions'][fun]:
+ if "functions" in self.modely._model_def["Functions"][fun]:
x, activ_fun = return_fuzzify(value, xlim, num_points)
fig = plt.figure()
ax = fig.add_subplot(111)
- plots.plot_fuzzy(ax, fun, x, activ_fun, value['centers'])
- elif 'code':
- function_inputs = return_standard_inputs(value, self.modely._model_def, xlim, num_points)
- function_output, function_input_list = return_function(value, function_inputs)
- if value['n_input'] == 2:
+ plots.plot_fuzzy(ax, fun, x, activ_fun, value["centers"])
+ elif "code":
+ function_inputs = return_standard_inputs(
+ value, self.modely._model_def, xlim, num_points
+ )
+ function_output, function_input_list = return_function(
+ value, function_inputs
+ )
+ if value["n_input"] == 2:
x0 = function_inputs[0].reshape(num_points, num_points).tolist()
x1 = function_inputs[1].reshape(num_points, num_points).tolist()
- output = function_output.reshape(num_points, num_points).tolist()
+ output = function_output.reshape(
+ num_points, num_points
+ ).tolist()
params = []
- for i, key in enumerate(value['params_and_consts']):
- params += [function_inputs[i + value['n_input']].tolist()]
- plots.plot_3d_function(plt, fun, x0, x1, params, output, function_input_list)
+ for i, key in enumerate(value["params_and_consts"]):
+ params += [function_inputs[i + value["n_input"]].tolist()]
+ plots.plot_3d_function(
+ plt, fun, x0, x1, params, output, function_input_list
+ )
else:
x = function_inputs[0].reshape(num_points).tolist()
output = function_output.reshape(num_points).tolist()
params = []
- for i, key in enumerate(value['params_and_consts']):
- params += [function_inputs[i + value['n_input']].tolist()]
- plots.plot_2d_function(plt, fun, x, params, output, function_input_list)
+ for i, key in enumerate(value["params_and_consts"]):
+ params += [function_inputs[i + value["n_input"]].tolist()]
+ plots.plot_2d_function(
+ plt, fun, x, params, output, function_input_list
+ )
plt.show()
def closePlots(self):
plt.close()
-
-
-
diff --git a/nnodely/visualizer/mplvisualizer.py b/src/nnodely/visualizer/mplvisualizer.py
similarity index 55%
rename from nnodely/visualizer/mplvisualizer.py
rename to src/nnodely/visualizer/mplvisualizer.py
index c1a130ad..ef4c5321 100644
--- a/nnodely/visualizer/mplvisualizer.py
+++ b/src/nnodely/visualizer/mplvisualizer.py
@@ -1,4 +1,7 @@
-import subprocess, json, os, importlib
+import subprocess
+import json
+import os
+import importlib
import numpy as np
from nnodely.visualizer.textvisualizer import TextVisualizer
@@ -8,28 +11,41 @@
from nnodely.basic.modeldef import ModelDef
from nnodely.support.logger import logging, nnLogger
+
log = nnLogger(__name__, logging.INFO)
+
def get_library_path(library_name):
spec = importlib.util.find_spec(library_name)
if spec is None:
raise ImportError(f"Library {library_name} not found")
return os.path.dirname(spec.origin)
+
class MPLVisualizer(TextVisualizer):
- def __init__(self, verbose = 1):
+ def __init__(self, verbose=1):
super().__init__(verbose)
# Path to the data visualizer script
import signal
import sys
- get_library_path('nnodely')
- self.__training_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','trainingplot.py')
- self.__time_series_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','resultsplot.py')
- self.__fuzzy_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','fuzzyplot.py')
- self.__function_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','functionplot.py')
+
+ get_library_path("nnodely")
+ self.__training_visualizer_script = os.path.join(
+ get_library_path("nnodely"), "visualizer", "dynamicmpl", "trainingplot.py"
+ )
+ self.__time_series_visualizer_script = os.path.join(
+ get_library_path("nnodely"), "visualizer", "dynamicmpl", "resultsplot.py"
+ )
+ self.__fuzzy_visualizer_script = os.path.join(
+ get_library_path("nnodely"), "visualizer", "dynamicmpl", "fuzzyplot.py"
+ )
+ self.__function_visualizer_script = os.path.join(
+ get_library_path("nnodely"), "visualizer", "dynamicmpl", "functionplot.py"
+ )
self.__process_training = {}
self.__process_results = {}
self.__process_function = {}
+
def signal_handler(sig, frame):
for key in self.__process_training.keys():
self.__process_training[key].terminate()
@@ -52,28 +68,41 @@ def showStartTraining(self):
def showTraining(self, epoch, train_losses, val_losses):
if epoch == 0:
for key in self.__process_training.keys():
- if self.__process_training[key] is not None and self.__process_training[key].poll() is None:
+ if (
+ self.__process_training[key] is not None
+ and self.__process_training[key].poll() is None
+ ):
self.__process_training[key].terminate()
self.__process_training[key].wait()
self.__process_training[key] = None
self.__process_training = {}
- for key in self.modely._model_def['Minimizers'].keys():
- self.__process_training[key] = subprocess.Popen(['python', self.__training_visualizer_script], stdin=subprocess.PIPE, text=True)
-
- num_of_epochs = self.modely.running_parameters['num_of_epochs']
- train_tag = self.modely.running_parameters['train_tag']
- val_tag = self.modely.running_parameters['val_tag']
- if epoch+1 <= num_of_epochs:
- for key in self.modely._model_def['Minimizers'].keys():
+ for key in self.modely._model_def["Minimizers"].keys():
+ self.__process_training[key] = subprocess.Popen(
+ ["python", self.__training_visualizer_script],
+ stdin=subprocess.PIPE,
+ text=True,
+ )
+
+ num_of_epochs = self.modely.running_parameters["num_of_epochs"]
+ train_tag = self.modely.running_parameters["train_tag"]
+ val_tag = self.modely.running_parameters["val_tag"]
+ if epoch + 1 <= num_of_epochs:
+ for key in self.modely._model_def["Minimizers"].keys():
if val_losses:
val_loss = val_losses[key][epoch]
title = f"Training on {train_tag} and {val_tag}"
else:
val_loss = []
title = f"Training on {train_tag}"
- data = {"title":title, "key": key, "last": num_of_epochs - (epoch + 1), "epoch": epoch,
- "train_losses": train_losses[key][epoch], "val_losses": val_loss}
+ data = {
+ "title": title,
+ "key": key,
+ "last": num_of_epochs - (epoch + 1),
+ "epoch": epoch,
+ "train_losses": train_losses[key][epoch],
+ "val_losses": val_loss,
+ }
try:
# Send data to the visualizer process
self.__process_training[key].stdin.write(f"{json.dumps(data)}\n")
@@ -82,98 +111,139 @@ def showTraining(self, epoch, train_losses, val_losses):
self.closeTraining()
log.warning("The visualizer process has been closed.")
- if epoch+1 == num_of_epochs:
- for key in self.modely._model_def['Minimizers'].keys():
+ if epoch + 1 == num_of_epochs:
+ for key in self.modely._model_def["Minimizers"].keys():
if self.__process_training[key] is not None:
self.__process_training[key].stdin.close()
def showResult(self, name_data):
super().showResult(name_data)
- check(name_data in self.modely.performance, ValueError, f"Results not available for {name_data}.")
+ check(
+ name_data in self.modely.performance,
+ ValueError,
+ f"Results not available for {name_data}.",
+ )
if name_data in self.__process_results:
- for key in self.modely._model_def['Minimizers'].keys():
- if key in self.__process_results[name_data] and self.__process_results[name_data][key].poll() is None:
+ for key in self.modely._model_def["Minimizers"].keys():
+ if (
+ key in self.__process_results[name_data]
+ and self.__process_results[name_data][key].poll() is None
+ ):
self.__process_results[name_data][key].terminate()
self.__process_results[name_data][key].wait()
self.__process_results[name_data][key] = None
self.__process_results[name_data] = {}
- for key in self.modely._model_def['Minimizers'].keys():
+ for key in self.modely._model_def["Minimizers"].keys():
# Start the data visualizer process
- self.__process_results[name_data][key] = subprocess.Popen(['python', self.__time_series_visualizer_script], stdin=subprocess.PIPE,
- text=True)
- np_data_A = np.array(self.modely.prediction[name_data][key]['A'])
+ self.__process_results[name_data][key] = subprocess.Popen(
+ ["python", self.__time_series_visualizer_script],
+ stdin=subprocess.PIPE,
+ text=True,
+ )
+ np_data_A = np.array(self.modely.prediction[name_data][key]["A"])
if len(np_data_A.shape) > 3 and np_data_A.shape[1] > 30:
- np_data_B = np.array(self.modely.prediction[name_data][key]['B'])
+ np_data_B = np.array(self.modely.prediction[name_data][key]["B"])
indices = np.linspace(0, np_data_A.shape[1] - 1, 30, dtype=int)
data_A = np_data_A[:, indices, :, :].tolist()
data_B = np_data_B[:, indices, :, :].tolist()
- data_idxs = np.array(self.modely.prediction[name_data]['idxs'])[:,indices].tolist()
+ data_idxs = np.array(self.modely.prediction[name_data]["idxs"])[
+ :, indices
+ ].tolist()
else:
- data_A = self.modely.prediction[name_data][key]['A']
- data_B = self.modely.prediction[name_data][key]['B']
- data_idxs = self.modely.prediction[name_data]['idxs'] if len(np_data_A.shape) > 3 else None
+ data_A = self.modely.prediction[name_data][key]["A"]
+ data_B = self.modely.prediction[name_data][key]["B"]
+ data_idxs = (
+ self.modely.prediction[name_data]["idxs"]
+ if len(np_data_A.shape) > 3
+ else None
+ )
- data = {"name_data": name_data,
- "key": key,
- "performance": self.modely.performance[name_data][key],
- "prediction_A": data_A,
- "prediction_B": data_B,
- "data_idxs": data_idxs,
- "sample_time": self.modely._model_def['Info']["SampleTime"]}
+ data = {
+ "name_data": name_data,
+ "key": key,
+ "performance": self.modely.performance[name_data][key],
+ "prediction_A": data_A,
+ "prediction_B": data_B,
+ "data_idxs": data_idxs,
+ "sample_time": self.modely._model_def["Info"]["SampleTime"],
+ }
try:
# Send data to the visualizer process
- self.__process_results[name_data][key].stdin.write(f"{json.dumps(data)}\n")
+ self.__process_results[name_data][key].stdin.write(
+ f"{json.dumps(data)}\n"
+ )
self.__process_results[name_data][key].stdin.flush()
self.__process_results[name_data][key].stdin.close()
except:
self.closeResult(self, name_data)
log.warning(f"The visualizer {name_data} process has been closed.")
- def showWeights(self, weights = None):
+ def showWeights(self, weights=None):
pass
- def showFunctions(self, functions = None, xlim = None, num_points = 1000):
+ def showFunctions(self, functions=None, xlim=None, num_points=1000):
check(self.modely.neuralized, ValueError, "The model has not been neuralized.")
- for key, value in self.modely._model_def['Functions'].items():
+ for key, value in self.modely._model_def["Functions"].items():
if key in functions:
- if key in self.__process_function and self.__process_function[key].poll() is None:
+ if (
+ key in self.__process_function
+ and self.__process_function[key].poll() is None
+ ):
self.__process_function[key].terminate()
self.__process_function[key].wait()
- if 'functions' in self.modely._model_def['Functions'][key]:
+ if "functions" in self.modely._model_def["Functions"][key]:
x, activ_fun = return_fuzzify(value, xlim, num_points)
- data = {"name": key,
- "x": x,
- "y": activ_fun,
- "chan_centers": value['centers']}
+ data = {
+ "name": key,
+ "x": x,
+ "y": activ_fun,
+ "chan_centers": value["centers"],
+ }
# Start the data visualizer process
- self.__process_function[key] = subprocess.Popen(['python', self.__fuzzy_visualizer_script],
- stdin=subprocess.PIPE,
- text=True)
- elif 'code':
+ self.__process_function[key] = subprocess.Popen(
+ ["python", self.__fuzzy_visualizer_script],
+ stdin=subprocess.PIPE,
+ text=True,
+ )
+ elif "code":
model_def = ModelDef(self.modely._model_def)
model_def.updateParameters(self.modely._model)
- function_inputs = return_standard_inputs(value, model_def, xlim, num_points)
- function_output, function_input_list = return_function(value, function_inputs)
+ function_inputs = return_standard_inputs(
+ value, model_def, xlim, num_points
+ )
+ function_output, function_input_list = return_function(
+ value, function_inputs
+ )
data = {"name": key}
- if value['n_input'] == 2:
- data['x0'] = function_inputs[0].reshape(num_points, num_points).tolist()
- data['x1'] = function_inputs[1].reshape(num_points, num_points).tolist()
- data['output'] = function_output.reshape(num_points, num_points).tolist()
+ if value["n_input"] == 2:
+ data["x0"] = (
+ function_inputs[0].reshape(num_points, num_points).tolist()
+ )
+ data["x1"] = (
+ function_inputs[1].reshape(num_points, num_points).tolist()
+ )
+ data["output"] = function_output.reshape(
+ num_points, num_points
+ ).tolist()
else:
- data['x0'] = function_inputs[0].reshape(num_points).tolist()
- data['output'] = function_output.reshape(num_points).tolist()
- data['params'] = []
- for i, key in enumerate(value['params_and_consts']):
- data['params'] += [function_inputs[i+value['n_input']].tolist()]
- data['input_names'] = function_input_list
+ data["x0"] = function_inputs[0].reshape(num_points).tolist()
+ data["output"] = function_output.reshape(num_points).tolist()
+ data["params"] = []
+ for i, key in enumerate(value["params_and_consts"]):
+ data["params"] += [
+ function_inputs[i + value["n_input"]].tolist()
+ ]
+ data["input_names"] = function_input_list
# Start the data visualizer process
- self.__process_function[key] = subprocess.Popen(['python', self.__function_visualizer_script],
- stdin=subprocess.PIPE,
- text=True)
+ self.__process_function[key] = subprocess.Popen(
+ ["python", self.__function_visualizer_script],
+ stdin=subprocess.PIPE,
+ text=True,
+ )
try:
# Send data to the visualizer process
self.__process_function[key].stdin.write(f"{json.dumps(data)}\n")
@@ -183,7 +253,7 @@ def showFunctions(self, functions = None, xlim = None, num_points = 1000):
self.closeFunctions()
log.warning(f"The visualizer {functions} process has been closed.")
- def closeFunctions(self, functions = None):
+ def closeFunctions(self, functions=None):
if functions is None:
for key in self.__process_function.keys():
self.__process_function[key].terminate()
@@ -195,10 +265,14 @@ def closeFunctions(self, functions = None):
self.__process_function[key].wait()
self.__process_function.pop(key)
- def closeTraining(self, minimizer = None):
+ def closeTraining(self, minimizer=None):
if minimizer is None:
- for key in self.modely._model_def['Minimizers'].keys():
- if key in self.__process_training and self.__process_training[key] is not None and self.__process_training[key].poll() is None:
+ for key in self.modely._model_def["Minimizers"].keys():
+ if (
+ key in self.__process_training
+ and self.__process_training[key] is not None
+ and self.__process_training[key].poll() is None
+ ):
self.__process_training[key].terminate()
self.__process_training[key].wait()
self.__process_training[key] = None
@@ -207,9 +281,13 @@ def closeTraining(self, minimizer = None):
self.__process_training[minimizer].wait()
self.__process_training.pop(minimizer)
- def closeResult(self, name_data = None, minimizer = None):
+ def closeResult(self, name_data=None, minimizer=None):
if name_data is None:
- check(minimizer is None, ValueError, "If name_data is None, minimizer must be None.")
+ check(
+ minimizer is None,
+ ValueError,
+ "If name_data is None, minimizer must be None.",
+ )
for name_data in self.__process_results.keys():
for key in self.__process_results[name_data].keys():
self.__process_results[name_data][key].terminate()
diff --git a/src/nnodely/visualizer/textvisualizer.py b/src/nnodely/visualizer/textvisualizer.py
new file mode 100644
index 00000000..649ef1bb
--- /dev/null
+++ b/src/nnodely/visualizer/textvisualizer.py
@@ -0,0 +1,493 @@
+import numpy as np
+from pprint import pformat
+
+from nnodely.visualizer.emptyvisualizer import EmptyVisualizer, color, GREEN, BLUE
+
+
+class TextVisualizer(EmptyVisualizer):
+ def __init__(self, verbose=1):
+ self.verbose = verbose
+
+ def __title(self, msg, lenght=80):
+ print(color((msg).center(lenght, "="), GREEN, True))
+
+ def __subtitle(self, msg, lenght=80):
+ print(color((msg).center(lenght, "-"), GREEN, True))
+
+ def __line(self):
+ print(color("=".center(80, "="), GREEN))
+
+ def __singleline(self):
+ print(color("-".center(80, "-"), GREEN))
+
+ def __info(self, name, dim=30):
+ print(color((name).ljust(dim), BLUE))
+
+ def __paramjson(self, name, value, dim=30):
+ lines = pformat(value, width=80 - dim).strip().splitlines()
+ vai = ("\n" + (" " * dim)).join(x for x in lines)
+ # pformat(value).strip().splitlines().rjust(40)
+ print(color((name).ljust(dim) + vai, GREEN))
+
+ def __param(self, name, value, dim=30):
+ print(color((name).ljust(dim) + value, GREEN))
+
+ def showModel(self, model):
+ if self.verbose >= 1:
+ self.__title(" nnodely Model ")
+ print(color(pformat(model), GREEN))
+ self.__line()
+
+ def showMinimize(self, variable_name):
+ if self.verbose >= 2:
+ self.__title(
+ f" Minimize Error of {variable_name} between"
+ f" {self.modely._model_def['Minimizers'][variable_name]['A']} and"
+ f" {self.modely._model_def['Minimizers'][variable_name]['B']} with {self.modely._model_def['Minimizers'][variable_name]['loss']} "
+ )
+ self.__line()
+
+ def showModelInputWindow(self):
+ if self.verbose >= 2:
+ input_ns_backward = {
+ key: value["ns"][0]
+ for key, value in self.modely._model_def["Inputs"].items()
+ }
+ input_ns_forward = {
+ key: value["ns"][1]
+ for key, value in self.modely._model_def["Inputs"].items()
+ }
+ self.__title(" nnodely Model Input Windows ")
+ # self.__paramjson("time_window_backward:",self.modely.input_tw_backward)
+ # self.__paramjson("time_window_forward:",self.modely.input_tw_forward)
+ self.__paramjson("sample_window_backward:", input_ns_backward)
+ self.__paramjson("sample_window_forward:", input_ns_forward)
+ self.__paramjson("input_n_samples:", self.modely._input_n_samples)
+ self.__param(
+ "max_samples [backw, forw]:",
+ f"[{self.modely._model_def['Info']['ns'][0]},{self.modely._model_def['Info']['ns'][1]}]",
+ )
+ self.__param("max_samples total:", f"{self.modely._max_n_samples}")
+ self.__line()
+
+ def showModelRelationSamples(self):
+ if self.verbose >= 2:
+ self.__title(" nnodely Model Relation Samples ")
+ self.__paramjson("Relation_samples:", self.modely.relation_samples)
+ self.__line()
+
+ def showBuiltModel(self):
+ if self.verbose >= 2:
+ self.__title(" nnodely Built Model ")
+ print(color(pformat(self.modely._model), GREEN))
+ self.__line()
+
+ def showWeights(self, weights=None):
+ self.__title(" nnodely Models Weights ")
+ for key, param in self.modely.parameters.items():
+ if weights is None or key in weights:
+ self.__paramjson(key, param)
+ self.__line()
+
+ def showWeightsInTrain(self, batch=None, epoch=None, weights=None):
+ if self.verbose >= 2:
+ par = self.modely.running_parameters
+ dim = len(self.modely._model_def["Minimizers"])
+ COLOR = BLUE
+ if epoch is not None:
+ print(
+ color(
+ "|"
+ + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ")
+ + "|",
+ COLOR,
+ ),
+ end="",
+ )
+ print(
+ color(
+ (f" Params end epochs {epoch + 1} ").center(
+ 20 * (dim + 1) - 1, "-"
+ )
+ + "|",
+ COLOR,
+ )
+ )
+
+ if batch is not None:
+ print(
+ color("|" + (f"{batch + 1}").center(10, " ") + "|", COLOR), end=""
+ )
+ print(
+ color(
+ (f" Params end batch {batch + 1} ").center(
+ 20 * (dim + 1) - 1, "-"
+ )
+ + "|",
+ COLOR,
+ )
+ )
+
+ for key, param in self.modely.parameters.items():
+ if weights is None or key in weights:
+ print(color("|" + (f"{key}").center(10, " ") + "|", COLOR), end="")
+ print(
+ color((f"{param}").center(20 * (dim + 1) - 1, " ") + "|", COLOR)
+ )
+
+ if epoch is not None:
+ print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|"))
+
+ def showDataset(self, name):
+ if self.verbose >= 1:
+ self.__title(" nnodely Model Dataset ")
+ self.__param("Dataset Name:", name)
+ self.__param("Number of files:", f"{self.modely._file_count}")
+ self.__param(
+ "Total number of samples:", f"{self.modely._num_of_samples[name]}"
+ )
+ for key in self.modely._model_def["Inputs"].keys():
+ if key in self.modely._data[name].keys():
+ self.__param(
+ f"Shape of {key}:", f"{self.modely._data[name][key].shape}"
+ )
+ self.__line()
+
+ def showStartTraining(self):
+ if self.verbose >= 1:
+ par = self.modely.running_parameters
+ dim = len(self.modely._model_def["Minimizers"])
+ self.__title(
+ " nnodely Training ",
+ 12 + (len(self.modely._model_def["Minimizers"]) + 1) * 20,
+ )
+ print(color("|" + ("Epoch").center(10, " ") + "|"), end="")
+ for key in self.modely._model_def["Minimizers"].keys():
+ print(color((f"{key}").center(19, " ") + "|"), end="")
+ print(color(("Total").center(19, " ") + "|"))
+
+ print(color("|" + (" ").center(10, " ") + "|"), end="")
+ for key in self.modely._model_def["Minimizers"].keys():
+ print(color(("Loss").center(19, " ") + "|"), end="")
+ print(color(("Loss").center(19, " ") + "|"))
+
+ print(color("|" + (" ").center(10, " ") + "|"), end="")
+ for key in self.modely._model_def["Minimizers"].keys():
+ if par["n_samples_val"]:
+ print(color(("train").center(9, " ") + "|"), end="")
+ print(color(("val").center(9, " ") + "|"), end="")
+ else:
+ print(color(("train").center(19, " ") + "|"), end="")
+ if par["n_samples_val"]:
+ print(color(("train").center(9, " ") + "|"), end="")
+ print(color(("val").center(9, " ") + "|"))
+ else:
+ print(color(("train").center(19, " ") + "|"))
+
+ print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|"))
+
+ def showTraining(self, epoch, train_losses, val_losses):
+ if self.verbose >= 1:
+ eng = lambda val: np.format_float_scientific(val, precision=3)
+ par = self.modely.running_parameters
+ show_epoch = (
+ 1 if par["num_of_epochs"] <= 100 else int(par["num_of_epochs"] / 100)
+ )
+ dim = len(self.modely._model_def["Minimizers"])
+ if epoch < par["num_of_epochs"]:
+ print("", end="\r")
+ print(
+ "|" + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ") + "|",
+ end="",
+ )
+ train_loss = []
+ val_loss = []
+ for key in self.modely._model_def["Minimizers"].keys():
+ train_loss.append(train_losses[key][epoch])
+ if val_losses:
+ val_loss.append(val_losses[key][epoch])
+ print(
+ (f"{eng(train_losses[key][epoch])}").center(9, " ") + "|",
+ end="",
+ )
+ print(
+ (f"{eng(val_losses[key][epoch])}").center(9, " ") + "|",
+ end="",
+ )
+ else:
+ print(
+ (f"{eng(train_losses[key][epoch])}").center(19, " ") + "|",
+ end="",
+ )
+
+ if val_losses:
+ print((f"{eng(np.mean(train_loss))}").center(9, " ") + "|", end="")
+ print((f"{eng(np.mean(val_loss))}").center(9, " ") + "|", end="")
+ else:
+ print((f"{eng(np.mean(train_loss))}").center(19, " ") + "|", end="")
+
+ if (epoch + 1) % show_epoch == 0:
+ print("", end="\r")
+ print(
+ color(
+ "|"
+ + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ")
+ + "|"
+ ),
+ end="",
+ )
+ for key in self.modely._model_def["Minimizers"].keys():
+ if val_losses:
+ print(
+ color(
+ (f"{eng(train_losses[key][epoch])}").center(9, " ")
+ + "|"
+ ),
+ end="",
+ )
+ print(
+ color(
+ (f"{eng(val_losses[key][epoch])}").center(9, " ")
+ + "|"
+ ),
+ end="",
+ )
+ else:
+ print(
+ color(
+ (f"{eng(train_losses[key][epoch])}").center(19, " ")
+ + "|"
+ ),
+ end="",
+ )
+ if val_losses:
+ print(
+ color((f"{eng(np.mean(train_loss))}").center(9, " ") + "|"),
+ end="",
+ )
+ print(color((f"{eng(np.mean(val_loss))}").center(9, " ") + "|"))
+ else:
+ print(
+ color((f"{eng(np.mean(train_loss))}").center(19, " ") + "|")
+ )
+
+ if epoch + 1 == par["num_of_epochs"]:
+ print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|"))
+
+ def showTrainingTime(self, time):
+ if self.verbose >= 1:
+ self.__title(" nnodely Training Time ")
+ self.__param("Total time of Training:", f"{time}")
+ self.__line()
+
+ def showTrainParams(self):
+ if self.verbose >= 1:
+ self.__title(" nnodely Model Train Parameters ")
+ par = self.modely.getTrainingInfo()
+
+ self.__paramjson("models:", par["models"])
+ self.__param("num of epochs:", str(par["num_of_epochs"]))
+ self.__param("update per epochs:", str(par["update_per_epochs"]))
+ if par["prediction_samples"] >= 0:
+ self.__info("â””>len(train_indexes)//(batch_size+step)")
+ else:
+ self.__info("â””>(n_samples-batch_size)/batch_size+1")
+
+ if par["shuffle_data"]:
+ self.__param("shuffle data:", str(par["shuffle_data"]))
+
+ if "early_stopping" in par and par["early_stopping"]:
+ self.__param("early stopping:", par["early_stopping"])
+ self.__paramjson("early stopping params:", par["early_stopping_params"])
+
+ if par["prediction_samples"] >= 0:
+ self.__param("prediction samples:", f"{par['prediction_samples']}")
+ self.__param("step:", f"{par['train_step']}")
+ self.__paramjson("closed loop:", par["closed_loop"])
+ self.__paramjson("connect:", par["connect"])
+
+ self.__param("train dataset:", f"{par['train_tag']}")
+ self.__param("\t- batch size:", f"{par['train_batch_size']}")
+ self.__param("\t- num of samples:", f"{par['n_samples_train']}")
+ if par["prediction_samples"] >= 0:
+ self.__param(
+ "\t- num of first samples:", f"{par['n_first_samples_train']}"
+ )
+
+ if par["n_samples_val"] > 0:
+ self.__param("validation dataset:", f"{par['val_tag']}")
+ self.__param("\t- batch size:", f"{par['val_batch_size']}")
+ self.__param("\t- num of samples:", f"{par['n_samples_val']}")
+ if par["prediction_samples"] >= 0:
+ self.__param(
+ "\t- num of first samples:", f"{par['n_first_samples_val']}"
+ )
+
+ if par["n_samples_test"] > 0:
+ self.__param("test dataset:", f"{par['test_tag']}")
+ self.__param("\t- num of samples:", f"{par['n_samples_test']}")
+ if "test_batch_size" in par:
+ self.__param("\t- batch size:", f"{par['test_batch_size']}")
+ if par["prediction_samples"] >= 0:
+ self.__param(
+ "\t- num of first samples:", f"{par['n_first_samples_test']}"
+ )
+
+ self.__paramjson("minimizers:", par["minimizers"])
+
+ self.__param("optimizer:", par["optimizer"])
+ self.__paramjson("optimizer defaults:", par["optimizer_defaults"])
+ if par["optimizer_params"] is not None:
+ self.__paramjson("optimizer params:", par["optimizer_params"])
+
+ self.__line()
+
+ def showResult(self, name_data):
+ eng = lambda val: np.format_float_scientific(val, precision=3)
+ if self.verbose >= 1:
+ dim_loss = max(
+ 5, len(max(self.modely._model_def["Minimizers"].keys(), key=len))
+ )
+ loss_type_list = set(
+ [
+ value["loss"]
+ for ind, (key, value) in enumerate(
+ self.modely._model_def["Minimizers"].items()
+ )
+ ]
+ )
+ self.__title(
+ f" nnodely Model Results for {name_data} ",
+ dim_loss + 2 + (len(loss_type_list) + 2) * 20,
+ )
+ print(color("|" + ("Loss").center(dim_loss, " ") + "|"), end="")
+ for loss in loss_type_list:
+ print(color((f"{loss}").center(19, " ") + "|"), end="")
+ print(color(("FVU").center(19, " ") + "|"), end="")
+ print(color(("AIC").center(19, " ") + "|"))
+
+ print(color("|" + ("").center(dim_loss, " ") + "|"), end="")
+ for i in range(len(loss_type_list)):
+ print(color(("small better").center(19, " ") + "|"), end="")
+ print(color(("small better").center(19, " ") + "|"), end="")
+ print(color(("lower better").center(19, " ") + "|"))
+
+ print(
+ color(
+ "|"
+ + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-")
+ + "|"
+ )
+ )
+ for ind, (key, value) in enumerate(
+ self.modely._model_def["Minimizers"].items()
+ ):
+ print(color("|" + (f"{key}").center(dim_loss, " ") + "|"), end="")
+ for loss in list(loss_type_list):
+ if value["loss"] == loss:
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data][key][value['loss']])}"
+ ).center(19, " ")
+ + "|"
+ ),
+ end="",
+ )
+ else:
+ print(color((" ").center(19, " ") + "|"), end="")
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data][key]['fvu']['total'])}"
+ ).center(19, " ")
+ + "|"
+ ),
+ end="",
+ )
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data][key]['aic']['value'])}"
+ ).center(19, " ")
+ + "|"
+ )
+ )
+
+ print(
+ color(
+ "|"
+ + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-")
+ + "|"
+ )
+ )
+ print(color("|" + ("Total").center(dim_loss, " ") + "|"), end="")
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data]['total']['mean_error'])}"
+ ).center(len(loss_type_list) * 20 - 1, " ")
+ + "|"
+ ),
+ end="",
+ )
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data]['total']['fvu'])}"
+ ).center(19, " ")
+ + "|"
+ ),
+ end="",
+ )
+ print(
+ color(
+ (
+ f"{eng(self.modely.performance[name_data]['total']['aic'])}"
+ ).center(19, " ")
+ + "|"
+ )
+ )
+
+ print(
+ color(
+ "|"
+ + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-")
+ + "|"
+ )
+ )
+
+ if self.verbose >= 2:
+ self.__title(" Detalied Results ")
+ print(color(pformat(self.modely.performance), GREEN))
+ self.__line()
+
+ def saveModel(self, name, path):
+ if self.verbose >= 1:
+ self.__title(f" Save {name} ")
+ self.__param("Model saved in:", path)
+ self.__line()
+
+ def loadModel(self, name, path):
+ if self.verbose >= 1:
+ self.__title(f" Load {name} ")
+ self.__param("Model loaded from:", path)
+ self.__line()
+
+ def exportModel(self, name, path):
+ if self.verbose >= 1:
+ self.__title(f" Export {name} ")
+ self.__param("Model exported in:", path)
+ self.__line()
+
+ def importModel(self, name, path):
+ if self.verbose >= 1:
+ self.__title(f" Import {name} ")
+ self.__param("Model imported from:", path)
+ self.__line()
+
+ def exportReport(self, name, path):
+ if self.verbose >= 1:
+ self.__title(f" Export {name} Report ")
+ self.__param("Report exported in:", path)
+ self.__line()
diff --git a/tests/__init__.py b/tests/__init__.py
index 9d1b47f2..210b6ce1 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -3,4 +3,5 @@
# Imposta un backend non-GUI solo su Windows
if sys.platform.startswith("win"):
import matplotlib
- matplotlib.use("Agg")
\ No newline at end of file
+
+ matplotlib.use("Agg")
diff --git a/tests/get_samples_data/data.csv b/tests/get_samples_data/data.csv
index 74a9c6f3..5ba268fb 100644
--- a/tests/get_samples_data/data.csv
+++ b/tests/get_samples_data/data.csv
@@ -7,4 +7,4 @@ x,y
6,4
0,5
0,6
-0,7
\ No newline at end of file
+0,7
diff --git a/tests/multifile/file1.csv b/tests/multifile/file1.csv
index 145603d0..4141b195 100644
--- a/tests/multifile/file1.csv
+++ b/tests/multifile/file1.csv
@@ -8,4 +8,4 @@ x,y
10,10
10,10
10,10
-10,10
\ No newline at end of file
+10,10
diff --git a/tests/multifile/file2.csv b/tests/multifile/file2.csv
index ac05b0d0..9ba54d2f 100644
--- a/tests/multifile/file2.csv
+++ b/tests/multifile/file2.csv
@@ -18,4 +18,4 @@ x,y
20,20
20,20
20,20
-20,20
\ No newline at end of file
+20,20
diff --git a/tests/multifile/file3.csv b/tests/multifile/file3.csv
index 79180bf8..bc734e0d 100644
--- a/tests/multifile/file3.csv
+++ b/tests/multifile/file3.csv
@@ -28,4 +28,4 @@ x,y
30,30
30,30
30,30
-30,30
\ No newline at end of file
+30,30
diff --git a/tests/multifile2/file1.csv b/tests/multifile2/file1.csv
index 7d5eaf2f..a28e024a 100644
--- a/tests/multifile2/file1.csv
+++ b/tests/multifile2/file1.csv
@@ -8,4 +8,4 @@ x,y
40,40
40,40
40,40
-40,40
\ No newline at end of file
+40,40
diff --git a/tests/multifile2/file2.csv b/tests/multifile2/file2.csv
index 7713584e..47c5fef4 100644
--- a/tests/multifile2/file2.csv
+++ b/tests/multifile2/file2.csv
@@ -18,4 +18,4 @@ x,y
50,50
50,50
50,50
-50,50
\ No newline at end of file
+50,50
diff --git a/tests/multifile2/file3.csv b/tests/multifile2/file3.csv
index 31898748..440c03f5 100644
--- a/tests/multifile2/file3.csv
+++ b/tests/multifile2/file3.csv
@@ -28,4 +28,4 @@ x,y
60,60
60,60
60,60
-60,60
\ No newline at end of file
+60,60
diff --git a/tests/multifile3/file1.csv b/tests/multifile3/file1.csv
index bee647ba..56266193 100644
--- a/tests/multifile3/file1.csv
+++ b/tests/multifile3/file1.csv
@@ -48,4 +48,4 @@ x,y
125,125
126,126
127,127
-128,128
\ No newline at end of file
+128,128
diff --git a/tests/test_data/testdata.dta b/tests/test_data/testdata.dta
index 535ad715..405cdd02 100644
--- a/tests/test_data/testdata.dta
+++ b/tests/test_data/testdata.dta
@@ -15,4 +15,3 @@ x1 y1 x2 y2 A1x A1y B1x B1y A2x A2y
0.812 0.818 0.348 0.453 - 0.350 1.375 0.571 1.199 - 0.575 1.375 0.699 3.214 - 0.274 0.742 12.556 0.100 -
0.813 0.816 0.354 0.445 - 0.350 1.375 0.567 1.194 - 0.575 1.375 0.695 3.211 - 0.273 0.733 12.570 0.110 -
0.814 0.814 0.361 0.436 - 0.350 1.375 0.562 1.189 - 0.575 1.375 0.690 3.207 - 0.272 0.723 12.585 0.120 -
-
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index d497d0c8..9971f510 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -1,4 +1,6 @@
-import sys, os, unittest
+import sys
+import os
+import unittest
import numpy as np
from nnodely import *
@@ -14,458 +16,1333 @@
# This file test the data loading in particular:
# The shape and the value of the inputs
-train_folder = os.path.join(os.path.dirname(__file__), 'data/')
-val_folder = os.path.join(os.path.dirname(__file__), 'val_data/')
-test_folder = os.path.join(os.path.dirname(__file__), 'test_data/')
+train_folder = os.path.join(os.path.dirname(__file__), "data/")
+val_folder = os.path.join(os.path.dirname(__file__), "val_data/")
+test_folder = os.path.join(os.path.dirname(__file__), "test_data/")
+
class ModelyCreateDatasetTest(unittest.TestCase):
-
def test_build_dataset_simple(self):
NeuObj.clearNames()
- input = Input('in1')
- output = Input('out')
+ input = Input("in1")
+ output = Input("out")
relation = Fir(input.tw(0.05))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), relation)
+ test.addMinimize("out", output.z(-1), relation)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time']
- test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,5,1), test._data['dataset_1']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['in1'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['dataset_1']['out'].shape)
- self.assertEqual([[1.225]], test._data['dataset_1']['out'][0].tolist())
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset_1']['out'].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset_1",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset_1"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset_1"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 1, 1), test._data["dataset_1"]["out"].shape)
+ self.assertEqual([[1.225]], test._data["dataset_1"]["out"][0].tolist())
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["dataset_1"]["out"].tolist(),
+ )
def test_build_dataset_tuple(self):
NeuObj.clearNames()
- inputA = Input('inA')
- inputB = Input('inB')
- out = Output('out', Fir(inputA.tw(0.05)+inputB.tw(0.05)))
+ inputA = Input("inA")
+ inputB = Input("inB")
+ out = Output("out", Fir(inputA.tw(0.05) + inputB.tw(0.05)))
test = Modely(visualizer=None)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.01)
- data_struct = ['','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time']
- test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((11,5,1), test._data['dataset_1']['inA'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inA'][0].tolist())
- self.assertEqual((11,5,1), test._data['dataset_1']['inB'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inB'][0].tolist())
-
- out = Output('out2', Fir(inputA.tw(0.05)))
+ data_struct = [
+ "",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ ("inA", "inB"),
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset_1",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((11, 5, 1), test._data["dataset_1"]["inA"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset_1"]["inA"][0].tolist(),
+ )
+ self.assertEqual((11, 5, 1), test._data["dataset_1"]["inB"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset_1"]["inB"][0].tolist(),
+ )
+
+ out = Output("out2", Fir(inputA.tw(0.05)))
test2 = Modely(visualizer=None)
- test2.addModel('out2', out)
+ test2.addModel("out2", out)
test2.neuralizeModel(0.01)
- data_struct = ['','y1','x2','y2','','A1x','A1y','B1x','B1y','',('A2x','f'),'A2y','B2x','out','','x3',('inA','inB'),'theta','time']
- test2.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((11,5,1), test._data['dataset_1']['inA'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inA'][0].tolist())
+ data_struct = [
+ "",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ ("A2x", "f"),
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ ("inA", "inB"),
+ "theta",
+ "time",
+ ]
+ test2.loadData(
+ name="dataset_1",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((11, 5, 1), test._data["dataset_1"]["inA"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset_1"]["inA"][0].tolist(),
+ )
def test_build_dataset_tuple_dim(self):
NeuObj.clearNames()
- inputA1 = Input('inA1')
- inputA = Input('inA', dimensions=3)
- inputB = Input('inB', dimensions=3)
- out = Output('out', inputA1.sw(1)+Fir(Linear(inputA.tw(0.05)+inputB.tw(0.05))))
+ inputA1 = Input("inA1")
+ inputA = Input("inA", dimensions=3)
+ inputB = Input("inB", dimensions=3)
+ out = Output(
+ "out", inputA1.sw(1) + Fir(Linear(inputA.tw(0.05) + inputB.tw(0.05)))
+ )
test = Modely(visualizer=None)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.01)
- data_struct = ['','y1','x2','y2','',('A1x','inA1'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time']
- test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((11,5,3), test._data['dataset_1']['inA'].shape)
- self.assertEqual([[0.984,12.493, 0.0],[0.983,12.493, 0.01],[0.982,12.495, 0.02],[0.98,12.498, 0.03],[0.977,12.502, 0.04]], test._data['dataset_1']['inA'][0].tolist())
- self.assertEqual((11,5,3), test._data['dataset_1']['inB'].shape)
- self.assertEqual([[0.984, 12.493, 0.0], [0.983, 12.493, 0.01], [0.982, 12.495, 0.02], [0.98, 12.498, 0.03],
- [0.977, 12.502, 0.04]], test._data['dataset_1']['inB'][0].tolist())
+ data_struct = [
+ "",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ ("A1x", "inA1"),
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ ("inA", "inB"),
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset_1",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((11, 5, 3), test._data["dataset_1"]["inA"].shape)
+ self.assertEqual(
+ [
+ [0.984, 12.493, 0.0],
+ [0.983, 12.493, 0.01],
+ [0.982, 12.495, 0.02],
+ [0.98, 12.498, 0.03],
+ [0.977, 12.502, 0.04],
+ ],
+ test._data["dataset_1"]["inA"][0].tolist(),
+ )
+ self.assertEqual((11, 5, 3), test._data["dataset_1"]["inB"].shape)
+ self.assertEqual(
+ [
+ [0.984, 12.493, 0.0],
+ [0.983, 12.493, 0.01],
+ [0.982, 12.495, 0.02],
+ [0.98, 12.498, 0.03],
+ [0.977, 12.502, 0.04],
+ ],
+ test._data["dataset_1"]["inB"][0].tolist(),
+ )
with self.assertRaises(ValueError):
- data_struct = ['','y1','x2','y2','',('inA1','inA'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time']
- test.loadData(name='dataset_2', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ ("inA1", "inA"),
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ ("inA", "inB"),
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset_2",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
with self.assertRaises(ValueError):
- data_struct = ['','y1','x2','y2','',('inA1','inA'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','inB','theta','time']
- test.loadData(name='dataset_3', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ ("inA1", "inA"),
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "inB",
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset_3",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
def test_build_multi_dataset_simple(self):
NeuObj.clearNames()
- input = Input('in1')
- output = Input('out')
+ input = Input("in1")
+ output = Input("out")
relation = Fir(input.tw(0.05))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), relation)
+ test.addMinimize("out", output.z(-1), relation)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time']
-
- test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "theta",
+ "time",
+ ]
+
+ test.loadData(
+ name="train_dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="validation_dataset",
+ source=val_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="test_dataset",
+ source=test_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
self.assertEqual(3, test._Loader__n_datasets)
- self.assertEqual((10,5,1), test._data['train_dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['train_dataset']['in1'][0].tolist())
- self.assertEqual((6,5,1), test._data['validation_dataset']['in1'].shape)
- self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877]], test._data['validation_dataset']['in1'][0].tolist())
- self.assertEqual((8,5,1), test._data['test_dataset']['in1'].shape)
- self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777]], test._data['test_dataset']['in1'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape)
- self.assertEqual([[1.225]], test._data['train_dataset']['out'][0].tolist())
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist())
- self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape)
- self.assertEqual([[2.225]], test._data['validation_dataset']['out'][0].tolist())
- self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist())
- self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape)
- self.assertEqual([[3.225]], test._data['test_dataset']['out'][0].tolist())
- self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist())
-
+ self.assertEqual((10, 5, 1), test._data["train_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["train_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((6, 5, 1), test._data["validation_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.884], [0.883], [0.882], [0.88], [0.877]],
+ test._data["validation_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((8, 5, 1), test._data["test_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.784], [0.783], [0.782], [0.78], [0.777]],
+ test._data["test_dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape)
+ self.assertEqual([[1.225]], test._data["train_dataset"]["out"][0].tolist())
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["train_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape)
+ self.assertEqual([[2.225]], test._data["validation_dataset"]["out"][0].tolist())
+ self.assertEqual(
+ [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]],
+ test._data["validation_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape)
+ self.assertEqual([[3.225]], test._data["test_dataset"]["out"][0].tolist())
+ self.assertEqual(
+ [
+ [[3.225]],
+ [[3.224]],
+ [[3.222]],
+ [[3.22]],
+ [[3.217]],
+ [[3.214]],
+ [[3.211]],
+ [[3.207]],
+ ],
+ test._data["test_dataset"]["out"].tolist(),
+ )
+
def test_build_dataset_medium1(self):
NeuObj.clearNames()
- input = Input('in1')
- output = Input('out')
+ input = Input("in1")
+ output = Input("out")
rel1 = Fir(input.tw(0.05))
rel2 = Fir(input.tw(0.01))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2)
+ test.addMinimize("out", output.z(-1), rel1 + rel2)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,5,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset']['in1'][0].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "theta",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
- self.assertEqual((10,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist())
-
def test_build_multi_dataset_medium1(self):
NeuObj.clearNames()
- input = Input('in1')
- output = Input('out')
+ input = Input("in1")
+ output = Input("out")
rel1 = Fir(input.tw(0.05))
rel2 = Fir(input.tw(0.01))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2)
+ test.addMinimize("out", output.z(-1), rel1 + rel2)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time']
-
- test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "theta",
+ "time",
+ ]
+
+ test.loadData(
+ name="train_dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="validation_dataset",
+ source=val_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="test_dataset",
+ source=test_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
self.assertEqual(3, test._Loader__n_datasets)
- self.assertEqual((10,5,1), test._data['train_dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['train_dataset']['in1'][0].tolist())
- self.assertEqual((6,5,1), test._data['validation_dataset']['in1'].shape)
- self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877]], test._data['validation_dataset']['in1'][0].tolist())
- self.assertEqual((8,5,1), test._data['test_dataset']['in1'].shape)
- self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777]], test._data['test_dataset']['in1'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist())
- self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape)
- self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist())
- self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape)
- self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist())
-
+ self.assertEqual((10, 5, 1), test._data["train_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["train_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((6, 5, 1), test._data["validation_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.884], [0.883], [0.882], [0.88], [0.877]],
+ test._data["validation_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((8, 5, 1), test._data["test_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.784], [0.783], [0.782], [0.78], [0.777]],
+ test._data["test_dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["train_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]],
+ test._data["validation_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[3.225]],
+ [[3.224]],
+ [[3.222]],
+ [[3.22]],
+ [[3.217]],
+ [[3.214]],
+ [[3.211]],
+ [[3.207]],
+ ],
+ test._data["test_dataset"]["out"].tolist(),
+ )
+
def test_build_dataset_medium2(self):
NeuObj.clearNames()
- input1 = Input('in1')
- input2 = Input('in2')
- output = Input('out')
+ input1 = Input("in1")
+ input2 = Input("in2")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw(0.01))
rel3 = Fir(input2.tw(0.02))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3)
+ test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,5,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset']['in1'][0].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 2, 1), test._data["dataset"]["in2"].shape)
+ self.assertEqual([[12.498], [12.502]], test._data["dataset"]["in2"][0].tolist())
+
+ self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
- self.assertEqual((10,2,1), test._data['dataset']['in2'].shape)
- self.assertEqual([[12.498], [12.502]], test._data['dataset']['in2'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist())
-
def test_build_dataset_complex1(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.02]))
+ rel2 = Fir(input1.tw([-0.01, 0.02]))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2)
+ test.addMinimize("out", output.z(-1), rel1 + rel2)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((9,7,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist())
-
- self.assertEqual((9,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
def test_build_multi_dataset_complex1(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.02]))
+ rel2 = Fir(input1.tw([-0.01, 0.02]))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2)
+ test.addMinimize("out", output.z(-1), rel1 + rel2)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
-
- test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+
+ test.loadData(
+ name="train_dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="validation_dataset",
+ source=val_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="test_dataset",
+ source=test_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
self.assertEqual(3, test._Loader__n_datasets)
- self.assertEqual((9,7,1), test._data['train_dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['train_dataset']['in1'][0].tolist())
- self.assertEqual((5,7,1), test._data['validation_dataset']['in1'].shape)
- self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877],[0.873],[0.869]], test._data['validation_dataset']['in1'][0].tolist())
- self.assertEqual((7,7,1), test._data['test_dataset']['in1'].shape)
- self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777],[0.773],[0.769]], test._data['test_dataset']['in1'][0].tolist())
-
- self.assertEqual((9,1,1), test._data['train_dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['train_dataset']['out'].tolist())
- self.assertEqual((5,1,1), test._data['validation_dataset']['out'].shape)
- self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]]], test._data['validation_dataset']['out'].tolist())
- self.assertEqual((7,1,1), test._data['test_dataset']['out'].shape)
- self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]]], test._data['test_dataset']['out'].tolist())
-
+ self.assertEqual((9, 7, 1), test._data["train_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]],
+ test._data["train_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((5, 7, 1), test._data["validation_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.884], [0.883], [0.882], [0.88], [0.877], [0.873], [0.869]],
+ test._data["validation_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((7, 7, 1), test._data["test_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.784], [0.783], [0.782], [0.78], [0.777], [0.773], [0.769]],
+ test._data["test_dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((9, 1, 1), test._data["train_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ ],
+ test._data["train_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((5, 1, 1), test._data["validation_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]]],
+ test._data["validation_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((7, 1, 1), test._data["test_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[3.225]],
+ [[3.224]],
+ [[3.222]],
+ [[3.22]],
+ [[3.217]],
+ [[3.214]],
+ [[3.211]],
+ ],
+ test._data["test_dataset"]["out"].tolist(),
+ )
+
def test_build_dataset_complex2(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.01]))
+ rel2 = Fir(input1.tw([-0.01, 0.01]))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1 + rel2)
+ test.addMinimize("out", output.z(-1), rel1 + rel2)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,6,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['dataset']['in1'][0].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 6, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
- self.assertEqual((10,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist())
-
def test_build_dataset_complex3(self):
NeuObj.clearNames()
- input1 = Input('in1')
- input2 = Input('in2')
- output = Input('out')
+ input1 = Input("in1")
+ input2 = Input("in2")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input2.tw(0.02))
- rel3 = Fir(input1.tw([-0.01,0.01]))
+ rel3 = Fir(input1.tw([-0.01, 0.01]))
rel4 = Fir(input2.last())
- fun = Output('out-net',rel1+rel2+rel3+rel4)
+ fun = Output("out-net", rel1 + rel2 + rel3 + rel4)
test = Modely(visualizer=None)
- test.addModel('fun', fun)
- test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3 + rel4)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3 + rel4)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,6,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['dataset']['in1'][0].tolist())
-
- self.assertEqual((10,2,1), test._data['dataset']['in2'].shape)
- self.assertEqual([[12.498], [12.502]], test._data['dataset']['in2'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 6, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 2, 1), test._data["dataset"]["in2"].shape)
+ self.assertEqual([[12.498], [12.502]], test._data["dataset"]["in2"][0].tolist())
+
+ self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
def test_build_multi_dataset_complex3(self):
NeuObj.clearNames()
- input1 = Input('in1')
- input2 = Input('in2')
- output = Input('out')
+ input1 = Input("in1")
+ input2 = Input("in2")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input2.tw(0.02))
- rel3 = Fir(input1.tw([-0.01,0.01]))
+ rel3 = Fir(input1.tw([-0.01, 0.01]))
rel4 = Fir(input2.last())
- fun = Output('out-net',rel1+rel2+rel3+rel4)
+ fun = Output("out-net", rel1 + rel2 + rel3 + rel4)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
- test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3 + rel4)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3 + rel4)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="train_dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="validation_dataset",
+ source=val_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="test_dataset",
+ source=test_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
self.assertEqual(3, test._Loader__n_datasets)
- self.assertEqual((10,6,1), test._data['train_dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['train_dataset']['in1'][0].tolist())
- self.assertEqual((6,6,1), test._data['validation_dataset']['in1'].shape)
- self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877],[0.873]], test._data['validation_dataset']['in1'][0].tolist())
- self.assertEqual((8,6,1), test._data['test_dataset']['in1'].shape)
- self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777],[0.773]], test._data['test_dataset']['in1'][0].tolist())
-
- self.assertEqual((10,2,1), test._data['train_dataset']['in2'].shape)
- self.assertEqual([[12.498], [12.502]], test._data['train_dataset']['in2'][0].tolist())
- self.assertEqual((6,2,1), test._data['validation_dataset']['in2'].shape)
- self.assertEqual([[12.498], [12.502]], test._data['validation_dataset']['in2'][0].tolist())
- self.assertEqual((8,2,1), test._data['test_dataset']['in2'].shape)
- self.assertEqual([[12.498], [12.502]], test._data['test_dataset']['in2'][0].tolist())
-
- self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist())
- self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape)
- self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist())
- self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape)
- self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist())
-
+ self.assertEqual((10, 6, 1), test._data["train_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]],
+ test._data["train_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((6, 6, 1), test._data["validation_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.884], [0.883], [0.882], [0.88], [0.877], [0.873]],
+ test._data["validation_dataset"]["in1"][0].tolist(),
+ )
+ self.assertEqual((8, 6, 1), test._data["test_dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.784], [0.783], [0.782], [0.78], [0.777], [0.773]],
+ test._data["test_dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((10, 2, 1), test._data["train_dataset"]["in2"].shape)
+ self.assertEqual(
+ [[12.498], [12.502]], test._data["train_dataset"]["in2"][0].tolist()
+ )
+ self.assertEqual((6, 2, 1), test._data["validation_dataset"]["in2"].shape)
+ self.assertEqual(
+ [[12.498], [12.502]], test._data["validation_dataset"]["in2"][0].tolist()
+ )
+ self.assertEqual((8, 2, 1), test._data["test_dataset"]["in2"].shape)
+ self.assertEqual(
+ [[12.498], [12.502]], test._data["test_dataset"]["in2"][0].tolist()
+ )
+
+ self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ [[1.2]],
+ ],
+ test._data["train_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]],
+ test._data["validation_dataset"]["out"].tolist(),
+ )
+ self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[3.225]],
+ [[3.224]],
+ [[3.222]],
+ [[3.22]],
+ [[3.217]],
+ [[3.214]],
+ [[3.211]],
+ [[3.207]],
+ ],
+ test._data["test_dataset"]["out"].tolist(),
+ )
+
def test_build_dataset_complex5(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.01]))
- rel3 = Fir(input1.tw([-0.02,0.02]))
- fun = Output('out-net',rel1+rel2+rel3)
+ rel2 = Fir(input1.tw([-0.01, 0.01]))
+ rel3 = Fir(input1.tw([-0.02, 0.02]))
+ fun = Output("out-net", rel1 + rel2 + rel3)
test = Modely(visualizer=None)
- test.addModel('fun', fun)
- test.addMinimize('out', output.z(-1), fun)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), fun)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((9,7,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist())
-
- self.assertEqual((9,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
def test_filter_data(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.01]))
- rel3 = Fir(input1.tw([-0.02,0.02]))
- fun = Output('out-net',rel1+rel2+rel3)
+ rel2 = Fir(input1.tw([-0.01, 0.01]))
+ rel3 = Fir(input1.tw([-0.02, 0.02]))
+ fun = Output("out-net", rel1 + rel2 + rel3)
test = Modely(visualizer=None)
- test.addModel('fun', fun)
- test.addMinimize('out', output.z(-1), fun)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), fun)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
def filter_fn(sample):
- return min(sample['in1']) > 0.957
+ return min(sample["in1"]) > 0.957
test.filterData(filter_fn)
- self.assertEqual((2,7,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist())
-
- self.assertEqual((2,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]]], test._data['dataset']['out'].tolist())
-
- test.loadData(name='dataset2', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.filterData(filter_fn, dataset_name='dataset2')
- self.assertEqual((2,7,1), test._data['dataset2']['in1'].shape)
- self.assertEqual((2, 1, 1), test._data['dataset2']['out'].shape)
+ self.assertEqual((2, 7, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((2, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual([[[1.225]], [[1.224]]], test._data["dataset"]["out"].tolist())
+
+ test.loadData(
+ name="dataset2",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.filterData(filter_fn, dataset_name="dataset2")
+ self.assertEqual((2, 7, 1), test._data["dataset2"]["in1"].shape)
+ self.assertEqual((2, 1, 1), test._data["dataset2"]["out"].shape)
def test_build_dataset_complex6(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.02]))
- rel3 = Fir(input1.tw([-0.05,0.01]))
- fun = Output('out-net',rel1+rel2+rel3)
+ rel2 = Fir(input1.tw([-0.01, 0.02]))
+ rel3 = Fir(input1.tw([-0.05, 0.01]))
+ fun = Output("out-net", rel1 + rel2 + rel3)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
- test.addMinimize('out', output.z(-1), fun)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), fun)
test.neuralizeModel(0.01)
- data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((9,7,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist())
-
- self.assertEqual((9,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist())
+ data_struct = [
+ "x1",
+ "y1",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]],
+ test._data["dataset"]["in1"][0].tolist(),
+ )
+
+ self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [
+ [[1.225]],
+ [[1.224]],
+ [[1.222]],
+ [[1.22]],
+ [[1.217]],
+ [[1.214]],
+ [[1.211]],
+ [[1.207]],
+ [[1.204]],
+ ],
+ test._data["dataset"]["out"].tolist(),
+ )
def test_build_dataset_custom(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
- rel2 = Fir(input1.tw([-0.01,0.02]))
- rel3 = Fir(input1.tw([-0.05,0.01]))
- fun = Output('out-net',rel1+rel2+rel3)
+ rel2 = Fir(input1.tw([-0.01, 0.02]))
+ rel3 = Fir(input1.tw([-0.05, 0.01]))
+ fun = Output("out-net", rel1 + rel2 + rel3)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
- test.addMinimize('out', output.z(-1), fun)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), fun)
test.neuralizeModel(0.01)
data_x = np.array(range(10))
data_a = 2
data_b = -3
- dataset = {'in1': data_x, 'out': (data_a*data_x) + data_b}
-
- test.loadData(name='dataset',source=dataset)
- self.assertEqual((4,7,1), test._data['dataset']['in1'].shape)
- self.assertEqual([[[0],[1],[2],[3],[4],[5],[6]],
- [[1],[2],[3],[4],[5],[6],[7]],
- [[2],[3],[4],[5],[6],[7],[8]],
- [[3],[4],[5],[6],[7],[8],[9]]],
- test._data['dataset']['in1'].tolist())
-
- self.assertEqual((4,1,1), test._data['dataset']['out'].shape)
- self.assertEqual([[[7]],[[9]],[[11]],[[13]]], test._data['dataset']['out'].tolist())
+ dataset = {"in1": data_x, "out": (data_a * data_x) + data_b}
+
+ test.loadData(name="dataset", source=dataset)
+ self.assertEqual((4, 7, 1), test._data["dataset"]["in1"].shape)
+ self.assertEqual(
+ [
+ [[0], [1], [2], [3], [4], [5], [6]],
+ [[1], [2], [3], [4], [5], [6], [7]],
+ [[2], [3], [4], [5], [6], [7], [8]],
+ [[3], [4], [5], [6], [7], [8], [9]],
+ ],
+ test._data["dataset"]["in1"].tolist(),
+ )
+
+ self.assertEqual((4, 1, 1), test._data["dataset"]["out"].shape)
+ self.assertEqual(
+ [[[7]], [[9]], [[11]], [[13]]], test._data["dataset"]["out"].tolist()
+ )
def test_build_multi_dataset_custom(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
+ input1 = Input("in1")
+ output = Input("out")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw([-0.01, 0.02]))
rel3 = Fir(input1.tw([-0.05, 0.01]))
- fun = Output('out-net', rel1 + rel2 + rel3)
+ fun = Output("out-net", rel1 + rel2 + rel3)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
- test.addMinimize('out', output.z(-1), fun)
+ test.addModel("fun", fun)
+ test.addMinimize("out", output.z(-1), fun)
test.neuralizeModel(0.01)
train_data_x = np.array(range(10))
@@ -473,174 +1350,247 @@ def test_build_multi_dataset_custom(self):
test_data_x = np.array(range(20, 30))
data_a = 2
data_b = -3
- train_dataset = {'in1': train_data_x, 'out': (data_a * train_data_x) + data_b}
- val_dataset = {'in1': val_data_x, 'out': (data_a * val_data_x) + data_b}
- test_dataset = {'in1': test_data_x, 'out': (data_a * test_data_x) + data_b}
+ train_dataset = {"in1": train_data_x, "out": (data_a * train_data_x) + data_b}
+ val_dataset = {"in1": val_data_x, "out": (data_a * val_data_x) + data_b}
+ test_dataset = {"in1": test_data_x, "out": (data_a * test_data_x) + data_b}
- test.loadData(name='train_dataset', source=train_dataset)
- test.loadData(name='val_dataset', source=val_dataset)
- test.loadData(name='test_dataset', source=test_dataset)
+ test.loadData(name="train_dataset", source=train_dataset)
+ test.loadData(name="val_dataset", source=val_dataset)
+ test.loadData(name="test_dataset", source=test_dataset)
self.assertEqual(3, test._Loader__n_datasets)
- self.assertEqual((4, 7, 1), test._data['train_dataset']['in1'].shape)
- self.assertEqual([[[0], [1], [2], [3], [4], [5], [6]],
- [[1], [2], [3], [4], [5], [6], [7]],
- [[2], [3], [4], [5], [6], [7], [8]],
- [[3], [4], [5], [6], [7], [8], [9]]],
- test._data['train_dataset']['in1'].tolist())
- self.assertEqual((4, 7, 1), test._data['val_dataset']['in1'].shape)
- self.assertEqual([[[10], [11], [12], [13], [14], [15], [16]],
- [[11], [12], [13], [14], [15], [16], [17]],
- [[12], [13], [14], [15], [16], [17], [18]],
- [[13], [14], [15], [16], [17], [18], [19]]],
- test._data['val_dataset']['in1'].tolist())
- self.assertEqual((4, 7, 1), test._data['test_dataset']['in1'].shape)
- self.assertEqual([[[20], [21], [22], [23], [24], [25], [26]],
- [[21], [22], [23], [24], [25], [26], [27]],
- [[22], [23], [24], [25], [26], [27], [28]],
- [[23], [24], [25], [26], [27], [28], [29]]],
- test._data['test_dataset']['in1'].tolist())
-
- self.assertEqual((4, 1, 1), test._data['train_dataset']['out'].shape)
- self.assertEqual([[[7]], [[9]], [[11]], [[13]]], test._data['train_dataset']['out'].tolist())
- self.assertEqual((4, 1, 1), test._data['val_dataset']['out'].shape)
- self.assertEqual([[[27]], [[29]], [[31]], [[33]]], test._data['val_dataset']['out'].tolist())
- self.assertEqual((4, 1, 1), test._data['test_dataset']['out'].shape)
- self.assertEqual([[[47]], [[49]], [[51]], [[53]]], test._data['test_dataset']['out'].tolist())
+ self.assertEqual((4, 7, 1), test._data["train_dataset"]["in1"].shape)
+ self.assertEqual(
+ [
+ [[0], [1], [2], [3], [4], [5], [6]],
+ [[1], [2], [3], [4], [5], [6], [7]],
+ [[2], [3], [4], [5], [6], [7], [8]],
+ [[3], [4], [5], [6], [7], [8], [9]],
+ ],
+ test._data["train_dataset"]["in1"].tolist(),
+ )
+ self.assertEqual((4, 7, 1), test._data["val_dataset"]["in1"].shape)
+ self.assertEqual(
+ [
+ [[10], [11], [12], [13], [14], [15], [16]],
+ [[11], [12], [13], [14], [15], [16], [17]],
+ [[12], [13], [14], [15], [16], [17], [18]],
+ [[13], [14], [15], [16], [17], [18], [19]],
+ ],
+ test._data["val_dataset"]["in1"].tolist(),
+ )
+ self.assertEqual((4, 7, 1), test._data["test_dataset"]["in1"].shape)
+ self.assertEqual(
+ [
+ [[20], [21], [22], [23], [24], [25], [26]],
+ [[21], [22], [23], [24], [25], [26], [27]],
+ [[22], [23], [24], [25], [26], [27], [28]],
+ [[23], [24], [25], [26], [27], [28], [29]],
+ ],
+ test._data["test_dataset"]["in1"].tolist(),
+ )
+
+ self.assertEqual((4, 1, 1), test._data["train_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[7]], [[9]], [[11]], [[13]]], test._data["train_dataset"]["out"].tolist()
+ )
+ self.assertEqual((4, 1, 1), test._data["val_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[27]], [[29]], [[31]], [[33]]], test._data["val_dataset"]["out"].tolist()
+ )
+ self.assertEqual((4, 1, 1), test._data["test_dataset"]["out"].shape)
+ self.assertEqual(
+ [[[47]], [[49]], [[51]], [[53]]], test._data["test_dataset"]["out"].tolist()
+ )
def test_vector_input_dataset(self):
NeuObj.clearNames()
- x = Input('x', dimensions=4)
- y = Input('y', dimensions=3)
- k = Input('k', dimensions=2)
- w = Input('w')
+ x = Input("x", dimensions=4)
+ y = Input("y", dimensions=3)
+ k = Input("k", dimensions=2)
+ w = Input("w")
-
- out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
- out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02)))))
+ out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
+ out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02)))))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
+ test.addMinimize("out", out, out2)
test.neuralizeModel(0.01)
## Custom dataset
- data_x = np.transpose(np.array(
- [np.linspace(1,100,100, dtype=np.float32),
- np.linspace(2, 101, 100, dtype=np.float32),
- np.linspace(3, 102, 100, dtype=np.float32),
- np.linspace(4, 103, 100, dtype=np.float32)]))
- data_y = np.transpose(np.array(
- [np.linspace(1,100,100, dtype=np.float32) + 10,
- np.linspace(2, 101, 100, dtype=np.float32) + 10,
- np.linspace(3, 102, 100, dtype=np.float32) + 10]))
- data_k = np.transpose(np.array(
- [np.linspace(1,100,100, dtype=np.float32) + 20,
- np.linspace(2, 101, 100, dtype=np.float32) + 20]))
- data_w = np.linspace(1,100,100, dtype=np.float32) + 30
- dataset = {'x': data_x, 'y': data_y, 'w': data_w, 'k': data_k}
-
- test.loadData(name='dataset', source=dataset)
-
- self.assertEqual((96, 2, 4), test._data['dataset']['x'].shape)
- self.assertEqual((96, 2, 3), test._data['dataset']['y'].shape)
- self.assertEqual((96, 1, 2), test._data['dataset']['k'].shape)
- self.assertEqual((96, 5, 1), test._data['dataset']['w'].shape)
-
- self.assertEqual([[4.0, 5.0, 6.0, 7.0], [5.0, 6.0, 7.0, 8.0]],
- test._data['dataset']['x'][0].tolist())
- self.assertEqual([[5.0, 6.0, 7.0, 8.0],[6.0, 7.0, 8.0, 9.0]],
- test._data['dataset']['x'][1].tolist())
- self.assertEqual([[99, 100, 101, 102], [100, 101, 102, 103]],
- test._data['dataset']['x'][-1].tolist())
- self.assertEqual([[98, 99, 100, 101],[99, 100, 101, 102]],
- test._data['dataset']['x'][-2].tolist())
-
- self.assertEqual([[14.0, 15.0, 16.0], [15.0, 16.0, 17.0]],
- test._data['dataset']['y'][0].tolist())
- self.assertEqual([[15.0, 16.0, 17.0], [16.0, 17.0, 18.0]],
- test._data['dataset']['y'][1].tolist())
- self.assertEqual([[109, 110, 111], [110, 111, 112]],
- test._data['dataset']['y'][-1].tolist())
- self.assertEqual([[108, 109, 110], [109, 110, 111]],
- test._data['dataset']['y'][-2].tolist())
-
- self.assertEqual([[25.0, 26.0]],
- test._data['dataset']['k'][0].tolist())
- self.assertEqual([[26.0, 27.0]],
- test._data['dataset']['k'][1].tolist())
- self.assertEqual([[120, 121]],
- test._data['dataset']['k'][-1].tolist())
- self.assertEqual([[119, 120]],
- test._data['dataset']['k'][-2].tolist())
-
- self.assertEqual([[31], [32], [33], [34], [35]],
- test._data['dataset']['w'][0].tolist())
- self.assertEqual([[32], [33], [34], [35], [36]],
- test._data['dataset']['w'][1].tolist())
- self.assertEqual([[126], [127], [128], [129], [130]],
- test._data['dataset']['w'][-1].tolist())
- self.assertEqual([[125], [126], [127], [128], [129]],
- test._data['dataset']['w'][-2].tolist())
+ data_x = np.transpose(
+ np.array(
+ [
+ np.linspace(1, 100, 100, dtype=np.float32),
+ np.linspace(2, 101, 100, dtype=np.float32),
+ np.linspace(3, 102, 100, dtype=np.float32),
+ np.linspace(4, 103, 100, dtype=np.float32),
+ ]
+ )
+ )
+ data_y = np.transpose(
+ np.array(
+ [
+ np.linspace(1, 100, 100, dtype=np.float32) + 10,
+ np.linspace(2, 101, 100, dtype=np.float32) + 10,
+ np.linspace(3, 102, 100, dtype=np.float32) + 10,
+ ]
+ )
+ )
+ data_k = np.transpose(
+ np.array(
+ [
+ np.linspace(1, 100, 100, dtype=np.float32) + 20,
+ np.linspace(2, 101, 100, dtype=np.float32) + 20,
+ ]
+ )
+ )
+ data_w = np.linspace(1, 100, 100, dtype=np.float32) + 30
+ dataset = {"x": data_x, "y": data_y, "w": data_w, "k": data_k}
+
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertEqual((96, 2, 4), test._data["dataset"]["x"].shape)
+ self.assertEqual((96, 2, 3), test._data["dataset"]["y"].shape)
+ self.assertEqual((96, 1, 2), test._data["dataset"]["k"].shape)
+ self.assertEqual((96, 5, 1), test._data["dataset"]["w"].shape)
+
+ self.assertEqual(
+ [[4.0, 5.0, 6.0, 7.0], [5.0, 6.0, 7.0, 8.0]],
+ test._data["dataset"]["x"][0].tolist(),
+ )
+ self.assertEqual(
+ [[5.0, 6.0, 7.0, 8.0], [6.0, 7.0, 8.0, 9.0]],
+ test._data["dataset"]["x"][1].tolist(),
+ )
+ self.assertEqual(
+ [[99, 100, 101, 102], [100, 101, 102, 103]],
+ test._data["dataset"]["x"][-1].tolist(),
+ )
+ self.assertEqual(
+ [[98, 99, 100, 101], [99, 100, 101, 102]],
+ test._data["dataset"]["x"][-2].tolist(),
+ )
+
+ self.assertEqual(
+ [[14.0, 15.0, 16.0], [15.0, 16.0, 17.0]],
+ test._data["dataset"]["y"][0].tolist(),
+ )
+ self.assertEqual(
+ [[15.0, 16.0, 17.0], [16.0, 17.0, 18.0]],
+ test._data["dataset"]["y"][1].tolist(),
+ )
+ self.assertEqual(
+ [[109, 110, 111], [110, 111, 112]], test._data["dataset"]["y"][-1].tolist()
+ )
+ self.assertEqual(
+ [[108, 109, 110], [109, 110, 111]], test._data["dataset"]["y"][-2].tolist()
+ )
+
+ self.assertEqual([[25.0, 26.0]], test._data["dataset"]["k"][0].tolist())
+ self.assertEqual([[26.0, 27.0]], test._data["dataset"]["k"][1].tolist())
+ self.assertEqual([[120, 121]], test._data["dataset"]["k"][-1].tolist())
+ self.assertEqual([[119, 120]], test._data["dataset"]["k"][-2].tolist())
+
+ self.assertEqual(
+ [[31], [32], [33], [34], [35]], test._data["dataset"]["w"][0].tolist()
+ )
+ self.assertEqual(
+ [[32], [33], [34], [35], [36]], test._data["dataset"]["w"][1].tolist()
+ )
+ self.assertEqual(
+ [[126], [127], [128], [129], [130]], test._data["dataset"]["w"][-1].tolist()
+ )
+ self.assertEqual(
+ [[125], [126], [127], [128], [129]], test._data["dataset"]["w"][-2].tolist()
+ )
def test_vector_input_dataset_files(self):
NeuObj.clearNames()
- x = Input('x', dimensions=4)
- y = Input('y', dimensions=3)
- k = Input('k', dimensions=2)
- w = Input('w')
+ x = Input("x", dimensions=4)
+ y = Input("y", dimensions=3)
+ k = Input("k", dimensions=2)
+ w = Input("w")
- out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
- out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02)))))
+ out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
+ out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02)))))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
+ test.addMinimize("out", out, out2)
test.neuralizeModel(0.01)
- data_folder = os.path.join(os.path.dirname(__file__), 'vector_data/')
- data_struct = ['x', 'y', '','', '', '', 'k', '', '', '', 'w']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1, delimiter='\t', header=None)
-
- self.assertEqual((22, 2, 4), test._data['dataset']['x'].shape)
- self.assertEqual((22, 2, 3), test._data['dataset']['y'].shape)
- self.assertEqual((22, 1, 2), test._data['dataset']['k'].shape)
- self.assertEqual((22, 5, 1), test._data['dataset']['w'].shape)
-
- self.assertEqual([[0.804, 0.825, 0.320, 0.488], [0.805, 0.825, 0.322, 0.485]],
- test._data['dataset']['x'][0].tolist())
- self.assertEqual([[0.805, 0.825, 0.322, 0.485],[0.806, 0.824, 0.325, 0.481]],
- test._data['dataset']['x'][1].tolist())
- self.assertEqual([[0.806, 0.824, 0.325, 0.481], [0.807, 0.823, 0.329, 0.477]],
- test._data['dataset']['x'][-1].tolist())
- self.assertEqual([[0.805, 0.825, 0.322, 0.485],[0.806, 0.824, 0.325, 0.481]],
- test._data['dataset']['x'][-2].tolist())
-
- self.assertEqual([[0.350, 1.375, 0.586], [0.350, 1.375, 0.585]],
- test._data['dataset']['y'][0].tolist())
- self.assertEqual([[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]],
- test._data['dataset']['y'][1].tolist())
- self.assertEqual([[0.350, 1.375, 0.584], [0.350, 1.375, 0.582]],
- test._data['dataset']['y'][-1].tolist())
- self.assertEqual([[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]],
- test._data['dataset']['y'][-2].tolist())
-
- self.assertEqual([[0.714, 1.227]],
- test._data['dataset']['k'][0].tolist())
- self.assertEqual([[0.712, 1.225]],
- test._data['dataset']['k'][1].tolist())
- self.assertEqual([[0.710, 1.224]],
- test._data['dataset']['k'][-1].tolist())
- self.assertEqual([[0.712, 1.225]],
- test._data['dataset']['k'][-2].tolist())
-
- self.assertEqual([[12.493], [12.493], [12.495], [12.498], [12.502]],
- test._data['dataset']['w'][0].tolist())
- self.assertEqual([[12.493], [12.495], [12.498], [12.502], [12.508]],
- test._data['dataset']['w'][1].tolist())
- self.assertEqual([[12.495], [12.498], [12.502], [12.508], [12.515]],
- test._data['dataset']['w'][-1].tolist())
- self.assertEqual([[12.493], [12.495], [12.498], [12.502], [12.508]],
- test._data['dataset']['w'][-2].tolist())
+ data_folder = os.path.join(os.path.dirname(__file__), "vector_data/")
+ data_struct = ["x", "y", "", "", "", "", "k", "", "", "", "w"]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=1,
+ delimiter="\t",
+ header=None,
+ )
+
+ self.assertEqual((22, 2, 4), test._data["dataset"]["x"].shape)
+ self.assertEqual((22, 2, 3), test._data["dataset"]["y"].shape)
+ self.assertEqual((22, 1, 2), test._data["dataset"]["k"].shape)
+ self.assertEqual((22, 5, 1), test._data["dataset"]["w"].shape)
+
+ self.assertEqual(
+ [[0.804, 0.825, 0.320, 0.488], [0.805, 0.825, 0.322, 0.485]],
+ test._data["dataset"]["x"][0].tolist(),
+ )
+ self.assertEqual(
+ [[0.805, 0.825, 0.322, 0.485], [0.806, 0.824, 0.325, 0.481]],
+ test._data["dataset"]["x"][1].tolist(),
+ )
+ self.assertEqual(
+ [[0.806, 0.824, 0.325, 0.481], [0.807, 0.823, 0.329, 0.477]],
+ test._data["dataset"]["x"][-1].tolist(),
+ )
+ self.assertEqual(
+ [[0.805, 0.825, 0.322, 0.485], [0.806, 0.824, 0.325, 0.481]],
+ test._data["dataset"]["x"][-2].tolist(),
+ )
+
+ self.assertEqual(
+ [[0.350, 1.375, 0.586], [0.350, 1.375, 0.585]],
+ test._data["dataset"]["y"][0].tolist(),
+ )
+ self.assertEqual(
+ [[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]],
+ test._data["dataset"]["y"][1].tolist(),
+ )
+ self.assertEqual(
+ [[0.350, 1.375, 0.584], [0.350, 1.375, 0.582]],
+ test._data["dataset"]["y"][-1].tolist(),
+ )
+ self.assertEqual(
+ [[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]],
+ test._data["dataset"]["y"][-2].tolist(),
+ )
+
+ self.assertEqual([[0.714, 1.227]], test._data["dataset"]["k"][0].tolist())
+ self.assertEqual([[0.712, 1.225]], test._data["dataset"]["k"][1].tolist())
+ self.assertEqual([[0.710, 1.224]], test._data["dataset"]["k"][-1].tolist())
+ self.assertEqual([[0.712, 1.225]], test._data["dataset"]["k"][-2].tolist())
+
+ self.assertEqual(
+ [[12.493], [12.493], [12.495], [12.498], [12.502]],
+ test._data["dataset"]["w"][0].tolist(),
+ )
+ self.assertEqual(
+ [[12.493], [12.495], [12.498], [12.502], [12.508]],
+ test._data["dataset"]["w"][1].tolist(),
+ )
+ self.assertEqual(
+ [[12.495], [12.498], [12.502], [12.508], [12.515]],
+ test._data["dataset"]["w"][-1].tolist(),
+ )
+ self.assertEqual(
+ [[12.493], [12.495], [12.498], [12.502], [12.508]],
+ test._data["dataset"]["w"][-2].tolist(),
+ )
## Load from file
## Try to train the model
@@ -649,326 +1599,1145 @@ def test_vector_input_dataset_files(self):
def test_multifiles(self):
NeuObj.clearNames()
- x = Input('x')
+ x = Input("x")
relation = Fir()(x.tw(0.05))
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error', output, x.next())
+ test.addModel("model", output)
+ test.addMinimize("error", output, x.next())
test.neuralizeModel(0.01)
## The folder contains 3 files with 10, 20 and 30 samples respectively
- data_struct = ['x']
+ data_struct = ["x"]
## each folder contains 3 files with 10, 20 and 30 samples respectively
- data_folder = os.path.join(os.path.dirname(__file__), 'multifile/')
- data_folder2 = os.path.join(os.path.dirname(__file__), 'multifile2/')
+ data_folder = os.path.join(os.path.dirname(__file__), "multifile/")
+ data_folder2 = os.path.join(os.path.dirname(__file__), "multifile2/")
## this folder contains only one file with 50 samples
- data_folder3 = os.path.join(os.path.dirname(__file__), 'multifile3/')
- test.loadData(name='dataset1', source=data_folder, format=data_struct, skiplines=1)
- test.loadData(name='dataset2', source=data_folder2, format=data_struct, skiplines=1)
- test.loadData(name='dataset3', source=data_folder3, format=data_struct, skiplines=1)
-
- self.assertListEqual(list(test._data['dataset1']['x'].shape), [45, 6, 1])
- self.assertListEqual(test._multifile['dataset1'], [5, 20, 45])
- self.assertListEqual(list(test._data['dataset2']['x'].shape), [45, 6, 1])
- self.assertListEqual(test._multifile['dataset2'], [5, 20, 45])
- self.assertListEqual(list(test._data['dataset3']['x'].shape), [45, 6, 1])
- self.assertEqual(test._num_of_samples['dataset1'], 45) ## 5 + 15 + 25
- self.assertEqual(test._num_of_samples['dataset2'], 45) ## 5 + 15 + 25
- self.assertEqual(test._num_of_samples['dataset3'], 45) ## 50 - 5
+ data_folder3 = os.path.join(os.path.dirname(__file__), "multifile3/")
+ test.loadData(
+ name="dataset1", source=data_folder, format=data_struct, skiplines=1
+ )
+ test.loadData(
+ name="dataset2", source=data_folder2, format=data_struct, skiplines=1
+ )
+ test.loadData(
+ name="dataset3", source=data_folder3, format=data_struct, skiplines=1
+ )
+
+ self.assertListEqual(list(test._data["dataset1"]["x"].shape), [45, 6, 1])
+ self.assertListEqual(test._multifile["dataset1"], [5, 20, 45])
+ self.assertListEqual(list(test._data["dataset2"]["x"].shape), [45, 6, 1])
+ self.assertListEqual(test._multifile["dataset2"], [5, 20, 45])
+ self.assertListEqual(list(test._data["dataset3"]["x"].shape), [45, 6, 1])
+ self.assertEqual(test._num_of_samples["dataset1"], 45) ## 5 + 15 + 25
+ self.assertEqual(test._num_of_samples["dataset2"], 45) ## 5 + 15 + 25
+ self.assertEqual(test._num_of_samples["dataset3"], 45) ## 50 - 5
## train one dataset using splits
- test.trainModel(dataset='dataset1', splits=[80, 10, 10], prediction_samples=3, num_of_epochs=1)
+ test.trainModel(
+ dataset="dataset1",
+ splits=[80, 10, 10],
+ prediction_samples=3,
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 36) ## 45 * 0.8
- self.assertEqual(tp['n_samples_val'], 4) ## 45 * 0.1
- self.assertEqual(tp['n_samples_test'], 5) ## 45 * 0.1
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])
- self.assertEqual(test.running_parameters['val_indexes'], [0])
+ self.assertEqual(tp["n_samples_train"], 36) ## 45 * 0.8
+ self.assertEqual(tp["n_samples_val"], 4) ## 45 * 0.1
+ self.assertEqual(tp["n_samples_test"], 5) ## 45 * 0.1
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ ],
+ )
+ self.assertEqual(test.running_parameters["val_indexes"], [0])
## train using one dataset for train and one for validation
- test.trainModel(train_dataset='dataset1', validation_dataset='dataset2', prediction_samples=3, num_of_epochs=1)
+ test.trainModel(
+ train_dataset="dataset1",
+ validation_dataset="dataset2",
+ prediction_samples=3,
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 45)
- self.assertEqual(tp['n_samples_val'], 45)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41])
+ self.assertEqual(tp["n_samples_train"], 45)
+ self.assertEqual(tp["n_samples_val"], 45)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ ],
+ )
## train using two dataset for train and one for validation
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset='dataset3', prediction_samples=3, num_of_epochs=1)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset="dataset3",
+ prediction_samples=3,
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 45)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41])
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 45)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"],
+ [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ ],
+ )
## train using two dataset for train and two for validation
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3'], prediction_samples=3, num_of_epochs=1)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset=["dataset2", "dataset3"],
+ prediction_samples=3,
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 90)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86])
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 90)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 62,
+ 63,
+ 64,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ ],
+ )
## train using two dataset for train and two for validation (dataset4 is ignored)
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3','dataset4'], num_of_epochs=1, prediction_samples=3)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset=["dataset2", "dataset3", "dataset4"],
+ num_of_epochs=1,
+ prediction_samples=3,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 90)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86])
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 90)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 62,
+ 63,
+ 64,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ ],
+ )
## Use all datasets by default
test.trainModel(splits=[80, 10, 10], prediction_samples=3)
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8
- self.assertEqual(tp['n_samples_val'], 14) ## 135 * 0.1
- self.assertEqual(tp['n_samples_test'], 13) ## 135 * 0.1
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
- 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+ self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8
+ self.assertEqual(tp["n_samples_val"], 14) ## 135 * 0.1
+ self.assertEqual(tp["n_samples_test"], 13) ## 135 * 0.1
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ 90,
+ 91,
+ 92,
+ 93,
+ 94,
+ 95,
+ 96,
+ 97,
+ 98,
+ 99,
+ 100,
+ 101,
+ 102,
+ 103,
+ 104,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ )
## splits multifile
- test.trainModel(dataset=['dataset1', 'dataset2', 'dataset3'], splits=[80, 10, 10], prediction_samples=3)
+ test.trainModel(
+ dataset=["dataset1", "dataset2", "dataset3"],
+ splits=[80, 10, 10],
+ prediction_samples=3,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8
- self.assertEqual(tp['n_samples_val'], 14) ## 90 * 0.1
- self.assertEqual(tp['n_samples_test'], 13) ## 90 * 0.1
- self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
- 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
- 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104])
- self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+ self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8
+ self.assertEqual(tp["n_samples_val"], 14) ## 90 * 0.1
+ self.assertEqual(tp["n_samples_test"], 13) ## 90 * 0.1
+ self.assertEqual(
+ test.running_parameters["train_indexes"],
+ [
+ 0,
+ 1,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 45,
+ 46,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ 90,
+ 91,
+ 92,
+ 93,
+ 94,
+ 95,
+ 96,
+ 97,
+ 98,
+ 99,
+ 100,
+ 101,
+ 102,
+ 103,
+ 104,
+ ],
+ )
+ self.assertEqual(
+ test.running_parameters["val_indexes"], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ )
## train one dataset using splits
- test.trainModel(dataset='dataset1', splits=[80, 10, 10], num_of_epochs=1)
+ test.trainModel(dataset="dataset1", splits=[80, 10, 10], num_of_epochs=1)
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 36) ## 45 * 0.8
- self.assertEqual(tp['n_samples_val'], 4) ## 45 * 0.1
- self.assertEqual(tp['n_samples_test'], 5) ## 45 * 0.1
- self.assertEqual(test.running_parameters['train_indexes'], list(range(36)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(4)))
+ self.assertEqual(tp["n_samples_train"], 36) ## 45 * 0.8
+ self.assertEqual(tp["n_samples_val"], 4) ## 45 * 0.1
+ self.assertEqual(tp["n_samples_test"], 5) ## 45 * 0.1
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(36)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(4)))
## train using one dataset for train and one for validation
- test.trainModel(train_dataset='dataset1', validation_dataset='dataset2', num_of_epochs=1)
+ test.trainModel(
+ train_dataset="dataset1", validation_dataset="dataset2", num_of_epochs=1
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 45)
- self.assertEqual(tp['n_samples_val'], 45)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], list(range(45)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(45)))
+ self.assertEqual(tp["n_samples_train"], 45)
+ self.assertEqual(tp["n_samples_val"], 45)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(45)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(45)))
## train using two dataset for train and one for validation
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset='dataset3', num_of_epochs=1)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset="dataset3",
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 45)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], list(range(90)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(45)))
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 45)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(90)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(45)))
## train using two dataset for train and two for validation
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3'], num_of_epochs=1)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset=["dataset2", "dataset3"],
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 90)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], list(range(90)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(90)))
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 90)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(90)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(90)))
## train using two dataset for train and two for validation (dataset4 is ignored)
- test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3','dataset4'], num_of_epochs=1)
+ test.trainModel(
+ train_dataset=["dataset1", "dataset2"],
+ validation_dataset=["dataset2", "dataset3", "dataset4"],
+ num_of_epochs=1,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45
- self.assertEqual(tp['n_samples_val'], 90)
- self.assertEqual(tp['n_samples_test'], 0)
- self.assertEqual(test.running_parameters['train_indexes'], list(range(90)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(90)))
+ self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45
+ self.assertEqual(tp["n_samples_val"], 90)
+ self.assertEqual(tp["n_samples_test"], 0)
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(90)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(90)))
## splits multifile
- test.trainModel(dataset=['dataset1', 'dataset2', 'dataset3'], splits=[80, 10, 10])
+ test.trainModel(
+ dataset=["dataset1", "dataset2", "dataset3"], splits=[80, 10, 10]
+ )
tp = test.getTrainingInfo()
- self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8
- self.assertEqual(tp['n_samples_val'], 14) ## 90 * 0.1
- self.assertEqual(tp['n_samples_test'], 13) ## 90 * 0.1
- self.assertEqual(test.running_parameters['train_indexes'], list(range(108)))
- self.assertEqual(test.running_parameters['val_indexes'], list(range(14)))
+ self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8
+ self.assertEqual(tp["n_samples_val"], 14) ## 90 * 0.1
+ self.assertEqual(tp["n_samples_test"], 13) ## 90 * 0.1
+ self.assertEqual(test.running_parameters["train_indexes"], list(range(108)))
+ self.assertEqual(test.running_parameters["val_indexes"], list(range(14)))
def test_multifiles_2(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
- relation = Fir()(x.tw(0.05))+Fir(y.sw([-2,2]))
+ x = Input("x")
+ y = Input("y")
+ relation = Fir()(x.tw(0.05)) + Fir(y.sw([-2, 2]))
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error', output, x.next())
+ test.addModel("model", output)
+ test.addMinimize("error", output, x.next())
test.neuralizeModel(0.01)
## The folder contains 3 files with 10, 20 and 30 samples respectively
- data_struct = ['x', 'y']
- data_folder = os.path.join(os.path.dirname(__file__), 'multifile/')
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
- self.assertListEqual(list(test._data['dataset']['x'].shape), [42, 6, 1])
- self.assertListEqual(list(test._data['dataset']['y'].shape), [42, 4, 1])
- self.assertListEqual(test._multifile['dataset'], [4, 18, 42])
+ data_struct = ["x", "y"]
+ data_folder = os.path.join(os.path.dirname(__file__), "multifile/")
+ test.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
+ self.assertListEqual(list(test._data["dataset"]["x"].shape), [42, 6, 1])
+ self.assertListEqual(list(test._data["dataset"]["y"].shape), [42, 4, 1])
+ self.assertListEqual(test._multifile["dataset"], [4, 18, 42])
def test_dataframe_multidimensional(self):
import pandas as pd
+
NeuObj.clearNames()
- x = Input('x', dimensions=4)
- y = Input('y', dimensions=3)
- k = Input('k', dimensions=2)
- w = Input('w')
+ x = Input("x", dimensions=4)
+ y = Input("y", dimensions=3)
+ k = Input("k", dimensions=2)
+ w = Input("w")
- out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
- out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02)))))
+ out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
+ out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02)))))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
+ test.addMinimize("out", out, out2)
test.neuralizeModel(0.01)
# Create a DataFrame with random values for each input
- df = pd.DataFrame({
- 'x': [np.array([1.0,2.0,3.0,4.0]) for _ in range(10)],
- 'y': [np.array([5.0,6.0,7.0]) for _ in range(10)],
- 'k': [np.array([8.0,9.0]) for _ in range(10)],
- 'w': np.array([10.0,11.0,12.0,13.0,14.0,15.0,16.0,17.0,18.0,19.0])})
-
- test.loadData(name='dataset', source=df)
- self.assertEqual((6, 2, 4), test._data['dataset']['x'].shape)
- self.assertEqual((6, 2, 3), test._data['dataset']['y'].shape)
- self.assertEqual((6, 1, 2), test._data['dataset']['k'].shape)
- self.assertEqual((6, 5, 1), test._data['dataset']['w'].shape)
+ df = pd.DataFrame(
+ {
+ "x": [np.array([1.0, 2.0, 3.0, 4.0]) for _ in range(10)],
+ "y": [np.array([5.0, 6.0, 7.0]) for _ in range(10)],
+ "k": [np.array([8.0, 9.0]) for _ in range(10)],
+ "w": np.array(
+ [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]
+ ),
+ }
+ )
+
+ test.loadData(name="dataset", source=df)
+ self.assertEqual((6, 2, 4), test._data["dataset"]["x"].shape)
+ self.assertEqual((6, 2, 3), test._data["dataset"]["y"].shape)
+ self.assertEqual((6, 1, 2), test._data["dataset"]["k"].shape)
+ self.assertEqual((6, 5, 1), test._data["dataset"]["w"].shape)
def test_dataframe_single_dimension(self):
import pandas as pd
+
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
- k = Input('k')
- w = Input('w')
+ x = Input("x")
+ y = Input("y")
+ k = Input("k")
+ w = Input("w")
- out = Output('out', Fir(x.tw(0.02) + y.tw(0.02)))
- out2 = Output('out2', Fir(k.last()) + Fir(w.tw(0.05,offset=-0.02)))
+ out = Output("out", Fir(x.tw(0.02) + y.tw(0.02)))
+ out2 = Output("out2", Fir(k.last()) + Fir(w.tw(0.05, offset=-0.02)))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
+ test.addMinimize("out", out, out2)
test.neuralizeModel(0.01)
# Create a DataFrame with random values for each input
- df = pd.DataFrame({
- 'x': np.linspace(1,100,100, dtype=np.float32),
- 'y': np.linspace(1,100,100, dtype=np.float32),
- 'k': np.linspace(1,100,100, dtype=np.float32),
- 'w': np.linspace(1,100,100, dtype=np.float32)})
-
- test.loadData(name='dataset', source=df)
- self.assertEqual((96, 2, 1), test._data['dataset']['x'].shape)
- self.assertEqual((96, 2, 1), test._data['dataset']['y'].shape)
- self.assertEqual((96, 1, 1), test._data['dataset']['k'].shape)
- self.assertEqual((96, 5, 1), test._data['dataset']['w'].shape)
+ df = pd.DataFrame(
+ {
+ "x": np.linspace(1, 100, 100, dtype=np.float32),
+ "y": np.linspace(1, 100, 100, dtype=np.float32),
+ "k": np.linspace(1, 100, 100, dtype=np.float32),
+ "w": np.linspace(1, 100, 100, dtype=np.float32),
+ }
+ )
+
+ test.loadData(name="dataset", source=df)
+ self.assertEqual((96, 2, 1), test._data["dataset"]["x"].shape)
+ self.assertEqual((96, 2, 1), test._data["dataset"]["y"].shape)
+ self.assertEqual((96, 1, 1), test._data["dataset"]["k"].shape)
+ self.assertEqual((96, 5, 1), test._data["dataset"]["w"].shape)
def test_dataframe_resampling(self):
import pandas as pd
+
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
- k = Input('k')
- w = Input('w')
+ x = Input("x")
+ y = Input("y")
+ k = Input("k")
+ w = Input("w")
- out = Output('out', Fir(x.tw(1.0) + y.tw(1.0)))
- out2 = Output('out2', Fir(k.last()) + Fir(w.tw(2.5,offset=-1.0)))
+ out = Output("out", Fir(x.tw(1.0) + y.tw(1.0)))
+ out2 = Output("out2", Fir(k.last()) + Fir(w.tw(2.5, offset=-1.0)))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
+ test.addMinimize("out", out, out2)
test.neuralizeModel(0.5)
# Create a DataFrame with random values for each input
- df = pd.DataFrame({
- 'time': np.array([1.0,1.5,2.0,4.0,4.5,5.0,7.0,7.5,8.0,8.5], dtype=np.float32),
- 'x': np.linspace(1,10,10, dtype=np.float32),
- 'y': np.linspace(1,10,10, dtype=np.float32),
- 'k': np.linspace(1,10,10, dtype=np.float32),
- 'w': np.linspace(1,10,10, dtype=np.float32)})
-
- test.loadData(name='dataset1', source=df, resampling=True)
- self.assertEqual((12, 2, 1), test._data['dataset1']['x'].shape)
- self.assertEqual((12, 2, 1), test._data['dataset1']['y'].shape)
- self.assertEqual((12, 1, 1), test._data['dataset1']['k'].shape)
- self.assertEqual((12, 5, 1), test._data['dataset1']['w'].shape)
-
- df['time'] = pd.to_datetime(df['time'], unit='s')
- df = df.set_index('time', drop=True)
- test.loadData(name='dataset2', source=df, resampling=True)
- self.assertEqual((12, 2, 1), test._data['dataset2']['x'].shape)
- self.assertEqual((12, 2, 1), test._data['dataset2']['y'].shape)
- self.assertEqual((12, 1, 1), test._data['dataset2']['k'].shape)
- self.assertEqual((12, 5, 1), test._data['dataset2']['w'].shape)
-
- df2 = pd.DataFrame({
- 'x': np.linspace(1,10,10, dtype=np.float32),
- 'y': np.linspace(1,10,10, dtype=np.float32),
- 'k': np.linspace(1,10,10, dtype=np.float32),
- 'w': np.linspace(1,10,10, dtype=np.float32)})
+ df = pd.DataFrame(
+ {
+ "time": np.array(
+ [1.0, 1.5, 2.0, 4.0, 4.5, 5.0, 7.0, 7.5, 8.0, 8.5], dtype=np.float32
+ ),
+ "x": np.linspace(1, 10, 10, dtype=np.float32),
+ "y": np.linspace(1, 10, 10, dtype=np.float32),
+ "k": np.linspace(1, 10, 10, dtype=np.float32),
+ "w": np.linspace(1, 10, 10, dtype=np.float32),
+ }
+ )
+
+ test.loadData(name="dataset1", source=df, resampling=True)
+ self.assertEqual((12, 2, 1), test._data["dataset1"]["x"].shape)
+ self.assertEqual((12, 2, 1), test._data["dataset1"]["y"].shape)
+ self.assertEqual((12, 1, 1), test._data["dataset1"]["k"].shape)
+ self.assertEqual((12, 5, 1), test._data["dataset1"]["w"].shape)
+
+ df["time"] = pd.to_datetime(df["time"], unit="s")
+ df = df.set_index("time", drop=True)
+ test.loadData(name="dataset2", source=df, resampling=True)
+ self.assertEqual((12, 2, 1), test._data["dataset2"]["x"].shape)
+ self.assertEqual((12, 2, 1), test._data["dataset2"]["y"].shape)
+ self.assertEqual((12, 1, 1), test._data["dataset2"]["k"].shape)
+ self.assertEqual((12, 5, 1), test._data["dataset2"]["w"].shape)
+
+ df2 = pd.DataFrame(
+ {
+ "x": np.linspace(1, 10, 10, dtype=np.float32),
+ "y": np.linspace(1, 10, 10, dtype=np.float32),
+ "k": np.linspace(1, 10, 10, dtype=np.float32),
+ "w": np.linspace(1, 10, 10, dtype=np.float32),
+ }
+ )
with self.assertRaises(TypeError):
- test.loadData(name='dataset3', source=df2, resampling=True)
+ test.loadData(name="dataset3", source=df2, resampling=True)
def test_load_data_modalities(self):
import pandas as pd
+
NeuObj.clearNames()
- x = Input('x')
+ x = Input("x")
relation = Fir()(x.tw(0.05))
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error', output, x.next())
+ test.addModel("model", output)
+ test.addMinimize("error", output, x.next())
test.neuralizeModel(0.01)
## Case 1: directory with files
- data_struct = ['x']
- data_folder = os.path.join(os.path.dirname(__file__), 'multifile/')
- test.loadData(name='dataset_directory', source=data_folder, format=data_struct, skiplines=1)
- self.assertListEqual(list(test._data['dataset_directory']['x'].shape), [45, 6, 1])
- self.assertListEqual(test._multifile['dataset_directory'], [5, 20, 45])
+ data_struct = ["x"]
+ data_folder = os.path.join(os.path.dirname(__file__), "multifile/")
+ test.loadData(
+ name="dataset_directory",
+ source=data_folder,
+ format=data_struct,
+ skiplines=1,
+ )
+ self.assertListEqual(
+ list(test._data["dataset_directory"]["x"].shape), [45, 6, 1]
+ )
+ self.assertListEqual(test._multifile["dataset_directory"], [5, 20, 45])
## Case 2: dictionary
- train_data_x = np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32)
- train_dataset = {'x': train_data_x, 'time': np.array(range(60), dtype=np.float32)}
- test.loadData(name='dataset_dictionary', source=train_dataset, )
- self.assertListEqual(list(test._data['dataset_dictionary']['x'].shape), [55, 6, 1])
+ train_data_x = np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32)
+ train_dataset = {
+ "x": train_data_x,
+ "time": np.array(range(60), dtype=np.float32),
+ }
+ test.loadData(
+ name="dataset_dictionary",
+ source=train_dataset,
+ )
+ self.assertListEqual(
+ list(test._data["dataset_dictionary"]["x"].shape), [55, 6, 1]
+ )
## Case 3: pandas DataFrame
- df = pd.DataFrame({
- 'time': np.array(range(60), dtype=np.float32),
- 'x': np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32)})
- test.loadData(name='dataset_pandas', source=df, resampling=True)
- self.assertListEqual(list(test._data['dataset_pandas']['x'].shape), [5896, 6, 1])
+ df = pd.DataFrame(
+ {
+ "time": np.array(range(60), dtype=np.float32),
+ "x": np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32),
+ }
+ )
+ test.loadData(name="dataset_pandas", source=df, resampling=True)
+ self.assertListEqual(
+ list(test._data["dataset_pandas"]["x"].shape), [5896, 6, 1]
+ )
diff --git a/tests/test_documentation.py b/tests/test_documentation.py
index 71d27a97..10897634 100644
--- a/tests/test_documentation.py
+++ b/tests/test_documentation.py
@@ -1,26 +1,32 @@
import unittest
-import subprocess
-import os
-class TestDocumentation(unittest.TestCase):
- def test_generate_docs(self):
- # Path to the Sphinx documentation source directory
- docs_source_dir = os.path.join(os.path.dirname(__file__), '..', 'docs')
-
- # Path to the output directory for the generated documentation
- docs_output_dir = os.path.join(docs_source_dir, '_build', 'html')
-
- # Command to generate the documentation
- #TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir]
- command = ['sphinx-build', '-b', 'html', docs_source_dir, docs_output_dir]
- # Run the command and capture the output
- result = subprocess.run(command, capture_output=True, text=True)
-
- # Check if the command was successful
- self.assertEqual(result.returncode, 0, f"Documentation generation failed: {result.stderr}")
-
- # Optionally, check if the output directory contains the expected files
- self.assertTrue(os.path.exists(docs_output_dir), "Output directory does not exist")
- self.assertTrue(os.path.isfile(os.path.join(docs_output_dir, 'index.html')),
- "index.html not found in output directory")
\ No newline at end of file
+class TestDocumentation(unittest.TestCase):
+ pass
+ # def test_generate_docs(self):
+ # # Path to the Sphinx documentation source directory
+ # docs_source_dir = os.path.join(os.path.dirname(__file__), "..", "docs")
+ #
+ # # Path to the output directory for the generated documentation
+ # docs_output_dir = os.path.join(docs_source_dir, "_build", "html")
+ #
+ # # Command to generate the documentation
+ # # TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir]
+ # command = ["sphinx-build", "-b", "html", docs_source_dir, docs_output_dir]
+ #
+ # # Run the command and capture the output
+ # result = subprocess.run(command, capture_output=True, text=True)
+ #
+ # # Check if the command was successful
+ # self.assertEqual(
+ # result.returncode, 0, f"Documentation generation failed: {result.stderr}"
+ # )
+ #
+ # # Optionally, check if the output directory contains the expected files
+ # self.assertTrue(
+ # os.path.exists(docs_output_dir), "Output directory does not exist"
+ # )
+ # self.assertTrue(
+ # os.path.isfile(os.path.join(docs_output_dir, "index.html")),
+ # "index.html not found in output directory",
+ # )
diff --git a/tests/test_export.py b/tests/test_export.py
index cd36e464..6cb0dbe6 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -1,8 +1,10 @@
-import os, unittest, torch, shutil
+import os
+import unittest
+import torch
+import shutil
import numpy as np
from nnodely import *
-from nnodely.basic.relation import NeuObj
from nnodely.support.logger import logging, nnLogger
log = nnLogger(__name__, logging.CRITICAL)
@@ -11,12 +13,17 @@
# 11 Tests
# Test of export and import the network to a file in different format
-class ModelyExportTest(unittest.TestCase):
+class ModelyExportTest(unittest.TestCase):
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
- self.assertEqual(len(data1),len(data2))
+ self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.TestAlmostEqual(pred, label, precision=precision)
else:
@@ -26,28 +33,32 @@ def __init__(self, *args, **kwargs):
super(ModelyExportTest, self).__init__(*args, **kwargs)
clearNames()
- self.result_path = './results'
+ self.result_path = "./results"
self.test = Modely(visualizer=None, seed=42, workspace=self.result_path)
- x = Input('x')
- y = Input('y')
- z = Input('z')
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
## create the relations
def myFun(K1, p1, p2):
return K1 * p1 * p2
- K_x = Parameter('k_x', dimensions=1, tw=1, init='init_constant', init_params={'value': 1})
- K_y = Parameter('k_y', dimensions=1, tw=1)
- w = Parameter('w', dimensions=1, tw=1, init='init_constant', init_params={'value': 1})
- t = Parameter('t', dimensions=1, tw=1)
- c_v = Constant('c_v', tw=1, values=[[1], [2]])
+ K_x = Parameter(
+ "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1}
+ )
+ K_y = Parameter("k_y", dimensions=1, tw=1)
+ w = Parameter(
+ "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1}
+ )
+ t = Parameter("t", dimensions=1, tw=1)
+ c_v = Constant("c_v", tw=1, values=[[1], [2]])
c = 5
- w_5 = Parameter('w_5', dimensions=1, tw=5)
- t_5 = Parameter('t_5', dimensions=1, tw=5)
+ w_5 = Parameter("w_5", dimensions=1, tw=5)
+ t_5 = Parameter("t_5", dimensions=1, tw=5)
c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
- c_5_2 = Constant('c_5_2', tw=5, values=c_5)
- parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v])
+ c_5_2 = Constant("c_5_2", tw=5, values=c_5)
+ parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v])
parfun_y = ParamFun(myFun, parameters_and_constants=[K_y])
parfun_z = ParamFun(myFun)
fir_w = Fir(W=w_5)(x.tw(5))
@@ -61,24 +72,28 @@ def fuzzyfun(x):
fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1))
fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1))
- out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1),c_v)))
- out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
- out3 = Output('out3', Add(fir_w, fir_t))
- out4 = Output('out4', Linear(output_dimension=1)(fuzzy+fuzzyTriang))
- out5 = Output('out5', Fir(time_part) + Fir(sample_select))
- out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy))
+ out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)))
+ out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
+ out3 = Output("out3", Add(fir_w, fir_t))
+ out4 = Output("out4", Linear(output_dimension=1)(fuzzy + fuzzyTriang))
+ out5 = Output("out5", Fir(time_part) + Fir(sample_select))
+ out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy))
with self.assertRaises(TypeError):
parfun_z(x.tw(5), t_5, c_5)
- out7 = Output('out7', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_z(x.tw(5), t_5, c_5_2)))
-
- self.test.addModel('modelA', out)
- self.test.addModel('modelB', [out2, out3, out4])
- self.test.addModel('modelC', [out4, out5, out6])
- self.test.addModel('modelD', [out7])
- self.test.addMinimize('error1', x.last(), out)
- self.test.addMinimize('error2', y.last(), out3, loss_function='rmse')
- self.test.addMinimize('error3', z.last(), out6, loss_function='rmse')
- self.test.addMinimize('error4', z.last(), out7, loss_function='rmse')
+ out7 = Output(
+ "out7",
+ Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))
+ + Fir(parfun_z(x.tw(5), t_5, c_5_2)),
+ )
+
+ self.test.addModel("modelA", out)
+ self.test.addModel("modelB", [out2, out3, out4])
+ self.test.addModel("modelC", [out4, out5, out6])
+ self.test.addModel("modelD", [out7])
+ self.test.addMinimize("error1", x.last(), out)
+ self.test.addMinimize("error2", y.last(), out3, loss_function="rmse")
+ self.test.addMinimize("error3", z.last(), out6, loss_function="rmse")
+ self.test.addMinimize("error4", z.last(), out7, loss_function="rmse")
def test_export_pt(self):
if os.path.exists(self.test.getWorkspace()):
@@ -87,21 +102,36 @@ def test_export_pt(self):
# Export torch file .pt
# Save torch model and load it
self.test.neuralizeModel(0.5)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.saveTorchModel()
self.test.neuralizeModel(clear_model=True)
# The new_out is different from the old_out because the model is cleared
- new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
# The new_out_after_load is the same as the old_out because the model is loaded with the same parameters
self.test.loadTorchModel()
- new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out_after_load = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
self.assertEqual(old_out, new_out)
self.assertEqual(old_out, new_out_after_load)
with self.assertRaises(RuntimeError):
- test2 = Modely(visualizer=None, workspace = self.result_path)
+ test2 = Modely(visualizer=None, workspace=self.result_path)
# You need not neuralized model to load a torch model
test2.loadTorchModel()
@@ -116,10 +146,20 @@ def test_export_json_not_neuralized(self):
# Save a not neuralized nnodely json model and load it
self.test.saveModel() # Save a model without parameter values and samples values
with self.assertRaises(RuntimeError):
- self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.loadModel() # Load the nnodely model without parameter values
with self.assertRaises(RuntimeError):
- self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
test2 = Modely(visualizer=None, workspace=self.test.getWorkspace())
test2.loadModel() # Load the nnodely model with parameter values
with self.assertRaises(AttributeError):
@@ -139,16 +179,36 @@ def test_export_json_untrained(self):
# Save a untrained nnodely json model and load it
# the new_out and new_out_after_load are different because the model saved model is not trained
self.test.neuralizeModel(0.5)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.saveModel() # Save a model without parameter values
self.test.neuralizeModel(clear_model=True) # Create a new torch model
- new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.loadModel() # Load the nnodely model without parameter values
# Use the preloaded torch model for inference
with self.assertRaises(RuntimeError):
- self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.neuralizeModel(0.5)
- new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out_after_load = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
self.assertEqual(old_out, new_out)
with self.assertRaises(AssertionError):
@@ -164,15 +224,35 @@ def test_export_json_trained(self):
# Export json of nnodely model with parameter valuess
# The old_out is the same as the new_out_after_load because the model is loaded with the same parameters
self.test.neuralizeModel(0.5)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.saveModel() # Save the model with and without parameter values
self.test.neuralizeModel(clear_model=True) # Create a new torch model
- new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.loadModel() # Load the nnodely model with parameter values
with self.assertRaises(RuntimeError):
- self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.neuralizeModel()
- new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out_after_load = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
self.assertEqual(old_out, new_out)
with self.assertRaises(AssertionError):
@@ -188,15 +268,30 @@ def test_import_json_new_object(self):
os.makedirs(self.result_path, exist_ok=True)
# Import nnodely json model in a new object
self.test.neuralizeModel(0.5)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.neuralizeModel()
self.test.saveModel() # Save the model with and without parameter values
test2 = Modely(visualizer=None, workspace=self.test.getWorkspace())
test2.loadModel() # Load the nnodely model with parameter values
with self.assertRaises(RuntimeError):
- test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ test2(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
test2.neuralizeModel()
- new_model_out_after_load = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_model_out_after_load = test2(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.assertEqual(old_out, new_model_out_after_load)
if os.path.exists(self.test.getWorkspace()):
@@ -209,19 +304,34 @@ def test_export_torch_script(self):
# Export and import of a torch script .py
# The old_out is the same as the new_out_after_load because the model is loaded with the same parameters
with self.assertRaises(RuntimeError):
- self.test.exportPythonModel() # The model is not neuralized yet
+ self.test.exportPythonModel() # The model is not neuralized yet
self.test.neuralizeModel(0.5)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.exportPythonModel() # Export the trace model
self.test.neuralizeModel(clear_model=True) # Create a new torch model
- new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.importPythonModel() # Import the tracer model
with self.assertRaises(RuntimeError):
- self.test.exportPythonModel() # The model is traced
+ self.test.exportPythonModel() # The model is traced
# Perform inference with the imported tracer model
- new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ new_out_after_load = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
- self.assertEqual(old_out, new_out)
+ self.assertEqual(old_out, new_out)
self.assertEqual(old_out, new_out_after_load)
if os.path.exists(self.test.getWorkspace()):
@@ -232,15 +342,25 @@ def test_export_torch_script_new_object(self):
shutil.rmtree(self.test.getWorkspace())
os.makedirs(self.result_path, exist_ok=True)
# Import of a torch script .py
- self.test.neuralizeModel(0.5,clear_model=True)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test.neuralizeModel(0.5, clear_model=True)
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.exportPythonModel() # Export the trace model
self.test.neuralizeModel(clear_model=True)
test2 = Modely(visualizer=None, workspace=self.test.getWorkspace())
test2.importPythonModel() # Load the nnodely model with parameter values
with self.assertRaises(RuntimeError):
- test2.exportPythonModel() # The model is traced
- new_out_after_load = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ test2.exportPythonModel() # The model is traced
+ new_out_after_load = test2(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.assertEqual(old_out, new_out_after_load)
if os.path.exists(self.test.getWorkspace()):
@@ -254,16 +374,28 @@ def test_export_trained_torch_script(self):
data_x = np.arange(0.0, 1, 0.1)
data_y = np.arange(0.0, 1, 0.1)
a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 1, 'lr': 0.01}
- self.test.neuralizeModel(0.5,clear_model=True)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ params = {"num_of_epochs": 1, "lr": 0.01}
+ self.test.neuralizeModel(0.5, clear_model=True)
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.exportPythonModel() # Export the trace model
- self.test.loadData(name='dataset', source=dataset) # Create the dataset
- self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model
- new_out_after_train = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ self.test.loadData(name="dataset", source=dataset) # Create the dataset
+ self.test.trainModel(
+ optimizer="SGD", training_params=params
+ ) # Train the traced model
+ new_out_after_train = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
- self.assertEqual(old_out, new_out_after_train)
+ self.assertEqual(old_out, new_out_after_train)
if os.path.exists(self.test.getWorkspace()):
shutil.rmtree(self.test.getWorkspace())
@@ -274,25 +406,44 @@ def test_export_torch_script_new_object_train(self):
os.makedirs(self.result_path, exist_ok=True)
# Perform training on an imported new tracer model
self.test.neuralizeModel(0.5, clear_model=True)
- old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ old_out = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
self.test.exportPythonModel() # Export the trace model
data_x = np.arange(0.0, 1, 0.1)
data_y = np.arange(0.0, 1, 0.1)
a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 1, 'lr': 0.01}
- self.test.loadData(name='dataset', source=dataset) # Create the dataset
- self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model
- old_out_after_train = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ params = {"num_of_epochs": 1, "lr": 0.01}
+ self.test.loadData(name="dataset", source=dataset) # Create the dataset
+ self.test.trainModel(
+ optimizer="SGD", training_params=params
+ ) # Train the traced model
+ old_out_after_train = self.test(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
- self.assertEqual(old_out, old_out_after_train)
+ self.assertEqual(old_out, old_out_after_train)
test2 = Modely(visualizer=None, workspace=self.test.getWorkspace())
test2.importPythonModel() # Load the nnodely model with parameter values
- test2.loadData(name='dataset', source=dataset) # Create the dataset
- test2.trainModel(optimizer='SGD', training_params=params) # Train the traced model
- new_out_after_train = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})
+ test2.loadData(name="dataset", source=dataset) # Create the dataset
+ test2.trainModel(
+ optimizer="SGD", training_params=params
+ ) # Train the traced model
+ new_out_after_train = test2(
+ {
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ }
+ )
with self.assertRaises(AssertionError):
- self.assertEqual(old_out, new_out_after_train)
+ self.assertEqual(old_out, new_out_after_train)
self.assertEqual(old_out_after_train, new_out_after_train)
if os.path.exists(self.test.getWorkspace()):
@@ -305,17 +456,42 @@ def test_export_onnx(self):
self.test.neuralizeModel(0.5, clear_model=True)
# Export all network with minimize
- self.test.exportONNX(inputs_order=['x', 'y', 'z'], outputs_order=['out', 'out2', 'out3', 'out4', 'out5', 'out6', 'out7']) # Export the onnx model
+ self.test.exportONNX(
+ inputs_order=["x", "y", "z"],
+ outputs_order=["out", "out2", "out3", "out4", "out5", "out6", "out7"],
+ ) # Export the onnx model
# Export the all models in onnx format
- self.test.exportONNX(models=['modelA','modelB','modelC','modelD'], inputs_order=['x', 'y'], outputs_order=['out', 'out2', 'out3', 'out4', 'out5', 'out6']) # Export the onnx model
+ self.test.exportONNX(
+ models=["modelA", "modelB", "modelC", "modelD"],
+ inputs_order=["x", "y"],
+ outputs_order=["out", "out2", "out3", "out4", "out5", "out6"],
+ ) # Export the onnx model
# Export only the modelB in onnx format
- self.test.exportONNX(inputs_order=['x', 'y'], outputs_order=['out3', 'out4', 'out2'], models=['modelB']) # Export the onnx model
- self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net.onnx')))
- self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net_modelA_modelB_modelC_modelD.onnx')))
- self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net_modelB.onnx')))
+ self.test.exportONNX(
+ inputs_order=["x", "y"],
+ outputs_order=["out3", "out4", "out2"],
+ models=["modelB"],
+ ) # Export the onnx model
+ self.assertTrue(
+ os.path.exists(os.path.join(self.test.getWorkspace(), "onnx", "net.onnx"))
+ )
+ self.assertTrue(
+ os.path.exists(
+ os.path.join(
+ self.test.getWorkspace(),
+ "onnx",
+ "net_modelA_modelB_modelC_modelD.onnx",
+ )
+ )
+ )
+ self.assertTrue(
+ os.path.exists(
+ os.path.join(self.test.getWorkspace(), "onnx", "net_modelB.onnx")
+ )
+ )
if os.path.exists(self.test.getWorkspace()):
- shutil.rmtree(self.test.getWorkspace())
+ shutil.rmtree(self.test.getWorkspace())
def test_export_report(self):
if os.path.exists(self.test.getWorkspace()):
@@ -327,18 +503,25 @@ def test_export_report(self):
data_x = np.arange(0.0, 10, 0.1)
data_y = np.arange(0.0, 10, 0.1)
a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 20, 'lr': 0.0005}
- self.test.loadData(name='dataset', source=dataset) # Create the dataset
- self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model
+ dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ params = {"num_of_epochs": 20, "lr": 0.0005}
+ self.test.loadData(name="dataset", source=dataset) # Create the dataset
+ self.test.trainModel(
+ optimizer="SGD", training_params=params
+ ) # Train the traced model
self.test.exportReport()
- self.test.loadData(name='dataset2', source=dataset) # Create the dataset
- self.test.trainAndAnalyze(optimizer='SGD', train_dataset='dataset', validation_dataset='dataset2', training_params=params) # Train the traced model
+ self.test.loadData(name="dataset2", source=dataset) # Create the dataset
+ self.test.trainAndAnalyze(
+ optimizer="SGD",
+ train_dataset="dataset",
+ validation_dataset="dataset2",
+ training_params=params,
+ ) # Train the traced model
self.test.exportReport()
- self.test.removeMinimize(['error1','error2','error3','error4'])
+ self.test.removeMinimize(["error1", "error2", "error3", "error4"])
self.test.exportReport()
if os.path.exists(self.test.getWorkspace()):
- shutil.rmtree(self.test.getWorkspace())
\ No newline at end of file
+ shutil.rmtree(self.test.getWorkspace())
diff --git a/tests/test_export_recurrent.py b/tests/test_export_recurrent.py
index 0874ca54..27824ba1 100644
--- a/tests/test_export_recurrent.py
+++ b/tests/test_export_recurrent.py
@@ -1,4 +1,9 @@
-import sys, os, unittest, torch, shutil, torch.onnx, importlib
+import os
+import unittest
+import torch
+import shutil
+import torch.onnx
+import importlib
import numpy as np
from nnodely import *
@@ -11,47 +16,65 @@
# 11 Tests
# Test of export and import the network to a file in different format
-class ModelyExportTest(unittest.TestCase):
+class ModelyExportTest(unittest.TestCase):
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
- self.assertEqual(len(data1),len(data2))
+ self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.TestAlmostEqual(pred, label, precision=precision)
else:
self.assertAlmostEqual(data1, data2, places=precision)
-
def test_export_and_import_train_python_module(self):
NeuObj.clearNames()
- result_path = 'results'
- network_name = 'net'
+ result_path = "results"
+ network_name = "net"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- x = Input('x')
- y = Input('y')
- z = Input('z')
- target = Input('target')
- a = Parameter('a', dimensions=1, sw=1, values=[[1]])
- b = Parameter('b', dimensions=1, sw=1, values=[[1]])
- c = Parameter('c', dimensions=1, sw=1, values=[[1]])
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
+ target = Input("target")
+ a = Parameter("a", dimensions=1, sw=1, values=[[1]])
+ b = Parameter("b", dimensions=1, sw=1, values=[[1]])
+ c = Parameter("c", dimensions=1, sw=1, values=[[1]])
fir_x = Fir(W=a)(x.last())
fir_y = Fir(W=b)(y.last())
fir_z = Fir(W=c)(z.last())
- data_x, data_y, data_z = np.random.rand(20), np.random.rand(20), np.random.rand(20)
- dataset = {'x':data_x, 'y':data_y, 'z':data_z, 'target':3*data_x + 3*data_y + 3*data_z}
+ data_x, data_y, data_z = (
+ np.random.rand(20),
+ np.random.rand(20),
+ np.random.rand(20),
+ )
+ dataset = {
+ "x": data_x,
+ "y": data_y,
+ "z": data_z,
+ "target": 3 * data_x + 3 * data_y + 3 * data_z,
+ }
fir_x.connect(y)
sum_rel = fir_x + fir_y + fir_z
sum_rel.closedLoop(z)
- out = Output('out', sum_rel)
- test.addModel('model', out)
- test.addMinimize('error', target.last(), out)
+ out = Output("out", sum_rel)
+ test.addModel("model", out)
+ test.addMinimize("error", target.last(), out)
test.neuralizeModel(0.5)
- test.loadData(name='test_dataset', source=dataset)
+ test.loadData(name="test_dataset", source=dataset)
## Train
- test.trainModel(optimizer='SGD', training_params={'num_of_epochs': 1, 'lr': 0.0001, 'train_batch_size': 1}, splits=[100,0,0], prediction_samples=10)
+ test.trainModel(
+ optimizer="SGD",
+ training_params={"num_of_epochs": 1, "lr": 0.0001, "train_batch_size": 1},
+ splits=[100, 0, 0],
+ prediction_samples=10,
+ )
## Inference
- sample = {'x':[1], 'y':[2], 'z':[3], 'target':[18]}
+ sample = {"x": [1], "y": [2], "z": [3], "target": [18]}
train_result = test(sample)
train_parameters = test.parameters
# Export the model
@@ -60,352 +83,520 @@ def test_export_and_import_train_python_module(self):
test.importPythonModel(name=network_name)
# Inference with imported model
self.assertEqual(train_result, test(sample))
- self.assertEqual(train_parameters['a'], test.parameters['a'])
- self.assertEqual(train_parameters['b'], test.parameters['b'])
- self.assertEqual(train_parameters['c'], test.parameters['c'])
+ self.assertEqual(train_parameters["a"], test.parameters["a"])
+ self.assertEqual(train_parameters["b"], test.parameters["b"])
+ self.assertEqual(train_parameters["c"], test.parameters["c"])
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_export_and_import_python_module(self):
NeuObj.clearNames()
- result_path = 'results'
- network_name = 'exported_model'
+ result_path = "results"
+ network_name = "exported_model"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- x = Input('x')
- y = Input('y')
- z = Input('z')
- a = Parameter('a', dimensions=1, sw=1, values=[[1]])
- b = Parameter('b', dimensions=1, sw=1, values=[[1]])
- c = Parameter('c', dimensions=1, sw=1, values=[[1]])
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
+ a = Parameter("a", dimensions=1, sw=1, values=[[1]])
+ b = Parameter("b", dimensions=1, sw=1, values=[[1]])
+ c = Parameter("c", dimensions=1, sw=1, values=[[1]])
fir_x = Fir(W=a)(x.last())
fir_y = Fir(W=b)(y.last())
fir_z = Fir(W=c)(z.last())
fir_x.connect(y)
sum_rel = fir_x + fir_y + fir_z
sum_rel.closedLoop(z)
- out = Output('out', sum_rel)
- test.addModel('model', out)
+ out = Output("out", sum_rel)
+ test.addModel("model", out)
test.neuralizeModel(0.5)
## Inference
- sample = {'x':[1], 'y':[2], 'z':[3]}
+ sample = {"x": [1], "y": [2], "z": [3]}
inference_result = test(sample)
- self.assertEqual(inference_result['out'], [5.0])
+ self.assertEqual(inference_result["out"], [5.0])
# Export the model
test.exportPythonModel(name=network_name)
## Load the exported model.py
## Import the python exported module
- #from results.exported_model import RecurrentModel
- module = importlib.import_module(result_path+'.'+network_name)
- RecurrentModel = getattr(module, 'RecurrentModel')
+ # from results.exported_model import RecurrentModel
+ module = importlib.import_module(result_path + "." + network_name)
+ RecurrentModel = getattr(module, "RecurrentModel")
model = RecurrentModel()
# Create dummy input data
- dummy_input = {'x': torch.ones(5, 1, 1, 1), 'target': torch.ones(10, 1, 1, 1), 'y': torch.zeros(1,1,1), 'z':torch.zeros(1,1,1)} # Adjust the shape as needed
+ dummy_input = {
+ "x": torch.ones(5, 1, 1, 1),
+ "target": torch.ones(10, 1, 1, 1),
+ "y": torch.zeros(1, 1, 1),
+ "z": torch.zeros(1, 1, 1),
+ } # Adjust the shape as needed
# Inference with imported model
with torch.no_grad():
output = model(dummy_input)
- self.assertEqual(output['out'], [torch.tensor([[[2.]]]), torch.tensor([[[4.]]]), torch.tensor([[[6.]]]), torch.tensor([[[8.]]]), torch.tensor([[[10.]]])])
+ self.assertEqual(
+ output["out"],
+ [
+ torch.tensor([[[2.0]]]),
+ torch.tensor([[[4.0]]]),
+ torch.tensor([[[6.0]]]),
+ torch.tensor([[[8.0]]]),
+ torch.tensor([[[10.0]]]),
+ ],
+ )
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_export_and_import_onnx_module(self):
NeuObj.clearNames()
- result_path = 'results'
+ result_path = "results"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- x = Input('x')
- y = Input('y')
- z = Input('z')
- a = Parameter('a', dimensions=1, sw=1, values=[[1]])
- b = Parameter('b', dimensions=1, sw=1, values=[[1]])
- c = Parameter('c', dimensions=1, sw=1, values=[[1]])
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
+ a = Parameter("a", dimensions=1, sw=1, values=[[1]])
+ b = Parameter("b", dimensions=1, sw=1, values=[[1]])
+ c = Parameter("c", dimensions=1, sw=1, values=[[1]])
fir_x = Fir(W=a)(x.last())
fir_y = Fir(W=b)(y.last())
fir_z = Fir(W=c)(z.last())
fir_x.connect(y)
sum_rel = fir_x + fir_y + fir_z
sum_rel.closedLoop(z)
- out = Output('out', sum_rel)
- test.addModel('model', out)
+ out = Output("out", sum_rel)
+ test.addModel("model", out)
test.neuralizeModel(0.5)
## Inference
- sample = {'x':[1], 'y':[2], 'z':[3]}
+ sample = {"x": [1], "y": [2], "z": [3]}
inference_result = test(sample)
- self.assertEqual(inference_result['out'], [5.0])
+ self.assertEqual(inference_result["out"], [5.0])
## Export in ONNX format
- test.exportONNX(['x','y','z'],['out']) # Export the onnx model
+ test.exportONNX(["x", "y", "z"], ["out"]) # Export the onnx model
## ONNX IMPORT
- dummy_input = {'x':np.ones(shape=(3, 1, 1, 1)).astype(np.float32),
- 'y':np.ones(shape=(1, 1, 1)).astype(np.float32),
- 'z':np.ones(shape=(1, 1, 1)).astype(np.float32)}
- outputs = Modely(visualizer=None,workspace=result_path).onnxInference(dummy_input)
+ dummy_input = {
+ "x": np.ones(shape=(3, 1, 1, 1)).astype(np.float32),
+ "y": np.ones(shape=(1, 1, 1)).astype(np.float32),
+ "z": np.ones(shape=(1, 1, 1)).astype(np.float32),
+ }
+ outputs = Modely(visualizer=None, workspace=result_path).onnxInference(
+ dummy_input
+ )
# Get the output
- expected_output = np.array([[[[3.]]], [[[5.]]], [[[7.]]]], dtype=np.float32)
+ expected_output = np.array([[[[3.0]]], [[[5.0]]], [[[7.0]]]], dtype=np.float32)
self.assertEqual(outputs[0].tolist(), expected_output.tolist())
# The connected variable is not needed
- sample = {'x':[3],'z':[5]}
+ sample = {"x": [3], "z": [5]}
inference_result = test(sample)
- dummy_input = {'x':3*np.ones(shape=(1, 1, 1, 1)).astype(np.float32),
- 'y':7*np.ones(shape=(1, 1, 1)).astype(np.float32),
- 'z':5*np.ones(shape=(1, 1, 1)).astype(np.float32)}
- outputs = Modely(visualizer=None, workspace=result_path).onnxInference(dummy_input)
- self.assertEqual(outputs[0][0][0], inference_result['out'])
+ dummy_input = {
+ "x": 3 * np.ones(shape=(1, 1, 1, 1)).astype(np.float32),
+ "y": 7 * np.ones(shape=(1, 1, 1)).astype(np.float32),
+ "z": 5 * np.ones(shape=(1, 1, 1)).astype(np.float32),
+ }
+ outputs = Modely(visualizer=None, workspace=result_path).onnxInference(
+ dummy_input
+ )
+ self.assertEqual(outputs[0][0][0], inference_result["out"])
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_export_and_import_onnx_module_easy(self):
NeuObj.clearNames()
- result_path = 'results'
+ result_path = "results"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- num_cycle = Input('num_cycle')
- x = Input('x')
- fir_x = Fir()(x.last()+1.0)
+ num_cycle = Input("num_cycle")
+ x = Input("x")
+ fir_x = Fir()(x.last() + 1.0)
fir_x.closedLoop(x)
- out1 = Output('out1', fir_x)
- out2 = Output('out2', num_cycle.last()+1.0)
- test.addModel('model', [out1,out2])
+ out1 = Output("out1", fir_x)
+ out2 = Output("out2", num_cycle.last() + 1.0)
+ test.addModel("model", [out1, out2])
test.neuralizeModel(0.5)
## Export in ONNX format
- test.exportONNX(['x','num_cycle'],['out1','out2']) # Export the onnx model
- output_nodely = test({'num_cycle':np.ones(shape=(10)).astype(np.float32).tolist(), 'x':np.ones(shape=(1)).astype(np.float32).tolist()})
+ test.exportONNX(["x", "num_cycle"], ["out1", "out2"]) # Export the onnx model
+ output_nodely = test(
+ {
+ "num_cycle": np.ones(shape=(10)).astype(np.float32).tolist(),
+ "x": np.ones(shape=(1)).astype(np.float32).tolist(),
+ }
+ )
## ONNX IMPORT
- outputs = Modely(visualizer=None,workspace=result_path).onnxInference(inputs={'num_cycle':np.ones(shape=(10, 1, 1, 1)).astype(np.float32), 'x':np.ones(shape=(1, 1, 1)).astype(np.float32)})
- self.assertEqual(output_nodely['out1'], outputs[0].squeeze().tolist())
- self.assertEqual(output_nodely['out2'], outputs[1].squeeze().tolist())
+ outputs = Modely(visualizer=None, workspace=result_path).onnxInference(
+ inputs={
+ "num_cycle": np.ones(shape=(10, 1, 1, 1)).astype(np.float32),
+ "x": np.ones(shape=(1, 1, 1)).astype(np.float32),
+ }
+ )
+ self.assertEqual(output_nodely["out1"], outputs[0].squeeze().tolist())
+ self.assertEqual(output_nodely["out2"], outputs[1].squeeze().tolist())
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_export_and_import_onnx_module_complex(self):
# Create nnodely structure
- result_path = 'results'
- network_name = 'vehicle'
+ result_path = "results"
+ network_name = "vehicle"
vehicle = Modely(visualizer=None, seed=2, workspace=result_path)
# Dimensions of the layers
- n = 25
+ n = 25
na = 21
- #Create neural model inputs
- velocity = Input('vel')
- brake = Input('brk')
- gear = Input('gear')
- torque = Input('trq')
- altitude = Input('alt',dimensions=na)
- acc = Input('acc')
+ # Create neural model inputs
+ velocity = Input("vel")
+ brake = Input("brk")
+ gear = Input("gear")
+ torque = Input("trq")
+ altitude = Input("alt", dimensions=na)
+ acc = Input("acc")
# Create neural network relations
- air_drag_force = Linear(b=True)(velocity.last()**2)
- breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))
- gravity_force = Linear(W_init='init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())
- fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())
- local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))
+ air_drag_force = Linear(b=True)(velocity.last() ** 2)
+ breaking_force = -Relu(
+ Fir(
+ W_init="init_negexp",
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )(brake.sw(n))
+ )
+ gravity_force = Linear(
+ W_init="init_constant", W_init_params={"value": 0}, dropout=0.1, W="gravity"
+ )(altitude.last())
+ fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last())
+ local_model = LocalModel(
+ input_function=lambda: Fir(
+ W_init="init_negexp",
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )
+ )
engine_force = local_model(torque.sw(n), fuzzi_gear)
# Create neural network output
- out = Output('accelleration', air_drag_force+breaking_force+gravity_force+engine_force)
+ out = Output(
+ "accelleration",
+ air_drag_force + breaking_force + gravity_force + engine_force,
+ )
# Add the neural model to the nnodely structure and neuralization of the model
- vehicle.addModel('acc',out)
- vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse')
+ vehicle.addModel("acc", out)
+ vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse")
vehicle.neuralizeModel(0.05)
# Load the training and the validation dataset
- data_struct = ['vel','trq','brk','gear','alt','acc']
- data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data')
- vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
+ data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"]
+ data_folder = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "vehicle_data"
+ )
+ vehicle.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
# Inference
- model_sample = vehicle.getSamples('dataset', window=1)
+ model_sample = vehicle.getSamples("dataset", window=1)
model_inference = vehicle(model_sample, sampled=True, prediction_samples=1)
## Export the Onnx Model
- vehicle.exportONNX(['vel','brk','gear','trq','alt'],['accelleration'], network_name, models='acc')
+ vehicle.exportONNX(
+ ["vel", "brk", "gear", "trq", "alt"],
+ ["accelleration"],
+ network_name,
+ models="acc",
+ )
# Onnx Import
- outputs = Modely(visualizer=None).onnxInference(model_sample, name=network_name, model_folder=os.path.join(result_path,'onnx'))
- self.assertEqual(outputs[0][0], model_inference['accelleration'])
+ outputs = Modely(visualizer=None).onnxInference(
+ model_sample,
+ name=network_name,
+ model_folder=os.path.join(result_path, "onnx"),
+ )
+ self.assertEqual(outputs[0][0], model_inference["accelleration"])
if os.path.exists(vehicle.getWorkspace()):
shutil.rmtree(vehicle.getWorkspace())
def test_export_python_module_recurrent(self):
NeuObj.clearNames()
- result_path = 'results'
- network_name = 'net'
+ result_path = "results"
+ network_name = "net"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- input1 = Input('input1')
- input2 = Input('input2', dimensions=3)
- input3 = Input('input3')
- input4 = Input('input4', dimensions=3)
- state1 = Input('state1')
- state2 = Input('state2', dimensions=3)
+ input1 = Input("input1")
+ input2 = Input("input2", dimensions=3)
+ input3 = Input("input3")
+ input4 = Input("input4", dimensions=3)
+ state1 = Input("state1")
+ state2 = Input("state2", dimensions=3)
rel_1 = Linear(b=True)(input1.last()) + Linear(b=True)(input3.last())
rel_1.closedLoop(state1)
- rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(output_dimension=3, b=True)(input4.last())
+ rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(
+ output_dimension=3, b=True
+ )(input4.last())
rel_2.closedLoop(state2)
- out1 = Output('out1', rel_1)
- out2 = Output('out2', rel_2)
- out3 = Output('input1-out', input1.last())
- out4 = Output('input2-out', input2.last())
- out5 = Output('input3-out', input3.sw(4))
- out6 = Output('input4-out', input4.sw(4))
- out7 = Output('state1-out', state1.last())
- out8 = Output('state2-out', state2.last())
+ out1 = Output("out1", rel_1)
+ out2 = Output("out2", rel_2)
+ out3 = Output("input1-out", input1.last())
+ out4 = Output("input2-out", input2.last())
+ out5 = Output("input3-out", input3.sw(4))
+ out6 = Output("input4-out", input4.sw(4))
+ out7 = Output("state1-out", state1.last())
+ out8 = Output("state2-out", state2.last())
- test.addModel('model', [out1, out2, out3, out4, out5, out6, out7, out8])
+ test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8])
test.neuralizeModel()
test.exportPythonModel(name=network_name)
## Load the exported model.py
## Import the python exported module
- #from results.net import RecurrentModel
- module = importlib.import_module(result_path+'.'+network_name)
- RecurrentModel = getattr(module, 'RecurrentModel')
+ # from results.net import RecurrentModel
+ module = importlib.import_module(result_path + "." + network_name)
+ RecurrentModel = getattr(module, "RecurrentModel")
recurrent_model = RecurrentModel()
## Without Horizon and without batch
- recurrent_sample = {'input1': torch.rand(size=(1,1,1,1), dtype=torch.float32),
- 'input2': torch.rand(size=(1,1,1,3), dtype=torch.float32),
- 'input3': torch.rand(size=(1,1,4,1), dtype=torch.float32),
- 'input4': torch.rand(size=(1,1,4,3), dtype=torch.float32)}
- recurrent_sample['state1'] = torch.rand(size=(1,1,1), dtype=torch.float32)
- recurrent_sample['state2'] = torch.rand(size=(1,1,3), dtype=torch.float32)
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [1,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [1,1,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [1,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [1,1,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [1,1,4,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [1,1,4,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [1,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [1,1,1,3])
+ recurrent_sample = {
+ "input1": torch.rand(size=(1, 1, 1, 1), dtype=torch.float32),
+ "input2": torch.rand(size=(1, 1, 1, 3), dtype=torch.float32),
+ "input3": torch.rand(size=(1, 1, 4, 1), dtype=torch.float32),
+ "input4": torch.rand(size=(1, 1, 4, 3), dtype=torch.float32),
+ }
+ recurrent_sample["state1"] = torch.rand(size=(1, 1, 1), dtype=torch.float32)
+ recurrent_sample["state2"] = torch.rand(size=(1, 1, 3), dtype=torch.float32)
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape),
+ [1, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape),
+ [1, 1, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape),
+ [1, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape),
+ [1, 1, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape),
+ [1, 1, 4, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape),
+ [1, 1, 4, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape),
+ [1, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape),
+ [1, 1, 1, 3],
+ )
## With Horizon and without batch
- recurrent_sample = {'input1': torch.rand(size=(5,1,1,1), dtype=torch.float32),
- 'input2': torch.rand(size=(5,1,1,3), dtype=torch.float32),
- 'input3': torch.rand(size=(5,1,4,1), dtype=torch.float32),
- 'input4': torch.rand(size=(5,1,4,3), dtype=torch.float32)}
- recurrent_sample['state1'] = torch.rand(size=(1,1,1), dtype=torch.float32)
- recurrent_sample['state2'] = torch.rand(size=(1,1,3), dtype=torch.float32)
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [5,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [5,1,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [5,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [5,1,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [5,1,4,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [5,1,4,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [5,1,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [5,1,1,3])
+ recurrent_sample = {
+ "input1": torch.rand(size=(5, 1, 1, 1), dtype=torch.float32),
+ "input2": torch.rand(size=(5, 1, 1, 3), dtype=torch.float32),
+ "input3": torch.rand(size=(5, 1, 4, 1), dtype=torch.float32),
+ "input4": torch.rand(size=(5, 1, 4, 3), dtype=torch.float32),
+ }
+ recurrent_sample["state1"] = torch.rand(size=(1, 1, 1), dtype=torch.float32)
+ recurrent_sample["state2"] = torch.rand(size=(1, 1, 3), dtype=torch.float32)
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape),
+ [5, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape),
+ [5, 1, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape),
+ [5, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape),
+ [5, 1, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape),
+ [5, 1, 4, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape),
+ [5, 1, 4, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape),
+ [5, 1, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape),
+ [5, 1, 1, 3],
+ )
## With Horizon and with batch
- recurrent_sample = {'input1': torch.rand(size=(5,2,1,1), dtype=torch.float32),
- 'input2': torch.rand(size=(5,2,1,3), dtype=torch.float32),
- 'input3': torch.rand(size=(5,2,4,1), dtype=torch.float32),
- 'input4': torch.rand(size=(5,2,4,3), dtype=torch.float32)}
- recurrent_sample['state1'] = torch.rand(size=(2,1,1), dtype=torch.float32)
- recurrent_sample['state2'] = torch.rand(size=(2,1,3), dtype=torch.float32)
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [5,2,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [5,2,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [5,2,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [5,2,1,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [5,2,4,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [5,2,4,3])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [5,2,1,1])
- self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [5,2,1,3])
+ recurrent_sample = {
+ "input1": torch.rand(size=(5, 2, 1, 1), dtype=torch.float32),
+ "input2": torch.rand(size=(5, 2, 1, 3), dtype=torch.float32),
+ "input3": torch.rand(size=(5, 2, 4, 1), dtype=torch.float32),
+ "input4": torch.rand(size=(5, 2, 4, 3), dtype=torch.float32),
+ }
+ recurrent_sample["state1"] = torch.rand(size=(2, 1, 1), dtype=torch.float32)
+ recurrent_sample["state2"] = torch.rand(size=(2, 1, 3), dtype=torch.float32)
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape),
+ [5, 2, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape),
+ [5, 2, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape),
+ [5, 2, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape),
+ [5, 2, 1, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape),
+ [5, 2, 4, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape),
+ [5, 2, 4, 3],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape),
+ [5, 2, 1, 1],
+ )
+ self.assertListEqual(
+ list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape),
+ [5, 2, 1, 3],
+ )
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_export_onnx_module_recurrent(self):
- result_path = 'results'
- test = Modely(visualizer=None, seed=42, workspace= result_path)
- input1 = Input('input1')
- input2 = Input('input2', dimensions=3)
- input3 = Input('input3')
- input4 = Input('input4', dimensions=3)
- state1 = Input('state1')
- state2 = Input('state2', dimensions=3)
+ result_path = "results"
+ test = Modely(visualizer=None, seed=42, workspace=result_path)
+ input1 = Input("input1")
+ input2 = Input("input2", dimensions=3)
+ input3 = Input("input3")
+ input4 = Input("input4", dimensions=3)
+ state1 = Input("state1")
+ state2 = Input("state2", dimensions=3)
rel_1 = Linear(b=True)(input1.last()) + Linear(b=True)(input3.last())
rel_1.closedLoop(state1)
- rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(output_dimension=3, b=True)(input4.last())
+ rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(
+ output_dimension=3, b=True
+ )(input4.last())
rel_2.closedLoop(state2)
- out1 = Output('out1', rel_1)
- out2 = Output('out2', rel_2)
- out3 = Output('out_input1', input1.last())
- out4 = Output('out_input2', input2.last())
- out5 = Output('out_input3', input3.sw(4))
- out6 = Output('out_input4', input4.sw(4))
- out7 = Output('out_state1', state1.last())
- out8 = Output('out_state2', state2.last())
+ out1 = Output("out1", rel_1)
+ out2 = Output("out2", rel_2)
+ out3 = Output("out_input1", input1.last())
+ out4 = Output("out_input2", input2.last())
+ out5 = Output("out_input3", input3.sw(4))
+ out6 = Output("out_input4", input4.sw(4))
+ out7 = Output("out_state1", state1.last())
+ out8 = Output("out_state2", state2.last())
- test.addModel('model', [out1, out2, out3, out4, out5, out6, out7, out8])
+ test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8])
test.neuralizeModel()
- test.exportONNX(inputs_order=['input1','input2','input3','input4','state1','state2'],outputs_order=['out1', 'out2', 'out_input1', 'out_input2', 'out_input3', 'out_input4', 'out_state1', 'out_state2'])
+ test.exportONNX(
+ inputs_order=["input1", "input2", "input3", "input4", "state1", "state2"],
+ outputs_order=[
+ "out1",
+ "out2",
+ "out_input1",
+ "out_input2",
+ "out_input3",
+ "out_input4",
+ "out_state1",
+ "out_state2",
+ ],
+ )
## Without Horizon and without batch
- recurrent_sample = {'input1': np.random.rand(1,1,1,1).astype(np.float32),
- 'input2': np.random.rand(1,1,1,3).astype(np.float32),
- 'input3': np.random.rand(1,1,4,1).astype(np.float32),
- 'input4': np.random.rand(1,1,4,3).astype(np.float32)}
- recurrent_sample['state1'] = np.random.rand(1,1,1).astype(np.float32)
- recurrent_sample['state2'] = np.random.rand(1,1,3).astype(np.float32)
- inference = Modely(visualizer=None).onnxInference(recurrent_sample, model_folder=os.path.join(test.getWorkspace(),'onnx'))
- self.assertListEqual(list(inference[0].shape), [1,1,1,1])
- self.assertListEqual(list(inference[1].shape), [1,1,1,3])
- self.assertListEqual(list(inference[2].shape), [1,1,1,1])
- self.assertListEqual(list(inference[3].shape), [1,1,1,3])
- self.assertListEqual(list(inference[4].shape), [1,1,4,1])
- self.assertListEqual(list(inference[5].shape), [1,1,4,3])
- self.assertListEqual(list(inference[6].shape), [1,1,1,1])
- self.assertListEqual(list(inference[7].shape), [1,1,1,3])
+ recurrent_sample = {
+ "input1": np.random.rand(1, 1, 1, 1).astype(np.float32),
+ "input2": np.random.rand(1, 1, 1, 3).astype(np.float32),
+ "input3": np.random.rand(1, 1, 4, 1).astype(np.float32),
+ "input4": np.random.rand(1, 1, 4, 3).astype(np.float32),
+ }
+ recurrent_sample["state1"] = np.random.rand(1, 1, 1).astype(np.float32)
+ recurrent_sample["state2"] = np.random.rand(1, 1, 3).astype(np.float32)
+ inference = Modely(visualizer=None).onnxInference(
+ recurrent_sample, model_folder=os.path.join(test.getWorkspace(), "onnx")
+ )
+ self.assertListEqual(list(inference[0].shape), [1, 1, 1, 1])
+ self.assertListEqual(list(inference[1].shape), [1, 1, 1, 3])
+ self.assertListEqual(list(inference[2].shape), [1, 1, 1, 1])
+ self.assertListEqual(list(inference[3].shape), [1, 1, 1, 3])
+ self.assertListEqual(list(inference[4].shape), [1, 1, 4, 1])
+ self.assertListEqual(list(inference[5].shape), [1, 1, 4, 3])
+ self.assertListEqual(list(inference[6].shape), [1, 1, 1, 1])
+ self.assertListEqual(list(inference[7].shape), [1, 1, 1, 3])
## With Horizon and without batch
- recurrent_sample = {'input1': np.random.rand(5,1,1,1).astype(np.float32),
- 'input2': np.random.rand(5,1,1,3).astype(np.float32),
- 'input3': np.random.rand(5,1,4,1).astype(np.float32),
- 'input4': np.random.rand(5,1,4,3).astype(np.float32)}
- recurrent_sample['state1'] = np.random.rand(1,1,1).astype(np.float32)
- recurrent_sample['state2'] = np.random.rand(1,1,3).astype(np.float32)
- inference = Modely(visualizer=None,workspace=result_path).onnxInference(recurrent_sample)
- self.assertListEqual(list(inference[0].shape), [5,1,1,1])
- self.assertListEqual(list(inference[1].shape), [5,1,1,3])
- self.assertListEqual(list(inference[2].shape), [5,1,1,1])
- self.assertListEqual(list(inference[3].shape), [5,1,1,3])
- self.assertListEqual(list(inference[4].shape), [5,1,4,1])
- self.assertListEqual(list(inference[5].shape), [5,1,4,3])
- self.assertListEqual(list(inference[6].shape), [5,1,1,1])
- self.assertListEqual(list(inference[7].shape), [5,1,1,3])
+ recurrent_sample = {
+ "input1": np.random.rand(5, 1, 1, 1).astype(np.float32),
+ "input2": np.random.rand(5, 1, 1, 3).astype(np.float32),
+ "input3": np.random.rand(5, 1, 4, 1).astype(np.float32),
+ "input4": np.random.rand(5, 1, 4, 3).astype(np.float32),
+ }
+ recurrent_sample["state1"] = np.random.rand(1, 1, 1).astype(np.float32)
+ recurrent_sample["state2"] = np.random.rand(1, 1, 3).astype(np.float32)
+ inference = Modely(visualizer=None, workspace=result_path).onnxInference(
+ recurrent_sample
+ )
+ self.assertListEqual(list(inference[0].shape), [5, 1, 1, 1])
+ self.assertListEqual(list(inference[1].shape), [5, 1, 1, 3])
+ self.assertListEqual(list(inference[2].shape), [5, 1, 1, 1])
+ self.assertListEqual(list(inference[3].shape), [5, 1, 1, 3])
+ self.assertListEqual(list(inference[4].shape), [5, 1, 4, 1])
+ self.assertListEqual(list(inference[5].shape), [5, 1, 4, 3])
+ self.assertListEqual(list(inference[6].shape), [5, 1, 1, 1])
+ self.assertListEqual(list(inference[7].shape), [5, 1, 1, 3])
# ## With Horizon and with batch
- recurrent_sample = {'input1': np.random.rand(5,2,1,1).astype(np.float32),
- 'input2': np.random.rand(5,2,1,3).astype(np.float32),
- 'input3': np.random.rand(5,2,4,1).astype(np.float32),
- 'input4': np.random.rand(5,2,4,3).astype(np.float32)}
- recurrent_sample['state1'] = np.random.rand(2,1,1).astype(np.float32)
- recurrent_sample['state2'] = np.random.rand(2,1,3).astype(np.float32)
- inference = Modely(visualizer=None).onnxInference(recurrent_sample, model_folder=os.path.join(test.getWorkspace(),'onnx'), name='net')
- self.assertListEqual(list(inference[0].shape), [5,2,1,1])
- self.assertListEqual(list(inference[1].shape), [5,2,1,3])
- self.assertListEqual(list(inference[2].shape), [5,2,1,1])
- self.assertListEqual(list(inference[3].shape), [5,2,1,3])
- self.assertListEqual(list(inference[4].shape), [5,2,4,1])
- self.assertListEqual(list(inference[5].shape), [5,2,4,3])
- self.assertListEqual(list(inference[6].shape), [5,2,1,1])
- self.assertListEqual(list(inference[7].shape), [5,2,1,3])
+ recurrent_sample = {
+ "input1": np.random.rand(5, 2, 1, 1).astype(np.float32),
+ "input2": np.random.rand(5, 2, 1, 3).astype(np.float32),
+ "input3": np.random.rand(5, 2, 4, 1).astype(np.float32),
+ "input4": np.random.rand(5, 2, 4, 3).astype(np.float32),
+ }
+ recurrent_sample["state1"] = np.random.rand(2, 1, 1).astype(np.float32)
+ recurrent_sample["state2"] = np.random.rand(2, 1, 3).astype(np.float32)
+ inference = Modely(visualizer=None).onnxInference(
+ recurrent_sample,
+ model_folder=os.path.join(test.getWorkspace(), "onnx"),
+ name="net",
+ )
+ self.assertListEqual(list(inference[0].shape), [5, 2, 1, 1])
+ self.assertListEqual(list(inference[1].shape), [5, 2, 1, 3])
+ self.assertListEqual(list(inference[2].shape), [5, 2, 1, 1])
+ self.assertListEqual(list(inference[3].shape), [5, 2, 1, 3])
+ self.assertListEqual(list(inference[4].shape), [5, 2, 4, 1])
+ self.assertListEqual(list(inference[5].shape), [5, 2, 4, 3])
+ self.assertListEqual(list(inference[6].shape), [5, 2, 1, 1])
+ self.assertListEqual(list(inference[7].shape), [5, 2, 1, 3])
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
@@ -413,69 +604,98 @@ def test_export_onnx_module_recurrent(self):
def test_export_and_import_python_module_complex_recurrent(self):
NeuObj.clearNames()
# Create nnodely structure
- result_path = 'results'
- network_name = 'vehicle'
+ result_path = "results"
+ network_name = "vehicle"
vehicle = Modely(visualizer=None, seed=2, workspace=result_path)
# Dimensions of the layers
- n = 25
+ n = 25
na = 21
- #Create neural model inputs
- velocity = Input('vel')
- brake = Input('brk')
- gear = Input('gear')
- torque = Input('trq')
- altitude = Input('alt',dimensions=na)
- acc = Input('acc')
+ # Create neural model inputs
+ velocity = Input("vel")
+ brake = Input("brk")
+ gear = Input("gear")
+ torque = Input("trq")
+ altitude = Input("alt", dimensions=na)
+ acc = Input("acc")
# Create neural network relations
- air_drag_force = Linear(b=True)(velocity.last()**2)
- breaking_force = -Relu(Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))
- gravity_force = Linear(W_init=init_constant, W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())
- fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())
- local_model = LocalModel(input_function=lambda: Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))
+ air_drag_force = Linear(b=True)(velocity.last() ** 2)
+ breaking_force = -Relu(
+ Fir(
+ W_init=init_negexp,
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )(brake.sw(n))
+ )
+ gravity_force = Linear(
+ W_init=init_constant, W_init_params={"value": 0}, dropout=0.1, W="gravity"
+ )(altitude.last())
+ fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last())
+ local_model = LocalModel(
+ input_function=lambda: Fir(
+ W_init=init_negexp,
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )
+ )
engine_force = local_model(torque.sw(n), fuzzi_gear)
- sum_rel = air_drag_force+breaking_force+gravity_force+engine_force
+ sum_rel = air_drag_force + breaking_force + gravity_force + engine_force
sum_rel.closedLoop(velocity)
# Create neural network output
- out = Output('accelleration', sum_rel)
+ out = Output("accelleration", sum_rel)
# Add the neural model to the nnodely structure and neuralization of the model
- vehicle.addModel('acc',[out])
- vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse')
+ vehicle.addModel("acc", [out])
+ vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse")
vehicle.neuralizeModel(0.05)
# Load the training and the validation dataset
- data_struct = ['vel','trq','brk','gear','alt','acc']
- data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data')
- vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
+ data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"]
+ data_folder = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "vehicle_data"
+ )
+ vehicle.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
# Inference
- sample = vehicle.getSamples('dataset', window=3)
+ sample = vehicle.getSamples("dataset", window=3)
model_inference = vehicle(sample, sampled=True, prediction_samples=3)
vehicle.exportPythonModel(name=network_name)
loaded_vehicle = Modely(visualizer=None, workspace=vehicle.getWorkspace())
loaded_vehicle.importPythonModel(name=network_name)
- model_import_inference = loaded_vehicle(sample, sampled=True, prediction_samples=3)
- self.assertEqual(model_inference['accelleration'], model_import_inference['accelleration'])
+ model_import_inference = loaded_vehicle(
+ sample, sampled=True, prediction_samples=3
+ )
+ self.assertEqual(
+ model_inference["accelleration"], model_import_inference["accelleration"]
+ )
## Load the exported model.py
## Import the python exported module
- #from results.vehicle import RecurrentModel
- module = importlib.import_module(result_path+'.'+network_name)
- RecurrentModel = getattr(module, 'RecurrentModel')
+ # from results.vehicle import RecurrentModel
+ module = importlib.import_module(result_path + "." + network_name)
+ RecurrentModel = getattr(module, "RecurrentModel")
recurrent_model = RecurrentModel()
- sample = vehicle.getSamples('dataset', window=3)
- recurrent_sample = {key: torch.tensor(np.array(value), dtype=torch.float32).unsqueeze(1) for key, value in sample.items()}
- recurrent_sample['vel'] = torch.zeros(1,1,1)
- model_sample = {key: value for key, value in sample.items() if key != 'vel'}
- self.TestAlmostEqual([item.detach().item() for item in recurrent_model(recurrent_sample)['accelleration']], vehicle(model_sample, sampled=True, prediction_samples=3)['accelleration'])
+ sample = vehicle.getSamples("dataset", window=3)
+ recurrent_sample = {
+ key: torch.tensor(np.array(value), dtype=torch.float32).unsqueeze(1)
+ for key, value in sample.items()
+ }
+ recurrent_sample["vel"] = torch.zeros(1, 1, 1)
+ model_sample = {key: value for key, value in sample.items() if key != "vel"}
+ self.TestAlmostEqual(
+ [
+ item.detach().item()
+ for item in recurrent_model(recurrent_sample)["accelleration"]
+ ],
+ vehicle(model_sample, sampled=True, prediction_samples=3)["accelleration"],
+ )
if os.path.exists(vehicle.getWorkspace()):
shutil.rmtree(vehicle.getWorkspace())
@@ -483,62 +703,100 @@ def test_export_and_import_python_module_complex_recurrent(self):
def test_export_and_import_onnx_module_complex_recurrent(self):
NeuObj.clearNames()
# Create nnodely structure
- result_path = 'results'
- network_name = 'vehicle'
- vehicle = Modely(visualizer=None, seed=42, workspace= result_path)
+ result_path = "results"
+ network_name = "vehicle"
+ vehicle = Modely(visualizer=None, seed=42, workspace=result_path)
# Dimensions of the layers
- n = 25
+ n = 25
na = 21
- #Create neural model inputs
- velocity = Input('vel')
- brake = Input('brk')
- gear = Input('gear')
- torque = Input('trq')
- altitude = Input('alt',dimensions=na)
- acc = Input('acc')
+ # Create neural model inputs
+ velocity = Input("vel")
+ brake = Input("brk")
+ gear = Input("gear")
+ torque = Input("trq")
+ altitude = Input("alt", dimensions=na)
+ acc = Input("acc")
# Create neural network relations
- air_drag_force = Linear(b=True)(velocity.last()**2)
- breaking_force = -Relu(Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))
- gravity_force = Linear(W_init=init_constant, W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())
- fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())
- local_model = LocalModel(input_function=lambda: Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))
+ air_drag_force = Linear(b=True)(velocity.last() ** 2)
+ breaking_force = -Relu(
+ Fir(
+ W_init=init_negexp,
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )(brake.sw(n))
+ )
+ gravity_force = Linear(
+ W_init=init_constant, W_init_params={"value": 0}, dropout=0.1, W="gravity"
+ )(altitude.last())
+ fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last())
+ local_model = LocalModel(
+ input_function=lambda: Fir(
+ W_init=init_negexp,
+ W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3},
+ )
+ )
engine_force = local_model(torque.sw(n), fuzzi_gear)
- sum_rel = air_drag_force+breaking_force+gravity_force+engine_force
+ sum_rel = air_drag_force + breaking_force + gravity_force + engine_force
sum_rel.closedLoop(velocity)
# Create neural network output
- out = Output('accelleration', sum_rel)
+ out = Output("accelleration", sum_rel)
# Add the neural model to the nnodely structure and neuralization of the model
- vehicle.addModel('acc',[out])
- vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse')
+ vehicle.addModel("acc", [out])
+ vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse")
vehicle.neuralizeModel(0.05)
# Load the training and the validation dataset
- data_struct = ['vel','trq','brk','gear','alt','acc']
- data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data')
- vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
+ data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"]
+ data_folder = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "vehicle_data"
+ )
+ vehicle.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
## Export the Onnx Model
vehicle.exportONNX(name=network_name)
- model_sample = vehicle.getSamples('dataset', window=1)
+ model_sample = vehicle.getSamples("dataset", window=1)
model_inference = vehicle(model_sample, sampled=True, prediction_samples=1)
## ONNX IMPORT
- onnx_sample = {key: (np.expand_dims(value, axis=1).astype(np.float32) if key != 'vel' else value) for key, value in model_sample.items()}
- outputs = Modely(visualizer=None).onnxInference(onnx_sample, name=network_name, model_folder=os.path.join(result_path,'onnx'))
- self.assertEqual(outputs[0][0], model_inference['accelleration'])
-
- model_sample = vehicle.getSamples('dataset', window=3)
- onnx_sample = {key: (np.expand_dims(value, axis=1).astype(np.float32) if key != 'vel' else np.expand_dims(np.array(value[0], dtype=np.float32), axis=0)) for key, value in model_sample.items()}
+ onnx_sample = {
+ key: (
+ np.expand_dims(value, axis=1).astype(np.float32)
+ if key != "vel"
+ else value
+ )
+ for key, value in model_sample.items()
+ }
+ outputs = Modely(visualizer=None).onnxInference(
+ onnx_sample,
+ name=network_name,
+ model_folder=os.path.join(result_path, "onnx"),
+ )
+ self.assertEqual(outputs[0][0], model_inference["accelleration"])
+
+ model_sample = vehicle.getSamples("dataset", window=3)
+ onnx_sample = {
+ key: (
+ np.expand_dims(value, axis=1).astype(np.float32)
+ if key != "vel"
+ else np.expand_dims(np.array(value[0], dtype=np.float32), axis=0)
+ )
+ for key, value in model_sample.items()
+ }
model_inference = vehicle(model_sample, sampled=True, prediction_samples=3)
- outputs = Modely(visualizer=None, workspace=result_path).onnxInference(onnx_sample, name=network_name)
- self.assertEqual(outputs[0].squeeze().tolist(), model_inference['accelleration'])
+ outputs = Modely(visualizer=None, workspace=result_path).onnxInference(
+ onnx_sample, name=network_name
+ )
+ self.assertEqual(
+ outputs[0].squeeze().tolist(), model_inference["accelleration"]
+ )
if os.path.exists(vehicle.getWorkspace()):
shutil.rmtree(vehicle.getWorkspace())
@@ -547,62 +805,76 @@ def test_export_sw_on_stream_sw_complex(self):
NeuObj.clearNames()
Stream.resetCount()
# Create nnodely structure
- result_path = 'results'
- network_name = 'swnet'
- input = Input('inin')
+ result_path = "results"
+ network_name = "swnet"
+ input = Input("inin")
sw_7 = input.sw(7)
- out61 = Output('out61', sw_7.sw(6))
- out62 = Output('out62', SamplePart(sw_7,1,7))
+ out61 = Output("out61", sw_7.sw(6))
+ out62 = Output("out62", SamplePart(sw_7, 1, 7))
test = Modely(visualizer=None, workspace=result_path)
- test.addModel('out', [out61,out62])
+ test.addModel("out", [out61, out62])
test.neuralizeModel()
sample = [14, 1, 2, 3, 4, 5, 6]
- results = test({'inin':sample})
-
- test.exportONNX(inputs_order=['inin','SamplePart1_sw1'],outputs_order=['out61','out62'],name=network_name)
- outputs = Modely(visualizer=None, workspace=result_path).onnxInference({'inin':np.array([[[[14],[1],[2],[3],[4],[5],[6]]]]).astype(np.float32),'SamplePart1_sw1':np.array([[[0],[0],[0],[0],[0],[0]]]).astype(np.float32)}, name=network_name)
- self.assertEqual(outputs[0].squeeze().tolist(), results['out61'][0])
- self.assertEqual(outputs[1].squeeze().tolist(), results['out62'][0])
- self.assertEqual(results['out61'][0], results['out62'][0])
+ results = test({"inin": sample})
+
+ test.exportONNX(
+ inputs_order=["inin", "SamplePart1_sw1"],
+ outputs_order=["out61", "out62"],
+ name=network_name,
+ )
+ outputs = Modely(visualizer=None, workspace=result_path).onnxInference(
+ {
+ "inin": np.array([[[[14], [1], [2], [3], [4], [5], [6]]]]).astype(
+ np.float32
+ ),
+ "SamplePart1_sw1": np.array([[[0], [0], [0], [0], [0], [0]]]).astype(
+ np.float32
+ ),
+ },
+ name=network_name,
+ )
+ self.assertEqual(outputs[0].squeeze().tolist(), results["out61"][0])
+ self.assertEqual(outputs[1].squeeze().tolist(), results["out62"][0])
+ self.assertEqual(results["out61"][0], results["out62"][0])
if os.path.exists(test.getWorkspace()):
shutil.rmtree(test.getWorkspace())
def test_partial_model_export(self):
- #We have 4 networks, A, B, C and D
+ # We have 4 networks, A, B, C and D
# Connection inside model 1
- #Aout is connected to Bin1
- #Bout is closed_loop to Ain2
- #Bout is clodes_loop to Bin2
+ # Aout is connected to Bin1
+ # Bout is closed_loop to Ain2
+ # Bout is clodes_loop to Bin2
# Connection inside model 2
- #Cout is connected to Din1
+ # Cout is connected to Din1
# Connection outside models in model 2
- #Dout closed_loop to Cin1
- #Cout connect to Din2
+ # Dout closed_loop to Cin1
+ # Cout connect to Din2
# Connection outside models between models
- #Dout closed_loop to Ain1
- #Aout connected to Din3
- #Bout closed_loop to Cin2
+ # Dout closed_loop to Ain1
+ # Aout connected to Din3
+ # Bout closed_loop to Cin2
- result_path = 'results'
+ result_path = "results"
NeuObj.clearNames()
- #log.setAllLevel(logging.INFO)
+ # log.setAllLevel(logging.INFO)
- #Network A and B -> Model1
- Ain1 = Input('Ain1')
- Ain2 = Input('Ain2')
- Bin1 = Input('Bin1')
- Bin2 = Input('Bin2')
- Bin3 = Input('Bin3')
+ # Network A and B -> Model1
+ Ain1 = Input("Ain1")
+ Ain2 = Input("Ain2")
+ Bin1 = Input("Bin1")
+ Bin2 = Input("Bin2")
+ Bin3 = Input("Bin3")
- pA = Parameter('PA', sw=1, values=[[3.0]])
- pB = Parameter('PB', sw=1, values=[[-5.0]])
+ pA = Parameter("PA", sw=1, values=[[3.0]])
+ pB = Parameter("PB", sw=1, values=[[-5.0]])
Aout = (Ain1.last() + Ain2.last()) * pA
Aout.connect(Bin1)
@@ -611,337 +883,437 @@ def test_partial_model_export(self):
Bout.closedLoop(Ain2)
Bout.closedLoop(Bin2)
- modelA = Output('Aout',Aout)
- modelB = Output('Bout',Bout)
+ modelA = Output("Aout", Aout)
+ modelB = Output("Bout", Bout)
- Cin1 = Input('Cin1')
- Cin2 = Input('Cin2')
- Din1 = Input('Din1')
- Din2 = Input('Din2')
- Din3 = Input('Din3')
+ Cin1 = Input("Cin1")
+ Cin2 = Input("Cin2")
+ Din1 = Input("Din1")
+ Din2 = Input("Din2")
+ Din3 = Input("Din3")
- pC = Parameter('PC', sw=1, values=[[-1.0]])
- pD = Parameter('PD', sw=1, values=[[2.0]])
+ pC = Parameter("PC", sw=1, values=[[-1.0]])
+ pD = Parameter("PD", sw=1, values=[[2.0]])
Cout = (Cin1.last() + Cin2.last()) * pC
Cout.connect(Din1)
Dout = (Din1.last() + Din2.last() + Din3.last()) * pD
- modelC = Output('Cout',Cout)
- modelD = Output('Dout',Dout)
+ modelC = Output("Cout", Cout)
+ modelD = Output("Dout", Dout)
m = Modely(workspace=result_path, visualizer=None)
with self.assertRaises(RuntimeError):
- m.addModel('modelA', [modelA])
+ m.addModel("modelA", [modelA])
with self.assertRaises(RuntimeError):
- m.addModel('modelB', [modelB])
-
- m.addModel('model1', [modelA, modelB])
- m.addModel('model2', [modelC, modelD])
-
- init_inputs = {'Din3': [1], 'Din2': [1], 'Cin1': [1], 'Cin2': [1], 'Ain1': [1], 'Bin3': [1]}
- init_states = {'Din1': [1], 'Bin2':[1], 'Ain2':[1], 'Bin1': [1]}
- init_states_diff = {'Din1': [12], 'Bin2': [1], 'Ain2': [1], 'Bin1': [12]}
+ m.addModel("modelB", [modelB])
+
+ m.addModel("model1", [modelA, modelB])
+ m.addModel("model2", [modelC, modelD])
+
+ init_inputs = {
+ "Din3": [1],
+ "Din2": [1],
+ "Cin1": [1],
+ "Cin2": [1],
+ "Ain1": [1],
+ "Bin3": [1],
+ }
+ init_states = {"Din1": [1], "Bin2": [1], "Ain2": [1], "Bin1": [1]}
+ init_states_diff = {"Din1": [12], "Bin2": [1], "Ain2": [1], "Bin1": [12]}
# Target with states = 0.0
- results_target = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0],
- 'Bout': [(3.0 + 1.0 + 0.0) * -5.0],
- 'Aout': [(1.0 + 0.0) * 3.0]}
+ results_target = {
+ "Dout": [(-2.0 + 1.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ "Bout": [(3.0 + 1.0 + 0.0) * -5.0],
+ "Aout": [(1.0 + 0.0) * 3.0],
+ }
# Target with states = 1.0
- results_target_state = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0],
- 'Bout': [(6.0 + 1.0 + 1.0) * -5.0],
- 'Aout': [(1.0 + 1.0) * 3.0]}
+ results_target_state = {
+ "Dout": [(-2.0 + 1.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ "Bout": [(6.0 + 1.0 + 1.0) * -5.0],
+ "Aout": [(1.0 + 1.0) * 3.0],
+ }
m.neuralizeModel()
self.assertEqual(results_target, m(init_inputs))
- self.assertEqual(results_target_state, m(init_inputs|init_states))
- self.assertEqual(results_target_state, m(init_inputs|init_states_diff))
+ self.assertEqual(results_target_state, m(init_inputs | init_states))
+ self.assertEqual(results_target_state, m(init_inputs | init_states_diff))
## Test loading of all models
m.saveTorchModel()
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model1', [modelA, modelB])
- l.addModel('model2', [modelC, modelD])
+ l.addModel("model1", [modelA, modelB])
+ l.addModel("model2", [modelC, modelD])
l.neuralizeModel()
- l.parameters['PA'] = [[22.0]]
+ l.parameters["PA"] = [[22.0]]
l.loadTorchModel()
self.assertEqual(results_target, l(init_inputs))
- self.assertEqual(results_target_state, l(init_inputs|init_states))
- self.assertEqual(results_target_state, l(init_inputs|init_states_diff))
+ self.assertEqual(results_target_state, l(init_inputs | init_states))
+ self.assertEqual(results_target_state, l(init_inputs | init_states_diff))
m.saveModel()
l = Modely(workspace=result_path, visualizer=None)
l.loadModel()
l.neuralizeModel()
self.assertEqual(results_target, l(init_inputs))
- self.assertEqual(results_target_state, l(init_inputs|init_states))
- self.assertEqual(results_target_state, l(init_inputs|init_states_diff))
+ self.assertEqual(results_target_state, l(init_inputs | init_states))
+ self.assertEqual(results_target_state, l(init_inputs | init_states_diff))
m.exportPythonModel()
l = Modely(workspace=result_path, visualizer=None)
l.importPythonModel()
self.assertEqual(results_target, l(init_inputs))
- self.assertEqual(results_target_state, l(init_inputs|init_states))
- self.assertEqual(results_target_state, l(init_inputs|init_states_diff))
-
- m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout'])
- init_inputs_ox = {'Din1':np.array([[[7.0]]]).astype(np.float32),
- 'Bin2':np.array([[[0]]]).astype(np.float32),
- 'Ain2':np.array([[[0]]]).astype(np.float32),
- 'Bin1':np.array([[[12.0]]]).astype(np.float32)}
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_ox |
- {'Din2': np.array([[[[1]]]]).astype(np.float32),
- 'Din3': np.array([[[[1]]]]).astype(np.float32),
- 'Cin1': np.array([[[[1]]]]).astype(np.float32),
- 'Cin2': np.array([[[[1]]]]).astype(np.float32),
- 'Ain1':np.array([[[[1]]]]).astype(np.float32),
- 'Bin3': np.array([[[[1]]]]).astype(np.float32)})
- self.assertEqual([[[[[0.0]]]], [[[[-2.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results)
+ self.assertEqual(results_target_state, l(init_inputs | init_states))
+ self.assertEqual(results_target_state, l(init_inputs | init_states_diff))
+
+ m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"])
+ init_inputs_ox = {
+ "Din1": np.array([[[7.0]]]).astype(np.float32),
+ "Bin2": np.array([[[0]]]).astype(np.float32),
+ "Ain2": np.array([[[0]]]).astype(np.float32),
+ "Bin1": np.array([[[12.0]]]).astype(np.float32),
+ }
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_ox
+ | {
+ "Din2": np.array([[[[1]]]]).astype(np.float32),
+ "Din3": np.array([[[[1]]]]).astype(np.float32),
+ "Cin1": np.array([[[[1]]]]).astype(np.float32),
+ "Cin2": np.array([[[[1]]]]).astype(np.float32),
+ "Ain1": np.array([[[[1]]]]).astype(np.float32),
+ "Bin3": np.array([[[[1]]]]).astype(np.float32),
+ }
+ )
+ self.assertEqual(
+ [[[[[0.0]]]], [[[[-2.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results
+ )
## Testing loading of model1
- results_target_m1 = {'Bout': [-20.0], 'Aout': [3.0]}
- results_target_state_m1 = {'Bout': [(6.0 + 1.0 + 1.0) * -5.0],
- 'Aout': [(1.0 + 1.0) * 3.0]}
- m.saveTorchModel(models='model1')
+ results_target_m1 = {"Bout": [-20.0], "Aout": [3.0]}
+ results_target_state_m1 = {
+ "Bout": [(6.0 + 1.0 + 1.0) * -5.0],
+ "Aout": [(1.0 + 1.0) * 3.0],
+ }
+ m.saveTorchModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model1', [modelA, modelB])
+ l.addModel("model1", [modelA, modelB])
l.neuralizeModel()
- l.parameters['PA'] = [[22.0]]
- l.loadTorchModel(name='net_model1')
+ l.parameters["PA"] = [[22.0]]
+ l.loadTorchModel(name="net_model1")
self.assertEqual(results_target_m1, l(init_inputs))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff))
- m.saveModel(models='model1')
+ m.saveModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.loadModel(name='net_model1')
+ l.loadModel(name="net_model1")
l.neuralizeModel()
self.assertEqual(results_target_m1, l(init_inputs))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff))
- m.exportPythonModel(models='model1')
+ m.exportPythonModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.importPythonModel(name='net_model1')
+ l.importPythonModel(name="net_model1")
self.assertEqual(results_target_m1, l(init_inputs))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states))
self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff))
- m.exportONNX(models='model1', outputs_order=['Bout', 'Aout'])
- init_inputs_m1_ox = {'Bin2':np.array([[[0]]]).astype(np.float32),
- 'Ain2':np.array([[[0]]]).astype(np.float32),
- 'Bin1':np.array([[[12.0]]]).astype(np.float32)}
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_m1_ox |
- {'Ain1':np.array([[[[1]]]]).astype(np.float32),
- 'Bin3': np.array([[[[1]]]]).astype(np.float32)}, name='net_model1')
+ m.exportONNX(models="model1", outputs_order=["Bout", "Aout"])
+ init_inputs_m1_ox = {
+ "Bin2": np.array([[[0]]]).astype(np.float32),
+ "Ain2": np.array([[[0]]]).astype(np.float32),
+ "Bin1": np.array([[[12.0]]]).astype(np.float32),
+ }
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_m1_ox
+ | {
+ "Ain1": np.array([[[[1]]]]).astype(np.float32),
+ "Bin3": np.array([[[[1]]]]).astype(np.float32),
+ },
+ name="net_model1",
+ )
self.assertEqual([[[[[-20.0]]]], [[[[3.0]]]]], results)
### Add the connect on model2 ###
- m.addConnect(Cout,Din2)
- m.addClosedLoop(Dout,Cin1)
+ m.addConnect(Cout, Din2)
+ m.addClosedLoop(Dout, Cin1)
m.neuralizeModel()
- init_inputs_2 = {'Din3': [1], 'Cin2': [1], 'Ain1': [1], 'Bin3': [1]}
- init_states_2 = {'Din1': [1], 'Cin1': [1], 'Din2': [1], 'Bin2':[1], 'Ain2':[1], 'Bin1': [1]}
- init_states_diff_2 = {'Din1': [12], 'Cin1': [1], 'Din2': [30], 'Bin2': [1], 'Ain2': [1], 'Bin1': [12]}
+ init_inputs_2 = {"Din3": [1], "Cin2": [1], "Ain1": [1], "Bin3": [1]}
+ init_states_2 = {
+ "Din1": [1],
+ "Cin1": [1],
+ "Din2": [1],
+ "Bin2": [1],
+ "Ain2": [1],
+ "Bin1": [1],
+ }
+ init_states_diff_2 = {
+ "Din1": [12],
+ "Cin1": [1],
+ "Din2": [30],
+ "Bin2": [1],
+ "Ain2": [1],
+ "Bin1": [12],
+ }
# Target with states = 0.0
- results_target_2 = {'Dout': [(-1.0 - 1.0 + 1.0) * 2.0],
- 'Cout': [(0.0 + 1.0) * -1.0],
- 'Bout': [(3.0 + 1.0 + 0.0) * -5.0],
- 'Aout': [(1.0 + 0.0) * 3.0]}
+ results_target_2 = {
+ "Dout": [(-1.0 - 1.0 + 1.0) * 2.0],
+ "Cout": [(0.0 + 1.0) * -1.0],
+ "Bout": [(3.0 + 1.0 + 0.0) * -5.0],
+ "Aout": [(1.0 + 0.0) * 3.0],
+ }
# Target with states = 1.0
- results_target_state_2 = {'Dout': [(-2.0 - 2.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0],
- 'Bout': [(6.0 + 1.0 + 1.0) * -5.0],
- 'Aout': [(1.0 + 1.0) * 3.0]}
+ results_target_state_2 = {
+ "Dout": [(-2.0 - 2.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ "Bout": [(6.0 + 1.0 + 1.0) * -5.0],
+ "Aout": [(1.0 + 1.0) * 3.0],
+ }
self.assertEqual(results_target_2, m(init_inputs_2))
- self.assertEqual(results_target_state_2, m(init_inputs_2|init_states_2))
- self.assertEqual(results_target_state_2, m(init_inputs_2|init_states_diff_2))
+ self.assertEqual(results_target_state_2, m(init_inputs_2 | init_states_2))
+ self.assertEqual(results_target_state_2, m(init_inputs_2 | init_states_diff_2))
## Test loading of all models
m.saveTorchModel()
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model1', [modelA, modelB])
- l.addModel('model2', [modelC, modelD])
+ l.addModel("model1", [modelA, modelB])
+ l.addModel("model2", [modelC, modelD])
l.neuralizeModel()
- l.parameters['PD'] = [[22.0]]
+ l.parameters["PD"] = [[22.0]]
l.loadTorchModel()
self.assertEqual(results_target, l(init_inputs))
- self.assertEqual(results_target_state, l(init_inputs|init_states))
- self.assertEqual(results_target_state, l(init_inputs|init_states_diff))
+ self.assertEqual(results_target_state, l(init_inputs | init_states))
+ self.assertEqual(results_target_state, l(init_inputs | init_states_diff))
l2 = Modely(workspace=result_path, visualizer=None)
- l2.addModel('model1', [modelA, modelB])
- l2.addModel('model2', [modelC, modelD])
- l2.addConnect(Cout,Din2)
- l2.addClosedLoop(Dout,Cin1)
+ l2.addModel("model1", [modelA, modelB])
+ l2.addModel("model2", [modelC, modelD])
+ l2.addConnect(Cout, Din2)
+ l2.addClosedLoop(Dout, Cin1)
l2.neuralizeModel()
- l.parameters['PA'] = [[22.0]]
+ l.parameters["PA"] = [[22.0]]
l2.loadTorchModel()
self.assertEqual(results_target_2, l2(init_inputs_2))
- self.assertEqual(results_target_state_2, l2(init_inputs_2|init_states_2))
- self.assertEqual(results_target_state_2, l2(init_inputs_2|init_states_diff_2))
+ self.assertEqual(results_target_state_2, l2(init_inputs_2 | init_states_2))
+ self.assertEqual(results_target_state_2, l2(init_inputs_2 | init_states_diff_2))
m.saveModel()
l = Modely(workspace=result_path, visualizer=None)
l.loadModel()
l.neuralizeModel()
self.assertEqual(results_target_2, l(init_inputs_2))
- self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_2))
- self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_diff_2))
+ self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_2))
+ self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_diff_2))
m.exportPythonModel()
l = Modely(workspace=result_path, visualizer=None)
l.importPythonModel()
self.assertEqual(results_target_2, l(init_inputs_2))
- self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_2))
- self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_diff_2))
-
- m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout'])
- init_inputs_2_ox = {'Din3': np.array([[[[1.0]]]]).astype(np.float32),
- 'Cin2': np.array([[[[1.0]]]]).astype(np.float32),
- 'Ain1': np.array([[[[1.0]]]]).astype(np.float32),
- 'Bin3': np.array([[[[1.0]]]]).astype(np.float32)}
- init_states_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32),
- 'Cin1': np.array([[[0.0]]]).astype(np.float32),
- 'Din2': np.array([[[20.0]]]).astype(np.float32),
- 'Bin2': np.array([[[0.0]]]).astype(np.float32),
- 'Ain2': np.array([[[0.0]]]).astype(np.float32),
- 'Bin1': np.array([[[34.0]]]).astype(np.float32)}
- init_states_diff_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32),
- 'Cin1': np.array([[[1.0]]]).astype(np.float32),
- 'Din2': np.array([[[23.0]]]).astype(np.float32),
- 'Bin2': np.array([[[1.0]]]).astype(np.float32),
- 'Ain2': np.array([[[1.0]]]).astype(np.float32),
- 'Bin1': np.array([[[12.0]]]).astype(np.float32)}
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox)
- self.assertEqual([[[[[-2.0]]]], [[[[-1.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results)
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_diff_2_ox)
- self.assertEqual([[[[[-6.0]]]], [[[[-2.0]]]], [[[[-40.0]]]], [[[[6.0]]]]], results)
-
+ self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_2))
+ self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_diff_2))
+
+ m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"])
+ init_inputs_2_ox = {
+ "Din3": np.array([[[[1.0]]]]).astype(np.float32),
+ "Cin2": np.array([[[[1.0]]]]).astype(np.float32),
+ "Ain1": np.array([[[[1.0]]]]).astype(np.float32),
+ "Bin3": np.array([[[[1.0]]]]).astype(np.float32),
+ }
+ init_states_2_ox = {
+ "Din1": np.array([[[12.0]]]).astype(np.float32),
+ "Cin1": np.array([[[0.0]]]).astype(np.float32),
+ "Din2": np.array([[[20.0]]]).astype(np.float32),
+ "Bin2": np.array([[[0.0]]]).astype(np.float32),
+ "Ain2": np.array([[[0.0]]]).astype(np.float32),
+ "Bin1": np.array([[[34.0]]]).astype(np.float32),
+ }
+ init_states_diff_2_ox = {
+ "Din1": np.array([[[12.0]]]).astype(np.float32),
+ "Cin1": np.array([[[1.0]]]).astype(np.float32),
+ "Din2": np.array([[[23.0]]]).astype(np.float32),
+ "Bin2": np.array([[[1.0]]]).astype(np.float32),
+ "Ain2": np.array([[[1.0]]]).astype(np.float32),
+ "Bin1": np.array([[[12.0]]]).astype(np.float32),
+ }
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_2_ox | init_states_2_ox
+ )
+ self.assertEqual(
+ [[[[[-2.0]]]], [[[[-1.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results
+ )
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_2_ox | init_states_diff_2_ox
+ )
+ self.assertEqual(
+ [[[[[-6.0]]]], [[[[-2.0]]]], [[[[-40.0]]]], [[[[6.0]]]]], results
+ )
## Testing loading of model1
# Target with states = 0.0
- results_target_m2 = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0]}
- results_target_state_m2 = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0]}
- results_target_2_m2 = {'Dout': [(-1.0 - 1.0 + 1.0) * 2.0],
- 'Cout': [(0.0 + 1.0) * -1.0]}
- results_target_state_2_m2 = {'Dout': [(-2.0 - 2.0 + 1.0) * 2.0],
- 'Cout': [(1.0 + 1.0) * -1.0]}
- m.saveTorchModel(models='model2')
+ results_target_m2 = {
+ "Dout": [(-2.0 + 1.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ }
+ results_target_state_m2 = {
+ "Dout": [(-2.0 + 1.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ }
+ results_target_2_m2 = {
+ "Dout": [(-1.0 - 1.0 + 1.0) * 2.0],
+ "Cout": [(0.0 + 1.0) * -1.0],
+ }
+ results_target_state_2_m2 = {
+ "Dout": [(-2.0 - 2.0 + 1.0) * 2.0],
+ "Cout": [(1.0 + 1.0) * -1.0],
+ }
+ m.saveTorchModel(models="model2")
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model2', [modelC, modelD])
+ l.addModel("model2", [modelC, modelD])
l.neuralizeModel()
- l.parameters['PD'] = [[22.0]]
- l.loadTorchModel(name='net_model2')
+ l.parameters["PD"] = [[22.0]]
+ l.loadTorchModel(name="net_model2")
self.assertEqual(results_target_m2, l(init_inputs))
self.assertEqual(results_target_state_m2, l(init_inputs | init_states))
self.assertEqual(results_target_state_m2, l(init_inputs | init_states_diff))
l2 = Modely(workspace=result_path, visualizer=None)
- l2.addModel('model2', [modelC, modelD])
- l2.addConnect(Cout,Din2)
- l2.addClosedLoop(Dout,Cin1)
+ l2.addModel("model2", [modelC, modelD])
+ l2.addConnect(Cout, Din2)
+ l2.addClosedLoop(Dout, Cin1)
l2.neuralizeModel()
- l2.parameters['PD'] = [[22.0]]
+ l2.parameters["PD"] = [[22.0]]
with self.assertRaises(FileNotFoundError):
- l2.loadTorchModel(name='net_m22',model_folder='test')
- l2.loadTorchModel(name='net_model2')
+ l2.loadTorchModel(name="net_m22", model_folder="test")
+ l2.loadTorchModel(name="net_model2")
self.assertEqual(results_target_2_m2, l2(init_inputs_2))
self.assertEqual(results_target_state_2_m2, l2(init_inputs_2 | init_states_2))
- self.assertEqual(results_target_state_2_m2, l2(init_inputs_2 | init_states_diff_2))
+ self.assertEqual(
+ results_target_state_2_m2, l2(init_inputs_2 | init_states_diff_2)
+ )
- m.saveModel(models='model2')
+ m.saveModel(models="model2")
l = Modely(workspace=result_path, visualizer=None)
with self.assertRaises(FileNotFoundError):
- l.loadModel(name='_model2')
- l.loadModel(name='net_model2')
+ l.loadModel(name="_model2")
+ l.loadModel(name="net_model2")
l.neuralizeModel()
self.assertEqual(results_target_2_m2, l(init_inputs_2))
self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_2))
- self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2))
+ self.assertEqual(
+ results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2)
+ )
- m.exportPythonModel(models='model2')
+ m.exportPythonModel(models="model2")
l = Modely(workspace=result_path, visualizer=None)
with self.assertRaises(FileNotFoundError):
- l.importPythonModel(name='net_model22')
- l.importPythonModel(name='net_model2')
+ l.importPythonModel(name="net_model22")
+ l.importPythonModel(name="net_model2")
self.assertEqual(results_target_2_m2, l(init_inputs_2))
self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_2))
- self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2))
-
- m.exportONNX(models='model2', outputs_order=['Cout', 'Dout'])
- init_inputs_2_ox = {'Din3': np.array([[[[1.0]]]]).astype(np.float32),
- 'Cin2': np.array([[[[1.0]]]]).astype(np.float32)}
- init_states_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32),
- 'Cin1': np.array([[[0.0]]]).astype(np.float32),
- 'Din2': np.array([[[20.0]]]).astype(np.float32)}
- init_states_diff_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32),
- 'Cin1': np.array([[[1.0]]]).astype(np.float32),
- 'Din2': np.array([[[23.0]]]).astype(np.float32)}
+ self.assertEqual(
+ results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2)
+ )
+
+ m.exportONNX(models="model2", outputs_order=["Cout", "Dout"])
+ init_inputs_2_ox = {
+ "Din3": np.array([[[[1.0]]]]).astype(np.float32),
+ "Cin2": np.array([[[[1.0]]]]).astype(np.float32),
+ }
+ init_states_2_ox = {
+ "Din1": np.array([[[12.0]]]).astype(np.float32),
+ "Cin1": np.array([[[0.0]]]).astype(np.float32),
+ "Din2": np.array([[[20.0]]]).astype(np.float32),
+ }
+ init_states_diff_2_ox = {
+ "Din1": np.array([[[12.0]]]).astype(np.float32),
+ "Cin1": np.array([[[1.0]]]).astype(np.float32),
+ "Din2": np.array([[[23.0]]]).astype(np.float32),
+ }
with self.assertRaises(FileNotFoundError):
- Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox, name='net_models2')
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox, name='net_model2')
+ Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_2_ox | init_states_2_ox, name="net_models2"
+ )
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_2_ox | init_states_2_ox, name="net_model2"
+ )
self.assertEqual([[[[[-1.0]]]], [[[[-2.0]]]]], results)
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_diff_2_ox, name='net_model2')
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs_2_ox | init_states_diff_2_ox, name="net_model2"
+ )
self.assertEqual([[[[[-2.0]]]], [[[[-6.0]]]]], results)
- #log.setAllLevel(logging.CRITICAL)
+ # log.setAllLevel(logging.CRITICAL)
if os.path.exists(m.getWorkspace()):
shutil.rmtree(m.getWorkspace())
def test_partial_model_export_intern_extern_connection(self):
- result_path = 'results'
+ result_path = "results"
NeuObj.clearNames()
- #Network A and B -> Model1
- Ain1 = Input('Ain1')
- Bin1 = Input('Bin1')
+ # Network A and B -> Model1
+ Ain1 = Input("Ain1")
+ Bin1 = Input("Bin1")
- pA = Parameter('PA', sw=1, values=[[3.0]])
- pB = Parameter('PB', sw=1, values=[[-5.0]])
+ pA = Parameter("PA", sw=1, values=[[3.0]])
+ pB = Parameter("PB", sw=1, values=[[-5.0]])
- Aout = (Ain1.last() * pA).sw(1, name='AoutD').s(-1,method='trapezoidal',int_name='int_AoutD',der_name='der_AoutD')
- Bout = (Bin1.last() * pB).tw(1, name='BoutD').s(1,method='trapezoidal',int_name='int_BoutD',der_name='der_BoutD')
+ Aout = (
+ (Ain1.last() * pA)
+ .sw(1, name="AoutD")
+ .s(-1, method="trapezoidal", int_name="int_AoutD", der_name="der_AoutD")
+ )
+ Bout = (
+ (Bin1.last() * pB)
+ .tw(1, name="BoutD")
+ .s(1, method="trapezoidal", int_name="int_BoutD", der_name="der_BoutD")
+ )
- modelA = Output('Aout',Aout)
- modelB = Output('Bout',Bout)
+ modelA = Output("Aout", Aout)
+ modelB = Output("Bout", Bout)
- Cin1 = Input('Cin1')
- Din1 = Input('Din1')
+ Cin1 = Input("Cin1")
+ Din1 = Input("Din1")
- pC = Parameter('PC', tw=1, values=[[-1.0]])
- pD = Parameter('PD', sw=1, values=[[2.0]])
+ pC = Parameter("PC", tw=1, values=[[-1.0]])
+ pD = Parameter("PD", sw=1, values=[[2.0]])
- Cout = (Cin1.tw(1) * pC).delay(1, name='CoutD') # TODO Change convetion
- Dout = (Din1.last() * pD).z(1, name='DoutD')
+ Cout = (Cin1.tw(1) * pC).delay(1, name="CoutD") # TODO Change convetion
+ Dout = (Din1.last() * pD).z(1, name="DoutD")
- modelC = Output('Cout',Cout)
- modelD = Output('Dout',Dout)
+ modelC = Output("Cout", Cout)
+ modelD = Output("Dout", Dout)
m = Modely(workspace=result_path, visualizer=None)
- m.addModel('model1', [modelA, modelB])
- m.addModel('model2', [modelC, modelD])
- m.addConnect(Cout,Bin1)
- m.addConnect(Aout,Din1)
- m.addClosedLoop(Dout,Ain1)
- m.addClosedLoop(Bout,Cin1)
-
- init_states = {'Ain1': [1,1], 'Bin1':[2,2], 'Cin1':[3,3], 'Din1': [4,4]}
- init_states_diff = {'Ain1': [1,1], 'Bin1':[12,20], 'Cin1':[3,3], 'Din1': [12,20]}
+ m.addModel("model1", [modelA, modelB])
+ m.addModel("model2", [modelC, modelD])
+ m.addConnect(Cout, Bin1)
+ m.addConnect(Aout, Din1)
+ m.addClosedLoop(Dout, Ain1)
+ m.addClosedLoop(Bout, Cin1)
+
+ init_states = {"Ain1": [1, 1], "Bin1": [2, 2], "Cin1": [3, 3], "Din1": [4, 4]}
+ init_states_diff = {
+ "Ain1": [1, 1],
+ "Bin1": [12, 20],
+ "Cin1": [3, 3],
+ "Din1": [12, 20],
+ }
# Target with states = 0.0
- results_target = {'Dout': [0.0, 1.5 * 2.0],
- 'Cout': [0.0, 3.0 * -1.0],
- 'Bout': [0.0, (-3.0 * -5.0)*2.0],
- 'Aout': [(1.0 * 3.0)*0.5, (1.0 * 3.0)+(1.0 * 3.0)*0.5]}
+ results_target = {
+ "Dout": [0.0, 1.5 * 2.0],
+ "Cout": [0.0, 3.0 * -1.0],
+ "Bout": [0.0, (-3.0 * -5.0) * 2.0],
+ "Aout": [(1.0 * 3.0) * 0.5, (1.0 * 3.0) + (1.0 * 3.0) * 0.5],
+ }
m.neuralizeModel()
self.assertEqual(results_target, m(init_states))
@@ -951,14 +1323,14 @@ def test_partial_model_export_intern_extern_connection(self):
# Test loading of all models
m.saveTorchModel()
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model1', [modelA, modelB])
- l.addModel('model2', [modelC, modelD])
- l.addConnect(Cout,Bin1)
- l.addConnect(Aout,Din1)
- l.addClosedLoop(Dout,Ain1)
- l.addClosedLoop(Bout,Cin1)
+ l.addModel("model1", [modelA, modelB])
+ l.addModel("model2", [modelC, modelD])
+ l.addConnect(Cout, Bin1)
+ l.addConnect(Aout, Din1)
+ l.addClosedLoop(Dout, Ain1)
+ l.addClosedLoop(Bout, Cin1)
l.neuralizeModel()
- l.parameters['PA'] = [[22.0]]
+ l.parameters["PA"] = [[22.0]]
l.loadTorchModel()
self.assertEqual(results_target, l(init_states))
l.resetStates()
@@ -980,47 +1352,53 @@ def test_partial_model_export_intern_extern_connection(self):
self.assertEqual(results_target, l(init_states_diff))
with self.assertRaises(TypeError):
- m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout'])
+ m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"])
- results_target_m1 = {'Aout': [1.5, 4.5], 'Bout': [-20.0, 20.0]}
+ results_target_m1 = {"Aout": [1.5, 4.5], "Bout": [-20.0, 20.0]}
# Test loading of Model1
- m.saveTorchModel(models='model1')
+ m.saveTorchModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.addModel('model1', [modelA, modelB])
+ l.addModel("model1", [modelA, modelB])
l.neuralizeModel()
- l.parameters['PA'] = [[22.0]]
- l.loadTorchModel(name='net_model1')
+ l.parameters["PA"] = [[22.0]]
+ l.loadTorchModel(name="net_model1")
self.assertEqual(results_target_m1, l(init_states))
l.resetStates()
self.assertEqual(results_target_m1, l(init_states))
- m.saveModel(models='model1')
+ m.saveModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.loadModel(name='net_model1')
+ l.loadModel(name="net_model1")
l.neuralizeModel()
self.assertEqual(results_target_m1, l(init_states))
l.resetStates()
self.assertEqual(results_target_m1, l(init_states))
- m.exportPythonModel(models='model1')
+ m.exportPythonModel(models="model1")
l = Modely(workspace=result_path, visualizer=None)
- l.importPythonModel(name='net_model1')
+ l.importPythonModel(name="net_model1")
self.assertEqual(results_target_m1, l(init_states))
l.resetStates()
self.assertEqual(results_target_m1, l(init_states))
- m.exportONNX(outputs_order=['Bout', 'Aout'],models='model1')
-
- init_inputs = {'Ain1': np.array([[[[1.0]]],[[[1.0]]]]).astype(np.float32),
- 'Bin1': np.array([[[[2.0]]],[[[2.0]]]]).astype(np.float32)}
- init_states = {'AoutD': np.array([[[0.0]]]).astype(np.float32),
- 'int_AoutD': np.array([[[0.]]]).astype(np.float32),
- 'der_AoutD': np.array([[[0.],[0.]]]).astype(np.float32),
- 'BoutD': np.array([[[0.0]]]).astype(np.float32),
- 'int_BoutD': np.array([[[0.],[0.]]]).astype(np.float32),
- 'der_BoutD': np.array([[[0.]]]).astype(np.float32)}
- results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs|init_states, name = 'net_model1' )
+ m.exportONNX(outputs_order=["Bout", "Aout"], models="model1")
+
+ init_inputs = {
+ "Ain1": np.array([[[[1.0]]], [[[1.0]]]]).astype(np.float32),
+ "Bin1": np.array([[[[2.0]]], [[[2.0]]]]).astype(np.float32),
+ }
+ init_states = {
+ "AoutD": np.array([[[0.0]]]).astype(np.float32),
+ "int_AoutD": np.array([[[0.0]]]).astype(np.float32),
+ "der_AoutD": np.array([[[0.0], [0.0]]]).astype(np.float32),
+ "BoutD": np.array([[[0.0]]]).astype(np.float32),
+ "int_BoutD": np.array([[[0.0], [0.0]]]).astype(np.float32),
+ "der_BoutD": np.array([[[0.0]]]).astype(np.float32),
+ }
+ results = Modely(workspace=result_path, visualizer=None).onnxInference(
+ init_inputs | init_states, name="net_model1"
+ )
self.assertEqual([[[-20.0]]], results[0][0])
self.assertEqual([[[20.0]]], results[0][1])
self.assertEqual([[[1.5]]], results[1][0])
@@ -1031,32 +1409,45 @@ def test_partial_model_export_intern_extern_connection(self):
def test_export_report_recurrent(self):
NeuObj.clearNames()
- result_path = 'results'
+ result_path = "results"
test = Modely(visualizer=None, seed=42, workspace=result_path)
- x = Input('x')
- y = Input('y')
- z = Input('z')
- target = Input('target')
- a = Parameter('a', dimensions=1, sw=1, values=[[1]])
- b = Parameter('b', dimensions=1, sw=1, values=[[1]])
- c = Parameter('c', dimensions=1, sw=1, values=[[1]])
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
+ target = Input("target")
+ a = Parameter("a", dimensions=1, sw=1, values=[[1]])
+ b = Parameter("b", dimensions=1, sw=1, values=[[1]])
+ c = Parameter("c", dimensions=1, sw=1, values=[[1]])
fir_x = Fir(W=a)(x.last())
fir_y = Fir(W=b)(y.last())
fir_z = Fir(W=c)(z.last())
- data_x, data_y, data_z = np.random.rand(20), np.random.rand(20), np.random.rand(20)
- dataset = {'x': data_x, 'y': data_y, 'z': data_z, 'target': 3 * data_x + 3 * data_y + 3 * data_z}
+ data_x, data_y, data_z = (
+ np.random.rand(20),
+ np.random.rand(20),
+ np.random.rand(20),
+ )
+ dataset = {
+ "x": data_x,
+ "y": data_y,
+ "z": data_z,
+ "target": 3 * data_x + 3 * data_y + 3 * data_z,
+ }
fir_x.connect(y)
sum_rel = fir_x + fir_y + fir_z
sum_rel.closedLoop(z)
- out = Output('out', sum_rel)
- test.addModel('model', out)
- test.addMinimize('error', target.last(), out)
+ out = Output("out", sum_rel)
+ test.addModel("model", out)
+ test.addMinimize("error", target.last(), out)
test.neuralizeModel(0.5)
- test.loadData(name='test_dataset', source=dataset)
+ test.loadData(name="test_dataset", source=dataset)
## Train
- test.trainAndAnalyze(optimizer='SGD', training_params={'num_of_epochs': 2, 'lr': 0.0001, 'train_batch_size': 1},
- splits=[100, 0, 0], prediction_samples=10) # Train the traced model
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ training_params={"num_of_epochs": 2, "lr": 0.0001, "train_batch_size": 1},
+ splits=[100, 0, 0],
+ prediction_samples=10,
+ ) # Train the traced model
test.exportReport()
if os.path.exists(test.getWorkspace()):
- shutil.rmtree(test.getWorkspace())
\ No newline at end of file
+ shutil.rmtree(test.getWorkspace())
diff --git a/tests/test_input_dimensions.py b/tests/test_input_dimensions.py
index 687443ca..5e930a02 100644
--- a/tests/test_input_dimensions.py
+++ b/tests/test_input_dimensions.py
@@ -1,4 +1,7 @@
-import unittest, sys, os, torch
+import unittest
+import sys
+import os
+import torch
import numpy as np
from nnodely import *
@@ -20,61 +23,61 @@
# And finally the dimensions for each relation
# relation_samples
-class ModelyNetworkBuildingTest(unittest.TestCase):
+class ModelyNetworkBuildingTest(unittest.TestCase):
def test_network_building_very_simple(self):
NeuObj.clearNames()
- input1 = Input('in1')
+ input1 = Input("in1")
rel1 = Fir(input1.last())
- fun = Output('out', rel1)
+ fun = Output("out", rel1)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0,test.input_tw_backward['in1'])
- #self.assertEqual(0,test.input_tw_forward['in1'])
- self.assertEqual(1,test.json['Inputs']['in1']['ns'][0])
- self.assertEqual(0,test.json['Inputs']['in1']['ns'][1])
- self.assertEqual(1,test.json['Inputs']['in1']['ntot'])
+ # self.assertEqual(0,test.input_tw_backward['in1'])
+ # self.assertEqual(0,test.input_tw_forward['in1'])
+ self.assertEqual(1, test.json["Inputs"]["in1"]["ns"][0])
+ self.assertEqual(0, test.json["Inputs"]["in1"]["ns"][1])
+ self.assertEqual(1, test.json["Inputs"]["in1"]["ntot"])
- self.assertEqual(1,test.json['Info']['ns'][0])
- self.assertEqual(0,test.json['Info']['ns'][1])
- self.assertEqual(1,test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(1, test.json["Info"]["ns"][0])
+ self.assertEqual(0, test.json["Info"]["ns"][1])
+ self.assertEqual(1, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_simple(self):
NeuObj.clearNames()
- input1 = Input('in1')
+ input1 = Input("in1")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw(0.01))
- fun = Output('out',rel1+rel2)
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.05,test.input_tw_backward['in1'])
- #self.assertEqual(0,test.input_tw_forward['in1'])
- self.assertEqual(5,test.json['Inputs']['in1']['ns'][0])
- self.assertEqual(0,test.json['Inputs']['in1']['ns'][1])
- self.assertEqual(5,test.json['Inputs']['in1']['ntot'])
+ # self.assertEqual(0.05,test.input_tw_backward['in1'])
+ # self.assertEqual(0,test.input_tw_forward['in1'])
+ self.assertEqual(5, test.json["Inputs"]["in1"]["ns"][0])
+ self.assertEqual(0, test.json["Inputs"]["in1"]["ns"][1])
+ self.assertEqual(5, test.json["Inputs"]["in1"]["ntot"])
- self.assertEqual(5,test.json['Info']['ns'][0])
- self.assertEqual(0,test.json['Info']['ns'][1])
- self.assertEqual(5,test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(0, test.json["Info"]["ns"][1])
+ self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw(self):
NeuObj.clearNames()
- input1 = Input('in1')
- input2 = Input('in2')
+ input1 = Input("in1")
+ input2 = Input("in2")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw(0.01))
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.02,0.02]))
- fun = Output('out',rel1+rel2+rel3+rel4)
+ rel4 = Fir(input2.tw([-0.02, 0.02]))
+ fun = Output("out", rel1 + rel2 + rel3 + rel4)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
# self.assertEqual({'in1': 0.05, 'in2': 0.05}, test.input_tw_backward)
@@ -82,109 +85,122 @@ def test_network_building_tw(self):
# self.assertEqual({'in1': 5, 'in2': 5},test.input_ns_backward)
# self.assertEqual({'in1': 0, 'in2': 2},test.input_ns_forward)
# self.assertEqual({'in1': 5, 'in2': 7},test.input_n_samples)
- self.assertEqual([5,0] ,test.json['Inputs']['in1']['ns'])
- self.assertEqual([5,2],test.json['Inputs']['in2']['ns'])
- self.assertEqual(5,test.json['Inputs']['in1']['ntot'])
- self.assertEqual(7,test.json['Inputs']['in2']['ntot'])
+ self.assertEqual([5, 0], test.json["Inputs"]["in1"]["ns"])
+ self.assertEqual([5, 2], test.json["Inputs"]["in2"]["ns"])
+ self.assertEqual(5, test.json["Inputs"]["in1"]["ntot"])
+ self.assertEqual(7, test.json["Inputs"]["in2"]["ntot"])
- self.assertEqual(5,test.json['Info']['ns'][0])
- self.assertEqual(2,test.json['Info']['ns'][1])
- self.assertEqual(7,test.json['Info']['ntot']) # 5 samples + 2 samples of the horizon
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(2, test.json["Info"]["ns"][1])
+ self.assertEqual(
+ 7, test.json["Info"]["ntot"]
+ ) # 5 samples + 2 samples of the horizon
def test_network_building_tw2(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.02,0.02]))
- rel5 = Fir(input2.tw([-0.03,0.03]))
+ rel4 = Fir(input2.tw([-0.02, 0.02]))
+ rel5 = Fir(input2.tw([-0.03, 0.03]))
rel6 = Fir(input2.tw([-0.03, 0]))
rel7 = Fir(input2.tw(0.03))
- fun = Output('out',rel3+rel4+rel5+rel6+rel7)
+ fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.05,test.input_tw_backward['in2'])
- #self.assertEqual(0.03,test.input_tw_forward['in2'])
- self.assertEqual(5,test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(3,test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(8,test.json['Inputs']['in2']['ntot']) # 5 samples + 3 samples of the horizon
+ # self.assertEqual(0.05,test.input_tw_backward['in2'])
+ # self.assertEqual(0.03,test.input_tw_forward['in2'])
+ self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(3, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 8, test.json["Inputs"]["in2"]["ntot"]
+ ) # 5 samples + 3 samples of the horizon
- self.assertEqual(5,test.json['Info']['ns'][0])
- self.assertEqual(3,test.json['Info']['ns'][1])
- self.assertEqual(8,test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(3, test.json["Info"]["ns"][1])
+ self.assertEqual(8, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw3(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.01,0.03]))
- rel5 = Fir(input2.tw([-0.04,0.01]))
- fun = Output('out',rel3+rel4+rel5)
+ rel4 = Fir(input2.tw([-0.01, 0.03]))
+ rel5 = Fir(input2.tw([-0.04, 0.01]))
+ fun = Output("out", rel3 + rel4 + rel5)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.05, test.input_tw_backward['in2'])
- #self.assertEqual(0.03, test.input_tw_forward['in2'])
- self.assertEqual(5, test.json['Inputs']['in2']['ns'][0],)
- self.assertEqual(3, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(8, test.json['Inputs']['in2']['ntot']) # 5 samples + 3 samples of the horizon
-
- self.assertEqual(5, test.json['Info']['ns'][0])
- self.assertEqual(3, test.json['Info']['ns'][1])
- self.assertEqual(8, test.json['Info']['ntot']) # 5 samples
+ # self.assertEqual(0.05, test.input_tw_backward['in2'])
+ # self.assertEqual(0.03, test.input_tw_forward['in2'])
+ self.assertEqual(
+ 5,
+ test.json["Inputs"]["in2"]["ns"][0],
+ )
+ self.assertEqual(3, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 8, test.json["Inputs"]["in2"]["ntot"]
+ ) # 5 samples + 3 samples of the horizon
+
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(3, test.json["Info"]["ns"][1])
+ self.assertEqual(8, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw_with_offest(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.04,0.02]))
+ rel4 = Fir(input2.tw([-0.04, 0.02]))
rel5 = Fir(input2.tw([-0.04, 0.02], offset=-0.04))
rel6 = Fir(input2.tw([-0.04, 0.02], offset=-0.01))
rel7 = Fir(input2.tw([-0.04, 0.02], offset=0.01))
- fun = Output('out',rel3+rel4+rel5+rel6+rel7)
+ fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.05, test.input_tw_backward['in2'])
- #self.assertEqual(0.02, test.input_tw_forward['in2'])
- self.assertEqual(5, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(2, test.json['Inputs']['in2']['ns'][1] )
- self.assertEqual(7, test.json['Inputs']['in2']['ntot']) # 5 samples + 2 samples of the horizon
+ # self.assertEqual(0.05, test.input_tw_backward['in2'])
+ # self.assertEqual(0.02, test.input_tw_forward['in2'])
+ self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 7, test.json["Inputs"]["in2"]["ntot"]
+ ) # 5 samples + 2 samples of the horizon
- self.assertEqual(5, test.json['Info']['ns'][0])
- self.assertEqual(2, test.json['Info']['ns'][1])
- self.assertEqual(7,test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(2, test.json["Info"]["ns"][1])
+ self.assertEqual(7, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw_negative(self):
NeuObj.clearNames()
- input2 = Input('in2')
- rel1 = Fir(input2.tw([-0.05,-0.01]))
- rel2 = Fir(input2.tw([-0.06,-0.03]))
- fun = Output('out',rel1+rel2)
+ input2 = Input("in2")
+ rel1 = Fir(input2.tw([-0.05, -0.01]))
+ rel2 = Fir(input2.tw([-0.06, -0.03]))
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.06,test.input_tw_backward['in2'])
- #self.assertEqual( -0.01, test.input_tw_forward['in2'])
- self.assertEqual(6, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(-1, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(5, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon
+ # self.assertEqual(0.06,test.input_tw_backward['in2'])
+ # self.assertEqual( -0.01, test.input_tw_forward['in2'])
+ self.assertEqual(6, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 5, test.json["Inputs"]["in2"]["ntot"]
+ ) # 6 samples - 1 samples of the horizon
- self.assertEqual(6, test.json['Info']['ns'][0])
- self.assertEqual(-1, test.json['Info']['ns'][1])
- self.assertEqual(5, test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(6, test.json["Info"]["ns"][0])
+ self.assertEqual(-1, test.json["Info"]["ns"][1])
+ self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw_negative_with_offset(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel1 = Fir(input2.tw([-0.05, -0.01], offset=-0.05))
rel2 = Fir(input2.tw([-0.02, -0.01], offset=-0.02))
rel3 = Fir(input2.tw([-0.06, -0.03], offset=-0.06))
@@ -195,55 +211,59 @@ def test_network_building_tw_negative_with_offset(self):
input2.tw([-0.06, -0.03], offset=-0.07)
with self.assertRaises(IndexError):
input2.tw([-0.06, -0.01], offset=-0.01)
- fun = Output('out', rel1 + rel2 + rel3 + rel4)
+ fun = Output("out", rel1 + rel2 + rel3 + rel4)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.06,test.input_tw_backward['in2'])
- #self.assertEqual( -0.01, test.input_tw_forward['in2'])
- self.assertEqual(6, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(-1, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(5, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon
+ # self.assertEqual(0.06,test.input_tw_backward['in2'])
+ # self.assertEqual( -0.01, test.input_tw_forward['in2'])
+ self.assertEqual(6, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 5, test.json["Inputs"]["in2"]["ntot"]
+ ) # 6 samples - 1 samples of the horizon
- self.assertEqual(6, test.json['Info']['ns'][0])
- self.assertEqual(-1, test.json['Info']['ns'][1])
- self.assertEqual(5, test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(6, test.json["Info"]["ns"][0])
+ self.assertEqual(-1, test.json["Info"]["ns"][1])
+ self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw_positive(self):
NeuObj.clearNames()
- input1 = Input('in1')
- rel = Fir(input1.tw([0.03,0.04]))
- fun = Output('out1', rel)
+ input1 = Input("in1")
+ rel = Fir(input1.tw([0.03, 0.04]))
+ fun = Output("out1", rel)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- input2 = Input('in2')
- rel1 = Fir(input2.tw([0.01,0.04]))
- rel2 = Fir(input2.tw([0.03,0.07]))
- fun = Output('out2',rel1+rel2)
+ input2 = Input("in2")
+ rel1 = Fir(input2.tw([0.01, 0.04]))
+ rel2 = Fir(input2.tw([0.03, 0.07]))
+ fun = Output("out2", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(-0.01, test.input_tw_backward['in2'])
- #self.assertEqual(0.07, test.input_tw_forward['in2'])
- self.assertEqual(-1, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(7, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(6, test.json['Inputs']['in2']['ntot']) # -1 samples + 6 samples of the horizon
+ # self.assertEqual(-0.01, test.input_tw_backward['in2'])
+ # self.assertEqual(0.07, test.input_tw_forward['in2'])
+ self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(7, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 6, test.json["Inputs"]["in2"]["ntot"]
+ ) # -1 samples + 6 samples of the horizon
- self.assertEqual(-1, test.json['Info']['ns'][0])
- self.assertEqual(7, test.json['Info']['ns'][1])
- self.assertEqual(6, test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(-1, test.json["Info"]["ns"][0])
+ self.assertEqual(7, test.json["Info"]["ns"][1])
+ self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_tw_positive_with_offset(self):
NeuObj.clearNames()
- input2 = Input('in2')
- rel1 = Fir(input2.tw([0.01,0.04],offset=0.02))
- rel2 = Fir(input2.tw([0.03,0.07],offset=0.04))
+ input2 = Input("in2")
+ rel1 = Fir(input2.tw([0.01, 0.04], offset=0.02))
+ rel2 = Fir(input2.tw([0.03, 0.07], offset=0.04))
with self.assertRaises(ValueError):
input2.tw([0.03, 0.02])
with self.assertRaises(IndexError):
@@ -251,116 +271,120 @@ def test_network_building_tw_positive_with_offset(self):
with self.assertRaises(IndexError):
input2.tw([0.03, 0.07], offset=0)
- fun = Output('out', rel1 + rel2)
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(-0.01,test.input_tw_backward['in2'])
- #self.assertEqual( 0.07, test.input_tw_forward['in2'])
- self.assertEqual(-1, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(7, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(6, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon
+ # self.assertEqual(-0.01,test.input_tw_backward['in2'])
+ # self.assertEqual( 0.07, test.input_tw_forward['in2'])
+ self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(7, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(
+ 6, test.json["Inputs"]["in2"]["ntot"]
+ ) # 6 samples - 1 samples of the horizon
- self.assertEqual(-1, test.json['Info']['ns'][0])
- self.assertEqual(7, test.json['Info']['ns'][1])
- self.assertEqual(6, test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(-1, test.json["Info"]["ns"][0])
+ self.assertEqual(7, test.json["Info"]["ns"][1])
+ self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_sw(self):
NeuObj.clearNames()
- input1 = Input('in1')
+ input1 = Input("in1")
rel3 = Fir(input1.sw(2))
- rel4 = Fir(input1.sw([-2,2]))
- rel5 = Fir(input1.sw([-3,3]))
+ rel4 = Fir(input1.sw([-2, 2]))
+ rel5 = Fir(input1.sw([-3, 3]))
rel6 = Fir(input1.sw([-3, 0]))
rel7 = Fir(input1.sw(3))
- fun = Output('out',rel3+rel4+rel5+rel6+rel7)
+ fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0,test.input_tw_backward['in1'])
- #self.assertEqual(0,test.input_tw_forward['in1'])
- self.assertEqual(3,test.json['Inputs']['in1']['ns'][0])
- self.assertEqual(3,test.json['Inputs']['in1']['ns'][1])
- self.assertEqual(6,test.json['Inputs']['in1']['ntot']) # 6 samples - 1 samples of the horizon
+ # self.assertEqual(0,test.input_tw_backward['in1'])
+ # self.assertEqual(0,test.input_tw_forward['in1'])
+ self.assertEqual(3, test.json["Inputs"]["in1"]["ns"][0])
+ self.assertEqual(3, test.json["Inputs"]["in1"]["ns"][1])
+ self.assertEqual(
+ 6, test.json["Inputs"]["in1"]["ntot"]
+ ) # 6 samples - 1 samples of the horizon
- self.assertEqual(3,test.json['Info']['ns'][0])
- self.assertEqual(3,test.json['Info']['ns'][1])
- self.assertEqual(6,test.json['Info']['ntot']) # 5 samples
+ self.assertEqual(3, test.json["Info"]["ns"][0])
+ self.assertEqual(3, test.json["Info"]["ns"][1])
+ self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples
def test_network_building_sw_with_offset(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.sw(5))
- rel4 = Fir(input2.sw([-4,2]))
+ rel4 = Fir(input2.sw([-4, 2]))
rel5 = Fir(input2.sw([-4, 2], offset=0))
rel6 = Fir(input2.sw([-4, 2], offset=1))
rel7 = Fir(input2.sw([-2, 2], offset=1))
rel8 = Fir(input2.sw([-4, 2], offset=-3))
- fun = Output('out',rel3+rel4+rel5+rel6+rel7+rel8)
+ fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7 + rel8)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0, test.input_tw_backward['in2'])
- #self.assertEqual(0, test.input_tw_forward['in2'])
- self.assertEqual(5, test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(2, test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(7, test.json['Inputs']['in2']['ntot'])
+ # self.assertEqual(0, test.input_tw_backward['in2'])
+ # self.assertEqual(0, test.input_tw_forward['in2'])
+ self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(7, test.json["Inputs"]["in2"]["ntot"])
- self.assertEqual(5, test.json['Info']['ns'][0])
- self.assertEqual(2, test.json['Info']['ns'][1])
- self.assertEqual(7, test.json['Info']['ntot'])
+ self.assertEqual(5, test.json["Info"]["ns"][0])
+ self.assertEqual(2, test.json["Info"]["ns"][1])
+ self.assertEqual(7, test.json["Info"]["ntot"])
def test_network_building_sw_and_tw(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
with self.assertRaises(TypeError):
- input2.sw(5)+input2.tw(0.05)
+ input2.sw(5) + input2.tw(0.05)
- rel1 = Fir(input2.sw([-4,2]))+Fir(input2.tw([-0.01,0]))
- fun = Output('out',rel1)
+ rel1 = Fir(input2.sw([-4, 2])) + Fir(input2.tw([-0.01, 0]))
+ fun = Output("out", rel1)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- #self.assertEqual(0.01,test.input_tw_backward['in2'])
- #self.assertEqual(0,test.input_tw_forward['in2'])
- self.assertEqual(4,test.json['Inputs']['in2']['ns'][0])
- self.assertEqual(2,test.json['Inputs']['in2']['ns'][1])
- self.assertEqual(6,test.json['Inputs']['in2']['ntot'])
+ # self.assertEqual(0.01,test.input_tw_backward['in2'])
+ # self.assertEqual(0,test.input_tw_forward['in2'])
+ self.assertEqual(4, test.json["Inputs"]["in2"]["ns"][0])
+ self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1])
+ self.assertEqual(6, test.json["Inputs"]["in2"]["ntot"])
- self.assertEqual(4,test.json['Info']['ns'][0])
- self.assertEqual(2,test.json['Info']['ns'][1])
- self.assertEqual(6,test.json['Info']['ntot'])
+ self.assertEqual(4, test.json["Info"]["ns"][0])
+ self.assertEqual(2, test.json["Info"]["ns"][1])
+ self.assertEqual(6, test.json["Info"]["ntot"])
def test_example_parametric_different_dim_input(self):
NeuObj.clearNames()
test = Modely(visualizer=None, seed=42)
- x = Input('x')
- y = Input('y')
- z = Input('z')
+ x = Input("x")
+ y = Input("y")
+ z = Input("z")
## create the relations
def myFun(K1, p1, p2):
return K1 * p1 * p2
- K_x = Parameter('k_x', dimensions=1, tw=1)
- K_y = Parameter('k_y', dimensions=1, tw=1)
- w = Parameter('w', dimensions=1, tw=1)
- t = Parameter('t', dimensions=1, tw=1)
- c_v = Constant('c_v', tw=1, values=[[1], [2]])
+ K_x = Parameter("k_x", dimensions=1, tw=1)
+ K_y = Parameter("k_y", dimensions=1, tw=1)
+ w = Parameter("w", dimensions=1, tw=1)
+ t = Parameter("t", dimensions=1, tw=1)
+ c_v = Constant("c_v", tw=1, values=[[1], [2]])
c = 5
- w_5 = Parameter('w_5', dimensions=1, tw=5)
- t_5 = Parameter('t_5', dimensions=1, tw=5)
+ w_5 = Parameter("w_5", dimensions=1, tw=5)
+ t_5 = Parameter("t_5", dimensions=1, tw=5)
c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
- c_5_2 = Constant('c_5_2', tw=5, values=c_5)
- parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v])
+ c_5_2 = Constant("c_5_2", tw=5, values=c_5)
+ parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v])
parfun_y = ParamFun(myFun, parameters_and_constants=[K_y])
parfun_z = ParamFun(myFun)
fir_w = Fir(W=w_5)(x.tw(5))
@@ -373,34 +397,37 @@ def fuzzyfun(x):
fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1))
- out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)))
- out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
- out3 = Output('out3', Add(fir_w, fir_t))
- out4 = Output('out4', Linear(output_dimension=1)(fuzzy))
- out5 = Output('out5', Fir(time_part) + Fir(sample_select))
- out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy))
+ out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)))
+ out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
+ out3 = Output("out3", Add(fir_w, fir_t))
+ out4 = Output("out4", Linear(output_dimension=1)(fuzzy))
+ out5 = Output("out5", Fir(time_part) + Fir(sample_select))
+ out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy))
with self.assertRaises(TypeError):
parfun_z(x.tw(5), t_5, c_5)
- out7 = Output('out7', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_z(x.tw(5), t_5, c_5_2)))
+ out7 = Output(
+ "out7",
+ Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))
+ + Fir(parfun_z(x.tw(5), t_5, c_5_2)),
+ )
# parfun = ParamFun(myFun, map_over_batch=True)
# p = Constant('co', values=[[2]])
# with self.assertRaises(TypeError):
# Output('out-12', parfun(p, x.sw(4)))
-
- test.addModel('modelA', out)
- test.addModel('modelB', [out2, out3, out4])
- test.addModel('modelC', [out4, out5, out6])
- test.addModel('modelD', [out7])
- test.addMinimize('error1', x.last(), out)
- test.addMinimize('error2', y.last(), out3, loss_function='rmse')
- test.addMinimize('error3', z.last(), out6, loss_function='rmse')
+ test.addModel("modelA", out)
+ test.addModel("modelB", [out2, out3, out4])
+ test.addModel("modelC", [out4, out5, out6])
+ test.addModel("modelD", [out7])
+ test.addMinimize("error1", x.last(), out)
+ test.addMinimize("error2", y.last(), out3, loss_function="rmse")
+ test.addMinimize("error3", z.last(), out6, loss_function="rmse")
test.neuralizeModel(0.5)
- self.assertEqual([10,0],test.json['Inputs']['x']['ns'])
- self.assertEqual([10,0],test.json['Inputs']['y']['ns'])
- self.assertEqual([1,0],test.json['Inputs']['z']['ns'])
+ self.assertEqual([10, 0], test.json["Inputs"]["x"]["ns"])
+ self.assertEqual([10, 0], test.json["Inputs"]["y"]["ns"])
+ self.assertEqual([1, 0], test.json["Inputs"]["z"]["ns"])
#
# self.assertEqual(4,test.json['Info']['ns'][0])
# self.assertEqual(2,test.json['Info']['ns'][1])
@@ -409,30 +436,51 @@ def fuzzyfun(x):
def test_batch_size_and_step(self):
NeuObj.clearNames()
test = Modely(visualizer=None, seed=42, log_internal=True)
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
rel_out = Fir(x.last()) + Fir(y.last())
rel_out.closedLoop(y)
- out = Output('out', rel_out)
+ out = Output("out", rel_out)
- test.addModel('modelA', out)
- test.addMinimize('error1', out, x.next())
+ test.addModel("modelA", out)
+ test.addMinimize("error1", out, x.next())
test.neuralizeModel()
data_x = np.random.rand(101, 1)
data_y = np.random.rand(101, 1)
- dataset = {'x': data_x, 'y': data_y}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"x": data_x, "y": data_y}
+ test.loadData(name="dataset", source=dataset)
## 100 // (step+batch) = 2
- test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=30, prediction_samples=20, shuffle_data=False)
+ test.trainModel(
+ train_dataset="dataset",
+ num_of_epochs=1,
+ train_batch_size=10,
+ step=30,
+ prediction_samples=20,
+ shuffle_data=False,
+ )
self.assertEqual(2 * 21, len(test.internals.keys()))
## Clip the step to the maximum number of samples (100 - prediction_samples - batch) = 70
- test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=200, prediction_samples=20, shuffle_data=True)
+ test.trainModel(
+ train_dataset="dataset",
+ num_of_epochs=1,
+ train_batch_size=10,
+ step=200,
+ prediction_samples=20,
+ shuffle_data=True,
+ )
self.assertEqual(1 * 21, len(test.internals.keys()))
- ## Clip the step to 0
- test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=-4, prediction_samples=20, shuffle_data=True)
+ ## Clip the step to 0
+ test.trainModel(
+ train_dataset="dataset",
+ num_of_epochs=1,
+ train_batch_size=10,
+ step=-4,
+ prediction_samples=20,
+ shuffle_data=True,
+ )
self.assertEqual(8 * 21, len(test.internals.keys()))
diff --git a/tests/test_json.py b/tests/test_json.py
index 689107ee..0c6b2107 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -1,4 +1,7 @@
-import sys, os, unittest, copy
+import sys
+import os
+import unittest
+import copy
import numpy as np
@@ -17,685 +20,1004 @@
# the dimensions that are propagated through the relations
# and the structure of the json itself
-def myFun(K1,K2,p1,p2):
+
+def myFun(K1, K2, p1, p2):
import torch
- return p1*K1+p2*torch.sin(K2)
-def myFun_out5(K1,p1):
+ return p1 * K1 + p2 * torch.sin(K2)
+
+
+def myFun_out5(K1, p1):
import torch
- return torch.stack([K1,K1,K1,K1,K1],dim=2).squeeze(-1)*p1
+
+ return torch.stack([K1, K1, K1, K1, K1], dim=2).squeeze(-1) * p1
+
def myFunPar(x, p1):
import torch
+
if len(p1.shape) == 0:
out = torch.tensor([[[1]]]).repeat((x.shape[0], 1, 1))
else:
- out = torch.tensor([[p1.shape]]).repeat((x.shape[0],1,1))
+ out = torch.tensor([[p1.shape]]).repeat((x.shape[0], 1, 1))
return out
+
NeuObj.count = 0
+
class ModelyJsonTest(unittest.TestCase):
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
- self.assertEqual(len(data1),len(data2))
+ self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.TestAlmostEqual(pred, label, precision=precision)
else:
self.assertAlmostEqual(data1, data2, places=precision)
def test_input(self):
- input = Input('in1')
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1}}, 'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json)
-
- #Discrete input removed
- #input = Input('in', values=[2,3,4])
- #self.assertEqual({'Inputs': {'in': {'dim': 1, 'discrete': [2,3,4], 'tw': [0,0], 'sw': [0, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json)
+ input = Input("in1")
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in1": {"dim": 1}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {},
+ },
+ input.json,
+ )
+
+ # Discrete input removed
+ # input = Input('in', values=[2,3,4])
+ # self.assertEqual({'Inputs': {'in': {'dim': 1, 'discrete': [2,3,4], 'tw': [0,0], 'sw': [0, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json)
def test_aritmetic(self):
Stream.resetCount()
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
inlast = input.last()
- out = inlast+inlast
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'sw': [-1, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add2': ['Add', ['SamplePart1', 'SamplePart1']],
- 'SamplePart1': ['SamplePart', ['in1'], -1, [-1, 0]]}},out.json)
+ out = inlast + inlast
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in1": {"dim": 1, "sw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add2": ["Add", ["SamplePart1", "SamplePart1"]],
+ "SamplePart1": ["SamplePart", ["in1"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
out = input.tw(1) + input.tw(1)
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add7': ['Add', ['TimePart4', 'TimePart6']],
- 'TimePart4': ['TimePart', ['in1'], -1, [-1, 0]],
- 'TimePart6': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json)
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add7": ["Add", ["TimePart4", "TimePart6"]],
+ "TimePart4": ["TimePart", ["in1"], -1, [-1, 0]],
+ "TimePart6": ["TimePart", ["in1"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
out = input.tw(1) * input.tw(1)
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Mul12': ['Mul', ['TimePart9', 'TimePart11']],
- 'TimePart9': ['TimePart', ['in1'], -1, [-1, 0]],
- 'TimePart11': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json)
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Mul12": ["Mul", ["TimePart9", "TimePart11"]],
+ "TimePart9": ["TimePart", ["in1"], -1, [-1, 0]],
+ "TimePart11": ["TimePart", ["in1"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
out = input.tw(1) - input.tw(1)
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Sub17': ['Sub', ['TimePart14', 'TimePart16']],
- 'TimePart14': ['TimePart', ['in1'], -1, [-1, 0]],
- 'TimePart16': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json)
- input = Input('in2', dimensions = 5)
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Sub17": ["Sub", ["TimePart14", "TimePart16"]],
+ "TimePart14": ["TimePart", ["in1"], -1, [-1, 0]],
+ "TimePart16": ["TimePart", ["in1"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
+ input = Input("in2", dimensions=5)
inlast = input.last()
out = inlast + inlast
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'sw': [-1, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add20': ['Add', ['SamplePart19', 'SamplePart19']],
- 'SamplePart19': ['SamplePart', ['in2'], -1, [-1, 0]]}},out.json)
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in2": {"dim": 5, "sw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add20": ["Add", ["SamplePart19", "SamplePart19"]],
+ "SamplePart19": ["SamplePart", ["in2"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
out = input.tw(1) + input.tw(1)
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [-1, 0]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add25': ['Add', ['TimePart22', 'TimePart24']],
- 'TimePart22': ['TimePart', ['in2'], -1, [-1, 0]],
- 'TimePart24': ['TimePart', ['in2'], -1, [-1, 0]]}}, out.json)
- out = input.tw([2,5]) + input.tw([3,6])
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [2, 6]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add30': ['Add', ['TimePart27', 'TimePart29']],
- 'TimePart27': ['TimePart', ['in2'], -1, [2, 5]],
- 'TimePart29': ['TimePart', ['in2'], -1, [3, 6]]}}, out.json)
- out = input.tw([-5,-2]) + input.tw([-6,-3])
- self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [-6, -2]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add35': ['Add', ['TimePart32', 'TimePart34']],
- 'TimePart32': ['TimePart', ['in2'], -1, [-5, -2]],
- 'TimePart34': ['TimePart', ['in2'], -1, [-6, -3]]}}, out.json)
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in2": {"dim": 5, "tw": [-1, 0]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add25": ["Add", ["TimePart22", "TimePart24"]],
+ "TimePart22": ["TimePart", ["in2"], -1, [-1, 0]],
+ "TimePart24": ["TimePart", ["in2"], -1, [-1, 0]],
+ },
+ },
+ out.json,
+ )
+ out = input.tw([2, 5]) + input.tw([3, 6])
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in2": {"dim": 5, "tw": [2, 6]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add30": ["Add", ["TimePart27", "TimePart29"]],
+ "TimePart27": ["TimePart", ["in2"], -1, [2, 5]],
+ "TimePart29": ["TimePart", ["in2"], -1, [3, 6]],
+ },
+ },
+ out.json,
+ )
+ out = input.tw([-5, -2]) + input.tw([-6, -3])
+ self.assertEqual(
+ {
+ "Info": {},
+ "Constants": {},
+ "Inputs": {"in2": {"dim": 5, "tw": [-6, -2]}},
+ "Functions": {},
+ "Parameters": {},
+ "Outputs": {},
+ "Relations": {
+ "Add35": ["Add", ["TimePart32", "TimePart34"]],
+ "TimePart32": ["TimePart", ["in2"], -1, [-5, -2]],
+ "TimePart34": ["TimePart", ["in2"], -1, [-6, -3]],
+ },
+ },
+ out.json,
+ )
def test_scalar_input_dimensions(self):
NeuObj.clearNames()
- input = Input('in1').last()
- out = input+input
- self.assertEqual({'dim': 1,'sw': 1}, out.dim)
+ input = Input("in1").last()
+ out = input + input
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
out = Fir(input)
- self.assertEqual({'dim': 1,'sw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
out = Fir(7)(input)
- self.assertEqual({'dim': 7,'sw': 1}, out.dim)
- out = Fuzzify(5, [-1,1])(input)
- self.assertEqual({'dim': 5,'sw': 1}, out.dim)
+ self.assertEqual({"dim": 7, "sw": 1}, out.dim)
+ out = Fuzzify(5, [-1, 1])(input)
+ self.assertEqual({"dim": 5, "sw": 1}, out.dim)
out = ParamFun(myFun)(input)
- self.assertEqual({'dim': 1,'sw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
out = ParamFun(myFun_out5)(input)
- self.assertEqual({'dim': 5, 'sw': 1}, out.dim)
+ self.assertEqual({"dim": 5, "sw": 1}, out.dim)
with self.assertRaises(ValueError):
out = Fir(Fir(7)(input))
#
with self.assertRaises(IndexError):
- out = Part(input,0,4)
+ out = Part(input, 0, 4)
inpart = ParamFun(myFun_out5)(input)
- out = Part(inpart,0,4)
- self.assertEqual({'dim': 4, 'sw': 1}, out.dim)
- out = Part(inpart,0,1)
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
- out = Part(inpart,1,3)
- self.assertEqual({'dim': 2, 'sw': 1}, out.dim)
- out = Select(inpart,0)
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
+ out = Part(inpart, 0, 4)
+ self.assertEqual({"dim": 4, "sw": 1}, out.dim)
+ out = Part(inpart, 0, 1)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
+ out = Part(inpart, 1, 3)
+ self.assertEqual({"dim": 2, "sw": 1}, out.dim)
+ out = Select(inpart, 0)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
with self.assertRaises(IndexError):
- out = Select(inpart,5)
+ out = Select(inpart, 5)
with self.assertRaises(IndexError):
- out = Select(inpart,-1)
+ out = Select(inpart, -1)
with self.assertRaises(KeyError):
- out = TimePart(inpart,-1,0)
+ out = TimePart(inpart, -1, 0)
def test_scalar_input_tw_dimensions(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
out = input.tw(1) + input.tw(1)
- self.assertEqual({'dim': 1, 'tw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "tw": 1}, out.dim)
out = Fir(input.tw(1))
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
out = Fir(5)(input.tw(1))
- self.assertEqual({'dim': 5, 'sw': 1}, out.dim)
- out = Fuzzify(5, [0,5])(input.tw(2))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
- out = Fuzzify(5,range=[-1,5])(input.tw(2))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
- out = Fuzzify(centers=[-1,5])(input.tw(2))
- self.assertEqual({'dim': 2, 'tw': 2}, out.dim)
+ self.assertEqual({"dim": 5, "sw": 1}, out.dim)
+ out = Fuzzify(5, [0, 5])(input.tw(2))
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
+ out = Fuzzify(5, range=[-1, 5])(input.tw(2))
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
+ out = Fuzzify(centers=[-1, 5])(input.tw(2))
+ self.assertEqual({"dim": 2, "tw": 2}, out.dim)
out = ParamFun(myFun)(input.tw(1))
- self.assertEqual({'dim': 1, 'tw' : 1}, out.dim)
+ self.assertEqual({"dim": 1, "tw": 1}, out.dim)
out = ParamFun(myFun_out5)(input.tw(2))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
- out = ParamFun(myFun_out5)(input.tw(2),input.tw(1))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
+ out = ParamFun(myFun_out5)(input.tw(2), input.tw(1))
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
inpart = ParamFun(myFun_out5)(input.tw(2))
- out = Part(inpart,0,4)
- self.assertEqual({'dim': 4,'tw': 2}, out.dim)
- out = Part(inpart,0,1)
- self.assertEqual({'dim': 1,'tw': 2}, out.dim)
- out = Part(inpart,1,3)
- self.assertEqual({'dim': 2,'tw': 2}, out.dim)
- out = Select(inpart,0)
- self.assertEqual({'dim': 1,'tw': 2}, out.dim)
+ out = Part(inpart, 0, 4)
+ self.assertEqual({"dim": 4, "tw": 2}, out.dim)
+ out = Part(inpart, 0, 1)
+ self.assertEqual({"dim": 1, "tw": 2}, out.dim)
+ out = Part(inpart, 1, 3)
+ self.assertEqual({"dim": 2, "tw": 2}, out.dim)
+ out = Select(inpart, 0)
+ self.assertEqual({"dim": 1, "tw": 2}, out.dim)
with self.assertRaises(IndexError):
- out = Select(inpart,5)
+ out = Select(inpart, 5)
with self.assertRaises(IndexError):
- out = Select(inpart,-1)
- out = TimePart(inpart, 0,1)
- self.assertEqual({'dim': 5, 'tw': 1}, out.dim)
- #out = TimeSelect(inpart,0)
- #self.assertEqual({'dim': 5}, out.dim)
- #with self.assertRaises(ValueError):
+ out = Select(inpart, -1)
+ out = TimePart(inpart, 0, 1)
+ self.assertEqual({"dim": 5, "tw": 1}, out.dim)
+ # out = TimeSelect(inpart,0)
+ # self.assertEqual({'dim': 5}, out.dim)
+ # with self.assertRaises(ValueError):
# out = TimeSelect(inpart,-3)
- twinput = input.tw([-2,4])
+ twinput = input.tw([-2, 4])
out = TimePart(twinput, 0, 1)
- self.assertEqual({'dim': 1, 'tw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "tw": 1}, out.dim)
def test_scalar_input_tw2_dimensions(self):
NeuObj.clearNames()
- input = Input('in1')
- out = input.tw([-1,1])+input.tw([-2,0])
- self.assertEqual({'dim': 1, 'tw': 2}, out.dim)
- out = input.tw(1)+input.tw([-1,0])
- self.assertEqual({'dim': 1, 'tw': 1}, out.dim)
+ input = Input("in1")
+ out = input.tw([-1, 1]) + input.tw([-2, 0])
+ self.assertEqual({"dim": 1, "tw": 2}, out.dim)
+ out = input.tw(1) + input.tw([-1, 0])
+ self.assertEqual({"dim": 1, "tw": 1}, out.dim)
out = Fir(input.tw(1) + input.tw([-1, 0]))
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
- out = input.tw([-1,0])+input.tw([-4,-3])+input.tw(1)
- self.assertEqual({'dim': 1,'tw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
+ out = input.tw([-1, 0]) + input.tw([-4, -3]) + input.tw(1)
+ self.assertEqual({"dim": 1, "tw": 1}, out.dim)
with self.assertRaises(ValueError):
- out = input.tw([-2,0])-input.tw([-1,0])
+ out = input.tw([-2, 0]) - input.tw([-1, 0])
with self.assertRaises(ValueError):
- out = input.tw([-2,0])+input.tw([-1,0])
+ out = input.tw([-2, 0]) + input.tw([-1, 0])
def test_scalar_input_sw_dimensions(self):
NeuObj.clearNames()
- input = Input('in1')
- out = input.sw([-1,1])+input.sw([-2,0])
- self.assertEqual({'dim': 1, 'sw': 2}, out.dim)
- out = input.sw(1)+input.sw([-1,0])
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
+ input = Input("in1")
+ out = input.sw([-1, 1]) + input.sw([-2, 0])
+ self.assertEqual({"dim": 1, "sw": 2}, out.dim)
+ out = input.sw(1) + input.sw([-1, 0])
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
out = Fir(input.sw(1) + input.sw([-1, 0]))
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
- out = input.sw([-1,0])+input.sw([-4,-3])+input.sw(1)
- self.assertEqual({'dim': 1,'sw': 1}, out.dim)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
+ out = input.sw([-1, 0]) + input.sw([-4, -3]) + input.sw(1)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
with self.assertRaises(ValueError):
- out = input.sw([-2,0])-input.sw([-1,0])
+ out = input.sw([-2, 0]) - input.sw([-1, 0])
with self.assertRaises(ValueError):
- out = input.sw([-2,0])+input.sw([-1,0])
+ out = input.sw([-2, 0]) + input.sw([-1, 0])
with self.assertRaises(TypeError):
out = input.sw(1) + input.tw([-1, 0])
with self.assertRaises(TypeError):
out = input.sw(1.2)
with self.assertRaises(TypeError):
- out = input.sw([-1.2,0.05])
+ out = input.sw([-1.2, 0.05])
def test_vector_input_dimensions(self):
NeuObj.clearNames()
- input = Input('in1', dimensions = 5)
- self.assertEqual({'dim': 5}, input.dim)
- self.assertEqual({'dim': 5, 'tw' : 2}, input.tw(2).dim)
+ input = Input("in1", dimensions=5)
+ self.assertEqual({"dim": 5}, input.dim)
+ self.assertEqual({"dim": 5, "tw": 2}, input.tw(2).dim)
out = input.tw(1) + input.tw(1)
- self.assertEqual({'dim': 5, 'tw': 1}, out.dim)
+ self.assertEqual({"dim": 5, "tw": 1}, out.dim)
out = Relu(input.tw(1))
- self.assertEqual({'dim': 5, 'tw': 1}, out.dim)
+ self.assertEqual({"dim": 5, "tw": 1}, out.dim)
with self.assertRaises(TypeError):
Fir(7)(input)
with self.assertRaises(TypeError):
- Fuzzify(7,[1,7])(input)
+ Fuzzify(7, [1, 7])(input)
out = ParamFun(myFun)(input.tw(1))
- self.assertEqual({'dim': 5, 'tw' : 1}, out.dim)
+ self.assertEqual({"dim": 5, "tw": 1}, out.dim)
out = ParamFun(myFun)(input.tw(2))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
- out = ParamFun(myFun)(input.tw(2),input.tw(1))
- self.assertEqual({'dim': 5, 'tw': 2}, out.dim)
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
+ out = ParamFun(myFun)(input.tw(2), input.tw(1))
+ self.assertEqual({"dim": 5, "tw": 2}, out.dim)
def test_parameter_and_linear(self):
NeuObj.clearNames()
- input = Input('in1').last()
- W15 = Parameter('W15', dimensions=(1, 5))
- b15 = Parameter('b15', dimensions=5)
- input4 = Input('in4', dimensions=4).last()
- W45 = Parameter('W45', dimensions=(4, 5))
- b45 = Parameter('b45', dimensions=5)
+ input = Input("in1").last()
+ W15 = Parameter("W15", dimensions=(1, 5))
+ b15 = Parameter("b15", dimensions=5)
+ input4 = Input("in4", dimensions=4).last()
+ W45 = Parameter("W45", dimensions=(4, 5))
+ b45 = Parameter("b45", dimensions=5)
out = Linear(input) + Linear(input4)
out3 = Linear(3)(input) + Linear(3)(input4)
- outW = Linear(W = W15)(input) + Linear(W = W45)(input4)
- outWb = Linear(W = W15,b = b15)(input) + Linear(W = W45, b = b45)(input4)
- self.assertEqual({'dim': 1, 'sw': 1}, out.dim)
- self.assertEqual({'dim': 3, 'sw': 1}, out3.dim)
- self.assertEqual({'dim': 5, 'sw': 1}, outW.dim)
- self.assertEqual({'dim': 5, 'sw': 1}, outWb.dim)
+ outW = Linear(W=W15)(input) + Linear(W=W45)(input4)
+ outWb = Linear(W=W15, b=b15)(input) + Linear(W=W45, b=b45)(input4)
+ self.assertEqual({"dim": 1, "sw": 1}, out.dim)
+ self.assertEqual({"dim": 3, "sw": 1}, out3.dim)
+ self.assertEqual({"dim": 5, "sw": 1}, outW.dim)
+ self.assertEqual({"dim": 5, "sw": 1}, outWb.dim)
NeuObj.clearNames()
- input2 = Input('in1').sw([-1,1])
- W15 = Parameter('W15', dimensions=(1, 5))
- b15 = Parameter('b15', dimensions=5)
- input42 = Input('in4', dimensions=4).sw([-1,1])
- W45 = Parameter('W45', dimensions=(4, 5))
- b45 = Parameter('b45', dimensions=5)
+ input2 = Input("in1").sw([-1, 1])
+ W15 = Parameter("W15", dimensions=(1, 5))
+ b15 = Parameter("b15", dimensions=5)
+ input42 = Input("in4", dimensions=4).sw([-1, 1])
+ W45 = Parameter("W45", dimensions=(4, 5))
+ b45 = Parameter("b45", dimensions=5)
out = Linear(input2) + Linear(input42)
out3 = Linear(3)(input2) + Linear(3)(input42)
- outW = Linear(W = W15)(input2) + Linear(W = W45)(input42)
- outWb = Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input42)
- self.assertEqual({'dim': 1, 'sw': 2}, out.dim)
- self.assertEqual({'dim': 3, 'sw': 2}, out3.dim)
- self.assertEqual({'dim': 5, 'sw': 2}, outW.dim)
- self.assertEqual({'dim': 5, 'sw': 2}, outWb.dim)
+ outW = Linear(W=W15)(input2) + Linear(W=W45)(input42)
+ outWb = Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input42)
+ self.assertEqual({"dim": 1, "sw": 2}, out.dim)
+ self.assertEqual({"dim": 3, "sw": 2}, out3.dim)
+ self.assertEqual({"dim": 5, "sw": 2}, outW.dim)
+ self.assertEqual({"dim": 5, "sw": 2}, outWb.dim)
with self.assertRaises(ValueError):
Linear(input) + Linear(input42)
with self.assertRaises(ValueError):
Linear(3)(input2) + Linear(3)(input4)
with self.assertRaises(ValueError):
- Linear(W = W15)(input) + Linear(W = W45)(input42)
+ Linear(W=W15)(input) + Linear(W=W45)(input42)
with self.assertRaises(ValueError):
- Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input4)
+ Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input4)
def test_input_paramfun_param_const(self):
NeuObj.clearNames()
- input2 = Input('in2')
- def fun_test(x,y,z,k):
- return x*y*z*k
+ input2 = Input("in2")
+
+ def fun_test(x, y, z, k):
+ return x * y * z * k
NeuObj.clearNames()
out = ParamFun(fun_test)(input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0k': {'dim': 1},'FParamFun0y': {'dim': 1},'FParamFun0z': {'dim': 1}}, out.json['Parameters'])
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {
+ "FParamFun0k": {"dim": 1},
+ "FParamFun0y": {"dim": 1},
+ "FParamFun0z": {"dim": 1},
+ },
+ out.json["Parameters"],
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test)(input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0k': {'dim': 1}, 'FParamFun0z': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test)(input2.tw(0.01), input2.tw(0.01))
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0k": {"dim": 1}, "FParamFun0z": {"dim": 1}},
+ out.json["Parameters"],
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants=['t'])(input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0z': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants=["t"])(
+ input2.tw(0.01), input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0z": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"]
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants=['t','r'])(input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'r': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants=["t", "r"])(
+ input2.tw(0.01), input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual({"r": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"])
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants={'k':'t'})(input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0z': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants={"k": "t"})(
+ input2.tw(0.01), input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0z": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"]
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants={'k':(1,2)})(input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 2, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0k': {'dim': [1,2]}, 'FParamFun0z': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants={"k": (1, 2)})(
+ input2.tw(0.01), input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 2, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0k": {"dim": [1, 2]}, "FParamFun0z": {"dim": 1}},
+ out.json["Parameters"],
+ )
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants={'k':(1,2),'y':'r'})(input2.tw(0.01),input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants={"k": (1, 2), "y": "r"})(
+ input2.tw(0.01), input2.tw(0.01)
+ )
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants=[1.0,(1,2),'gg'])(input2.tw(0.01),input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants=[1.0, (1, 2), "gg"])(
+ input2.tw(0.01), input2.tw(0.01)
+ )
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants=[(1,2),'pp','c',[[1.0]]])(input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants=[(1, 2), "pp", "c", [[1.0]]])(
+ input2.tw(0.01)
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants={'k':(1,2)})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01))
- self.assertEqual({'dim': 2, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0k': {'dim': [1,2]}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants={"k": (1, 2)})(
+ input2.tw(0.01), input2.tw(0.01), input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 2, "tw": 0.01}, out.dim)
+ self.assertEqual({"FParamFun0k": {"dim": [1, 2]}}, out.json["Parameters"])
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants={'z':(1,2)})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants={"z": (1, 2)})(
+ input2.tw(0.01), input2.tw(0.01), input2.tw(0.01)
+ )
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants={'z':'g'})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants={"z": "g"})(
+ input2.tw(0.01), input2.tw(0.01), input2.tw(0.01)
+ )
with self.assertRaises(ValueError):
- ParamFun(fun_test,parameters_and_constants={'z':'o'})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants={"z": "o"})(
+ input2.tw(0.01), input2.tw(0.01), input2.tw(0.01)
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants=['pp','tt'])(input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0y': {'dim': 1}, 'pp': {'dim': 1},'tt': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants=["pp", "tt"])(input2.tw(0.01))
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0y": {"dim": 1}, "pp": {"dim": 1}, "tt": {"dim": 1}},
+ out.json["Parameters"],
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants={'y':'pp','k':'el'})(input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'FParamFun0z': {'dim': 1}, 'pp': {'dim': 1},'el': {'dim': 1}}, out.json['Parameters'])
+ out = ParamFun(fun_test, parameters_and_constants={"y": "pp", "k": "el"})(
+ input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual(
+ {"FParamFun0z": {"dim": 1}, "pp": {"dim": 1}, "el": {"dim": 1}},
+ out.json["Parameters"],
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants=['pp','oo',Constant('el',values=2.0)])(input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'oo': {'dim': 1}, 'pp': {'dim': 1}}, out.json['Parameters'])
- self.assertEqual({'el': {'dim': 1,'values':[2.0]}}, out.json['Constants'])
+ out = ParamFun(
+ fun_test, parameters_and_constants=["pp", "oo", Constant("el", values=2.0)]
+ )(input2.tw(0.01))
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual({"oo": {"dim": 1}, "pp": {"dim": 1}}, out.json["Parameters"])
+ self.assertEqual({"el": {"dim": 1, "values": [2.0]}}, out.json["Constants"])
with self.assertRaises(NameError):
- ParamFun(fun_test,parameters_and_constants=['pp','oo','el'])(input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants=["pp", "oo", "el"])(
+ input2.tw(0.01)
+ )
with self.assertRaises(NameError):
- ParamFun(fun_test,parameters_and_constants=['pp','oo','el'])(input2.tw(0.01))
+ ParamFun(fun_test, parameters_and_constants=["pp", "oo", "el"])(
+ input2.tw(0.01)
+ )
NeuObj.clearNames()
- out = ParamFun(fun_test,parameters_and_constants=['pp',Constant('oo',values=[[2.0]]),Constant('ll',sw=1,values=[[7.0]])])(input2.tw(0.01))
- self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim)
- self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters'])
- self.assertEqual({'oo': {'dim': [1,1],'values':[[2.0]]}, 'll': {'dim': 1,'sw': 1,'values':[[7.0]]}}, out.json['Constants'])
+ out = ParamFun(
+ fun_test,
+ parameters_and_constants=[
+ "pp",
+ Constant("oo", values=[[2.0]]),
+ Constant("ll", sw=1, values=[[7.0]]),
+ ],
+ )(input2.tw(0.01))
+ self.assertEqual({"dim": 1, "tw": 0.01}, out.dim)
+ self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"])
+ self.assertEqual(
+ {
+ "oo": {"dim": [1, 1], "values": [[2.0]]},
+ "ll": {"dim": 1, "sw": 1, "values": [[7.0]]},
+ },
+ out.json["Constants"],
+ )
NeuObj.clearNames()
- pp = Parameter('pp')
- ll = Constant('ll', values=[[1,2,3],[1,2,3]])
- oo = Constant('oo', values=[1,2,3])
- out = ParamFun(fun_test,parameters_and_constants=[pp,ll,oo])(input2.tw(0.01))
- self.assertEqual({'dim': 3, 'sw': 2}, out.dim)
- self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters'])
- self.assertEqual({'oo': {'dim': 3, 'values': [1,2,3]}, 'll': {'dim': [2,3], 'values':[[1,2,3],[1,2,3]]}}, out.json['Constants'])
-
- out = ParamFun(fun_test,parameters_and_constants={'z':pp,'y':ll,'k':oo})(input2.tw(0.01))
- self.assertEqual({'dim': 3, 'sw': 2}, out.dim)
- self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters'])
- self.assertEqual(['ll', 'pp', 'oo'],out.json['Functions']['FParamFun4']['params_and_consts'])
- self.assertEqual({'oo': {'dim': 3, 'values': [1,2,3]}, 'll': {'dim': [2,3], 'values':[[1,2,3],[1,2,3]]}}, out.json['Constants'])
+ pp = Parameter("pp")
+ ll = Constant("ll", values=[[1, 2, 3], [1, 2, 3]])
+ oo = Constant("oo", values=[1, 2, 3])
+ out = ParamFun(fun_test, parameters_and_constants=[pp, ll, oo])(input2.tw(0.01))
+ self.assertEqual({"dim": 3, "sw": 2}, out.dim)
+ self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"])
+ self.assertEqual(
+ {
+ "oo": {"dim": 3, "values": [1, 2, 3]},
+ "ll": {"dim": [2, 3], "values": [[1, 2, 3], [1, 2, 3]]},
+ },
+ out.json["Constants"],
+ )
+
+ out = ParamFun(fun_test, parameters_and_constants={"z": pp, "y": ll, "k": oo})(
+ input2.tw(0.01)
+ )
+ self.assertEqual({"dim": 3, "sw": 2}, out.dim)
+ self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"])
+ self.assertEqual(
+ ["ll", "pp", "oo"], out.json["Functions"]["FParamFun4"]["params_and_consts"]
+ )
+ self.assertEqual(
+ {
+ "oo": {"dim": 3, "values": [1, 2, 3]},
+ "ll": {"dim": [2, 3], "values": [[1, 2, 3], [1, 2, 3]]},
+ },
+ out.json["Constants"],
+ )
NeuObj.clearNames()
Stream.resetCount()
- pp = Parameter('pp')
- ll = Constant('ll', values=[1,2,3])
- oo = Constant('oo', tw=0.01, values=[[1]])
- out = ParamFun(fun_test)(input2.tw(0.01),ll,oo,pp)
- self.assertEqual({'dim': 3, 'tw': 0.01}, out.dim)
- self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters'])
- self.assertEqual({'oo': {'dim': 1, 'tw':0.01, 'values': [[1]]}, 'll': {'dim': 3, 'values': [1,2,3]}}, out.json['Constants'])
- self.assertEqual(['TimePart1', 'll', 'oo', 'pp'], out.json['Relations']['ParamFun2'][1])
+ pp = Parameter("pp")
+ ll = Constant("ll", values=[1, 2, 3])
+ oo = Constant("oo", tw=0.01, values=[[1]])
+ out = ParamFun(fun_test)(input2.tw(0.01), ll, oo, pp)
+ self.assertEqual({"dim": 3, "tw": 0.01}, out.dim)
+ self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"])
+ self.assertEqual(
+ {
+ "oo": {"dim": 1, "tw": 0.01, "values": [[1]]},
+ "ll": {"dim": 3, "values": [1, 2, 3]},
+ },
+ out.json["Constants"],
+ )
+ self.assertEqual(
+ ["TimePart1", "ll", "oo", "pp"], out.json["Relations"]["ParamFun2"][1]
+ )
def test_check_multiple_streams_compatibility_paramfun(self):
NeuObj.clearNames()
log.setAllLevel(logging.WARNING)
- x = Input('x')
- F = Input('F')
+ x = Input("x")
+ F = Input("F")
def myFun(p1, p2, k1, k2):
import torch
+
return k1 * torch.sin(p1) + k2 * torch.cos(p2)
- K1 = Parameter('k1', dimensions=1, sw=1, values=[[2.0]])
- K2 = Parameter('k2', dimensions=1, sw=1, values=[[3.0]])
- parfun = ParamFun(myFun, parameters_and_constants=[K1,K2])
+ K1 = Parameter("k1", dimensions=1, sw=1, values=[[2.0]])
+ K2 = Parameter("k2", dimensions=1, sw=1, values=[[3.0]])
+ parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])
rel1 = parfun(x.last(), F.last())
- rel2 = parfun(Tanh(F.sw(2)+F.sw([-2,-0])+F.sw([-3,-1])+F.sw([-4,-2])), Tanh(F.sw([0,2])))
- rel3 = parfun(Tanh(F.sw([-2,1])))
- rel4 = parfun(Tanh(F.sw([-2,1])), K1)
+ rel2 = parfun(
+ Tanh(F.sw(2) + F.sw([-2, -0]) + F.sw([-3, -1]) + F.sw([-4, -2])),
+ Tanh(F.sw([0, 2])),
+ )
+ rel3 = parfun(Tanh(F.sw([-2, 1])))
+ rel4 = parfun(Tanh(F.sw([-2, 1])), K1)
rel5 = parfun(K1, Tanh(F.sw(1)))
with self.assertRaises(TypeError):
parfun(Fir(3)(parfun(x.tw(0.4), x.tw(0.4))))
- out1 = Output('out1', rel1)
- out2 = Output('out2', rel2)
- out3 = Output('out3', rel3)
- out4 = Output('out4', rel4)
- out5 = Output('out5', rel5)
+ out1 = Output("out1", rel1)
+ out2 = Output("out2", rel2)
+ out3 = Output("out3", rel3)
+ out4 = Output("out4", rel4)
+ out5 = Output("out5", rel5)
# m = MPLVisualizer(5)
# m.showFunctions(list(example.json['Functions'].keys()), xlim=[[-5, 5], [-1, 1]])
exampleA = Modely(visualizer=None, seed=2)
with self.assertRaises(TypeError):
- exampleA.addModel('model', [out1, out2, out3])
- exampleA.addModel('model_A', [out1, out2])
+ exampleA.addModel("model", [out1, out2, out3])
+ exampleA.addModel("model_A", [out1, out2])
with self.assertRaises(TypeError):
- exampleA.addModel('model_B', [out3])
- exampleA.addModel('model_A2', [out1, out2, out4, out5])
+ exampleA.addModel("model_B", [out3])
+ exampleA.addModel("model_A2", [out1, out2, out4, out5])
exampleA.neuralizeModel(0.25)
exampleB = Modely(visualizer=None, seed=2)
- exampleB.addModel('model_B', [out3])
+ exampleB.addModel("model_B", [out3])
exampleB.neuralizeModel(1)
- resultsA = exampleA({'x': [1, 3, 3]})
- self.TestAlmostEqual([4.682941913604736, 3.2822399139404297, 3.2822399139404297], resultsA['out1'])
- self.TestAlmostEqual([[3.0, 3.0],[3.0 , 3.0],[3.0 , 3.0]], resultsA['out2'])
- self.TestAlmostEqual([[-1.2484405040740967, -1.2484405040740967, -1.2484405040740967],
- [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967],
- [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967]], resultsA['out4'])
- self.TestAlmostEqual([4.818594932556152, 4.818594932556152, 4.818594932556152], resultsA['out5'])
-
- resultsB = exampleB({'F': [1, 3, 4]})
- self.TestAlmostEqual([[3.831000328063965, 4.128425598144531, 4.133065223693848]], resultsB['out3'])
+ resultsA = exampleA({"x": [1, 3, 3]})
+ self.TestAlmostEqual(
+ [4.682941913604736, 3.2822399139404297, 3.2822399139404297],
+ resultsA["out1"],
+ )
+ self.TestAlmostEqual([[3.0, 3.0], [3.0, 3.0], [3.0, 3.0]], resultsA["out2"])
+ self.TestAlmostEqual(
+ [
+ [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967],
+ [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967],
+ [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967],
+ ],
+ resultsA["out4"],
+ )
+ self.TestAlmostEqual(
+ [4.818594932556152, 4.818594932556152, 4.818594932556152], resultsA["out5"]
+ )
+
+ resultsB = exampleB({"F": [1, 3, 4]})
+ self.TestAlmostEqual(
+ [[3.831000328063965, 4.128425598144531, 4.133065223693848]],
+ resultsB["out3"],
+ )
log.setAllLevel(logging.CRITICAL)
def test_check_multiple_streams_compatibility_linear(self):
NeuObj.clearNames()
log.setAllLevel(logging.WARNING)
- x = Input('x',dimensions=3)
- f = Input('f')
+ x = Input("x", dimensions=3)
+ f = Input("f")
lin = Linear()
l1out = lin(x.last()) + Fir(lin(x.tw(2.0))) + Fir(lin(x.sw(3)))
l2out = lin(f.last()) + Fir(lin(f.tw(2.0))) + Fir(lin(f.sw(3)))
- out1 = Output('out1', l1out)
- out2 = Output('out2', l2out)
+ out1 = Output("out1", l1out)
+ out2 = Output("out2", l2out)
exampleA = Modely(visualizer=None, seed=2)
with self.assertRaises(TypeError):
- exampleA.addModel('model', [out1, out2])
- exampleA.addModel('model_A', [out1])
+ exampleA.addModel("model", [out1, out2])
+ exampleA.addModel("model_A", [out1])
with self.assertRaises(TypeError):
- exampleA.addModel('model_B', [out2])
+ exampleA.addModel("model_B", [out2])
exampleA.neuralizeModel(1)
exampleB = Modely(visualizer=None, seed=2)
- exampleB.addModel('model_B', [out2])
+ exampleB.addModel("model_B", [out2])
exampleB.neuralizeModel(1)
- resultsA = exampleA({'x': [[1, 3, 3], [1, 2, 1], [2, 3, 4]]})
- self.TestAlmostEqual([12.507442474365234], resultsA['out1'])
+ resultsA = exampleA({"x": [[1, 3, 3], [1, 2, 1], [2, 3, 4]]})
+ self.TestAlmostEqual([12.507442474365234], resultsA["out1"])
- resultsB = exampleB({'f': [1, 3, 3, 1, 2, 1]})
- self.TestAlmostEqual([6.585615158081055, 4.480303764343262, 4.106618881225586, 3.18161678314209], resultsB['out2'])
+ resultsB = exampleB({"f": [1, 3, 3, 1, 2, 1]})
+ self.TestAlmostEqual(
+ [6.585615158081055, 4.480303764343262, 4.106618881225586, 3.18161678314209],
+ resultsB["out2"],
+ )
log.setAllLevel(logging.CRITICAL)
def test_check_multiple_streams_compatibility_fir(self):
NeuObj.clearNames()
log.setAllLevel(logging.WARNING)
- x = Input('x')
+ x = Input("x")
fir = Fir()
with self.assertRaises(TypeError):
fir(x.last()) + fir(x.tw(2.0)) + fir(x.sw(3))
- out1 = Output('out1', fir(x.last()))
- out2 = Output('out2', fir(x.tw(2.0)))
+ out1 = Output("out1", fir(x.last()))
+ out2 = Output("out2", fir(x.tw(2.0)))
exampleA = Modely(visualizer=None, seed=2)
with self.assertRaises(TypeError):
- exampleA.addModel('model', [out1, out2])
- exampleA.addModel('model_A', [out1])
+ exampleA.addModel("model", [out1, out2])
+ exampleA.addModel("model_A", [out1])
with self.assertRaises(TypeError):
- exampleA.addModel('model_B', [out2])
+ exampleA.addModel("model_B", [out2])
exampleA.neuralizeModel(1)
exampleB = Modely(visualizer=None, seed=2)
- exampleB.addModel('model_B', [out2])
+ exampleB.addModel("model_B", [out2])
exampleB.neuralizeModel(1)
- resultsA = exampleA({'x': [1, 3]})
- self.TestAlmostEqual([0.6146950721740723, 1.8440852165222168], resultsA['out1'])
+ resultsA = exampleA({"x": [1, 3]})
+ self.TestAlmostEqual([0.6146950721740723, 1.8440852165222168], resultsA["out1"])
- resultsB = exampleB({'x': [1, 4, 5]})
- self.TestAlmostEqual([2.138746500015259, 4.363844871520996], resultsB['out2'])
+ resultsB = exampleB({"x": [1, 4, 5]})
+ self.TestAlmostEqual([2.138746500015259, 4.363844871520996], resultsB["out2"])
log.setAllLevel(logging.CRITICAL)
def test_constant_and_parameter(self):
NeuObj.clearNames()
- c1 = Constant('c1', values=5.0) # {'dim': 1} -> shape (1,)
- c11 = Constant('c11', values=5.0) # {'dim': 1} -> shape (1,)
- c2 = Constant('c2', values=[5.0, 2.0, 1.0]) # {'dim': 3} -> shape (3,)
- c3 = Constant('c3', values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]]) # {'dim': (2,3)} -> shape (2,3)
- c4 = Constant('c4', sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3} -> shape (2,3)
- c5 = Constant('c5', tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3} -> shape (2,3)
- c6 = Constant('c6', sw=2, values=[[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], [[5.0, 2.0, 1.0], [3.0, 4.0,5.0]]]) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3)
- self.assertEqual({'dim': 1}, c1.dim)
- self.assertEqual({'dim': 1}, c11.dim)
- self.assertEqual({'dim': 3}, c2.dim)
- self.assertEqual({'dim': [2,3]}, c3.dim)
- self.assertEqual({'dim': 3, 'sw': 2}, c4.dim)
- self.assertEqual({'dim': 3, 'tw': 4}, c5.dim)
- self.assertEqual({'dim': [2,3], 'sw':2}, c6.dim)
-
- p1 = Parameter('p1', values=5.0) # {'dim': 1} -> shape (1,)
- p11 = Parameter('p11', values=[5.0]) # {'dim': 1} -> shape (1,)
- p111 = Parameter('p111', values=[[5.0]]) # {'dim': 1} -> shape (1,)
- p2 = Parameter('p2', values=[5.0, 2.0, 1.0]) # {'dim': 3}
- p22 = Parameter('p22', sw=1, values=[[2, 3, 4]]) # {'dim': 3, 'sw': 1} -> shape (1,3)
- p3 = Parameter('p3', values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0],[3.0, 4.0, 5.0],[3.0, 4.0, 5.0],[3.0, 4.0, 5.0]]) # {'dim': [5,3]}
- p4 = Parameter('p4', sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3}
- p5 = Parameter('p5', tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3}
- p6 = Parameter('p6', sw=2, values=[[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]]]) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3)
- self.assertEqual({'dim': 1}, p1.dim)
- self.assertEqual({'dim': 1}, p11.dim)
- self.assertEqual({'dim': [1,1]}, p111.dim)
- self.assertEqual({'dim': 3}, p2.dim)
- self.assertEqual({'dim': 3, 'sw':1}, p22.dim)
- self.assertEqual({'dim': [5,3]}, p3.dim)
- self.assertEqual({'dim': 3, 'sw': 2}, p4.dim)
- self.assertEqual({'dim': 3, 'tw': 4}, p5.dim)
- self.assertEqual({'dim': [2,3], 'sw':2}, p6.dim)
-
- x = Input('x',dimensions=5)
- out1 = Output('out1', ParamFun(myFunPar, parameters_and_constants=[p1])(x.last()))
- out11 = Output('out11', ParamFun(myFunPar, parameters_and_constants=[p11])(x.last()))
- out111 = Output('out111', ParamFun(myFunPar, parameters_and_constants=[p111])(x.last()))
- out2 = Output('out2', ParamFun(myFunPar, parameters_and_constants=[p2])(x.last()))
- out22 = Output('out22', ParamFun(myFunPar, parameters_and_constants=[p22])(x.last()))
- out3 = Output('out3', ParamFun(myFunPar, parameters_and_constants=[p3])(x.last()))
- out4 = Output('out4', ParamFun(myFunPar, parameters_and_constants=[p4])(x.last()))
- out5 = Output('out5', ParamFun(myFunPar, parameters_and_constants=[p5])(x.last()))
- out6 = Output('out6', ParamFun(myFunPar, parameters_and_constants=[p6])(x.last()))
+ c1 = Constant("c1", values=5.0) # {'dim': 1} -> shape (1,)
+ c11 = Constant("c11", values=5.0) # {'dim': 1} -> shape (1,)
+ c2 = Constant("c2", values=[5.0, 2.0, 1.0]) # {'dim': 3} -> shape (3,)
+ c3 = Constant(
+ "c3", values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]]
+ ) # {'dim': (2,3)} -> shape (2,3)
+ c4 = Constant(
+ "c4", sw=2, values=[[2, 3, 4], [1, 2, 3]]
+ ) # {'sw':2, 'dim': 3} -> shape (2,3)
+ c5 = Constant(
+ "c5", tw=4, values=[[2, 3, 4], [1, 2, 3]]
+ ) # {'tw':4, 'dim': 3} -> shape (2,3)
+ c6 = Constant(
+ "c6",
+ sw=2,
+ values=[
+ [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]],
+ [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]],
+ ],
+ ) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3)
+ self.assertEqual({"dim": 1}, c1.dim)
+ self.assertEqual({"dim": 1}, c11.dim)
+ self.assertEqual({"dim": 3}, c2.dim)
+ self.assertEqual({"dim": [2, 3]}, c3.dim)
+ self.assertEqual({"dim": 3, "sw": 2}, c4.dim)
+ self.assertEqual({"dim": 3, "tw": 4}, c5.dim)
+ self.assertEqual({"dim": [2, 3], "sw": 2}, c6.dim)
+
+ p1 = Parameter("p1", values=5.0) # {'dim': 1} -> shape (1,)
+ p11 = Parameter("p11", values=[5.0]) # {'dim': 1} -> shape (1,)
+ p111 = Parameter("p111", values=[[5.0]]) # {'dim': 1} -> shape (1,)
+ p2 = Parameter("p2", values=[5.0, 2.0, 1.0]) # {'dim': 3}
+ p22 = Parameter(
+ "p22", sw=1, values=[[2, 3, 4]]
+ ) # {'dim': 3, 'sw': 1} -> shape (1,3)
+ p3 = Parameter(
+ "p3",
+ values=[
+ [5.0, 2.0, 1.0],
+ [3.0, 4.0, 5.0],
+ [3.0, 4.0, 5.0],
+ [3.0, 4.0, 5.0],
+ [3.0, 4.0, 5.0],
+ ],
+ ) # {'dim': [5,3]}
+ p4 = Parameter("p4", sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3}
+ p5 = Parameter("p5", tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3}
+ p6 = Parameter(
+ "p6",
+ sw=2,
+ values=[
+ [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]],
+ [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]],
+ ],
+ ) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3)
+ self.assertEqual({"dim": 1}, p1.dim)
+ self.assertEqual({"dim": 1}, p11.dim)
+ self.assertEqual({"dim": [1, 1]}, p111.dim)
+ self.assertEqual({"dim": 3}, p2.dim)
+ self.assertEqual({"dim": 3, "sw": 1}, p22.dim)
+ self.assertEqual({"dim": [5, 3]}, p3.dim)
+ self.assertEqual({"dim": 3, "sw": 2}, p4.dim)
+ self.assertEqual({"dim": 3, "tw": 4}, p5.dim)
+ self.assertEqual({"dim": [2, 3], "sw": 2}, p6.dim)
+
+ x = Input("x", dimensions=5)
+ out1 = Output(
+ "out1", ParamFun(myFunPar, parameters_and_constants=[p1])(x.last())
+ )
+ out11 = Output(
+ "out11", ParamFun(myFunPar, parameters_and_constants=[p11])(x.last())
+ )
+ out111 = Output(
+ "out111", ParamFun(myFunPar, parameters_and_constants=[p111])(x.last())
+ )
+ out2 = Output(
+ "out2", ParamFun(myFunPar, parameters_and_constants=[p2])(x.last())
+ )
+ out22 = Output(
+ "out22", ParamFun(myFunPar, parameters_and_constants=[p22])(x.last())
+ )
+ out3 = Output(
+ "out3", ParamFun(myFunPar, parameters_and_constants=[p3])(x.last())
+ )
+ out4 = Output(
+ "out4", ParamFun(myFunPar, parameters_and_constants=[p4])(x.last())
+ )
+ out5 = Output(
+ "out5", ParamFun(myFunPar, parameters_and_constants=[p5])(x.last())
+ )
+ out6 = Output(
+ "out6", ParamFun(myFunPar, parameters_and_constants=[p6])(x.last())
+ )
nn = Modely(visualizer=None)
- nn.addModel('model', [out1,out11,out111,out2,out22,out3,out4,out5,out6])
+ nn.addModel("model", [out1, out11, out111, out2, out22, out3, out4, out5, out6])
nn.neuralizeModel(2.0)
- results = nn({'x':[[1,2,3,4,5]]})
- self.assertEqual(results['out1'][0], p1.dim['dim'])
- self.assertEqual(results['out11'][0], p11.dim['dim'])
- self.assertEqual(results['out111'][0][0], p111.dim['dim'])
- self.assertEqual(results['out2'][0], p2.dim['dim'])
- self.assertEqual(results['out22'][0][0][1], p22.dim['dim'])
- self.assertEqual(results['out22'][0][0][0], p22.dim['sw'])
- self.assertEqual(results['out3'][0][0], p3.dim['dim'])
- self.assertEqual(results['out3'][0][0][1], p4.dim['dim'])
- self.assertEqual(results['out4'][0][0][0], p4.dim['sw'])
- self.assertEqual(results['out5'][0][0][1], p5.dim['dim'])
- self.assertEqual(results['out5'][0][0][0], p5.dim['tw']/2.0)
- self.assertEqual(results['out6'][0][0][1:3], p6.dim['dim'])
- self.assertEqual(results['out6'][0][0][0], p6.dim['sw'])
-
- NeuObj.clearNames(['x','out1','out11','out2','out22','out3','out4','out5','out6'])
- x = Input('x')
+ results = nn({"x": [[1, 2, 3, 4, 5]]})
+ self.assertEqual(results["out1"][0], p1.dim["dim"])
+ self.assertEqual(results["out11"][0], p11.dim["dim"])
+ self.assertEqual(results["out111"][0][0], p111.dim["dim"])
+ self.assertEqual(results["out2"][0], p2.dim["dim"])
+ self.assertEqual(results["out22"][0][0][1], p22.dim["dim"])
+ self.assertEqual(results["out22"][0][0][0], p22.dim["sw"])
+ self.assertEqual(results["out3"][0][0], p3.dim["dim"])
+ self.assertEqual(results["out3"][0][0][1], p4.dim["dim"])
+ self.assertEqual(results["out4"][0][0][0], p4.dim["sw"])
+ self.assertEqual(results["out5"][0][0][1], p5.dim["dim"])
+ self.assertEqual(results["out5"][0][0][0], p5.dim["tw"] / 2.0)
+ self.assertEqual(results["out6"][0][0][1:3], p6.dim["dim"])
+ self.assertEqual(results["out6"][0][0][0], p6.dim["sw"])
+
+ NeuObj.clearNames(
+ ["x", "out1", "out11", "out2", "out22", "out3", "out4", "out5", "out6"]
+ )
+ x = Input("x")
with self.assertRaises(TypeError):
- Output('out1', Linear(W=p1, b=p1)(x.last()))
+ Output("out1", Linear(W=p1, b=p1)(x.last()))
with self.assertRaises(TypeError):
- Output('out1', Linear(W=p11, b=p11)(x.last()))
- out1 = Output('out1', Linear(W=p111, b=p1)(x.last()))
- out11 = Output('out11', Linear(W=p111, b=p11)(x.last()))
+ Output("out1", Linear(W=p11, b=p11)(x.last()))
+ out1 = Output("out1", Linear(W=p111, b=p1)(x.last()))
+ out11 = Output("out11", Linear(W=p111, b=p11)(x.last()))
with self.assertRaises(TypeError):
- Output('out111', Linear(W=p111, b=p111)(x.last()))
+ Output("out111", Linear(W=p111, b=p111)(x.last()))
- x5 = Input('x5',dimensions=5)
+ x5 = Input("x5", dimensions=5)
with self.assertRaises(TypeError):
- Output('out2', Linear(W=p1, b=p1)(x5.last()))
+ Output("out2", Linear(W=p1, b=p1)(x5.last()))
with self.assertRaises(TypeError):
- Output('out2', Linear(output_dimension=3, W=p1, b=p1)(x5.last()))
- out2 = Output('out2', Linear(W=p3, b=p2)(x5.last()))
- out3 = Output('out3', Linear(output_dimension=3, W=p3, b=p2)(x5.last()))
+ Output("out2", Linear(output_dimension=3, W=p1, b=p1)(x5.last()))
+ out2 = Output("out2", Linear(W=p3, b=p2)(x5.last()))
+ out3 = Output("out3", Linear(output_dimension=3, W=p3, b=p2)(x5.last()))
with self.assertRaises(TypeError):
- Output('out3', Linear(output_dimension=3, W=p3, b=p22)(x5.last()))
+ Output("out3", Linear(output_dimension=3, W=p3, b=p22)(x5.last()))
with self.assertRaises(TypeError):
- Output('out6', Linear(W=p6)(x.sw(2)))
+ Output("out6", Linear(W=p6)(x.sw(2)))
- x2 = Input('x2', dimensions=2)
+ x2 = Input("x2", dimensions=2)
with self.assertRaises(TypeError):
- Output('out4', Linear(output_dimension=3, W=p4, b=p2)(x2.last()))
+ Output("out4", Linear(output_dimension=3, W=p4, b=p2)(x2.last()))
- out4 = Output('out4', Fir(W=p4, b=p2)(x.sw(2)))
- out41 = Output('out41', Fir(output_dimension=3, W=p4, b=p2)(x.sw(2)))
+ out4 = Output("out4", Fir(W=p4, b=p2)(x.sw(2)))
+ out41 = Output("out41", Fir(output_dimension=3, W=p4, b=p2)(x.sw(2)))
with self.assertRaises(TypeError):
- Output('out41', Fir(output_dimension=3, W=p5, b=p2)(x.sw(2)))
+ Output("out41", Fir(output_dimension=3, W=p5, b=p2)(x.sw(2)))
with self.assertRaises(ValueError):
- Output('out41', Fir(output_dimension=3, W=p4, b=p2)(x.sw(4)))
+ Output("out41", Fir(output_dimension=3, W=p4, b=p2)(x.sw(4)))
with self.assertRaises(TypeError):
- Output('out41', Fir(output_dimension=3, W=p4, b=p4)(x.sw(2)))
- out5 = Output('out5', Fir(output_dimension=3, W=p5, b=p2)(x.tw(4)))
- out51 = Output('out51', Fir(W=p5, b=p2)(x.tw(4)))
+ Output("out41", Fir(output_dimension=3, W=p4, b=p4)(x.sw(2)))
+ out5 = Output("out5", Fir(output_dimension=3, W=p5, b=p2)(x.tw(4)))
+ out51 = Output("out51", Fir(W=p5, b=p2)(x.tw(4)))
with self.assertRaises(ValueError):
- Output('out6', Fir(output_dimension=3, W=p6)(x.sw(2)))
+ Output("out6", Fir(output_dimension=3, W=p6)(x.sw(2)))
with self.assertRaises(TypeError):
- Output('out6', Fir(W=p6)(x.sw(2)))
+ Output("out6", Fir(W=p6)(x.sw(2)))
nn = Modely(visualizer=None)
- nn.addModel('model', [out1, out11, out111, out2, out22, out3, out4, out41, out5, out51, out6])
+ nn.addModel(
+ "model",
+ [out1, out11, out111, out2, out22, out3, out4, out41, out5, out51, out6],
+ )
nn.neuralizeModel(2.0)
- results = nn({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'x5': [[1, 2, 3, 4, 5]]})
- self.assertEqual((1,),np.array(results['out1']).shape)
- self.assertEqual((1,),np.array(results['out11']).shape)
- self.assertEqual((1, 1, 3),np.array(results['out2']).shape)
- self.assertEqual((1, 1, 3), np.array(results['out3']).shape)
- self.assertEqual((1, 1, 3), np.array(results['out4']).shape)
- self.assertEqual((1, 1, 3), np.array(results['out41']).shape)
- self.assertEqual((1, 1, 3), np.array(results['out5']).shape)
- self.assertEqual((1, 1, 3), np.array(results['out51']).shape)
+ results = nn({"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "x5": [[1, 2, 3, 4, 5]]})
+ self.assertEqual((1,), np.array(results["out1"]).shape)
+ self.assertEqual((1,), np.array(results["out11"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out2"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out3"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out4"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out41"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out5"]).shape)
+ self.assertEqual((1, 1, 3), np.array(results["out51"]).shape)
def test_multi_model_json_and_subjson(self):
Stream.resetCount()
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
+
+ c1 = Constant("c1", values=5.0)
+ c3 = Constant("c3", values=5.0)
- c1 = Constant('c1', values=5.0)
- c3 = Constant('c3', values=5.0)
+ rel2 = Linear(W=Parameter("W2", values=[[2.0]]), b=False)(y.last())
+ rel4 = Linear(W=Parameter("W4", values=[[4.0]]), b=False)(y.last())
- rel2 = Linear(W=Parameter('W2', values=[[2.0]]), b=False)(y.last())
- rel4 = Linear(W=Parameter('W4', values=[[4.0]]), b=False)(y.last())
-
def fun2(x, a):
return x * a
-
+
def fun4(x, b):
return x + b
- out1 = Output('out1', c1 + Linear(W=Parameter('W1', values=[[1.0]]), b=False)(x.last()))
- out2 = Output('out2', rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last()))
- out3 = Output('out3', c3 + Linear(W=Parameter('W3', values=[[3.0]]), b=False)(x.last()))
- out4 = Output('out4', rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last()))
+ out1 = Output(
+ "out1", c1 + Linear(W=Parameter("W1", values=[[1.0]]), b=False)(x.last())
+ )
+ out2 = Output(
+ "out2", rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last())
+ )
+ out3 = Output(
+ "out3", c3 + Linear(W=Parameter("W3", values=[[3.0]]), b=False)(x.last())
+ )
+ out4 = Output(
+ "out4", rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last())
+ )
nn = Modely(visualizer=None)
- nn.addModel('model_A', [out1, out2])
- nn.addModel('model_B', [out3, out4])
- nn.addClosedLoop(rel2,y)
-
- subjson_A = subjson_from_model(nn.json, 'model_A')
- subjson_B = subjson_from_model(nn.json, 'model_B')
- self.assertEqual(subjson_A['Constants']['c1'],nn.json['Constants']['c1'])
- self.assertEqual(subjson_B['Constants']['c3'],nn.json['Constants']['c3'])
- self.assertEqual(subjson_A['Functions']['FParamFun11'], nn.json['Functions']['FParamFun11'])
- self.assertEqual(subjson_B['Functions']['FParamFun16'], nn.json['Functions']['FParamFun16'])
- self.assertEqual(subjson_A['Inputs']['x'], nn.json['Inputs']['x'])
- self.assertEqual(subjson_B['Inputs']['x'], nn.json['Inputs']['x'])
- self.assertEqual(subjson_A['Models'], 'model_A')
- self.assertEqual(subjson_B['Models'], 'model_B')
- self.assertEqual(sorted(list(subjson_A['Relations'].keys())), sorted(nn.json['Models']['model_A']['Relations']))
- self.assertEqual(sorted(list(subjson_B['Relations'].keys())), sorted(nn.json['Models']['model_B']['Relations']))
- self.assertEqual(sorted(list(subjson_A['Parameters'].keys())), sorted(['W2', 'W1']))
- self.assertEqual(sorted(list(subjson_B['Parameters'].keys())), sorted(['W3', 'W4']))
- self.assertEqual(sorted(list(subjson_A['Outputs'].keys())), sorted(['out1', 'out2']))
- self.assertEqual(sorted(list(subjson_B['Outputs'].keys())), sorted(['out3', 'out4']))
- yval = copy.deepcopy(nn.json['Inputs']['y'])
- del yval['closedLoop']
- del yval['local']
- self.assertEqual(subjson_A['Inputs']['y'], nn.json['Inputs']['y'])
- self.assertEqual(subjson_B['Inputs']['y'], yval)
+ nn.addModel("model_A", [out1, out2])
+ nn.addModel("model_B", [out3, out4])
+ nn.addClosedLoop(rel2, y)
+
+ subjson_A = subjson_from_model(nn.json, "model_A")
+ subjson_B = subjson_from_model(nn.json, "model_B")
+ self.assertEqual(subjson_A["Constants"]["c1"], nn.json["Constants"]["c1"])
+ self.assertEqual(subjson_B["Constants"]["c3"], nn.json["Constants"]["c3"])
+ self.assertEqual(
+ subjson_A["Functions"]["FParamFun11"], nn.json["Functions"]["FParamFun11"]
+ )
+ self.assertEqual(
+ subjson_B["Functions"]["FParamFun16"], nn.json["Functions"]["FParamFun16"]
+ )
+ self.assertEqual(subjson_A["Inputs"]["x"], nn.json["Inputs"]["x"])
+ self.assertEqual(subjson_B["Inputs"]["x"], nn.json["Inputs"]["x"])
+ self.assertEqual(subjson_A["Models"], "model_A")
+ self.assertEqual(subjson_B["Models"], "model_B")
+ self.assertEqual(
+ sorted(list(subjson_A["Relations"].keys())),
+ sorted(nn.json["Models"]["model_A"]["Relations"]),
+ )
+ self.assertEqual(
+ sorted(list(subjson_B["Relations"].keys())),
+ sorted(nn.json["Models"]["model_B"]["Relations"]),
+ )
+ self.assertEqual(
+ sorted(list(subjson_A["Parameters"].keys())), sorted(["W2", "W1"])
+ )
+ self.assertEqual(
+ sorted(list(subjson_B["Parameters"].keys())), sorted(["W3", "W4"])
+ )
+ self.assertEqual(
+ sorted(list(subjson_A["Outputs"].keys())), sorted(["out1", "out2"])
+ )
+ self.assertEqual(
+ sorted(list(subjson_B["Outputs"].keys())), sorted(["out3", "out4"])
+ )
+ yval = copy.deepcopy(nn.json["Inputs"]["y"])
+ del yval["closedLoop"]
+ del yval["local"]
+ self.assertEqual(subjson_A["Inputs"]["y"], nn.json["Inputs"]["y"])
+ self.assertEqual(subjson_B["Inputs"]["y"], yval)
aa = Modely(visualizer=None)
- aa.addModel('model_A', [out1, out2])
+ aa.addModel("model_A", [out1, out2])
aa.addClosedLoop(rel2, y)
self.assertEqual(subjson_A, aa.json)
bb = Modely(visualizer=None)
- bb.addModel('model_B', [out3, out4])
+ bb.addModel("model_B", [out3, out4])
self.assertEqual(subjson_B, bb.json)
def test_add_remove_models(self):
Stream.resetCount()
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
- c1 = Constant('c1', values=5.0)
- c3 = Constant('c3', values=5.0)
+ c1 = Constant("c1", values=5.0)
+ c3 = Constant("c3", values=5.0)
- rel2 = Linear(W=Parameter('W2', values=[[2.0]]), b=False)(y.last())
- rel4 = Linear(W=Parameter('W4', values=[[4.0]]), b=False)(y.last())
+ rel2 = Linear(W=Parameter("W2", values=[[2.0]]), b=False)(y.last())
+ rel4 = Linear(W=Parameter("W4", values=[[4.0]]), b=False)(y.last())
def fun2(x, a):
return x * a
@@ -703,39 +1025,47 @@ def fun2(x, a):
def fun4(x, b):
return x + b
- out1 = Output('out1', c1 + Linear(W=Parameter('W1', values=[[1.0]]), b=False)(x.last()))
- out2 = Output('out2', rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last()))
- out3 = Output('out3', c3 + Linear(W=Parameter('W3', values=[[3.0]]), b=False)(x.last()))
- out4 = Output('out4', rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last()))
+ out1 = Output(
+ "out1", c1 + Linear(W=Parameter("W1", values=[[1.0]]), b=False)(x.last())
+ )
+ out2 = Output(
+ "out2", rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last())
+ )
+ out3 = Output(
+ "out3", c3 + Linear(W=Parameter("W3", values=[[3.0]]), b=False)(x.last())
+ )
+ out4 = Output(
+ "out4", rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last())
+ )
nn = Modely(visualizer=None)
- nn.addModel('model_A', [out1, out2])
+ nn.addModel("model_A", [out1, out2])
model_A_json_1 = nn.json
- nn.addModel('model_B', [out3, out4])
- nn.removeModel('model_B')
+ nn.addModel("model_B", [out3, out4])
+ nn.removeModel("model_B")
model_A_json_2 = nn.json
self.assertEqual(model_A_json_1, model_A_json_2)
def test_add_remove_minimize(self):
clearNames()
- input1 = Input('in1').last()
- input2 = Input('in2').last()
- input3 = Input('in3').last()
- output1 = Output('out1', input1)
- output2 = Output('out2', input1)
- output3 = Output('out3', input1)
+ input1 = Input("in1").last()
+ input2 = Input("in2").last()
+ input3 = Input("in3").last()
+ output1 = Output("out1", input1)
+ output2 = Output("out2", input1)
+ output3 = Output("out3", input1)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1, output2, output3])
- test.addMinimize('error1', input1, output1)
+ test.addModel("model", [output1, output2, output3])
+ test.addMinimize("error1", input1, output1)
test_json_1 = test.json
- test.addMinimize('error2', input2, output2)
+ test.addMinimize("error2", input2, output2)
test_json_2 = test.json
- test.addMinimize('error3', input3, output3)
+ test.addMinimize("error3", input3, output3)
test_json_3 = test.json
- test.removeMinimize('error3')
+ test.removeMinimize("error3")
self.assertEqual(test_json_2, test.json)
- test.addMinimize('error3', input3, output3)
+ test.addMinimize("error3", input3, output3)
self.assertEqual(test_json_3, test.json)
- test.removeMinimize(['error3','error2'])
- self.assertEqual(test_json_1, test.json)
\ No newline at end of file
+ test.removeMinimize(["error3", "error2"])
+ self.assertEqual(test_json_1, test.json)
diff --git a/tests/test_losses.py b/tests/test_losses.py
index c7860722..34da3ebc 100644
--- a/tests/test_losses.py
+++ b/tests/test_losses.py
@@ -1,4 +1,6 @@
-import unittest, os, sys
+import unittest
+import os
+import sys
import numpy as np
import torch
@@ -15,11 +17,17 @@
# Test the looses comparison between closed loop and states
# Test the looses comparison between connect and states
-data_folder = os.path.join(os.path.dirname(__file__), '_data/')
+data_folder = os.path.join(os.path.dirname(__file__), "_data/")
+
class ModelyTrainingTest(unittest.TestCase):
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
@@ -29,162 +37,452 @@ def TestAlmostEqual(self, data1, data2, precision=4):
def test_losses_compare(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out', Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out", Fir(W=a)(input1.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]}
- test.loadData(name='dataset', source=dataset)
- test.trainAndAnalyze(optimizer='SGD', num_of_epochs=5, lr=0.5, splits=[70,20,10])
- self.TestAlmostEqual( [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual( [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error1']['train'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error2']['train'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'],
- (test._training['error1']['val'][-1] + test._training['error2']['val'][-1]) / 2.0)
- self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['total']['mean_error'], (test._training['error1']['val'][-1]+test._training['error2']['val'][-1])/2.0)
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
+ }
+ test.loadData(name="dataset", source=dataset)
+ test.trainAndAnalyze(
+ optimizer="SGD", num_of_epochs=5, lr=0.5, splits=[70, 20, 10]
+ )
+ self.TestAlmostEqual(
+ [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error2"]["train"]
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"]
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["total"]["mean_error"],
+ (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1])
+ / 2.0,
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["total"]["mean_error"],
+ (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1])
+ / 2.0,
+ )
test.neuralizeModel(clear_model=True)
- test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2)
- self.TestAlmostEqual( [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual( [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([6.0, 11.0, 6.0, 11.0, 6.0], test._training['error1']['train'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val'])
- self.TestAlmostEqual([11.0, 6.0, 11.0, 6.0, 11.0], test._training['error2']['train'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1])
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ splits=[60, 20, 20],
+ num_of_epochs=5,
+ lr=0.5,
+ train_batch_size=2,
+ )
+ self.TestAlmostEqual(
+ [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [6.0, 11.0, 6.0, 11.0, 6.0], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [11.0, 6.0, 11.0, 6.0, 11.0], test._training["error2"]["train"]
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"]
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
def test_losses_compare_closed_loop_state(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[1]])
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[1]])
relation = Fir(W=a)(input1.last())
relation.closedLoop(input1)
- output1 = Output('out', relation)
+ output1 = Output("out", relation)
- test = Modely(visualizer=None,seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]}
- test.loadData(name='dataset', source=dataset)
- test.trainAndAnalyze(optimizer='SGD', num_of_epochs=5, lr=0.5, shuffle_data=False, splits=[70,20,10])
- self.TestAlmostEqual([[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual([[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error1']['train'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error2']['train'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'],
- (test._training['error1']['val'][-1] + test._training['error2']['val'][-1]) / 2.0)
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'], (test._training['error1']['val'][-1]+test._training['error2']['val'][-1])/2.0)
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
+ }
+ test.loadData(name="dataset", source=dataset)
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ num_of_epochs=5,
+ lr=0.5,
+ shuffle_data=False,
+ splits=[70, 20, 10],
+ )
+ self.TestAlmostEqual(
+ [[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error2"]["train"]
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"]
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["total"]["mean_error"],
+ (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1])
+ / 2.0,
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["total"]["mean_error"],
+ (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1])
+ / 2.0,
+ )
test.neuralizeModel(clear_model=True)
- test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2)
- self.TestAlmostEqual([[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual([[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([6.0, 11.0, 6.0, 11.0, 6.0], test._training['error1']['train'])
- self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val'])
- self.TestAlmostEqual([11.0, 6.0, 11.0, 6.0, 11.0], test._training['error2']['train'])
- self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1])
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ splits=[60, 20, 20],
+ num_of_epochs=5,
+ lr=0.5,
+ train_batch_size=2,
+ )
+ self.TestAlmostEqual(
+ [[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [6.0, 11.0, 6.0, 11.0, 6.0], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [11.0, 6.0, 11.0, 6.0, 11.0], test._training["error2"]["train"]
+ )
+ self.TestAlmostEqual(
+ [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"]
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_test"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
test.neuralizeModel(clear_model=True)
with self.assertRaises(ValueError):
- test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2, prediction_samples=4)
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ splits=[60, 20, 20],
+ num_of_epochs=5,
+ lr=0.5,
+ train_batch_size=2,
+ prediction_samples=4,
+ )
test.neuralizeModel(clear_model=True)
- test.trainAndAnalyze(optimizer='SGD', splits=[50, 50, 0], num_of_epochs=5, lr=0.001, train_batch_size=2, prediction_samples=3)
-
- self.TestAlmostEqual([[[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[[1.1285]], [[1.1285]]], [[[1.2735]], [[1.2735]]], [[[1.4371]], [[1.4371]]], [[[1.6217]], [[1.6217]]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual([[[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training['error1']['train'])
- self.TestAlmostEqual([0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training['error1']['val'])
- self.TestAlmostEqual([16.0, 15.4923, 14.9602, 14.4059, 13.8328], test._training['error2']['train'])
- self.TestAlmostEqual([15.4923, 14.9602, 14.4059, 13.8328, 13.2457], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ splits=[50, 50, 0],
+ num_of_epochs=5,
+ lr=0.001,
+ train_batch_size=2,
+ prediction_samples=3,
+ )
+
+ self.TestAlmostEqual(
+ [
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ ],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [
+ [[[1.1285]], [[1.1285]]],
+ [[[1.2735]], [[1.2735]]],
+ [[[1.4371]], [[1.4371]]],
+ [[[1.6217]], [[1.6217]]],
+ ],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ ],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 15.4923, 14.9602, 14.4059, 13.8328],
+ test._training["error2"]["train"],
+ )
+ self.TestAlmostEqual(
+ [15.4923, 14.9602, 14.4059, 13.8328, 13.2457],
+ test._training["error2"]["val"],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
def test_losses_compare_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out', Fir(W=a)(input1.last()))
-
- test = Modely(visualizer=None,seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out", Fir(W=a)(input1.last()))
+
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]}
- test.loadData(name='dataset', source=dataset)
- test.trainAndAnalyze(optimizer='SGD', splits=[50, 50, 0], num_of_epochs=5, lr=0.001, train_batch_size=2, prediction_samples=3, closed_loop={'in1': 'out'})
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
+ }
+ test.loadData(name="dataset", source=dataset)
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ splits=[50, 50, 0],
+ num_of_epochs=5,
+ lr=0.001,
+ train_batch_size=2,
+ prediction_samples=3,
+ closed_loop={"in1": "out"},
+ )
- self.TestAlmostEqual([[[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A'])
- self.TestAlmostEqual([[[[1.1285]], [[1.1285]]], [[[1.2735]], [[1.2735]]], [[[1.4371]], [[1.4371]]], [[[1.6217]], [[1.6217]]]] ,test.prediction['dataset_train']['error1']['B'])
- self.TestAlmostEqual([[[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A'])
- self.TestAlmostEqual([1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training['error1']['train'])
- self.TestAlmostEqual([0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training['error1']['val'])
- self.TestAlmostEqual([16.0, 15.4923, 14.9602, 14.4059, 13.8328], test._training['error2']['train'])
- self.TestAlmostEqual([15.4923, 14.9602, 14.4059, 13.8328, 13.2457], test._training['error2']['val'])
- self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1])
- self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1])
+ self.TestAlmostEqual(
+ [
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ [[[2.0]], [[2.0]]],
+ ],
+ test.prediction["dataset_train"]["error1"]["A"],
+ )
+ self.TestAlmostEqual(
+ [
+ [[[1.1285]], [[1.1285]]],
+ [[[1.2735]], [[1.2735]]],
+ [[[1.4371]], [[1.4371]]],
+ [[[1.6217]], [[1.6217]]],
+ ],
+ test.prediction["dataset_train"]["error1"]["B"],
+ )
+ self.TestAlmostEqual(
+ [
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ [[[5.0]], [[5.0]]],
+ ],
+ test.prediction["dataset_train"]["error2"]["A"],
+ )
+ self.TestAlmostEqual(
+ [1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training["error1"]["train"]
+ )
+ self.TestAlmostEqual(
+ [0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training["error1"]["val"]
+ )
+ self.TestAlmostEqual(
+ [16.0, 15.4923, 14.9602, 14.4059, 13.8328],
+ test._training["error2"]["train"],
+ )
+ self.TestAlmostEqual(
+ [15.4923, 14.9602, 14.4059, 13.8328, 13.2457],
+ test._training["error2"]["val"],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error1"]["mse"],
+ test._training["error1"]["val"][-1],
+ )
+ self.TestAlmostEqual(
+ test.performance["dataset_val"]["error2"]["mse"],
+ test._training["error2"]["val"][-1],
+ )
def test_categorical_crossentropy(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2', dimensions=5)
-
- k = Parameter('k', values=[[0.1,0.1,0.1,0.1,0.6]])
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2", dimensions=5)
+
+ k = Parameter("k", values=[[0.1, 0.1, 0.1, 0.1, 0.6]])
linear = Linear(output_dimension=5, W=k, b=False)(input1.last())
- output = Output('out', linear)
+ output = Output("out", linear)
test = Modely(visualizer=None, seed=42, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error1', output, target1.last(), loss_function='cross_entropy')
- test.addMinimize('error2', output, target2.last(), loss_function='cross_entropy')
+ test.addModel("model", output)
+ test.addMinimize(
+ "error1", output, target1.last(), loss_function="cross_entropy"
+ )
+ test.addMinimize(
+ "error2", output, target2.last(), loss_function="cross_entropy"
+ )
test.neuralizeModel()
-
- dataset = {'in1': [1], 'out1': [4], 'out2':[[0.0,0.0,0.0,0.0,1.0]]}
- test.loadData(name='dataset', source=dataset)
- test.trainAndAnalyze(optimizer='SGD', train_dataset='dataset', train_batch_size=1, num_of_epochs=1, lr=0.0)
+
+ dataset = {"in1": [1], "out1": [4], "out2": [[0.0, 0.0, 0.0, 0.0, 1.0]]}
+ test.loadData(name="dataset", source=dataset)
+ test.trainAndAnalyze(
+ optimizer="SGD",
+ train_dataset="dataset",
+ train_batch_size=1,
+ num_of_epochs=1,
+ lr=0.0,
+ )
loss = torch.nn.CrossEntropyLoss()
- self.assertAlmostEqual(1.2314292192459106, loss(torch.tensor(test.prediction['dataset']['error1']['A']).squeeze(), torch.tensor(test.prediction['dataset']['error1']['B'], dtype=torch.long).squeeze()).item())
- self.assertAlmostEqual(1.2314292192459106, loss(torch.tensor(test.prediction['dataset']['error2']['A']).squeeze(), torch.tensor(test.prediction['dataset']['error2']['B'], dtype=torch.float32).squeeze()).item())
+ self.assertAlmostEqual(
+ 1.2314292192459106,
+ loss(
+ torch.tensor(test.prediction["dataset"]["error1"]["A"]).squeeze(),
+ torch.tensor(
+ test.prediction["dataset"]["error1"]["B"], dtype=torch.long
+ ).squeeze(),
+ ).item(),
+ )
+ self.assertAlmostEqual(
+ 1.2314292192459106,
+ loss(
+ torch.tensor(test.prediction["dataset"]["error2"]["A"]).squeeze(),
+ torch.tensor(
+ test.prediction["dataset"]["error2"]["B"], dtype=torch.float32
+ ).squeeze(),
+ ).item(),
+ )
diff --git a/tests/test_model_predict.py b/tests/test_model_predict.py
index 5ebf8f05..9b4bdc17 100644
--- a/tests/test_model_predict.py
+++ b/tests/test_model_predict.py
@@ -1,4 +1,7 @@
-import sys, os, torch, unittest
+import sys
+import os
+import torch
+import unittest
import numpy as np
from nnodely import *
@@ -17,25 +20,35 @@
# The second dimension indicates the output time dimension for each sample.
# The third is the size of the signal
+
def myfun(x, P):
- return x*P
+ return x * P
+
-def myfun2(a, b ,c):
+def myfun2(a, b, c):
import torch
+
return torch.sin(a + b) * c
+
def myfun3(a, b, p1, p2):
import torch
- at = torch.transpose(a[:, :, 0:2],1,2)
+
+ at = torch.transpose(a[:, :, 0:2], 1, 2)
bt = torch.transpose(b, 1, 2)
- return torch.matmul(p1,at+bt)+p2.t()
+ return torch.matmul(p1, at + bt) + p2.t()
+
class ModelyPredictTest(unittest.TestCase):
-
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
- self.assertEqual(len(data1),len(data2))
+ self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.TestAlmostEqual(pred, label, precision=precision)
else:
@@ -43,681 +56,942 @@ def TestAlmostEqual(self, data1, data2, precision=4):
def test_single_in(self):
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
+ in1 = Input("in1")
+ in2 = Input("in2")
out_fun = Fir(in1.tw(0.1)) + Fir(in2.last())
- out = Output('out', out_fun)
+ out = Output("out", out_fun)
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.01)
- results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]],'in2': [[5]]})
- self.assertEqual(1, len(results['out']))
- self.TestAlmostEqual([33.74938201904297], results['out'])
- results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]], 'in2': [[5], [7]]})
- self.assertEqual(2, len(results['out']))
- self.TestAlmostEqual([33.74938201904297, 40.309326171875], results['out'])
- results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]], 'in2': [[5], [7], [9]]})
- self.assertEqual(3, len(results['out']))
- self.TestAlmostEqual([33.74938201904297, 40.309326171875, 46.86927032470703], results['out'])
+ results = test(
+ {"in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]], "in2": [[5]]}
+ )
+ self.assertEqual(1, len(results["out"]))
+ self.TestAlmostEqual([33.74938201904297], results["out"])
+ results = test(
+ {
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]],
+ "in2": [[5], [7]],
+ }
+ )
+ self.assertEqual(2, len(results["out"]))
+ self.TestAlmostEqual([33.74938201904297, 40.309326171875], results["out"])
+ results = test(
+ {
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]],
+ "in2": [[5], [7], [9]],
+ }
+ )
+ self.assertEqual(3, len(results["out"]))
+ self.TestAlmostEqual(
+ [33.74938201904297, 40.309326171875, 46.86927032470703], results["out"]
+ )
def test_activation(self):
NeuObj.clearNames()
- in1 = Input('in1')
+ in1 = Input("in1")
out_fun = ELU(in1.last()) + Relu(in1.last()) + Tanh(in1.last())
- out = Output('out', out_fun)
+ out = Output("out", out_fun)
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel()
- results = test({'in1': [-1, -0.5, 0, 0.2, 2, 10]})
- self.assertEqual(6, len(results['out']))
- self.TestAlmostEqual([-1.3937146663665771,-0.8555865287780762,0,0.5973753333091736,4.964027404785156,21.0], results['out'])
+ results = test({"in1": [-1, -0.5, 0, 0.2, 2, 10]})
+ self.assertEqual(6, len(results["out"]))
+ self.TestAlmostEqual(
+ [
+ -1.3937146663665771,
+ -0.8555865287780762,
+ 0,
+ 0.5973753333091736,
+ 4.964027404785156,
+ 21.0,
+ ],
+ results["out"],
+ )
def test_single_in_window(self):
NeuObj.clearNames()
# Here there is more sample for each time step but the dimensions of the input is 1
- in1 = Input('in1')
+ in1 = Input("in1")
# Finestre nel tempo
- out1 = Output('x.tw(1)', in1.tw(1))
- out2 = Output('x.tw([-1,0])', in1.tw([-1, 0]))
- out3 = Output('x.tw([-3,0])', in1.tw([-3, 0]))
- out4 = Output('x.tw([1,3])', in1.tw([1, 3]))
- out5 = Output('x.tw([-1,3])', in1.tw([-1, 3]))
- out6 = Output('x.tw([0,1])', in1.tw([0, 1]))
- out7 = Output('x.tw([-3,-2])', in1.tw([-3, -2]))
+ out1 = Output("x.tw(1)", in1.tw(1))
+ out2 = Output("x.tw([-1,0])", in1.tw([-1, 0]))
+ out3 = Output("x.tw([-3,0])", in1.tw([-3, 0]))
+ out4 = Output("x.tw([1,3])", in1.tw([1, 3]))
+ out5 = Output("x.tw([-1,3])", in1.tw([-1, 3]))
+ out6 = Output("x.tw([0,1])", in1.tw([0, 1]))
+ out7 = Output("x.tw([-3,-2])", in1.tw([-3, -2]))
## TODO: adjust the z function
- # Finesatre nei samples
- out8 = Output('x.z(-1)', in1.z(-1))
- out9 = Output('x.z(0)', in1.z(0))
- out10 = Output('x.z(2)', in1.z(2))
- out11 = Output('x.sw([-1,0])', in1.sw([-1, 0]))
- out12 = Output('x.sw([1,2])', in1.sw([1, 2]))
- out13 = Output('x.sw([-3,1])', in1.sw([-3, 1]))
- out14 = Output('x.sw([-3,-2])', in1.sw([-3, -2]))
- out15 = Output('x.sw([0,1])', in1.sw([0, 1]))
+ # Finesatre nei samples
+ out8 = Output("x.z(-1)", in1.z(-1))
+ out9 = Output("x.z(0)", in1.z(0))
+ out10 = Output("x.z(2)", in1.z(2))
+ out11 = Output("x.sw([-1,0])", in1.sw([-1, 0]))
+ out12 = Output("x.sw([1,2])", in1.sw([1, 2]))
+ out13 = Output("x.sw([-3,1])", in1.sw([-3, 1]))
+ out14 = Output("x.sw([-3,-2])", in1.sw([-3, -2]))
+ out15 = Output("x.sw([0,1])", in1.sw([0, 1]))
test = Modely(visualizer=None, seed=1)
- #test.addModel('out',[out0,out1,out2,out3,out4,out5,out6,out7,out11,out12,out13,out14,out15])
- test.addModel('out',[out1, out2, out3, out4, out5, out6, out7, out8, out9, out10, out11, out12, out13, out14, out15])
+ # test.addModel('out',[out0,out1,out2,out3,out4,out5,out6,out7,out11,out12,out13,out14,out15])
+ test.addModel(
+ "out",
+ [
+ out1,
+ out2,
+ out3,
+ out4,
+ out5,
+ out6,
+ out7,
+ out8,
+ out9,
+ out10,
+ out11,
+ out12,
+ out13,
+ out14,
+ out15,
+ ],
+ )
test.neuralizeModel(1)
# Time -2,-1,0,1,2,3 # zero represent the last passed instant
- results = test({'in1': [[-2],[-1],[0],[1],[7],[3]]})
+ results = test({"in1": [[-2], [-1], [0], [1], [7], [3]]})
# Time window
- self.assertEqual((1,), np.array(results['x.tw(1)']).shape)
- self.TestAlmostEqual([0], results['x.tw(1)'])
- self.assertEqual((1,), np.array(results['x.tw([-1,0])']).shape)
- self.TestAlmostEqual([0], results['x.tw([-1,0])'])
- self.assertEqual((1, 3), np.array(results['x.tw([-3,0])']).shape)
- self.TestAlmostEqual([[-2, -1, 0]], results['x.tw([-3,0])'])
- self.assertEqual((1, 2), np.array(results['x.tw([1,3])']).shape)
- self.TestAlmostEqual([[7, 3]], results['x.tw([1,3])'])
- self.assertEqual((1, 4), np.array(results['x.tw([-1,3])']).shape)
- self.TestAlmostEqual([[0, 1, 7, 3]], results['x.tw([-1,3])'])
- self.assertEqual((1,), np.array(results['x.tw([0,1])']).shape)
- self.TestAlmostEqual([1], results['x.tw([0,1])'])
- self.assertEqual((1,), np.array(results['x.tw([-3,-2])']).shape)
- self.TestAlmostEqual([-2],results['x.tw([-3,-2])'])
+ self.assertEqual((1,), np.array(results["x.tw(1)"]).shape)
+ self.TestAlmostEqual([0], results["x.tw(1)"])
+ self.assertEqual((1,), np.array(results["x.tw([-1,0])"]).shape)
+ self.TestAlmostEqual([0], results["x.tw([-1,0])"])
+ self.assertEqual((1, 3), np.array(results["x.tw([-3,0])"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0]], results["x.tw([-3,0])"])
+ self.assertEqual((1, 2), np.array(results["x.tw([1,3])"]).shape)
+ self.TestAlmostEqual([[7, 3]], results["x.tw([1,3])"])
+ self.assertEqual((1, 4), np.array(results["x.tw([-1,3])"]).shape)
+ self.TestAlmostEqual([[0, 1, 7, 3]], results["x.tw([-1,3])"])
+ self.assertEqual((1,), np.array(results["x.tw([0,1])"]).shape)
+ self.TestAlmostEqual([1], results["x.tw([0,1])"])
+ self.assertEqual((1,), np.array(results["x.tw([-3,-2])"]).shape)
+ self.TestAlmostEqual([-2], results["x.tw([-3,-2])"])
# Sample window
- self.assertEqual((1,), np.array(results['x.z(-1)']).shape)
- self.TestAlmostEqual([1], results['x.z(-1)'])
- self.assertEqual((1,), np.array(results['x.z(0)']).shape)
- self.TestAlmostEqual([0], results['x.z(0)'])
- self.assertEqual((1,), np.array(results['x.z(2)']).shape)
- self.TestAlmostEqual([-2], results['x.z(2)'])
- self.assertEqual((1,), np.array(results['x.sw([-1,0])']).shape)
- self.TestAlmostEqual([0], results['x.sw([-1,0])'])
- self.assertEqual((1,), np.array(results['x.sw([1,2])']).shape)
- self.TestAlmostEqual([7], results['x.sw([1,2])'])
- self.assertEqual((1,4), np.array(results['x.sw([-3,1])']).shape)
- self.TestAlmostEqual([[-2,-1,0,1]], results['x.sw([-3,1])'])
- self.assertEqual((1,), np.array(results['x.sw([-3,-2])']).shape)
- self.TestAlmostEqual([-2], results['x.sw([-3,-2])'])
- self.assertEqual((1,), np.array(results['x.sw([0,1])']).shape)
- self.TestAlmostEqual([1],results['x.sw([0,1])'])
-
+ self.assertEqual((1,), np.array(results["x.z(-1)"]).shape)
+ self.TestAlmostEqual([1], results["x.z(-1)"])
+ self.assertEqual((1,), np.array(results["x.z(0)"]).shape)
+ self.TestAlmostEqual([0], results["x.z(0)"])
+ self.assertEqual((1,), np.array(results["x.z(2)"]).shape)
+ self.TestAlmostEqual([-2], results["x.z(2)"])
+ self.assertEqual((1,), np.array(results["x.sw([-1,0])"]).shape)
+ self.TestAlmostEqual([0], results["x.sw([-1,0])"])
+ self.assertEqual((1,), np.array(results["x.sw([1,2])"]).shape)
+ self.TestAlmostEqual([7], results["x.sw([1,2])"])
+ self.assertEqual((1, 4), np.array(results["x.sw([-3,1])"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0, 1]], results["x.sw([-3,1])"])
+ self.assertEqual((1,), np.array(results["x.sw([-3,-2])"]).shape)
+ self.TestAlmostEqual([-2], results["x.sw([-3,-2])"])
+ self.assertEqual((1,), np.array(results["x.sw([0,1])"]).shape)
+ self.TestAlmostEqual([1], results["x.sw([0,1])"])
+
def test_single_in_window_offset(self):
NeuObj.clearNames()
# Here there is more sample for each time step but the dimensions of the input is 1
- in1 = Input('in1')
+ in1 = Input("in1")
# Finestre nel tempo
- out1 = Output('x.tw(1)', in1.tw(1,offset=-1))
- out2 = Output('x.tw([-1,0])', in1.tw([-1, 0],offset=-1))
- out3 = Output('x.tw([1,3])', in1.tw([1, 3],offset=1))
- out4 = Output('x.tw([-3,-2])', in1.tw([-3, -2],offset=-3))
+ out1 = Output("x.tw(1)", in1.tw(1, offset=-1))
+ out2 = Output("x.tw([-1,0])", in1.tw([-1, 0], offset=-1))
+ out3 = Output("x.tw([1,3])", in1.tw([1, 3], offset=1))
+ out4 = Output("x.tw([-3,-2])", in1.tw([-3, -2], offset=-3))
# Finesatre nei samples
- out5 = Output('x.sw([-1,0])', in1.sw([-1, 0],offset=-1))
- out6 = Output('x.sw([-3,1])', in1.sw([-3, 1],offset=-3))
- out7 = Output('x.sw([0,1])', in1.sw([0, 1],offset=0))
- out8 = Output('x.sw([-3, 3])', in1.sw([-3, 3], offset=2))
- out9 = Output('x.sw([-3, 3])-2', in1.sw([-3, 3], offset=-1))
+ out5 = Output("x.sw([-1,0])", in1.sw([-1, 0], offset=-1))
+ out6 = Output("x.sw([-3,1])", in1.sw([-3, 1], offset=-3))
+ out7 = Output("x.sw([0,1])", in1.sw([0, 1], offset=0))
+ out8 = Output("x.sw([-3, 3])", in1.sw([-3, 3], offset=2))
+ out9 = Output("x.sw([-3, 3])-2", in1.sw([-3, 3], offset=-1))
- test = Modely(visualizer = None, seed = 1)
- test.addModel('out',[out1,out2,out3,out4,out5,out6,out7,out8,out9])
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", [out1, out2, out3, out4, out5, out6, out7, out8, out9])
test.neuralizeModel(1)
# Time -2,-1,0,1,2,3 # zero represent the last passed instant
- results = test({'in1': [[-2],[-1],[0],[1],[7],[3]]})
+ results = test({"in1": [[-2], [-1], [0], [1], [7], [3]]})
# Time window
- self.assertEqual((1,), np.array(results['x.tw(1)']).shape)
- self.TestAlmostEqual([0], results['x.tw(1)'])
- self.assertEqual((1,), np.array(results['x.tw([-1,0])']).shape)
- self.TestAlmostEqual([0], results['x.tw([-1,0])'])
- self.assertEqual((1,2), np.array(results['x.tw([1,3])']).shape)
- self.TestAlmostEqual([[0, -4]], results['x.tw([1,3])'])
- self.assertEqual((1,), np.array(results['x.tw([-3,-2])']).shape)
- self.TestAlmostEqual([0],results['x.tw([-3,-2])'])
+ self.assertEqual((1,), np.array(results["x.tw(1)"]).shape)
+ self.TestAlmostEqual([0], results["x.tw(1)"])
+ self.assertEqual((1,), np.array(results["x.tw([-1,0])"]).shape)
+ self.TestAlmostEqual([0], results["x.tw([-1,0])"])
+ self.assertEqual((1, 2), np.array(results["x.tw([1,3])"]).shape)
+ self.TestAlmostEqual([[0, -4]], results["x.tw([1,3])"])
+ self.assertEqual((1,), np.array(results["x.tw([-3,-2])"]).shape)
+ self.TestAlmostEqual([0], results["x.tw([-3,-2])"])
# # Sample window
- self.assertEqual((1,), np.array(results['x.sw([-1,0])']).shape)
- self.TestAlmostEqual([0], results['x.sw([-1,0])'])
- self.assertEqual((1,4), np.array(results['x.sw([-3,1])']).shape)
- self.TestAlmostEqual([[0,1,2,3]], results['x.sw([-3,1])'])
- self.assertEqual((1,), np.array(results['x.sw([0,1])']).shape)
- self.TestAlmostEqual([0],results['x.sw([0,1])'])
- self.assertEqual((1,6), np.array(results['x.sw([-3, 3])']).shape)
- self.TestAlmostEqual([[-5,-4,-3,-2,4,0]],results['x.sw([-3, 3])'])
- self.assertEqual((1,6), np.array(results['x.sw([-3, 3])-2']).shape)
- self.TestAlmostEqual([[-2,-1,0,1,7,3]],results['x.sw([-3, 3])-2'])
-
+ self.assertEqual((1,), np.array(results["x.sw([-1,0])"]).shape)
+ self.TestAlmostEqual([0], results["x.sw([-1,0])"])
+ self.assertEqual((1, 4), np.array(results["x.sw([-3,1])"]).shape)
+ self.TestAlmostEqual([[0, 1, 2, 3]], results["x.sw([-3,1])"])
+ self.assertEqual((1,), np.array(results["x.sw([0,1])"]).shape)
+ self.TestAlmostEqual([0], results["x.sw([0,1])"])
+ self.assertEqual((1, 6), np.array(results["x.sw([-3, 3])"]).shape)
+ self.TestAlmostEqual([[-5, -4, -3, -2, 4, 0]], results["x.sw([-3, 3])"])
+ self.assertEqual((1, 6), np.array(results["x.sw([-3, 3])-2"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0, 1, 7, 3]], results["x.sw([-3, 3])-2"])
+
def test_multi_in_window_offset(self):
NeuObj.clearNames()
# Here there is more sample for each time step but the dimensions of the input is 1
- in1 = Input('in1',dimensions=3)
+ in1 = Input("in1", dimensions=3)
# Finestre nel tempo
- out1 = Output('x.tw(1)', in1.tw(1, offset=-1))
- out2 = Output('x.tw([-1,0])', in1.tw([-1, 0], offset=-1))
- out3 = Output('x.tw([1,3])', in1.tw([1, 3], offset=1))
- out4 = Output('x.tw([-3,-2])', in1.tw([-3, -2], offset=-3))
+ out1 = Output("x.tw(1)", in1.tw(1, offset=-1))
+ out2 = Output("x.tw([-1,0])", in1.tw([-1, 0], offset=-1))
+ out3 = Output("x.tw([1,3])", in1.tw([1, 3], offset=1))
+ out4 = Output("x.tw([-3,-2])", in1.tw([-3, -2], offset=-3))
# Finesatre nei samples
- out5 = Output('x.sw([-1,0])', in1.sw([-1, 0], offset=-1))
- out6 = Output('x.sw([-3,1])', in1.sw([-3, 1], offset=-3))
- out7 = Output('x.sw([0,1])', in1.sw([0, 1], offset=0))
- out8 = Output('x.sw([-3, 3])', in1.sw([-3, 3], offset=2))
- out9 = Output('x.sw([-3, 3])-2', in1.sw([-3, 3], offset=-1))
+ out5 = Output("x.sw([-1,0])", in1.sw([-1, 0], offset=-1))
+ out6 = Output("x.sw([-3,1])", in1.sw([-3, 1], offset=-3))
+ out7 = Output("x.sw([0,1])", in1.sw([0, 1], offset=0))
+ out8 = Output("x.sw([-3, 3])", in1.sw([-3, 3], offset=2))
+ out9 = Output("x.sw([-3, 3])-2", in1.sw([-3, 3], offset=-1))
- test = Modely(visualizer = None, seed = 1)
- test.addModel('out',[out1, out2, out3, out4, out5, out6, out7, out8, out9])
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", [out1, out2, out3, out4, out5, out6, out7, out8, out9])
test.neuralizeModel(1)
# Single input
# Time -2, -1, 0, 1, 2, 3 # zero represent the last passed instant
- results = test({'in1': [[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]]})
+ results = test(
+ {
+ "in1": [
+ [-2, 3, 4],
+ [-1, 2, 2],
+ [0, 0, 0],
+ [1, 2, 3],
+ [2, 7, 3],
+ [3, 3, 3],
+ ]
+ }
+ )
# Time window
- self.assertEqual((1,1,3), np.array(results['x.tw(1)']).shape)
- self.TestAlmostEqual([[[0,0,0]]], results['x.tw(1)'])
- self.assertEqual((1,1,3), np.array(results['x.tw([-1,0])']).shape)
- self.TestAlmostEqual([[[0,0,0]]], results['x.tw([-1,0])'])
- self.assertEqual((1,2,3), np.array(results['x.tw([1,3])']).shape)
- self.TestAlmostEqual([[[0,0,0], [1,-4,0]]], results['x.tw([1,3])'])
- self.assertEqual((1,1,3), np.array(results['x.tw([-3,-2])']).shape)
- self.TestAlmostEqual([[[0,0,0]]], results['x.tw([-3,-2])'])
+ self.assertEqual((1, 1, 3), np.array(results["x.tw(1)"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw(1)"])
+ self.assertEqual((1, 1, 3), np.array(results["x.tw([-1,0])"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw([-1,0])"])
+ self.assertEqual((1, 2, 3), np.array(results["x.tw([1,3])"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0], [1, -4, 0]]], results["x.tw([1,3])"])
+ self.assertEqual((1, 1, 3), np.array(results["x.tw([-3,-2])"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw([-3,-2])"])
# # Sample window
- self.assertEqual((1,1,3), np.array(results['x.sw([-1,0])']).shape)
- self.TestAlmostEqual([[[0,0,0]]], results['x.sw([-1,0])'])
- self.assertEqual((1,4,3), np.array(results['x.sw([-3,1])']).shape)
- self.TestAlmostEqual([[[0,0,0], [1,-1,-2], [2,-3,-4], [3,-1,-1]]], results['x.sw([-3,1])'])
- self.assertEqual((1,1,3), np.array(results['x.sw([0,1])']).shape)
- self.TestAlmostEqual([[[0,0,0]]], results['x.sw([0,1])'])
- self.assertEqual((1,6,3), np.array(results['x.sw([-3, 3])']).shape)
- self.TestAlmostEqual([[[-5,0,1],[-4,-1,-1], [-3,-3,-3], [-2,-1,0], [-1,4,0], [0,0,0]]], results['x.sw([-3, 3])'])
- self.assertEqual((1,6,3), np.array(results['x.sw([-3, 3])-2']).shape)
- self.TestAlmostEqual([[[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]]], results['x.sw([-3, 3])-2'])
+ self.assertEqual((1, 1, 3), np.array(results["x.sw([-1,0])"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0]]], results["x.sw([-1,0])"])
+ self.assertEqual((1, 4, 3), np.array(results["x.sw([-3,1])"]).shape)
+ self.TestAlmostEqual(
+ [[[0, 0, 0], [1, -1, -2], [2, -3, -4], [3, -1, -1]]],
+ results["x.sw([-3,1])"],
+ )
+ self.assertEqual((1, 1, 3), np.array(results["x.sw([0,1])"]).shape)
+ self.TestAlmostEqual([[[0, 0, 0]]], results["x.sw([0,1])"])
+ self.assertEqual((1, 6, 3), np.array(results["x.sw([-3, 3])"]).shape)
+ self.TestAlmostEqual(
+ [
+ [
+ [-5, 0, 1],
+ [-4, -1, -1],
+ [-3, -3, -3],
+ [-2, -1, 0],
+ [-1, 4, 0],
+ [0, 0, 0],
+ ]
+ ],
+ results["x.sw([-3, 3])"],
+ )
+ self.assertEqual((1, 6, 3), np.array(results["x.sw([-3, 3])-2"]).shape)
+ self.TestAlmostEqual(
+ [[[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3]]],
+ results["x.sw([-3, 3])-2"],
+ )
# Multi input
- results = test({'in1': [[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3], [2, 2, 2]]})
- self.assertEqual((2,6,3), np.array(results['x.sw([-3, 3])']).shape)
- self.TestAlmostEqual([[[-5,0,1], [-4,-1,-1], [-3,-3,-3], [-2,-1,0], [-1,4,0], [0,0,0]],
- [[-3,0,0], [-2,-2,-2], [-1,0,1], [0,5,1], [1,1,1], [0,0,0]]], results['x.sw([-3, 3])'])
- self.assertEqual((2,6,3), np.array(results['x.sw([-3, 3])-2']).shape)
- self.TestAlmostEqual([[[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]],
- [[-2,0,-1],[-1,-2,-3],[0,0,0],[1,5,0],[2,1,0],[1,0,-1]]], results['x.sw([-3, 3])-2'])
-
+ results = test(
+ {
+ "in1": [
+ [-2, 3, 4],
+ [-1, 2, 2],
+ [0, 0, 0],
+ [1, 2, 3],
+ [2, 7, 3],
+ [3, 3, 3],
+ [2, 2, 2],
+ ]
+ }
+ )
+ self.assertEqual((2, 6, 3), np.array(results["x.sw([-3, 3])"]).shape)
+ self.TestAlmostEqual(
+ [
+ [
+ [-5, 0, 1],
+ [-4, -1, -1],
+ [-3, -3, -3],
+ [-2, -1, 0],
+ [-1, 4, 0],
+ [0, 0, 0],
+ ],
+ [[-3, 0, 0], [-2, -2, -2], [-1, 0, 1], [0, 5, 1], [1, 1, 1], [0, 0, 0]],
+ ],
+ results["x.sw([-3, 3])"],
+ )
+ self.assertEqual((2, 6, 3), np.array(results["x.sw([-3, 3])-2"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3]],
+ [
+ [-2, 0, -1],
+ [-1, -2, -3],
+ [0, 0, 0],
+ [1, 5, 0],
+ [2, 1, 0],
+ [1, 0, -1],
+ ],
+ ],
+ results["x.sw([-3, 3])-2"],
+ )
+
def test_single_in_window_offset_aritmetic(self):
NeuObj.clearNames()
# Elementwise arithmetic, Activation, Trigonometric
# the dimensions and time window remain unchanged, for the
# binary operators must be equal
- in1 = Input('in1')
- in2 = Input('in2', dimensions=2)
- out1 = Output('sum', in1.tw(1, offset=-1) + in1.tw([-1,0]))
- out2 = Output('sub', in1.tw([1, 3], offset=1) - in1.tw([-3, -1], offset=-2))
- out3 = Output('mul', in1.tw([-2, 2]) * in1.tw([-3, 1], offset=-2))
+ in1 = Input("in1")
+ in2 = Input("in2", dimensions=2)
+ out1 = Output("sum", in1.tw(1, offset=-1) + in1.tw([-1, 0]))
+ out2 = Output("sub", in1.tw([1, 3], offset=1) - in1.tw([-3, -1], offset=-2))
+ out3 = Output("mul", in1.tw([-2, 2]) * in1.tw([-3, 1], offset=-2))
- out4 = Output('sum2', in2.tw(1, offset=-1) + in2.tw([-1,0]))
- out5 = Output('sub2', in2.tw([1, 3], offset=1) - in2.tw([-3, -1], offset=-2))
- out6 = Output('mul2', in2.tw([-2, 2]) * in2.tw([-3, 1], offset=-2))
+ out4 = Output("sum2", in2.tw(1, offset=-1) + in2.tw([-1, 0]))
+ out5 = Output("sub2", in2.tw([1, 3], offset=1) - in2.tw([-3, -1], offset=-2))
+ out6 = Output("mul2", in2.tw([-2, 2]) * in2.tw([-3, 1], offset=-2))
test = Modely(visualizer=None)
- test.addModel('out',[out1, out2, out3, out4, out5, out6])
+ test.addModel("out", [out1, out2, out3, out4, out5, out6])
test.neuralizeModel(1)
# Single input
- #Time -2 -1 0 1 2 3 -2 -1 0 1 2 3
- results = test({'in1': [[1], [2], [8], [4], [-1], [6]], 'in2': [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3]]})
-
- self.assertEqual((1,), np.array(results['sum']).shape)
- self.TestAlmostEqual([8], results['sum'])
- self.assertEqual((1,2), np.array(results['sub']).shape) # [-1,6]+1 - [1,2]-2
- self.TestAlmostEqual([[1,7]], results['sub'])
- self.assertEqual((1,4), np.array(results['mul']).shape) #[2, 8, 4, -1]*[1, 2, 8, 4]-2
- self.TestAlmostEqual([[-2,0,24,-2]], results['mul'])#[2, 8, 4, -1]*[-1, 0, 6, 2]
-
- self.assertEqual((1,1,2), np.array(results['sum2']).shape)
- self.TestAlmostEqual([[[0,5]]], results['sum2'])
- self.assertEqual((1,2,2), np.array(results['sub2']).shape) #[[2,7],[3,3]]-[2,7] - [[-2,3],[-1,2]]-[-1,2]
- self.TestAlmostEqual([[[1,-1],[1,-4]]], results['sub2']) # [[0,0],[1,-4]] - [[-1,1],[0,0]]
- #[[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-2, 3], [-1, 2], [0, 5], [1, 2]]-[-1, 2]
+ # Time -2 -1 0 1 2 3 -2 -1 0 1 2 3
+ results = test(
+ {
+ "in1": [[1], [2], [8], [4], [-1], [6]],
+ "in2": [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3]],
+ }
+ )
+
+ self.assertEqual((1,), np.array(results["sum"]).shape)
+ self.TestAlmostEqual([8], results["sum"])
+ self.assertEqual((1, 2), np.array(results["sub"]).shape) # [-1,6]+1 - [1,2]-2
+ self.TestAlmostEqual([[1, 7]], results["sub"])
+ self.assertEqual(
+ (1, 4), np.array(results["mul"]).shape
+ ) # [2, 8, 4, -1]*[1, 2, 8, 4]-2
+ self.TestAlmostEqual(
+ [[-2, 0, 24, -2]], results["mul"]
+ ) # [2, 8, 4, -1]*[-1, 0, 6, 2]
+
+ self.assertEqual((1, 1, 2), np.array(results["sum2"]).shape)
+ self.TestAlmostEqual([[[0, 5]]], results["sum2"])
+ self.assertEqual(
+ (1, 2, 2), np.array(results["sub2"]).shape
+ ) # [[2,7],[3,3]]-[2,7] - [[-2,3],[-1,2]]-[-1,2]
+ self.TestAlmostEqual(
+ [[[1, -1], [1, -4]]], results["sub2"]
+ ) # [[0,0],[1,-4]] - [[-1,1],[0,0]]
+ # [[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-2, 3], [-1, 2], [0, 5], [1, 2]]-[-1, 2]
# [[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-1, 1], [0, 0], [1, 3], [2, 0]]
- self.assertEqual((1,4,2), np.array(results['mul2']).shape)
- self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]]], results['mul2'])
+ self.assertEqual((1, 4, 2), np.array(results["mul2"]).shape)
+ self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]]], results["mul2"])
# Multi input
# Time -2 -1 0 1 2 3 4 -2 -1 0 1 2 3 4
- results = test({'in1': [1, 2, 8, 4, -1, 6, 9], 'in2': [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3], [0, 0]]})
- self.assertEqual((2,), np.array(results['sum']).shape)
- self.TestAlmostEqual([8,4], results['sum'])
+ results = test(
+ {
+ "in1": [1, 2, 8, 4, -1, 6, 9],
+ "in2": [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3], [0, 0]],
+ }
+ )
+ self.assertEqual((2,), np.array(results["sum"]).shape)
+ self.TestAlmostEqual([8, 4], results["sum"])
# [6,9]-6 - [2,8]-8 = [0,3] - [-6,0]
- self.assertEqual((2,2), np.array(results['sub']).shape) # [-1,6]+1 - [1,2]-2
- self.TestAlmostEqual([[1,7],[6,3]], results['sub'])
- #[8, 4, -1, 6] * [2, 8, 4, -1]-8 = [8, 4, -1, 6] * [-6, 0, -4, -9]
- self.assertEqual((2,4), np.array(results['mul']).shape)
- self.TestAlmostEqual([[-2,0,24,-2],[-48,0,4,-54]], results['mul'])
-
- self.assertEqual((2,1,2), np.array(results['sum2']).shape)
- self.TestAlmostEqual([[[0,5]],[[1, 2]]], results['sum2'])
- self.assertEqual((2,2,2), np.array(results['sub2']).shape)
- #[[3, 3], [0, 0]]-[3,3] - [[-1, 2], [0, 5]]-[0, 5] = [[0, 0], [-3, -3]] - [[-1, -3], [0, 0]]
- self.TestAlmostEqual([[[1,-1],[1,-4]],[[1,3],[-3,-3]]], results['sub2'])
- #[[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, 2], [0, 5], [1, 2], [2, 7]]-[0, 5]
- #[[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, -3], [0, 0], [1, -3], [2, 2]]
- self.assertEqual((2,4,2), np.array(results['mul2']).shape)
- self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]],[[0, -15], [0, 0], [2, -21], [6, 6]]], results['mul2'])
+ self.assertEqual((2, 2), np.array(results["sub"]).shape) # [-1,6]+1 - [1,2]-2
+ self.TestAlmostEqual([[1, 7], [6, 3]], results["sub"])
+ # [8, 4, -1, 6] * [2, 8, 4, -1]-8 = [8, 4, -1, 6] * [-6, 0, -4, -9]
+ self.assertEqual((2, 4), np.array(results["mul"]).shape)
+ self.TestAlmostEqual([[-2, 0, 24, -2], [-48, 0, 4, -54]], results["mul"])
+
+ self.assertEqual((2, 1, 2), np.array(results["sum2"]).shape)
+ self.TestAlmostEqual([[[0, 5]], [[1, 2]]], results["sum2"])
+ self.assertEqual((2, 2, 2), np.array(results["sub2"]).shape)
+ # [[3, 3], [0, 0]]-[3,3] - [[-1, 2], [0, 5]]-[0, 5] = [[0, 0], [-3, -3]] - [[-1, -3], [0, 0]]
+ self.TestAlmostEqual([[[1, -1], [1, -4]], [[1, 3], [-3, -3]]], results["sub2"])
+ # [[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, 2], [0, 5], [1, 2], [2, 7]]-[0, 5]
+ # [[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, -3], [0, 0], [1, -3], [2, 2]]
+ self.assertEqual((2, 4, 2), np.array(results["mul2"]).shape)
+ self.TestAlmostEqual(
+ [[[1, 2], [0, 0], [1, 6], [4, 0]], [[0, -15], [0, 0], [2, -21], [6, 6]]],
+ results["mul2"],
+ )
def test_single_in_window_offset_fir(self):
NeuObj.clearNames()
# The input must be scalar and the time dimension is compress to 1,
# Vector input not allowed, it could be done that a number of fir filters equal to the size of the vector are constructed
# Should weights be shared or not?
- in1 = Input('in1')
- out1 = Output('Fir3', Fir(3)(in1.last()))
- out2 = Output('Fir5', Fir(5)(in1.tw(1)))#
- out3 = Output('Fir2', Fir(2)(in1.tw([-1,0])))#
- out4 = Output('Fir1', Fir(1)(in1.tw([-3,3])))#
- out5 = Output('Fir7', Fir(7)(in1.tw(3,offset=-1)))#
- out6 = Output('Fir4', Fir(4)(in1.tw([2,3],offset=2)))#
- out7 = Output('Fir6', Fir(6)(in1.sw([-2,-1], offset=-2)))#
-
- test = Modely(visualizer = None, seed = 1)
- test.addModel('out',[out1,out2,out3,out4,out5,out6,out7])
+ in1 = Input("in1")
+ out1 = Output("Fir3", Fir(3)(in1.last()))
+ out2 = Output("Fir5", Fir(5)(in1.tw(1))) #
+ out3 = Output("Fir2", Fir(2)(in1.tw([-1, 0]))) #
+ out4 = Output("Fir1", Fir(1)(in1.tw([-3, 3]))) #
+ out5 = Output("Fir7", Fir(7)(in1.tw(3, offset=-1))) #
+ out6 = Output("Fir4", Fir(4)(in1.tw([2, 3], offset=2))) #
+ out7 = Output("Fir6", Fir(6)(in1.sw([-2, -1], offset=-2))) #
+
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", [out1, out2, out3, out4, out5, out6, out7])
test.neuralizeModel(1)
# Single input
# Time -2 -1 0 1 2 3
- results = test({'in1': [[1], [2], [7], [4], [5], [6]]})
- self.assertEqual((1,1,3), np.array(results['Fir3']).shape)
- self.assertEqual((1,1,5), np.array(results['Fir5']).shape)
- self.assertEqual((1,1,2), np.array(results['Fir2']).shape)
- self.assertEqual((1,), np.array(results['Fir1']).shape)
- self.assertEqual((1,1,7), np.array(results['Fir7']).shape)
- self.assertEqual((1,1,4), np.array(results['Fir4']).shape)
- self.assertEqual((1,1,6), np.array(results['Fir6']).shape)
+ results = test({"in1": [[1], [2], [7], [4], [5], [6]]})
+ self.assertEqual((1, 1, 3), np.array(results["Fir3"]).shape)
+ self.assertEqual((1, 1, 5), np.array(results["Fir5"]).shape)
+ self.assertEqual((1, 1, 2), np.array(results["Fir2"]).shape)
+ self.assertEqual((1,), np.array(results["Fir1"]).shape)
+ self.assertEqual((1, 1, 7), np.array(results["Fir7"]).shape)
+ self.assertEqual((1, 1, 4), np.array(results["Fir4"]).shape)
+ self.assertEqual((1, 1, 6), np.array(results["Fir6"]).shape)
# Multi input there are 3 temporal instant
# Time -2 -1 0 1 2 3 4 5
- results = test({'in1': [[1], [2], [7], [4], [5], [6], [7], [8]]})
- self.assertEqual((3,1,3), np.array(results['Fir3']).shape)
- self.assertEqual((3,1,5), np.array(results['Fir5']).shape)
- self.assertEqual((3,1,2), np.array(results['Fir2']).shape)
- self.assertEqual((3,), np.array(results['Fir1']).shape)
- self.assertEqual((3,1,7), np.array(results['Fir7']).shape)
- self.assertEqual((3,1,4), np.array(results['Fir4']).shape)
- self.assertEqual((3,1,6), np.array(results['Fir6']).shape)
-
+ results = test({"in1": [[1], [2], [7], [4], [5], [6], [7], [8]]})
+ self.assertEqual((3, 1, 3), np.array(results["Fir3"]).shape)
+ self.assertEqual((3, 1, 5), np.array(results["Fir5"]).shape)
+ self.assertEqual((3, 1, 2), np.array(results["Fir2"]).shape)
+ self.assertEqual((3,), np.array(results["Fir1"]).shape)
+ self.assertEqual((3, 1, 7), np.array(results["Fir7"]).shape)
+ self.assertEqual((3, 1, 4), np.array(results["Fir4"]).shape)
+ self.assertEqual((3, 1, 6), np.array(results["Fir6"]).shape)
+
def test_fir_and_parameter(self):
NeuObj.clearNames()
- x = Input('x')
- p1 = Parameter('p1', tw=3, values=[[1],[2],[3],[6],[2],[3]])
+ x = Input("x")
+ p1 = Parameter("p1", tw=3, values=[[1], [2], [3], [6], [2], [3]])
with self.assertRaises(TypeError):
Fir(W=p1)(x)
with self.assertRaises(ValueError):
Fir(W=p1)(x.tw([-3, 1]))
- out1 = Output('out1', Fir(W=p1)(x.tw([-2, 1])))
+ out1 = Output("out1", Fir(W=p1)(x.tw([-2, 1])))
- p2 = Parameter('p2', sw=1, values=[[-2]])
+ p2 = Parameter("p2", sw=1, values=[[-2]])
with self.assertRaises(TypeError):
Fir(W=p2)(x.tw([-2, 1]))
- out2 = Output('out2', Fir(W=p2)(x.last()))
+ out2 = Output("out2", Fir(W=p2)(x.last()))
- p3 = Parameter('p3', dimensions=2, sw=1, values=[[-2,1]])
+ p3 = Parameter("p3", dimensions=2, sw=1, values=[[-2, 1]])
with self.assertRaises(TypeError):
Fir(W=p3)(x.tw([-2, 1]))
- out3 = Output('out3', Fir(W=p3)(x.last()))
+ out3 = Output("out3", Fir(W=p3)(x.last()))
- p4 = Parameter('p4', dimensions=2, tw=2, values=[[-2,1],[2,0],[0,1],[4,0]])
+ p4 = Parameter(
+ "p4", dimensions=2, tw=2, values=[[-2, 1], [2, 0], [0, 1], [4, 0]]
+ )
with self.assertRaises(TypeError):
Fir(W=p4)(x.sw([-2, 0]))
- out4 = Output('out4', Fir(W=p4)(x.tw([-2, 0])))
+ out4 = Output("out4", Fir(W=p4)(x.tw([-2, 0])))
- p5 = Parameter('p6', sw=2, dimensions=2, values=[[-2,1],[2,0]])
+ p5 = Parameter("p6", sw=2, dimensions=2, values=[[-2, 1], [2, 0]])
with self.assertRaises(TypeError):
- Fir(W = p5)(x)
+ Fir(W=p5)(x)
with self.assertRaises(TypeError):
- Fir(W = p5)(x.tw([-2,1]))
+ Fir(W=p5)(x.tw([-2, 1]))
with self.assertRaises(ValueError):
- Fir(W = p5)(x.sw([-2,1]))
- out5 = Output('out5', Fir(W=p5)(x.sw([-2, 0])))
+ Fir(W=p5)(x.sw([-2, 1]))
+ out5 = Output("out5", Fir(W=p5)(x.sw([-2, 0])))
test = Modely(visualizer=None)
- test.addModel('out',[out1, out2, out3, out4, out5])
+ test.addModel("out", [out1, out2, out3, out4, out5])
test.neuralizeModel(0.5)
# Time -3, -2, -1, 0, 1, 2, 3
input = [-2, -1, 0, 1, 2, 3, 12]
- results = test({'x': input})
-
- self.assertEqual((2,), np.array(results['out1']).shape)
- self.TestAlmostEqual([15,56], results['out1'])
- self.assertEqual((2,), np.array(results['out2']).shape)
- self.TestAlmostEqual([-2, -4], results['out2'])
- self.assertEqual((2, 1, 2), np.array(results['out3']).shape)
- self.TestAlmostEqual([[[-2,1]], [[-4,2]]], results['out3'])
- self.assertEqual((2, 1, 2), np.array(results['out4']).shape)
- self.TestAlmostEqual([[[6.0, -2.0]], [[10.0, 0.0]]], results['out4'])
- self.assertEqual((2, 1, 2), np.array(results['out5']).shape)
- self.TestAlmostEqual([[[2.0, 0.0]], [[2.0, 1.0]]], results['out5'])
-
+ results = test({"x": input})
+
+ self.assertEqual((2,), np.array(results["out1"]).shape)
+ self.TestAlmostEqual([15, 56], results["out1"])
+ self.assertEqual((2,), np.array(results["out2"]).shape)
+ self.TestAlmostEqual([-2, -4], results["out2"])
+ self.assertEqual((2, 1, 2), np.array(results["out3"]).shape)
+ self.TestAlmostEqual([[[-2, 1]], [[-4, 2]]], results["out3"])
+ self.assertEqual((2, 1, 2), np.array(results["out4"]).shape)
+ self.TestAlmostEqual([[[6.0, -2.0]], [[10.0, 0.0]]], results["out4"])
+ self.assertEqual((2, 1, 2), np.array(results["out5"]).shape)
+ self.TestAlmostEqual([[[2.0, 0.0]], [[2.0, 1.0]]], results["out5"])
+
def test_single_in_window_offset_parametric_function(self):
NeuObj.clearNames()
# An input dimension is temporal and does not remain unchanged unless redefined on output
# If there are multiple inputs the function returns an error if the dimensions are not defined
- in1 = Input('in1')
+ in1 = Input("in1")
parfun = ParamFun(myfun)
- out = Output('out', parfun(in1.last()))
- test = Modely(visualizer = None, seed = 1)
- test.addModel('out',out)
+ out = Output("out", parfun(in1.last()))
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- #results = test({'in1': 1})
- #self.TestAlmostEqual(results['out'], [0.7576315999031067])
- results = test({'in1': [[1]]})
- self.assertEqual((1,), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'],[0.7576315999031067])
- results = test({'in1': [2]})
- self.TestAlmostEqual(results['out'],[1.5152631998062134])
- results = test({'in1': [1,2]})
- self.assertEqual((2,), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'],[0.7576315999031067,1.5152631998062134])
-
- NeuObj.clearNames('out')
- out = Output('out', ParamFun(myfun)(in1.tw(0.2)))
- test = Modely(visualizer=None, seed = 1)
- test.addModel('out',out)
+ # results = test({'in1': 1})
+ # self.TestAlmostEqual(results['out'], [0.7576315999031067])
+ results = test({"in1": [[1]]})
+ self.assertEqual((1,), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [0.7576315999031067])
+ results = test({"in1": [2]})
+ self.TestAlmostEqual(results["out"], [1.5152631998062134])
+ results = test({"in1": [1, 2]})
+ self.assertEqual((2,), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [0.7576315999031067, 1.5152631998062134])
+
+ NeuObj.clearNames("out")
+ out = Output("out", ParamFun(myfun)(in1.tw(0.2)))
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- results = test({'in1': [1]})
+ results = test({"in1": [1]})
with self.assertRaises(StopIteration):
- results = test({'in1': [2]})
- results = test({'in1': [1,2]})
- self.assertEqual((1,2), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134]])
- test({'in1': [[1, 2]]}, num_of_samples=5, sampled=True)
- results = test({'in1': [[1,2]]}, sampled=True)
- self.assertEqual((1,2), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134]])
- results = test({'in1': [1, 2, 3, 4, 5]})# Qui vengono costruite gli input a due a due con shift di 1
- self.assertEqual((4,2), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134], [1.5152631998062134, 2.272894859313965], [2.272894859313965, 3.0305263996124268], [3.0305263996124268, 3.7881579399108887]])
- results = test({'in1': [[1, 2], [2, 3], [3, 4], [4, 5]]}, sampled=True)
- self.assertEqual((4,2), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134], [1.5152631998062134, 2.272894859313965], [2.272894859313965, 3.0305263996124268], [3.0305263996124268, 3.7881579399108887]])
-
- out = Output('out2', ParamFun(myfun)(in1.last(),in1.last()))
- test = Modely(visualizer=None, seed = 1)
- test.addModel('out',out)
+ results = test({"in1": [2]})
+ results = test({"in1": [1, 2]})
+ self.assertEqual((1, 2), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [[0.7576315999031067, 1.5152631998062134]])
+ test({"in1": [[1, 2]]}, num_of_samples=5, sampled=True)
+ results = test({"in1": [[1, 2]]}, sampled=True)
+ self.assertEqual((1, 2), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [[0.7576315999031067, 1.5152631998062134]])
+ results = test(
+ {"in1": [1, 2, 3, 4, 5]}
+ ) # Qui vengono costruite gli input a due a due con shift di 1
+ self.assertEqual((4, 2), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [0.7576315999031067, 1.5152631998062134],
+ [1.5152631998062134, 2.272894859313965],
+ [2.272894859313965, 3.0305263996124268],
+ [3.0305263996124268, 3.7881579399108887],
+ ],
+ )
+ results = test({"in1": [[1, 2], [2, 3], [3, 4], [4, 5]]}, sampled=True)
+ self.assertEqual((4, 2), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [0.7576315999031067, 1.5152631998062134],
+ [1.5152631998062134, 2.272894859313965],
+ [2.272894859313965, 3.0305263996124268],
+ [3.0305263996124268, 3.7881579399108887],
+ ],
+ )
+
+ out = Output("out2", ParamFun(myfun)(in1.last(), in1.last()))
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- #results = test({'in1': 1})
- #self.TestAlmostEqual(results['out'],[1])
- results = test({'in1': [1]})
- self.TestAlmostEqual(results['out2'],[1])
- results = test({'in1': [2]})
- self.TestAlmostEqual(results['out2'],[4])
- results = test({'in1': [1,2]})
- self.TestAlmostEqual(results['out2'],[1,4])
+ # results = test({'in1': 1})
+ # self.TestAlmostEqual(results['out'],[1])
+ results = test({"in1": [1]})
+ self.TestAlmostEqual(results["out2"], [1])
+ results = test({"in1": [2]})
+ self.TestAlmostEqual(results["out2"], [4])
+ results = test({"in1": [1, 2]})
+ self.TestAlmostEqual(results["out2"], [1, 4])
- out = Output('out3', ParamFun(myfun)(in1.tw(0.1), in1.tw(0.1)))
+ out = Output("out3", ParamFun(myfun)(in1.tw(0.1), in1.tw(0.1)))
test = Modely(visualizer=None)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- #results = test({'in1': 2})
- #self.TestAlmostEqual(results['out'], [4])
- results = test({'in1': [2]})
- self.TestAlmostEqual(results['out3'], [4])
- results = test({'in1': [2,1]})
- self.TestAlmostEqual(results['out3'], [4,1])
+ # results = test({'in1': 2})
+ # self.TestAlmostEqual(results['out'], [4])
+ results = test({"in1": [2]})
+ self.TestAlmostEqual(results["out3"], [4])
+ results = test({"in1": [2, 1]})
+ self.TestAlmostEqual(results["out3"], [4, 1])
- out = Output('out4', ParamFun(myfun)(in1.tw(0.2), in1.tw(0.2)))
+ out = Output("out4", ParamFun(myfun)(in1.tw(0.2), in1.tw(0.2)))
test = Modely(visualizer=None)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [2,4]})
- self.assertEqual((1,2), np.array(results['out4']).shape)
- self.TestAlmostEqual(results['out4'], [[4,16]])
- results = test({'in1': [[1, 2], [3, 2]]}, sampled=True)
- self.assertEqual((2,2), np.array(results['out4']).shape)
- self.TestAlmostEqual(results['out4'], [[1.0, 4.0], [9.0, 4.0]])
+ results = test({"in1": [2, 4]})
+ self.assertEqual((1, 2), np.array(results["out4"]).shape)
+ self.TestAlmostEqual(results["out4"], [[4, 16]])
+ results = test({"in1": [[1, 2], [3, 2]]}, sampled=True)
+ self.assertEqual((2, 2), np.array(results["out4"]).shape)
+ self.TestAlmostEqual(results["out4"], [[1.0, 4.0], [9.0, 4.0]])
- out = Output('out5', ParamFun(myfun)(in1.tw(0.3), in1.tw(0.3)))
+ out = Output("out5", ParamFun(myfun)(in1.tw(0.3), in1.tw(0.3)))
test = Modely(visualizer=None)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- test({'in1': [2]})
+ test({"in1": [2]})
with self.assertRaises(StopIteration):
- test({'in1': [2, 4]})
-
- results = test({'in1': [3,2,1]})
- self.assertEqual((1,3), np.array(results['out5']).shape)
- self.TestAlmostEqual(results['out5'], [[9,4,1]])
- results = test({'in1': [[1,2,2],[3,4,5]]}, sampled=True)
- self.assertEqual((2,3), np.array(results['out5']).shape)
- self.TestAlmostEqual(results['out5'], [[1, 4, 4], [9, 16, 25]])
- results = test({'in1': [[3, 2, 1], [2, 1, 0]]}, sampled=True)
- self.assertEqual((2,3), np.array(results['out5']).shape)
- self.TestAlmostEqual(results['out5'], [[9, 4, 1],[4, 1, 0]])
- results = test({'in1': [3,2,1,0]})
- self.assertEqual((2,3), np.array(results['out5']).shape)
- self.TestAlmostEqual(results['out5'], [[9, 4, 1],[4, 1, 0]])
-
- out = Output('out6', ParamFun(myfun)(in1.tw(0.4), in1.tw(0.4)))
+ test({"in1": [2, 4]})
+
+ results = test({"in1": [3, 2, 1]})
+ self.assertEqual((1, 3), np.array(results["out5"]).shape)
+ self.TestAlmostEqual(results["out5"], [[9, 4, 1]])
+ results = test({"in1": [[1, 2, 2], [3, 4, 5]]}, sampled=True)
+ self.assertEqual((2, 3), np.array(results["out5"]).shape)
+ self.TestAlmostEqual(results["out5"], [[1, 4, 4], [9, 16, 25]])
+ results = test({"in1": [[3, 2, 1], [2, 1, 0]]}, sampled=True)
+ self.assertEqual((2, 3), np.array(results["out5"]).shape)
+ self.TestAlmostEqual(results["out5"], [[9, 4, 1], [4, 1, 0]])
+ results = test({"in1": [3, 2, 1, 0]})
+ self.assertEqual((2, 3), np.array(results["out5"]).shape)
+ self.TestAlmostEqual(results["out5"], [[9, 4, 1], [4, 1, 0]])
+
+ out = Output("out6", ParamFun(myfun)(in1.tw(0.4), in1.tw(0.4)))
test = Modely(visualizer=None)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- test({'in1': [[1, 2, 2], [3, 4, 5]]})
-
+ test({"in1": [[1, 2, 2], [3, 4, 5]]})
+
def test_vectorial_input_parametric_function(self):
NeuObj.clearNames()
# Vector input for parametric function
- in1 = Input('in1', dimensions=3)
- in2 = Input('in2', dimensions=2)
- p1 = Parameter('p1', sw=1, dimensions=(3,2), values=[[[1,2],[3,4],[5,6]]])
- p2 = Parameter('p2', sw=1, dimensions=(1,3), values=[[1,2,3]])
- parfun = ParamFun(myfun3, parameters_and_constants=[p1,p2])
- out = Output('out', parfun(in1.last(),in2.last()))
- test = Modely(visualizer = None, seed = 1)
- test.addModel('out',out)
+ in1 = Input("in1", dimensions=3)
+ in2 = Input("in2", dimensions=2)
+ p1 = Parameter("p1", sw=1, dimensions=(3, 2), values=[[[1, 2], [3, 4], [5, 6]]])
+ p2 = Parameter("p2", sw=1, dimensions=(1, 3), values=[[1, 2, 3]])
+ parfun = ParamFun(myfun3, parameters_and_constants=[p1, p2])
+ out = Output("out", parfun(in1.last(), in2.last()))
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [[1,2,3]],'in2':[[5,6]]})
- self.assertEqual((1,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[23,52,81]])
+ results = test({"in1": [[1, 2, 3]], "in2": [[5, 6]]})
+ self.assertEqual((1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [[23, 52, 81]])
+
+ results = test({"in1": [[1, 2, 3], [5, 6, 7]], "in2": [[5, 6], [7, 8]]})
+ self.assertEqual((2, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(results["out"], [[23, 52, 81], [41, 94, 147]])
- results = test({'in1': [[1,2,3],[5,6,7]],'in2':[[5,6],[7,8]]})
- self.assertEqual((2,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[23,52,81],[41,94,147]])
-
def test_parametric_function_and_fir(self):
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- out = Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ out = Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4))))
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- test({'in1': [1, 2, 2]})
- results = test({'in1': [[1], [2], [2], [4]]})
- self.TestAlmostEqual(results['out'][0], -0.03262542933225632)
- results = test({'in1': [[[1], [2], [2], [4]],[[2], [2], [4], [5]]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.03262542933225632, -0.001211114227771759])
- results = test({'in1': [[1], [2], [2], [4], [5]]})
- self.TestAlmostEqual(results['out'], [-0.03262542933225632, -0.001211114227771759])
+ test({"in1": [1, 2, 2]})
+ results = test({"in1": [[1], [2], [2], [4]]})
+ self.TestAlmostEqual(results["out"][0], -0.03262542933225632)
+ results = test(
+ {"in1": [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True
+ )
+ self.TestAlmostEqual(
+ results["out"], [-0.03262542933225632, -0.001211114227771759]
+ )
+ results = test({"in1": [[1], [2], [2], [4], [5]]})
+ self.TestAlmostEqual(
+ results["out"], [-0.03262542933225632, -0.001211114227771759]
+ )
with self.assertRaises(RuntimeError):
- Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4),in2.tw(0.2))))
+ Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2))))
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- out = Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4),in2.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ out = Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4))))
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- with self.assertRaises(StopIteration): ## TODO: change to KeyError when checking the inputs
- test({'in1': [[1, 2, 2, 4]]})
-
- results = test({'in1': [1, 2, 2, 4], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044, 0.5163354873657227])
-
- results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [-0.4379930794239044, 0.5163354873657227])
+ with self.assertRaises(
+ StopIteration
+ ): ## TODO: change to KeyError when checking the inputs
+ test({"in1": [[1, 2, 2, 4]]})
+
+ results = test({"in1": [1, 2, 2, 4], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True)
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True
+ )
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]},
+ sampled=True,
+ )
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044, 0.5163354873657227])
+
+ results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True)
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True
+ )
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]},
+ sampled=True,
+ )
+ self.TestAlmostEqual(results["out"], [-0.4379930794239044, 0.5163354873657227])
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- out = Output('out', Fir(3)(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ out = Output("out", Fir(3)(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4))))
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [1, 2, 2, 4]]}, sampled=True)
- self.assertEqual((2,1,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]], [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]]])
-
- NeuObj.clearNames('out')
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [1, 2, 2, 4]]},
+ sampled=True,
+ )
+ self.assertEqual((2, 1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]],
+ [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]],
+ ],
+ )
+
+ NeuObj.clearNames("out")
parfun = ParamFun(myfun2)
with self.assertRaises(TypeError):
- Output('out', parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4)))))
+ Output("out", parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4)))))
parfun = ParamFun(myfun2)
- out = Output('out', parfun(Fir(3)(parfun(in1.tw(0.4)))))
+ out = Output("out", parfun(Fir(3)(parfun(in1.tw(0.4)))))
test = Modely(visualizer=None, seed=1)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True)
- self.assertEqual((2,1,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.24776744842529297, 0.2278038114309311, 0.2481299340724945]]])
-
- results = test({'in1': [1, 2, 2, 4, 3],'in2': [6, 2, 2, 4, 4]})
- self.assertEqual((2,1,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.1667831689119339,0.16757671535015106, 0.1605043113231659]]])
- results = test({'in1': [[1, 2, 2, 4],[2, 2, 4, 3]],'in2': [[6, 2, 2, 4],[2, 2, 4, 4]]}, sampled=True)
- self.assertEqual((2,1,3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.1667831689119339, 0.16757671535015106,0.1605043113231659]]])
+ results = test({"in1": [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True)
+ self.assertEqual((2, 1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]],
+ [[0.24776744842529297, 0.2278038114309311, 0.2481299340724945]],
+ ],
+ )
+
+ results = test({"in1": [1, 2, 2, 4, 3], "in2": [6, 2, 2, 4, 4]})
+ self.assertEqual((2, 1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]],
+ [[0.1667831689119339, 0.16757671535015106, 0.1605043113231659]],
+ ],
+ )
+ results = test(
+ {"in1": [[1, 2, 2, 4], [2, 2, 4, 3]], "in2": [[6, 2, 2, 4], [2, 2, 4, 4]]},
+ sampled=True,
+ )
+ self.assertEqual((2, 1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]],
+ [[0.1667831689119339, 0.16757671535015106, 0.1605043113231659]],
+ ],
+ )
def test_parametric_function_and_fir_with_parameters(self):
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
- k2 = Parameter('k2', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
- out = Output('out', Fir(ParamFun(myfun2, parameters_and_constants=[k1, k2])(in1.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
+ k2 = Parameter("k2", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
+ out = Output(
+ "out", Fir(ParamFun(myfun2, parameters_and_constants=[k1, k2])(in1.tw(0.4)))
+ )
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- test({'in1': [1, 2, 2]})
- results = test({'in1': [[1], [2], [2], [4]]})
- self.TestAlmostEqual(results['out'][0], 0.06549876928329468)
- results = test({'in1': [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.06549876928329468, -0.38155099749565125])
- results = test({'in1': [[1], [2], [2], [4], [5]]})
- self.TestAlmostEqual(results['out'], [0.06549876928329468, -0.38155099749565125])
+ test({"in1": [1, 2, 2]})
+ results = test({"in1": [[1], [2], [2], [4]]})
+ self.TestAlmostEqual(results["out"][0], 0.06549876928329468)
+ results = test(
+ {"in1": [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True
+ )
+ self.TestAlmostEqual(
+ results["out"], [0.06549876928329468, -0.38155099749565125]
+ )
+ results = test({"in1": [[1], [2], [2], [4], [5]]})
+ self.TestAlmostEqual(
+ results["out"], [0.06549876928329468, -0.38155099749565125]
+ )
with self.assertRaises(RuntimeError):
- Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2))))
+ Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2))))
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
- k_fir = Parameter('k_fir', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
- out = Output('out', Fir(W=k_fir)(ParamFun(myfun2, parameters_and_constants=[k1])(in1.tw(0.4), in2.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
+ k_fir = Parameter(
+ "k_fir", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]
+ )
+ out = Output(
+ "out",
+ Fir(W=k_fir)(
+ ParamFun(myfun2, parameters_and_constants=[k1])(
+ in1.tw(0.4), in2.tw(0.4)
+ )
+ ),
+ )
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
with self.assertRaises(StopIteration):
- test({'in1': [[1, 2, 2, 4]]})
-
- results = test({'in1': [1, 2, 2, 4], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159, 1.446676254272461])
-
- results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]})
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159])
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True)
- self.TestAlmostEqual(results['out'], [0.3850506544113159, 1.446676254272461])
+ test({"in1": [[1, 2, 2, 4]]})
+
+ results = test({"in1": [1, 2, 2, 4], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True)
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True
+ )
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]},
+ sampled=True,
+ )
+ self.TestAlmostEqual(results["out"], [0.3850506544113159, 1.446676254272461])
+
+ results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True)
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]})
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True
+ )
+ self.TestAlmostEqual(results["out"], [0.3850506544113159])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]},
+ sampled=True,
+ )
+ self.TestAlmostEqual(results["out"], [0.3850506544113159, 1.446676254272461])
NeuObj.clearNames()
- in1 = Input('in1')
- in2 = Input('in2')
- k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
- k_fir = Parameter('k_fir', dimensions=3, tw=0.4,
- values=[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])
- out = Output('out', Fir(3, W=k_fir)(ParamFun(myfun2, parameters_and_constants=[k1])(in1.tw(0.4), in2.tw(0.4))))
+ in1 = Input("in1")
+ in2 = Input("in2")
+ k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]])
+ k_fir = Parameter(
+ "k_fir",
+ dimensions=3,
+ tw=0.4,
+ values=[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]],
+ )
+ out = Output(
+ "out",
+ Fir(3, W=k_fir)(
+ ParamFun(myfun2, parameters_and_constants=[k1])(
+ in1.tw(0.4), in2.tw(0.4)
+ )
+ ),
+ )
test = Modely(visualizer=None)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [1, 2, 2, 4]]}, sampled=True)
- self.assertEqual((2, 1, 3), np.array(results['out']).shape)
- self.TestAlmostEqual(results['out'], [[[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]],
- [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]]])
+ results = test(
+ {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [1, 2, 2, 4]]},
+ sampled=True,
+ )
+ self.assertEqual((2, 1, 3), np.array(results["out"]).shape)
+ self.TestAlmostEqual(
+ results["out"],
+ [
+ [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]],
+ [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]],
+ ],
+ )
parfun = ParamFun(myfun2)
with self.assertRaises(TypeError):
- Output('out', parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4)))))
+ Output("out", parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4)))))
parfun = ParamFun(myfun2)
- out = Output('out2', parfun(Fir(3)(parfun(in1.tw(0.4)))))
+ out = Output("out2", parfun(Fir(3)(parfun(in1.tw(0.4)))))
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
- results = test({'in1': [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True)
- self.assertEqual((2, 1, 3), np.array(results['out2']).shape)
- self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
- [[0.8505557179450989, 0.7224123477935791, 0.77630215883255]]])
-
- results = test({'in1': [1, 2, 2, 4, 3], 'in2': [6, 2, 2, 4, 4]})
- self.assertEqual((2, 1, 3), np.array(results['out2']).shape)
- self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
- [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]]])
- results = test({'in1': [[1, 2, 2, 4], [2, 2, 4, 3]], 'in2': [[6, 2, 2, 4], [2, 2, 4, 4]]}, sampled=True)
- self.assertEqual((2, 1, 3), np.array(results['out2']).shape)
- self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
- [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]]])
+ results = test({"in1": [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True)
+ self.assertEqual((2, 1, 3), np.array(results["out2"]).shape)
+ self.TestAlmostEqual(
+ results["out2"],
+ [
+ [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
+ [[0.8505557179450989, 0.7224123477935791, 0.77630215883255]],
+ ],
+ )
+
+ results = test({"in1": [1, 2, 2, 4, 3], "in2": [6, 2, 2, 4, 4]})
+ self.assertEqual((2, 1, 3), np.array(results["out2"]).shape)
+ self.TestAlmostEqual(
+ results["out2"],
+ [
+ [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
+ [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]],
+ ],
+ )
+ results = test(
+ {"in1": [[1, 2, 2, 4], [2, 2, 4, 3]], "in2": [[6, 2, 2, 4], [2, 2, 4, 4]]},
+ sampled=True,
+ )
+ self.assertEqual((2, 1, 3), np.array(results["out2"]).shape)
+ self.TestAlmostEqual(
+ results["out2"],
+ [
+ [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]],
+ [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]],
+ ],
+ )
def test_trigonometri_parameter_and_numeric_constant(self):
NeuObj.clearNames()
- in1 = Input('in1').last()
- par = Parameter('par', values=5)
- in4 = Input('in4', dimensions=4).last()
- par4 = Parameter('par4', values=[1,2,3,4])
+ in1 = Input("in1").last()
+ par = Parameter("par", values=5)
+ in4 = Input("in4", dimensions=4).last()
+ par4 = Parameter("par4", values=[1, 2, 3, 4])
add = in1 + par + 5.2
sub = in1 - par - 5.2
mul = in1 * par * 5.2
div = in1 / par / 5.2
- pow = in1 ** par ** 2
+ pow = in1**par**2
sin1 = Sin(par) + Sin(5.2)
cos1 = Cos(par) + Cos(5.2)
tan1 = Tan(par) + Tan(5.2)
@@ -726,17 +1000,17 @@ def test_trigonometri_parameter_and_numeric_constant(self):
tot1 = add + sub + mul + div + pow + sin1 + cos1 + tan1 + relu1 + tanh1
add = 5.2 + in1 + (3 + par)
- sub = - 5.2 - par + (3 - in1)
+ sub = -5.2 - par + (3 - in1)
mul = 5.2 * in1 * (2 * par)
div = 5.2 / in1 / (3 / par)
- pow = (0.2 ** in1) + (2 ** par)
+ pow = (0.2**in1) + (2**par)
tot11 = add + sub + mul + div + pow
add4 = in4 + par4 + 5.2
sub4 = in4 - par4 - 5.2
mul4 = in4 * par4 * 5.2
div4 = in4 / par4 / 5.2
- pow4 = in4 ** par4 ** 2
+ pow4 = in4**par4**2
sin4 = Sin(par4) + Sin(5.2)
cos4 = Cos(par4) + Cos(5.2)
tan4 = Tan(par4) + Tan(5.2)
@@ -745,92 +1019,124 @@ def test_trigonometri_parameter_and_numeric_constant(self):
tot4 = add4 + sub4 + mul4 + div4 + pow4 + sin4 + cos4 + tan4 + relu4 + tanh4
add4 = 5.2 + in4 + (3 + par4)
- sub4 = - 5.2 - par4 + (3 - in4)
+ sub4 = -5.2 - par4 + (3 - in4)
mul4 = 5.2 * in4 * (2 * par4)
div4 = 5.2 / in4 / (3 / par4)
- pow4 = (0.2 ** in4) + (2 ** par4)
+ pow4 = (0.2**in4) + (2**par4)
tot41 = add4 + sub4 + mul4 + div4 + pow4
- out1 = Output('out1', tot1)
- out11 = Output('out11', tot11)
- out4 = Output('out4', tot4)
- out41 = Output('out41', tot41)
+ out1 = Output("out1", tot1)
+ out11 = Output("out11", tot11)
+ out4 = Output("out4", tot4)
+ out41 = Output("out41", tot41)
- linW = Parameter('linW', dimensions=(4,1),values=[[1],[1],[1],[1]])
- outtot = Output('outtot', tot1 + Linear(W=linW)(tot4))
+ linW = Parameter("linW", dimensions=(4, 1), values=[[1], [1], [1], [1]])
+ outtot = Output("outtot", tot1 + Linear(W=linW)(tot4))
test = Modely(visualizer=None, seed=1)
- test.addModel('out',[out1,out4,outtot,out11,out41])
+ test.addModel("out", [out1, out4, outtot, out11, out41])
test.neuralizeModel()
- results = test({'in1': [1, 2, -2],'in4': [[6, 2, 2, 4], [7, 2, 2, 4], [-6, -5, 5, 4]]})
- self.assertEqual((3,), np.array(results['out1']).shape)
- self.assertEqual((3,1,4), np.array(results['out4']).shape)
- self.assertEqual((3,), np.array(results['outtot']).shape)
-
- self.TestAlmostEqual([34.8819529, 33554496.0, -33554480.0], results['out1'] )
- self.TestAlmostEqual([[[58.9539756, 46.1638031, 554.231201171875, 4294967296.0]], [[67.3462829589843, 46.16380310058594, 554.231201171875, 4294967296.0]], [[ -41.75371170043945, 567.6907348632812, 1953220.375, 4294967296.0]]], results['out4'])
- self.TestAlmostEqual([4294967808.0, 4328522240.0, 4263366656.0], results['outtot'])
-
- self.assertEqual((3,), np.array(results['out11']).shape)
- self.assertEqual((3,1,4), np.array(results['out41']).shape)
-
- self.TestAlmostEqual([98.86666667, 146.37333333, -45.33333333], results['out11'])
- self.TestAlmostEqual([[[ 70.68895289, 53.37333333, 79.04 , 190.13493333]],
- [[ 81.04763185, 53.37333333, 79.04 , 190.13493333]],
- [[15570.31111111, 3030.30666667, 171.04032 , 190.13493333]]], results['out41'],precision=2)
+ results = test(
+ {"in1": [1, 2, -2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4], [-6, -5, 5, 4]]}
+ )
+ self.assertEqual((3,), np.array(results["out1"]).shape)
+ self.assertEqual((3, 1, 4), np.array(results["out4"]).shape)
+ self.assertEqual((3,), np.array(results["outtot"]).shape)
+
+ self.TestAlmostEqual([34.8819529, 33554496.0, -33554480.0], results["out1"])
+ self.TestAlmostEqual(
+ [
+ [[58.9539756, 46.1638031, 554.231201171875, 4294967296.0]],
+ [[67.3462829589843, 46.16380310058594, 554.231201171875, 4294967296.0]],
+ [[-41.75371170043945, 567.6907348632812, 1953220.375, 4294967296.0]],
+ ],
+ results["out4"],
+ )
+ self.TestAlmostEqual(
+ [4294967808.0, 4328522240.0, 4263366656.0], results["outtot"]
+ )
+
+ self.assertEqual((3,), np.array(results["out11"]).shape)
+ self.assertEqual((3, 1, 4), np.array(results["out41"]).shape)
+
+ self.TestAlmostEqual(
+ [98.86666667, 146.37333333, -45.33333333], results["out11"]
+ )
+ self.TestAlmostEqual(
+ [
+ [[70.68895289, 53.37333333, 79.04, 190.13493333]],
+ [[81.04763185, 53.37333333, 79.04, 190.13493333]],
+ [[15570.31111111, 3030.30666667, 171.04032, 190.13493333]],
+ ],
+ results["out41"],
+ precision=2,
+ )
def test_check_modify_stream(self):
NeuObj.clearNames()
- in1 = Input('in1').last()
- par = Parameter('par', values=5)
- add1 = in1 + par # 1 + 5 = 6
- add2 = add1 + 5.2 # 6 + 5.2 = 11.2
- tot1 = add1 + add2 # 6 + 11.2 = 17.2
- out1 = Output('out1', tot1) # = 17.2
- tot2 = add1 + in1 # 6 + 1 = 7
- out12 = Output('out12', tot1 + tot2) # = 24.2
- out2 = Output('out2', tot2) #= 7
- test = Modely(visualizer=None,seed=1)
- test.addModel('out',[out1,out12,out2])
+ in1 = Input("in1").last()
+ par = Parameter("par", values=5)
+ add1 = in1 + par # 1 + 5 = 6
+ add2 = add1 + 5.2 # 6 + 5.2 = 11.2
+ tot1 = add1 + add2 # 6 + 11.2 = 17.2
+ out1 = Output("out1", tot1) # = 17.2
+ tot2 = add1 + in1 # 6 + 1 = 7
+ out12 = Output("out12", tot1 + tot2) # = 24.2
+ out2 = Output("out2", tot2) # = 7
+ test = Modely(visualizer=None, seed=1)
+ test.addModel("out", [out1, out12, out2])
test.neuralizeModel()
- results = test({'in1': [1]})
- self.assertEqual((1,), np.array(results['out1']).shape)
- self.TestAlmostEqual([17.2], results['out1'] )
- self.TestAlmostEqual([24.2], results['out12'])
- self.TestAlmostEqual([7], results['out2'])
-
+ results = test({"in1": [1]})
+ self.assertEqual((1,), np.array(results["out1"]).shape)
+ self.TestAlmostEqual([17.2], results["out1"])
+ self.TestAlmostEqual([24.2], results["out12"])
+ self.TestAlmostEqual([7], results["out2"])
def test_parameter_and_linear(self):
NeuObj.clearNames()
- input = Input('in1').last()
- W15 = Parameter('W15', dimensions=(1, 5), values=[[1,2,3,4,5]])
- b15 = Parameter('b15', dimensions=5, values=[1,2,3,4,5])
- input4 = Input('in4', dimensions=4).last()
- W45 = Parameter('W45', dimensions=(4, 5), values=[[1,2,3,4,5],[5,3,3,4,5],[1,2,3,4,7],[-8,2,3,4,5]])
- b45 = Parameter('b45', dimensions=5, values=[5,2,3,4,5])
-
- o = Output('out' , Linear(input) + Linear(input4))
- o3 = Output('out3' , Linear(3)(input) + Linear(3)(input4))
- oW = Output('outW' , Linear(W = W15)(input) + Linear(W = W45)(input4))
- oWb = Output('outWb' , Linear(W = W15,b = b15)(input) + Linear(W = W45, b = b45)(input4))
+ input = Input("in1").last()
+ W15 = Parameter("W15", dimensions=(1, 5), values=[[1, 2, 3, 4, 5]])
+ b15 = Parameter("b15", dimensions=5, values=[1, 2, 3, 4, 5])
+ input4 = Input("in4", dimensions=4).last()
+ W45 = Parameter(
+ "W45",
+ dimensions=(4, 5),
+ values=[
+ [1, 2, 3, 4, 5],
+ [5, 3, 3, 4, 5],
+ [1, 2, 3, 4, 7],
+ [-8, 2, 3, 4, 5],
+ ],
+ )
+ b45 = Parameter("b45", dimensions=5, values=[5, 2, 3, 4, 5])
+
+ o = Output("out", Linear(input) + Linear(input4))
+ o3 = Output("out3", Linear(3)(input) + Linear(3)(input4))
+ oW = Output("outW", Linear(W=W15)(input) + Linear(W=W45)(input4))
+ oWb = Output(
+ "outWb", Linear(W=W15, b=b15)(input) + Linear(W=W45, b=b45)(input4)
+ )
n = Modely(visualizer=None, seed=1)
- n.addModel('out',[o,o3,oW,oWb])
- #n.addModel('out', [oW])
+ n.addModel("out", [o, o3, oW, oWb])
+ # n.addModel('out', [oW])
n.neuralizeModel()
- results = n({'in1': [1, 2], 'in4': [[6, 2, 2, 4], [7, 2, 2, 4]]})
- #self.assertEqual((2,), np.array(results['out']).shape)
- #self.TestAlmostEqual([9.274794578552246,10.3853759765625], results['out'])
- #self.assertEqual((2,1,3), np.array(results['out3']).shape)
- #self.TestAlmostEqual([[[9.247159004211426, 6.103044033050537,7.719359397888184]],[[10.68740463256836, 6.687504291534424,8.585973739624023]]], results['out3'])
- #W15 = torch.tensor([[1,2,3,4,5]])
- #in1 = torch.tensor([[1, 2]])
- #W45 = torch.tensor([[1, 2, 3, 4, 5], [5, 3, 3, 4, 5], [1, 2, 3, 4, 7], [-8, 2, 3, 4, 5]])
- #in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]])
- #torch.matmul(W45.t(),in4.t())+torch.matmul(W15.t(),in1)
- self.assertEqual((2, 1, 5), np.array(results['outW']).shape)
- self.TestAlmostEqual([[[-13.0,32.0,45.0,60.0,79.0]],[[-11.0,36.0,51.,68.,89.]]], results['outW'])
+ results = n({"in1": [1, 2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4]]})
+ # self.assertEqual((2,), np.array(results['out']).shape)
+ # self.TestAlmostEqual([9.274794578552246,10.3853759765625], results['out'])
+ # self.assertEqual((2,1,3), np.array(results['out3']).shape)
+ # self.TestAlmostEqual([[[9.247159004211426, 6.103044033050537,7.719359397888184]],[[10.68740463256836, 6.687504291534424,8.585973739624023]]], results['out3'])
+ # W15 = torch.tensor([[1,2,3,4,5]])
+ # in1 = torch.tensor([[1, 2]])
+ # W45 = torch.tensor([[1, 2, 3, 4, 5], [5, 3, 3, 4, 5], [1, 2, 3, 4, 7], [-8, 2, 3, 4, 5]])
+ # in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]])
+ # torch.matmul(W45.t(),in4.t())+torch.matmul(W15.t(),in1)
+ self.assertEqual((2, 1, 5), np.array(results["outW"]).shape)
+ self.TestAlmostEqual(
+ [[[-13.0, 32.0, 45.0, 60.0, 79.0]], [[-11.0, 36.0, 51.0, 68.0, 89.0]]],
+ results["outW"],
+ )
# W15 = torch.tensor([[1,2,3,4,5]])
# b15 = torch.tensor([[5, 2, 3, 4, 5]])
# in1 = torch.tensor([[1, 2]])
@@ -838,107 +1144,247 @@ def test_parameter_and_linear(self):
# b45 = torch.tensor([[1, 2, 3, 4, 5]])
# in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]])
# oo = torch.matmul(W45.t(),in4.t())+b45.t()+torch.matmul(W15.t(),in1)+b15.t()
- self.assertEqual((2, 1, 5), np.array(results['outWb']).shape)
- self.TestAlmostEqual([[[-7, 36, 51, 68, 89]],[[-5, 40, 57, 76, 99]]], results['outWb'])
+ self.assertEqual((2, 1, 5), np.array(results["outWb"]).shape)
+ self.TestAlmostEqual(
+ [[[-7, 36, 51, 68, 89]], [[-5, 40, 57, 76, 99]]], results["outWb"]
+ )
NeuObj.clearNames()
- input2 = Input('in1').sw([-1,1])
- input42 = Input('in4', dimensions=4).sw([-1,1])
-
- o = Output('out' , Linear(input2) + Linear(input42))
- o3 = Output('out3' , Linear(3)(input2) + Linear(3)(input42))
- oW = Output('outW' , Linear(W = W15)(input2) + Linear(W = W45)(input42))
- oWb = Output('outWb' , Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input42))
+ input2 = Input("in1").sw([-1, 1])
+ input42 = Input("in4", dimensions=4).sw([-1, 1])
+
+ o = Output("out", Linear(input2) + Linear(input42))
+ o3 = Output("out3", Linear(3)(input2) + Linear(3)(input42))
+ oW = Output("outW", Linear(W=W15)(input2) + Linear(W=W45)(input42))
+ oWb = Output(
+ "outWb", Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input42)
+ )
n = Modely(visualizer=None)
- n.addModel('out',[o,o3,oW,oWb])
+ n.addModel("out", [o, o3, oW, oWb])
n.neuralizeModel()
- results = n({'in1': [1, 2], 'in4': [[6, 2, 2, 4], [7, 2, 2, 4]]})
- self.assertEqual((1, 2), np.array(results['out']).shape)
- self.TestAlmostEqual([[7.3881096839904785, 7.91458797454834]], results['out'])
- self.assertEqual((1, 2, 3), np.array(results['out3']).shape)
- self.TestAlmostEqual([[[8.117439270019531, 6.014362812042236, 7.489190578460693],
- [9.261265754699707, 6.2568135261535645, 7.929978370666504]]], results['out3'])
- self.assertEqual((1, 2, 5), np.array(results['outW']).shape)
- self.TestAlmostEqual([[[-13.0, 32.0, 45.0, 60.0, 79.0], [-11.0, 36.0, 51., 68., 89.]]], results['outW'])
- self.assertEqual((1, 2, 5), np.array(results['outWb']).shape)
- self.TestAlmostEqual([[[-7, 36, 51, 68, 89], [-5, 40, 57, 76, 99]]], results['outWb'])
+ results = n({"in1": [1, 2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4]]})
+ self.assertEqual((1, 2), np.array(results["out"]).shape)
+ self.TestAlmostEqual([[7.3881096839904785, 7.91458797454834]], results["out"])
+ self.assertEqual((1, 2, 3), np.array(results["out3"]).shape)
+ self.TestAlmostEqual(
+ [
+ [
+ [8.117439270019531, 6.014362812042236, 7.489190578460693],
+ [9.261265754699707, 6.2568135261535645, 7.929978370666504],
+ ]
+ ],
+ results["out3"],
+ )
+ self.assertEqual((1, 2, 5), np.array(results["outW"]).shape)
+ self.TestAlmostEqual(
+ [[[-13.0, 32.0, 45.0, 60.0, 79.0], [-11.0, 36.0, 51.0, 68.0, 89.0]]],
+ results["outW"],
+ )
+ self.assertEqual((1, 2, 5), np.array(results["outWb"]).shape)
+ self.TestAlmostEqual(
+ [[[-7, 36, 51, 68, 89], [-5, 40, 57, 76, 99]]], results["outWb"]
+ )
def test_initialization(self):
NeuObj.clearNames()
- input = Input('in1')
- W = Parameter('W', dimensions=(1,1), init='init_constant')
- b = Parameter('b', dimensions=1, init='init_constant')
- o = Output('out', Linear(W=W,b=b)(input.last()))
-
- W5 = Parameter('W5', dimensions=(1,1), init='init_constant', init_params={'value':5})
- b2 = Parameter('b2', dimensions=1, init='init_constant', init_params={'value':2})
- o52 = Output('out52', Linear(W=W5,b=b2)(input.last()))
-
- par = Parameter('par', dimensions=3, sw=2, init='init_constant')
- opar = Output('outpar', Fir(W=par)(input.sw(2)))
-
- par2 = Parameter('par2', dimensions=3, sw=2, init='init_constant', init_params={'value':2})
- opar2 = Output('outpar2', Fir(W=par2, b=False)(input.sw(2)))
-
- ol = Output('outl', Linear(output_dimension=1,b=True,W_init='init_constant',b_init='init_constant')(input.last()))
- ol52 = Output('outl52', Linear(output_dimension=1,b=True,W_init='init_constant',b_init='init_constant',W_init_params={'value':5},b_init_params={'value':2})(input.last()))
- ofpar = Output('outfpar', Fir(output_dimension=3,W_init='init_constant')(input.sw(2)))
- ofpar2 = Output('outfpar2', Fir(output_dimension=3,W_init='init_constant',W_init_params={'value':2})(input.sw(2)))
+ input = Input("in1")
+ W = Parameter("W", dimensions=(1, 1), init="init_constant")
+ b = Parameter("b", dimensions=1, init="init_constant")
+ o = Output("out", Linear(W=W, b=b)(input.last()))
+
+ W5 = Parameter(
+ "W5", dimensions=(1, 1), init="init_constant", init_params={"value": 5}
+ )
+ b2 = Parameter(
+ "b2", dimensions=1, init="init_constant", init_params={"value": 2}
+ )
+ o52 = Output("out52", Linear(W=W5, b=b2)(input.last()))
+
+ par = Parameter("par", dimensions=3, sw=2, init="init_constant")
+ opar = Output("outpar", Fir(W=par)(input.sw(2)))
+
+ par2 = Parameter(
+ "par2", dimensions=3, sw=2, init="init_constant", init_params={"value": 2}
+ )
+ opar2 = Output("outpar2", Fir(W=par2, b=False)(input.sw(2)))
+
+ ol = Output(
+ "outl",
+ Linear(
+ output_dimension=1,
+ b=True,
+ W_init="init_constant",
+ b_init="init_constant",
+ )(input.last()),
+ )
+ ol52 = Output(
+ "outl52",
+ Linear(
+ output_dimension=1,
+ b=True,
+ W_init="init_constant",
+ b_init="init_constant",
+ W_init_params={"value": 5},
+ b_init_params={"value": 2},
+ )(input.last()),
+ )
+ ofpar = Output(
+ "outfpar", Fir(output_dimension=3, W_init="init_constant")(input.sw(2))
+ )
+ ofpar2 = Output(
+ "outfpar2",
+ Fir(output_dimension=3, W_init="init_constant", W_init_params={"value": 2})(
+ input.sw(2)
+ ),
+ )
+
+ outnegexp = Output(
+ "outnegexp", Fir(output_dimension=3, W_init="init_negexp")(input.sw(2))
+ )
+ outnegexp2 = Output(
+ "outnegexp2",
+ Fir(
+ output_dimension=3,
+ W_init="init_negexp",
+ W_init_params={"size_index": 1, "first_value": 3, "lambda": 1},
+ )(input.sw(2)),
+ )
+
+ outexp = Output(
+ "outexp", Fir(output_dimension=3, W_init="init_exp")(input.sw(2))
+ )
+ outexp2 = Output(
+ "outexp2",
+ Fir(
+ output_dimension=3,
+ W_init="init_exp",
+ W_init_params={
+ "size_index": 1,
+ "max_value": 2,
+ "lambda": 2,
+ "monotonicity": "increasing",
+ },
+ )(input.sw(2)),
+ )
+ outexp2D = Output(
+ "outexp2D",
+ Fir(
+ output_dimension=3,
+ W_init="init_exp",
+ W_init_params={
+ "size_index": 1,
+ "max_value": 2,
+ "lambda": 2,
+ "monotonicity": "decreasing",
+ },
+ )(input.sw(2)),
+ )
+
+ outlin = Output(
+ "outlin", Fir(output_dimension=3, W_init="init_lin")(input.sw(2))
+ )
+ outlin2 = Output(
+ "outlin2",
+ Fir(
+ output_dimension=3,
+ W_init="init_lin",
+ W_init_params={"size_index": 1, "first_value": 4, "last_value": 5},
+ )(input.sw(2)),
+ )
- outnegexp = Output('outnegexp', Fir(output_dimension=3,W_init='init_negexp')(input.sw(2)))
- outnegexp2 = Output('outnegexp2', Fir(output_dimension=3,W_init='init_negexp',W_init_params={'size_index':1, 'first_value':3, 'lambda':1})(input.sw(2)))
-
- outexp = Output('outexp', Fir(output_dimension=3,W_init='init_exp')(input.sw(2)))
- outexp2 = Output('outexp2', Fir(output_dimension=3,W_init='init_exp',W_init_params={'size_index':1, 'max_value':2, 'lambda':2, 'monotonicity':'increasing'})(input.sw(2)))
- outexp2D = Output('outexp2D', Fir(output_dimension=3,W_init='init_exp',W_init_params={'size_index':1, 'max_value':2, 'lambda':2, 'monotonicity':'decreasing'})(input.sw(2)))
-
- outlin = Output('outlin', Fir(output_dimension=3,W_init='init_lin')(input.sw(2)))
- outlin2 = Output('outlin2', Fir(output_dimension=3,W_init='init_lin',W_init_params={'size_index':1, 'first_value':4, 'last_value':5})(input.sw(2)))
-
- n = Modely(visualizer=None,seed=1)
- n.addModel('model',[o,o52,opar,opar2,ol,ol52,ofpar,ofpar2,outnegexp,outnegexp2,outexp,outexp2,outexp2D,outlin,outlin2])
+ n = Modely(visualizer=None, seed=1)
+ n.addModel(
+ "model",
+ [
+ o,
+ o52,
+ opar,
+ opar2,
+ ol,
+ ol52,
+ ofpar,
+ ofpar2,
+ outnegexp,
+ outnegexp2,
+ outexp,
+ outexp2,
+ outexp2D,
+ outlin,
+ outlin2,
+ ],
+ )
n.neuralizeModel()
- results = n({'in1': [1, 1, 2]})
- self.assertEqual((2,), np.array(results['out']).shape)
- self.TestAlmostEqual([2,3], results['out'])
- self.assertEqual((2,), np.array(results['out52']).shape)
- self.TestAlmostEqual([7,12], results['out52'])
-
- self.assertEqual((2,1,3), np.array(results['outpar']).shape)
- self.TestAlmostEqual([[[2,2,2]],[[3,3,3]]], results['outpar'])
- self.assertEqual((2,1,3), np.array(results['outpar2']).shape)
- self.TestAlmostEqual([[[4,4,4]],[[6,6,6]]], results['outpar2'])
-
- self.assertEqual((2,), np.array(results['outl']).shape)
- self.TestAlmostEqual([2,3], results['outl'])
- self.assertEqual((2,), np.array(results['outl52']).shape)
- self.TestAlmostEqual([7.0,12.0], results['outl52'])
-
- self.assertEqual((2,1,3), np.array(results['outfpar']).shape)
- self.TestAlmostEqual([[[2,2,2]],[[3,3,3]]], results['outfpar'])
- self.assertEqual((2,1,3), np.array(results['outfpar2']).shape)
- self.TestAlmostEqual([[[4,4,4]],[[6,6,6]]], results['outfpar2'])
-
- self.assertEqual((2,1,3), np.array(results['outnegexp']).shape)
- self.TestAlmostEqual([[[1.0497870445251465,1.0497870445251465,1.0497870445251465]],[[2.0497870445251465,2.0497870445251465,2.0497870445251465]]], results['outnegexp'])
- self.assertEqual((2,1,3), np.array(results['outnegexp2']).shape)
- self.TestAlmostEqual([[[2.2072765827178955,3.63918399810791,6.0]],[[3.310914993286133,5.458775997161865,9.0]]], results['outnegexp2'])
-
- self.assertEqual((2,1,3), np.array(results['outexp']).shape)
- self.TestAlmostEqual([[[1.0497870445251465,1.0497870445251465,1.0497870445251465]],[[1.099574089050293,1.099574089050293,1.099574089050293]]], results['outexp'])
- self.assertEqual((2,1,3), np.array(results['outexp2']).shape)
- self.TestAlmostEqual([[[0.5413411259651184,1.47151780128479,4]],[[0.81201171875,2.2072768211364746,6]]], results['outexp2'])
- self.assertEqual((2,1,3), np.array(results['outexp2D']).shape)
- self.TestAlmostEqual([[[4.0,1.47151780128479,0.5413411259651184]],[[6,2.2072768211364746,0.81201171875]]], results['outexp2D'])
-
- self.assertEqual((2,1,3), np.array(results['outlin']).shape)
- self.TestAlmostEqual([[[1,1,1]],[[1,1,1]]], results['outlin'])
- self.assertEqual((2,1,3), np.array(results['outlin2']).shape)
- self.TestAlmostEqual([[[8,9,10]],[[12,13.5,15.0]]], results['outlin2'])
+ results = n({"in1": [1, 1, 2]})
+ self.assertEqual((2,), np.array(results["out"]).shape)
+ self.TestAlmostEqual([2, 3], results["out"])
+ self.assertEqual((2,), np.array(results["out52"]).shape)
+ self.TestAlmostEqual([7, 12], results["out52"])
+
+ self.assertEqual((2, 1, 3), np.array(results["outpar"]).shape)
+ self.TestAlmostEqual([[[2, 2, 2]], [[3, 3, 3]]], results["outpar"])
+ self.assertEqual((2, 1, 3), np.array(results["outpar2"]).shape)
+ self.TestAlmostEqual([[[4, 4, 4]], [[6, 6, 6]]], results["outpar2"])
+
+ self.assertEqual((2,), np.array(results["outl"]).shape)
+ self.TestAlmostEqual([2, 3], results["outl"])
+ self.assertEqual((2,), np.array(results["outl52"]).shape)
+ self.TestAlmostEqual([7.0, 12.0], results["outl52"])
+
+ self.assertEqual((2, 1, 3), np.array(results["outfpar"]).shape)
+ self.TestAlmostEqual([[[2, 2, 2]], [[3, 3, 3]]], results["outfpar"])
+ self.assertEqual((2, 1, 3), np.array(results["outfpar2"]).shape)
+ self.TestAlmostEqual([[[4, 4, 4]], [[6, 6, 6]]], results["outfpar2"])
+
+ self.assertEqual((2, 1, 3), np.array(results["outnegexp"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[1.0497870445251465, 1.0497870445251465, 1.0497870445251465]],
+ [[2.0497870445251465, 2.0497870445251465, 2.0497870445251465]],
+ ],
+ results["outnegexp"],
+ )
+ self.assertEqual((2, 1, 3), np.array(results["outnegexp2"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[2.2072765827178955, 3.63918399810791, 6.0]],
+ [[3.310914993286133, 5.458775997161865, 9.0]],
+ ],
+ results["outnegexp2"],
+ )
+
+ self.assertEqual((2, 1, 3), np.array(results["outexp"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[1.0497870445251465, 1.0497870445251465, 1.0497870445251465]],
+ [[1.099574089050293, 1.099574089050293, 1.099574089050293]],
+ ],
+ results["outexp"],
+ )
+ self.assertEqual((2, 1, 3), np.array(results["outexp2"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[0.5413411259651184, 1.47151780128479, 4]],
+ [[0.81201171875, 2.2072768211364746, 6]],
+ ],
+ results["outexp2"],
+ )
+ self.assertEqual((2, 1, 3), np.array(results["outexp2D"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[4.0, 1.47151780128479, 0.5413411259651184]],
+ [[6, 2.2072768211364746, 0.81201171875]],
+ ],
+ results["outexp2D"],
+ )
+
+ self.assertEqual((2, 1, 3), np.array(results["outlin"]).shape)
+ self.TestAlmostEqual([[[1, 1, 1]], [[1, 1, 1]]], results["outlin"])
+ self.assertEqual((2, 1, 3), np.array(results["outlin2"]).shape)
+ self.TestAlmostEqual([[[8, 9, 10]], [[12, 13.5, 15.0]]], results["outlin2"])
def test_sample_part_and_select(self):
NeuObj.clearNames()
- in1 = Input('in1')
+ in1 = Input("in1")
# Offset before the sample window
with self.assertRaises(IndexError):
in1.sw([-5, -2], offset=-6)
@@ -952,25 +1398,25 @@ def test_sample_part_and_select(self):
with self.assertRaises(IndexError):
in1.sw([0, 3], offset=3)
- sw3,sw32 = in1.sw([-5, -2], offset=-4), in1.sw([0, 3], offset=0)
- out_sw3 = Output('in_sw3', sw3)
- out_sw32 = Output('in_sw32', sw32)
- #Get after the window
+ sw3, sw32 = in1.sw([-5, -2], offset=-4), in1.sw([0, 3], offset=0)
+ out_sw3 = Output("in_sw3", sw3)
+ out_sw32 = Output("in_sw32", sw32)
+ # Get after the window
with self.assertRaises(ValueError):
SamplePart(sw3, 0, 4)
- #Empty sample window
+ # Empty sample window
with self.assertRaises(ValueError):
SamplePart(sw3, 0, 0)
- #Get before the sample window
+ # Get before the sample window
with self.assertRaises(ValueError):
SamplePart(sw3, -1, 0)
# Get after the window
with self.assertRaises(ValueError):
SamplePart(sw32, 0, 4)
- #Empty sample window
+ # Empty sample window
with self.assertRaises(ValueError):
SamplePart(sw32, 0, 0)
- #Get before the sample window
+ # Get before the sample window
with self.assertRaises(ValueError):
SamplePart(sw32, -1, 0)
@@ -986,13 +1432,13 @@ def test_sample_part_and_select(self):
# Offset after the sample window
with self.assertRaises(IndexError):
SamplePart(sw32, 0, 3, offset=3)
- in_SP1first = Output('in_SP1first', SamplePart(sw3, 0, 1))
- in_SP1mid = Output('in_SP1mid', SamplePart(sw3, 1, 2))
- in_SP1last = Output('in_SP1last', SamplePart(sw3, 2, 3))
- in_SP1all = Output('in_SP1all', SamplePart(sw3, 0, 3))
- in_SP1off1 = Output('in_SP1off1', SamplePart(sw3, 0, 3, offset=0))
- in_SP1off2 = Output('in_SP1off2', SamplePart(sw3, 0, 3, offset=1))
- in_SP1off3 = Output('in_SP1off3', SamplePart(sw3, 0, 3, offset=2))
+ in_SP1first = Output("in_SP1first", SamplePart(sw3, 0, 1))
+ in_SP1mid = Output("in_SP1mid", SamplePart(sw3, 1, 2))
+ in_SP1last = Output("in_SP1last", SamplePart(sw3, 2, 3))
+ in_SP1all = Output("in_SP1all", SamplePart(sw3, 0, 3))
+ in_SP1off1 = Output("in_SP1off1", SamplePart(sw3, 0, 3, offset=0))
+ in_SP1off2 = Output("in_SP1off2", SamplePart(sw3, 0, 3, offset=1))
+ in_SP1off3 = Output("in_SP1off3", SamplePart(sw3, 0, 3, offset=2))
with self.assertRaises(ValueError):
SampleSelect(sw3, -1)
with self.assertRaises(ValueError):
@@ -1001,75 +1447,89 @@ def test_sample_part_and_select(self):
SampleSelect(sw32, -1)
with self.assertRaises(ValueError):
SampleSelect(sw32, 3)
- in_SS1 = Output('in_SS1', SampleSelect(sw3, 0))
- in_SS2 = Output('in_SS2', SampleSelect(sw3, 1))
- in_SS3 = Output('in_SS3', SampleSelect(sw3, 2))
+ in_SS1 = Output("in_SS1", SampleSelect(sw3, 0))
+ in_SS2 = Output("in_SS2", SampleSelect(sw3, 1))
+ in_SS3 = Output("in_SS3", SampleSelect(sw3, 2))
test = Modely(visualizer=None)
- test.addModel('out',[out_sw3, out_sw32,
- in_SP1first, in_SP1mid, in_SP1last, in_SP1all, in_SP1off1, in_SP1off2, in_SP1off3,
- in_SS1, in_SS2, in_SS3])
+ test.addModel(
+ "out",
+ [
+ out_sw3,
+ out_sw32,
+ in_SP1first,
+ in_SP1mid,
+ in_SP1last,
+ in_SP1all,
+ in_SP1off1,
+ in_SP1off2,
+ in_SP1off3,
+ in_SS1,
+ in_SS2,
+ in_SS3,
+ ],
+ )
test.neuralizeModel()
- results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7]})
-
- self.assertEqual((1, 3), np.array(results['in_sw3']).shape)
- self.TestAlmostEqual([[-1,0,1]], results['in_sw3'])
- self.assertEqual((1, 3), np.array(results['in_sw32']).shape)
- self.TestAlmostEqual([[0,1,2]], results['in_sw32'])
-
- self.assertEqual((1,), np.array(results['in_SP1first']).shape)
- self.TestAlmostEqual([-1], results['in_SP1first'])
- self.assertEqual((1,), np.array(results['in_SP1mid']).shape)
- self.TestAlmostEqual([0], results['in_SP1mid'])
- self.assertEqual((1,), np.array(results['in_SP1last']).shape)
- self.TestAlmostEqual([1], results['in_SP1last'])
- self.assertEqual((1,3), np.array(results['in_SP1all']).shape)
- self.TestAlmostEqual([[-1,0,1]], results['in_SP1all'])
- self.assertEqual((1,3), np.array(results['in_SP1off1']).shape)
- self.TestAlmostEqual([[0,1,2]], results['in_SP1off1'])
- self.assertEqual((1,3), np.array(results['in_SP1off2']).shape)
- self.TestAlmostEqual([[-1,0,1]], results['in_SP1off2'])
- self.assertEqual((1,3), np.array(results['in_SP1off3']).shape)
- self.TestAlmostEqual([[-2,-1,0]], results['in_SP1off3'])
-
- self.assertEqual((1,), np.array(results['in_SS1']).shape)
- self.TestAlmostEqual([-1], results['in_SS1'])
- self.assertEqual((1,), np.array(results['in_SS2']).shape)
- self.TestAlmostEqual([0], results['in_SS2'])
- self.assertEqual((1,), np.array(results['in_SS3']).shape)
- self.TestAlmostEqual([1], results['in_SS3'])
-
- results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7, 10]})
-
- self.assertEqual((2, 3), np.array(results['in_sw3']).shape)
- self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_sw3'])
- self.assertEqual((2, 3), np.array(results['in_sw32']).shape)
- self.TestAlmostEqual([[0, 1, 2],[0, 1, 4]], results['in_sw32'])
-
- self.assertEqual((2,), np.array(results['in_SP1first']).shape)
- self.TestAlmostEqual([-1,-1], results['in_SP1first'])
- self.assertEqual((2,), np.array(results['in_SP1mid']).shape)
- self.TestAlmostEqual([0,0], results['in_SP1mid'])
- self.assertEqual((2,), np.array(results['in_SP1last']).shape)
- self.TestAlmostEqual([1,1], results['in_SP1last'])
- self.assertEqual((2, 3), np.array(results['in_SP1all']).shape)
- self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_SP1all'])
- self.assertEqual((2, 3), np.array(results['in_SP1off1']).shape)
- self.TestAlmostEqual([[0, 1, 2],[0, 1, 2]], results['in_SP1off1'])
- self.assertEqual((2, 3), np.array(results['in_SP1off2']).shape)
- self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_SP1off2'])
- self.assertEqual((2, 3), np.array(results['in_SP1off3']).shape)
- self.TestAlmostEqual([[-2, -1, 0],[-2, -1, 0]], results['in_SP1off3'])
-
- self.assertEqual((2,), np.array(results['in_SS1']).shape)
- self.TestAlmostEqual([-1,-1], results['in_SS1'])
- self.assertEqual((2,), np.array(results['in_SS2']).shape)
- self.TestAlmostEqual([0,0], results['in_SS2'])
- self.assertEqual((2,), np.array(results['in_SS3']).shape)
- self.TestAlmostEqual([1,1], results['in_SS3'])
+ results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7]})
+
+ self.assertEqual((1, 3), np.array(results["in_sw3"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_sw3"])
+ self.assertEqual((1, 3), np.array(results["in_sw32"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_sw32"])
+
+ self.assertEqual((1,), np.array(results["in_SP1first"]).shape)
+ self.TestAlmostEqual([-1], results["in_SP1first"])
+ self.assertEqual((1,), np.array(results["in_SP1mid"]).shape)
+ self.TestAlmostEqual([0], results["in_SP1mid"])
+ self.assertEqual((1,), np.array(results["in_SP1last"]).shape)
+ self.TestAlmostEqual([1], results["in_SP1last"])
+ self.assertEqual((1, 3), np.array(results["in_SP1all"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_SP1all"])
+ self.assertEqual((1, 3), np.array(results["in_SP1off1"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_SP1off1"])
+ self.assertEqual((1, 3), np.array(results["in_SP1off2"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_SP1off2"])
+ self.assertEqual((1, 3), np.array(results["in_SP1off3"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0]], results["in_SP1off3"])
+
+ self.assertEqual((1,), np.array(results["in_SS1"]).shape)
+ self.TestAlmostEqual([-1], results["in_SS1"])
+ self.assertEqual((1,), np.array(results["in_SS2"]).shape)
+ self.TestAlmostEqual([0], results["in_SS2"])
+ self.assertEqual((1,), np.array(results["in_SS3"]).shape)
+ self.TestAlmostEqual([1], results["in_SS3"])
+
+ results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7, 10]})
+
+ self.assertEqual((2, 3), np.array(results["in_sw3"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_sw3"])
+ self.assertEqual((2, 3), np.array(results["in_sw32"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_sw32"])
+
+ self.assertEqual((2,), np.array(results["in_SP1first"]).shape)
+ self.TestAlmostEqual([-1, -1], results["in_SP1first"])
+ self.assertEqual((2,), np.array(results["in_SP1mid"]).shape)
+ self.TestAlmostEqual([0, 0], results["in_SP1mid"])
+ self.assertEqual((2,), np.array(results["in_SP1last"]).shape)
+ self.TestAlmostEqual([1, 1], results["in_SP1last"])
+ self.assertEqual((2, 3), np.array(results["in_SP1all"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_SP1all"])
+ self.assertEqual((2, 3), np.array(results["in_SP1off1"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, 2]], results["in_SP1off1"])
+ self.assertEqual((2, 3), np.array(results["in_SP1off2"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_SP1off2"])
+ self.assertEqual((2, 3), np.array(results["in_SP1off3"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0], [-2, -1, 0]], results["in_SP1off3"])
+
+ self.assertEqual((2,), np.array(results["in_SS1"]).shape)
+ self.TestAlmostEqual([-1, -1], results["in_SS1"])
+ self.assertEqual((2,), np.array(results["in_SS2"]).shape)
+ self.TestAlmostEqual([0, 0], results["in_SS2"])
+ self.assertEqual((2,), np.array(results["in_SS3"]).shape)
+ self.TestAlmostEqual([1, 1], results["in_SS3"])
def test_time_part(self):
NeuObj.clearNames()
- in1 = Input('in1')
+ in1 = Input("in1")
# Offset before the time window
with self.assertRaises(IndexError):
in1.tw([-5, -2], offset=-6)
@@ -1084,8 +1544,8 @@ def test_time_part(self):
in1.tw([0, 3], offset=3)
tw3, tw32 = in1.tw([-5, -2], offset=-4), in1.tw([0, 3], offset=0)
- out_tw3 = Output('in_tw3', tw3)
- out_tw32 = Output('in_tw32', tw32)
+ out_tw3 = Output("in_tw3", tw3)
+ out_tw32 = Output("in_tw32", tw32)
# Get after the window
with self.assertRaises(ValueError):
TimePart(tw3, 0, 4)
@@ -1118,69 +1578,81 @@ def test_time_part(self):
with self.assertRaises(IndexError):
TimePart(tw32, 0, 3, offset=3)
- in_TP1first = Output('in_TP1first', TimePart(tw32, 0, 1))
- in_TP1mid = Output('in_TP1mid', TimePart(tw32, 1, 2))
- in_TP1last = Output('in_TP1last', TimePart(tw32, 2, 3))
- in_TP1all = Output('in_TP1all', TimePart(tw32, 0, 3))
- in_TP1off1 = Output('in_TP1off1', TimePart(tw32, 0, 3, offset=0))
- in_TP1off2 = Output('in_TP1off2', TimePart(tw32, 0, 3, offset=1))
- in_TP1off3 = Output('in_TP1off3', TimePart(tw32, 0, 3, offset=2))
+ in_TP1first = Output("in_TP1first", TimePart(tw32, 0, 1))
+ in_TP1mid = Output("in_TP1mid", TimePart(tw32, 1, 2))
+ in_TP1last = Output("in_TP1last", TimePart(tw32, 2, 3))
+ in_TP1all = Output("in_TP1all", TimePart(tw32, 0, 3))
+ in_TP1off1 = Output("in_TP1off1", TimePart(tw32, 0, 3, offset=0))
+ in_TP1off2 = Output("in_TP1off2", TimePart(tw32, 0, 3, offset=1))
+ in_TP1off3 = Output("in_TP1off3", TimePart(tw32, 0, 3, offset=2))
test = Modely(visualizer=None)
- test.addModel('out',[out_tw3, out_tw32,
- in_TP1first, in_TP1mid, in_TP1last, in_TP1all, in_TP1off1, in_TP1off2, in_TP1off3])
+ test.addModel(
+ "out",
+ [
+ out_tw3,
+ out_tw32,
+ in_TP1first,
+ in_TP1mid,
+ in_TP1last,
+ in_TP1all,
+ in_TP1off1,
+ in_TP1off2,
+ in_TP1off3,
+ ],
+ )
test.neuralizeModel(1)
- results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7]})
-
- self.assertEqual((1, 3), np.array(results['in_tw3']).shape)
- self.TestAlmostEqual([[-1, 0, 1]], results['in_tw3'])
- self.assertEqual((1, 3), np.array(results['in_tw32']).shape)
- self.TestAlmostEqual([[0, 1, 2]], results['in_tw32'])
-
- self.assertEqual((1,), np.array(results['in_TP1first']).shape)
- self.TestAlmostEqual([0], results['in_TP1first'])
- self.assertEqual((1,), np.array(results['in_TP1mid']).shape)
- self.TestAlmostEqual([1], results['in_TP1mid'])
- self.assertEqual((1,), np.array(results['in_TP1last']).shape)
- self.TestAlmostEqual([2], results['in_TP1last'])
- self.assertEqual((1, 3), np.array(results['in_TP1all']).shape)
- self.TestAlmostEqual([[0, 1, 2]], results['in_TP1all'])
- self.assertEqual((1, 3), np.array(results['in_TP1off1']).shape)
- self.TestAlmostEqual([[0, 1, 2]], results['in_TP1off1'])
- self.assertEqual((1, 3), np.array(results['in_TP1off2']).shape)
- self.TestAlmostEqual([[-1, 0, 1]], results['in_TP1off2'])
- self.assertEqual((1, 3), np.array(results['in_TP1off3']).shape)
- self.TestAlmostEqual([[-2, -1, 0]], results['in_TP1off3'])
-
- results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7, 10]})
-
- self.assertEqual((2, 3), np.array(results['in_tw3']).shape)
- self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results['in_tw3'])
- self.assertEqual((2, 3), np.array(results['in_tw32']).shape)
- self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_tw32'])
-
- self.assertEqual((2,), np.array(results['in_TP1first']).shape)
- self.TestAlmostEqual([0, 0], results['in_TP1first'])
- self.assertEqual((2,), np.array(results['in_TP1mid']).shape)
- self.TestAlmostEqual([1, 1], results['in_TP1mid'])
- self.assertEqual((2,), np.array(results['in_TP1last']).shape)
- self.TestAlmostEqual([2, 4], results['in_TP1last'])
- self.assertEqual((2, 3), np.array(results['in_TP1all']).shape)
- self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_TP1all'])
- self.assertEqual((2, 3), np.array(results['in_TP1off1']).shape)
- self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_TP1off1'])
- self.assertEqual((2, 3), np.array(results['in_TP1off2']).shape)
- self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 3]], results['in_TP1off2'])
- self.assertEqual((2, 3), np.array(results['in_TP1off3']).shape)
- self.TestAlmostEqual([[-2, -1, 0], [-4, -3, 0]], results['in_TP1off3'])
-
+ results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7]})
+
+ self.assertEqual((1, 3), np.array(results["in_tw3"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_tw3"])
+ self.assertEqual((1, 3), np.array(results["in_tw32"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_tw32"])
+
+ self.assertEqual((1,), np.array(results["in_TP1first"]).shape)
+ self.TestAlmostEqual([0], results["in_TP1first"])
+ self.assertEqual((1,), np.array(results["in_TP1mid"]).shape)
+ self.TestAlmostEqual([1], results["in_TP1mid"])
+ self.assertEqual((1,), np.array(results["in_TP1last"]).shape)
+ self.TestAlmostEqual([2], results["in_TP1last"])
+ self.assertEqual((1, 3), np.array(results["in_TP1all"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_TP1all"])
+ self.assertEqual((1, 3), np.array(results["in_TP1off1"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_TP1off1"])
+ self.assertEqual((1, 3), np.array(results["in_TP1off2"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_TP1off2"])
+ self.assertEqual((1, 3), np.array(results["in_TP1off3"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0]], results["in_TP1off3"])
+
+ results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7, 10]})
+
+ self.assertEqual((2, 3), np.array(results["in_tw3"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_tw3"])
+ self.assertEqual((2, 3), np.array(results["in_tw32"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_tw32"])
+
+ self.assertEqual((2,), np.array(results["in_TP1first"]).shape)
+ self.TestAlmostEqual([0, 0], results["in_TP1first"])
+ self.assertEqual((2,), np.array(results["in_TP1mid"]).shape)
+ self.TestAlmostEqual([1, 1], results["in_TP1mid"])
+ self.assertEqual((2,), np.array(results["in_TP1last"]).shape)
+ self.TestAlmostEqual([2, 4], results["in_TP1last"])
+ self.assertEqual((2, 3), np.array(results["in_TP1all"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_TP1all"])
+ self.assertEqual((2, 3), np.array(results["in_TP1off1"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_TP1off1"])
+ self.assertEqual((2, 3), np.array(results["in_TP1off2"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 3]], results["in_TP1off2"])
+ self.assertEqual((2, 3), np.array(results["in_TP1off3"]).shape)
+ self.TestAlmostEqual([[-2, -1, 0], [-4, -3, 0]], results["in_TP1off3"])
+
def test_part_and_select(self):
NeuObj.clearNames()
- in1 = Input('in1',dimensions=4)
+ in1 = Input("in1", dimensions=4)
tw3, tw32 = in1.tw([-5, -2], offset=-4), in1.tw([0, 3], offset=0)
- out_tw3 = Output('in_tw3', tw3)
- out_tw32 = Output('in_tw32', tw32)
+ out_tw3 = Output("in_tw3", tw3)
+ out_tw32 = Output("in_tw32", tw32)
# Get after the window
with self.assertRaises(IndexError):
Part(tw3, 0, 5)
@@ -1200,10 +1672,10 @@ def test_part_and_select(self):
with self.assertRaises(IndexError):
Part(tw32, -1, 0)
- in_P1first = Output('in_P1first', Part(tw32, 0, 1))
- in_P1mid = Output('in_P1mid', Part(tw32, 1, 2))
- in_P1last = Output('in_P1last', Part(tw32, 2, 4))
- in_P1all = Output('in_P1all', Part(tw32, 0, 4))
+ in_P1first = Output("in_P1first", Part(tw32, 0, 1))
+ in_P1mid = Output("in_P1mid", Part(tw32, 1, 2))
+ in_P1last = Output("in_P1last", Part(tw32, 2, 4))
+ in_P1all = Output("in_P1all", Part(tw32, 0, 4))
with self.assertRaises(IndexError):
Select(tw3, -1)
@@ -1213,279 +1685,395 @@ def test_part_and_select(self):
Select(tw32, -1)
with self.assertRaises(IndexError):
Select(tw32, 4)
- in_S1 = Output('in_S1', Select(tw3, 0))
- in_S2 = Output('in_S2', Select(tw3, 1))
- in_S3 = Output('in_S3', Select(tw3, 2))
- in_S4 = Output('in_S4', Select(tw3, 3))
+ in_S1 = Output("in_S1", Select(tw3, 0))
+ in_S2 = Output("in_S2", Select(tw3, 1))
+ in_S3 = Output("in_S3", Select(tw3, 2))
+ in_S4 = Output("in_S4", Select(tw3, 3))
test = Modely(visualizer=None)
- test.addModel('out',[out_tw3, out_tw32,
- in_P1first, in_P1mid, in_P1last, in_P1all,
- in_S1, in_S2, in_S3, in_S4])
+ test.addModel(
+ "out",
+ [
+ out_tw3,
+ out_tw32,
+ in_P1first,
+ in_P1mid,
+ in_P1last,
+ in_P1all,
+ in_S1,
+ in_S2,
+ in_S3,
+ in_S4,
+ ],
+ )
test.neuralizeModel(1)
- results = test({'in1': [[0,1,2,4], [1,3,4,5], [2,5,6,7], [3,3,4,1], [4,4,6,7], [5,6,7,8], [6,7,5,4],[7,2,3,1]]})
-
- self.assertEqual((1, 3, 4), np.array(results['in_tw3']).shape)
- self.TestAlmostEqual([[[-1,-2,-2,-1], [0,0,0,0], [1,2,2,2]]], results['in_tw3'])
- self.assertEqual((1, 3, 4), np.array(results['in_tw32']).shape)
- self.TestAlmostEqual([[[0,0,0,0], [1,1,-2,-4],[2,-4,-4,-7]]], results['in_tw32'])
-
- self.assertEqual((1,3), np.array(results['in_P1first']).shape)
- self.TestAlmostEqual([[0,1,2]], results['in_P1first'])
- self.assertEqual((1,3), np.array(results['in_P1mid']).shape)
- self.TestAlmostEqual([[0,1,-4]], results['in_P1mid'])
- self.assertEqual((1,3,2), np.array(results['in_P1last']).shape)
- self.TestAlmostEqual([[[0,0],[-2,-4],[-4,-7]]], results['in_P1last'])
- self.assertEqual((1,3,4), np.array(results['in_P1all']).shape)
- self.TestAlmostEqual([[[0,0,0,0], [1,1,-2,-4],[2,-4,-4,-7]]], results['in_P1all'])
-
- self.assertEqual((1,3), np.array(results['in_S1']).shape)
- self.TestAlmostEqual([[-1,0,1]], results['in_S1'])
- self.assertEqual((1,3), np.array(results['in_S2']).shape)
- self.TestAlmostEqual([[-2,0,2]], results['in_S2'])
- self.assertEqual((1,3), np.array(results['in_S3']).shape)
- self.TestAlmostEqual([[-2,0,2]], results['in_S3'])
- self.assertEqual((1,3), np.array(results['in_S4']).shape)
- self.TestAlmostEqual([[-1,0,2]], results['in_S4'])
-
- results = test({'in1': [[0,1,2,4], [1,3,4,5], [2,5,6,7], [3,3,4,1], [4,4,6,7], [5,6,7,8], [6,7,5,4],[7,2,3,1],[0,7,0,0]]})
-
- self.assertEqual((2, 3, 4), np.array(results['in_tw3']).shape)
- self.TestAlmostEqual([[[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]],
- [[-1, -2, -2, -2], [0, 0, 0, 0], [1, -2, -2, -6]]], results['in_tw3'])
- self.assertEqual((2, 3, 4), np.array(results['in_tw32']).shape)
- self.TestAlmostEqual([[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]],
- [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]]], results['in_tw32'])
-
- self.assertEqual((2, 3), np.array(results['in_P1first']).shape)
- self.TestAlmostEqual([[0, 1, 2],[0, 1, -6]], results['in_P1first'])
- self.assertEqual((2, 3), np.array(results['in_P1mid']).shape)
- self.TestAlmostEqual([[0, 1, -4],[0, -5, 0]], results['in_P1mid'])
- self.assertEqual((2, 3, 2), np.array(results['in_P1last']).shape)
- self.TestAlmostEqual([[[0, 0], [-2, -4], [-4, -7]],
- [[0, 0], [-2, -3], [-5, -4]]], results['in_P1last'])
- self.assertEqual((2, 3, 4), np.array(results['in_P1all']).shape)
- self.TestAlmostEqual([[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]],
- [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]]], results['in_P1all'])
-
- self.assertEqual((2, 3), np.array(results['in_S1']).shape)
- self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_S1'])
- self.assertEqual((2, 3), np.array(results['in_S2']).shape)
- self.TestAlmostEqual([[-2, 0, 2],[-2, 0, -2]], results['in_S2'])
- self.assertEqual((2, 3), np.array(results['in_S3']).shape)
- self.TestAlmostEqual([[-2, 0, 2],[-2, 0, -2]], results['in_S3'])
- self.assertEqual((2, 3), np.array(results['in_S4']).shape)
- self.TestAlmostEqual([[-1, 0, 2],[-2, 0, -6]], results['in_S4'])
+ results = test(
+ {
+ "in1": [
+ [0, 1, 2, 4],
+ [1, 3, 4, 5],
+ [2, 5, 6, 7],
+ [3, 3, 4, 1],
+ [4, 4, 6, 7],
+ [5, 6, 7, 8],
+ [6, 7, 5, 4],
+ [7, 2, 3, 1],
+ ]
+ }
+ )
+
+ self.assertEqual((1, 3, 4), np.array(results["in_tw3"]).shape)
+ self.TestAlmostEqual(
+ [[[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]]], results["in_tw3"]
+ )
+ self.assertEqual((1, 3, 4), np.array(results["in_tw32"]).shape)
+ self.TestAlmostEqual(
+ [[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]]], results["in_tw32"]
+ )
+
+ self.assertEqual((1, 3), np.array(results["in_P1first"]).shape)
+ self.TestAlmostEqual([[0, 1, 2]], results["in_P1first"])
+ self.assertEqual((1, 3), np.array(results["in_P1mid"]).shape)
+ self.TestAlmostEqual([[0, 1, -4]], results["in_P1mid"])
+ self.assertEqual((1, 3, 2), np.array(results["in_P1last"]).shape)
+ self.TestAlmostEqual([[[0, 0], [-2, -4], [-4, -7]]], results["in_P1last"])
+ self.assertEqual((1, 3, 4), np.array(results["in_P1all"]).shape)
+ self.TestAlmostEqual(
+ [[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]]], results["in_P1all"]
+ )
+
+ self.assertEqual((1, 3), np.array(results["in_S1"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1]], results["in_S1"])
+ self.assertEqual((1, 3), np.array(results["in_S2"]).shape)
+ self.TestAlmostEqual([[-2, 0, 2]], results["in_S2"])
+ self.assertEqual((1, 3), np.array(results["in_S3"]).shape)
+ self.TestAlmostEqual([[-2, 0, 2]], results["in_S3"])
+ self.assertEqual((1, 3), np.array(results["in_S4"]).shape)
+ self.TestAlmostEqual([[-1, 0, 2]], results["in_S4"])
+
+ results = test(
+ {
+ "in1": [
+ [0, 1, 2, 4],
+ [1, 3, 4, 5],
+ [2, 5, 6, 7],
+ [3, 3, 4, 1],
+ [4, 4, 6, 7],
+ [5, 6, 7, 8],
+ [6, 7, 5, 4],
+ [7, 2, 3, 1],
+ [0, 7, 0, 0],
+ ]
+ }
+ )
+
+ self.assertEqual((2, 3, 4), np.array(results["in_tw3"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]],
+ [[-1, -2, -2, -2], [0, 0, 0, 0], [1, -2, -2, -6]],
+ ],
+ results["in_tw3"],
+ )
+ self.assertEqual((2, 3, 4), np.array(results["in_tw32"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]],
+ [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]],
+ ],
+ results["in_tw32"],
+ )
+
+ self.assertEqual((2, 3), np.array(results["in_P1first"]).shape)
+ self.TestAlmostEqual([[0, 1, 2], [0, 1, -6]], results["in_P1first"])
+ self.assertEqual((2, 3), np.array(results["in_P1mid"]).shape)
+ self.TestAlmostEqual([[0, 1, -4], [0, -5, 0]], results["in_P1mid"])
+ self.assertEqual((2, 3, 2), np.array(results["in_P1last"]).shape)
+ self.TestAlmostEqual(
+ [[[0, 0], [-2, -4], [-4, -7]], [[0, 0], [-2, -3], [-5, -4]]],
+ results["in_P1last"],
+ )
+ self.assertEqual((2, 3, 4), np.array(results["in_P1all"]).shape)
+ self.TestAlmostEqual(
+ [
+ [[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]],
+ [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]],
+ ],
+ results["in_P1all"],
+ )
+
+ self.assertEqual((2, 3), np.array(results["in_S1"]).shape)
+ self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_S1"])
+ self.assertEqual((2, 3), np.array(results["in_S2"]).shape)
+ self.TestAlmostEqual([[-2, 0, 2], [-2, 0, -2]], results["in_S2"])
+ self.assertEqual((2, 3), np.array(results["in_S3"]).shape)
+ self.TestAlmostEqual([[-2, 0, 2], [-2, 0, -2]], results["in_S3"])
+ self.assertEqual((2, 3), np.array(results["in_S4"]).shape)
+ self.TestAlmostEqual([[-1, 0, 2], [-2, 0, -6]], results["in_S4"])
def test_predict_paramfun_param_const(self):
NeuObj.clearNames()
- input2 = Input('in2')
- pp = Parameter('pp', values=[[7],[8],[9]])
- ll = Constant('ll', values=[[12],[13],[14]])
- oo = Constant('oo', values=[[1],[2],[3]])
+ input2 = Input("in2")
+ pp = Parameter("pp", values=[[7], [8], [9]])
+ ll = Constant("ll", values=[[12], [13], [14]])
+ oo = Constant("oo", values=[[1], [2], [3]])
pp, oo, input2.tw(0.03), ll
+
def fun_test(x, y, z, k):
return (x + y) * (z - k)
NeuObj.clearNames()
- out = Output('out',ParamFun(fun_test,parameters_and_constants=[ll,oo,pp])(input2.tw(0.03)))
+ out = Output(
+ "out",
+ ParamFun(fun_test, parameters_and_constants=[ll, oo, pp])(input2.tw(0.03)),
+ )
test = Modely(visualizer=None)
- test.addModel('out',[out])
+ test.addModel("out", [out])
test.neuralizeModel(0.01)
- results = test({'in2': [0, 1, 2]})
- self.assertEqual((1, 3), np.array(results['out']).shape)
- self.assertEqual([[-72.0, -84.0, -96.0]], results['out'])
+ results = test({"in2": [0, 1, 2]})
+ self.assertEqual((1, 3), np.array(results["out"]).shape)
+ self.assertEqual([[-72.0, -84.0, -96.0]], results["out"])
NeuObj.clearNames()
- out = Output('out',ParamFun(fun_test,parameters_and_constants={'z':pp,'y':ll,'k':oo})(input2.tw(0.03)))
+ out = Output(
+ "out",
+ ParamFun(fun_test, parameters_and_constants={"z": pp, "y": ll, "k": oo})(
+ input2.tw(0.03)
+ ),
+ )
test = Modely(visualizer=None)
- test.addModel('out',[out])
+ test.addModel("out", [out])
test.neuralizeModel(0.01)
- results = test({'in2': [0, 1, 2]})
- self.assertEqual((1, 3), np.array(results['out']).shape)
- self.assertEqual([[72.0, 84.0, 96.0]], results['out'])
+ results = test({"in2": [0, 1, 2]})
+ self.assertEqual((1, 3), np.array(results["out"]).shape)
+ self.assertEqual([[72.0, 84.0, 96.0]], results["out"])
NeuObj.clearNames()
parfun = ParamFun(fun_test)
- out1 = Output('out1', parfun(input2.tw(0.03), ll, pp, oo))
- out2 = Output('out2', parfun(input2.tw(0.03), ll, oo, pp))
- out3 = Output('out3', parfun(pp, oo, input2.tw(0.03), ll))
+ out1 = Output("out1", parfun(input2.tw(0.03), ll, pp, oo))
+ out2 = Output("out2", parfun(input2.tw(0.03), ll, oo, pp))
+ out3 = Output("out3", parfun(pp, oo, input2.tw(0.03), ll))
test = Modely(visualizer=None)
- test.addModel('out',[out1,out2,out3])
+ test.addModel("out", [out1, out2, out3])
test.neuralizeModel(0.01)
- results = test({'in2': [0, 1, 2]})
- self.assertEqual((1, 3), np.array(results['out1']).shape)
- self.assertEqual((1, 3), np.array(results['out2']).shape)
- self.assertEqual((1, 3), np.array(results['out3']).shape)
- self.assertEqual([[72.0, 84.0, 96.0]], results['out1'])
- self.assertEqual([[-72.0, -84.0, -96.0]], results['out2'])
- self.assertEqual([[-96.0, -120.0, -144.0]], results['out3'])
+ results = test({"in2": [0, 1, 2]})
+ self.assertEqual((1, 3), np.array(results["out1"]).shape)
+ self.assertEqual((1, 3), np.array(results["out2"]).shape)
+ self.assertEqual((1, 3), np.array(results["out3"]).shape)
+ self.assertEqual([[72.0, 84.0, 96.0]], results["out1"])
+ self.assertEqual([[-72.0, -84.0, -96.0]], results["out2"])
+ self.assertEqual([[-96.0, -120.0, -144.0]], results["out3"])
def test_predict_paramfun_map_over_batch(self):
NeuObj.clearNames()
- input2 = Input('in2')
- pp = Parameter('pp', sw=3, values=[[7],[8],[9]])
- ll = Constant('ll', sw=3, values=[[12],[13],[14]])
- oo = Constant('oo', sw=3, values=[[1],[2],[3]])
+ input2 = Input("in2")
+ pp = Parameter("pp", sw=3, values=[[7], [8], [9]])
+ ll = Constant("ll", sw=3, values=[[12], [13], [14]])
+ oo = Constant("oo", sw=3, values=[[1], [2], [3]])
def fun_test(x, y, z, k):
return (x + y) * (z - k)
- fun_map = ParamFun(fun_test,parameters_and_constants=[ll,oo, pp])
- fun = ParamFun(fun_test, parameters_and_constants=[ll,oo, pp])
+ fun_map = ParamFun(fun_test, parameters_and_constants=[ll, oo, pp])
+ fun = ParamFun(fun_test, parameters_and_constants=[ll, oo, pp])
fun_map_2 = ParamFun(fun_test, map_over_batch=True)
- out1 = Output('out1',fun_map(input2.tw(0.03)))
- out2 = Output('out2', fun(input2.tw(0.03)))
+ out1 = Output("out1", fun_map(input2.tw(0.03)))
+ out2 = Output("out2", fun(input2.tw(0.03)))
test = Modely(visualizer=None)
- test.addModel('out',[out1,out2])
+ test.addModel("out", [out1, out2])
test.neuralizeModel(0.01)
- results = test({'in2': [0, 1, 2]})
- self.assertEqual((1, 3), np.array(results['out1']).shape)
- self.assertEqual([[-72.0, -84.0, -96.0]], results['out1'])
- self.assertEqual((1, 3), np.array(results['out2']).shape)
- self.assertEqual([[-72.0, -84.0, -96.0]], results['out2'])
-
- out3 = Output('out3', fun_map_2(input2.tw(0.03), 4.0, pp, ll))
- out4 = Output('out4', fun_map_2(input2.tw(0.01), 2.0, pp, oo))
+ results = test({"in2": [0, 1, 2]})
+ self.assertEqual((1, 3), np.array(results["out1"]).shape)
+ self.assertEqual([[-72.0, -84.0, -96.0]], results["out1"])
+ self.assertEqual((1, 3), np.array(results["out2"]).shape)
+ self.assertEqual([[-72.0, -84.0, -96.0]], results["out2"])
+
+ out3 = Output("out3", fun_map_2(input2.tw(0.03), 4.0, pp, ll))
+ out4 = Output("out4", fun_map_2(input2.tw(0.01), 2.0, pp, oo))
with self.assertRaises(ValueError):
fun_map_2(4.0, 1, pp, ll)
- test.addModel('out-new', [out3,out4])
+ test.addModel("out-new", [out3, out4])
with self.assertRaises(NameError):
- test.addModel('out',[out1,out2])
+ test.addModel("out", [out1, out2])
test.neuralizeModel(0.01)
- results = test({'in2': [0, 1, 2]})
- self.assertEqual((1, 3), np.array(results['out3']).shape)
- self.assertEqual((1, 3), np.array(results['out4']).shape)
+ results = test({"in2": [0, 1, 2]})
+ self.assertEqual((1, 3), np.array(results["out3"]).shape)
+ self.assertEqual((1, 3), np.array(results["out4"]).shape)
# ([0,1,2]+4)*([7,8,9]-[12,13,14]) -> [4,5,6]*[-5,-5,-5]
- self.assertEqual([[-20.0, -25.0, -30.0]], results['out3'])
- self.assertEqual([[24.0, 24.0, 24.0]], results['out4'])
+ self.assertEqual([[-20.0, -25.0, -30.0]], results["out3"])
+ self.assertEqual([[24.0, 24.0, 24.0]], results["out4"])
def test_predict_fuzzify(self):
NeuObj.clearNames()
- input = Input('in1')
- fuzzi = Fuzzify(6, range=[0, 5], functions='Rectangular')(input.last())
- out = Output('out', fuzzi)
+ input = Input("in1")
+ fuzzi = Fuzzify(6, range=[0, 5], functions="Rectangular")(input.last())
+ out = Output("out", fuzzi)
test = Modely(visualizer=None)
- test.addModel('out',[out])
+ test.addModel("out", [out])
test.neuralizeModel()
- results = test({'in1': [0, 1, 2]})
- self.assertEqual((3, 1, 6), np.array(results['out']).shape)
- self.assertEqual([[[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]],[[0.0, 1.0, 0.0, 0.0, 0.0, 0.0]],[[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]]], results['out'])
+ results = test({"in1": [0, 1, 2]})
+ self.assertEqual((3, 1, 6), np.array(results["out"]).shape)
+ self.assertEqual(
+ [
+ [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]],
+ [[0.0, 1.0, 0.0, 0.0, 0.0, 0.0]],
+ [[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]],
+ ],
+ results["out"],
+ )
def fun(x):
import torch
+
return torch.sign(x)
- fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=[fun,fun])(input.last())
- out = Output('out2', fuz)
- test.addModel('out2',[out])
+ fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=[fun, fun])(
+ input.last()
+ )
+ out = Output("out2", fuz)
+ test.addModel("out2", [out])
test.neuralizeModel()
- results = test({'in1': [0, 1, 2]})
- self.assertEqual((3, 1, 11), np.array(results['out2']).shape)
- self.assertEqual([[[1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0]],
- [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0]],
- [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0]]], results['out2'])
+ results = test({"in1": [0, 1, 2]})
+ self.assertEqual((3, 1, 11), np.array(results["out2"]).shape)
+ self.assertEqual(
+ [
+ [[1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0]],
+ [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0]],
+ [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0]],
+ ],
+ results["out2"],
+ )
def test_sw_on_stream_sw_by_heand(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
sw_from_input = input.sw(7)
- state = Input('state')
+ state = Input("state")
sw_from_output = Connect(sw_from_input, state)
- out_aux = Output('out_aux', sw_from_output)
- out1 = Output('out1', state.sw(3))
+ out_aux = Output("out_aux", sw_from_output)
+ out1 = Output("out1", state.sw(3))
test = Modely(visualizer=None)
- test.addModel('out_A', [out_aux,out1])
+ test.addModel("out_A", [out_aux, out1])
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4, 3), np.array(results['out1']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1'])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 3), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out1"],
+ )
NeuObj.clearNames()
- state = Input('state')
- out_aux = Output('out_aux', sw_from_input)
- out1 = Output('out1', state.sw(3))
+ state = Input("state")
+ out_aux = Output("out_aux", sw_from_input)
+ out1 = Output("out1", state.sw(3))
test = Modely(visualizer=None)
- test.addModel('out_A', [out_aux,out1])
+ test.addModel("out_A", [out_aux, out1])
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={'state': 'out_aux'})
+ results = test(
+ {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={"state": "out_aux"}
+ )
with self.assertRaises(ValueError):
- test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={'out_aux': 'state'})
- self.assertEqual((4, 3), np.array(results['out1']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1'])
+ test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={"out_aux": "state"})
+ self.assertEqual((4, 3), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out1"],
+ )
def test_sw_on_stream_sw(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
sw_from_input = input.sw(7)
- out1 = Output('out1', sw_from_input.sw(3))
+ out1 = Output("out1", sw_from_input.sw(3))
test = Modely(visualizer=None)
- test.addModel('out_A', out1)
+ test.addModel("out_A", out1)
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4, 3), np.array(results['out1']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1'])
-
- NeuObj.clearNames(['out1','out2'])
- out1 = Output('out1', sw_from_input.sw(3))
- out2 = Output('out2', sw_from_input.sw(8))
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 3), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out1"],
+ )
+
+ NeuObj.clearNames(["out1", "out2"])
+ out1 = Output("out1", sw_from_input.sw(3))
+ out2 = Output("out2", sw_from_input.sw(8))
with self.assertRaises(ValueError):
- Output('out3', sw_from_input.sw(-1))
+ Output("out3", sw_from_input.sw(-1))
test = Modely(visualizer=None)
- test.addModel('out_A', [out1,out2])
+ test.addModel("out_A", [out1, out2])
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4, 3), np.array(results['out1']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1'])
- self.assertEqual((4, 8), np.array(results['out2']).shape)
- self.assertEqual([[0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
- [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
- [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
- [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]], results['out2'])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 3), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out1"],
+ )
+ self.assertEqual((4, 8), np.array(results["out2"]).shape)
+ self.assertEqual(
+ [
+ [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
+ [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
+ ],
+ results["out2"],
+ )
def test_tw_on_stream_tw(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
tw_from_input = input.tw(3.5)
- out1 = Output('out1', tw_from_input.tw(1.5))
+ out1 = Output("out1", tw_from_input.tw(1.5))
test = Modely(visualizer=None)
- test.addModel('out_A', out1)
+ test.addModel("out_A", out1)
test.neuralizeModel(0.5)
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4, 3), np.array(results['out1']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1'])
-
- out1 = Output('out11', tw_from_input.tw(1.5))
- out2 = Output('out21', tw_from_input.tw(4))
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 3), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out1"],
+ )
+
+ out1 = Output("out11", tw_from_input.tw(1.5))
+ out2 = Output("out21", tw_from_input.tw(4))
with self.assertRaises(ValueError):
- Output('out3', tw_from_input.tw(-1))
+ Output("out3", tw_from_input.tw(-1))
test = Modely(visualizer=None)
- test.addModel('out_A', [out1,out2])
+ test.addModel("out_A", [out1, out2])
test.neuralizeModel(0.5)
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4, 3), np.array(results['out11']).shape)
- self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out11'])
- self.assertEqual((4, 8), np.array(results['out21']).shape)
- self.assertEqual([[0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
- [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
- [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
- [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]], results['out21'])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 3), np.array(results["out11"]).shape)
+ self.assertEqual(
+ [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]],
+ results["out11"],
+ )
+ self.assertEqual((4, 8), np.array(results["out21"]).shape)
+ self.assertEqual(
+ [
+ [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
+ [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
+ ],
+ results["out21"],
+ )
def test_sw_on_stream_sw_complex(self):
NeuObj.clearNames()
- input = Input('in1')
- state = Input('state')
+ input = Input("in1")
+ state = Input("state")
sw_3 = input.sw(3)
sw_7 = input.sw(7)
@@ -1493,200 +2081,317 @@ def test_sw_on_stream_sw_complex(self):
state4 = state.sw(4)
state8 = state.sw(8)
- out21 = Output('out21', sw_3.sw(2))
- out61 = Output('out61', sw_7.sw(6))
- out22 = Output('out22', SamplePart(sw_3,1,3))
- out62 = Output('out62', SamplePart(sw_7,1,7))
+ out21 = Output("out21", sw_3.sw(2))
+ out61 = Output("out61", sw_7.sw(6))
+ out22 = Output("out22", SamplePart(sw_3, 1, 3))
+ out62 = Output("out62", SamplePart(sw_7, 1, 7))
test = Modely(visualizer=None)
- test.addModel('out_A', [out21,out61,out22,out62])
+ test.addModel("out_A", [out21, out61, out22, out62])
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual(results['out21'], results['out22'])
- self.assertEqual(results['out61'], results['out62'])
-
- out31 = Output('out31', sw_3)
- out71 = Output('out71', sw_7)
- out32 = Output('out32', SamplePart(state4,1,4))
- out72 = Output('out72', SamplePart(state8,1,8))
- out33 = Output('out33', sw_3.sw(3))
- out73 = Output('out73', sw_7.sw(7))
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(results["out21"], results["out22"])
+ self.assertEqual(results["out61"], results["out62"])
+
+ out31 = Output("out31", sw_3)
+ out71 = Output("out71", sw_7)
+ out32 = Output("out32", SamplePart(state4, 1, 4))
+ out72 = Output("out72", SamplePart(state8, 1, 8))
+ out33 = Output("out33", sw_3.sw(3))
+ out73 = Output("out73", sw_7.sw(7))
test = Modely(visualizer=None)
- test.addModel('out_B', [out31,out71,out32,out72,out33,out73])
+ test.addModel("out_B", [out31, out71, out32, out72, out33, out73])
test.addConnect(sw_7, state)
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual(results['out31'], results['out32'])
- self.assertEqual(results['out32'], results['out33'])
- self.assertEqual(results['out71'], results['out72'])
- self.assertEqual(results['out72'], results['out73'])
-
- out41 = Output('out41', state4)
- out42 = Output('out42', sw_3.sw(4))
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(results["out31"], results["out32"])
+ self.assertEqual(results["out32"], results["out33"])
+ self.assertEqual(results["out71"], results["out72"])
+ self.assertEqual(results["out72"], results["out73"])
+
+ out41 = Output("out41", state4)
+ out42 = Output("out42", sw_3.sw(4))
test = Modely(visualizer=None)
- test.addModel('out_C', [out41,out42])
+ test.addModel("out_C", [out41, out42])
test.addConnect(sw_3, state)
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual(results['out41'], results['out42'])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(results["out41"], results["out42"])
def test_sw_on_stream_tw_and_opposite(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
sw_3 = input.sw(3)
tw_4 = input.tw(1)
- out3tw1 = Output('out3tw1', sw_3.tw(0.2))
- out3tw10 = Output('out3tw10', sw_3.tw(2))
- out4sw2 = Output('out4sw2', tw_4.sw(2))
- out4sw6 = Output('out4sw6', tw_4.sw(6))
+ out3tw1 = Output("out3tw1", sw_3.tw(0.2))
+ out3tw10 = Output("out3tw10", sw_3.tw(2))
+ out4sw2 = Output("out4sw2", tw_4.sw(2))
+ out4sw6 = Output("out4sw6", tw_4.sw(6))
test = Modely(visualizer=None)
- test.addModel('out_A', [out3tw1, out3tw10, out4sw2, out4sw6])
+ test.addModel("out_A", [out3tw1, out3tw10, out4sw2, out4sw6])
test.neuralizeModel(0.2)
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9, -3]})
- self.assertEqual(results['out3tw1'], [4, 5, 6, 7, 8, 9, -3])
- self.assertEqual(results['out3tw10'], [[0, 0, 0, 0, 0, 0, 0, 2, 3, 4],
- [0, 0, 0, 0, 0,0, 2, 3, 4, 5],
- [0, 0, 0, 0, 0, 2, 3, 4 ,5, 6],
- [0, 0, 0, 0, 2, 3, 4, 5, 6, 7],
- [0, 0, 0, 2, 3, 4, 5, 6, 7, 8],
- [0, 0, 2, 3, 4, 5, 6, 7, 8, 9],
- [0, 2, 3, 4, 5, 6, 7, 8, 9, -3]])
- self.assertEqual(results['out4sw2'], [[3, 4],[4,5],[5,6],[6,7],[7,8],[8,9], [9,-3]])
- self.assertEqual(results['out4sw6'], [[0, 14, 1, 2, 3, 4],
- [14, 1, 2, 3, 4, 5],
- [1, 2, 3, 4 ,5, 6],
- [2, 3, 4, 5, 6, 7],
- [3, 4, 5, 6, 7, 8],
- [4, 5, 6, 7, 8, 9],
- [5, 6, 7, 8, 9, -3]])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9, -3]})
+ self.assertEqual(results["out3tw1"], [4, 5, 6, 7, 8, 9, -3])
+ self.assertEqual(
+ results["out3tw10"],
+ [
+ [0, 0, 0, 0, 0, 0, 0, 2, 3, 4],
+ [0, 0, 0, 0, 0, 0, 2, 3, 4, 5],
+ [0, 0, 0, 0, 0, 2, 3, 4, 5, 6],
+ [0, 0, 0, 0, 2, 3, 4, 5, 6, 7],
+ [0, 0, 0, 2, 3, 4, 5, 6, 7, 8],
+ [0, 0, 2, 3, 4, 5, 6, 7, 8, 9],
+ [0, 2, 3, 4, 5, 6, 7, 8, 9, -3],
+ ],
+ )
+ self.assertEqual(
+ results["out4sw2"],
+ [[3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, -3]],
+ )
+ self.assertEqual(
+ results["out4sw6"],
+ [
+ [0, 14, 1, 2, 3, 4],
+ [14, 1, 2, 3, 4, 5],
+ [1, 2, 3, 4, 5, 6],
+ [2, 3, 4, 5, 6, 7],
+ [3, 4, 5, 6, 7, 8],
+ [4, 5, 6, 7, 8, 9],
+ [5, 6, 7, 8, 9, -3],
+ ],
+ )
def test_sw_on_stream_sw_delay(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
sw_3 = input.sw(3)
- out21 = Output('out21', sw_3.sw(2))
- out41 = Output('out41', sw_3.sw(4))
- out22 = Output('out22', sw_3.sw([-2,0]))
- out42 = Output('out42', sw_3.sw([-4,0]))
+ out21 = Output("out21", sw_3.sw(2))
+ out41 = Output("out41", sw_3.sw(4))
+ out22 = Output("out22", sw_3.sw([-2, 0]))
+ out42 = Output("out42", sw_3.sw([-4, 0]))
- out231 = Output('out231', sw_3.sw([-3,-1]))
- out451 = Output('out451', sw_3.sw([-5,-1]))
+ out231 = Output("out231", sw_3.sw([-3, -1]))
+ out451 = Output("out451", sw_3.sw([-5, -1]))
- out242 = Output('out242', sw_3.sw([-4,-2]))
- out462 = Output('out462', sw_3.sw([-6,-2]))
+ out242 = Output("out242", sw_3.sw([-4, -2]))
+ out462 = Output("out462", sw_3.sw([-6, -2]))
test = Modely(visualizer=None)
- test.addModel('out_A', [out21,out41,out22,out42,out231,out451,out242,out462])
+ test.addModel(
+ "out_A", [out21, out41, out22, out42, out231, out451, out242, out462]
+ )
test.neuralizeModel()
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual(results['out21'], results['out22'])
- self.assertEqual(results['out21'], [[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]])
-
- self.assertEqual(results['out41'], results['out42'])
- self.assertEqual(results['out41'], [[0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9]])
-
- self.assertEqual(results['out231'], [[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]])
- self.assertEqual(results['out451'], [[0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8]])
-
- self.assertEqual(results['out242'], [[0,14],[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7]])
- self.assertEqual(results['out462'], [[0, 0, 0, 14], [0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(results["out21"], results["out22"])
+ self.assertEqual(
+ results["out21"],
+ [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9]],
+ )
+
+ self.assertEqual(results["out41"], results["out42"])
+ self.assertEqual(
+ results["out41"],
+ [
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ [5, 6, 7, 8],
+ [6, 7, 8, 9],
+ ],
+ )
+
+ self.assertEqual(
+ results["out231"],
+ [[14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]],
+ )
+ self.assertEqual(
+ results["out451"],
+ [
+ [0, 0, 14, 1],
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ [5, 6, 7, 8],
+ ],
+ )
+
+ self.assertEqual(
+ results["out242"],
+ [[0, 14], [14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]],
+ )
+ self.assertEqual(
+ results["out462"],
+ [
+ [0, 0, 0, 14],
+ [0, 0, 14, 1],
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ ],
+ )
def test_tw_on_stream_tw_delay(self):
NeuObj.clearNames()
- input = Input('in1')
+ input = Input("in1")
tw_3 = input.tw(1.5)
- out21 = Output('out21', tw_3.tw(1))
- out41 = Output('out41', tw_3.tw(2))
- out22 = Output('out22', tw_3.tw([-1,0]))
- out42 = Output('out42', tw_3.tw([-2,0]))
+ out21 = Output("out21", tw_3.tw(1))
+ out41 = Output("out41", tw_3.tw(2))
+ out22 = Output("out22", tw_3.tw([-1, 0]))
+ out42 = Output("out42", tw_3.tw([-2, 0]))
- out231 = Output('out231', tw_3.tw([-1.5,-0.5]))
- out451 = Output('out451', tw_3.tw([-2.5,-0.5]))
+ out231 = Output("out231", tw_3.tw([-1.5, -0.5]))
+ out451 = Output("out451", tw_3.tw([-2.5, -0.5]))
- out242 = Output('out242', tw_3.tw([-2,-1]))
- out462 = Output('out462', tw_3.tw([-3,-1]))
+ out242 = Output("out242", tw_3.tw([-2, -1]))
+ out462 = Output("out462", tw_3.tw([-3, -1]))
test = Modely(visualizer=None)
- test.addModel('out_A', [out21,out41,out22,out42,out231,out451,out242,out462])
+ test.addModel(
+ "out_A", [out21, out41, out22, out42, out231, out451, out242, out462]
+ )
test.neuralizeModel(0.5)
- results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual(results['out21'], results['out22'])
- self.assertEqual(results['out21'], [[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]])
-
- self.assertEqual(results['out41'], results['out42'])
- self.assertEqual(results['out41'], [[0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9]])
-
- self.assertEqual(results['out231'], [[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]])
- self.assertEqual(results['out451'], [[0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8]])
-
- self.assertEqual(results['out242'], [[0,14],[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7]])
- self.assertEqual(results['out462'], [[0, 0, 0, 14], [0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]])
+ results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(results["out21"], results["out22"])
+ self.assertEqual(
+ results["out21"],
+ [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9]],
+ )
+
+ self.assertEqual(results["out41"], results["out42"])
+ self.assertEqual(
+ results["out41"],
+ [
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ [5, 6, 7, 8],
+ [6, 7, 8, 9],
+ ],
+ )
+
+ self.assertEqual(
+ results["out231"],
+ [[14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]],
+ )
+ self.assertEqual(
+ results["out451"],
+ [
+ [0, 0, 14, 1],
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ [5, 6, 7, 8],
+ ],
+ )
+
+ self.assertEqual(
+ results["out242"],
+ [[0, 14], [14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]],
+ )
+ self.assertEqual(
+ results["out462"],
+ [
+ [0, 0, 0, 14],
+ [0, 0, 14, 1],
+ [0, 14, 1, 2],
+ [14, 1, 2, 3],
+ [1, 2, 3, 4],
+ [2, 3, 4, 5],
+ [3, 4, 5, 6],
+ [4, 5, 6, 7],
+ ],
+ )
def test_z_on_stream_sw(self):
NeuObj.clearNames()
- input = Input('inin')
+ input = Input("inin")
sw_from_input = input.sw(5)
- out2 = Output('out2', sw_from_input.z(1))
+ out2 = Output("out2", sw_from_input.z(1))
with self.assertRaises(ValueError):
- Output('out3', sw_from_input.z(-1))
+ Output("out3", sw_from_input.z(-1))
with self.assertRaises(TypeError):
- Output('out3', sw_from_input.delay(1))
+ Output("out3", sw_from_input.delay(1))
test = Modely(visualizer=None)
- test.addModel('out_A', [out2])
+ test.addModel("out_A", [out2])
test.neuralizeModel(0.5)
- results = test({'inin': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((6, 5), np.array(results['out2']).shape)
- self.assertEqual( [[0.0, 14.0, 1.0, 2.0, 3.0],
- [14.0, 1.0, 2.0, 3.0, 4.0],
- [1.0, 2.0, 3.0, 4.0, 5.0],
- [2.0, 3.0, 4.0, 5.0, 6.0],
- [3.0, 4.0, 5.0, 6.0, 7.0],
- [4.0, 5.0, 6.0, 7.0, 8.0]], results['out2'])
+ results = test({"inin": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((6, 5), np.array(results["out2"]).shape)
+ self.assertEqual(
+ [
+ [0.0, 14.0, 1.0, 2.0, 3.0],
+ [14.0, 1.0, 2.0, 3.0, 4.0],
+ [1.0, 2.0, 3.0, 4.0, 5.0],
+ [2.0, 3.0, 4.0, 5.0, 6.0],
+ [3.0, 4.0, 5.0, 6.0, 7.0],
+ [4.0, 5.0, 6.0, 7.0, 8.0],
+ ],
+ results["out2"],
+ )
def test_delay_on_stream_tw(self):
NeuObj.clearNames()
- input = Input('inin')
+ input = Input("inin")
tw_from_input = input.tw(3.5)
- out1 = Output('out1', tw_from_input.delay(1))
+ out1 = Output("out1", tw_from_input.delay(1))
with self.assertRaises(ValueError):
- Output('out3', tw_from_input.delay(-1))
+ Output("out3", tw_from_input.delay(-1))
with self.assertRaises(TypeError):
- Output('out3', tw_from_input.z(1))
+ Output("out3", tw_from_input.z(1))
test = Modely(visualizer=None)
- test.addModel('out_A', [out1])
+ test.addModel("out_A", [out1])
test.neuralizeModel(0.5)
- results = test({'inin': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
- self.assertEqual((4,7), np.array(results['out1']).shape)
- self.assertEqual([[0.0, 0.0, 14.0, 1.0, 2.0, 3.0, 4.0],
- [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0],
- [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
- [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]], results['out1'])
- #TODO add test with initialization of state variable
+ results = test({"inin": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual((4, 7), np.array(results["out1"]).shape)
+ self.assertEqual(
+ [
+ [0.0, 0.0, 14.0, 1.0, 2.0, 3.0, 4.0],
+ [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0],
+ [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
+ ],
+ results["out1"],
+ )
+ # TODO add test with initialization of state variable
def test_localmodel(self):
NeuObj.clearNames()
- x = Input('x')
- F = Input('F')
- activationA = Fuzzify(2, [0, 1], functions='Triangular')(x.tw(1))
- activationB = Fuzzify(2, [0, 1], functions='Triangular')(F.tw(1))
+ x = Input("x")
+ F = Input("F")
+ activationA = Fuzzify(2, [0, 1], functions="Triangular")(x.tw(1))
+ activationB = Fuzzify(2, [0, 1], functions="Triangular")(F.tw(1))
def myFun(in1, p1, p2):
return p1 * in1 + p2
- p1_0 = Parameter('p1_0', values=[[1]])
- p1_1 = Parameter('p1_1', values=[[2]])
- p2_0 = Parameter('p2_0', values=[[2]])
- p2_1 = Parameter('p2_1', values=[[3]])
+ p1_0 = Parameter("p1_0", values=[[1]])
+ p1_1 = Parameter("p1_1", values=[[2]])
+ p2_0 = Parameter("p2_0", values=[[2]])
+ p2_1 = Parameter("p2_1", values=[[3]])
def input_function_gen(idx_list):
if idx_list == [0, 0]:
@@ -1700,248 +2405,470 @@ def input_function_gen(idx_list):
return ParamFun(myFun, parameters_and_constants=[p1, p2])
def output_function_gen(idx_list):
- pfir = Parameter('pfir_' + str(idx_list), tw=1, dimensions=2,
- values=[[1 + idx_list[0], 2 + idx_list[1]], [3 + idx_list[0], 4 + idx_list[1]]])
+ pfir = Parameter(
+ "pfir_" + str(idx_list),
+ tw=1,
+ dimensions=2,
+ values=[
+ [1 + idx_list[0], 2 + idx_list[1]],
+ [3 + idx_list[0], 4 + idx_list[1]],
+ ],
+ )
return Fir(2, W=pfir)
- loc = LocalModel(input_function=input_function_gen, output_function=output_function_gen, pass_indexes=True)(x.tw(1), (activationA, activationB))
+ loc = LocalModel(
+ input_function=input_function_gen,
+ output_function=output_function_gen,
+ pass_indexes=True,
+ )(x.tw(1), (activationA, activationB))
# Example of the structure of the local model
- pfir00 = Parameter('N_pfir_[0, 0]', tw=1, dimensions=2, values=[[1, 2], [3, 4]])
- pfir01 = Parameter('N_pfir_[0, 1]', tw=1, dimensions=2, values=[[1, 3], [3, 5]])
- pfir10 = Parameter('N_pfir_[1, 0]', tw=1, dimensions=2, values=[[2, 2], [4, 4]])
- pfir11 = Parameter('N_pfir_[1, 1]', tw=1, dimensions=2, values=[[2, 3], [4, 5]])
+ pfir00 = Parameter("N_pfir_[0, 0]", tw=1, dimensions=2, values=[[1, 2], [3, 4]])
+ pfir01 = Parameter("N_pfir_[0, 1]", tw=1, dimensions=2, values=[[1, 3], [3, 5]])
+ pfir10 = Parameter("N_pfir_[1, 0]", tw=1, dimensions=2, values=[[2, 2], [4, 4]])
+ pfir11 = Parameter("N_pfir_[1, 1]", tw=1, dimensions=2, values=[[2, 3], [4, 5]])
parfun_00 = ParamFun(myFun, parameters_and_constants=[p1_0, p2_0])(x.tw(1))
parfun_01 = ParamFun(myFun, parameters_and_constants=[p1_0, p2_1])(x.tw(1))
parfun_10 = ParamFun(myFun, parameters_and_constants=[p1_1, p2_0])(x.tw(1))
parfun_11 = ParamFun(myFun, parameters_and_constants=[p1_1, p2_1])(x.tw(1))
- out_in_00 = Output('parfun00', parfun_00)
- out_in_01 = Output('parfun01', parfun_01)
- out_in_10 = Output('parfun10', parfun_10)
- out_in_11 = Output('parfun11', parfun_11)
- actA = Output('fuzzyA', activationA)
- actB = Output('fuzzyB', activationB)
+ out_in_00 = Output("parfun00", parfun_00)
+ out_in_01 = Output("parfun01", parfun_01)
+ out_in_10 = Output("parfun10", parfun_10)
+ out_in_11 = Output("parfun11", parfun_11)
+ actA = Output("fuzzyA", activationA)
+ actB = Output("fuzzyB", activationB)
act_selA0 = Select(activationA, 0)
act_selA1 = Select(activationA, 1)
act_selB0 = Select(activationB, 0)
act_selB1 = Select(activationB, 1)
- out_act_selA0 = Output('fuzzy_selA0', act_selA0)
- out_act_selA1 = Output('fuzzy_selA1', act_selA1)
- out_act_selB0 = Output('fuzzy_selB0', act_selB0)
- out_act_selB1 = Output('fuzzy_selB1', act_selB1)
+ out_act_selA0 = Output("fuzzy_selA0", act_selA0)
+ out_act_selA1 = Output("fuzzy_selA1", act_selA1)
+ out_act_selB0 = Output("fuzzy_selB0", act_selB0)
+ out_act_selB1 = Output("fuzzy_selB1", act_selB1)
mul00 = parfun_00 * act_selA0 * act_selB0
mul01 = parfun_01 * act_selA0 * act_selB1
mul10 = parfun_10 * act_selA1 * act_selB0
mul11 = parfun_11 * act_selA1 * act_selB1
- out_mul00 = Output('mul00', mul00)
- out_mul01 = Output('mul01', mul01)
- out_mul10 = Output('mul10', mul10)
- out_mul11 = Output('mul11', mul11)
+ out_mul00 = Output("mul00", mul00)
+ out_mul01 = Output("mul01", mul01)
+ out_mul10 = Output("mul10", mul10)
+ out_mul11 = Output("mul11", mul11)
fir00 = Fir(2, W=pfir00)(mul00)
fir01 = Fir(2, W=pfir01)(mul01)
fir10 = Fir(2, W=pfir10)(mul10)
fir11 = Fir(2, W=pfir11)(mul11)
- out_fir00 = Output('fir00', fir00)
- out_fir01 = Output('fir01', fir01)
- out_fir10 = Output('fir10', fir10)
- out_fir11 = Output('fir11', fir11)
+ out_fir00 = Output("fir00", fir00)
+ out_fir01 = Output("fir01", fir01)
+ out_fir10 = Output("fir10", fir10)
+ out_fir11 = Output("fir11", fir11)
sum = fir00 + fir01 + fir10 + fir11
- out_sum = Output('out_sum', sum)
- out = Output('out', loc)
+ out_sum = Output("out_sum", sum)
+ out = Output("out", loc)
test = Modely(visualizer=None)
- test.addModel('all_out', [out_in_00, out_in_01, out_in_10, out_in_11,
- out_act_selA0, out_act_selA1, out_act_selB0, out_act_selB1,
- out_mul00, out_mul01, out_mul10, out_mul11,
- out_fir00, out_fir01, out_fir10, out_fir11,
- out_sum])
- test.addModel('out', out)
+ test.addModel(
+ "all_out",
+ [
+ out_in_00,
+ out_in_01,
+ out_in_10,
+ out_in_11,
+ out_act_selA0,
+ out_act_selA1,
+ out_act_selB0,
+ out_act_selB1,
+ out_mul00,
+ out_mul01,
+ out_mul10,
+ out_mul11,
+ out_fir00,
+ out_fir01,
+ out_fir10,
+ out_fir11,
+ out_sum,
+ ],
+ )
+ test.addModel("out", out)
test.neuralizeModel(0.5)
# Three semples with a dimensions 2
- result = test({'x': [0, 1, -2, 3], 'F': [-2, 2, 1, 5]})
- self.assertEqual(result['out_sum'],result['out'])
+ result = test({"x": [0, 1, -2, 3], "F": [-2, 2, 1, 5]})
+ self.assertEqual(result["out_sum"], result["out"])
def test_integrate_derivate(self):
NeuObj.clearNames()
- input = Input('in1')
-
- in1_s = Output('in1_s', input.s(1))
- in1_s2 = Output('in1_s2', input.s(2))
- in1_s2_2 = Output('in1_s2_2', Differentiate(input.s(1)))
- in1_s2_3 = Output('in1_s2_3', input.s(1).s(1))
- in1_s_2 = Output('in1_s_2', input.s(2).s(-1))
-
- in1_sm = Output('in1_sm', input.s(-1))
- in1_sm2 = Output('in1_sm2', input.s(-2))
- in1_sm2_2 = Output('in1_sm2_2', Integrate(input.s(-1)))
- in1_sm2_3 = Output('in1_sm2_3', input.s(-1).s(-1))
- in1_sm_2 = Output('in1_sm_2', input.s(-2).s(1))
-
- in1_1 = Output('in1_1', Integrate(input.s(1)))
- in1_2 = Output('in1_2', Integrate(Integrate(input.s(2))))
- in1_3 = Output('in1_3', Integrate(Integrate(Differentiate(input.s(1)))))
-
- in1_1_2 = Output('in1_1_2', Differentiate(input.s(-1)))
- in1_2_2 = Output('in1_2_2', Differentiate(Differentiate(input.s(-2))))
- in1_3_2 = Output('in1_3_2', Differentiate(Differentiate(Integrate(input.s(-1)))))
+ input = Input("in1")
+
+ in1_s = Output("in1_s", input.s(1))
+ in1_s2 = Output("in1_s2", input.s(2))
+ in1_s2_2 = Output("in1_s2_2", Differentiate(input.s(1)))
+ in1_s2_3 = Output("in1_s2_3", input.s(1).s(1))
+ in1_s_2 = Output("in1_s_2", input.s(2).s(-1))
+
+ in1_sm = Output("in1_sm", input.s(-1))
+ in1_sm2 = Output("in1_sm2", input.s(-2))
+ in1_sm2_2 = Output("in1_sm2_2", Integrate(input.s(-1)))
+ in1_sm2_3 = Output("in1_sm2_3", input.s(-1).s(-1))
+ in1_sm_2 = Output("in1_sm_2", input.s(-2).s(1))
+
+ in1_1 = Output("in1_1", Integrate(input.s(1)))
+ in1_2 = Output("in1_2", Integrate(Integrate(input.s(2))))
+ in1_3 = Output("in1_3", Integrate(Integrate(Differentiate(input.s(1)))))
+
+ in1_1_2 = Output("in1_1_2", Differentiate(input.s(-1)))
+ in1_2_2 = Output("in1_2_2", Differentiate(Differentiate(input.s(-2))))
+ in1_3_2 = Output(
+ "in1_3_2", Differentiate(Differentiate(Integrate(input.s(-1))))
+ )
test = Modely(visualizer=None)
- test.addModel('out_A', [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2])
+ test.addModel(
+ "out_A",
+ [
+ in1_s,
+ in1_s2,
+ in1_s2_2,
+ in1_s2_3,
+ in1_s_2,
+ in1_sm,
+ in1_sm2,
+ in1_sm2_2,
+ in1_sm2_3,
+ in1_sm_2,
+ in1_1,
+ in1_2,
+ in1_3,
+ in1_1_2,
+ in1_2_2,
+ in1_3_2,
+ ],
+ )
test.neuralizeModel(1)
- inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
+ inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
results = test(inin)
- self.assertEqual(results['in1_1'], inin['in1'])
- self.assertEqual(results['in1_2'], inin['in1'])
- self.assertEqual(results['in1_3'], inin['in1'])
- self.assertEqual(results['in1_1_2'], inin['in1'])
- self.assertEqual(results['in1_2_2'], inin['in1'])
- self.assertEqual(results['in1_3_2'], inin['in1'])
+ self.assertEqual(results["in1_1"], inin["in1"])
+ self.assertEqual(results["in1_2"], inin["in1"])
+ self.assertEqual(results["in1_3"], inin["in1"])
+ self.assertEqual(results["in1_1_2"], inin["in1"])
+ self.assertEqual(results["in1_2_2"], inin["in1"])
+ self.assertEqual(results["in1_3_2"], inin["in1"])
inin_s = [14, -13, 1, 1, 1, 1, 1, 1, 1, 1]
inin_s2 = [14, -27, 14, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(results['in1_s'], inin_s)
- self.assertEqual(results['in1_s_2'], inin_s)
- self.assertEqual(results['in1_s2'], inin_s2)
- self.assertEqual(results['in1_s2_2'], inin_s2)
- self.assertEqual(results['in1_s2_3'], inin_s2)
+ self.assertEqual(results["in1_s"], inin_s)
+ self.assertEqual(results["in1_s_2"], inin_s)
+ self.assertEqual(results["in1_s2"], inin_s2)
+ self.assertEqual(results["in1_s2_2"], inin_s2)
+ self.assertEqual(results["in1_s2_3"], inin_s2)
inin_sm = [14, 15, 17, 20, 24, 29, 35, 42, 50, 59]
inin_sm2 = [14, 29, 46, 66, 90, 119, 154, 196, 246, 305]
- self.assertEqual(results['in1_sm'], inin_sm)
- self.assertEqual(results['in1_sm_2'], inin_sm)
- self.assertEqual(results['in1_sm2'], inin_sm2)
- self.assertEqual(results['in1_sm2_2'], inin_sm2)
- self.assertEqual(results['in1_sm2_3'], inin_sm2)
+ self.assertEqual(results["in1_sm"], inin_sm)
+ self.assertEqual(results["in1_sm_2"], inin_sm)
+ self.assertEqual(results["in1_sm2"], inin_sm2)
+ self.assertEqual(results["in1_sm2_2"], inin_sm2)
+ self.assertEqual(results["in1_sm2_3"], inin_sm2)
test = Modely(visualizer=None)
- test.addModel('out_A',
- [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2])
+ test.addModel(
+ "out_A",
+ [
+ in1_s,
+ in1_s2,
+ in1_s2_2,
+ in1_s2_3,
+ in1_s_2,
+ in1_sm,
+ in1_sm2,
+ in1_sm2_2,
+ in1_sm2_3,
+ in1_sm_2,
+ in1_1,
+ in1_2,
+ in1_3,
+ in1_1_2,
+ in1_2_2,
+ in1_3_2,
+ ],
+ )
test.neuralizeModel(0.01)
- inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
+ inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
results = test(inin)
- self.TestAlmostEqual(results['in1_1'], inin['in1'])
- self.TestAlmostEqual(results['in1_2'], inin['in1'])
- self.TestAlmostEqual(results['in1_3'], inin['in1'])
- self.TestAlmostEqual(results['in1_1_2'], inin['in1'])
- self.TestAlmostEqual(results['in1_2_2'], inin['in1'])
- self.TestAlmostEqual(results['in1_3_2'], inin['in1'])
+ self.TestAlmostEqual(results["in1_1"], inin["in1"])
+ self.TestAlmostEqual(results["in1_2"], inin["in1"])
+ self.TestAlmostEqual(results["in1_3"], inin["in1"])
+ self.TestAlmostEqual(results["in1_1_2"], inin["in1"])
+ self.TestAlmostEqual(results["in1_2_2"], inin["in1"])
+ self.TestAlmostEqual(results["in1_3_2"], inin["in1"])
inin_s = [1400, -1300, 100, 100, 100, 100, 100, 100, 100, 100]
inin_s2 = [140000, -270000, 140000, 0, 0, 0, 0, 0, 0, 0]
- self.TestAlmostEqual(results['in1_s'], inin_s)
- self.TestAlmostEqual(results['in1_s_2'], inin_s)
- self.TestAlmostEqual(results['in1_s2'], inin_s2)
- self.TestAlmostEqual(results['in1_s2_2'], inin_s2)
- self.TestAlmostEqual(results['in1_s2_3'], inin_s2)
+ self.TestAlmostEqual(results["in1_s"], inin_s)
+ self.TestAlmostEqual(results["in1_s_2"], inin_s)
+ self.TestAlmostEqual(results["in1_s2"], inin_s2)
+ self.TestAlmostEqual(results["in1_s2_2"], inin_s2)
+ self.TestAlmostEqual(results["in1_s2_3"], inin_s2)
inin_sm = [0.14, 0.15, 0.17, 0.20, 0.24, 0.29, 0.35, 0.42, 0.50, 0.59]
- inin_sm2 = [0.0014, 0.0029, 0.0046, 0.0066, 0.0090, 0.0119, 0.0154, 0.0196, 0.0246, 0.0305]
- self.TestAlmostEqual(results['in1_sm'], inin_sm)
- self.TestAlmostEqual(results['in1_sm_2'], inin_sm)
- self.TestAlmostEqual(results['in1_sm2'], inin_sm2)
- self.TestAlmostEqual(results['in1_sm2_2'], inin_sm2)
- self.TestAlmostEqual(results['in1_sm2_3'], inin_sm2)
+ inin_sm2 = [
+ 0.0014,
+ 0.0029,
+ 0.0046,
+ 0.0066,
+ 0.0090,
+ 0.0119,
+ 0.0154,
+ 0.0196,
+ 0.0246,
+ 0.0305,
+ ]
+ self.TestAlmostEqual(results["in1_sm"], inin_sm)
+ self.TestAlmostEqual(results["in1_sm_2"], inin_sm)
+ self.TestAlmostEqual(results["in1_sm2"], inin_sm2)
+ self.TestAlmostEqual(results["in1_sm2_2"], inin_sm2)
+ self.TestAlmostEqual(results["in1_sm2_3"], inin_sm2)
def test_integrate_derivate_trapezoidal(self):
NeuObj.clearNames()
- input = Input('in1')
-
- in1_s = Output('in1_s', input.s(1,method='trapezoidal'))
- in1_s2 = Output('in1_s2', input.s(2,method='trapezoidal'))
- in1_s2_2 = Output('in1_s2_2', Differentiate(input.s(1,method='trapezoidal'),method='trapezoidal'))
- in1_s2_3 = Output('in1_s2_3', input.s(1,method='trapezoidal').s(1,method='trapezoidal'))
- in1_s_2 = Output('in1_s_2', input.s(2,method='trapezoidal').s(-1,method='trapezoidal'))
-
- in1_sm = Output('in1_sm', input.s(-1,method='trapezoidal'))
- in1_sm2 = Output('in1_sm2', input.s(-2,method='trapezoidal'))
- in1_sm2_2 = Output('in1_sm2_2', Integrate(input.s(-1,method='trapezoidal'),method='trapezoidal'))
- in1_sm2_3 = Output('in1_sm2_3', input.s(-1,method='trapezoidal').s(-1,method='trapezoidal'))
- in1_sm_2 = Output('in1_sm_2', input.s(-2,method='trapezoidal').s(1,method='trapezoidal'))
-
- in1_1 = Output('in1_1', Integrate(input.s(1,method='trapezoidal'),method='trapezoidal'))
- in1_2 = Output('in1_2', Integrate(Integrate(input.s(2,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'))
- in1_3 = Output('in1_3', Integrate(Integrate(Differentiate(input.s(1,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'),method='trapezoidal'))
-
- in1_1_2 = Output('in1_1_2', Differentiate(input.s(-1,method='trapezoidal'),method='trapezoidal'))
- in1_2_2 = Output('in1_2_2', Differentiate(Differentiate(input.s(-2,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'))
- in1_3_2 = Output('in1_3_2', Differentiate(Differentiate(Integrate(input.s(-1,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'),method='trapezoidal'))
+ input = Input("in1")
+
+ in1_s = Output("in1_s", input.s(1, method="trapezoidal"))
+ in1_s2 = Output("in1_s2", input.s(2, method="trapezoidal"))
+ in1_s2_2 = Output(
+ "in1_s2_2",
+ Differentiate(input.s(1, method="trapezoidal"), method="trapezoidal"),
+ )
+ in1_s2_3 = Output(
+ "in1_s2_3", input.s(1, method="trapezoidal").s(1, method="trapezoidal")
+ )
+ in1_s_2 = Output(
+ "in1_s_2", input.s(2, method="trapezoidal").s(-1, method="trapezoidal")
+ )
+
+ in1_sm = Output("in1_sm", input.s(-1, method="trapezoidal"))
+ in1_sm2 = Output("in1_sm2", input.s(-2, method="trapezoidal"))
+ in1_sm2_2 = Output(
+ "in1_sm2_2",
+ Integrate(input.s(-1, method="trapezoidal"), method="trapezoidal"),
+ )
+ in1_sm2_3 = Output(
+ "in1_sm2_3", input.s(-1, method="trapezoidal").s(-1, method="trapezoidal")
+ )
+ in1_sm_2 = Output(
+ "in1_sm_2", input.s(-2, method="trapezoidal").s(1, method="trapezoidal")
+ )
+
+ in1_1 = Output(
+ "in1_1", Integrate(input.s(1, method="trapezoidal"), method="trapezoidal")
+ )
+ in1_2 = Output(
+ "in1_2",
+ Integrate(
+ Integrate(input.s(2, method="trapezoidal"), method="trapezoidal"),
+ method="trapezoidal",
+ ),
+ )
+ in1_3 = Output(
+ "in1_3",
+ Integrate(
+ Integrate(
+ Differentiate(
+ input.s(1, method="trapezoidal"), method="trapezoidal"
+ ),
+ method="trapezoidal",
+ ),
+ method="trapezoidal",
+ ),
+ )
+
+ in1_1_2 = Output(
+ "in1_1_2",
+ Differentiate(input.s(-1, method="trapezoidal"), method="trapezoidal"),
+ )
+ in1_2_2 = Output(
+ "in1_2_2",
+ Differentiate(
+ Differentiate(input.s(-2, method="trapezoidal"), method="trapezoidal"),
+ method="trapezoidal",
+ ),
+ )
+ in1_3_2 = Output(
+ "in1_3_2",
+ Differentiate(
+ Differentiate(
+ Integrate(input.s(-1, method="trapezoidal"), method="trapezoidal"),
+ method="trapezoidal",
+ ),
+ method="trapezoidal",
+ ),
+ )
test = Modely(visualizer=None)
- test.addModel('out_A', [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2])
+ test.addModel(
+ "out_A",
+ [
+ in1_s,
+ in1_s2,
+ in1_s2_2,
+ in1_s2_3,
+ in1_s_2,
+ in1_sm,
+ in1_sm2,
+ in1_sm2_2,
+ in1_sm2_3,
+ in1_sm_2,
+ in1_1,
+ in1_2,
+ in1_3,
+ in1_1_2,
+ in1_2_2,
+ in1_3_2,
+ ],
+ )
test.neuralizeModel(1)
- inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
+ inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
results = test(inin)
- self.assertEqual(results['in1_1'], inin['in1'])
- self.assertEqual(results['in1_2'], inin['in1'])
- self.assertEqual(results['in1_3'], inin['in1'])
- self.assertEqual(results['in1_1_2'], inin['in1'])
- self.assertEqual(results['in1_2_2'], inin['in1'])
- self.assertEqual(results['in1_3_2'], inin['in1'])
-
- inin_sm = [7., 14.5, 16., 18.5, 22., 26.5, 32., 38.5, 46., 54.5]
- inin_sm2 = [ 3.5 , 14.25, 29.5 , 46.75, 67. , 91.25, 120.5 , 155.75, 198. , 248.25]
- self.assertEqual(results['in1_sm'], inin_sm)
- self.assertEqual(results['in1_sm_2'], inin_sm)
- self.assertEqual(results['in1_sm2'], inin_sm2)
- self.assertEqual(results['in1_sm2_2'], inin_sm2)
- self.assertEqual(results['in1_sm2_3'], inin_sm2)
+ self.assertEqual(results["in1_1"], inin["in1"])
+ self.assertEqual(results["in1_2"], inin["in1"])
+ self.assertEqual(results["in1_3"], inin["in1"])
+ self.assertEqual(results["in1_1_2"], inin["in1"])
+ self.assertEqual(results["in1_2_2"], inin["in1"])
+ self.assertEqual(results["in1_3_2"], inin["in1"])
+
+ inin_sm = [7.0, 14.5, 16.0, 18.5, 22.0, 26.5, 32.0, 38.5, 46.0, 54.5]
+ inin_sm2 = [3.5, 14.25, 29.5, 46.75, 67.0, 91.25, 120.5, 155.75, 198.0, 248.25]
+ self.assertEqual(results["in1_sm"], inin_sm)
+ self.assertEqual(results["in1_sm_2"], inin_sm)
+ self.assertEqual(results["in1_sm2"], inin_sm2)
+ self.assertEqual(results["in1_sm2_2"], inin_sm2)
+ self.assertEqual(results["in1_sm2_3"], inin_sm2)
test = Modely(visualizer=None)
- test.addModel('out_A',
- [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2])
+ test.addModel(
+ "out_A",
+ [
+ in1_s,
+ in1_s2,
+ in1_s2_2,
+ in1_s2_3,
+ in1_s_2,
+ in1_sm,
+ in1_sm2,
+ in1_sm2_2,
+ in1_sm2_3,
+ in1_sm_2,
+ in1_1,
+ in1_2,
+ in1_3,
+ in1_1_2,
+ in1_2_2,
+ in1_3_2,
+ ],
+ )
test.neuralizeModel(0.01)
- inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
+ inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
results = test(inin)
- self.TestAlmostEqual(results['in1_1'], inin['in1'], precision=4)
- self.TestAlmostEqual(results['in1_2'], inin['in1'], precision=4)
- self.TestAlmostEqual(results['in1_3'], inin['in1'], precision=4)
- self.TestAlmostEqual(results['in1_1_2'], inin['in1'], precision=4)
- self.TestAlmostEqual(results['in1_2_2'], inin['in1'], precision=3)
- self.TestAlmostEqual(results['in1_3_2'], inin['in1'], precision=3)
-
- inin_sm = [ 0.07 , 0.145, 0.16 , 0.185, 0.22 , 0.265, 0.32 , 0.385, 0.46 , 0.545]
- inin_sm2 = [0.00035 , 0.001425, 0.00295 , 0.004675, 0.0067 , 0.009125, 0.01205 , 0.015575, 0.0198 , 0.024825]
- self.TestAlmostEqual(results['in1_sm'], inin_sm)
- self.TestAlmostEqual(results['in1_sm_2'], inin_sm)
- self.TestAlmostEqual(results['in1_sm2'], inin_sm2)
- self.TestAlmostEqual(results['in1_sm2_2'], inin_sm2)
- self.TestAlmostEqual(results['in1_sm2_3'], inin_sm2)
+ self.TestAlmostEqual(results["in1_1"], inin["in1"], precision=4)
+ self.TestAlmostEqual(results["in1_2"], inin["in1"], precision=4)
+ self.TestAlmostEqual(results["in1_3"], inin["in1"], precision=4)
+ self.TestAlmostEqual(results["in1_1_2"], inin["in1"], precision=4)
+ self.TestAlmostEqual(results["in1_2_2"], inin["in1"], precision=3)
+ self.TestAlmostEqual(results["in1_3_2"], inin["in1"], precision=3)
+
+ inin_sm = [0.07, 0.145, 0.16, 0.185, 0.22, 0.265, 0.32, 0.385, 0.46, 0.545]
+ inin_sm2 = [
+ 0.00035,
+ 0.001425,
+ 0.00295,
+ 0.004675,
+ 0.0067,
+ 0.009125,
+ 0.01205,
+ 0.015575,
+ 0.0198,
+ 0.024825,
+ ]
+ self.TestAlmostEqual(results["in1_sm"], inin_sm)
+ self.TestAlmostEqual(results["in1_sm_2"], inin_sm)
+ self.TestAlmostEqual(results["in1_sm2"], inin_sm2)
+ self.TestAlmostEqual(results["in1_sm2_2"], inin_sm2)
+ self.TestAlmostEqual(results["in1_sm2_3"], inin_sm2)
def test_derivate_wrt_input(self):
NeuObj.clearNames()
- x = Input('x')
+ x = Input("x")
x_last = x.last()
def parametric_fun(x, a, b, c, d):
import torch
- return x ** 3 * a + x ** 2 * b + torch.sin(x) * c + d
+
+ return x**3 * a + x**2 * b + torch.sin(x) * c + d
def dx_parametric_fun(x, a, b, c, d):
import torch
- return (3 * x ** 2 * a) + (2 * x * b) + c * torch.cos(x)
- fun = ParamFun(parametric_fun,['a','b','c','d'])(x_last)
- approx_y = Output('out', fun)
- approx_dy_dx = Output('d_out', Differentiate(fun, x_last))
+ return (3 * x**2 * a) + (2 * x * b) + c * torch.cos(x)
+
+ fun = ParamFun(parametric_fun, ["a", "b", "c", "d"])(x_last)
+ approx_y = Output("out", fun)
+ approx_dy_dx = Output("d_out", Differentiate(fun, x_last))
test = Modely(visualizer=None, seed=12)
- test.addModel('model', [approx_dy_dx, approx_y])
+ test.addModel("model", [approx_dy_dx, approx_y])
test.neuralizeModel()
- results = test({'x':[1,2]})
- self.assertAlmostEqual(results['out'][0], parametric_fun(torch.tensor(1), torch.tensor(test.parameters['a']),
- torch.tensor(test.parameters['b']),
- torch.tensor(test.parameters['c']),
- torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5)
- self.assertAlmostEqual(results['d_out'][0], dx_parametric_fun(torch.tensor(1), torch.tensor(test.parameters['a']),
- torch.tensor(test.parameters['b']),
- torch.tensor(test.parameters['c']),
- torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5)
- self.assertAlmostEqual(results['out'][1], parametric_fun(torch.tensor(2), torch.tensor(test.parameters['a']),
- torch.tensor(test.parameters['b']),
- torch.tensor(test.parameters['c']),
- torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5)
- self.assertAlmostEqual(results['d_out'][1], dx_parametric_fun(torch.tensor(2), torch.tensor(test.parameters['a']),
- torch.tensor(test.parameters['b']),
- torch.tensor(test.parameters['c']),
- torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5)
-
+ results = test({"x": [1, 2]})
+ self.assertAlmostEqual(
+ results["out"][0],
+ parametric_fun(
+ torch.tensor(1),
+ torch.tensor(test.parameters["a"]),
+ torch.tensor(test.parameters["b"]),
+ torch.tensor(test.parameters["c"]),
+ torch.tensor(test.parameters["d"]),
+ )
+ .detach()
+ .numpy()
+ .tolist()[0],
+ places=5,
+ )
+ self.assertAlmostEqual(
+ results["d_out"][0],
+ dx_parametric_fun(
+ torch.tensor(1),
+ torch.tensor(test.parameters["a"]),
+ torch.tensor(test.parameters["b"]),
+ torch.tensor(test.parameters["c"]),
+ torch.tensor(test.parameters["d"]),
+ )
+ .detach()
+ .numpy()
+ .tolist()[0],
+ places=5,
+ )
+ self.assertAlmostEqual(
+ results["out"][1],
+ parametric_fun(
+ torch.tensor(2),
+ torch.tensor(test.parameters["a"]),
+ torch.tensor(test.parameters["b"]),
+ torch.tensor(test.parameters["c"]),
+ torch.tensor(test.parameters["d"]),
+ )
+ .detach()
+ .numpy()
+ .tolist()[0],
+ places=5,
+ )
+ self.assertAlmostEqual(
+ results["d_out"][1],
+ dx_parametric_fun(
+ torch.tensor(2),
+ torch.tensor(test.parameters["a"]),
+ torch.tensor(test.parameters["b"]),
+ torch.tensor(test.parameters["c"]),
+ torch.tensor(test.parameters["d"]),
+ )
+ .detach()
+ .numpy()
+ .tolist()[0],
+ places=5,
+ )
diff --git a/tests/test_model_predict_recurrent.py b/tests/test_model_predict_recurrent.py
index 5bbb37a8..fdb17d4e 100644
--- a/tests/test_model_predict_recurrent.py
+++ b/tests/test_model_predict_recurrent.py
@@ -1,4 +1,7 @@
-import unittest, sys, os, torch
+import unittest
+import sys
+import os
+import torch
import numpy as np
from nnodely import *
@@ -19,17 +22,22 @@
# The second dimension indicates the output time dimension for each sample.
# The third is the size of the signal
+
def myfun(x, P):
- out = x*P
- return out[:,1:,:]
+ out = x * P
+ return out[:, 1:, :]
+
def myfunsum(x, P):
out = x + P
return out
-def matmul(x,y):
+
+def matmul(x, y):
import torch
- return torch.matmul(torch.transpose(x,1,2),y)
+
+ return torch.matmul(torch.transpose(x, 1, 2), y)
+
# def myfun2(a, b ,c):
# import torch
@@ -41,10 +49,15 @@ def matmul(x,y):
# bt = torch.transpose(b, 1, 2)
# return torch.matmul(p1,at+bt)+p2.t()
+
class ModelyRecurrentPredictTest(unittest.TestCase):
-
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
@@ -54,482 +67,666 @@ def TestAlmostEqual(self, data1, data2, precision=4):
def test_predict_and_states_values_fir_simple_closed_loop(self):
NeuObj.clearNames()
- x = Input('x')
- x_state = Input('x_state')
- p = Parameter('p', dimensions=1, sw=1, values=[[1.0]])
+ x = Input("x")
+ x_state = Input("x_state")
+ p = Parameter("p", dimensions=1, sw=1, values=[[1.0]])
rel_x = Fir(W=p)(x_state.last())
rel_x = ClosedLoop(rel_x, x_state)
- out = Output('out', rel_x)
+ out = Output("out", rel_x)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
- test.addMinimize('pos_x', x.next(), out)
+ test.addModel("out", out)
+ test.addMinimize("pos_x", x.next(), out)
test.neuralizeModel(0.01)
- result = test(inputs={'x': [2], 'x_state':[1]})
- self.assertEqual(test.states['x_state'], torch.tensor([[result['out']]]).tolist())
- self.assertEqual({'out': [1]}, result)
- result = test(inputs={'x': [2]})
- self.assertEqual(test.states['x_state'], torch.tensor([[[1.0]]]).tolist())
- self.assertEqual({'out': [1.0]}, result)
+ result = test(inputs={"x": [2], "x_state": [1]})
+ self.assertEqual(
+ test.states["x_state"], torch.tensor([[result["out"]]]).tolist()
+ )
+ self.assertEqual({"out": [1]}, result)
+ result = test(inputs={"x": [2]})
+ self.assertEqual(test.states["x_state"], torch.tensor([[[1.0]]]).tolist())
+ self.assertEqual({"out": [1.0]}, result)
test.resetStates()
- result = test(inputs={'x': [2]})
- self.assertEqual(test.states['x_state'], torch.tensor([[[0.0]]]).tolist())
- self.assertEqual({'out': [0.0]}, result)
+ result = test(inputs={"x": [2]})
+ self.assertEqual(test.states["x_state"], torch.tensor([[[0.0]]]).tolist())
+ self.assertEqual({"out": [0.0]}, result)
- test.removeConnection('x_state')
+ test.removeConnection("x_state")
test.neuralizeModel(0.01)
- result = test(inputs={'x': [2], 'x_state':[1]})
- self.assertEqual({'out': [1]}, result)
- result = test(inputs={'x': [2]})
- self.assertEqual({'out': [0.0]}, result)
- result = test(inputs={'x_state': [2.0]})
- self.assertEqual({'out': [2.0]}, result)
-
+ result = test(inputs={"x": [2], "x_state": [1]})
+ self.assertEqual({"out": [1]}, result)
+ result = test(inputs={"x": [2]})
+ self.assertEqual({"out": [0.0]}, result)
+ result = test(inputs={"x_state": [2.0]})
+ self.assertEqual({"out": [2.0]}, result)
def test_predict_values_fir_simple_closed_loop_predict(self):
NeuObj.clearNames()
- x = Input('x')
- x_in = Input('x_in')
- p = Parameter('p', dimensions=1, sw=1, values=[[1.0]])
+ x = Input("x")
+ x_in = Input("x_in")
+ p = Parameter("p", dimensions=1, sw=1, values=[[1.0]])
rel_x = Fir(W=p)(x_in.last())
- #rel_x = ClosedLoop(rel_x, x_state)
- out = Output('out', rel_x)
+ # rel_x = ClosedLoop(rel_x, x_state)
+ out = Output("out", rel_x)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
- test.addMinimize('pos_x', x.next(), out)
+ test.addModel("out", out)
+ test.addMinimize("pos_x", x.next(), out)
test.neuralizeModel(0.01)
- result = test(inputs={'x': [2], 'x_in':[1]},closed_loop={'x_in':'out'})
- self.assertEqual({'out':[1]}, result)
- result = test(inputs={'x': [2]}, closed_loop={'x_in':'out'})
- self.assertEqual({'out': [0.0]}, result)
+ result = test(inputs={"x": [2], "x_in": [1]}, closed_loop={"x_in": "out"})
+ self.assertEqual({"out": [1]}, result)
+ result = test(inputs={"x": [2]}, closed_loop={"x_in": "out"})
+ self.assertEqual({"out": [0.0]}, result)
def test_predict_values_fir_closed_loop(self):
NeuObj.clearNames()
## the memory is not shared between different calls
- x = Input('x')
- F = Input('F')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- x_out = Fir(W=p)(x.tw(0.5))+F.last()
+ x = Input("x")
+ F = Input("F")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ x_out = Fir(W=p)(x.tw(0.5)) + F.last()
x_out.closedLoop(F)
- out = Output('out',x_out)
+ out = Output("out", x_out)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
## one sample prediction with F initialized with zeros
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5]})
- self.assertEqual(result['out'], [15.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [15.0])
## 5 samples prediction with F initialized with zero only the first time
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]})
- self.assertEqual(result['out'], [15.0, 35.0, 60.0, 90.0, 125.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(result["out"], [15.0, 35.0, 60.0, 90.0, 125.0])
## one sample prediction with F initialized with [1]
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1]})
- self.assertEqual(result['out'], [16.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]})
+ self.assertEqual(result["out"], [16.0])
## 5 samples prediction with F initialized with [1] only the first time
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1]})
- self.assertEqual(result['out'], [16.0, 36.0, 61.0, 91.0, 126.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1]})
+ self.assertEqual(result["out"], [16.0, 36.0, 61.0, 91.0, 126.0])
## 5 samples prediction with F initialized with [1] the first time, [2] the second time and [3] the third time
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3]})
- self.assertEqual(result['out'], [16.0, 22.0, 28.0, 58.0, 93.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1, 2, 3]})
+ self.assertEqual(result["out"], [16.0, 22.0, 28.0, 58.0, 93.0])
## one sample prediction with F initialized with [1] (the other values are ignored)
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1,2,3]})
- self.assertEqual(result['out'], [16.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1, 2, 3]})
+ self.assertEqual(result["out"], [16.0])
def test_predict_values_fir_closed_loop_predict(self):
NeuObj.clearNames()
## the memory is not shared between different calls
- x = Input('x')
- F = Input('F')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- x_out = Fir(W=p)(x.tw(0.5))+F.last()
- out = Output('out',x_out)
+ x = Input("x")
+ F = Input("F")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ x_out = Fir(W=p)(x.tw(0.5)) + F.last()
+ out = Output("out", x_out)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
## one sample prediction with F initialized with zeros
- result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [15.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]}, closed_loop={"F": "out"})
+ self.assertEqual(result["out"], [15.0])
## 5 samples prediction with F initialized with zero only the first time
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [15.0, 35.0, 60.0, 90.0, 125.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}, closed_loop={"F": "out"}
+ )
+ self.assertEqual(result["out"], [15.0, 35.0, 60.0, 90.0, 125.0])
## one sample prediction with F initialized with [1]
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [16.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]}, closed_loop={"F": "out"})
+ self.assertEqual(result["out"], [16.0])
## 5 samples prediction with F initialized with [1] only the first time
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [16.0, 36.0, 61.0, 91.0, 126.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1]},
+ closed_loop={"F": "out"},
+ )
+ self.assertEqual(result["out"], [16.0, 36.0, 61.0, 91.0, 126.0])
## 5 samples prediction with F initialized with [1] the first time, [2] the second time and [3] the third time
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [16.0, 22.0, 28.0, 58.0, 93.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1, 2, 3]},
+ closed_loop={"F": "out"},
+ )
+ self.assertEqual(result["out"], [16.0, 22.0, 28.0, 58.0, 93.0])
## one sample prediction with F initialized with [1] (the other values are ignored)
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1,2,3]}, closed_loop={'F':'out'})
- self.assertEqual(result['out'], [16.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "F": [1, 2, 3]}, closed_loop={"F": "out"}
+ )
+ self.assertEqual(result["out"], [16.0])
def test_predict_values_2fir_closed_loop(self):
NeuObj.clearNames()
## the memory is not shared between different calls
- x = Input('x')
- y = Input('y')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- n = Parameter('n', tw=0.5, dimensions=1, values=[[-1.0],[-1.0],[-1.0],[-1.0],[-1.0]])
+ x = Input("x")
+ y = Input("y")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ n = Parameter(
+ "n", tw=0.5, dimensions=1, values=[[-1.0], [-1.0], [-1.0], [-1.0], [-1.0]]
+ )
fir_pos = Fir(W=p)(x.tw(0.5))
fir_neg = Fir(W=n)(y.tw(0.5))
fir_pos.closedLoop(x)
fir_neg.closedLoop(y)
- out_pos = Output('out_pos', fir_pos)
- out_neg = Output('out_neg', fir_neg)
- out = Output('out',fir_neg+fir_pos)
+ out_pos = Output("out_pos", fir_pos)
+ out_neg = Output("out_neg", fir_neg)
+ out = Output("out", fir_neg + fir_pos)
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
- test.addModel('out_pos',out_pos)
- test.addModel('out_neg',out_neg)
+ test.addModel("out", out)
+ test.addModel("out_pos", out_pos)
+ test.addModel("out_neg", out_neg)
test.neuralizeModel(0.1)
## one sample prediction with both close loops
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]})
- self.assertEqual(result['out'], [0.0])
- self.assertEqual(result['out_pos'], [15.0])
- self.assertEqual(result['out_neg'], [-15.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [0.0])
+ self.assertEqual(result["out_pos"], [15.0])
+ self.assertEqual(result["out_neg"], [-15.0])
## three sample prediction due to the max dimensions of inputs + prediction_samples
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, prediction_samples=2, num_of_samples=3)
- self.assertEqual(result['out'], [0.0, 30.0, 58.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0])
- self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]},
+ prediction_samples=2,
+ num_of_samples=3,
+ )
+ self.assertEqual(result["out"], [0.0, 30.0, 58.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0])
+ self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0])
## three sample prediction with both close loops but y gets initialized for 3 steps
## (!! since all the inputs are recurrent we must specify the prediction horizon (defualt=1))
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6,7]}, prediction_samples=2, num_of_samples=3)
- self.assertEqual(result['out'], [0.0, 30.0, 58.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0])
- self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]},
+ prediction_samples=2,
+ num_of_samples=3,
+ )
+ self.assertEqual(result["out"], [0.0, 30.0, 58.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0])
+ self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0])
test.resetStates()
- result = test(inputs={'x': [1, 2, 3, 4, 5], 'y': [1, 2, 3, 4, 5, 6, 7]}, num_of_samples=3)
- self.assertEqual(result['out'], [0.0, 9.0, 31.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0])
- self.assertEqual(result['out_neg'], [-15.0, -20.0, -25.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]}, num_of_samples=3
+ )
+ self.assertEqual(result["out"], [0.0, 9.0, 31.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0])
+ self.assertEqual(result["out_neg"], [-15.0, -20.0, -25.0])
def test_predict_values_2fir_closed_loop_predict(self):
NeuObj.clearNames()
## the memory is not shared between different calls
- x = Input('x')
- y = Input('y')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- n = Parameter('n', tw=0.5, dimensions=1, values=[[-1.0],[-1.0],[-1.0],[-1.0],[-1.0]])
+ x = Input("x")
+ y = Input("y")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ n = Parameter(
+ "n", tw=0.5, dimensions=1, values=[[-1.0], [-1.0], [-1.0], [-1.0], [-1.0]]
+ )
fir_pos = Fir(W=p)(x.tw(0.5))
fir_neg = Fir(W=n)(y.tw(0.5))
- out_pos = Output('out_pos', fir_pos)
- out_neg = Output('out_neg', fir_neg)
- out = Output('out',fir_neg+fir_pos)
+ out_pos = Output("out_pos", fir_pos)
+ out_neg = Output("out_neg", fir_neg)
+ out = Output("out", fir_neg + fir_pos)
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
- test.addModel('out_pos',out_pos)
- test.addModel('out_neg',out_neg)
+ test.addModel("out", out)
+ test.addModel("out_pos", out_pos)
+ test.addModel("out_neg", out_neg)
test.neuralizeModel(0.1)
## two sample one prediction for x in close loop
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6]}, closed_loop={'x':'out_pos'})
- self.assertEqual(result['out'], [0.0, 9.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0])
- self.assertEqual(result['out_neg'], [-15.0, -20.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6]},
+ closed_loop={"x": "out_pos"},
+ )
+ self.assertEqual(result["out"], [0.0, 9.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0])
+ self.assertEqual(result["out_neg"], [-15.0, -20.0])
## two sample one prediction for y in close loop
- result = test(inputs={'x':[1,2,3,4,5,6], 'y':[1,2,3,4,5]}, closed_loop={'y':'out_pos'})
- self.assertEqual(result['out'], [0.0, -9.0])
- self.assertEqual(result['out_pos'], [15.0, 20.0])
- self.assertEqual(result['out_neg'], [-15.0, -29.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6], "y": [1, 2, 3, 4, 5]},
+ closed_loop={"y": "out_pos"},
+ )
+ self.assertEqual(result["out"], [0.0, -9.0])
+ self.assertEqual(result["out_pos"], [15.0, 20.0])
+ self.assertEqual(result["out_neg"], [-15.0, -29.0])
## one sample prediction with both close loops
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, closed_loop={'x':'out_pos', 'y':'out_neg'})
- self.assertEqual(result['out'], [0.0])
- self.assertEqual(result['out_pos'], [15.0])
- self.assertEqual(result['out_neg'], [-15.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]},
+ closed_loop={"x": "out_pos", "y": "out_neg"},
+ )
+ self.assertEqual(result["out"], [0.0])
+ self.assertEqual(result["out_pos"], [15.0])
+ self.assertEqual(result["out_neg"], [-15.0])
## three sample prediction due to the max dimensions of inputs + prediction_samples
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, closed_loop={'x':'out_pos', 'y':'out_neg'}, prediction_samples=2, num_of_samples=3)
- self.assertEqual(result['out'], [0.0, 30.0, 58.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0])
- self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]},
+ closed_loop={"x": "out_pos", "y": "out_neg"},
+ prediction_samples=2,
+ num_of_samples=3,
+ )
+ self.assertEqual(result["out"], [0.0, 30.0, 58.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0])
+ self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0])
## three sample prediction with both close loops but y gets initialized for 3 steps
## (!! since all the inputs are recurrent we must specify the prediction horizon (defualt=1))
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6,7]}, closed_loop={'x':'out_pos', 'y':'out_neg'}, prediction_samples=2, num_of_samples=3)
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]},
+ closed_loop={"x": "out_pos", "y": "out_neg"},
+ prediction_samples=2,
+ num_of_samples=3,
+ )
## 1+2+3+4+5 -1-2-3-4-5 2+3+4+5+15 -2-3-4-5+15
- #self.assertEqual(result['out'], [0.0, 9.0, 31.0])
- self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0])
- self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0])
+ # self.assertEqual(result['out'], [0.0, 9.0, 31.0])
+ self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0])
+ self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0])
def test_predict_values_3states_closed_loop(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- F_state = Input('F')
- y_state = Input('y')
- z_state = Input('z')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- x_out = Fir(W=p)(x.tw(0.5))+F_state.last()+y_state.last()+z_state.last()
+ x = Input("x")
+ F_state = Input("F")
+ y_state = Input("y")
+ z_state = Input("z")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ x_out = Fir(W=p)(x.tw(0.5)) + F_state.last() + y_state.last() + z_state.last()
x_out = ClosedLoop(x_out, F_state)
x_out = ClosedLoop(x_out, y_state)
x_out = ClosedLoop(x_out, z_state)
- out = Output('out',x_out)
+ out = Output("out", x_out)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]})
- self.assertEqual(result['out'], [15.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [15.0])
## 5 sample prediction with state variables not initialized
## (the first prediction will preserve the state of the previous test [15.0])
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]})
- self.assertEqual(result['out'], [60.0, 200.0, 625.0, 1905.0, 5750.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(result["out"], [60.0, 200.0, 625.0, 1905.0, 5750.0])
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]})
- self.assertEqual(result['out'], [15.0, 65.0, 220.0, 220*3+30, (220*3+30)*3+35])
+ result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]})
+ self.assertEqual(
+ result["out"], [15.0, 65.0, 220.0, 220 * 3 + 30, (220 * 3 + 30) * 3 + 35]
+ )
## one sample prediction with state variables initialized with zero
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5]})
- self.assertEqual(result['out'], [15.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [15.0])
## one sample prediction with F initialized with [1] and the others not initialized (so they will have 15.0 in the memory)
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1]})
- self.assertEqual(result['out'], [46.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]})
+ self.assertEqual(result["out"], [46.0])
## one sample prediction with all the state variables initialized
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1], 'y':[2], 'z':[3]})
- self.assertEqual(result['out'], [21.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1], "y": [2], "z": [3]})
+ self.assertEqual(result["out"], [21.0])
## 5 samples prediction with state variables initialized as many times as they have values to take
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3], 'y':[2,3], 'z':[3]})
- self.assertEqual(result['out'], [21.0, 46.0, 120.0, 390.0, 1205.0])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "F": [1, 2, 3],
+ "y": [2, 3],
+ "z": [3],
+ }
+ )
+ self.assertEqual(result["out"], [21.0, 46.0, 120.0, 390.0, 1205.0])
# 2 samples prediction with state variables inizialized only at %prediction_samples
- result = test(inputs={'F': [1,2,3,4], 'y': [1,2], 'z': [1,2,3,4,5]}, prediction_samples=2, num_of_samples=4)
+ result = test(
+ inputs={"F": [1, 2, 3, 4], "y": [1, 2], "z": [1, 2, 3, 4, 5]},
+ prediction_samples=2,
+ num_of_samples=4,
+ )
# 1+1+1 = 3, 3+3+3 = 9, 9+9+9 = 27, 4+0+4 = 8, 8+8+8 = 24
- self.assertEqual(result['out'], [3.0, 9.0, 27.0, 8.0])
- #self.assertEqual(result['out'], [3.0,9.0,27.0,8.0])
- #self.assertEqual(result['out'], [3.0, 6.0, 12.0, 20.0])
+ self.assertEqual(result["out"], [3.0, 9.0, 27.0, 8.0])
+ # self.assertEqual(result['out'], [3.0,9.0,27.0,8.0])
+ # self.assertEqual(result['out'], [3.0, 6.0, 12.0, 20.0])
def test_predict_values_3states_closed_loop_predict(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- F_state = Input('F')
- y_state = Input('y')
- z_state = Input('z')
- p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- x_out = Fir(W=p)(x.tw(0.5))+F_state.last()+y_state.last()+z_state.last()
- out = Output('out',x_out)
+ x = Input("x")
+ F_state = Input("F")
+ y_state = Input("y")
+ z_state = Input("z")
+ p = Parameter(
+ "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ x_out = Fir(W=p)(x.tw(0.5)) + F_state.last() + y_state.last() + z_state.last()
+ out = Output("out", x_out)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
+ test.addModel("out", out)
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]},closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [15.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(result["out"], [15.0])
## 5 sample prediction with state variables not initialized
## (the first prediction will preserve the state of the previous test [15.0])
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}, closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [15.0, 65.0, 220.0, 220*3+30, (220*3+30)*3+35])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(
+ result["out"], [15.0, 65.0, 220.0, 220 * 3 + 30, (220 * 3 + 30) * 3 + 35]
+ )
## one sample prediction with state variables initialized with zero
test.resetStates()
- result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [15.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(result["out"], [15.0])
## one sample prediction with F initialized with [1] and the others not initialized (so they will have 15.0 in the memory)
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}, closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [16.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "F": [1]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(result["out"], [16.0])
## one sample prediction with all the state variables initialized
- result = test(inputs={'x':[1,2,3,4,5], 'F':[1], 'y':[2], 'z':[3]}, closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [21.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "F": [1], "y": [2], "z": [3]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(result["out"], [21.0])
## 5 samples prediction with state variables initialized as many times as they have values to take
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3], 'y':[2,3], 'z':[3]}, closed_loop={'F':'out','y':'out','z':'out'})
- self.assertEqual(result['out'], [21.0, 46.0, 120.0, 390.0, 1205.0])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "F": [1, 2, 3],
+ "y": [2, 3],
+ "z": [3],
+ },
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ )
+ self.assertEqual(result["out"], [21.0, 46.0, 120.0, 390.0, 1205.0])
# 2 samples prediction with state variables inizialized only at %prediction_samples
# 1+1+1 = 3, 3+3+3 = 9, 9+9+9 = 27, 4+0+4 = 8, 8+8+8 = 24
- result = test(inputs={'F': [1,2,3,4], 'y': [1,2], 'z': [1,2,3,4,5]}, closed_loop={'F':'out','y':'out','z':'out'}, prediction_samples=2, num_of_samples=4)
- self.assertEqual(result['out'], [3.0,9.0,27.0,8.0])
+ result = test(
+ inputs={"F": [1, 2, 3, 4], "y": [1, 2], "z": [1, 2, 3, 4, 5]},
+ closed_loop={"F": "out", "y": "out", "z": "out"},
+ prediction_samples=2,
+ num_of_samples=4,
+ )
+ self.assertEqual(result["out"], [3.0, 9.0, 27.0, 8.0])
def test_predict_values_and_states_3states_more_window_closed_loop(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- y_state = Input('y')
- z_state = Input('z')
- x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]])
- z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]])
+ x = Input("x")
+ y_state = Input("y")
+ z_state = Input("z")
+ x_p = Parameter(
+ "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ y_p = Parameter(
+ "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]]
+ )
+ z_p = Parameter(
+ "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]]
+ )
x_fir = Fir(W=x_p)(x.tw(0.5))
y_fir = Fir(W=y_p)(y_state.tw(0.5))
z_fir = Fir(W=z_p)(z_state.tw(0.5))
y_fir = ClosedLoop(y_fir, y_state)
z_fir = ClosedLoop(z_fir, z_state)
- out_x = Output('out_x', x_fir)
- out_y = Output('out_y', y_fir)
- out_z = Output('out_z', z_fir)
- out = Output('out',x_fir+y_fir+z_fir)
+ out_x = Output("out_x", x_fir)
+ out_y = Output("out_y", y_fir)
+ out_z = Output("out_z", z_fir)
+ out = Output("out", x_fir + y_fir + z_fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out_all',[out, out_x, out_y, out_z])
+ test.addModel("out_all", [out, out_x, out_y, out_z])
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]})
- self.assertEqual(result['out'], [15.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [0.0])
- self.assertEqual(result['out_z'], [0.0])
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
- self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [15.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [0.0])
+ self.assertEqual(result["out_z"], [0.0])
+ self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
## 1 sample prediction with state variables all initialized
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]})
- self.assertEqual(result['out'], [90.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [30.0])
- self.assertEqual(result['out_z'], [45.0])
- self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [5.0], [30.0]]])
- self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [45.0]]])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]}
+ )
+ self.assertEqual(result["out"], [90.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [30.0])
+ self.assertEqual(result["out_z"], [45.0])
+ self.assertEqual(test.states["y"], [[[2.0], [3.0], [4.0], [5.0], [30.0]]])
+ self.assertEqual(test.states["z"], [[[2.0], [3.0], [4.0], [5.0], [45.0]]])
## clear state of y
- test.resetStates({'y'})
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
- self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [45.0]]])
+ test.resetStates({"y"})
+ self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["z"], [[[2.0], [3.0], [4.0], [5.0], [45.0]]])
## multi-sample prediction with states initialized as many times as they have values
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]})
- self.assertEqual(result['out'], [90.0, 120.0, 309.0, 1101.0, 4155.0])
- self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0])
- self.assertEqual(result['out_y'], [30.0, 40.0, 50.0, 144.0, 424.0])
- self.assertEqual(result['out_z'], [45.0, 60.0, 234.0, 927.0, 3696.0])
- self.assertEqual(test.states['y'], [[[6.0], [7.0], [50.0], [144.0], [424.0]]])
- self.assertEqual(test.states['z'], [[[6.0], [60.0], [234.0], [927.0], [3696.0]]])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "y": [1, 2, 3, 4, 5, 6, 7],
+ "z": [1, 2, 3, 4, 5, 6],
+ }
+ )
+ self.assertEqual(result["out"], [90.0, 120.0, 309.0, 1101.0, 4155.0])
+ self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0])
+ self.assertEqual(result["out_y"], [30.0, 40.0, 50.0, 144.0, 424.0])
+ self.assertEqual(result["out_z"], [45.0, 60.0, 234.0, 927.0, 3696.0])
+ self.assertEqual(test.states["y"], [[[6.0], [7.0], [50.0], [144.0], [424.0]]])
+ self.assertEqual(
+ test.states["z"], [[[6.0], [60.0], [234.0], [927.0], [3696.0]]]
+ )
## Clear all states
test.resetStates()
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
- self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
def test_predict_values_and_states_3states_more_window_closed_loop_predict(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- y_state = Input('y')
- z_state = Input('z')
- x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]])
- z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]])
+ x = Input("x")
+ y_state = Input("y")
+ z_state = Input("z")
+ x_p = Parameter(
+ "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ y_p = Parameter(
+ "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]]
+ )
+ z_p = Parameter(
+ "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]]
+ )
x_fir = Fir(W=x_p)(x.tw(0.5))
y_fir = Fir(W=y_p)(y_state.tw(0.5))
z_fir = Fir(W=z_p)(z_state.tw(0.5))
- out_x = Output('out_x', x_fir)
- out_y = Output('out_y', y_fir)
- out_z = Output('out_z', z_fir)
- out = Output('out',x_fir+y_fir+z_fir)
+ out_x = Output("out_x", x_fir)
+ out_y = Output("out_y", y_fir)
+ out_z = Output("out_z", z_fir)
+ out = Output("out", x_fir + y_fir + z_fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out_all',[out, out_x, out_y, out_z])
+ test.addModel("out_all", [out, out_x, out_y, out_z])
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'y':'out_y', 'z':'out_z'})
- self.assertEqual(result['out'], [15.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [0.0])
- self.assertEqual(result['out_z'], [0.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5]}, closed_loop={"y": "out_y", "z": "out_z"}
+ )
+ self.assertEqual(result["out"], [15.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [0.0])
+ self.assertEqual(result["out_z"], [0.0])
## 1 sample prediction with state variables all initialized
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}, closed_loop={'y':'out_y', 'z':'out_z'})
- self.assertEqual(result['out'], [90.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [30.0])
- self.assertEqual(result['out_z'], [45.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]},
+ closed_loop={"y": "out_y", "z": "out_z"},
+ )
+ self.assertEqual(result["out"], [90.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [30.0])
+ self.assertEqual(result["out_z"], [45.0])
## multi-sample prediction with states initialized as many times as they have values
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}, closed_loop={'y':'out_y', 'z':'out_z'})
- self.assertEqual(result['out'], [90.0, 120.0, 309.0, 1101.0, 4155.0])
- self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0])
- self.assertEqual(result['out_y'], [30.0, 40.0, 50.0, 144.0, 424.0])
- self.assertEqual(result['out_z'], [45.0, 60.0, 234.0, 927.0, 3696.0])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "y": [1, 2, 3, 4, 5, 6, 7],
+ "z": [1, 2, 3, 4, 5, 6],
+ },
+ closed_loop={"y": "out_y", "z": "out_z"},
+ )
+ self.assertEqual(result["out"], [90.0, 120.0, 309.0, 1101.0, 4155.0])
+ self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0])
+ self.assertEqual(result["out_y"], [30.0, 40.0, 50.0, 144.0, 424.0])
+ self.assertEqual(result["out_z"], [45.0, 60.0, 234.0, 927.0, 3696.0])
def test_predict_values_and_states_2states_more_window_connect(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- y_state = Input('y')
- z_state = Input('z')
- x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]])
- z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]])
+ x = Input("x")
+ y_state = Input("y")
+ z_state = Input("z")
+ x_p = Parameter(
+ "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ y_p = Parameter(
+ "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]]
+ )
+ z_p = Parameter(
+ "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]]
+ )
x_fir = Fir(W=x_p)(x.tw(0.5))
y_fir = Fir(W=y_p)(y_state.tw(0.5))
z_fir = Fir(W=z_p)(z_state.tw(0.5))
x_fir = Connect(x_fir, y_state)
x_fir = Connect(x_fir, z_state)
- out_x = Output('out_x', x_fir)
- out_y = Output('out_y', y_fir)
- out_z = Output('out_z', z_fir)
- out = Output('out',x_fir+y_fir+z_fir)
+ out_x = Output("out_x", x_fir)
+ out_y = Output("out_y", y_fir)
+ out_z = Output("out_z", z_fir)
+ out = Output("out", x_fir + y_fir + z_fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out_all',[out, out_x, out_y, out_z])
+ test.addModel("out_all", [out, out_x, out_y, out_z])
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]})
- self.assertEqual(result['out'], [90.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [30.0])
- self.assertEqual(result['out_z'], [45.0])
+ result = test(inputs={"x": [1, 2, 3, 4, 5]})
+ self.assertEqual(result["out"], [90.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [30.0])
+ self.assertEqual(result["out_z"], [45.0])
# self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [15.0]]])
# self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [15.0]]])
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [15.0], [float('inf')]]])
- self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [15.0], [float('inf')]]])
+ self.assertEqual(
+ test.states["y"], [[[0.0], [0.0], [0.0], [15.0], [float("inf")]]]
+ )
+ self.assertEqual(
+ test.states["z"], [[[0.0], [0.0], [0.0], [15.0], [float("inf")]]]
+ )
# Replace insead of rolling
# self.assertEqual(test.model.states['y'].numpy().tolist(), [[[0.0], [0.0], [0.0], [15.0], [0.0]]])
# self.assertEqual(test.model.states['z'].numpy().tolist(), [[[0.0], [0.0], [0.0], [15.0], [0.0]]])
## 1 sample prediction with state variables all initialized
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]})
- self.assertEqual(result['out_x'], [15.0])
- #(1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]}
+ )
+ self.assertEqual(result["out_x"], [15.0])
+ # (1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3
# self.assertEqual(result['out'], [160.0])
# self.assertEqual(result['out_y'], [58.0])
# self.assertEqual(result['out_z'], [87.0])
- self.assertEqual(result['out'], [140.0]) # fir_x = (1+2+3+4+5)*1 fir_y = (1+2+3+4+15)*2 fir_z = (1+2+3+4+15)*3 result total = 140
- self.assertEqual(result['out_y'], [50.0])
- self.assertEqual(result['out_z'], [75.0])
+ self.assertEqual(
+ result["out"], [140.0]
+ ) # fir_x = (1+2+3+4+5)*1 fir_y = (1+2+3+4+15)*2 fir_z = (1+2+3+4+15)*3 result total = 140
+ self.assertEqual(result["out_y"], [50.0])
+ self.assertEqual(result["out_z"], [75.0])
# self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]])
# self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]])
- self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]])
- self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]])
+ self.assertEqual(
+ test.states["y"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]]
+ )
+ self.assertEqual(
+ test.states["z"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]]
+ )
# Replace instead of rolling
- #(1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3
+ # (1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3
# self.assertEqual(result['out'], [140.0])
# self.assertEqual(result['out_y'], [50.0])
# self.assertEqual(result['out_z'], [75.0])
# self.assertEqual(test.model.states['y'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]])
# self.assertEqual(test.model.states['z'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]])
## clear state of y
- test.resetStates({'y'})
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ test.resetStates({"y"})
+ self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
# self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]])
- self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]])
+ self.assertEqual(
+ test.states["z"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]]
+ )
# # Replace insead of rolling
# ## clear state of y
# test.resetStates({'y'})
# self.assertEqual(test.model.states['y'].numpy().tolist(), [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
# self.assertEqual(test.model.states['z'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]])
## multi-sample prediction with states initialized as many times as they have values
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]})
- self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0])
- self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)])
- self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)])
- self.assertEqual(result['out'], [sum(x) for x in zip(result['out_x'],result['out_y'],result['out_z'])])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "y": [1, 2, 3, 4, 5, 6, 7],
+ "z": [1, 2, 3, 4, 5, 6],
+ }
+ )
+ self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0])
+ self.assertEqual(
+ result["out_y"],
+ [
+ 2 * (1 + 2 + 3 + 4 + 15),
+ 2 * (2 + 3 + 4 + 5 + 20),
+ 2 * (3 + 4 + 5 + 6 + 25),
+ 2 * (4 + 5 + 6 + 25 + 30),
+ 2 * (5 + 6 + 25 + 30 + 35),
+ ],
+ )
+ self.assertEqual(
+ result["out_z"],
+ [
+ 3 * (1 + 2 + 3 + 4 + 15),
+ 3 * (2 + 3 + 4 + 5 + 20),
+ 3 * (3 + 4 + 5 + 20 + 25),
+ 3 * (4 + 5 + 20 + 25 + 30),
+ 3 * (5 + 20 + 25 + 30 + 35),
+ ],
+ )
+ self.assertEqual(
+ result["out"],
+ [sum(x) for x in zip(result["out_x"], result["out_y"], result["out_z"])],
+ )
# self.assertEqual(test.states['y'], [[[6.0], [7.0], [25.0], [30.0], [35.0]]])
# self.assertEqual(test.states['z'], [[[6.0], [20.0], [25.0], [30.0], [35.0]]])
- self.assertEqual(test.states['y'], [[[6.0], [25.0], [30.0], [35.0], [float('inf')]]])
- self.assertEqual(test.states['z'], [[[20.0], [25.0], [30.0], [35.0], [float('inf')]]])
+ self.assertEqual(
+ test.states["y"], [[[6.0], [25.0], [30.0], [35.0], [float("inf")]]]
+ )
+ self.assertEqual(
+ test.states["z"], [[[20.0], [25.0], [30.0], [35.0], [float("inf")]]]
+ )
# Replace instead of rolling
# self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)])
# self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)])
@@ -538,354 +735,547 @@ def test_predict_values_and_states_2states_more_window_connect(self):
# self.assertEqual(test.model.states['z'].numpy().tolist(), [[[20.0], [25.0], [30.0], [35.0], [5.0]]])
## Clear all states
test.resetStates()
- self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
- self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
+ self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]])
def test_predict_values_and_states_2states_more_window_connect_predict(self):
NeuObj.clearNames()
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- y_state = Input('y')
- z_state = Input('z')
- x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]])
- y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]])
- z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]])
+ x = Input("x")
+ y_state = Input("y")
+ z_state = Input("z")
+ x_p = Parameter(
+ "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]]
+ )
+ y_p = Parameter(
+ "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]]
+ )
+ z_p = Parameter(
+ "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]]
+ )
x_fir = Fir(W=x_p)(x.tw(0.5))
y_fir = Fir(W=y_p)(y_state.tw(0.5))
z_fir = Fir(W=z_p)(z_state.tw(0.5))
- out_x = Output('out_x', x_fir)
- out_y = Output('out_y', y_fir)
- out_z = Output('out_z', z_fir)
- out = Output('out',x_fir+y_fir+z_fir)
+ out_x = Output("out_x", x_fir)
+ out_y = Output("out_y", y_fir)
+ out_z = Output("out_z", z_fir)
+ out = Output("out", x_fir + y_fir + z_fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out_all',[out, out_x, out_y, out_z])
+ test.addModel("out_all", [out, out_x, out_y, out_z])
test.neuralizeModel(0.1)
## one sample prediction with state variables not initialized
## (they will have the last valid state)
- result = test(inputs={'x':[1,2,3,4,5]}, connect={'y':'out_x','z':'out_x'})
- self.assertEqual(result['out'], [90.0])
- self.assertEqual(result['out_x'], [15.0])
- self.assertEqual(result['out_y'], [30.0])
- self.assertEqual(result['out_z'], [45.0])
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5]}, connect={"y": "out_x", "z": "out_x"}
+ )
+ self.assertEqual(result["out"], [90.0])
+ self.assertEqual(result["out_x"], [15.0])
+ self.assertEqual(result["out_y"], [30.0])
+ self.assertEqual(result["out_z"], [45.0])
## 1 sample prediction with state variables all initialized
- result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}, connect={'y':'out_x','z':'out_x'})
- self.assertEqual(result['out_x'], [15.0])
- #(1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3
+ result = test(
+ inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]},
+ connect={"y": "out_x", "z": "out_x"},
+ )
+ self.assertEqual(result["out_x"], [15.0])
+ # (1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3
# self.assertEqual(result['out'], [160.0])
# self.assertEqual(result['out_y'], [58.0])
# self.assertEqual(result['out_z'], [87.0])
# Replace instead of rolling
- #(1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3
- self.assertEqual(result['out'], [140.0])
- self.assertEqual(result['out_y'], [50.0])
- self.assertEqual(result['out_z'], [75.0])
+ # (1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3
+ self.assertEqual(result["out"], [140.0])
+ self.assertEqual(result["out_y"], [50.0])
+ self.assertEqual(result["out_z"], [75.0])
## multi-sample prediction with states initialized as many times as they have values
- result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}, connect={'y':'out_x','z':'out_x'})
- self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0])
+ result = test(
+ inputs={
+ "x": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "y": [1, 2, 3, 4, 5, 6, 7],
+ "z": [1, 2, 3, 4, 5, 6],
+ },
+ connect={"y": "out_x", "z": "out_x"},
+ )
+ self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0])
# self.assertEqual(result['out_y'], [2*(2+3+4+5+15), 2*(3+4+5+6+20), 2*(4+5+6+7+25), 2*(5+6+7+25+30), 2*(6+7+25+30+35)])
# self.assertEqual(result['out_z'], [3*(2+3+4+5+15), 3*(3+4+5+6+20), 3*(4+5+6+20+25), 3*(5+6+20+25+30), 3*(6+20+25+30+35)])
# Reaplce instead of rolling
- self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)])
- self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)])
- self.assertEqual(result['out'], [sum(x) for x in zip(result['out_x'],result['out_y'],result['out_z'])])
+ self.assertEqual(
+ result["out_y"],
+ [
+ 2 * (1 + 2 + 3 + 4 + 15),
+ 2 * (2 + 3 + 4 + 5 + 20),
+ 2 * (3 + 4 + 5 + 6 + 25),
+ 2 * (4 + 5 + 6 + 25 + 30),
+ 2 * (5 + 6 + 25 + 30 + 35),
+ ],
+ )
+ self.assertEqual(
+ result["out_z"],
+ [
+ 3 * (1 + 2 + 3 + 4 + 15),
+ 3 * (2 + 3 + 4 + 5 + 20),
+ 3 * (3 + 4 + 5 + 20 + 25),
+ 3 * (4 + 5 + 20 + 25 + 30),
+ 3 * (5 + 20 + 25 + 30 + 35),
+ ],
+ )
+ self.assertEqual(
+ result["out"],
+ [sum(x) for x in zip(result["out_x"], result["out_y"], result["out_z"])],
+ )
def test_predict_values_and_connect_variables_2models_more_window_connect(self):
clearNames()
## Model1
- input1 = Input('in1')
- a = Parameter('a', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output1 = Output('out1', Fir(W=a)(input1.tw(0.05)))
+ input1 = Input("in1")
+ a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output1 = Output("out1", Fir(W=a)(input1.tw(0.05)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', output1)
- test.addMinimize('error1', input1.next(), output1)
+ test.addModel("model1", output1)
+ test.addMinimize("error1", input1.next(), output1)
test.neuralizeModel(0.01)
## Model2
- input2 = Input('in2')
- input3 = Input('in3')
- b = Parameter('b', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- c = Parameter('c', dimensions=1, tw=0.03, values=[[1],[1],[1]])
- output2 = Output('out2', Fir(W=b)(input2.tw(0.05))+Fir(W=c)(input3.tw(0.03)))
-
- test.addModel('model2', output2)
- test.addConnect(output1,input3)
- test.addMinimize('error2', input2.next(), output2)
+ input2 = Input("in2")
+ input3 = Input("in3")
+ b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ c = Parameter("c", dimensions=1, tw=0.03, values=[[1], [1], [1]])
+ output2 = Output("out2", Fir(W=b)(input2.tw(0.05)) + Fir(W=c)(input3.tw(0.03)))
+
+ test.addModel("model2", output2)
+ test.addConnect(output1, input3)
+ test.addMinimize("error2", input2.next(), output2)
test.neuralizeModel(0.01)
## Without connect
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=-1)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=-1,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0])
## connect out1 to in3 for 4 samples
test.resetStates()
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=3)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ },
+ prediction_samples=3,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0])
# self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]])
# Replace insead of rolling
- self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]])
+ self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]])
## connect out1 to in3 for 3 samples
test.resetStates()
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=2)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 60.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ },
+ prediction_samples=2,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 60.0])
# self.assertEqual(test.states['in3'], [[[0.0], [0.], [30.]]])
# Replace insead of rolling
- self.assertEqual(test.states['in3'], [[[0.], [30.], [float('inf')]]])
+ self.assertEqual(test.states["in3"], [[[0.0], [30.0], [float("inf")]]])
## connect out1 to in3 for 4 samples (initialize in3 with data)
test.resetStates()
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=3)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- #(1+2+3+4+5)+(2+3+15)
- #(2+3+4+5+6)+(3+15+20)
- #self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0])
- #self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=3,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ # (1+2+3+4+5)+(2+3+15)
+ # (2+3+4+5+6)+(3+15+20)
+ # self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0])
+ # self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]])
# Replace insead of rolling
- #(1+2+3+4+5)+(1+2+15)
- #(2+3+4+5+6)+(2+15+20)
- self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 105.0])
- self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]])
+ # (1+2+3+4+5)+(1+2+15)
+ # (2+3+4+5+6)+(2+15+20)
+ self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 105.0])
+ self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]])
## connect out1 to in3 for 3 samples (initialize in3 with data)
test.resetStates()
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=2)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=2,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
# (4+5+6+7+8)+(5+6+30)
# self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 71.0])
# self.assertEqual(test.states['in3'], [[[5.], [6.], [30.]]])
# Replace insead of rolling
# (4+5+6+7+8)+(4+5+30)
- self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 69.0])
- self.assertEqual(test.states['in3'], [[[5.], [30.], [float('inf')]]])
+ self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 69.0])
+ self.assertEqual(test.states["in3"], [[[5.0], [30.0], [float("inf")]]])
## Test remove connect
- test.removeConnection('in3')
+ test.removeConnection("in3")
test.neuralizeModel()
- results = test(inputs={'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
- 'in2': [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
- 'in3': [[1], [2], [3], [4], [5], [6]]}, prediction_samples=-1)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=-1,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0])
## Test connect via string
- test.addConnect('out1', 'in3')
+ test.addConnect("out1", "in3")
test.neuralizeModel()
- results = test(inputs={'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
- 'in2': [[1], [2], [3], [4], [5], [6], [7], [8], [9]]}, prediction_samples=3)
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0])
- self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]])
-
- def test_predict_values_and_connect_variables_2models_more_window_connect_predict(self):
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ },
+ prediction_samples=3,
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0])
+ self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]])
+
+ def test_predict_values_and_connect_variables_2models_more_window_connect_predict(
+ self,
+ ):
NeuObj.clearNames()
## Model1
- input1 = Input('in1')
- a = Parameter('a', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output1 = Output('out1', Fir(W=a)(input1.tw(0.05)))
+ input1 = Input("in1")
+ a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output1 = Output("out1", Fir(W=a)(input1.tw(0.05)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', output1)
- test.addMinimize('error1', input1.next(), output1)
+ test.addModel("model1", output1)
+ test.addMinimize("error1", input1.next(), output1)
test.neuralizeModel(0.01)
## Model2
- input2 = Input('in2')
- input3 = Input('in3')
- b = Parameter('b', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- c = Parameter('c', dimensions=1, tw=0.03, values=[[1],[1],[1]])
+ input2 = Input("in2")
+ input3 = Input("in3")
+ b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ c = Parameter("c", dimensions=1, tw=0.03, values=[[1], [1], [1]])
input_connect = input3.tw(0.03)
- output2 = Output('out2', Fir(W=b)(input2.tw(0.05))+Fir(W=c)(input_connect))
- output_connect = Output('out_connect', input_connect)
+ output2 = Output("out2", Fir(W=b)(input2.tw(0.05)) + Fir(W=c)(input_connect))
+ output_connect = Output("out_connect", input_connect)
- test.addModel('model2', [output2, output_connect])
- test.addMinimize('error2', input2.next(), output2)
+ test.addModel("model2", [output2, output_connect])
+ test.addMinimize("error2", input2.next(), output2)
test.neuralizeModel(0.01)
## Without connect
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]})
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ }
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0])
## connect out1 to in3 for 4 samples
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=3, connect={'in3':'out1'})
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0])
- self.assertEqual(results['out_connect'][-1], [20.0, 25.0, 30.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ },
+ prediction_samples=3,
+ connect={"in3": "out1"},
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0])
+ self.assertEqual(results["out_connect"][-1], [20.0, 25.0, 30.0])
# Replace insead of rolling
# self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[25.], [30.], [20.]]])
## connect out1 to in3 for 3 samples
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=2, connect={'in3':'out1'})
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 60.0])
- self.assertEqual(results['out_connect'][-1], [0.0, 0., 30.])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ },
+ prediction_samples=2,
+ connect={"in3": "out1"},
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 60.0])
+ self.assertEqual(results["out_connect"][-1], [0.0, 0.0, 30.0])
# Replace insead of rolling
# self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[0.], [30.], [0.]]])
## connect out1 to in3 for 4 samples (initialize in3 with data)
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=3, connect={'in3':'out1'})
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
- #(1+2+3+4+5)+(2+3+15)
- #(2+3+4+5+6)+(3+15+20)
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=3,
+ connect={"in3": "out1"},
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
+ # (1+2+3+4+5)+(2+3+15)
+ # (2+3+4+5+6)+(3+15+20)
# self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0])
- self.assertEqual(results['out_connect'][-1], [20., 25., 30.])
+ self.assertEqual(results["out_connect"][-1], [20.0, 25.0, 30.0])
# Replace insead of rolling
- #(1+2+3+4+5)+(1+2+15)
- #(2+3+4+5+6)+(2+15+20)
- self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 105.0])
- #self.assertEqual(results['out_connect'][-1], [25.0, 30.0, 20.0])
+ # (1+2+3+4+5)+(1+2+15)
+ # (2+3+4+5+6)+(2+15+20)
+ self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 105.0])
+ # self.assertEqual(results['out_connect'][-1], [25.0, 30.0, 20.0])
## connect out1 to in3 for 3 samples (initialize in3 with data)
- results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=2, connect={'in3':'out1'})
- self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0])
+ results = test(
+ inputs={
+ "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]],
+ "in3": [[1], [2], [3], [4], [5], [6]],
+ },
+ prediction_samples=2,
+ connect={"in3": "out1"},
+ )
+ self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0])
# (4+5+6+7+8)+(5+6+30)
# self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 71.0])
# self.assertEqual(results['out_connect'][-1], [5., 6., 30.])
- self.assertEqual(results['out_connect'][-1], [4., 5., 30.])
+ self.assertEqual(results["out_connect"][-1], [4.0, 5.0, 30.0])
# Replace insead of rolling
# (4+5+6+7+8)+(4+5+30)
- self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 69.0])
+ self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 69.0])
# self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[5.], [30.], [4.]]])
- def test_predict_values_and_states_only_state_variables_more_window_closed_loop(self):
+ def test_predict_values_and_states_only_state_variables_more_window_closed_loop(
+ self,
+ ):
NeuObj.clearNames()
- x_state = Input('x_state')
- p = Parameter('p', dimensions=1, tw=0.03, values=[[1.0], [1.0], [1.0]])
+ x_state = Input("x_state")
+ p = Parameter("p", dimensions=1, tw=0.03, values=[[1.0], [1.0], [1.0]])
rel_x = Fir(W=p)(x_state.tw(0.03))
rel_x = ClosedLoop(rel_x, x_state)
- out = Output('out', rel_x)
+ out = Output("out", rel_x)
- test = Modely(visualizer = None, seed=42)
- test.addModel('out',out)
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("out", out)
test.neuralizeModel(0.01)
- result = test(inputs={'x_state':[1, 2, 3]})
- self.assertEqual(test.states['x_state'], [[[2.],[3.],[6.]]])
+ result = test(inputs={"x_state": [1, 2, 3]})
+ self.assertEqual(test.states["x_state"], [[[2.0], [3.0], [6.0]]])
result = test()
- self.assertEqual(test.states['x_state'], [[[3.],[6.],[11.]]])
+ self.assertEqual(test.states["x_state"], [[[3.0], [6.0], [11.0]]])
def test_predict_values_linear_and_fir_2models_same_window_connect(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=1)
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=1)
lin_out = Linear(W=W, b=b)(input1.sw(2))
- inout = Input('inout')
- a = Parameter('a', sw = 2, values=[[4],[5]])
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
lin_out.connect(inout)
- output1 = Output('out1', lin_out)
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- output3 = Output('out3', Fir(W=a)(lin_out))
+ output1 = Output("out1", lin_out)
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ output3 = Output("out3", Fir(W=a)(lin_out))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2,output3])
+ test.addModel("model", [output1, output2, output3])
test.neuralizeModel()
# [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-10,-16]}))
- self.assertEqual({'out1': [[-10.0,-16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0,2.0],[2.0,3.0]]}))
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-10, -16]}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]]}),
+ )
def test_predict_values_linear_and_fir_2models_same_window_connect_predict(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=[1])
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
lin_out = Linear(W=W, b=b)(input1.sw(2))
- output1 = Output('out1', lin_out)
+ output1 = Output("out1", lin_out)
- inout = Input('inout')
- a = Parameter('a', sw = 2, values=[[4],[5]])
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- output3 = Output('out3', Fir(W=a)(lin_out))
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ output3 = Output("out3", Fir(W=a)(lin_out))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2,output3])
+ test.addModel("model", [output1, output2, output3])
test.neuralizeModel()
# [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-10,-16]}))
- self.assertEqual({'out1': [[-10.0,-16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0,2.0],[2.0,3.0]]},connect={'inout': 'out1'}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-30,-30]}, connect={'inout': 'out1'}))
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-10, -16]}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]]}, connect={"inout": "out1"}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]},
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-30, -30]},
+ connect={"inout": "out1"},
+ ),
+ )
def test_predict_values_linear_and_fir_2models_more_window_connect(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=1)
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=1)
lin_out = Linear(W=W, b=b)(input1.sw(2))
- inout = Input('inout')
- a = Parameter('a', sw = 2, values=[[4], [5]])
- a_big = Parameter('ab', sw = 5, values=[[1], [2], [3], [4], [5]])
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
+ a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]])
lin_out.connect(inout)
- output1 = Output('out1', lin_out)
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- output3 = Output('out3', Fir(W=a_big)(inout.sw(5)))
- output4 = Output('out4', Fir(W=a)(lin_out))
+ output1 = Output("out1", lin_out)
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ output3 = Output("out3", Fir(W=a_big)(inout.sw(5)))
+ output4 = Output("out4", Fir(W=a)(lin_out))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2,output3,output4])
+ test.addModel("model", [output1, output2, output3, output4])
test.neuralizeModel()
# [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]]}))
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0]],
+ "out2": [-120.0],
+ "out3": [-120.0],
+ "out4": [-120.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]]}),
+ )
test.resetStates()
# out2 # = [[-10,-16]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120]
# out3 # = [[-10,-16]] -> 1) [0,0,-10,-10,-16]*[1,2,3,4,5] -> [-10*3+-16*5+-10*4=-150]
# self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]}))
# Replace instead of rolling
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'inout': [0, 0, 0, -10, -16]}))
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0]],
+ "out2": [-120.0],
+ "out3": [-120.0],
+ "out4": [-120.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}),
+ )
test.resetStates()
# out2 # = [[-10,-16],[-16,-10]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120] 2) [-16,-10]*[4,5] -> [-16*4+-10*5=-114] -> [-120,-114]
# out3 # = [[-10,-16],[-16,-10]] -> 1) [0,0,0,-10,-16]*[1,2,3,4,5] -> [-16*5+-10*4=-120] 2) [0,0,-10,-16,-10]*[1,2,3,4,5] -> [-10*3+-16*4+-10*5 = -144] -> [-120,-144]
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0, -10.0]], 'out2': [-120.0,-114.0], 'out3': [-120.0,-144], 'out4': [-120.0,-114.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0,2.0]]}))
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -10.0]],
+ "out2": [-120.0, -114.0],
+ "out3": [-120.0, -144],
+ "out4": [-120.0, -114.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}),
+ )
with self.assertRaises(ValueError):
test.removeConnection(input1)
test.removeConnection(inout)
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [0.0], 'out3': [0.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]]}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'inout': [0, 0, 0, -10, -16]}))
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -10.0]], 'out2': [0.0, 0.0], 'out3': [0.0, 0.0], 'out4': [-120.0, -114.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}))
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [0.0], "out3": [0.0], "out4": [-120.0]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]]}),
+ )
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0]],
+ "out2": [-120.0],
+ "out3": [-120.0],
+ "out4": [-120.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}),
+ )
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -10.0]],
+ "out2": [0.0, 0.0],
+ "out3": [0.0, 0.0],
+ "out4": [-120.0, -114.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}),
+ )
def test_predict_values_linear_and_fir_2models_more_window_connect_predict(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=[1])
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
lin_out = Linear(W=W, b=b)(input1.sw(2))
- output1 = Output('out1', lin_out)
+ output1 = Output("out1", lin_out)
- inout = Input('inout')
- a = Parameter('a', sw = 2, values=[[4], [5]])
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- a_big = Parameter('ab', sw = 5, values=[[1], [2], [3], [4], [5]])
- output3 = Output('out3', Fir(W=a_big)(inout.sw(5)))
- output4 = Output('out4', Fir(W=a)(lin_out))
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]])
+ output3 = Output("out3", Fir(W=a_big)(inout.sw(5)))
+ output4 = Output("out4", Fir(W=a)(lin_out))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2,output3,output4])
+ test.addModel("model", [output1, output2, output3, output4])
test.neuralizeModel()
# [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]]}, connect={'inout': 'out1'}))
- #self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]},
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0]],
+ "out2": [-120.0],
+ "out3": [-120.0],
+ "out4": [-120.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}),
+ )
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0]],
+ "out2": [-120.0],
+ "out3": [-120.0],
+ "out4": [-120.0],
+ },
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]]}, connect={"inout": "out1"}),
+ )
+ # self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]},
# test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]}, connect={'inout': 'out1'}))
with self.assertRaises(StopIteration):
self.assertEqual({}, test())
@@ -894,255 +1284,452 @@ def test_predict_values_linear_and_fir_2models_more_window_connect_predict(self)
with self.assertRaises(StopIteration):
self.assertEqual({}, test(prediction_samples=4))
- self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [9.0], 'out3': [9.0], 'out4': [9.0]},
- test(connect={'inout': 'out1'}))
- self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [9.0], 'out3': [9.0], 'out4': [9.0]},
- test(connect={'inout': 'out1'}, prediction_samples=0))
- self.assertEqual({'out1': [[1.0, 1.0],[1.0, 1.0]], 'out2': [9.0,9.0], 'out3': [9.0,12.0], 'out4': [9.0,9.0]},
- test(connect={'inout': 'out1'}, prediction_samples=1, num_of_samples=2))
+ self.assertEqual(
+ {"out1": [[1.0, 1.0]], "out2": [9.0], "out3": [9.0], "out4": [9.0]},
+ test(connect={"inout": "out1"}),
+ )
+ self.assertEqual(
+ {"out1": [[1.0, 1.0]], "out2": [9.0], "out3": [9.0], "out4": [9.0]},
+ test(connect={"inout": "out1"}, prediction_samples=0),
+ )
+ self.assertEqual(
+ {
+ "out1": [[1.0, 1.0], [1.0, 1.0]],
+ "out2": [9.0, 9.0],
+ "out3": [9.0, 12.0],
+ "out4": [9.0, 9.0],
+ },
+ test(connect={"inout": "out1"}, prediction_samples=1, num_of_samples=2),
+ )
# [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1]
# [[2,3],[1,2]]*[-1,-5] = [[2*-1+3*-5=-17],[1*-1+2*-5=-11]]+[1]
# out2 # = [[-10,-16],[-16,-10]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120] 2) [-16,-10]*[4,5] -> [-16*4+-10*5=-114] -> [-120,-114]
# out3 # = [[-10,-16],[-16,-10]] -> 1) [0,0,0,-10,-16]*[1,2,3,4,5] -> [-16*5+-10*4=-120] 2) [0,0,-10,-16,-10]*[1,2,3,4,5] -> [-10*3+-16*4+-10*5 = -144] -> [-120,-144]
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0, -10.0]], 'out2': [-120.0,-114.0], 'out3': [-120.0,-144], 'out4': [-120.0,-114.0]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0,2.0]]},
- connect={'inout': 'out1'}))
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -10.0]],
+ "out2": [-120.0, -114.0],
+ "out3": [-120.0, -144],
+ "out4": [-120.0, -114.0],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}, connect={"inout": "out1"}
+ ),
+ )
def test_predict_values_linear_and_fir_2models_more_window_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=1)
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=1)
relation1 = Linear(W=W, b=b)(input1.sw(2))
# input2 = Input('inout') #TODO loop forever
# test.addConnect(output1, input1) # With this
- input2 = Input('in2')
- a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]])
- relation2 = Fir(output_dimension=2,W=a)(input2.sw(5))
+ input2 = Input("in2")
+ a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]])
+ relation2 = Fir(output_dimension=2, W=a)(input2.sw(5))
relation1.closedLoop(input2)
relation2.closedLoop(input1)
- output1 = Output('out1', relation1)
- output2 = Output('out2', relation2)
+ output1 = Output("out1", relation1)
+ output2 = Output("out2", relation2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
+ test.addModel("model", [output1, output2])
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]},prediction_samples=0))
-
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=1, num_of_samples=2))
- self.assertEqual({'out1': [[465.0,1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test())
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=0,
+ ),
+ )
+
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=1,
+ num_of_samples=2,
+ ),
+ )
+ self.assertEqual(
+ {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test()
+ )
test.resetStates()
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0],[465.0,1291.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]],[[2230.0, 3102.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2, num_of_samples=3))
-
- def test_predict_values_linear_and_fir_2models_more_window_closed_loop_on_models(self):
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=2,
+ num_of_samples=3,
+ ),
+ )
+
+ def test_predict_values_linear_and_fir_2models_more_window_closed_loop_on_models(
+ self,
+ ):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=1)
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=1)
relation1 = Linear(W=W, b=b)(input1.sw(2))
# input2 = Input('inout') #TODO loop forever
# test.addConnect(output1, input1) # With this
- input2 = Input('in2')
- a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]])
- relation2 = Fir(output_dimension=2,W=a)(input2.sw(5))
+ input2 = Input("in2")
+ a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]])
+ relation2 = Fir(output_dimension=2, W=a)(input2.sw(5))
- output1 = Output('out1', relation1)
- output2 = Output('out2', relation2)
+ output1 = Output("out1", relation1)
+ output2 = Output("out2", relation2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
+ test.addModel("model", [output1, output2])
test.addClosedLoop(output1, input2)
test.addClosedLoop(output2, input1)
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]},prediction_samples=0))
-
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=1, num_of_samples=2))
- self.assertEqual({'out1': [[465.0,1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test())
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=0,
+ ),
+ )
+
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=1,
+ num_of_samples=2,
+ ),
+ )
+ self.assertEqual(
+ {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test()
+ )
test.resetStates()
- self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0],[465.0,1291.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]],[[2230.0, 3102.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2, num_of_samples=3))
-
- test.removeConnection('in1')
- test.removeConnection('in2')
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=2,
+ num_of_samples=3,
+ ),
+ )
+
+ test.removeConnection("in1")
+ test.removeConnection("in2")
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}))
-
- test.addClosedLoop('out1', 'in2')
- test.addClosedLoop('out2', 'in1')
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}),
+ )
+
+ test.addClosedLoop("out1", "in2")
+ test.addClosedLoop("out2", "in1")
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]],
- 'out2': [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2,
- num_of_samples=3))
-
- def test_predict_values_linear_and_fir_2models_more_window_closed_loop_predict(self):
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=2,
+ num_of_samples=3,
+ ),
+ )
+
+ def test_predict_values_linear_and_fir_2models_more_window_closed_loop_predict(
+ self,
+ ):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=[1])
- output1 = Output('out1', Linear(W=W, b=b)(input1.sw(2)))
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
+ output1 = Output("out1", Linear(W=W, b=b)(input1.sw(2)))
# input2 = Input('inout') #TODO loop forever
# test.addConnect(output1, input1) # With this
- input2 = Input('in2')
- a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]])
- output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(5)))
+ input2 = Input("in2")
+ a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]])
+ output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(5)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
+ test.addModel("model", [output1, output2])
test.neuralizeModel()
# 1*-1+2*-5+1 = -10 2*-1+3*-5+1 = -16 -10*1+-16*2+-5*3+2*4+3*5 = -34 -16*2+-10*3+-5*4+2*5+3*6 = -86
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, closed_loop={'in1':'out2', 'in2':'out1'}))
- self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=0, closed_loop={'in1':'out2', 'in2':'out1'}))
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=2, prediction_samples=2, closed_loop={'in1':'out2', 'in2':'out1'}))
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ closed_loop={"in1": "out2", "in2": "out1"},
+ ),
+ )
+ self.assertEqual(
+ {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]},
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=0,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ ),
+ )
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, 465.0]],
+ "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ num_of_samples=2,
+ prediction_samples=2,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ ),
+ )
with self.assertRaises(StopIteration):
- self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test())
+ self.assertEqual(
+ {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test()
+ )
with self.assertRaises(StopIteration):
- self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test(prediction_samples=0))
+ self.assertEqual(
+ {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]},
+ test(prediction_samples=0),
+ )
with self.assertRaises(StopIteration):
- self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test(prediction_samples=3))
- self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [[[0.0, 0.0]]]},
- test(closed_loop={'in1': 'out2', 'in2': 'out1'}))
- self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [[[0.0, 0.0]]]},
- test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=0))
- self.assertEqual({'out1': [[1.0, 1.0],[1.0,1.0]], 'out2': [[[0.0, 0.0]],[[9.0,13.0]]]},
- test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=1, num_of_samples=2))
- self.assertEqual({'out1': [[1.0, 1.0],[1.0,1.0],[1.0,-73.0]], 'out2': [[[0.0, 0.0]],[[9.0,13.0]],[[12.0,18.0]]]},
- test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=2, num_of_samples=3))
-
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,1.0], [1.0,1.0], [1.0,1.0], [1.0,1.0]],
- 'out2': [[[-34.0, -86.0]],[[-8.0,-40.0]],[[8.0,8.0]],[[8.0,18.0]],[[3.0,9.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=5))
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,1.0], [1.0,1.0], [1.0,1.0], [1.0,1.0]],
- 'out2': [[[-34.0, -86.0]],[[-8.0,-40.0]],[[8.0,8.0]],[[8.0,18.0]],[[3.0,9.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=-1, num_of_samples=5))
-
- #-34*-1+ -86*-5+1 = 465.0
- #-140*-1+ -230*-5+1 = 465.0
- #8*-1 + 18*-5+1 = -97.0
- #[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]] * [465.0, 1291.0]
- #-16*1+-5*2+2*3+-10.0*4-16.0*5 = 140
- #-5*1+2*2-10*3+-16*4+465*5 = 2230 , -5*3+2*4-10*5+-16*6+465*7 = 3102
- #2*1+3*2 = 8, 2*3+3*4 = 18
- #3*1+1*4+1*5 = 12.0, 3*3+1*6+1*7 = 22.0
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0], [1.0, 1.0], [1.0, -97.0]],
- 'out2': [[[-34.0, -86.0]], [[-140.0, -230.0]],[[2230.0, 3102.0]], [[8.0, 18.0]], [[12.0, 22.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=5,
- prediction_samples=2, closed_loop={'in1': 'out2', 'in2': 'out1'}))
+ self.assertEqual(
+ {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]},
+ test(prediction_samples=3),
+ )
+ self.assertEqual(
+ {"out1": [[1.0, 1.0]], "out2": [[[0.0, 0.0]]]},
+ test(closed_loop={"in1": "out2", "in2": "out1"}),
+ )
+ self.assertEqual(
+ {"out1": [[1.0, 1.0]], "out2": [[[0.0, 0.0]]]},
+ test(closed_loop={"in1": "out2", "in2": "out1"}, prediction_samples=0),
+ )
+ self.assertEqual(
+ {"out1": [[1.0, 1.0], [1.0, 1.0]], "out2": [[[0.0, 0.0]], [[9.0, 13.0]]]},
+ test(
+ closed_loop={"in1": "out2", "in2": "out1"},
+ prediction_samples=1,
+ num_of_samples=2,
+ ),
+ )
+ self.assertEqual(
+ {
+ "out1": [[1.0, 1.0], [1.0, 1.0], [1.0, -73.0]],
+ "out2": [[[0.0, 0.0]], [[9.0, 13.0]], [[12.0, 18.0]]],
+ },
+ test(
+ closed_loop={"in1": "out2", "in2": "out1"},
+ prediction_samples=2,
+ num_of_samples=3,
+ ),
+ )
+
+ self.assertEqual(
+ {
+ "out1": [
+ [-10.0, -16.0],
+ [-16.0, 1.0],
+ [1.0, 1.0],
+ [1.0, 1.0],
+ [1.0, 1.0],
+ ],
+ "out2": [
+ [[-34.0, -86.0]],
+ [[-8.0, -40.0]],
+ [[8.0, 8.0]],
+ [[8.0, 18.0]],
+ [[3.0, 9.0]],
+ ],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ num_of_samples=5,
+ ),
+ )
+ self.assertEqual(
+ {
+ "out1": [
+ [-10.0, -16.0],
+ [-16.0, 1.0],
+ [1.0, 1.0],
+ [1.0, 1.0],
+ [1.0, 1.0],
+ ],
+ "out2": [
+ [[-34.0, -86.0]],
+ [[-8.0, -40.0]],
+ [[8.0, 8.0]],
+ [[8.0, 18.0]],
+ [[3.0, 9.0]],
+ ],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ prediction_samples=-1,
+ num_of_samples=5,
+ ),
+ )
+
+ # -34*-1+ -86*-5+1 = 465.0
+ # -140*-1+ -230*-5+1 = 465.0
+ # 8*-1 + 18*-5+1 = -97.0
+ # [[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]] * [465.0, 1291.0]
+ # -16*1+-5*2+2*3+-10.0*4-16.0*5 = 140
+ # -5*1+2*2-10*3+-16*4+465*5 = 2230 , -5*3+2*4-10*5+-16*6+465*7 = 3102
+ # 2*1+3*2 = 8, 2*3+3*4 = 18
+ # 3*1+1*4+1*5 = 12.0, 3*3+1*6+1*7 = 22.0
+ self.assertEqual(
+ {
+ "out1": [
+ [-10.0, -16.0],
+ [-16.0, 465.0],
+ [465.0, 1291.0],
+ [1.0, 1.0],
+ [1.0, -97.0],
+ ],
+ "out2": [
+ [[-34.0, -86.0]],
+ [[-140.0, -230.0]],
+ [[2230.0, 3102.0]],
+ [[8.0, 18.0]],
+ [[12.0, 22.0]],
+ ],
+ },
+ test(
+ {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]},
+ num_of_samples=5,
+ prediction_samples=2,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ ),
+ )
def test_predict_parameters(self):
NeuObj.clearNames()
- input1 = Input('in1')
- cl1 = Input('cl1')
- co1 = Input('co1')
- W = Parameter('W', values=[[1], [2], [3]])
+ input1 = Input("in1")
+ cl1 = Input("cl1")
+ co1 = Input("co1")
+ W = Parameter("W", values=[[1], [2], [3]])
parfun = ParamFun(myfunsum, parameters_and_constants=[W])
matmulfun = ParamFun(matmul)
parfun_out = parfun(input1.sw(3))
- output = Output('out', parfun_out)
- matmul_outcl = matmulfun(parfun_out, cl1.sw(3))+1.0
+ output = Output("out", parfun_out)
+ matmul_outcl = matmulfun(parfun_out, cl1.sw(3)) + 1.0
matmul_outcl.connect(co1)
matmul_outcl.closedLoop(cl1)
- outputCl = Output('outCl', matmul_outcl)
- outputCo = Output('outCo', matmulfun(parfun_out, co1.sw(3)))
+ outputCl = Output("outCl", matmul_outcl)
+ outputCo = Output("outCo", matmulfun(parfun_out, co1.sw(3)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output,outputCl,outputCo])
+ test.addModel("model", [output, outputCl, outputCo])
test.neuralizeModel()
# Test only one input
- result = test({'in1':[1,2,3]})
- self.assertEqual((1,3), np.array(result['out']).shape)
- self.assertEqual((1,), np.array(result['outCl']).shape)
- self.assertEqual((1,), np.array(result['outCo']).shape)
- self.assertEqual([[2.0,4.0,6.0]], result['out'])
- self.assertEqual([1.0], result['outCl'])
- self.assertEqual([6.0],result['outCo'])
- self.assertEqual(test.states['cl1'], [[[0.], [0.], [1.]]])
+ result = test({"in1": [1, 2, 3]})
+ self.assertEqual((1, 3), np.array(result["out"]).shape)
+ self.assertEqual((1,), np.array(result["outCl"]).shape)
+ self.assertEqual((1,), np.array(result["outCo"]).shape)
+ self.assertEqual([[2.0, 4.0, 6.0]], result["out"])
+ self.assertEqual([1.0], result["outCl"])
+ self.assertEqual([6.0], result["outCo"])
+ self.assertEqual(test.states["cl1"], [[[0.0], [0.0], [1.0]]])
# self.assertEqual(test.states['co1'], [[[0.], [0.], [1.]]])
- self.assertEqual(test.states['co1'], [[[0.], [1.], [float('inf')]]])
+ self.assertEqual(test.states["co1"], [[[0.0], [1.0], [float("inf")]]])
# Test two input
test.resetStates()
- result = test({'in1':[1,2,3,4]})
- self.assertEqual((2,3), np.array(result['out']).shape)
- self.assertEqual((2,), np.array(result['outCl']).shape)
- self.assertEqual((2,), np.array(result['outCo']).shape)
- self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out'])
- self.assertEqual([1.0,1*7+1], result['outCl'])
- self.assertEqual([1 * 6.0, 1. * 5 + 7. * 8.], result['outCo'])
+ result = test({"in1": [1, 2, 3, 4]})
+ self.assertEqual((2, 3), np.array(result["out"]).shape)
+ self.assertEqual((2,), np.array(result["outCl"]).shape)
+ self.assertEqual((2,), np.array(result["outCo"]).shape)
+ self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"])
+ self.assertEqual([1.0, 1 * 7 + 1], result["outCl"])
+ self.assertEqual([1 * 6.0, 1.0 * 5 + 7.0 * 8.0], result["outCo"])
# self.assertEqual(test.states['co1'], [[[0.], [1.], [8.]]])
- self.assertEqual(test.states['co1'], [[[1.], [8.], [float('inf')]]])
- self.assertEqual(test.states['cl1'], [[[0.], [1.], [8.]]])
+ self.assertEqual(test.states["co1"], [[[1.0], [8.0], [float("inf")]]])
+ self.assertEqual(test.states["cl1"], [[[0.0], [1.0], [8.0]]])
# Test two input
test.resetStates()
- result = test({'in1':[1,2,3,4], 'cl1':[2,2,2,2,2,2]})
- self.assertEqual((2,3), np.array(result['out']).shape)
- self.assertEqual((2,), np.array(result['outCl']).shape)
- self.assertEqual((2,), np.array(result['outCo']).shape)
- self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out'])
+ result = test({"in1": [1, 2, 3, 4], "cl1": [2, 2, 2, 2, 2, 2]})
+ self.assertEqual((2, 3), np.array(result["out"]).shape)
+ self.assertEqual((2,), np.array(result["outCl"]).shape)
+ self.assertEqual((2,), np.array(result["outCo"]).shape)
+ self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"])
# 2*2+4*2+6*2+1, 2*3+5*2+7*2+1
- self.assertEqual([25.0, 31.], result['outCl'])
- self.assertEqual([150.0, 342.0], result['outCo'])
- self.assertEqual(test.states['cl1'], [[[2.], [2.], [31.]]])
- #self.assertEqual(test.states['co1'], [[[0.], [25.], [31.]]])
- self.assertEqual(test.states['co1'], [[[25.], [31.], [float('inf')]]])
+ self.assertEqual([25.0, 31.0], result["outCl"])
+ self.assertEqual([150.0, 342.0], result["outCo"])
+ self.assertEqual(test.states["cl1"], [[[2.0], [2.0], [31.0]]])
+ # self.assertEqual(test.states['co1'], [[[0.], [25.], [31.]]])
+ self.assertEqual(test.states["co1"], [[[25.0], [31.0], [float("inf")]]])
# Test two input
test.resetStates()
- result = test({'in1':[1,2,3,4], 'co1':[2,2,2,2,2,2]})
- self.assertEqual((2,3), np.array(result['out']).shape)
- self.assertEqual((2,), np.array(result['outCl']).shape)
- self.assertEqual((2,), np.array(result['outCo']).shape)
- self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out'])
+ result = test({"in1": [1, 2, 3, 4], "co1": [2, 2, 2, 2, 2, 2]})
+ self.assertEqual((2, 3), np.array(result["out"]).shape)
+ self.assertEqual((2,), np.array(result["outCl"]).shape)
+ self.assertEqual((2,), np.array(result["outCo"]).shape)
+ self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"])
# 2*0+4*0+6*0+1
- self.assertEqual([1.0, 7*1+1.], result['outCl'])
+ self.assertEqual([1.0, 7 * 1 + 1.0], result["outCl"])
# 2*2+4*2+6*1, 2*3+2*5+7*8
- self.assertEqual([18.0, 72.0], result['outCo'])
- self.assertEqual(test.states['cl1'], [[[0.], [1.], [8.]]])
- #self.assertEqual(test.states['co1'], [[[2.], [2.], [8.]]])
- self.assertEqual(test.states['co1'], [[[2.], [8.], [float('inf')]]])
+ self.assertEqual([18.0, 72.0], result["outCo"])
+ self.assertEqual(test.states["cl1"], [[[0.0], [1.0], [8.0]]])
+ # self.assertEqual(test.states['co1'], [[[2.], [2.], [8.]]])
+ self.assertEqual(test.states["co1"], [[[2.0], [8.0], [float("inf")]]])
test.resetStates()
- result = test({'co1':[2,2,2,2,2,2]})
- self.assertEqual((4,3), np.array(result['out']).shape)
- self.assertEqual((4,), np.array(result['outCl']).shape)
- self.assertEqual((4,), np.array(result['outCo']).shape)
- self.assertEqual([[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0]], result['out'])
- self.assertEqual(test.states['cl1'], [[[4.], [15.], [55.]]])
+ result = test({"co1": [2, 2, 2, 2, 2, 2]})
+ self.assertEqual((4, 3), np.array(result["out"]).shape)
+ self.assertEqual((4,), np.array(result["outCl"]).shape)
+ self.assertEqual((4,), np.array(result["outCo"]).shape)
+ self.assertEqual(
+ [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
+ result["out"],
+ )
+ self.assertEqual(test.states["cl1"], [[[4.0], [15.0], [55.0]]])
# self.assertEqual(test.states['co1'], [[[2.], [2.], [55.]]])
- self.assertEqual(test.states['co1'], [[[2.], [55.], [float('inf')]]])
+ self.assertEqual(test.states["co1"], [[[2.0], [55.0], [float("inf")]]])
test.resetStates()
- result = test({'co1':[2,2,2,2,2,2]}, prediction_samples = 2)
- self.assertEqual((4,3), np.array(result['out']).shape)
- self.assertEqual((4,), np.array(result['outCl']).shape)
- self.assertEqual((4,), np.array(result['outCo']).shape)
-
+ result = test({"co1": [2, 2, 2, 2, 2, 2]}, prediction_samples=2)
+ self.assertEqual((4, 3), np.array(result["out"]).shape)
+ self.assertEqual((4,), np.array(result["outCl"]).shape)
+ self.assertEqual((4,), np.array(result["outCo"]).shape)
# Test output recurrent
@@ -1209,167 +1796,336 @@ def test_predict_parameters(self):
def test_parameters_predict_closed_loop_perdict(self):
NeuObj.clearNames()
- input1 = Input('in1')
- W = Parameter('W', sw=3, values=[[1], [2], [3]])
- out = Output('out',Fir(W=W)(input1.sw(3)))
+ input1 = Input("in1")
+ W = Parameter("W", sw=3, values=[[1], [2], [3]])
+ out = Output("out", Fir(W=W)(input1.sw(3)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [out])
+ test.addModel("model", [out])
test.neuralizeModel()
- result = test({'in1':[1,2,3]})
- self.assertEqual((1,), np.array(result['out']).shape)
- self.assertEqual([14.0], result['out'])
-
- result = test({'in1': [1, 2, 3]},closed_loop={'in1':'out'})
- self.assertEqual((1,), np.array(result['out']).shape)
- self.assertEqual([14.0], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'})
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]}, closed_loop = {'in1':'out'}, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0, 3*4+2*3+2*1, 5*3+4*2+3*1, 26*3+5*2+4*1, 92*3+26*2+1*5], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=-1)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=0)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=1)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=2)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=-1, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=0, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=1, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1,26*3+5*2+4*1,0*3+0*2+5*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=2, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,0*3+5*2+4*1,14*3+0*2+5*1], result['out'])
-
- result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,181*3+50*2+14*1,0*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3]})
+ self.assertEqual((1,), np.array(result["out"]).shape)
+ self.assertEqual([14.0], result["out"])
+
+ result = test({"in1": [1, 2, 3]}, closed_loop={"in1": "out"})
+ self.assertEqual((1,), np.array(result["out"]).shape)
+ self.assertEqual([14.0], result["out"])
+
+ result = test({"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"})
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, num_of_samples=5
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 26 * 3 + 5 * 2 + 4 * 1,
+ 92 * 3 + 26 * 2 + 1 * 5,
+ ],
+ result["out"],
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=-1
+ )
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=0
+ )
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=1
+ )
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=2
+ )
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=3
+ )
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"]
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]},
+ closed_loop={"in1": "out"},
+ prediction_samples=-1,
+ num_of_samples=5,
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]},
+ closed_loop={"in1": "out"},
+ prediction_samples=0,
+ num_of_samples=5,
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]},
+ closed_loop={"in1": "out"},
+ prediction_samples=1,
+ num_of_samples=5,
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 26 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]},
+ closed_loop={"in1": "out"},
+ prediction_samples=2,
+ num_of_samples=5,
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 50 * 3 + 14 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 14 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
+
+ result = test(
+ {"in1": [1, 2, 3, 4, 5]},
+ closed_loop={"in1": "out"},
+ prediction_samples=3,
+ num_of_samples=5,
+ )
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 50 * 3 + 14 * 2 + 3 * 1,
+ 181 * 3 + 50 * 2 + 14 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
def test_parameters_predict_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1')
- W = Parameter('W', sw=3, values=[[1], [2], [3]])
+ input1 = Input("in1")
+ W = Parameter("W", sw=3, values=[[1], [2], [3]])
relation = Fir(W=W)(input1.sw(3))
relation.closedLoop(input1)
- out = Output('out',relation)
+ out = Output("out", relation)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [out])
+ test.addModel("model", [out])
test.neuralizeModel()
- result = test({'in1':[1,2,3]})
- self.assertEqual((1,), np.array(result['out']).shape)
- self.assertEqual([14.0], result['out'])
+ result = test({"in1": [1, 2, 3]})
+ self.assertEqual((1,), np.array(result["out"]).shape)
+ self.assertEqual([14.0], result["out"])
test.resetStates()
- result = test({'in1': [1, 2, 3]})
- self.assertEqual((1,), np.array(result['out']).shape)
- self.assertEqual([14.0], result['out'])
+ result = test({"in1": [1, 2, 3]})
+ self.assertEqual((1,), np.array(result["out"]).shape)
+ self.assertEqual([14.0], result["out"])
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]})
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]})
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0, 3*4+2*3+2*1, 5*3+4*2+3*1, 26*3+5*2+4*1, 92*3+26*2+1*5], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 26 * 3 + 5 * 2 + 4 * 1,
+ 92 * 3 + 26 * 2 + 1 * 5,
+ ],
+ result["out"],
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=-1)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=-1)
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=0)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=0)
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=1)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=1)
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=2)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=2)
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=3)
- self.assertEqual((3,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=3)
+ self.assertEqual((3,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"]
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=-1, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=-1, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=0, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=0, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 4 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=1, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1,26*3+5*2+4*1,0*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=1, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 5 * 3 + 4 * 2 + 3 * 1,
+ 26 * 3 + 5 * 2 + 4 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=2, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,0*3+5*2+4*1,14*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=2, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 50 * 3 + 14 * 2 + 3 * 1,
+ 0 * 3 + 5 * 2 + 4 * 1,
+ 14 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
test.resetStates()
- result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=3, num_of_samples = 5)
- self.assertEqual((5,), np.array(result['out']).shape)
- self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,181*3+50*2+14*1,0*3+0*2+5*1], result['out'])
+ result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=3, num_of_samples=5)
+ self.assertEqual((5,), np.array(result["out"]).shape)
+ self.assertEqual(
+ [
+ 14.0,
+ 3 * 14 + 2 * 3 + 2 * 1,
+ 50 * 3 + 14 * 2 + 3 * 1,
+ 181 * 3 + 50 * 2 + 14 * 1,
+ 0 * 3 + 0 * 2 + 5 * 1,
+ ],
+ result["out"],
+ )
def test_derivate_wrt_input_closed_loop(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
x_last = x.last()
y_last = y.last()
- p=Parameter('fir',sw=1,values=[[-0.5]])
+ p = Parameter("fir", sw=1, values=[[-0.5]])
fun = Sin(x_last) + Fir(W=p)(x_last) + Cos(y_last)
out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last)
out_der.closedLoop(x)
- out = Output('out', out_der)
+ out = Output("out", out_der)
m = Modely(visualizer=None)
- m.addModel('model', [out])
+ m.addModel("model", [out])
m.neuralizeModel()
K = -0.5
@@ -1385,36 +2141,46 @@ def fun_data(x, y, K):
x_data.append(x)
y_data.append(y)
- result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples=10)
- self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out'])
-
- result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples='auto')
- self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out'])
+ result = m(
+ {"x": [-0.2], "y": [0.5]},
+ closed_loop={"y": "out"},
+ num_of_samples=10,
+ prediction_samples=10,
+ )
+ self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"])
+
+ result = m(
+ {"x": [-0.2], "y": [0.5]},
+ closed_loop={"y": "out"},
+ num_of_samples=10,
+ prediction_samples="auto",
+ )
+ self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"])
def test_derivate_wrt_input_connect(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
x_last = x.last()
y_last = y.last()
- p1 = Parameter('p1', sw=1, values=[[-0.5]])
+ p1 = Parameter("p1", sw=1, values=[[-0.5]])
fun = Sin(x_last) + Fir(W=p1)(x_last) + Cos(y_last)
out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last)
- x2 = Input('x2')
- y2 = Input('y2')
+ x2 = Input("x2")
+ y2 = Input("y2")
x2_last = x2.last()
y2_last = y2.last()
- p2 = Parameter('p2', sw=1, values=[[3]])
+ p2 = Parameter("p2", sw=1, values=[[3]])
fun2 = Sin(x2_last) + Fir(W=p2)(x2_last) + Cos(y2_last)
out_der2 = Differentiate(fun2, x2_last) + Differentiate(fun2, y2_last)
out_der.connect(x2)
- out1 = Output('out1', out_der)
- out2 = Output('out2', out_der2)
+ out1 = Output("out1", out_der)
+ out2 = Output("out2", out_der2)
m = Modely(visualizer=None)
- m.addModel('model', [out1,out2])
+ m.addModel("model", [out1, out2])
m.neuralizeModel()
K1 = -0.5
@@ -1424,89 +2190,150 @@ def fun_data(x, y, K):
return K + np.cos(x) - np.sin(y)
def fun_data2(x, y, K1, K2):
- return K2 + np.cos(fun_data(x,y,K1)) - np.sin(fun_data(x,y,K1))
+ return K2 + np.cos(fun_data(x, y, K1)) - np.sin(fun_data(x, y, K1))
x_data, y_data = [], []
x = [-0.2, 0, 0, 0, 0, 0, 0, 0, 0, 0]
y = [0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- for (xi,yi) in zip(x,y):
+ for xi, yi in zip(x, y):
r = fun_data2(xi, yi, K1, K2)
x_data.append(r)
y_data.append(r)
- result = m({'x': [-0.2], 'y': [0.5]}, connect={'y2':'out1'}, num_of_samples=10, prediction_samples=10)
- self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out2'])
-
- result = m({'x': [-0.2], 'y': [0.5]}, connect={'y2': 'out1'}, num_of_samples=10, prediction_samples='auto')
- self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result['out2'])
+ result = m(
+ {"x": [-0.2], "y": [0.5]},
+ connect={"y2": "out1"},
+ num_of_samples=10,
+ prediction_samples=10,
+ )
+ self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out2"])
+
+ result = m(
+ {"x": [-0.2], "y": [0.5]},
+ connect={"y2": "out1"},
+ num_of_samples=10,
+ prediction_samples="auto",
+ )
+ self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out2"])
def test_inference_sampled_data(self):
NeuObj.clearNames()
- data_folder = os.path.join(os.path.dirname(__file__), 'get_samples_data/')
+ data_folder = os.path.join(os.path.dirname(__file__), "get_samples_data/")
## the state is saved inside the model so the memory is shared between different calls
- x = Input('x')
- y_state = Input('y')
- x_p = Parameter('x_p', sw=2, dimensions=1, values=[[1.0],[1.0]])
- y_p = Parameter('y_p', sw=3, dimensions=1, values=[[2.0],[2.0],[2.0]])
+ x = Input("x")
+ y_state = Input("y")
+ x_p = Parameter("x_p", sw=2, dimensions=1, values=[[1.0], [1.0]])
+ y_p = Parameter("y_p", sw=3, dimensions=1, values=[[2.0], [2.0], [2.0]])
x_fir = Fir(W=x_p)(x.sw([-2, 0]))
y_fir = Fir(W=y_p)(y_state.sw([0, 3]))
y_fir.closedLoop(y_state)
- out_x = Output('out_x', x_fir)
- out_y = Output('out_y', y_fir)
- out = Output('out',x_fir+y_fir)
+ out_x = Output("out_x", x_fir)
+ out_y = Output("out_y", y_fir)
+ out = Output("out", x_fir + y_fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out_all',[out, out_x, out_y])
+ test.addModel("out_all", [out, out_x, out_y])
test.neuralizeModel(0.1)
- data_struct = ['x','y']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
+ data_struct = ["x", "y"]
+ test.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
## Using vectorized data
- result, logs = test(inputs={'x':[1,2,3,4,5,6], 'y':[1,2,3,4,5,6,7]}, prediction_samples=3, log_internal=True)
- self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0])
- self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0])
- self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])])
- self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]})
- self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]})
- self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]})
- self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]})
- self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]})
- self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])})
- self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])})
- self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])})
+ result, logs = test(
+ inputs={"x": [1, 2, 3, 4, 5, 6], "y": [1, 2, 3, 4, 5, 6, 7]},
+ prediction_samples=3,
+ log_internal=True,
+ )
+ self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0])
+ self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0])
+ self.assertEqual(
+ result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])]
+ )
+ self.assertDictEqual(
+ logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]}
+ )
+ self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])})
+ self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])})
+ self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])})
## Using sampled data
test.resetStates()
- result, logs = test(inputs={'x':[[1,2],[2,3],[3,4],[4,5],[5,6]], 'y':[[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7]]}, sampled=True, prediction_samples=3, log_internal=True)
- self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0])
- self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0])
- self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])])
- self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]})
- self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]})
- self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]})
- self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]})
- self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]})
- self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])})
- self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])})
- self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])})
+ result, logs = test(
+ inputs={
+ "x": [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]],
+ "y": [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]],
+ },
+ sampled=True,
+ prediction_samples=3,
+ log_internal=True,
+ )
+ self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0])
+ self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0])
+ self.assertEqual(
+ result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])]
+ )
+ self.assertDictEqual(
+ logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]}
+ )
+ self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])})
+ self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])})
+ self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])})
# ## Using get_sampled from a dataset
test.resetStates()
- inputs = test.getSamples('dataset', window=5)
- result, logs = test(inputs=inputs, sampled=True, prediction_samples=3, log_internal=True)
- self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0])
- self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0])
- self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])])
- self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]})
- self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]})
- self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]})
- self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]})
- self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]})
- self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])})
- self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])})
- self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])})
-
+ inputs = test.getSamples("dataset", window=5)
+ result, logs = test(
+ inputs=inputs, sampled=True, prediction_samples=3, log_internal=True
+ )
+ self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0])
+ self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0])
+ self.assertEqual(
+ result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])]
+ )
+ self.assertDictEqual(
+ logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]}
+ )
+ self.assertDictEqual(
+ logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]}
+ )
+ self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])})
+ self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])})
+ self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])})
# def test_state_initialization_inference(self):
# NeuObj.clearNames()
@@ -1592,4 +2419,4 @@ def test_inference_sampled_data(self):
# mass_dyn = Modely()
# mass_dyn.addModel('', [mass_x, mass_y, mass_dx, mass_dy])
# mass_dyn.neuralizeModel(0.01)
- # example = mass_dyn({'x0': [7], 'y0': [7], 'dx0': [5], 'dy0': [5], 'Fx': [100], 'Fy': [100]}, num_of_samples=200)
\ No newline at end of file
+ # example = mass_dyn({'x0': [7], 'y0': [7], 'dx0': [5], 'dy0': [5], 'Fx': [100], 'Fy': [100]}, num_of_samples=200)
diff --git a/tests/test_network_element.py b/tests/test_network_element.py
index 150c5644..d9184e3c 100644
--- a/tests/test_network_element.py
+++ b/tests/test_network_element.py
@@ -1,4 +1,7 @@
-import unittest, sys, os, torch
+import unittest
+import sys
+import os
+import torch
import numpy as np
@@ -14,12 +17,17 @@
# 11 Tests
# This file tests the dimensions and the of the element created in the pytorch environment
-class ModelyNetworkBuildingTest(unittest.TestCase):
+class ModelyNetworkBuildingTest(unittest.TestCase):
def TestAlmostEqual(self, data1, data2, precision=4):
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
if type(data1) == type(data2) == list:
- self.assertEqual(len(data1),len(data2))
+ self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.TestAlmostEqual(pred, label, precision=precision)
else:
@@ -27,428 +35,648 @@ def TestAlmostEqual(self, data1, data2, precision=4):
def test_network_building_very_simple(self):
NeuObj.clearNames()
- input1 = Input('in1').last()
+ input1 = Input("in1").last()
rel1 = Fir(input1)
- fun = Output('out', rel1)
+ fun = Output("out", rel1)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
list_of_dimensions = [[1, 1], [1, 1]]
- for ind, (key, value) in enumerate({k: v for k, v in test._model.relation_forward.items() if 'Fir' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
-
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
+
def test_network_building_simple(self):
NeuObj.clearNames()
Stream.resetCount()
- input1 = Input('in1')
+ input1 = Input("in1")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw(0.01))
- fun = Output('out',rel1+rel2)
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
-
- list_of_dimensions = {'Fir2':[5,1],'Fir5':[1,1]}
- for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items():
- self.assertEqual(list_of_dimensions[key],list(value.weights.shape))
+
+ list_of_dimensions = {"Fir2": [5, 1], "Fir5": [1, 1]}
+ for key, value in {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items():
+ self.assertEqual(list_of_dimensions[key], list(value.weights.shape))
def test_network_building_tw(self):
NeuObj.clearNames()
Stream.resetCount()
- input1 = Input('in1')
- input2 = Input('in2')
+ input1 = Input("in1")
+ input2 = Input("in2")
rel1 = Fir(input1.tw(0.05))
rel2 = Fir(input1.tw(0.01))
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.02,0.02]))
- fun = Output('out',rel1+rel2+rel3+rel4)
+ rel4 = Fir(input2.tw([-0.02, 0.02]))
+ fun = Output("out", rel1 + rel2 + rel3 + rel4)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
-
- list_of_dimensions = {'Fir2':[5,1], 'Fir5':[1,1], 'Fir8':[5,1], 'Fir11':[4,1]}
- for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items():
- self.assertEqual(list_of_dimensions[key],list(value.weights.shape))
-
+
+ list_of_dimensions = {
+ "Fir2": [5, 1],
+ "Fir5": [1, 1],
+ "Fir8": [5, 1],
+ "Fir11": [4, 1],
+ }
+ for key, value in {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items():
+ self.assertEqual(list_of_dimensions[key], list(value.weights.shape))
+
def test_network_building_tw2(self):
Stream.resetCount()
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.02,0.02]))
- rel5 = Fir(input2.tw([-0.03,0.03]))
+ rel4 = Fir(input2.tw([-0.02, 0.02]))
+ rel5 = Fir(input2.tw([-0.03, 0.03]))
rel6 = Fir(input2.tw([-0.03, 0]))
rel7 = Fir(input2.tw(0.03))
- fun = Output('out',rel3+rel4+rel5+rel6+rel7)
+ fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- self.assertEqual(test._max_n_samples, 8) # 5 samples + 3 samples of the horizon
- self.assertEqual({'in2': 8} , test._input_n_samples)
-
- list_of_dimensions = {'Fir2':[5,1], 'Fir5':[4,1], 'Fir8':[6,1], 'Fir11':[3,1], 'Fir14':[3,1]}
- for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items():
- self.assertEqual(list_of_dimensions[key],list(value.weights.shape))
+ self.assertEqual(test._max_n_samples, 8) # 5 samples + 3 samples of the horizon
+ self.assertEqual({"in2": 8}, test._input_n_samples)
+
+ list_of_dimensions = {
+ "Fir2": [5, 1],
+ "Fir5": [4, 1],
+ "Fir8": [6, 1],
+ "Fir11": [3, 1],
+ "Fir14": [3, 1],
+ }
+ for key, value in {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items():
+ self.assertEqual(list_of_dimensions[key], list(value.weights.shape))
def test_network_building_tw3(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.01,0.03]))
- rel5 = Fir(input2.tw([-0.04,0.01]))
- fun = Output('out',rel3+rel4+rel5)
+ rel4 = Fir(input2.tw([-0.01, 0.03]))
+ rel5 = Fir(input2.tw([-0.04, 0.01]))
+ fun = Output("out", rel3 + rel4 + rel5)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = [[5,1], [4,1], [5,1]]
- for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
+ list_of_dimensions = [[5, 1], [4, 1], [5, 1]]
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
def test_network_building_tw_with_offest(self):
NeuObj.clearNames()
Stream.resetCount()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.tw(0.05))
- rel4 = Fir(input2.tw([-0.04,0.02]))
- rel5 = Fir(input2.tw([-0.04,0.02],offset=0))
- rel6 = Fir(input2.tw([-0.04,0.02],offset=0.01))
- fun = Output('out',rel3+rel4+rel5+rel6)
-
+ rel4 = Fir(input2.tw([-0.04, 0.02]))
+ rel5 = Fir(input2.tw([-0.04, 0.02], offset=0))
+ rel6 = Fir(input2.tw([-0.04, 0.02], offset=0.01))
+ fun = Output("out", rel3 + rel4 + rel5 + rel6)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = {'Fir2':[5,1], 'Fir5':[6,1], 'Fir8':[6,1], 'Fir11':[6,1]}
- for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items():
- self.assertEqual(list_of_dimensions[key],list(value.weights.shape))
+ list_of_dimensions = {
+ "Fir2": [5, 1],
+ "Fir5": [6, 1],
+ "Fir8": [6, 1],
+ "Fir11": [6, 1],
+ }
+ for key, value in {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items():
+ self.assertEqual(list_of_dimensions[key], list(value.weights.shape))
def test_network_building_tw_negative(self):
NeuObj.clearNames()
- input2 = Input('in2')
- rel1 = Fir(input2.tw([-0.04,-0.01]))
- rel2 = Fir(input2.tw([-0.06,-0.03]))
- fun = Output('out',rel1+rel2)
+ input2 = Input("in2")
+ rel1 = Fir(input2.tw([-0.04, -0.01]))
+ rel2 = Fir(input2.tw([-0.06, -0.03]))
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = [[3,1], [3,1]]
- for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
+ list_of_dimensions = [[3, 1], [3, 1]]
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
def test_network_building_tw_positive(self):
NeuObj.clearNames()
- input2 = Input('in2')
- rel1 = Fir(input2.tw([0.01,0.04]))
- rel2 = Fir(input2.tw([0.03,0.06]))
- fun = Output('out',rel1+rel2)
+ input2 = Input("in2")
+ rel1 = Fir(input2.tw([0.01, 0.04]))
+ rel2 = Fir(input2.tw([0.03, 0.06]))
+ fun = Output("out", rel1 + rel2)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = [[3,1], [3,1]]
- for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
+ list_of_dimensions = [[3, 1], [3, 1]]
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
def test_network_building_sw_with_offset(self):
Stream.resetCount()
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
rel3 = Fir(input2.sw(5))
- rel4 = Fir(input2.sw([-4,2]))
- rel5 = Fir(input2.sw([-4,2],offset=0))
- rel6 = Fir(input2.sw([-4,2],offset=1))
- fun = Output('out',rel3+rel4+rel5+rel6)
+ rel4 = Fir(input2.sw([-4, 2]))
+ rel5 = Fir(input2.sw([-4, 2], offset=0))
+ rel6 = Fir(input2.sw([-4, 2], offset=1))
+ fun = Output("out", rel3 + rel4 + rel5 + rel6)
test = Modely(visualizer=None, seed=1)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = {'Fir2':[5,1], 'Fir5':[6,1], 'Fir8':[6,1], 'Fir11':[6,1]}
- for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items():
- self.assertEqual(list_of_dimensions[key],list(value.weights.shape))
+ list_of_dimensions = {
+ "Fir2": [5, 1],
+ "Fir5": [6, 1],
+ "Fir8": [6, 1],
+ "Fir11": [6, 1],
+ }
+ for key, value in {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items():
+ self.assertEqual(list_of_dimensions[key], list(value.weights.shape))
def test_network_building_sw_and_tw(self):
NeuObj.clearNames()
- input2 = Input('in2')
+ input2 = Input("in2")
with self.assertRaises(TypeError):
- input2.sw(5)+input2.tw(0.05)
+ input2.sw(5) + input2.tw(0.05)
- rel1 = Fir(input2.sw([-4,2]))+Fir(input2.tw([-0.01,0]))
- fun = Output('out',rel1)
+ rel1 = Fir(input2.sw([-4, 2])) + Fir(input2.tw([-0.01, 0]))
+ fun = Output("out", rel1)
test = Modely(visualizer=None)
- test.addModel('fun',fun)
+ test.addModel("fun", fun)
test.neuralizeModel(0.01)
- list_of_dimensions = [[6,1], [1,1]]
- for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
+ list_of_dimensions = [[6, 1], [1, 1]]
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Fir" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
def test_network_linear(self):
NeuObj.clearNames()
- input = Input('in1')
- rel1 = Linear(input.sw([-4,2]))
+ input = Input("in1")
+ rel1 = Linear(input.sw([-4, 2]))
rel2 = Linear(5)(input.sw([-1, 2]))
- fun1 = Output('out1',rel1)
- fun2 = Output('out2', rel2)
+ fun1 = Output("out1", rel1)
+ fun2 = Output("out2", rel2)
- input5 = Input('in5', dimensions=3)
- rel15 = Linear(input5.sw([-4,2]))
+ input5 = Input("in5", dimensions=3)
+ rel15 = Linear(input5.sw([-4, 2]))
rel25 = Linear(5)(input5.last())
- fun15 = Output('out51',rel15)
- fun25 = Output('out52', rel25)
+ fun15 = Output("out51", rel15)
+ fun25 = Output("out52", rel25)
- test = Modely(seed =1, visualizer=None)
- test.addModel('fun',[fun1,fun2,fun15,fun25])
+ test = Modely(seed=1, visualizer=None)
+ test.addModel("fun", [fun1, fun2, fun15, fun25])
test.neuralizeModel(0.01)
- list_of_dimensions = [[1,1],[1,5],[3,1],[3,5]]
- for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Linear' in k}.items()):
- self.assertEqual(list_of_dimensions[ind],list(value.weights.shape))
+ list_of_dimensions = [[1, 1], [1, 5], [3, 1], [3, 5]]
+ for ind, (key, value) in enumerate(
+ {
+ k: v for k, v in test._model.relation_forward.items() if "Linear" in k
+ }.items()
+ ):
+ self.assertEqual(list_of_dimensions[ind], list(value.weights.shape))
def test_network_linear_interpolation_train(self):
NeuObj.clearNames()
- x = Input('x')
- param = Parameter(name='a', sw=1)
- rel1 = Fir(W=param)(Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[2.0, 4.0, 6.0, 8.0], mode='linear')(x.last()))
- out = Output('out',rel1)
-
- test = Modely(seed = 1, visualizer=None)
- test.addModel('fun',[out])
- test.addMinimize('error', out, x.last())
+ x = Input("x")
+ param = Parameter(name="a", sw=1)
+ rel1 = Fir(W=param)(
+ Interpolation(
+ x_points=[1.0, 2.0, 3.0, 4.0],
+ y_points=[2.0, 4.0, 6.0, 8.0],
+ mode="linear",
+ )(x.last())
+ )
+ out = Output("out", rel1)
+
+ test = Modely(seed=1, visualizer=None)
+ test.addModel("fun", [out])
+ test.addMinimize("error", out, x.last())
test.neuralizeModel(0.01)
- dataset = {'x':np.random.uniform(1,4,100)}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"x": np.random.uniform(1, 4, 100)}
+ test.loadData(name="dataset", source=dataset)
test.trainModel(num_of_epochs=100, train_batch_size=10)
- self.assertAlmostEqual(test.parameters['a'][0][0], 0.5, places=2)
+ self.assertAlmostEqual(test.parameters["a"][0][0], 0.5, places=2)
def test_network_linear_interpolation(self):
NeuObj.clearNames()
- x = Input('x')
- rel1 = Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[1.0, 4.0, 9.0, 16.0], mode='linear')(x.last())
- out = Output('out',rel1)
+ x = Input("x")
+ rel1 = Interpolation(
+ x_points=[1.0, 2.0, 3.0, 4.0], y_points=[1.0, 4.0, 9.0, 16.0], mode="linear"
+ )(x.last())
+ out = Output("out", rel1)
- test = Modely(seed=1,visualizer=None)
- test.addModel('fun',[out])
+ test = Modely(seed=1, visualizer=None)
+ test.addModel("fun", [out])
test.neuralizeModel(0.01)
- inference = test(inputs={'x':[1.5,2.5,3.5]})
- self.assertEqual(inference['out'],[2.5,6.5,12.5])
+ inference = test(inputs={"x": [1.5, 2.5, 3.5]})
+ self.assertEqual(inference["out"], [2.5, 6.5, 12.5])
- x1 = Input('x1')
- rel1 = Interpolation(x_points=[1.0, 4.0, 3.0, 2.0],y_points=[1.0, 16.0, 9.0, 4.0], mode='linear')(x1.last())
- out = Output('out1',rel1)
+ x1 = Input("x1")
+ rel1 = Interpolation(
+ x_points=[1.0, 4.0, 3.0, 2.0], y_points=[1.0, 16.0, 9.0, 4.0], mode="linear"
+ )(x1.last())
+ out = Output("out1", rel1)
test = Modely(visualizer=None)
- test.addModel('fun',[out])
+ test.addModel("fun", [out])
test.neuralizeModel(0.01)
- inference = test(inputs={'x1':[1.5,2.5,3.5]})
- self.assertEqual(inference['out1'],[2.5,6.5,12.5])
+ inference = test(inputs={"x1": [1.5, 2.5, 3.5]})
+ self.assertEqual(inference["out1"], [2.5, 6.5, 12.5])
def test_softmax_and_sigmoid(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y', dimensions=3)
+ x = Input("x")
+ y = Input("y", dimensions=3)
softmax = Softmax(y.last())
sigmoid = Sigmoid(x.last())
- out = Output('softmax',softmax)
- out2 = Output('sigmoid',sigmoid)
+ out = Output("softmax", softmax)
+ out2 = Output("sigmoid", sigmoid)
test = Modely(visualizer=None)
- test.addModel('model',[out,out2])
+ test.addModel("model", [out, out2])
test.neuralizeModel(0.01)
- inference = test(inputs={'x':[-1000.0, 0.0, 1000.0], 'y':[[-1.0,0.0,1.0],[-1000.0,0.0,1000.0],[1.0,2.0,3.0]]})
- self.assertEqual(inference['sigmoid'],[0.0, 0.5, 1.0])
- self.assertEqual(inference['softmax'],[[[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]],
- [[0.0, 0.0, 1.0]],
- [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]]])
+ inference = test(
+ inputs={
+ "x": [-1000.0, 0.0, 1000.0],
+ "y": [[-1.0, 0.0, 1.0], [-1000.0, 0.0, 1000.0], [1.0, 2.0, 3.0]],
+ }
+ )
+ self.assertEqual(inference["sigmoid"], [0.0, 0.5, 1.0])
+ self.assertEqual(
+ inference["softmax"],
+ [
+ [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]],
+ [[0.0, 0.0, 1.0]],
+ [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]],
+ ],
+ )
def test_sech_cosh_function(self):
torch.manual_seed(1)
- input = Input('in1')
+ input = Input("in1")
sech_rel = Sech(input.last())
sech_rel_2 = Sech(input.sw(2))
cosh_rel = Cosh(input.last())
cosh_rel_2 = Cosh(input.sw(2))
- input5 = Input('in5', dimensions=5)
+ input5 = Input("in5", dimensions=5)
sech_rel_5 = Sech(input5.last())
cosh_rel_5 = Cosh(input5.last())
- out1 = Output('sech_out_1', sech_rel)
- out2 = Output('sech_out_2', sech_rel_2)
- out3 = Output('sech_out_3', sech_rel_5)
- out4 = Output('cosh_out_1', cosh_rel)
- out5 = Output('cosh_out_2', cosh_rel_2)
- out6 = Output('cosh_out_3', cosh_rel_5)
+ out1 = Output("sech_out_1", sech_rel)
+ out2 = Output("sech_out_2", sech_rel_2)
+ out3 = Output("sech_out_3", sech_rel_5)
+ out4 = Output("cosh_out_1", cosh_rel)
+ out5 = Output("cosh_out_2", cosh_rel_2)
+ out6 = Output("cosh_out_3", cosh_rel_5)
test = Modely(visualizer=None)
- test.addModel('model',[out1,out2,out3,out4,out5,out6])
+ test.addModel("model", [out1, out2, out3, out4, out5, out6])
test.neuralizeModel(0.01)
- result = test(inputs={'in1':[[3.0],[-2.0]], 'in5':[[4.0,1.0,0.0,-6.0,2.0]]})
- self.TestAlmostEqual([0.2658022344112396], result['sech_out_1'])
- self.TestAlmostEqual([[0.0993279218673706, 0.2658022344112396]], result['sech_out_2'])
- self.TestAlmostEqual([[[0.03661899268627167, 0.6480542421340942, 1.0, 0.004957473836839199, 0.2658022344112396]]], result['sech_out_3'])
- self.TestAlmostEqual([3.762195587158203], result['cosh_out_1'])
- self.TestAlmostEqual([[10.067662239074707, 3.762195587158203]], result['cosh_out_2'])
- self.TestAlmostEqual([[[27.3082332611084, 1.5430806875228882, 1.0, 201.71563720703125, 3.762195587158203]]], result['cosh_out_3'])
+ result = test(
+ inputs={"in1": [[3.0], [-2.0]], "in5": [[4.0, 1.0, 0.0, -6.0, 2.0]]}
+ )
+ self.TestAlmostEqual([0.2658022344112396], result["sech_out_1"])
+ self.TestAlmostEqual(
+ [[0.0993279218673706, 0.2658022344112396]], result["sech_out_2"]
+ )
+ self.TestAlmostEqual(
+ [
+ [
+ [
+ 0.03661899268627167,
+ 0.6480542421340942,
+ 1.0,
+ 0.004957473836839199,
+ 0.2658022344112396,
+ ]
+ ]
+ ],
+ result["sech_out_3"],
+ )
+ self.TestAlmostEqual([3.762195587158203], result["cosh_out_1"])
+ self.TestAlmostEqual(
+ [[10.067662239074707, 3.762195587158203]], result["cosh_out_2"]
+ )
+ self.TestAlmostEqual(
+ [
+ [
+ [
+ 27.3082332611084,
+ 1.5430806875228882,
+ 1.0,
+ 201.71563720703125,
+ 3.762195587158203,
+ ]
+ ]
+ ],
+ result["cosh_out_3"],
+ )
def test_concatenate_time_concatenate(self):
NeuObj.clearNames()
- input = Input('in1')
- input2 = Input('in2')
- concatenate_rel = Concatenate(input.last(),input2.last())
- timeconcatenate_rel = TimeConcatenate(input.last(),input2.last())
- concatenate_tw_rel = Concatenate(input.tw(3),input2.tw(3))
- timeconcatenate_tw_rel = TimeConcatenate(input.tw(3),input2.tw(3))
-
- input3 = Input('in3', dimensions=5)
- input4 = Input('in4', dimensions=5)
-
- concatenate_rel_5 = Concatenate(input3.last(),input4.last())
- timeconcatenate_rel_5 = TimeConcatenate(input3.last(),input4.last())
- concatenate_tw_rel_5 = Concatenate(input3.tw(3),input4.tw(3))
- timeconcatenate_tw_rel_5 = TimeConcatenate(input3.tw(3),input4.tw(3))
-
- out1 = Output('concatenate', concatenate_rel)
- out2 = Output('time_concatenate', timeconcatenate_rel)
- out3 = Output('concatenate_tw', concatenate_tw_rel)
- out4 = Output('time_concatenate_tw', timeconcatenate_tw_rel)
- out5 = Output('concatenate_5', concatenate_rel_5)
- out6 = Output('time_concatenate_5', timeconcatenate_rel_5)
- out7 = Output('concatenate_tw_5', concatenate_tw_rel_5)
- out8 = Output('time_concatenate_tw_5', timeconcatenate_tw_rel_5)
-
- test = Modely(seed=1,visualizer=None)
- test.addModel('model',[out1,out2,out3,out4,out5,out6,out7,out8])
+ input = Input("in1")
+ input2 = Input("in2")
+ concatenate_rel = Concatenate(input.last(), input2.last())
+ timeconcatenate_rel = TimeConcatenate(input.last(), input2.last())
+ concatenate_tw_rel = Concatenate(input.tw(3), input2.tw(3))
+ timeconcatenate_tw_rel = TimeConcatenate(input.tw(3), input2.tw(3))
+
+ input3 = Input("in3", dimensions=5)
+ input4 = Input("in4", dimensions=5)
+
+ concatenate_rel_5 = Concatenate(input3.last(), input4.last())
+ timeconcatenate_rel_5 = TimeConcatenate(input3.last(), input4.last())
+ concatenate_tw_rel_5 = Concatenate(input3.tw(3), input4.tw(3))
+ timeconcatenate_tw_rel_5 = TimeConcatenate(input3.tw(3), input4.tw(3))
+
+ out1 = Output("concatenate", concatenate_rel)
+ out2 = Output("time_concatenate", timeconcatenate_rel)
+ out3 = Output("concatenate_tw", concatenate_tw_rel)
+ out4 = Output("time_concatenate_tw", timeconcatenate_tw_rel)
+ out5 = Output("concatenate_5", concatenate_rel_5)
+ out6 = Output("time_concatenate_5", timeconcatenate_rel_5)
+ out7 = Output("concatenate_tw_5", concatenate_tw_rel_5)
+ out8 = Output("time_concatenate_tw_5", timeconcatenate_tw_rel_5)
+
+ test = Modely(seed=1, visualizer=None)
+ test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8])
test.neuralizeModel(1)
- result = test(inputs={'in1':[[1.0],[2.0],[3.0]], 'in2':[[4.0],[5.0],[6.0]],
- 'in3':[[7.0,8.0,9.0,10.0,11.0],[12.0,13.0,14.0,15.0,16.0],[17.0,18.0,19.0,20.0,21.0]],
- 'in4':[[22.0,23.0,24.0,25.0,26.0],[27.0,28.0,29.0,30.0,31.0],[32.0,33.0,34.0,35.0,36.0]]})
- self.assertEqual((1,1,2), np.array(result['concatenate']).shape)
- self.assertEqual([[[3.0, 6.0]]], result['concatenate'])
- self.assertEqual((1,2), np.array(result['time_concatenate']).shape)
- self.assertEqual([[3.0, 6.0]], result['time_concatenate'])
- self.assertEqual((1,3,2), np.array(result['concatenate_tw']).shape)
- self.assertEqual([[[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]], result['concatenate_tw'])
- self.assertEqual((1,6), np.array(result['time_concatenate_tw']).shape)
- self.assertEqual([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]], result['time_concatenate_tw'])
- self.assertEqual((1,1,10), np.array(result['concatenate_5']).shape)
- self.assertEqual([[[17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]], result['concatenate_5'])
- self.assertEqual((1,2,5), np.array(result['time_concatenate_5']).shape)
- self.assertEqual([[[17.0, 18.0, 19.0, 20.0, 21.0], [32.0, 33.0, 34.0, 35.0, 36.0]]], result['time_concatenate_5'])
- self.assertEqual((1,3,10), np.array(result['concatenate_tw_5']).shape)
- self.assertEqual([[[7.0, 8.0, 9.0, 10.0, 11.0, 22.0, 23.0, 24.0, 25.0, 26.0],
- [12.0, 13.0, 14.0, 15.0, 16.0, 27.0, 28.0, 29.0, 30.0, 31.0],
- [17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]], result['concatenate_tw_5'])
- self.assertEqual((1,6,5), np.array(result['time_concatenate_tw_5']).shape)
- self.assertEqual([[[7.0, 8.0, 9.0, 10.0, 11.0],
- [12.0, 13.0, 14.0, 15.0, 16.0],
- [17.0, 18.0, 19.0, 20.0, 21.0],
- [22.0, 23.0, 24.0, 25.0, 26.0],
- [27.0, 28.0, 29.0, 30.0, 31.0],
- [32.0, 33.0, 34.0, 35.0, 36.0]]], result['time_concatenate_tw_5'])
+ result = test(
+ inputs={
+ "in1": [[1.0], [2.0], [3.0]],
+ "in2": [[4.0], [5.0], [6.0]],
+ "in3": [
+ [7.0, 8.0, 9.0, 10.0, 11.0],
+ [12.0, 13.0, 14.0, 15.0, 16.0],
+ [17.0, 18.0, 19.0, 20.0, 21.0],
+ ],
+ "in4": [
+ [22.0, 23.0, 24.0, 25.0, 26.0],
+ [27.0, 28.0, 29.0, 30.0, 31.0],
+ [32.0, 33.0, 34.0, 35.0, 36.0],
+ ],
+ }
+ )
+ self.assertEqual((1, 1, 2), np.array(result["concatenate"]).shape)
+ self.assertEqual([[[3.0, 6.0]]], result["concatenate"])
+ self.assertEqual((1, 2), np.array(result["time_concatenate"]).shape)
+ self.assertEqual([[3.0, 6.0]], result["time_concatenate"])
+ self.assertEqual((1, 3, 2), np.array(result["concatenate_tw"]).shape)
+ self.assertEqual(
+ [[[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]], result["concatenate_tw"]
+ )
+ self.assertEqual((1, 6), np.array(result["time_concatenate_tw"]).shape)
+ self.assertEqual(
+ [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]], result["time_concatenate_tw"]
+ )
+ self.assertEqual((1, 1, 10), np.array(result["concatenate_5"]).shape)
+ self.assertEqual(
+ [[[17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]],
+ result["concatenate_5"],
+ )
+ self.assertEqual((1, 2, 5), np.array(result["time_concatenate_5"]).shape)
+ self.assertEqual(
+ [[[17.0, 18.0, 19.0, 20.0, 21.0], [32.0, 33.0, 34.0, 35.0, 36.0]]],
+ result["time_concatenate_5"],
+ )
+ self.assertEqual((1, 3, 10), np.array(result["concatenate_tw_5"]).shape)
+ self.assertEqual(
+ [
+ [
+ [7.0, 8.0, 9.0, 10.0, 11.0, 22.0, 23.0, 24.0, 25.0, 26.0],
+ [12.0, 13.0, 14.0, 15.0, 16.0, 27.0, 28.0, 29.0, 30.0, 31.0],
+ [17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0],
+ ]
+ ],
+ result["concatenate_tw_5"],
+ )
+ self.assertEqual((1, 6, 5), np.array(result["time_concatenate_tw_5"]).shape)
+ self.assertEqual(
+ [
+ [
+ [7.0, 8.0, 9.0, 10.0, 11.0],
+ [12.0, 13.0, 14.0, 15.0, 16.0],
+ [17.0, 18.0, 19.0, 20.0, 21.0],
+ [22.0, 23.0, 24.0, 25.0, 26.0],
+ [27.0, 28.0, 29.0, 30.0, 31.0],
+ [32.0, 33.0, 34.0, 35.0, 36.0],
+ ]
+ ],
+ result["time_concatenate_tw_5"],
+ )
def test_equation_learner(self):
NeuObj.clearNames()
- x = Input('x')
- F = Input('F')
-
- def myFun(p1,p2,k1,k2):
- return k1*p1+k2*p2
-
- K1 = Parameter('k1', dimensions = 1, sw = 1,values=[[2.0]])
- K2 = Parameter('k2', dimensions = 1, sw = 1,values=[[3.0]])
- parfun = ParamFun(myFun, parameters_and_constants=[K1,K2])
- parfun2 = ParamFun(myFun, parameters_and_constants=[K1,K2])
- parfun3 = ParamFun(myFun, parameters_and_constants=[K1,K2])
- fuzzi = Fuzzify(centers=[0,1,2,3])
-
- linear_layer_in = Linear(output_dimension=3, W_init=init_constant, W_init_params={'value':0}, b_init=init_constant, b_init_params={'value':0}, b=False)
- linear_layer_in_dim_2 = Linear(output_dimension=3, W_init=init_constant, W_init_params={'value':0}, b_init=init_constant, b_init_params={'value':0}, b=False)
- linear_layer_out = Linear(output_dimension=1, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False)
-
- equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in)(x.last())
- equation_learner_out = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in, linear_out=linear_layer_out)(x.last())
- equation_learner_tw = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in)(x.sw(2))
- equation_learner_multi_tw = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in_dim_2)((x.sw(2),F.sw(2)))
-
- linear_layer_in_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1})
- linear_layer_in_2_dim_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1})
-
- equation_learner_2 = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2)(x.last())
- equation_learner_2_tw = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2)(x.sw(2))
- equation_learner_2_multi_tw = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2_dim_2)((x.sw(2),F.sw(2)))
-
- linear_layer_in_3 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False)
- linear_layer_in_3_dim_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False)
-
- equation_learner_3 = EquationLearner(functions=[parfun, Add, fuzzi], linear_in=linear_layer_in_3)(x.last())
- equation_learner_3_tw = EquationLearner(functions=[parfun2, Add, fuzzi], linear_in=linear_layer_in_3)(x.sw(2))
- equation_learner_3_multi_tw = EquationLearner(functions=[parfun3, Add, fuzzi], linear_in=linear_layer_in_3_dim_2)((x.sw(2),F.sw(2)))
-
- out = Output('el',equation_learner)
- out2 = Output('el_out',equation_learner_out)
- out3 = Output('el_tw',equation_learner_tw)
- out4 = Output('el_multi_tw',equation_learner_multi_tw)
- out5 = Output('el2',equation_learner_2)
- out6 = Output('el2_tw',equation_learner_2_tw)
- out7 = Output('el2_multi_tw',equation_learner_2_multi_tw)
- out8 = Output('el3',equation_learner_3)
- out9 = Output('el3_tw',equation_learner_3_tw)
- out10 = Output('el3_multi_tw',equation_learner_3_multi_tw)
+ x = Input("x")
+ F = Input("F")
+
+ def myFun(p1, p2, k1, k2):
+ return k1 * p1 + k2 * p2
+
+ K1 = Parameter("k1", dimensions=1, sw=1, values=[[2.0]])
+ K2 = Parameter("k2", dimensions=1, sw=1, values=[[3.0]])
+ parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])
+ parfun2 = ParamFun(myFun, parameters_and_constants=[K1, K2])
+ parfun3 = ParamFun(myFun, parameters_and_constants=[K1, K2])
+ fuzzi = Fuzzify(centers=[0, 1, 2, 3])
+
+ linear_layer_in = Linear(
+ output_dimension=3,
+ W_init=init_constant,
+ W_init_params={"value": 0},
+ b_init=init_constant,
+ b_init_params={"value": 0},
+ b=False,
+ )
+ linear_layer_in_dim_2 = Linear(
+ output_dimension=3,
+ W_init=init_constant,
+ W_init_params={"value": 0},
+ b_init=init_constant,
+ b_init_params={"value": 0},
+ b=False,
+ )
+ linear_layer_out = Linear(
+ output_dimension=1,
+ W_init=init_constant,
+ W_init_params={"value": 1},
+ b_init=init_constant,
+ b_init_params={"value": 0},
+ b=False,
+ )
+
+ equation_learner = EquationLearner(
+ functions=[Tan, Sin, Cos], linear_in=linear_layer_in
+ )(x.last())
+ equation_learner_out = EquationLearner(
+ functions=[Tan, Sin, Cos],
+ linear_in=linear_layer_in,
+ linear_out=linear_layer_out,
+ )(x.last())
+ equation_learner_tw = EquationLearner(
+ functions=[Tan, Sin, Cos], linear_in=linear_layer_in
+ )(x.sw(2))
+ equation_learner_multi_tw = EquationLearner(
+ functions=[Tan, Sin, Cos], linear_in=linear_layer_in_dim_2
+ )((x.sw(2), F.sw(2)))
+
+ linear_layer_in_2 = Linear(
+ output_dimension=5, W_init=init_constant, W_init_params={"value": 1}
+ )
+ linear_layer_in_2_dim_2 = Linear(
+ output_dimension=5, W_init=init_constant, W_init_params={"value": 1}
+ )
+
+ equation_learner_2 = EquationLearner(
+ functions=[Add, Mul, Identity], linear_in=linear_layer_in_2
+ )(x.last())
+ equation_learner_2_tw = EquationLearner(
+ functions=[Add, Mul, Identity], linear_in=linear_layer_in_2
+ )(x.sw(2))
+ equation_learner_2_multi_tw = EquationLearner(
+ functions=[Add, Mul, Identity], linear_in=linear_layer_in_2_dim_2
+ )((x.sw(2), F.sw(2)))
+
+ linear_layer_in_3 = Linear(
+ output_dimension=5,
+ W_init=init_constant,
+ W_init_params={"value": 1},
+ b_init=init_constant,
+ b_init_params={"value": 0},
+ b=False,
+ )
+ linear_layer_in_3_dim_2 = Linear(
+ output_dimension=5,
+ W_init=init_constant,
+ W_init_params={"value": 1},
+ b_init=init_constant,
+ b_init_params={"value": 0},
+ b=False,
+ )
+
+ equation_learner_3 = EquationLearner(
+ functions=[parfun, Add, fuzzi], linear_in=linear_layer_in_3
+ )(x.last())
+ equation_learner_3_tw = EquationLearner(
+ functions=[parfun2, Add, fuzzi], linear_in=linear_layer_in_3
+ )(x.sw(2))
+ equation_learner_3_multi_tw = EquationLearner(
+ functions=[parfun3, Add, fuzzi], linear_in=linear_layer_in_3_dim_2
+ )((x.sw(2), F.sw(2)))
+
+ out = Output("el", equation_learner)
+ out2 = Output("el_out", equation_learner_out)
+ out3 = Output("el_tw", equation_learner_tw)
+ out4 = Output("el_multi_tw", equation_learner_multi_tw)
+ out5 = Output("el2", equation_learner_2)
+ out6 = Output("el2_tw", equation_learner_2_tw)
+ out7 = Output("el2_multi_tw", equation_learner_2_multi_tw)
+ out8 = Output("el3", equation_learner_3)
+ out9 = Output("el3_tw", equation_learner_3_tw)
+ out10 = Output("el3_multi_tw", equation_learner_3_multi_tw)
example = Modely(visualizer=None)
- example.addModel('model',[out,out2,out3,out4,out5,out6,out7,out8,out9,out10])
+ example.addModel(
+ "model", [out, out2, out3, out4, out5, out6, out7, out8, out9, out10]
+ )
example.neuralizeModel()
- result = example({'x':[[1.0],[2.0]], 'F':[[3.0],[4.0]]})
- self.assertEqual(result['el'], [[[0.0, 0.0, 1.0]]])
- self.assertEqual(result['el_out'], [1.0])
- self.assertEqual(result['el_tw'], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]])
- self.assertEqual(result['el_multi_tw'], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]])
- self.assertEqual(result['el2'], [[[4.0, 4.0, 2.0]]])
- self.assertEqual(result['el2_tw'], [[[2.0, 1.0, 1.0], [4.0, 4.0, 2.0]]])
- self.assertEqual(result['el2_multi_tw'], [[[8.0, 16.0, 4.0], [12.0, 36.0, 6.0]]])
- self.assertEqual(result['el3'], [[[10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]])
- self.assertEqual(result['el3_tw'], [[[5.0, 2.0, 0.0, 1.0, 0.0, 0.0], [10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]])
- self.assertEqual(result['el3_multi_tw'], [[[20.0, 8.0, 0.0, 0.0, 0.0, 1.0], [30.0, 12.0, 0.0, 0.0, 0.0, 1.0]]])
+ result = example({"x": [[1.0], [2.0]], "F": [[3.0], [4.0]]})
+ self.assertEqual(result["el"], [[[0.0, 0.0, 1.0]]])
+ self.assertEqual(result["el_out"], [1.0])
+ self.assertEqual(result["el_tw"], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]])
+ self.assertEqual(result["el_multi_tw"], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]])
+ self.assertEqual(result["el2"], [[[4.0, 4.0, 2.0]]])
+ self.assertEqual(result["el2_tw"], [[[2.0, 1.0, 1.0], [4.0, 4.0, 2.0]]])
+ self.assertEqual(
+ result["el2_multi_tw"], [[[8.0, 16.0, 4.0], [12.0, 36.0, 6.0]]]
+ )
+ self.assertEqual(result["el3"], [[[10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]])
+ self.assertEqual(
+ result["el3_tw"],
+ [[[5.0, 2.0, 0.0, 1.0, 0.0, 0.0], [10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]],
+ )
+ self.assertEqual(
+ result["el3_multi_tw"],
+ [[[20.0, 8.0, 0.0, 0.0, 0.0, 1.0], [30.0, 12.0, 0.0, 0.0, 0.0, 1.0]]],
+ )
def test_localmodel(self):
NeuObj.clearNames()
- x = Input('x')
- activationA = Fuzzify(2, [0, 1], functions='Triangular')(x.last())
+ x = Input("x")
+ activationA = Fuzzify(2, [0, 1], functions="Triangular")(x.last())
loc = LocalModel(input_function=Fir())(x.tw(1), activationA)
- out = Output('out', loc)
- example = Modely(visualizer=None,seed=5)
- example.addModel('out', out)
+ out = Output("out", loc)
+ example = Modely(visualizer=None, seed=5)
+ example.addModel("out", out)
example.neuralizeModel(0.25)
# The output is 2 samples
- self.assertEqual({'out': [1.7170718908309937, 1.9410502910614014]}, example({'x': [-1, 0, 1, 2, 0]}))
- self.assertEqual({'out': [1.7170718908309937, 1.9410502910614014]}, example({'x': [[-1, 0, 1, 2], [0, 1, 2, 0]]}, sampled=True))
+ self.assertEqual(
+ {"out": [1.7170718908309937, 1.9410502910614014]},
+ example({"x": [-1, 0, 1, 2, 0]}),
+ )
+ self.assertEqual(
+ {"out": [1.7170718908309937, 1.9410502910614014]},
+ example({"x": [[-1, 0, 1, 2], [0, 1, 2, 0]]}, sampled=True),
+ )
def test_arithmetic(self):
NeuObj.clearNames()
- y = Input('y', dimensions=10)
+ y = Input("y", dimensions=10)
- k = Parameter('k', dimensions=1)
+ k = Parameter("k", dimensions=1)
rel1 = y.last() + (5 * y.last())
rel2 = y.last() - (5 * y.last())
rel3 = y.last() + (k * y.last())
@@ -459,32 +687,32 @@ def test_arithmetic(self):
rel8 = y.last() / (k * y.last())
rel9 = Sum(y.last())
- out = Output('out', rel1)
- out2 = Output('out2', rel2)
- out3 = Output('out3', rel3)
- out4 = Output('out4', rel4)
- out5 = Output('out5', rel5)
- out6 = Output('out6', rel6)
- out7 = Output('out7', rel7)
- out8 = Output('out8', rel8)
- out9 = Output('out9', rel9)
+ out = Output("out", rel1)
+ out2 = Output("out2", rel2)
+ out3 = Output("out3", rel3)
+ out4 = Output("out4", rel4)
+ out5 = Output("out5", rel5)
+ out6 = Output("out6", rel6)
+ out7 = Output("out7", rel7)
+ out8 = Output("out8", rel8)
+ out9 = Output("out9", rel9)
example = Modely(visualizer=None, seed=42)
- example.addModel('out', [out,out2,out3,out4,out5,out6,out7,out8,out9])
+ example.addModel("out", [out, out2, out3, out4, out5, out6, out7, out8, out9])
example.neuralizeModel(0.25)
- self.assertEqual(rel1.dim['dim'], 10)
- self.assertEqual(rel2.dim['dim'], 10)
- self.assertEqual(rel3.dim['dim'], 10)
- self.assertEqual(rel4.dim['dim'], 10)
- self.assertEqual(rel5.dim['dim'], 10)
- self.assertEqual(rel6.dim['dim'], 10)
- self.assertEqual(rel7.dim['dim'], 10)
- self.assertEqual(rel8.dim['dim'], 10)
- self.assertEqual(rel9.dim['dim'], 1)
+ self.assertEqual(rel1.dim["dim"], 10)
+ self.assertEqual(rel2.dim["dim"], 10)
+ self.assertEqual(rel3.dim["dim"], 10)
+ self.assertEqual(rel4.dim["dim"], 10)
+ self.assertEqual(rel5.dim["dim"], 10)
+ self.assertEqual(rel6.dim["dim"], 10)
+ self.assertEqual(rel7.dim["dim"], 10)
+ self.assertEqual(rel8.dim["dim"], 10)
+ self.assertEqual(rel9.dim["dim"], 1)
def test_rungekutta(self):
NeuObj.clearNames()
- x = Input('x')
+ x = Input("x")
def fun(x):
return x
@@ -493,24 +721,24 @@ def fun(x):
rk2_rel = RK2(f=fun)(x.last())
rk4_rel = RK4(f=fun)(x.last())
- out_fe = Output('fe', fe_rel)
- out_rk2 = Output('rk2', rk2_rel)
- out_rk4 = Output('rk4', rk4_rel)
+ out_fe = Output("fe", fe_rel)
+ out_rk2 = Output("rk2", rk2_rel)
+ out_rk4 = Output("rk4", rk4_rel)
model = Modely(visualizer=None)
- model.addModel('model', [out_fe, out_rk2, out_rk4])
+ model.addModel("model", [out_fe, out_rk2, out_rk4])
model.neuralizeModel(1)
- inputs = {'x': [[1.0], [2.0]]}
+ inputs = {"x": [[1.0], [2.0]]}
result = model(inputs=inputs)
# expected: fe -> [2.0, 4.0], rk2 -> [2.5, 5.0]
- self.TestAlmostEqual([2.0, 4.0], result['fe'])
- self.TestAlmostEqual([2.5, 5.0], result['rk2'])
- self.TestAlmostEqual([2.708333, 5.41666], result['rk4'])
+ self.TestAlmostEqual([2.0, 4.0], result["fe"])
+ self.TestAlmostEqual([2.5, 5.0], result["rk2"])
+ self.TestAlmostEqual([2.708333, 5.41666], result["rk4"])
# now test with step h = 0.1
model.neuralizeModel(0.1, clear_model=True)
result = model(inputs=inputs)
- self.TestAlmostEqual([1.1, 2.2], result['fe'])
- self.TestAlmostEqual([1.105, 2.21], result['rk2'])
- self.TestAlmostEqual([1.10517, 2.21034], result['rk4'])
\ No newline at end of file
+ self.TestAlmostEqual([1.1, 2.2], result["fe"])
+ self.TestAlmostEqual([1.105, 2.21], result["rk2"])
+ self.TestAlmostEqual([1.10517, 2.21034], result["rk4"])
diff --git a/tests/test_parameters_of_train.py b/tests/test_parameters_of_train.py
index ab73f05b..f99e2cf8 100644
--- a/tests/test_parameters_of_train.py
+++ b/tests/test_parameters_of_train.py
@@ -1,4 +1,6 @@
-import unittest, os, sys
+import unittest
+import os
+import sys
import numpy as np
from nnodely import *
@@ -13,239 +15,324 @@
# 13 Tests
# Test the train parameter and the optimizer options
-data_folder = os.path.join(os.path.dirname(__file__), 'data/')
+data_folder = os.path.join(os.path.dirname(__file__), "data/")
+
def funIn(x, w):
return x * w
+
def funOut(x, w):
return x / w
-def linear_fun(x,a,b):
- return x*a+b
+
+def linear_fun(x, a, b):
+ return x * a + b
+
class ModelyTrainingTestParameter(unittest.TestCase):
def test_network_mass_spring_damper(self):
NeuObj.clearNames()
- x = Input('x') # Position
- F = Input('F') # Force
+ x = Input("x") # Position
+ F = Input("F") # Force
# List the output of the model
- x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last()))
+ x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last()))
# Add the neural model to the nnodely structure and neuralization of the model
test = Modely(visualizer=None)
- test.addModel('x_z',x_z)
- test.addMinimize('next-pos', x.z(-1), x_z, 'mse')
+ test.addModel("x_z", x_z)
+ test.addMinimize("next-pos", x.z(-1), x_z, "mse")
# Create the neural network
- test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset
+ test.neuralizeModel(
+ sample_time=0.05
+ ) # The sampling time depends to the dataset
# Data load
- data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.trainModel(splits=[80,10,10])
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.trainModel(splits=[80, 10, 10])
tp = test.getTrainingInfo()
- self.assertEqual((15-6), test._num_of_samples['dataset'])
- self.assertEqual(round((15-6)*80/100),tp['n_samples_train'])
- self.assertEqual(round((15-6)*10/100),tp['n_samples_val'])
- self.assertEqual(round((15-6)*10/100),tp['n_samples_test'])
- self.assertEqual(round((15-6)*80/100),tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(1,tp['val_batch_size'])
- self.assertEqual(100,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
+ self.assertEqual((15 - 6), test._num_of_samples["dataset"])
+ self.assertEqual(round((15 - 6) * 80 / 100), tp["n_samples_train"])
+ self.assertEqual(round((15 - 6) * 10 / 100), tp["n_samples_val"])
+ self.assertEqual(round((15 - 6) * 10 / 100), tp["n_samples_test"])
+ self.assertEqual(round((15 - 6) * 80 / 100), tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(1, tp["val_batch_size"])
+ self.assertEqual(100, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
def test_build_dataset_batch_connect(self):
NeuObj.clearNames()
data_x = np.random.rand(500) * 20 - 10
data_a = 2
data_b = -3
- dataset = {'in1': data_x, 'out': linear_fun(data_x, data_a, data_b)}
+ dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)}
- input1 = Input('in1')
- out = Input('out')
+ input1 = Input("in1")
+ out = Input("out")
rel1 = Fir(input1.tw(0.05))
- y = Output('y', rel1)
+ y = Output("y", rel1)
test = Modely(visualizer=None, seed=42)
- test.addModel('y',y)
- test.addMinimize('pos', out.next(), y)
+ test.addModel("y", y)
+ test.addMinimize("pos", out.next(), y)
test.neuralizeModel(0.01)
- test.loadData(name='dataset',source=dataset)
+ test.loadData(name="dataset", source=dataset)
training_params = {}
- training_params['train_batch_size'] = 4
- training_params['val_batch_size'] = 4
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
- test.trainModel(splits=[70,20,10], closed_loop={'in1':'y'}, prediction_samples=5, training_params = training_params)
+ training_params["train_batch_size"] = 4
+ training_params["val_batch_size"] = 4
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
+ test.trainModel(
+ splits=[70, 20, 10],
+ closed_loop={"in1": "y"},
+ prediction_samples=5,
+ training_params=training_params,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(346,tp['n_samples_train']) ## ((500 - 5) * 0.7) = 346
- self.assertEqual(99,tp['n_samples_val']) ## ((500 - 5) * 0.2) = 99
- self.assertEqual(50,tp['n_samples_test']) ## ((500 - 5) * 0.1) = 50
- self.assertEqual(495, test._num_of_samples['dataset']) ## 500 - 5 = 495
- self.assertEqual(4,tp['train_batch_size'])
- self.assertEqual(4,tp['val_batch_size'])
- self.assertEqual(5,tp['num_of_epochs'])
- self.assertEqual(5, tp['prediction_samples'])
- self.assertEqual(0, tp['step'])
- self.assertEqual({'in1':'y'}, tp['closed_loop'])
- self.assertEqual(0.1,tp['optimizer_defaults']['lr'])
- self.assertEqual(((494*0.7)-tp['prediction_samples'])//4, tp['update_per_epochs'])
- #self.assertEqual(1, tp['unused_samples'])
+ self.assertEqual(346, tp["n_samples_train"]) ## ((500 - 5) * 0.7) = 346
+ self.assertEqual(99, tp["n_samples_val"]) ## ((500 - 5) * 0.2) = 99
+ self.assertEqual(50, tp["n_samples_test"]) ## ((500 - 5) * 0.1) = 50
+ self.assertEqual(495, test._num_of_samples["dataset"]) ## 500 - 5 = 495
+ self.assertEqual(4, tp["train_batch_size"])
+ self.assertEqual(4, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(5, tp["prediction_samples"])
+ self.assertEqual(0, tp["step"])
+ self.assertEqual({"in1": "y"}, tp["closed_loop"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(
+ ((494 * 0.7) - tp["prediction_samples"]) // 4, tp["update_per_epochs"]
+ )
+ # self.assertEqual(1, tp['unused_samples'])
def test_recurrent_train_closed_loop(self):
NeuObj.clearNames()
data_x = np.random.rand(500) * 20 - 10
data_a = 2
data_b = -3
- dataset = {'in1': data_x, 'out': linear_fun(data_x, data_a, data_b)}
+ dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)}
- x = Input('in1')
- p = Parameter('p', dimensions=1, sw=1, values=[[1.0]])
+ x = Input("in1")
+ p = Parameter("p", dimensions=1, sw=1, values=[[1.0]])
fir = Fir(W=p)(x.last())
- out = Output('out', fir)
+ out = Output("out", fir)
test = Modely(visualizer=None, seed=42)
- test.addModel('out',out)
- test.addMinimize('pos', x.next(), out)
+ test.addModel("out", out)
+ test.addMinimize("pos", x.next(), out)
test.neuralizeModel(0.01)
- test.loadData(name='dataset',source=dataset)
+ test.loadData(name="dataset", source=dataset)
training_params = {}
- training_params['train_batch_size'] = 4
- training_params['val_batch_size'] = 4
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 50
-
- test.trainModel(splits=[100,0,0], closed_loop={'in1':'out'}, prediction_samples=3, step=1, training_params = training_params)
+ training_params["train_batch_size"] = 4
+ training_params["val_batch_size"] = 4
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 50
+
+ test.trainModel(
+ splits=[100, 0, 0],
+ closed_loop={"in1": "out"},
+ prediction_samples=3,
+ step=1,
+ training_params=training_params,
+ )
tp = test.getTrainingInfo()
- self.assertEqual((len(data_x)-1)*100/100,tp['n_samples_train']) ## ((500 - 1) * 1) = 499
- self.assertEqual(0,tp['n_samples_val']) ## ((500 - 5) * 0) = 0
- self.assertEqual(0,tp['n_samples_test']) ## ((500 - 5) * 0) = 0
- self.assertEqual((len(data_x)-1) * 100 / 100, test._num_of_samples['dataset'])
- self.assertEqual(4,tp['train_batch_size'])
- self.assertEqual(4,tp['val_batch_size'])
- self.assertEqual(50,tp['num_of_epochs'])
- self.assertEqual(3, tp['prediction_samples'])
- self.assertEqual(1, tp['step'])
- self.assertEqual({'in1':'out'}, tp['closed_loop'])
- self.assertEqual(0.1,tp['optimizer_defaults']['lr'])
-
- self.assertEqual(99, tp['update_per_epochs']) ## 499 // (4+1) = 99
- #self.assertEqual(100, tp['unused_samples']) ## 99 * step + 1
+ self.assertEqual(
+ (len(data_x) - 1) * 100 / 100, tp["n_samples_train"]
+ ) ## ((500 - 1) * 1) = 499
+ self.assertEqual(0, tp["n_samples_val"]) ## ((500 - 5) * 0) = 0
+ self.assertEqual(0, tp["n_samples_test"]) ## ((500 - 5) * 0) = 0
+ self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples["dataset"])
+ self.assertEqual(4, tp["train_batch_size"])
+ self.assertEqual(4, tp["val_batch_size"])
+ self.assertEqual(50, tp["num_of_epochs"])
+ self.assertEqual(3, tp["prediction_samples"])
+ self.assertEqual(1, tp["step"])
+ self.assertEqual({"in1": "out"}, tp["closed_loop"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+
+ self.assertEqual(99, tp["update_per_epochs"]) ## 499 // (4+1) = 99
+ # self.assertEqual(100, tp['unused_samples']) ## 99 * step + 1
def test_recurrent_train_single_close_loop(self):
NeuObj.clearNames()
data_x = np.array(list(range(1, 101, 1)), dtype=np.float32)
- dataset = {'x': data_x, 'y': 2 * data_x}
+ dataset = {"x": data_x, "y": 2 * data_x}
- x = Input('x')
- y = Input('y')
- out = Output('out', Fir(x.last()))
+ x = Input("x")
+ y = Input("y")
+ out = Output("out", Fir(x.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('out', out)
- test.addMinimize('pos', y.last(), out)
+ test.addModel("out", out)
+ test.addMinimize("pos", y.last(), out)
test.neuralizeModel(0.01)
- test.loadData(name='dataset', source=dataset)
+ test.loadData(name="dataset", source=dataset)
training_params = {}
- training_params['train_batch_size'] = 4
- training_params['val_batch_size'] = 4
- training_params['lr'] = 0.01
- training_params['num_of_epochs'] = 50
- test.trainModel(splits=[80, 20, 0], closed_loop={'x': 'out'}, prediction_samples=3, step=2, training_params=training_params)
+ training_params["train_batch_size"] = 4
+ training_params["val_batch_size"] = 4
+ training_params["lr"] = 0.01
+ training_params["num_of_epochs"] = 50
+ test.trainModel(
+ splits=[80, 20, 0],
+ closed_loop={"x": "out"},
+ prediction_samples=3,
+ step=2,
+ training_params=training_params,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(round((len(data_x) - 0) * 80 / 100), tp['n_samples_train'])
- self.assertEqual((len(data_x) - 0) * 20 / 100, tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual((len(data_x) - 0) * 100 / 100, test._num_of_samples['dataset'])
- self.assertEqual(4, tp['train_batch_size'])
- self.assertEqual(4, tp['val_batch_size'])
- self.assertEqual(50, tp['num_of_epochs'])
- self.assertEqual(3, tp['prediction_samples'])
- self.assertEqual(2, tp['step'])
- self.assertEqual({'x': 'out'}, tp['closed_loop'])
- self.assertEqual(0.01, tp['optimizer_defaults']['lr'])
- self.assertEqual(((100*0.8)-3)//(4+2), tp['update_per_epochs'])
- #self.assertEqual(100*0.8 - (tp['update_per_epochs']*4) - 3, tp['unused_samples'])
+ self.assertEqual(round((len(data_x) - 0) * 80 / 100), tp["n_samples_train"])
+ self.assertEqual((len(data_x) - 0) * 20 / 100, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual((len(data_x) - 0) * 100 / 100, test._num_of_samples["dataset"])
+ self.assertEqual(4, tp["train_batch_size"])
+ self.assertEqual(4, tp["val_batch_size"])
+ self.assertEqual(50, tp["num_of_epochs"])
+ self.assertEqual(3, tp["prediction_samples"])
+ self.assertEqual(2, tp["step"])
+ self.assertEqual({"x": "out"}, tp["closed_loop"])
+ self.assertEqual(0.01, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(((100 * 0.8) - 3) // (4 + 2), tp["update_per_epochs"])
+ # self.assertEqual(100*0.8 - (tp['update_per_epochs']*4) - 3, tp['unused_samples'])
def test_recurrent_train_multiple_close_loop(self):
NeuObj.clearNames()
data_x = np.array(list(range(1, 101, 1)), dtype=np.float32)
- dataset = {'x': data_x, 'y': 2 * data_x}
+ dataset = {"x": data_x, "y": 2 * data_x}
- x = Input('x')
- y = Input('y')
- out_x = Output('out_x', Fir(x.last()))
- out_y = Output('out_y', Fir(y.last()))
+ x = Input("x")
+ y = Input("y")
+ out_x = Output("out_x", Fir(x.last()))
+ out_y = Output("out_y", Fir(y.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('out_x', out_x)
- test.addModel('out_y', out_y)
- test.addMinimize('pos_x', x.next(), out_x)
- test.addMinimize('pos_y', y.next(), out_y)
+ test.addModel("out_x", out_x)
+ test.addModel("out_y", out_y)
+ test.addMinimize("pos_x", x.next(), out_x)
+ test.addMinimize("pos_y", y.next(), out_y)
test.neuralizeModel(0.01)
- test.loadData(name='dataset', source=dataset)
+ test.loadData(name="dataset", source=dataset)
training_params = {}
- training_params['train_batch_size'] = 4
- training_params['val_batch_size'] = 4
- training_params['lr'] = 0.01
- training_params['num_of_epochs'] = 32
-
- test.trainModel(splits=[80, 20, 0], closed_loop={'x': 'out_x', 'y': 'out_y'}, prediction_samples=3,
- training_params=training_params)
+ training_params["train_batch_size"] = 4
+ training_params["val_batch_size"] = 4
+ training_params["lr"] = 0.01
+ training_params["num_of_epochs"] = 32
+
+ test.trainModel(
+ splits=[80, 20, 0],
+ closed_loop={"x": "out_x", "y": "out_y"},
+ prediction_samples=3,
+ training_params=training_params,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(round((len(data_x) - 1) * 80 / 100), tp['n_samples_train'])
- self.assertEqual(round((len(data_x) - 1) * 20 / 100), tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples['dataset'])
- self.assertEqual(4, tp['train_batch_size'])
- self.assertEqual(4, tp['val_batch_size'])
- self.assertEqual(32, tp['num_of_epochs'])
- self.assertEqual(3, tp['prediction_samples'])
- self.assertEqual(0, tp['step'])
- self.assertEqual({'x': 'out_x', 'y': 'out_y'}, tp['closed_loop'])
- self.assertEqual(0.01, tp['optimizer_defaults']['lr'])
- self.assertEqual(19, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(round((len(data_x) - 1) * 80 / 100), tp["n_samples_train"])
+ self.assertEqual(round((len(data_x) - 1) * 20 / 100), tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples["dataset"])
+ self.assertEqual(4, tp["train_batch_size"])
+ self.assertEqual(4, tp["val_batch_size"])
+ self.assertEqual(32, tp["num_of_epochs"])
+ self.assertEqual(3, tp["prediction_samples"])
+ self.assertEqual(0, tp["step"])
+ self.assertEqual({"x": "out_x", "y": "out_y"}, tp["closed_loop"])
+ self.assertEqual(0.01, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(19, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_build_dataset_batch(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
- rel1 = Output('out1',Fir(input1.tw(0.05)))
+ input1 = Input("in1")
+ output = Input("out")
+ rel1 = Output("out1", Fir(input1.tw(0.05)))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1)
+ test.addMinimize("out", output.z(-1), rel1)
test.neuralizeModel(0.01)
- data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,5,1), test._data['dataset']['in1'].shape)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
training_params = {}
- training_params['train_batch_size'] = 1
- training_params['val_batch_size'] = 1
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
+ training_params["train_batch_size"] = 1
+ training_params["val_batch_size"] = 1
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
with self.assertRaises(RuntimeError):
- test.trainModel(splits=[70,20,10],training_params = training_params)
- test.addModel('out',rel1)
+ test.trainModel(splits=[70, 20, 10], training_params=training_params)
+ test.addModel("out", rel1)
test.neuralizeModel(0.01)
- test.trainModel(splits=[70,20,10],training_params = training_params)
+ test.trainModel(splits=[70, 20, 10], training_params=training_params)
tp = test.getTrainingInfo()
# 15 lines in the dataset
@@ -254,47 +341,76 @@ def test_build_dataset_batch(self):
# 10 / 1 * 0.2 = 2 for validation
# 10 / 1 * 0.1 = 1 for test
- self.assertEqual(7,tp['n_samples_train'])
- self.assertEqual(2,tp['n_samples_val'])
- self.assertEqual(1,tp['n_samples_test'])
- self.assertEqual(10, test._num_of_samples['dataset'])
- self.assertEqual(1,tp['train_batch_size'])
- self.assertEqual(1,tp['val_batch_size'])
- self.assertEqual(5,tp['num_of_epochs'])
- self.assertEqual(0.1,tp['optimizer_defaults']['lr'])
-
- n_samples = tp['n_samples_train']
- batch_size = tp['train_batch_size']
+ self.assertEqual(7, tp["n_samples_train"])
+ self.assertEqual(2, tp["n_samples_val"])
+ self.assertEqual(1, tp["n_samples_test"])
+ self.assertEqual(10, test._num_of_samples["dataset"])
+ self.assertEqual(1, tp["train_batch_size"])
+ self.assertEqual(1, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+
+ n_samples = tp["n_samples_train"]
+ batch_size = tp["train_batch_size"]
list_of_batch_indexes = range(0, n_samples - batch_size + 1, batch_size)
- self.assertEqual(len(list_of_batch_indexes), tp['update_per_epochs'])
- #self.assertEqual(n_samples - list_of_batch_indexes[-1] - batch_size, tp['unused_samples'])
+ self.assertEqual(len(list_of_batch_indexes), tp["update_per_epochs"])
+ # self.assertEqual(n_samples - list_of_batch_indexes[-1] - batch_size, tp['unused_samples'])
- test.trainModel(splits=[70, 20, 10], training_params=training_params, num_of_epochs=100)
+ test.trainModel(
+ splits=[70, 20, 10], training_params=training_params, num_of_epochs=100
+ )
tp = test.getTrainingInfo()
- self.assertEqual(100, tp['num_of_epochs'])
+ self.assertEqual(100, tp["num_of_epochs"])
def test_build_dataset_batch2(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
- rel1 = Output('out1',Fir(input1.tw(0.05)))
+ input1 = Input("in1")
+ output = Input("out")
+ rel1 = Output("out1", Fir(input1.tw(0.05)))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1)
- test.addModel('model', rel1)
+ test.addMinimize("out", output.z(-1), rel1)
+ test.addModel("model", rel1)
test.neuralizeModel(0.01)
- data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset',source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10,5,1), test._data['dataset']['in1'].shape)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
training_params = {}
- training_params['train_batch_size'] = 25
- training_params['val_batch_size'] = 25
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
- test.trainModel(splits=[50,0,50],training_params = training_params)
+ training_params["train_batch_size"] = 25
+ training_params["val_batch_size"] = 25
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
+ test.trainModel(splits=[50, 0, 50], training_params=training_params)
tp = test.getTrainingInfo()
# 15 lines in the dataset
@@ -303,38 +419,64 @@ def test_build_dataset_batch2(self):
# 10 / 1 * 0.5 = 5 for training
# 10 / 1 * 0.0 = 0 for validation
# 10 / 1 * 0.5 = 5 for test
- self.assertEqual((15 - 5), test._num_of_samples['dataset'])
- self.assertEqual(round((15 - 5) * 50 / 100), tp['n_samples_train'])
- self.assertEqual(round((15 - 5) * 0 / 100), tp['n_samples_val'])
- self.assertEqual(round((15 - 5) * 50 / 100), tp['n_samples_test'])
- self.assertEqual(round((15 - 5) * 50 / 100), tp['train_batch_size'])
- self.assertEqual(25, tp['val_batch_size'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual((15 - 5), test._num_of_samples["dataset"])
+ self.assertEqual(round((15 - 5) * 50 / 100), tp["n_samples_train"])
+ self.assertEqual(round((15 - 5) * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(round((15 - 5) * 50 / 100), tp["n_samples_test"])
+ self.assertEqual(round((15 - 5) * 50 / 100), tp["train_batch_size"])
+ self.assertEqual(25, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_build_dataset_batch3(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
- rel1 = Output('out1',Fir(input1.tw(0.05)))
+ input1 = Input("in1")
+ output = Input("out")
+ rel1 = Output("out1", Fir(input1.tw(0.05)))
- test = Modely(workspace='results', visualizer=None)
- test.addMinimize('out', output.next(), rel1)
- test.addModel('model', rel1)
+ test = Modely(workspace="results", visualizer=None)
+ test.addMinimize("out", output.next(), rel1)
+ test.addModel("model", rel1)
test.neuralizeModel(0.01)
- data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3',
- 'in1', 'in2', 'time']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10, 5, 1), test._data['dataset']['in1'].shape)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
training_params = {}
- training_params['train_batch_size'] = 2
- training_params['val_batch_size'] = 2
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
+ training_params["train_batch_size"] = 2
+ training_params["val_batch_size"] = 2
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
test.trainModel(splits=[40, 30, 30], training_params=training_params)
tp = test.getTrainingInfo()
# 15 lines in the dataset
@@ -345,38 +487,64 @@ def test_build_dataset_batch3(self):
# 10 * 0.4 = 2 for training
# 10 * 0.3 = 1 for validation
# 10 * 0.3 = 1 for test
- self.assertEqual((15 - 5), test._num_of_samples['dataset'])
- self.assertEqual(round((15 - 5) * 40 / 100), tp['n_samples_train'])
- self.assertEqual(round((15 - 5) * 30 / 100), tp['n_samples_val'])
- self.assertEqual(round((15 - 5) * 30 / 100), tp['n_samples_test'])
- self.assertEqual(2, tp['train_batch_size'])
- self.assertEqual(2, tp['val_batch_size'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual(2, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual((15 - 5), test._num_of_samples["dataset"])
+ self.assertEqual(round((15 - 5) * 40 / 100), tp["n_samples_train"])
+ self.assertEqual(round((15 - 5) * 30 / 100), tp["n_samples_val"])
+ self.assertEqual(round((15 - 5) * 30 / 100), tp["n_samples_test"])
+ self.assertEqual(2, tp["train_batch_size"])
+ self.assertEqual(2, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(2, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_build_dataset_batch4(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
- rel1 = Output('out1',Fir(input1.tw(0.05)))
+ input1 = Input("in1")
+ output = Input("out")
+ rel1 = Output("out1", Fir(input1.tw(0.05)))
test = Modely(visualizer=None)
- test.addMinimize('out', output.z(-1), rel1)
- test.addModel('model', rel1)
+ test.addMinimize("out", output.z(-1), rel1)
+ test.addModel("model", rel1)
test.neuralizeModel(0.01)
- data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3',
- 'in1', 'in2', 'time']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- self.assertEqual((10, 5, 1), test._data['dataset']['in1'].shape)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape)
training_params = {}
- training_params['train_batch_size'] = 2
- training_params['val_batch_size'] = 2
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
+ training_params["train_batch_size"] = 2
+ training_params["val_batch_size"] = 2
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
test.trainModel(splits=[80, 10, 10], training_params=training_params)
tp = test.getTrainingInfo()
@@ -388,42 +556,44 @@ def test_build_dataset_batch4(self):
# 10 * 0.8 = 8 for training
# 10 * 0.1 = 1 for validation
# 10 * 0.1 = 1 for test
- self.assertEqual((15 - 5), test._num_of_samples['dataset'])
- self.assertEqual(round((15 - 5) * 80 / 100), tp['n_samples_train'])
- self.assertEqual(round((15 - 5) * 10 / 100), tp['n_samples_val'])
- self.assertEqual(round((15 - 5) * 10 / 100), tp['n_samples_test'])
- self.assertEqual(2, tp['train_batch_size'])
- self.assertEqual(1, tp['val_batch_size'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual(4, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual((15 - 5), test._num_of_samples["dataset"])
+ self.assertEqual(round((15 - 5) * 80 / 100), tp["n_samples_train"])
+ self.assertEqual(round((15 - 5) * 10 / 100), tp["n_samples_val"])
+ self.assertEqual(round((15 - 5) * 10 / 100), tp["n_samples_test"])
+ self.assertEqual(2, tp["train_batch_size"])
+ self.assertEqual(1, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(4, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_build_dataset_from_code(self):
NeuObj.clearNames()
- input1 = Input('in1')
- output = Input('out')
- rel1 = Output('out1',Fir(input1.tw(0.05)))
+ input1 = Input("in1")
+ output = Input("out")
+ rel1 = Output("out1", Fir(input1.tw(0.05)))
test = Modely(visualizer=None)
- test.addMinimize('out', output.next(), rel1)
- test.addModel('model', rel1)
+ test.addMinimize("out", output.next(), rel1)
+ test.addModel("model", rel1)
test.neuralizeModel(0.01)
x_size = 20
data_x = np.random.rand(x_size) * 20 - 10
data_a = 2
data_b = -3
- dataset = {'in1': data_x, 'out': data_x * data_a + data_b}
+ dataset = {"in1": data_x, "out": data_x * data_a + data_b}
- test.loadData(name='dataset', source=dataset, skiplines=0)
- self.assertEqual((15, 5, 1), test._data['dataset']['in1'].shape) ## 20 data - 5 tw = 15 sample | 0.05/0.01 = 5 in1
+ test.loadData(name="dataset", source=dataset, skiplines=0)
+ self.assertEqual(
+ (15, 5, 1), test._data["dataset"]["in1"].shape
+ ) ## 20 data - 5 tw = 15 sample | 0.05/0.01 = 5 in1
training_params = {}
- training_params['train_batch_size'] = 2
- training_params['val_batch_size'] = 2
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
+ training_params["train_batch_size"] = 2
+ training_params["val_batch_size"] = 2
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
test.trainModel(splits=[80, 20, 0], training_params=training_params)
tp = test.getTrainingInfo()
@@ -435,146 +605,200 @@ def test_build_dataset_from_code(self):
# 15 * 0.8 = 12 for training
# 15 * 0.2 = 3 for validation
# 15 * 0.0 = 0 for test
- self.assertEqual((20 - 5), test._num_of_samples['dataset'])
- self.assertEqual(round((20 - 5) * 80 / 100), tp['n_samples_train'])
- self.assertEqual(round((20 - 5) * 20 / 100), tp['n_samples_val'])
- self.assertEqual(round((20 - 5) * 0 / 100), tp['n_samples_test'])
- self.assertEqual(2, tp['train_batch_size'])
- self.assertEqual(2, tp['val_batch_size'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual(6, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual((20 - 5), test._num_of_samples["dataset"])
+ self.assertEqual(round((20 - 5) * 80 / 100), tp["n_samples_train"])
+ self.assertEqual(round((20 - 5) * 20 / 100), tp["n_samples_val"])
+ self.assertEqual(round((20 - 5) * 0 / 100), tp["n_samples_test"])
+ self.assertEqual(2, tp["train_batch_size"])
+ self.assertEqual(2, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(6, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_network_multi_dataset(self):
NeuObj.clearNames()
- train_folder = os.path.join(os.path.dirname(__file__), 'data/')
- val_folder = os.path.join(os.path.dirname(__file__), 'val_data/')
- test_folder = os.path.join(os.path.dirname(__file__), 'test_data/')
+ train_folder = os.path.join(os.path.dirname(__file__), "data/")
+ val_folder = os.path.join(os.path.dirname(__file__), "val_data/")
+ test_folder = os.path.join(os.path.dirname(__file__), "test_data/")
- x = Input('x') # Position
- F = Input('F') # Force
+ x = Input("x") # Position
+ F = Input("F") # Force
# List the output of the model
- x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last()))
+ x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last()))
# Add the neural model to the nnodely structure and neuralization of the model
test = Modely(visualizer=None)
- test.addModel('x_z', x_z)
- test.addMinimize('next-pos', x.z(-1), x_z, 'mse')
+ test.addModel("x_z", x_z)
+ test.addMinimize("next-pos", x.z(-1), x_z, "mse")
# Create the neural network
- test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset
+ test.neuralizeModel(
+ sample_time=0.05
+ ) # The sampling time depends to the dataset
# Data load
- data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3',
- 'in1', 'in2', 'time']
- test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t',
- header=None)
- test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t',
- header=None)
- test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t',
- header=None)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="train_dataset",
+ source=train_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="validation_dataset",
+ source=val_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.loadData(
+ name="test_dataset",
+ source=test_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
training_params = {}
- training_params['train_batch_size'] = 3
- training_params['val_batch_size'] = 2
- training_params['lr'] = 0.1
- training_params['num_of_epochs'] = 5
- #test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', test_dataset='test_dataset', training_params=training_params)
- test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', training_params=training_params)
+ training_params["train_batch_size"] = 3
+ training_params["val_batch_size"] = 2
+ training_params["lr"] = 0.1
+ training_params["num_of_epochs"] = 5
+ # test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', test_dataset='test_dataset', training_params=training_params)
+ test.trainModel(
+ train_dataset="train_dataset",
+ validation_dataset="validation_dataset",
+ training_params=training_params,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(9, test._num_of_samples['train_dataset'])
- self.assertEqual(5, test._num_of_samples['validation_dataset'])
- #self.assertEqual(7, test._num_of_samples['test_dataset'])
- self.assertEqual(9, tp['n_samples_train'])
- self.assertEqual(5, tp['n_samples_val'])
- self.assertEqual(3, tp['train_batch_size'])
- self.assertEqual(2, tp['val_batch_size'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual(3, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(9, test._num_of_samples["train_dataset"])
+ self.assertEqual(5, test._num_of_samples["validation_dataset"])
+ # self.assertEqual(7, test._num_of_samples['test_dataset'])
+ self.assertEqual(9, tp["n_samples_train"])
+ self.assertEqual(5, tp["n_samples_val"])
+ self.assertEqual(3, tp["train_batch_size"])
+ self.assertEqual(2, tp["val_batch_size"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(3, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_train_vector_input(self):
NeuObj.clearNames()
- x = Input('x', dimensions=4)
- y = Input('y', dimensions=3)
- k = Input('k', dimensions=2)
- w = Input('w')
+ x = Input("x", dimensions=4)
+ y = Input("y", dimensions=3)
+ k = Input("k", dimensions=2)
+ w = Input("w")
- out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
- out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02)))))
+ out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02))))
+ out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02)))))
test = Modely(visualizer=None)
- test.addMinimize('out', out, out2)
- test.addModel('model', out)
+ test.addMinimize("out", out, out2)
+ test.addModel("model", out)
test.neuralizeModel(0.01)
- data_folder = os.path.join(os.path.dirname(__file__), 'vector_data/')
- data_struct = ['x', 'y', '', '', '', '', 'k', '', '', '', 'w']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1, delimiter='\t', header=None)
+ data_folder = os.path.join(os.path.dirname(__file__), "vector_data/")
+ data_struct = ["x", "y", "", "", "", "", "k", "", "", "", "w"]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=1,
+ delimiter="\t",
+ header=None,
+ )
training_params = {}
- training_params['train_batch_size'] = 1
- training_params['val_batch_size'] = 1
- training_params['lr'] = 0.01
- training_params['num_of_epochs'] = 7
+ training_params["train_batch_size"] = 1
+ training_params["val_batch_size"] = 1
+ training_params["lr"] = 0.01
+ training_params["num_of_epochs"] = 7
test.trainModel(splits=[80, 10, 10], training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(22, test._num_of_samples['dataset'])
- self.assertEqual(18, tp['n_samples_train'])
- self.assertEqual(2, tp['n_samples_val'])
- self.assertEqual(2, tp['n_samples_test'])
- self.assertEqual(1, tp['train_batch_size'])
- self.assertEqual(1, tp['val_batch_size'])
- self.assertEqual(7, tp['num_of_epochs'])
- self.assertEqual(0.01, tp['optimizer_defaults']['lr'])
- self.assertEqual(18, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(22, test._num_of_samples["dataset"])
+ self.assertEqual(18, tp["n_samples_train"])
+ self.assertEqual(2, tp["n_samples_val"])
+ self.assertEqual(2, tp["n_samples_test"])
+ self.assertEqual(1, tp["train_batch_size"])
+ self.assertEqual(1, tp["val_batch_size"])
+ self.assertEqual(7, tp["num_of_epochs"])
+ self.assertEqual(0.01, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(18, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
training_params = {}
- training_params['train_batch_size'] = 6
- training_params['val_batch_size'] = 2
+ training_params["train_batch_size"] = 6
+ training_params["val_batch_size"] = 2
test.trainModel(splits=[80, 10, 10], training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(22, test._num_of_samples['dataset'])
- self.assertEqual(18, tp['n_samples_train'])
- self.assertEqual(2, tp['n_samples_val'])
- self.assertEqual(2, tp['n_samples_test'])
- self.assertEqual(6, tp['train_batch_size'])
- self.assertEqual(2, tp['val_batch_size'])
- self.assertEqual(100, tp['num_of_epochs'])
- self.assertEqual(0.001, tp['optimizer_defaults']['lr'])
- self.assertEqual(3, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(22, test._num_of_samples["dataset"])
+ self.assertEqual(18, tp["n_samples_train"])
+ self.assertEqual(2, tp["n_samples_val"])
+ self.assertEqual(2, tp["n_samples_test"])
+ self.assertEqual(6, tp["train_batch_size"])
+ self.assertEqual(2, tp["val_batch_size"])
+ self.assertEqual(100, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(3, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
def test_optimizer_configuration(self):
NeuObj.clearNames()
## Model1
- input1 = Input('in1')
- a = Parameter('a', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
- shared_w = Parameter('w', values=[[5]])
- output1 = Output('out1',
- Fir(W=a)(input1.tw(0.05)) + ParamFun(funIn, parameters_and_constants={'w': shared_w})(
- input1.last()))
+ input1 = Input("in1")
+ a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ shared_w = Parameter("w", values=[[5]])
+ output1 = Output(
+ "out1",
+ Fir(W=a)(input1.tw(0.05))
+ + ParamFun(funIn, parameters_and_constants={"w": shared_w})(input1.last()),
+ )
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', output1)
- test.addMinimize('error1', input1.last(), output1)
+ test.addModel("model1", output1)
+ test.addMinimize("error1", input1.last(), output1)
## Model2
- input2 = Input('in2')
- b = Parameter('b', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
- output2 = Output('out2',
- Fir(W=b)(input2.tw(0.05)) + ParamFun(funOut, parameters_and_constants={'w': shared_w})(
- input2.last()))
-
- test.addModel('model2', output2)
- test.addMinimize('error2', input2.last(), output2)
+ input2 = Input("in2")
+ b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output2 = Output(
+ "out2",
+ Fir(W=b)(input2.tw(0.05))
+ + ParamFun(funOut, parameters_and_constants={"w": shared_w})(input2.last()),
+ )
+
+ test.addModel("model2", output2)
+ test.addMinimize("error2", input2.last(), output2)
test.neuralizeModel(0.01)
# Dataset for train
@@ -582,15 +806,25 @@ def test_optimizer_configuration(self):
data_in2 = np.linspace(10, 15, 60)
data_out1 = 2
data_out2 = -3
- dataset = {'in1': data_in1, 'in2': data_in2, 'out1': data_in1 * data_out1, 'out2': data_in2 * data_out2}
- test.loadData(name='dataset1', source=dataset)
+ dataset = {
+ "in1": data_in1,
+ "in2": data_in2,
+ "out1": data_in1 * data_out1,
+ "out2": data_in2 * data_out2,
+ }
+ test.loadData(name="dataset1", source=dataset)
data_in1 = np.linspace(0, 5, 100)
data_in2 = np.linspace(10, 15, 100)
data_out1 = 2
data_out2 = -3
- dataset = {'in1': data_in1, 'in2': data_in2, 'out1': data_in1 * data_out1, 'out2': data_in2 * data_out2}
- test.loadData(name='dataset2', source=dataset)
+ dataset = {
+ "in1": data_in1,
+ "in2": data_in2,
+ "out1": data_in1 * data_out1,
+ "out2": data_in2 * data_out2,
+ }
+ test.loadData(name="dataset2", source=dataset)
# Optimizer
# Basic usage
@@ -598,83 +832,93 @@ def test_optimizer_configuration(self):
# We train all the models with split [100,0,0], lr =0.01 and epochs = 100
test.trainModel()
tp = test.getTrainingInfo()
- self.assertEqual(['model1', 'model2'], tp['models'])
- self.assertEqual(152, tp['n_samples_train'])
- self.assertEqual(0, tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual(100, tp['num_of_epochs'])
- self.assertEqual(0.001, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(["model1", "model2"], tp["models"])
+ self.assertEqual(152, tp["n_samples_train"])
+ self.assertEqual(0, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(100, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
# We train only model1 with split [100,0,0]
# TODO Learning rate automoatically optimized based on the mean and variance of the output
# TODO num_of_epochs automatically defined
# now is 0.001 for learning rate and 100 for the epochs and optimizer Adam
- test.trainModel(models='model1', splits=[100, 0, 0])
+ test.trainModel(models="model1", splits=[100, 0, 0])
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(100, tp['num_of_epochs'])
- self.assertEqual(152, tp['n_samples_train'])
- self.assertEqual(0, tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(100, tp["num_of_epochs"])
+ self.assertEqual(152, tp["n_samples_train"])
+ self.assertEqual(0, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
# Set number of epoch and learning rate via parameters it works only for standard parameters
- test.trainModel(models='model1', splits=[100, 0, 0], lr=0.5, num_of_epochs=5)
+ test.trainModel(models="model1", splits=[100, 0, 0], lr=0.5, num_of_epochs=5)
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(5, tp['num_of_epochs'])
- self.assertEqual(152, tp['n_samples_train'])
- self.assertEqual(0, tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual(0.5, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(5, tp["num_of_epochs"])
+ self.assertEqual(152, tp["n_samples_train"])
+ self.assertEqual(0, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(0.5, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
# Set number of epoch and learning rate via parameters it works only for standard parameters and use two different dataset one for train and one for validation
- test.trainModel(models='model1', train_dataset='dataset1', validation_dataset='dataset2', lr=0.6, num_of_epochs=10)
+ test.trainModel(
+ models="model1",
+ train_dataset="dataset1",
+ validation_dataset="dataset2",
+ lr=0.6,
+ num_of_epochs=10,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(10, tp['num_of_epochs'])
- self.assertEqual(56, tp['n_samples_train'])
- self.assertEqual(96, tp['n_samples_val'])
- self.assertEqual(0, tp['n_samples_test'])
- self.assertEqual(0.6,tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(10, tp["num_of_epochs"])
+ self.assertEqual(56, tp["n_samples_train"])
+ self.assertEqual(96, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(0.6, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
# Use dictionary for set number of epoch, learning rate, etc.. This configuration works only standard parameters (all the parameters that are input of the trainModel).
training_params = {
- 'models': ['model2'],
- 'splits': [55, 40, 5],
- 'num_of_epochs': 20,
- 'lr': 0.7
+ "models": ["model2"],
+ "splits": [55, 40, 5],
+ "num_of_epochs": 20,
+ "lr": 0.7,
}
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(['model2'], tp['models'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.7, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model2"], tp["models"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.7, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
# If I add a function parameter it has the priority
# In this case apply train parameter but on a different model
- test.trainModel(models='model1', training_params=training_params)
+ test.trainModel(models="model1", training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.7, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.7, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
##################################
# Modify additional parameters in the optimizer that are not present in the standard parameter
@@ -683,85 +927,102 @@ def test_optimizer_configuration(self):
# max priority to the function parameter ('lr' : 0.2)
# then the standard_optimizer_parameters ('lr' : 0.1)
# finally the standard_train_parameters ('lr' : 0.5)
- optimizer_defaults = {
- 'lr': 0.1,
- 'betas': (0.5, 0.99)
- }
- test.trainModel(training_params=training_params, optimizer_defaults=optimizer_defaults, lr=0.2)
+ optimizer_defaults = {"lr": 0.1, "betas": (0.5, 0.99)}
+ test.trainModel(
+ training_params=training_params,
+ optimizer_defaults=optimizer_defaults,
+ lr=0.2,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model2'], tp['models'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.2, tp['optimizer_defaults']['lr'])
- self.assertEqual((0.5, 0.99), tp['optimizer_defaults']['betas'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
-
- test.trainModel(training_params=training_params, optimizer_defaults=optimizer_defaults)
+ self.assertEqual(["model2"], tp["models"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.2, tp["optimizer_defaults"]["lr"])
+ self.assertEqual((0.5, 0.99), tp["optimizer_defaults"]["betas"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+
+ test.trainModel(
+ training_params=training_params, optimizer_defaults=optimizer_defaults
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model2'], tp['models'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.1, tp['optimizer_defaults']['lr'])
- self.assertEqual((0.5, 0.99), tp['optimizer_defaults']['betas'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model2"], tp["models"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.1, tp["optimizer_defaults"]["lr"])
+ self.assertEqual((0.5, 0.99), tp["optimizer_defaults"]["betas"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(['model2'], tp['models'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.7, tp['optimizer_defaults']['lr'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model2"], tp["models"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.7, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
##################################
# Modify the non standard args of the optimizer using the optimizer_defaults
# In this case use the SGD with 0.2 of momentum
- optimizer_defaults = {
- 'momentum': 0.002
- }
- test.trainModel(optimizer='SGD', training_params=training_params, optimizer_defaults=optimizer_defaults, lr=0.2)
+ optimizer_defaults = {"momentum": 0.002}
+ test.trainModel(
+ optimizer="SGD",
+ training_params=training_params,
+ optimizer_defaults=optimizer_defaults,
+ lr=0.2,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model2'], tp['models'])
- self.assertEqual('SGD', tp['optimizer'])
- self.assertEqual(20, tp['num_of_epochs'])
- self.assertEqual(round(152 * 55 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 40 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.2, tp['optimizer_defaults']['lr'])
- self.assertEqual(0.002, tp['optimizer_defaults']['momentum'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(["model2"], tp["models"])
+ self.assertEqual("SGD", tp["optimizer"])
+ self.assertEqual(20, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.2, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(0.002, tp["optimizer_defaults"]["momentum"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
# Modify standard optimizer parameter for each training parameter
training_params = {
- 'models': ['model1'],
- 'splits': [100, 0, 0],
- 'num_of_epochs': 30,
- 'lr': 0.5,
- 'lr_param': {'a': 0.1}
+ "models": ["model1"],
+ "splits": [100, 0, 0],
+ "num_of_epochs": 30,
+ "lr": 0.5,
+ "lr_param": {"a": 0.1},
}
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(30, tp['num_of_epochs'])
- self.assertEqual(round(152 * 100 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_val'])
- self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test'])
- self.assertEqual(0.5, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'lr': 0.1, 'params': 'a'},
- {'lr': 0.0, 'params': 'b'},
- {'params': 'w'}], tp['optimizer_params'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(30, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(
+ 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"]
+ )
+ self.assertEqual(0.5, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(
+ [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
##################################
# Modify standard optimizer parameter for each training parameter using optimizer_params
@@ -771,71 +1032,77 @@ def test_optimizer_configuration(self):
# then the optimizer_params inside the train_parameters ( {'params':['a'],'lr':0.7} )
# finally the train_parameters ( 'lr_param'={'a': 0.1})
training_params = {
- 'models': ['model1'],
- 'splits': [100, 0, 0],
- 'num_of_epochs': 40,
- 'lr': 0.5,
- 'lr_param': {'a': 0.1},
- 'optimizer_params': [{'params': ['a'], 'lr': 0.7}],
- 'optimizer_defaults': {'lr': 0.12}
- }
- optimizer_params = [
- {'params': ['a'], 'lr': 0.6}
- ]
- optimizer_defaults = {
- 'lr': 0.2
+ "models": ["model1"],
+ "splits": [100, 0, 0],
+ "num_of_epochs": 40,
+ "lr": 0.5,
+ "lr_param": {"a": 0.1},
+ "optimizer_params": [{"params": ["a"], "lr": 0.7}],
+ "optimizer_defaults": {"lr": 0.12},
}
- test.trainModel(training_params=training_params, optimizer_params=optimizer_params,
- optimizer_defaults=optimizer_defaults, lr_param={'a': 0.4})
+ optimizer_params = [{"params": ["a"], "lr": 0.6}]
+ optimizer_defaults = {"lr": 0.2}
+ test.trainModel(
+ training_params=training_params,
+ optimizer_params=optimizer_params,
+ optimizer_defaults=optimizer_defaults,
+ lr_param={"a": 0.4},
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual(40, tp['num_of_epochs'])
- self.assertEqual(round(152 * 100 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_val'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_test'])
- self.assertEqual(0.2, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'lr': 0.4, 'params': 'a'}], tp['optimizer_params'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
-
- test.trainModel(training_params=training_params, optimizer_params=optimizer_params,
- optimizer_defaults=optimizer_defaults)
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual(40, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"])
+ self.assertEqual(0.2, tp["optimizer_defaults"]["lr"])
+ self.assertEqual([{"lr": 0.4, "params": "a"}], tp["optimizer_params"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
+
+ test.trainModel(
+ training_params=training_params,
+ optimizer_params=optimizer_params,
+ optimizer_defaults=optimizer_defaults,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(0.2, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'lr': 0.6, 'params': 'a'}], tp['optimizer_params'])
+ self.assertEqual(0.2, tp["optimizer_defaults"]["lr"])
+ self.assertEqual([{"lr": 0.6, "params": "a"}], tp["optimizer_params"])
- test.trainModel(training_params=training_params, optimizer_params=optimizer_params)
+ test.trainModel(
+ training_params=training_params, optimizer_params=optimizer_params
+ )
tp = test.getTrainingInfo()
- self.assertEqual(0.12, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'lr': 0.6, 'params': 'a'}], tp['optimizer_params'])
+ self.assertEqual(0.12, tp["optimizer_defaults"]["lr"])
+ self.assertEqual([{"lr": 0.6, "params": "a"}], tp["optimizer_params"])
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(0.12, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params'])
+ self.assertEqual(0.12, tp["optimizer_defaults"]["lr"])
+ self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"])
- del training_params['optimizer_defaults']
+ del training_params["optimizer_defaults"]
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(0.5, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params'])
+ self.assertEqual(0.5, tp["optimizer_defaults"]["lr"])
+ self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"])
- del training_params['optimizer_params']
+ del training_params["optimizer_params"]
test.trainModel(training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual(0.5, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'lr': 0.1, 'params': 'a'},
- {'lr': 0.0, 'params': 'b'},
- {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual(0.5, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(
+ [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
test.trainModel()
tp = test.getTrainingInfo()
- self.assertEqual(0.001, tp['optimizer_defaults']['lr'])
- self.assertEqual([{'params': 'a'},
- {'params': 'b'},
- {'params': 'w'}], tp['optimizer_params'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+ self.assertEqual(
+ [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"]
+ )
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
##################################
@@ -848,65 +1115,82 @@ def test_optimizer_configuration(self):
# finally the train_parameters ('lr'= 0.5)
class RMSprop(Optimizer):
def __init__(self, optimizer_defaults={}, optimizer_params=[]):
- super(RMSprop, self).__init__('RMSprop', optimizer_defaults, optimizer_params)
+ super(RMSprop, self).__init__(
+ "RMSprop", optimizer_defaults, optimizer_params
+ )
def get_torch_optimizer(self):
import torch
- return torch.optim.RMSprop(self.replace_key_with_params(), **self.optimizer_defaults)
+
+ return torch.optim.RMSprop(
+ self.replace_key_with_params(), **self.optimizer_defaults
+ )
training_params = {
- 'models': ['model1'],
- 'splits': [100, 0, 0],
- 'num_of_epochs': 40,
- 'lr': 0.5,
- 'lr_param': {'a': 0.1},
- 'optimizer_params': [{'params': ['a'], 'lr': 0.7}],
- 'optimizer_defaults': {'lr': 0.12}
- }
- optimizer_defaults = {
- 'alpha': 0.8
+ "models": ["model1"],
+ "splits": [100, 0, 0],
+ "num_of_epochs": 40,
+ "lr": 0.5,
+ "lr_param": {"a": 0.1},
+ "optimizer_params": [{"params": ["a"], "lr": 0.7}],
+ "optimizer_defaults": {"lr": 0.12},
}
+ optimizer_defaults = {"alpha": 0.8}
optimizer = RMSprop(optimizer_defaults)
- test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3}, lr=0.4)
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ optimizer_defaults={"lr": 0.3},
+ lr=0.4,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual('RMSprop', tp['optimizer'])
- self.assertEqual(40, tp['num_of_epochs'])
- self.assertEqual(round(152 * 100 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_val'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_test'])
- self.assertEqual({'lr': 0.4}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.1})
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual("RMSprop", tp["optimizer"])
+ self.assertEqual(40, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"])
+ self.assertEqual({"lr": 0.4}, tp["optimizer_defaults"])
+ self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"])
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ optimizer_defaults={"lr": 0.1},
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.1}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.1}, tp["optimizer_defaults"])
+ self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"])
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.12}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"])
+ self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"])
- del training_params['optimizer_defaults']
+ del training_params["optimizer_defaults"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"])
- del training_params['optimizer_params']
+ del training_params["optimizer_params"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
test.trainModel(optimizer=optimizer)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.001}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a'}, {'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.001}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"]
+ )
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
##################################
##################################
@@ -918,69 +1202,97 @@ def get_torch_optimizer(self):
# then the train_parameters ( 'lr_param'={'a': 0.1} )
# finnaly the optimizer_paramsat the time of the optimizer initialization [{'params':['a'],'lr':0.6}]
training_params = {
- 'models': ['model1'],
- 'splits': [100, 0, 0],
- 'num_of_epochs': 40,
- 'lr': 0.5,
- 'lr_param': {'a': 0.1},
- 'optimizer_params': [{'params': ['a'], 'lr': 0.7}],
- 'optimizer_defaults': {'lr': 0.12}
- }
- optimizer_defaults = {
- 'alpha': 0.8
+ "models": ["model1"],
+ "splits": [100, 0, 0],
+ "num_of_epochs": 40,
+ "lr": 0.5,
+ "lr_param": {"a": 0.1},
+ "optimizer_params": [{"params": ["a"], "lr": 0.7}],
+ "optimizer_defaults": {"lr": 0.12},
}
+ optimizer_defaults = {"alpha": 0.8}
optimizer_params = [
- {'params': ['a'], 'lr': 0.6}, {'params': 'w', 'lr': 0.12, 'alpha': 0.02}
+ {"params": ["a"], "lr": 0.6},
+ {"params": "w", "lr": 0.12, "alpha": 0.02},
]
optimizer = RMSprop(optimizer_defaults, optimizer_params)
- test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3},
- optimizer_params=[{'params': ['a'], 'lr': 1.0}, {'params': ['b'], 'lr': 1.2}],
- lr_param={'a': 0.2})
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ optimizer_defaults={"lr": 0.3},
+ optimizer_params=[
+ {"params": ["a"], "lr": 1.0},
+ {"params": ["b"], "lr": 1.2},
+ ],
+ lr_param={"a": 0.2},
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual('RMSprop', tp['optimizer'])
- self.assertEqual(40, tp['num_of_epochs'])
- self.assertEqual(round(152 * 100 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_val'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_test'])
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.2}, {'params': 'b', 'lr': 1.2}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3},
- optimizer_params=[{'params': ['a'], 'lr': 0.1}, {'params': ['b'], 'lr': 0.2}])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual("RMSprop", tp["optimizer"])
+ self.assertEqual(40, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"])
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.2}, {"params": "b", "lr": 1.2}],
+ tp["optimizer_params"],
+ )
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ optimizer_defaults={"lr": 0.3},
+ optimizer_params=[
+ {"params": ["a"], "lr": 0.1},
+ {"params": ["b"], "lr": 0.2},
+ ],
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.1}, {'params': 'b', 'lr': 0.2}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3})
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.1}, {"params": "b", "lr": 0.2}],
+ tp["optimizer_params"],
+ )
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ optimizer_defaults={"lr": 0.3},
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"])
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.12}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"])
+ self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"])
- del training_params['optimizer_defaults']
+ del training_params["optimizer_defaults"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"])
- del training_params['optimizer_params']
+ del training_params["optimizer_params"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'alpha': 0.02, 'lr': 0.12, 'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"lr": 0.1, "params": "a"}, {"alpha": 0.02, "lr": 0.12, "params": "w"}],
+ tp["optimizer_params"],
+ )
test.trainModel(optimizer=optimizer)
tp = test.getTrainingInfo()
- self.assertEqual({'alpha': 0.8, 'lr': 0.001}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.6}, {'params': 'w', 'lr': 0.12, 'alpha': 0.02}],
- tp['optimizer_params'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual({"alpha": 0.8, "lr": 0.001}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.6}, {"params": "w", "lr": 0.12, "alpha": 0.02}],
+ tp["optimizer_params"],
+ )
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
##################################
##################################
@@ -992,122 +1304,252 @@ def get_torch_optimizer(self):
# then the train_parameters ( 'lr_param'={'a': 0.1} )
# The other parameters are the defaults
training_params = {
- 'models': ['model1'],
- 'splits': [100, 0, 0],
- 'num_of_epochs': 40,
- 'lr': 0.5,
- 'lr_param': {'a': 0.1},
- 'add_optimizer_params': [{'params': ['a'], 'lr': 0.7}],
- 'add_optimizer_defaults': {'lr': 0.12}
+ "models": ["model1"],
+ "splits": [100, 0, 0],
+ "num_of_epochs": 40,
+ "lr": 0.5,
+ "lr_param": {"a": 0.1},
+ "add_optimizer_params": [{"params": ["a"], "lr": 0.7}],
+ "add_optimizer_defaults": {"lr": 0.12},
}
optimizer = RMSprop()
- test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3},
- add_optimizer_params=[{'params': ['a'], 'lr': 1.0}, {'params': ['b'], 'lr': 1.2}],
- lr_param={'a': 0.2})
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ add_optimizer_defaults={"lr": 0.3},
+ add_optimizer_params=[
+ {"params": ["a"], "lr": 1.0},
+ {"params": ["b"], "lr": 1.2},
+ ],
+ lr_param={"a": 0.2},
+ )
tp = test.getTrainingInfo()
- self.assertEqual(['model1'], tp['models'])
- self.assertEqual('RMSprop', tp['optimizer'])
- self.assertEqual(40, tp['num_of_epochs'])
- self.assertEqual(round(152 * 100 / 100), tp['n_samples_train'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_val'])
- self.assertEqual(round(152 * 0 / 100), tp['n_samples_test'])
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.2}, {'params': 'b', 'lr': 1.2}, {'params': 'w'}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3},
- add_optimizer_params=[{'params': ['a'], 'lr': 0.23}, {'params': ['b'], 'lr': 0.2}])
+ self.assertEqual(["model1"], tp["models"])
+ self.assertEqual("RMSprop", tp["optimizer"])
+ self.assertEqual(40, tp["num_of_epochs"])
+ self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"])
+ self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"])
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.2}, {"params": "b", "lr": 1.2}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ add_optimizer_defaults={"lr": 0.3},
+ add_optimizer_params=[
+ {"params": ["a"], "lr": 0.23},
+ {"params": ["b"], "lr": 0.2},
+ ],
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.23}, {'params': 'b', 'lr': 0.2}, {'params': 'w'}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3},
- add_optimizer_params=[{'params': ['b'], 'lr': 0.2}])
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.23}, {"params": "b", "lr": 0.2}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ add_optimizer_defaults={"lr": 0.3},
+ add_optimizer_params=[{"params": ["b"], "lr": 0.2}],
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.1}, {'params': 'b', 'lr': 0.2}, {'params': 'w'}], tp['optimizer_params'])
-
- test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3})
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.1}, {"params": "b", "lr": 0.2}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
+
+ test.trainModel(
+ optimizer=optimizer,
+ training_params=training_params,
+ add_optimizer_defaults={"lr": 0.3},
+ )
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.3}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.12}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
- del training_params['add_optimizer_defaults']
+ del training_params["add_optimizer_defaults"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
- del training_params['add_optimizer_params']
+ del training_params["add_optimizer_params"]
test.trainModel(optimizer=optimizer, training_params=training_params)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.5}, tp['optimizer_defaults'])
- self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.5}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}],
+ tp["optimizer_params"],
+ )
test.trainModel(optimizer=optimizer)
tp = test.getTrainingInfo()
- self.assertEqual({'lr': 0.001}, tp['optimizer_defaults'])
- self.assertEqual([{'params': 'a'}, {'params': 'b'}, {'params': 'w'}], tp['optimizer_params'])
+ self.assertEqual({"lr": 0.001}, tp["optimizer_defaults"])
+ self.assertEqual(
+ [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"]
+ )
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(24, tp['unused_samples'])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(24, tp['unused_samples'])
def test_train_sampled_datasets(self):
NeuObj.clearNames()
- x = Input('x') # Position
- F = Input('F') # Force
+ x = Input("x") # Position
+ F = Input("F") # Force
# List the output of the model
- x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last()))
+ x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last()))
# Add the neural model to the nnodely structure and neuralization of the model
test = Modely(visualizer=None)
- test.addModel('x_z',x_z)
- test.addMinimize('next-pos', x.z(-1), x_z, 'mse')
+ test.addModel("x_z", x_z)
+ test.addMinimize("next-pos", x.z(-1), x_z, "mse")
# Create the neural network
- test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset
+ test.neuralizeModel(
+ sample_time=0.05
+ ) # The sampling time depends to the dataset
# Data load
- data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time']
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None)
- test.trainModel(train_dataset='dataset', num_of_epochs=3)
+ data_struct = [
+ "x",
+ "F",
+ "x2",
+ "y2",
+ "",
+ "A1x",
+ "A1y",
+ "B1x",
+ "B1y",
+ "",
+ "A2x",
+ "A2y",
+ "B2x",
+ "out",
+ "",
+ "x3",
+ "in1",
+ "in2",
+ "time",
+ ]
+ test.loadData(
+ name="dataset",
+ source=data_folder,
+ format=data_struct,
+ skiplines=4,
+ delimiter="\t",
+ header=None,
+ )
+ test.trainModel(train_dataset="dataset", num_of_epochs=3)
tp = test.getTrainingInfo()
- self.assertEqual((15-6), test._num_of_samples['dataset'])
- self.assertEqual(round(15-6),tp['n_samples_train'])
- self.assertEqual(round(0),tp['n_samples_val'])
- self.assertEqual(round(0),tp['n_samples_test'])
- self.assertEqual(round(15-6),tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(128,tp['val_batch_size'])
- self.assertEqual(3,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
+ self.assertEqual((15 - 6), test._num_of_samples["dataset"])
+ self.assertEqual(round(15 - 6), tp["n_samples_train"])
+ self.assertEqual(round(0), tp["n_samples_val"])
+ self.assertEqual(round(0), tp["n_samples_test"])
+ self.assertEqual(round(15 - 6), tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(128, tp["val_batch_size"])
+ self.assertEqual(3, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
## Passing a sampled dataset
import torch
- train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]],
- [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]],
- [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8190]],[[0.8180]],[[0.8160]],[[0.8140]],[[0.8130]]])}
- ## Not the same number of samples
+ train_data = {
+ "x": torch.tensor(
+ [
+ [
+ [0.8030],
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ ],
+ [
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ ],
+ [
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ ],
+ [
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ [0.8160],
+ ],
+ ]
+ ),
+ "F": torch.tensor(
+ [
+ [[0.8240]],
+ [[0.8230]],
+ [[0.8220]],
+ [[0.8200]],
+ [[0.8190]],
+ [[0.8180]],
+ [[0.8160]],
+ [[0.8140]],
+ [[0.8130]],
+ ]
+ ),
+ }
+ ## Not the same number of samples
with self.assertRaises(ValueError):
test.trainModel(train_dataset=train_data)
with self.assertRaises(ValueError):
test.trainAndAnalyze(test_dataset=train_data)
- train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140]],
- [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])}
+ train_data = {
+ "x": torch.tensor(
+ [
+ [[0.8030], [0.8030], [0.8040], [0.8040], [0.8050], [0.8060]],
+ [[0.8030], [0.8040], [0.8040], [0.8050], [0.8060], [0.8070]],
+ [[0.8080], [0.8090], [0.8100], [0.8120], [0.8130], [0.8140]],
+ [[0.8090], [0.8100], [0.8120], [0.8130], [0.8140], [0.8150]],
+ ]
+ ),
+ "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]),
+ }
## Not the correct number of dimensions
with self.assertRaises(ValueError):
test.trainModel(train_dataset=train_data)
@@ -1117,92 +1559,327 @@ def test_train_sampled_datasets(self):
test.trainAndAnalyze(test_dataset=train_data)
with self.assertRaises(ValueError):
test.trainAndAnalyze(train_dataset=train_data, test_dataset=train_data)
-
- train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]],
- [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]],
- [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]]),
- 't': torch.tensor([[[0.0]],[[0.05]],[[0.1]],[[0.15]]])}
+
+ train_data = {
+ "x": torch.tensor(
+ [
+ [
+ [0.8030],
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ ],
+ [
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ ],
+ [
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ ],
+ [
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ [0.8160],
+ ],
+ ]
+ ),
+ "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]),
+ "t": torch.tensor([[[0.0]], [[0.05]], [[0.1]], [[0.15]]]),
+ }
## The extra sample is ignored
- test.trainModel(train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3)
+ test.trainModel(
+ train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3
+ )
- train_data = {'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])}
+ train_data = {
+ "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]])
+ }
## If there is a missing input the training fails
with self.assertRaises(KeyError):
- test.trainModel(train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3)
-
- train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]],
- [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]],
- [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]],
- [[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100],[0.8120]],
- [[0.8060],[0.8070],[0.8080],[0.8090],[0.8100],[0.8120],[0.8130]],
- [[0.8070],[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140]],
- [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]],
- [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8190]],[[0.8180]],[[0.8160]],[[0.8140]],[[0.8130]]])}
- val_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]],
- [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]],
- [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]],
- [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8130]]])}
-
+ test.trainModel(
+ train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3
+ )
+
+ train_data = {
+ "x": torch.tensor(
+ [
+ [
+ [0.8030],
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ ],
+ [
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ ],
+ [
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ ],
+ [
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ ],
+ [
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ ],
+ [
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ ],
+ [
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ ],
+ [
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ ],
+ [
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ [0.8160],
+ ],
+ ]
+ ),
+ "F": torch.tensor(
+ [
+ [[0.8240]],
+ [[0.8230]],
+ [[0.8220]],
+ [[0.8200]],
+ [[0.8190]],
+ [[0.8180]],
+ [[0.8160]],
+ [[0.8140]],
+ [[0.8130]],
+ ]
+ ),
+ }
+ val_data = {
+ "x": torch.tensor(
+ [
+ [
+ [0.8030],
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ ],
+ [
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ ],
+ [
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ ],
+ [
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ ],
+ [
+ [0.8090],
+ [0.8100],
+ [0.8120],
+ [0.8130],
+ [0.8140],
+ [0.8150],
+ [0.8160],
+ ],
+ ]
+ ),
+ "F": torch.tensor(
+ [[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]], [[0.8130]]]
+ ),
+ }
+
test.trainModel(train_dataset=train_data, num_of_epochs=3)
tp = test.getTrainingInfo()
- self.assertEqual(9, test._num_of_samples['dataset'])
- self.assertEqual(9,tp['n_samples_train'])
- self.assertEqual(0,tp['n_samples_val'])
- self.assertEqual(0,tp['n_samples_test'])
- self.assertEqual(9,tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(128,tp['val_batch_size'])
- self.assertEqual(3,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
-
- test.trainModel(train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3)
+ self.assertEqual(9, test._num_of_samples["dataset"])
+ self.assertEqual(9, tp["n_samples_train"])
+ self.assertEqual(0, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(9, tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(128, tp["val_batch_size"])
+ self.assertEqual(3, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+
+ test.trainModel(
+ train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3
+ )
tp = test.getTrainingInfo()
- self.assertEqual(9, test._num_of_samples['dataset'])
- self.assertEqual(9,tp['n_samples_train'])
- self.assertEqual(5,tp['n_samples_val'])
- self.assertEqual(0,tp['n_samples_test'])
- self.assertEqual(9,tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(5,tp['val_batch_size'])
- self.assertEqual(3,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
-
- test_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]],
- [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]],
- [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]],
- [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]]]),
- 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])}
-
- test.trainAndAnalyze(test_dataset=test_data, test_batch_size=2, train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3)
+ self.assertEqual(9, test._num_of_samples["dataset"])
+ self.assertEqual(9, tp["n_samples_train"])
+ self.assertEqual(5, tp["n_samples_val"])
+ self.assertEqual(0, tp["n_samples_test"])
+ self.assertEqual(9, tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(5, tp["val_batch_size"])
+ self.assertEqual(3, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+
+ test_data = {
+ "x": torch.tensor(
+ [
+ [
+ [0.8030],
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ ],
+ [
+ [0.8030],
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ ],
+ [
+ [0.8040],
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ ],
+ [
+ [0.8040],
+ [0.8050],
+ [0.8060],
+ [0.8070],
+ [0.8080],
+ [0.8090],
+ [0.8100],
+ ],
+ ]
+ ),
+ "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]),
+ }
+
+ test.trainAndAnalyze(
+ test_dataset=test_data,
+ test_batch_size=2,
+ train_dataset=train_data,
+ validation_dataset=val_data,
+ num_of_epochs=3,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(9, test._num_of_samples['dataset'])
- self.assertEqual(9,tp['n_samples_train'])
- self.assertEqual(5,tp['n_samples_val'])
- self.assertEqual(4,tp['n_samples_test'])
- self.assertEqual(9,tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(5,tp['val_batch_size'])
- self.assertEqual(3,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
-
- test.trainAndAnalyze(test_dataset=test_data, test_batch_size=2, train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3, prediction_samples=2)
+ self.assertEqual(9, test._num_of_samples["dataset"])
+ self.assertEqual(9, tp["n_samples_train"])
+ self.assertEqual(5, tp["n_samples_val"])
+ self.assertEqual(4, tp["n_samples_test"])
+ self.assertEqual(9, tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(5, tp["val_batch_size"])
+ self.assertEqual(3, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
+
+ test.trainAndAnalyze(
+ test_dataset=test_data,
+ test_batch_size=2,
+ train_dataset=train_data,
+ validation_dataset=val_data,
+ num_of_epochs=3,
+ prediction_samples=2,
+ )
tp = test.getTrainingInfo()
- self.assertEqual(9, test._num_of_samples['dataset'])
- self.assertEqual(9,tp['n_samples_train'])
- self.assertEqual(5,tp['n_samples_val'])
- self.assertEqual(4,tp['n_samples_test'])
- self.assertEqual(9,tp['train_batch_size'])
- self.assertEqual(1, tp['update_per_epochs'])
- #self.assertEqual(0, tp['unused_samples'])
- self.assertEqual(5,tp['val_batch_size'])
- self.assertEqual(3,tp['num_of_epochs'])
- self.assertEqual(0.001,tp['optimizer_defaults']['lr'])
\ No newline at end of file
+ self.assertEqual(9, test._num_of_samples["dataset"])
+ self.assertEqual(9, tp["n_samples_train"])
+ self.assertEqual(5, tp["n_samples_val"])
+ self.assertEqual(4, tp["n_samples_test"])
+ self.assertEqual(9, tp["train_batch_size"])
+ self.assertEqual(1, tp["update_per_epochs"])
+ # self.assertEqual(0, tp['unused_samples'])
+ self.assertEqual(5, tp["val_batch_size"])
+ self.assertEqual(3, tp["num_of_epochs"])
+ self.assertEqual(0.001, tp["optimizer_defaults"]["lr"])
diff --git a/tests/test_results.py b/tests/test_results.py
index 62426625..4b7652bf 100644
--- a/tests/test_results.py
+++ b/tests/test_results.py
@@ -1,4 +1,6 @@
-import unittest, os, sys
+import unittest
+import os
+import sys
import numpy as np
from nnodely import *
@@ -15,303 +17,631 @@
# in closed loop and states cases
# in connect and states cases
-data_folder = os.path.join(os.path.dirname(__file__), '_data/')
+data_folder = os.path.join(os.path.dirname(__file__), "_data/")
+
class ModelyTrainingTest(unittest.TestCase):
def test_analysis_results(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out', Fir(W=a)(input1.last()))
-
- test = Modely(visualizer=None,seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out", Fir(W=a)(input1.last()))
+
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]}
- test.loadData(name='dataset', source=dataset)
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
+ }
+ test.loadData(name="dataset", source=dataset)
# Test prediction
- test.analyzeModel('dataset')
- self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]],
- 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]},
- test.prediction['dataset']['error1'])
- self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse'])
- self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse'])
- self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error'])
-
- test.analyzeModel('dataset', batch_size=5)
- self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]],
- 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]},
- test.prediction['dataset']['error1'])
- self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse'])
- self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse'])
- self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error'])
-
- test.analyzeModel('dataset', batch_size=6)
- self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]],
- 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]},
- test.prediction['dataset']['error1'])
- self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse'])
- self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse'])
- self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error'])
-
- dataset = {'in1': [1,1,1,1,1,1,2,2,3,3], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]}
- test.loadData(name='dataset2', source=dataset)
-
- test.analyzeModel('dataset2')
- self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0, test.performance['dataset2']['error1']['mse'], places=6)
- self.assertAlmostEqual(((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0, test.performance['dataset2']['error2']['mse'], places=6)
- self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 + ((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0 )/2.0, test.performance['dataset2']['total']['mean_error'], places=6)
-
- test.analyzeModel('dataset2', batch_size=5)
- self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0, test.performance['dataset2']['error1']['mse'], places=6)
- self.assertAlmostEqual(((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0, test.performance['dataset2']['error2']['mse'], places=6)
- self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 + ((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0 )/2.0, test.performance['dataset2']['total']['mean_error'], places=6)
-
- test.analyzeModel('dataset2', batch_size=6)
- self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]],
- 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]},
- test.prediction['dataset2']['error1'])
- self.assertEqual((1.0 ** 2) * 6.0 / 6.0, test.performance['dataset2']['error1']['mse'])
- self.assertEqual((2.0 ** 2) * 6.0 / 6.0, test.performance['dataset2']['error2']['mse'])
- self.assertEqual((1+4)/2.0, test.performance['dataset2']['total']['mean_error'])
-
- test.analyzeModel('dataset2', minimize_gain={'error1': 0.5, 'error2': 0.0})
- self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0 * 0.5, test.performance['dataset2']['error1']['mse'], places=6)
- self.assertAlmostEqual(0.0, test.performance['dataset2']['error2']['mse'], places=6)
- self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 * 0.5 + 0.0)/2.0, test.performance['dataset2']['total']['mean_error'], places=6)
+ test.analyzeModel("dataset")
+ self.assertEqual(
+ {
+ "A": [
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ ],
+ "B": [
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ ],
+ },
+ test.prediction["dataset"]["error1"],
+ )
+ self.assertEqual(
+ (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"]
+ )
+
+ test.analyzeModel("dataset", batch_size=5)
+ self.assertEqual(
+ {
+ "A": [
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ ],
+ "B": [
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ [[1.0]],
+ ],
+ },
+ test.prediction["dataset"]["error1"],
+ )
+ self.assertEqual(
+ (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"]
+ )
+
+ test.analyzeModel("dataset", batch_size=6)
+ self.assertEqual(
+ {
+ "A": [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]],
+ "B": [[[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]]],
+ },
+ test.prediction["dataset"]["error1"],
+ )
+ self.assertEqual(
+ (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"]
+ )
+
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 2, 2, 3, 3],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
+ }
+ test.loadData(name="dataset2", source=dataset)
+
+ test.analyzeModel("dataset2")
+ self.assertAlmostEqual(
+ (1.0**2.0) * 8.0 / 10.0,
+ test.performance["dataset2"]["error1"]["mse"],
+ places=6,
+ )
+ self.assertAlmostEqual(
+ ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0,
+ test.performance["dataset2"]["error2"]["mse"],
+ places=6,
+ )
+ self.assertAlmostEqual(
+ ((1.0**2) * 8.0 / 10.0 + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0) / 2.0,
+ test.performance["dataset2"]["total"]["mean_error"],
+ places=6,
+ )
+
+ test.analyzeModel("dataset2", batch_size=5)
+ self.assertAlmostEqual(
+ (1.0**2.0) * 8.0 / 10.0,
+ test.performance["dataset2"]["error1"]["mse"],
+ places=6,
+ )
+ self.assertAlmostEqual(
+ ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0,
+ test.performance["dataset2"]["error2"]["mse"],
+ places=6,
+ )
+ self.assertAlmostEqual(
+ ((1.0**2) * 8.0 / 10.0 + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0) / 2.0,
+ test.performance["dataset2"]["total"]["mean_error"],
+ places=6,
+ )
+
+ test.analyzeModel("dataset2", batch_size=6)
+ self.assertEqual(
+ {
+ "A": [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]],
+ "B": [[[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]]],
+ },
+ test.prediction["dataset2"]["error1"],
+ )
+ self.assertEqual(
+ (1.0**2) * 6.0 / 6.0, test.performance["dataset2"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (2.0**2) * 6.0 / 6.0, test.performance["dataset2"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (1 + 4) / 2.0, test.performance["dataset2"]["total"]["mean_error"]
+ )
+
+ test.analyzeModel("dataset2", minimize_gain={"error1": 0.5, "error2": 0.0})
+ self.assertAlmostEqual(
+ (1.0**2.0) * 8.0 / 10.0 * 0.5,
+ test.performance["dataset2"]["error1"]["mse"],
+ places=6,
+ )
+ self.assertAlmostEqual(
+ 0.0, test.performance["dataset2"]["error2"]["mse"], places=6
+ )
+ self.assertAlmostEqual(
+ ((1.0**2) * 8.0 / 10.0 * 0.5 + 0.0) / 2.0,
+ test.performance["dataset2"]["total"]["mean_error"],
+ places=6,
+ )
def test_analysis_results_closed_loop_state(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[2]])
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[2]])
relation = Fir(W=a)(input1.last())
relation.closedLoop(input1)
- output1 = Output('out', relation)
+ output1 = Output("out", relation)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- #Prediction samples = None
- dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]}
- test.loadData(name='dataset', source=dataset)
+ # Prediction samples = None
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
+ }
+ test.loadData(name="dataset", source=dataset)
# Test prediction
- test.analyzeModel('dataset',prediction_samples=-1)
- self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]],
- 'B': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]]},
- test.prediction['dataset']['error1'])
- self.assertEqual((0.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse'])
- self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse'])
- self.assertEqual((0+1)/2.0, test.performance['dataset']['total']['mean_error'])
-
- dataset = {'in1': [1,1,1,1,1,1,2,2,3,3], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]}
- test.loadData(name='dataset2', source=dataset)
-
- test.analyzeModel('dataset2', batch_size=5)
- self.assertEqual((0.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse'])
- self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse'])
- self.assertEqual((0+1)/2.0, test.performance['dataset']['total']['mean_error'])
+ test.analyzeModel("dataset", prediction_samples=-1)
+ self.assertEqual(
+ {
+ "A": [
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ ],
+ "B": [
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ [[2.0]],
+ ],
+ },
+ test.prediction["dataset"]["error1"],
+ )
+ self.assertEqual(
+ (0.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (0 + 1) / 2.0, test.performance["dataset"]["total"]["mean_error"]
+ )
+
+ dataset = {
+ "in1": [1, 1, 1, 1, 1, 1, 2, 2, 3, 3],
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
+ }
+ test.loadData(name="dataset2", source=dataset)
+
+ test.analyzeModel("dataset2", batch_size=5)
+ self.assertEqual(
+ (0.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"]
+ )
+ self.assertEqual(
+ (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"]
+ )
+ self.assertEqual(
+ (0 + 1) / 2.0, test.performance["dataset"]["total"]["mean_error"]
+ )
# Prediction samples = 5
- dataset = {'in1': [1,2,3,4,5,6,7,8,9,10], 'out1': [11,12,13,14,15,16,17,18,19,20], 'out2': [10,20,30,40,50,60,70,80,90,100]}
- test.loadData(name='dataset3', source=dataset)
-
- test.analyzeModel('dataset3', prediction_samples=5, batch_size=2)
- A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]]
- B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
- [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3)
- test.analyzeModel(splits=[50,30,20], dataset='dataset3', prediction_samples=1, batch_size=1)
- A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]]
- B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2'])
-
- test.analyzeModel('dataset3', prediction_samples=5, batch_size=4)
- A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]]
- B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
- [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3)
-
- test.analyzeModel('dataset3', prediction_samples=4, batch_size=6)
- A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]]]
- B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/30.0, test.performance['dataset3']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/30.0, test.performance['dataset3']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/30.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/30.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3)
-
- dataset = {'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]}
- test.loadData(name='dataset4', source=dataset)
- test.trainModel(dataset='dataset4', prediction_samples=-1) #TODO FIX
- test.analyzeModel('dataset4', prediction_samples=-1) #TODO FIX
-
+ dataset = {
+ "in1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "out1": [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
+ "out2": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
+ }
+ test.loadData(name="dataset3", source=dataset)
+
+ test.analyzeModel("dataset3", prediction_samples=5, batch_size=2)
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
+ [[[64.0]], [[128.0]], [[192.0]], [[256.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset3"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset3"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ )
+ / 2.0,
+ test.performance["dataset3"]["total"]["mean_error"],
+ places=3,
+ )
+ test.analyzeModel(
+ splits=[50, 30, 20], dataset="dataset3", prediction_samples=1, batch_size=1
+ )
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
+ [[[64.0]], [[128.0]], [[192.0]], [[256.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"])
+
+ test.analyzeModel("dataset3", prediction_samples=5, batch_size=4)
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
+ [[[64.0]], [[128.0]], [[192.0]], [[256.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset3"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset3"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ )
+ / 2.0,
+ test.performance["dataset3"]["total"]["mean_error"],
+ places=3,
+ )
+
+ test.analyzeModel("dataset3", prediction_samples=4, batch_size=6)
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0,
+ test.performance["dataset3"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0,
+ test.performance["dataset3"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0
+ )
+ / 2.0,
+ test.performance["dataset3"]["total"]["mean_error"],
+ places=3,
+ )
+
+ dataset = {
+ "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+ "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
+ }
+ test.loadData(name="dataset4", source=dataset)
+ test.trainModel(dataset="dataset4", prediction_samples=-1) # TODO FIX
+ test.analyzeModel("dataset4", prediction_samples=-1) # TODO FIX
def test_analysis_results_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target1 = Input('out1')
- target2 = Input('out2')
- a = Parameter('a', sw=1, values=[[2]])
- output1 = Output('out', Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target1 = Input("out1")
+ target2 = Input("out2")
+ a = Parameter("a", sw=1, values=[[2]])
+ output1 = Output("out", Fir(W=a)(input1.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', output1)
- test.addMinimize('error1', target1.last(), output1)
- test.addMinimize('error2', target2.last(), output1)
+ test.addModel("model", output1)
+ test.addMinimize("error1", target1.last(), output1)
+ test.addMinimize("error2", target2.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'out1': [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
- 'out2': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]}
- test.loadData(name='dataset', source=dataset)
-
- test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=5, batch_size=2)
- A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]]
- B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
- [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
- test.performance['dataset']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
- test.performance['dataset']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + np.sum(
- (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0) / 2.0,
- test.performance['dataset']['total']['mean_error'], places=3)
-
- test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=5, batch_size=4)
- A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]]
- B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
- [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
- test.performance['dataset']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
- test.performance['dataset']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + np.sum(
- (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0) / 2.0,
- test.performance['dataset']['total']['mean_error'], places=3)
-
- test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=4, batch_size=6)
- A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]],
- [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]],
- [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]],
- [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]],
- [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]]]
- B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]],
- [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]],
- [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]],
- [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]],
- [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]]]
- C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]],
- [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]],
- [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]],
- [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]],
- [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]]]
- self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1'])
- self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2'])
- self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0,
- test.performance['dataset']['error1']['mse'], places=3)
- self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0,
- test.performance['dataset']['error2']['mse'], places=3)
- self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0 + np.sum(
- (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0) / 2.0,
- test.performance['dataset']['total']['mean_error'], places=3)
+ dataset = {
+ "in1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "out1": [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
+ "out2": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
+ }
+ test.loadData(name="dataset", source=dataset)
+
+ test.analyzeModel(
+ "dataset", closed_loop={"in1": "out"}, prediction_samples=5, batch_size=2
+ )
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
+ [[[64.0]], [[128.0]], [[192.0]], [[256.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ )
+ / 2.0,
+ test.performance["dataset"]["total"]["mean_error"],
+ places=3,
+ )
+
+ test.analyzeModel(
+ "dataset", closed_loop={"in1": "out"}, prediction_samples=5, batch_size=4
+ )
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]]],
+ [[[64.0]], [[128.0]], [[192.0]], [[256.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0,
+ test.performance["dataset"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0
+ )
+ / 2.0,
+ test.performance["dataset"]["total"]["mean_error"],
+ places=3,
+ )
+
+ test.analyzeModel(
+ "dataset", closed_loop={"in1": "out"}, prediction_samples=4, batch_size=6
+ )
+ A = [
+ [[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]],
+ [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]],
+ [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]],
+ [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]],
+ [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]],
+ ]
+ B = [
+ [[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]],
+ [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]],
+ [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]],
+ [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]],
+ [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]],
+ ]
+ C = [
+ [[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]],
+ [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]],
+ [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]],
+ [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]],
+ [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]],
+ ]
+ self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"])
+ self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"])
+ self.assertAlmostEqual(
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0,
+ test.performance["dataset"]["error1"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0,
+ test.performance["dataset"]["error2"]["mse"],
+ places=3,
+ )
+ self.assertAlmostEqual(
+ (
+ np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0
+ + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0
+ )
+ / 2.0,
+ test.performance["dataset"]["total"]["mean_error"],
+ places=3,
+ )
diff --git a/tests/test_train.py b/tests/test_train.py
index a9fe6fe4..90f7e370 100644
--- a/tests/test_train.py
+++ b/tests/test_train.py
@@ -1,4 +1,7 @@
-import unittest, os, sys, torch
+import unittest
+import os
+import sys
+import torch
import numpy as np
from nnodely import *
@@ -14,283 +17,354 @@
# 5 Tests
# This file tests the value of the training parameters
-data_folder = os.path.join(os.path.dirname(__file__), '_data/')
+data_folder = os.path.join(os.path.dirname(__file__), "_data/")
+
class ModelyTrainingTest(unittest.TestCase):
def test_training_values_fir(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('out1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out', Fir(W=a)(input1.last()))
- output2 = Output('out2', Fir(W_init='init_constant', W_init_params={'value':1})(input1.last()))
- output3 = Output('out3', Fir(W_init='init_exp', b_init='init_exp')(input1.last()))
- output4 = Output('out4', Fir(W_init='init_lin', b_init='init_lin')(input1.last()))
- output5 = Output('out5', Fir(W_init='init_negexp', b_init='init_negexp')(input1.last()))
-
- test = Modely(visualizer=None,seed=42)
- test.addModel('model', [output1,output2,output3,output4,output5])
- test.addMinimize('error', target.last(), output1)
+ input1 = Input("in1")
+ target = Input("out1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out", Fir(W=a)(input1.last()))
+ output2 = Output(
+ "out2",
+ Fir(W_init="init_constant", W_init_params={"value": 1})(input1.last()),
+ )
+ output3 = Output(
+ "out3", Fir(W_init="init_exp", b_init="init_exp")(input1.last())
+ )
+ output4 = Output(
+ "out4", Fir(W_init="init_lin", b_init="init_lin")(input1.last())
+ )
+ output5 = Output(
+ "out5", Fir(W_init="init_negexp", b_init="init_negexp")(input1.last())
+ )
+
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("model", [output1, output2, output3, output4, output5])
+ test.addMinimize("error", target.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1], 'in2':[[1,2,3]], 'out1': [2]}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"in1": [1], "in2": [[1, 2, 3]], "out1": [2]}
+ test.loadData(name="dataset", source=dataset)
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2)
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2)
+ self.assertListEqual([[1.0]], test.parameters["a"])
def test_training_values_linear(self):
NeuObj.clearNames()
- input1 = Input('in1')
- input2 = Input('in2', dimensions=3)
- target = Input('out1')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output1 = Output('out', Linear(W=W,b=b)(input1.last()))
- output2 = Output('out2', Linear(W_init='init_constant', W_init_params={'value':1})(input1.last()))
- output3 = Output('out3', Linear(W_init='init_exp', b_init='init_exp')(input2.last()))
- output4 = Output('out4', Linear(W_init='init_negexp', b_init='init_negexp')(input2.last()))
- output5 = Output('out5', Linear(W_init='init_lin', b_init='init_lin')(input2.last()))
+ input1 = Input("in1")
+ input2 = Input("in2", dimensions=3)
+ target = Input("out1")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output1 = Output("out", Linear(W=W, b=b)(input1.last()))
+ output2 = Output(
+ "out2",
+ Linear(W_init="init_constant", W_init_params={"value": 1})(input1.last()),
+ )
+ output3 = Output(
+ "out3", Linear(W_init="init_exp", b_init="init_exp")(input2.last())
+ )
+ output4 = Output(
+ "out4", Linear(W_init="init_negexp", b_init="init_negexp")(input2.last())
+ )
+ output5 = Output(
+ "out5", Linear(W_init="init_lin", b_init="init_lin")(input2.last())
+ )
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2,output3,output4,output5])
- test.addMinimize('error', target.last(), output1)
+ test.addModel("model", [output1, output2, output3, output4, output5])
+ test.addMinimize("error", target.last(), output1)
test.neuralizeModel()
- dataset = {'in1': [1], 'in2':[[1,2,3]], 'out1': [3]}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"in1": [1], "in2": [[1, 2, 3]], "out1": [3]}
+ test.loadData(name="dataset", source=dataset)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
def test_training_clear_model(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('int1')
- a = Parameter('a', sw=1, values=[[1]])
+ input1 = Input("in1")
+ target = Input("int1")
+ a = Parameter("a", sw=1, values=[[1]])
fir_out = Fir(W=a)(input1.last())
- output1 = Output('out1', fir_out)
+ output1 = Output("out1", fir_out)
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output2 = Output('out2', Linear(W=W,b=b)(fir_out))
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output2 = Output("out2", Linear(W=W, b=b)(fir_out))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]}))
-
- dataset = {'in1': [1], 'int1': [3]}
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]}))
+
+ dataset = {"in1": [1], "int1": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
def test_network_linear_interpolation_train(self):
NeuObj.clearNames()
- x = Input('x')
- param = Parameter(name='a', sw=1)
- rel1 = Fir(W=param)(Interpolation([1.0, 2.0, 3.0, 4.0], [2.0, 4.0, 6.0, 8.0], mode='linear')(x.last()))
- out = Output('out',rel1)
+ x = Input("x")
+ param = Parameter(name="a", sw=1)
+ rel1 = Fir(W=param)(
+ Interpolation([1.0, 2.0, 3.0, 4.0], [2.0, 4.0, 6.0, 8.0], mode="linear")(
+ x.last()
+ )
+ )
+ out = Output("out", rel1)
test = Modely(visualizer=None, seed=1)
- test.addModel('fun',[out])
- test.addMinimize('error', out, x.last())
+ test.addModel("fun", [out])
+ test.addMinimize("error", out, x.last())
test.neuralizeModel(0.01)
- dataset = {'x':np.random.uniform(1,4,100)}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"x": np.random.uniform(1, 4, 100)}
+ test.loadData(name="dataset", source=dataset)
test.trainModel(num_of_epochs=100, train_batch_size=10)
- self.assertAlmostEqual(test.parameters['a'][0][0], 0.5, places=2)
+ self.assertAlmostEqual(test.parameters["a"][0][0], 0.5, places=2)
def test_multimodel_with_loss_gain_and_lr_gain(self):
NeuObj.clearNames()
## Model1
- input1 = Input('in1')
- a1 = Parameter('a1', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output11 = Output('out11', Fir(W=a1)(input1.tw(0.05)))
- a2 = Parameter('a2', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
- output12 = Output('out12', Fir(W=a2)(input1.tw(0.05)))
- a3 = Parameter('a3', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
- output13 = Output('out13', Fir(W=a3)(input1.tw(0.05)))
+ input1 = Input("in1")
+ a1 = Parameter("a1", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output11 = Output("out11", Fir(W=a1)(input1.tw(0.05)))
+ a2 = Parameter("a2", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output12 = Output("out12", Fir(W=a2)(input1.tw(0.05)))
+ a3 = Parameter("a3", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output13 = Output("out13", Fir(W=a3)(input1.tw(0.05)))
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', [output11, output12, output13])
- test.addMinimize('error11', input1.next(), output11)
- test.addMinimize('error12', input1.next(), output12)
- test.addMinimize('error13', input1.next(), output13)
+ test.addModel("model1", [output11, output12, output13])
+ test.addMinimize("error11", input1.next(), output11)
+ test.addMinimize("error12", input1.next(), output12)
+ test.addMinimize("error13", input1.next(), output13)
## Model2
- input2 = Input('in2')
- b1 = Parameter('b1', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output21 = Output('out21', Fir(W=b1)(input2.tw(0.05)))
- b2 = Parameter('b2', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output22 = Output('out22', Fir(W=b2)(input2.tw(0.05)))
- b3 = Parameter('b3', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]])
- output23 = Output('out23', Fir(W=b3)(input2.tw(0.05)))
-
- test.addModel('model2', [output21, output22, output23])
- test.addMinimize('error21', input2.next(), output21)
- test.addMinimize('error22', input2.next(), output22)
- test.addMinimize('error23', input2.next(), output23)
+ input2 = Input("in2")
+ b1 = Parameter("b1", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output21 = Output("out21", Fir(W=b1)(input2.tw(0.05)))
+ b2 = Parameter("b2", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output22 = Output("out22", Fir(W=b2)(input2.tw(0.05)))
+ b3 = Parameter("b3", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]])
+ output23 = Output("out23", Fir(W=b3)(input2.tw(0.05)))
+
+ test.addModel("model2", [output21, output22, output23])
+ test.addMinimize("error21", input2.next(), output21)
+ test.addMinimize("error22", input2.next(), output22)
+ test.addMinimize("error23", input2.next(), output23)
test.neuralizeModel(0.01)
data_in1 = [1, 1, 1, 1, 1, 2]
data_in2 = [1, 1, 1, 1, 1, 2]
- dataset = {'in1': data_in1, 'in2': data_in2}
+ dataset = {"in1": data_in1, "in2": data_in2}
- test.loadData(name='dataset', source=dataset)
+ test.loadData(name="dataset", source=dataset)
## Train only model1
- self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['a2'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b2'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['a3'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b3'], [[1], [1], [1], [1], [1]])
- test.trainModel(optimizer='SGD', models='model1', splits=[100,0,0], lr=1, num_of_epochs=1)
- self.assertListEqual(test.parameters['a1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b2'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b3'], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["a2"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b2"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["a3"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b3"], [[1], [1], [1], [1], [1]])
+ test.trainModel(
+ optimizer="SGD", models="model1", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual(test.parameters["a1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b2"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b3"], [[1], [1], [1], [1], [1]])
## Train only model2
test.neuralizeModel(0.01, clear_model=True)
- test.trainModel(optimizer='SGD', models='model2', splits=[100,0,0], lr=1, num_of_epochs=1)
- self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a2'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a3'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]])
+ test.trainModel(
+ optimizer="SGD", models="model2", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a2"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a3"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]])
## Train both models
test.neuralizeModel(0.01, clear_model=True)
- test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1)
- self.assertListEqual(test.parameters['a1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]])
+ test.trainModel(
+ optimizer="SGD",
+ models=["model1", "model2"],
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ )
+ self.assertListEqual(test.parameters["a1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]])
## Train both models but set the gain of a to zero and the gain of b to double
test.neuralizeModel(0.01, clear_model=True)
- test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, lr_param={'a1':0, 'b1':2})
- self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b1'], [[-11], [-11], [-11], [-11], [-11]])
- self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]])
+ test.trainModel(
+ optimizer="SGD",
+ models=["model1", "model2"],
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ lr_param={"a1": 0, "b1": 2},
+ )
+ self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b1"], [[-11], [-11], [-11], [-11], [-11]])
+ self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]])
## Train both models but set the minimize gain of error1 to zero and the minimize gain of error2 to double
test.neuralizeModel(0.01, clear_model=True)
- test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, minimize_gain={'error11':0})
- self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]])
- self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]])
+ test.trainModel(
+ optimizer="SGD",
+ models=["model1", "model2"],
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ minimize_gain={"error11": 0},
+ )
+ self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]])
+ self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]])
## Train both models but set the minimize gain of error1 to zero and the minimize gain of error2 to double
test.neuralizeModel(0.01, clear_model=True)
- test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, minimize_gain={'error11':-1,'error22':2})
- self.assertListEqual(test.parameters['a1'], [[7], [7], [7], [7], [7]])
- self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b2'], [[-11], [-11], [-11], [-11], [-11]])
- self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]])
- self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]])
+ test.trainModel(
+ optimizer="SGD",
+ models=["model1", "model2"],
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ minimize_gain={"error11": -1, "error22": 2},
+ )
+ self.assertListEqual(test.parameters["a1"], [[7], [7], [7], [7], [7]])
+ self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b2"], [[-11], [-11], [-11], [-11], [-11]])
+ self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]])
+ self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]])
def test_train_equation_learner(self):
# TODO aggiungi la verifica dei parametri
NeuObj.clearNames()
+
def func(x):
return np.cos(x) + np.sin(x)
-
- data_x = np.random.uniform(0, 2*np.pi, 200)
+
+ data_x = np.random.uniform(0, 2 * np.pi, 200)
data_y = func(data_x)
- dataset = {'x': data_x, 'y': data_y}
+ dataset = {"x": data_x, "y": data_y}
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
linear_in = Linear(output_dimension=5)
linear_in_2 = Linear(output_dimension=5)
- linear_out = Linear(output_dimension=1, W_init=init_constant, W_init_params={'value':1})
+ linear_out = Linear(
+ output_dimension=1, W_init=init_constant, W_init_params={"value": 1}
+ )
- equation_learner = EquationLearner(functions=[Sin, Identity, Add, Cos], linear_in=linear_in) ## W=1*5 , b=1, activation_out=4
- equation_learner2 = EquationLearner(functions=[Add, Identity, Mul],linear_in=linear_in_2, linear_out=linear_out) ## INGRESSO W=4*5, b=5, activation_out=3 USCITA W=3*1, b=1
+ equation_learner = EquationLearner(
+ functions=[Sin, Identity, Add, Cos], linear_in=linear_in
+ ) ## W=1*5 , b=1, activation_out=4
+ equation_learner2 = EquationLearner(
+ functions=[Add, Identity, Mul], linear_in=linear_in_2, linear_out=linear_out
+ ) ## INGRESSO W=4*5, b=5, activation_out=3 USCITA W=3*1, b=1
eq1 = equation_learner(x.last())
eq2 = equation_learner2(eq1)
- out = Output('eq2', eq2)
+ out = Output("eq2", eq2)
example = Modely(visualizer=None)
- example.addModel('model',[out])
- example.addMinimize('error', out, y.last())
+ example.addModel("model", [out])
+ example.addMinimize("error", out, y.last())
example.neuralizeModel()
- example.loadData(name='dataset', source=dataset)
+ example.loadData(name="dataset", source=dataset)
## Print the initial weights
- optimizer_defaults = {'weight_decay': 0.3,}
- example.trainModel(train_dataset='dataset', lr=0.01, num_of_epochs=2, optimizer_defaults=optimizer_defaults, early_stopping=select_best_model)
+ optimizer_defaults = {
+ "weight_decay": 0.3,
+ }
+ example.trainModel(
+ train_dataset="dataset",
+ lr=0.01,
+ num_of_epochs=2,
+ optimizer_defaults=optimizer_defaults,
+ early_stopping=select_best_model,
+ )
def test_train_derivate_wrt_input(self):
NeuObj.clearNames()
- x = Input('x')
- dy_dx_target = Input('dy_dx')
+ x = Input("x")
+ dy_dx_target = Input("dy_dx")
x_last = x.last()
def parametric_fun(x, a, b, c, d):
import torch
- return x ** 3 * a + x ** 2 * b + torch.sin(x) * c + d
+
+ return x**3 * a + x**2 * b + torch.sin(x) * c + d
def dx_parametric_fun(x, a, b, c, d):
import torch
- return (3 * x ** 2 * a) + (2 * x * b) + c * torch.cos(x)
- fun = ParamFun(parametric_fun,['a','b','c','d'])(x_last)
- approx_dy_dx = Output('d_out', Differentiate(fun, x_last))
+ return (3 * x**2 * a) + (2 * x * b) + c * torch.cos(x)
+
+ fun = ParamFun(parametric_fun, ["a", "b", "c", "d"])(x_last)
+ approx_dy_dx = Output("d_out", Differentiate(fun, x_last))
test = Modely(visualizer=None, seed=12)
@@ -300,45 +374,108 @@ def dx_parametric_fun(x, a, b, c, d):
data_b = -0.03
data_c = 2.02
data_d = -1.05
- dataset = {'x': data_x, 'dy_dx': dx_parametric_fun(data_x, data_a, data_b, data_c, data_d)}
+ dataset = {
+ "x": data_x,
+ "dy_dx": dx_parametric_fun(data_x, data_a, data_b, data_c, data_d),
+ }
# d y_approx / d x == dy_dx
# Se x era una time window and dy_dx dovrà essere una time window
- test.addModel('model', [approx_dy_dx])
- test.addMinimize('sob_err', 'd_out', dy_dx_target.last())
+ test.addModel("model", [approx_dy_dx])
+ test.addMinimize("sob_err", "d_out", dy_dx_target.last())
test.neuralizeModel()
- test.loadData('data', dataset)
- test.trainModel(num_of_epochs=1000, splits=[70,20,10], lr=0.3)
- self.assertAlmostEqual(test.parameters['a'][0], data_a, places=4)
- self.assertAlmostEqual(test.parameters['b'][0], data_b, places=4)
- self.assertAlmostEqual(test.parameters['c'][0], data_c, places=4)
- #The value data_d is not match because the derivative does not depend on it
-
+ test.loadData("data", dataset)
+ test.trainModel(num_of_epochs=1000, splits=[70, 20, 10], lr=0.3)
+ self.assertAlmostEqual(test.parameters["a"][0], data_a, places=4)
+ self.assertAlmostEqual(test.parameters["b"][0], data_b, places=4)
+ self.assertAlmostEqual(test.parameters["c"][0], data_c, places=4)
+ # The value data_d is not match because the derivative does not depend on it
+
def test_step(self):
NeuObj.clearNames()
- x = Input('x')
+ x = Input("x")
relation = Fir()(x.tw(0.05))
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error', output, x.next())
+ test.addModel("model", output)
+ test.addMinimize("error", output, x.next())
test.neuralizeModel(0.01)
- train_data_x = np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32)
- train_dataset = {'x': train_data_x, 'time': np.array(range(60), dtype=np.float32)}
- test.loadData(name='dataset', source=train_dataset, )
- self.assertListEqual(list(test._data['dataset']['x'].shape), [55, 6, 1]) ## 60 observations, time window of 6 so in total 54+1 samples
- test.trainModel(step=10, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 20) ## chosed 10 sample at index = [0, 21] and for each sample the horizon is 10 so in total 4*10=40
- test.trainModel(step=10, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=True, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 20) ## shuffle data does not change the number of samples
- test.trainModel(step=0, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 40) ## chosed 10 sample at index = [0, 11, 22, 33] and for each sample the horizon is 10 so in total 4*10=40
- test.trainModel(step=1000, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 10) ## clip step to max value = 36 so just 1 sample * 10 prediction samples
- test.trainModel(step=10, train_batch_size=1, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 50) ## chosed sample = [0, 11, 22, 33, 44] and for each sample the horizon is 10 so in total 5*10=50
- test.trainModel(step=0, train_batch_size=1, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9)
- self.assertEqual(len(test.internals.keys()), 460) ## 46 sample * 10 horizon
\ No newline at end of file
+ train_data_x = np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32)
+ train_dataset = {
+ "x": train_data_x,
+ "time": np.array(range(60), dtype=np.float32),
+ }
+ test.loadData(
+ name="dataset",
+ source=train_dataset,
+ )
+ self.assertListEqual(
+ list(test._data["dataset"]["x"].shape), [55, 6, 1]
+ ) ## 60 observations, time window of 6 so in total 54+1 samples
+ test.trainModel(
+ step=10,
+ train_batch_size=10,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=9,
+ )
+ self.assertEqual(
+ len(test.internals.keys()), 20
+ ) ## chosed 10 sample at index = [0, 21] and for each sample the horizon is 10 so in total 4*10=40
+ test.trainModel(
+ step=10,
+ train_batch_size=10,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=True,
+ prediction_samples=9,
+ )
+ self.assertEqual(
+ len(test.internals.keys()), 20
+ ) ## shuffle data does not change the number of samples
+ test.trainModel(
+ step=0,
+ train_batch_size=10,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=9,
+ )
+ self.assertEqual(
+ len(test.internals.keys()), 40
+ ) ## chosed 10 sample at index = [0, 11, 22, 33] and for each sample the horizon is 10 so in total 4*10=40
+ test.trainModel(
+ step=1000,
+ train_batch_size=10,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=9,
+ )
+ self.assertEqual(
+ len(test.internals.keys()), 10
+ ) ## clip step to max value = 36 so just 1 sample * 10 prediction samples
+ test.trainModel(
+ step=10,
+ train_batch_size=1,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=9,
+ )
+ self.assertEqual(
+ len(test.internals.keys()), 50
+ ) ## chosed sample = [0, 11, 22, 33, 44] and for each sample the horizon is 10 so in total 5*10=50
+ test.trainModel(
+ step=0,
+ train_batch_size=1,
+ train_dataset="dataset",
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=9,
+ )
+ self.assertEqual(len(test.internals.keys()), 460) ## 46 sample * 10 horizon
diff --git a/tests/test_train_recurrent.py b/tests/test_train_recurrent.py
index da059a37..47f6dbf4 100644
--- a/tests/test_train_recurrent.py
+++ b/tests/test_train_recurrent.py
@@ -1,6 +1,7 @@
-import unittest, os, sys
+import unittest
+import os
+import sys
import numpy as np
-from pygments.unistring import xid_start
from nnodely import *
from nnodely.basic.relation import NeuObj
@@ -14,701 +15,1352 @@
# 15 Tests
# Test the value of the weight after the recurrent training
+
# Linear function
-def linear_fun(x,a,b):
- return x*a+b
+def linear_fun(x, a, b):
+ return x * a + b
+
-data_x = np.random.rand(500)*20-10
+data_x = np.random.rand(500) * 20 - 10
data_a = 2
data_b = -3
-dataset = {'in1': data_x, 'out': linear_fun(data_x,data_a,data_b)}
-data_folder = '/tests/_data/'
+dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)}
+data_folder = "/tests/_data/"
+
class ModelyTrainingTest(unittest.TestCase):
def assertAlmostEqual(self, data1, data2, precision=3):
if type(data1) == type(data2) == list:
- assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2,dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}'
+ assert (
+ np.asarray(data1, dtype=np.float32).ndim
+ == np.asarray(data2, dtype=np.float32).ndim
+ ), (
+ f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}"
+ )
self.assertEqual(len(data1), len(data2))
for pred, label in zip(data1, data2):
self.assertAlmostEqual(pred, label, precision=precision)
elif type(data1) == type(data2) == dict:
self.assertEqual(len(data1.items()), len(data2.items()))
- for (pred_key,pred_value), (label_key,label_value) in zip(data1.items(), data2.items()):
+ for (pred_key, pred_value), (label_key, label_value) in zip(
+ data1.items(), data2.items()
+ ):
self.assertAlmostEqual(pred_value, label_value, precision=precision)
else:
super().assertAlmostEqual(data1, data2, places=precision)
def test_recurrent_shuffle(self):
NeuObj.clearNames()
- target = Input('target')
- x = Input('x')
+ target = Input("target")
+ x = Input("x")
relation = Fir(x.last())
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, seed=42, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('out', target.next(), 'out')
+ test.addModel("model", output)
+ test.addMinimize("out", target.next(), "out")
test.neuralizeModel(0.01)
- dataset = {'x': [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], 'target': [21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]}
- test.loadData(name='dataset', source=dataset)
-
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=4, prediction_samples=1, step=1, shuffle_data=True)
- self.assertListEqual([[[4.0]], [[1.0]], [[9.0]], [[18.0]]], test.internals['inout_0_0']['XY']['x'])
- self.assertListEqual([[[25.0]], [[22.0]], [[30.0]], [[39.0]]], test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[26.0]], [[23.0]], [[31.0]], [[40.0]]], test.internals['inout_0_1']['XY']['target'])
- self.assertListEqual([[[5.0]], [[16.0]], [[3.0]], [[13.0]]], test.internals['inout_1_0']['XY']['x'])
- self.assertListEqual([[[26.0]], [[37.0]], [[24.0]], [[34.0]]], test.internals['inout_1_0']['XY']['target'])
- self.assertListEqual([[[27.0]], [[38.0]], [[25.0]], [[35.0]]], test.internals['inout_1_1']['XY']['target'])
- self.assertListEqual([[[15.0]], [[2.0]], [[17.0]], [[14.0]]], test.internals['inout_2_0']['XY']['x'])
- self.assertListEqual([[[36.0]], [[23.0]], [[38.0]], [[35.0]]], test.internals['inout_2_0']['XY']['target'])
- self.assertListEqual([[[37.0]], [[24.0]], [[39.0]], [[36.0]]], test.internals['inout_2_1']['XY']['target'])
-
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2,
- prediction_samples=2, step=0, shuffle_data=True)
+ dataset = {
+ "x": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ ],
+ "target": [
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ ],
+ }
+ test.loadData(name="dataset", source=dataset)
+
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=0.01,
+ num_of_epochs=1,
+ train_batch_size=4,
+ prediction_samples=1,
+ step=1,
+ shuffle_data=True,
+ )
+ self.assertListEqual(
+ [[[4.0]], [[1.0]], [[9.0]], [[18.0]]],
+ test.internals["inout_0_0"]["XY"]["x"],
+ )
+ self.assertListEqual(
+ [[[25.0]], [[22.0]], [[30.0]], [[39.0]]],
+ test.internals["inout_0_0"]["XY"]["target"],
+ )
+ self.assertListEqual(
+ [[[26.0]], [[23.0]], [[31.0]], [[40.0]]],
+ test.internals["inout_0_1"]["XY"]["target"],
+ )
+ self.assertListEqual(
+ [[[5.0]], [[16.0]], [[3.0]], [[13.0]]],
+ test.internals["inout_1_0"]["XY"]["x"],
+ )
+ self.assertListEqual(
+ [[[26.0]], [[37.0]], [[24.0]], [[34.0]]],
+ test.internals["inout_1_0"]["XY"]["target"],
+ )
+ self.assertListEqual(
+ [[[27.0]], [[38.0]], [[25.0]], [[35.0]]],
+ test.internals["inout_1_1"]["XY"]["target"],
+ )
+ self.assertListEqual(
+ [[[15.0]], [[2.0]], [[17.0]], [[14.0]]],
+ test.internals["inout_2_0"]["XY"]["x"],
+ )
+ self.assertListEqual(
+ [[[36.0]], [[23.0]], [[38.0]], [[35.0]]],
+ test.internals["inout_2_0"]["XY"]["target"],
+ )
+ self.assertListEqual(
+ [[[37.0]], [[24.0]], [[39.0]], [[36.0]]],
+ test.internals["inout_2_1"]["XY"]["target"],
+ )
+
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=0.01,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=2,
+ step=0,
+ shuffle_data=True,
+ )
# ( number_samples - window_size - prediction_samples )// (batch_size + step=0) * (predictoin_samples+1)
- self.assertEqual((20-1-2)//2*3, len(test.internals.keys()))
+ self.assertEqual((20 - 1 - 2) // 2 * 3, len(test.internals.keys()))
with self.assertRaises(ValueError):
- test.trainModel(dataset='dataset', splits=[40,30,30], optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2,
- prediction_samples=50, step=0, shuffle_data=True)
+ test.trainModel(
+ dataset="dataset",
+ splits=[40, 30, 30],
+ optimizer="SGD",
+ lr=0.01,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=50,
+ step=0,
+ shuffle_data=True,
+ )
with self.assertRaises(ValueError):
- test.trainModel(dataset='dataset', splits=[40,10,40], optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2,
- prediction_samples=50, step=0, shuffle_data=True)
+ test.trainModel(
+ dataset="dataset",
+ splits=[40, 10, 40],
+ optimizer="SGD",
+ lr=0.01,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=50,
+ step=0,
+ shuffle_data=True,
+ )
from nnodely.support.earlystopping import early_stop_patience, select_best_model
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=15,
- train_batch_size=2, early_stopping=early_stop_patience, early_stopping_params={'patience':2}, select_model=select_best_model,
- prediction_samples=2, step=0, shuffle_data=True)
+
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=0.01,
+ num_of_epochs=15,
+ train_batch_size=2,
+ early_stopping=early_stop_patience,
+ early_stopping_params={"patience": 2},
+ select_model=select_best_model,
+ prediction_samples=2,
+ step=0,
+ shuffle_data=True,
+ )
def test_train_multifiles(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
- relation = Fir()(x.tw(0.05))+Fir(y.sw([-2,2]))
+ x = Input("x")
+ y = Input("y")
+ relation = Fir()(x.tw(0.05)) + Fir(y.sw([-2, 2]))
relation.closedLoop(x)
- output = Output('out', relation)
+ output = Output("out", relation)
test = Modely(visualizer=None, log_internal=True)
- test.addModel('model', output)
- test.addMinimize('error', 'out', x.next())
+ test.addModel("model", output)
+ test.addMinimize("error", "out", x.next())
test.neuralizeModel(0.01)
## The folder contains 3 files with 10, 20 and 30 samples respectively
- data_struct = ['x', 'y']
- data_folder = os.path.join(os.path.dirname(__file__), 'multifile/')
- test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1)
- self.assertEqual(len(test._data['dataset']['x']), 42)
- self.assertEqual(len(test._data['dataset']['y']), 42)
-
- test.trainModel(splits=[70, 20, 10], train_batch_size = 3, num_of_epochs=1, prediction_samples=2)
- self.assertEqual(len(list(test.internals.keys())), 3*7)
- self.assertEqual(list(np.mean(np.array(test.internals['inout_0_0']['XY']['y']),axis=1)),
- list(np.mean(np.array(test.internals['inout_0_1']['XY']['y']),axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_0_0']['XY']['y']), axis=1)),
- list(np.mean(np.array(test.internals['inout_0_2']['XY']['y']), axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_1_0']['XY']['y']),axis=1)),
- list(np.mean(np.array(test.internals['inout_1_1']['XY']['y']),axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_1_0']['XY']['y']), axis=1)),
- list(np.mean(np.array(test.internals['inout_1_2']['XY']['y']), axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_2_0']['XY']['y']),axis=1)),
- list(np.mean(np.array(test.internals['inout_2_1']['XY']['y']),axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_2_0']['XY']['y']), axis=1)),
- list(np.mean(np.array(test.internals['inout_2_2']['XY']['y']), axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_6_0']['XY']['y']),axis=1)),
- list(np.mean(np.array(test.internals['inout_6_1']['XY']['y']),axis=1)))
- self.assertEqual(list(np.mean(np.array(test.internals['inout_6_0']['XY']['y']), axis=1)),
- list(np.mean(np.array(test.internals['inout_6_2']['XY']['y']), axis=1)))
+ data_struct = ["x", "y"]
+ data_folder = os.path.join(os.path.dirname(__file__), "multifile/")
+ test.loadData(
+ name="dataset", source=data_folder, format=data_struct, skiplines=1
+ )
+ self.assertEqual(len(test._data["dataset"]["x"]), 42)
+ self.assertEqual(len(test._data["dataset"]["y"]), 42)
+
+ test.trainModel(
+ splits=[70, 20, 10],
+ train_batch_size=3,
+ num_of_epochs=1,
+ prediction_samples=2,
+ )
+ self.assertEqual(len(list(test.internals.keys())), 3 * 7)
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_0_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_0_1"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_0_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_0_2"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_1_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_1_1"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_1_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_1_2"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_2_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_2_1"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_2_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_2_2"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_6_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_6_1"]["XY"]["y"]), axis=1)),
+ )
+ self.assertEqual(
+ list(np.mean(np.array(test.internals["inout_6_0"]["XY"]["y"]), axis=1)),
+ list(np.mean(np.array(test.internals["inout_6_2"]["XY"]["y"]), axis=1)),
+ )
def test_training_values_fir_connect_linear(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
+ input1 = Input("in1")
+ target = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
relation = Fir(W=a)(input1.last())
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
relation.connect(inout)
- output1 = Output('out1', relation)
- output2 = Output('out2', Linear(W=W,b=b)(inout.last()))
+ output1 = Output("out1", relation)
+ output2 = Output("out2", Linear(W=W, b=b)(inout.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]}))
-
- dataset = {'in1': [1], 'target1': [3]}
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]}))
+
+ dataset = {"in1": [1], "target1": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
-
- dataset = {'in1': [1,1], 'target1': [3,3]}
- test.loadData(name='dataset2', source=dataset)
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
+
+ dataset = {"in1": [1, 1], "target1": [3, 3]}
+ test.loadData(name="dataset2", source=dataset)
test.neuralizeModel(clear_model=True)
# the out is 3.0 due the mean of the error is not the same of two epochs
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=2
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
def test_training_values_fir_train_connect_linear(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('out1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1-net',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target = Input("out1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1-net", Fir(W=a)(input1.last()))
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=1)
- output2 = Output('out2-net', Linear(W=W,b=b)(inout.last()))
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=1)
+ output2 = Output("out2-net", Linear(W=W, b=b)(inout.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1-net': [1.0], 'out2-net': [2.0]}, test({'in1': [1]}, connect={'inout': 'out1-net'}))
-
- dataset = {'in1': [1], 'out1': [3]}
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'})
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertEqual(
+ {"out1-net": [1.0], "out2-net": [2.0]},
+ test({"in1": [1]}, connect={"inout": "out1-net"}),
+ )
+
+ dataset = {"in1": [1], "out1": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'})
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2, connect={'inout': 'out1-net'})
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=2,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
with self.assertRaises(KeyError):
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2)
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2)
- dataset = {'in1': [1,1], 'out1': [3,3]}
- test.loadData(name='dataset2', source=dataset)
+ dataset = {"in1": [1, 1], "out1": [3, 3]}
+ test.loadData(name="dataset2", source=dataset)
test.neuralizeModel(clear_model=True)
# the out is 3.0 due the mean of the error is not the same of two epochs
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, connect={'inout': 'out1-net'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2, connect={'inout': 'out1-net'})
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=2,
+ connect={"inout": "out1-net"},
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
def test_training_values_fir_connect_linear_only_model(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1", Fir(W=a)(input1.last()))
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output2 = Output('out2', Linear(W=W,b=b)(inout.last()))
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output2 = Output("out2", Linear(W=W, b=b)(inout.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', output1)
- test.addModel('model2', output2)
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model1", output1)
+ test.addModel("model2", output2)
+ test.addMinimize("error", target.last(), output2)
test.addConnect(output1, inout)
test.neuralizeModel()
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]}))
+ self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]}))
- dataset = {'in1': [1], 'target1': [3]}
- test.loadData(name='dataset', source=dataset)
+ dataset = {"in1": [1], "target1": [3]}
+ test.loadData(name="dataset", source=dataset)
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ models="model1", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(
+ models="model1", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ models="model2", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ models="model2", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
def test_training_values_fir_train_connect_linear_only_model(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1", Fir(W=a)(input1.last()))
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output2 = Output('out2', Linear(W=W,b=b)(inout.last()))
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output2 = Output("out2", Linear(W=W, b=b)(inout.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model1', output1)
- test.addModel('model2', output2)
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model1", output1)
+ test.addModel("model2", output2)
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [1.0], 'out2': [1.0]}, test({'in1': [1]}))
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]}, connect={'inout': 'out1'}))
-
- dataset = {'in1': [1], 'target1': [3]}
- test.loadData(name='dataset', source=dataset)
-
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[-51.0]], test.parameters['W'])
- self.assertListEqual([-15.0], test.parameters['b'])
- self.assertListEqual([[-51.0]], test.parameters['a'])
+ self.assertEqual({"out1": [1.0], "out2": [1.0]}, test({"in1": [1]}))
+ self.assertEqual(
+ {"out1": [1.0], "out2": [2.0]},
+ test({"in1": [1]}, connect={"inout": "out1"}),
+ )
+
+ dataset = {"in1": [1], "target1": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-51.0]], test.parameters["W"])
+ self.assertListEqual([-15.0], test.parameters["b"])
+ self.assertListEqual([[-51.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[3.0]], test.parameters['a'])
- test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ models="model1",
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[3.0]], test.parameters["a"])
+ test.trainModel(
+ models="model1",
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'})
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ models="model2",
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ models="model2",
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
def test_training_values_fir_connect_linear_more_prediction(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('out1')
- a = Parameter('a', sw=1, values=[[1]])
- relation =Fir(W=a)(input1.last())
+ input1 = Input("in1")
+ target = Input("out1")
+ a = Parameter("a", sw=1, values=[[1]])
+ relation = Fir(W=a)(input1.last())
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=1)
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=1)
relation.connect(inout)
- output1 = Output('out1-net', relation)
- output2 = Output('out2-net', Linear(W=W,b=b)(inout.last()))
+ output1 = Output("out1-net", relation)
+ output2 = Output("out2-net", Linear(W=W, b=b)(inout.last()))
- test = Modely(visualizer=None,seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error', target.last(), output2)
+ test = Modely(visualizer=None, seed=42)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1-net': [1.0], 'out2-net': [2.0]}, test({'in1': [1]}))
-
- dataset = {'in1': [0,2,7,1], 'out1': [3,4,5,1], 'inout': [1,1,2,2]}
- test.loadData(name='dataset2', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[-9.0]], test.parameters['W'])
- self.assertEqual([0.5], test.parameters['b'])
- self.assertListEqual([[-9.0]], test.parameters['a'])
+ self.assertEqual({"out1-net": [1.0], "out2-net": [2.0]}, test({"in1": [1]}))
+
+ dataset = {"in1": [0, 2, 7, 1], "out1": [3, 4, 5, 1], "inout": [1, 1, 2, 2]}
+ test.loadData(name="dataset2", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[-9.0]], test.parameters["W"])
+ self.assertEqual([0.5], test.parameters["b"])
+ self.assertListEqual([[-9.0]], test.parameters["a"])
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10)
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[-9.0]], test.parameters['W'])
- self.assertEqual([0.5], test.parameters['b'])
- self.assertListEqual([[-9.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[-9.0]], test.parameters["W"])
+ self.assertEqual([0.5], test.parameters["b"])
+ self.assertListEqual([[-9.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- #TODO add this test for check prediction_Sample -1 for connect
+ # TODO add this test for check prediction_Sample -1 for connect
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=-1)
# self.assertListEqual([[-9.0]], test.parameters['W']) ?
# self.assertEqual([0.5], test.parameters['b']) ?
# self.assertListEqual([[-9.0]], test.parameters['a']) ?
- dataset = {'in1': [0, 2, 7, 1, 5, 0, 2], 'out1': [1, 4, 8, 2, 6, 1, 1]}
- test.loadData(name='dataset3', source=dataset)
+ dataset = {"in1": [0, 2, 7, 1, 5, 0, 2], "out1": [1, 4, 8, 2, 6, 1, 1]}
+ test.loadData(name="dataset3", source=dataset)
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=2, prediction_samples=3)
- self.assertListEqual([[-162.75]], test.parameters['W'])
- self.assertEqual([-15.75], test.parameters['b'])
- self.assertListEqual([[-162.75]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[-162.75]], test.parameters["W"])
+ self.assertEqual([-15.75], test.parameters["b"])
+ self.assertListEqual([[-162.75]], test.parameters["a"])
# Because is a connect and the window is 1 the initialization of the state is overwritten by the out1
- test.loadData(name='dataset4', source=dataset|{'inout': [0,0,0,0,0,0,0]})
+ test.loadData(
+ name="dataset4", source=dataset | {"inout": [0, 0, 0, 0, 0, 0, 0]}
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset4', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=2, prediction_samples=3)
- self.assertListEqual([[-162.75]], test.parameters['W'])
- self.assertEqual([-15.75], test.parameters['b'])
- self.assertListEqual([[-162.75]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset4",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[-162.75]], test.parameters["W"])
+ self.assertEqual([-15.75], test.parameters["b"])
+ self.assertListEqual([[-162.75]], test.parameters["a"])
def test_training_values_fir_train_connect_linear_more_prediction(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1", Fir(W=a)(input1.last()))
- inout = Input('inout')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output2 = Output('out2', Linear(W=W,b=b)(inout.last()))
+ inout = Input("inout")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output2 = Output("out2", Linear(W=W, b=b)(inout.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error', target.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error", target.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]},connect={'inout': 'out1'}))
-
- dataset = {'in1': [0,2,7,1], 'target1': [3,4,5,1]}
- test.loadData(name='dataset2', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout': 'out1'})
- self.assertListEqual([[-9.0]], test.parameters['W'])
- self.assertListEqual([0.5], test.parameters['b'])
- self.assertListEqual([[-9.0]], test.parameters['a'])
+ self.assertEqual(
+ {"out1": [1.0], "out2": [2.0]},
+ test({"in1": [1]}, connect={"inout": "out1"}),
+ )
+
+ dataset = {"in1": [0, 2, 7, 1], "target1": [3, 4, 5, 1]}
+ test.loadData(name="dataset2", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-9.0]], test.parameters["W"])
+ self.assertListEqual([0.5], test.parameters["b"])
+ self.assertListEqual([[-9.0]], test.parameters["a"])
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, connect={'inout': 'out1'})
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ connect={"inout": "out1"},
+ )
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout': 'out1'}) # TODO add this test
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout': 'out1'})
- self.assertListEqual([[-9.0]], test.parameters['W'])
- self.assertListEqual([0.5], test.parameters['b'])
- self.assertListEqual([[-9.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-9.0]], test.parameters["W"])
+ self.assertListEqual([0.5], test.parameters["b"])
+ self.assertListEqual([[-9.0]], test.parameters["a"])
# Because is a connect and the window is 1 the initialization of the state is overwritten by the out1
- dataset = {'in1': [0,2,7,1], 'target1': [3,4,5,1], 'inout':[1,1,2,2]}
- test.loadData(name='dataset3', source=dataset)
+ dataset = {"in1": [0, 2, 7, 1], "target1": [3, 4, 5, 1], "inout": [1, 1, 2, 2]}
+ test.loadData(name="dataset3", source=dataset)
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout': 'out1'})
- self.assertListEqual([[-9.0]], test.parameters['W'])
- self.assertListEqual([0.5], test.parameters['b'])
- self.assertListEqual([[-9.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-9.0]], test.parameters["W"])
+ self.assertListEqual([0.5], test.parameters["b"])
+ self.assertListEqual([[-9.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[-273.0]], test.parameters['W'])
- self.assertListEqual([-137.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[-273.0]], test.parameters["W"])
+ self.assertListEqual([-137.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=-1)
- self.assertListEqual([[-273.0]], test.parameters['W'])
- self.assertListEqual([-137.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=-1,
+ )
+ self.assertListEqual([[-273.0]], test.parameters["W"])
+ self.assertListEqual([-137.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1)
- self.assertListEqual([[-273.0]], test.parameters['W'])
- self.assertListEqual([-137.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
-
- dataset = {'in1': [0, 2, 7, 1, 5, 0, 2], 'target1': [1, 4, 8, 2, 6, 1, 1]}
- test.loadData(name='dataset4', source=dataset)
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ )
+ self.assertListEqual([[-273.0]], test.parameters["W"])
+ self.assertListEqual([-137.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+
+ dataset = {"in1": [0, 2, 7, 1, 5, 0, 2], "target1": [1, 4, 8, 2, 6, 1, 1]}
+ test.loadData(name="dataset4", source=dataset)
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset4', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1,
- train_batch_size=2, prediction_samples=3, connect={'inout': 'out1'})
- self.assertListEqual([[-162.75]], test.parameters['W'])
- self.assertListEqual([-15.75], test.parameters['b'])
- self.assertListEqual([[-162.75]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset4",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-162.75]], test.parameters["W"])
+ self.assertListEqual([-15.75], test.parameters["b"])
+ self.assertListEqual([[-162.75]], test.parameters["a"])
# Because is a connect and the window is 1 the initialization of the state is overwritten by the out1
- test.loadData(name='dataset5', source=dataset | {'inout': [0, 0, 0, 0, 0, 0, 0]})
+ test.loadData(
+ name="dataset5", source=dataset | {"inout": [0, 0, 0, 0, 0, 0, 0]}
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset5', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1,
- train_batch_size=2, prediction_samples=3, connect={'inout': 'out1'})
- self.assertListEqual([[-162.75]], test.parameters['W'])
- self.assertListEqual([-15.75], test.parameters['b'])
- self.assertListEqual([[-162.75]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset5",
+ optimizer="SGD",
+ shuffle_data=False,
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[-162.75]], test.parameters["W"])
+ self.assertListEqual([-15.75], test.parameters["b"])
+ self.assertListEqual([[-162.75]], test.parameters["a"])
def test_training_values_fir_connect_linear_more_window(self):
NeuObj.clearNames()
- input1 = Input('in1', dimensions=2)
- W = Parameter('W', values=[[-1], [-5]])
- b = Parameter('b', values=1)
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=1)
lin_out = Linear(W=W, b=b)(input1.sw(2))
- inout = Input('inout')
- a = Parameter('a', sw=2, values=[[4], [5]])
- a_big = Parameter('ab', sw=5, values=[[1], [2], [3], [4], [5]])
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
+ a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]])
lin_out.connect(inout)
- output1 = Output('out1', lin_out)
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- output3 = Output('out3', Fir(W=a_big)(inout.sw(5)))
- output4 = Output('out4', Fir(W=a)(lin_out))
+ output1 = Output("out1", lin_out)
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ output3 = Output("out3", Fir(W=a_big)(inout.sw(5)))
+ output4 = Output("out4", Fir(W=a)(lin_out))
- target = Input('target')
+ target = Input("target")
test = Modely(visualizer=None, seed=42, log_internal=True)
- test.addModel('model', [output1, output2, output3, output4])
- test.addMinimize('error2', target.last(), output2)
- #test.addMinimize('error3', target.last(), output3)
- #test.addMinimize('error4', target.last(), output4)
+ test.addModel("model", [output1, output2, output3, output4])
+ test.addMinimize("error2", target.last(), output2)
+ # test.addMinimize('error3', target.last(), output3)
+ # test.addMinimize('error4', target.last(), output4)
test.neuralizeModel()
# Dataset with only one sample
- dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1,3]}
- self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]],
- 'out2': [-96.0, -194.0, -179.0, -125.0],
- 'out3': [-96.0, -206.0, -235.0, -239.0],
- 'out4': [-96.0, -194.0, -179.0, -125.0]
- }, test(dataset))
- test.loadData(name='dataset', source=dataset)
+ dataset = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2]],
+ "target": [3, 4, 5, 1, 3],
+ }
+ self.assertEqual(
+ {
+ "out1": [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]],
+ "out2": [-96.0, -194.0, -179.0, -125.0],
+ "out3": [-96.0, -206.0, -235.0, -239.0],
+ "out4": [-96.0, -194.0, -179.0, -125.0],
+ },
+ test(dataset),
+ )
+ test.loadData(name="dataset", source=dataset)
# TODO add and error
# dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1]}
# test.loadData(name='dataset2', source=dataset)
- self.assertListEqual([[-1], [-5]], test.parameters['W'])
- self.assertEqual([1 ], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[6143], [5627]], test.parameters['W'])
- self.assertEqual([2305], test.parameters['b'])
- self.assertListEqual([[-3836], [-3323]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertEqual([1], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[6143], [5627]], test.parameters["W"])
+ self.assertEqual([2305], test.parameters["b"])
+ self.assertListEqual([[-3836], [-3323]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10)
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1)
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=1,
+ )
# Data set with more samples
test.neuralizeModel(clear_model=True)
- dataset2 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0]}
- test.loadData(name='dataset2', source=dataset2)
+ dataset2 = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]],
+ "target": [3, 4, 5, 1, 3, 0, 1, 0],
+ }
+ test.loadData(name="dataset2", source=dataset2)
self.maxDiff = None
- self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0], [-13.0, -30.0], [-30.0, -28.0], [-28.0, 1.0]],
- 'out2': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
- 'out3': [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0],
- 'out4': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0]
- }, test(dataset2))
+ self.assertEqual(
+ {
+ "out1": [
+ [-4.0, -16.0],
+ [-16.0, -26.0],
+ [-26.0, -15.0],
+ [-15.0, -13.0],
+ [-13.0, -30.0],
+ [-30.0, -28.0],
+ [-28.0, 1.0],
+ ],
+ "out2": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
+ "out3": [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0],
+ "out4": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
+ },
+ test(dataset2),
+ )
# Use a train_batch_size of 4
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) # TODO add this test
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4)
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Use a small batch but with a prediction sample of 3 = to 4 samples
test.neuralizeModel(clear_model=True)
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=4,
+ )
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Different minimize
- test.removeMinimize('error2')
- test.addMinimize('error3', target.last(), output3)
+ test.removeMinimize("error2")
+ test.addMinimize("error3", target.last(), output3)
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[-1], [-5]], test.parameters['W'])
- self.assertEqual([1], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertEqual([1], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Use a train_batch_size of 4
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertEqual([3142], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertEqual([3142], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4)
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertEqual([3142], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertEqual([3142], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, shuffle_data=False, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]], test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_2']['XY']['in1'])
- self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]], test.internals['inout_0_3']['XY']['in1'])
- self.assertListEqual([[[3.0]]], test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]]], test.internals['inout_0_1']['XY']['target'])
- self.assertListEqual([[[1.0]]], test.internals['inout_0_2']['XY']['target'])
- self.assertListEqual([[[0.0]]], test.internals['inout_0_3']['XY']['target'])
- self.assertDictEqual({'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, test.internals['inout_0_0']['out'])
- self.assertDictEqual({'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[4*-1+2*-5+1.0], [6*-1+5*-5+1.0]]],
- 'out2': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]],
- 'out3': [[[(-15)*3.0+(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]],
- 'out4': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]]}, test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[6*-1+5*-5+1.0], [4*-1+5*-5+1.0]]],
- 'out2': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]],
- 'out3': [[[(-15)*2.0+(4*-1+2*-5+1.0)*3.0+(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]],
- 'out4': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]]}, test.internals['inout_0_2']['out'])
- self.assertDictEqual({'out1': [[[4*-1+5*-5+1.0], [0*-1+0*-5+1.0]]],
- 'out2': [[[(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]],
- 'out3': [[[(-15)*1.0+(4*-1+2*-5+1.0)*2.0+(6*-1+5*-5+1.0)*3.0+(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]],
- 'out4': [[[(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]]}, test.internals['inout_0_3']['out'])
- self.assertDictEqual({'inout': [[[0.0],[0.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state'])
- self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state'])
- self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state'])
- self.assertDictEqual({'inout': [[[-13.0], [-30.0], [-28.0], [1.0], [np.inf]]]}, test.internals['inout_0_3']['state'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_0_3"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_3"]["XY"]["target"])
+ self.assertDictEqual(
+ {
+ "out1": [[[-15.0], [-13.0]]],
+ "out2": [[[-125.0]]],
+ "out3": [[[-125.0]]],
+ "out4": [[[-125.0]]],
+ },
+ test.internals["inout_0_0"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[-13.0], [-30.0]]],
+ "out2": [[[-202.0]]],
+ "out3": [[[-247.0]]],
+ "out4": [[[-202.0]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
+ "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ "out3": [
+ [
+ [
+ (-15) * 3.0
+ + (4 * -1 + 2 * -5 + 1.0) * 4
+ + (6 * -1 + 5 * -5 + 1.0) * 5
+ ]
+ ]
+ ],
+ "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
+ "out2": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 2.0
+ + (4 * -1 + 2 * -5 + 1.0) * 3.0
+ + (6 * -1 + 5 * -5 + 1.0) * 4.0
+ + (4 * -1 + 5 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_2"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]],
+ "out2": [
+ [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 1.0
+ + (4 * -1 + 2 * -5 + 1.0) * 2.0
+ + (6 * -1 + 5 * -5 + 1.0) * 3.0
+ + (4 * -1 + 5 * -5 + 1.0) * 4.0
+ + (0 * -1 + 0 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_3"]["out"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]},
+ test.internals["inout_0_0"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]},
+ test.internals["inout_0_1"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]},
+ test.internals["inout_0_2"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[-13.0], [-30.0], [-28.0], [1.0], [np.inf]]]},
+ test.internals["inout_0_3"]["state"],
+ )
# Replace instead of roll
# self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [0.0]]]}, test.internals['inout_0_0']['state'])
# self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [0.0]]]}, test.internals['inout_0_1']['state'])
# self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [0.0]]]}, test.internals['inout_0_2']['state'])
# self.assertDictEqual({'inout': [[[-13.0], [-30.0], [-28.0], [1.0], [-15.0]]]}, test.internals['inout_0_3']['state'])
- self.assertListEqual([[22273.5], [20993.0]], test.parameters['W'])
- self.assertEqual([6154.0], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]], test.parameters['ab'])
+ self.assertListEqual([[22273.5], [20993.0]], test.parameters["W"])
+ self.assertEqual([6154.0], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual(
+ [[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]],
+ test.parameters["ab"],
+ )
with self.assertRaises(KeyError):
- test.internals['inout_1_0']
+ test.internals["inout_1_0"]
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, train_batch_size=1,
- prediction_samples=2)
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]], test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_2']['XY']['in1'])
- self.assertListEqual([[[3.0]]], test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]]], test.internals['inout_0_1']['XY']['target'])
- self.assertListEqual([[[1.0]]], test.internals['inout_0_2']['XY']['target'])
- self.assertDictEqual({'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, test.internals['inout_0_0']['out'])
- self.assertDictEqual({'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[4*-1+2*-5+1.0], [6*-1+5*-5+1.0]]],
- 'out2': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]],
- 'out3': [[[(-15)*3.0+(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]],
- 'out4': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]]}, test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[6*-1+5*-5+1.0], [4*-1+5*-5+1.0]]],
- 'out2': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]],
- 'out3': [[[(-15)*2.0+(4*-1+2*-5+1.0)*3.0+(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]],
- 'out4': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]]}, test.internals['inout_0_2']['out'])
- self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state'])
- self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state'])
- self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=2,
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"])
+ self.assertDictEqual(
+ {
+ "out1": [[[-15.0], [-13.0]]],
+ "out2": [[[-125.0]]],
+ "out3": [[[-125.0]]],
+ "out4": [[[-125.0]]],
+ },
+ test.internals["inout_0_0"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[-13.0], [-30.0]]],
+ "out2": [[[-202.0]]],
+ "out3": [[[-247.0]]],
+ "out4": [[[-202.0]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
+ "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ "out3": [
+ [
+ [
+ (-15) * 3.0
+ + (4 * -1 + 2 * -5 + 1.0) * 4
+ + (6 * -1 + 5 * -5 + 1.0) * 5
+ ]
+ ]
+ ],
+ "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
+ "out2": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 2.0
+ + (4 * -1 + 2 * -5 + 1.0) * 3.0
+ + (6 * -1 + 5 * -5 + 1.0) * 4.0
+ + (4 * -1 + 5 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_2"]["out"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]},
+ test.internals["inout_0_0"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]},
+ test.internals["inout_0_1"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]},
+ test.internals["inout_0_2"]["state"],
+ )
# Replace instead of rolling
# self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [0.0]]]}, test.internals['inout_0_0']['state'])
# self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [0.0]]]}, test.internals['inout_0_1']['state'])
# self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [0.0]]]}, test.internals['inout_0_2']['state'])
- W = test.internals['inout_1_0']['param']['W']
- b = test.internals['inout_1_0']['param']['b']
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_1_0']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_1_1']['XY']['in1'])
- self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]], test.internals['inout_1_2']['XY']['in1'])
- self.assertListEqual([[[0.0]]], test.internals['inout_1_0']['XY']['target'])
- self.assertListEqual([[[1.0]]], test.internals['inout_1_1']['XY']['target'])
- self.assertListEqual([[[0.0]]], test.internals['inout_1_2']['XY']['target'])
- self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [np.inf]]]}, test.internals['inout_1_0']['state'])
- self.assertAlmostEqual({'inout': [[[0.0], [W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [W[0][0]*4.0+W[1][0]*5.0+b[0]], [np.inf]]]}, test.internals['inout_1_1']['state'])
- self.assertAlmostEqual({'inout': [[[W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [W[0][0]*4.0+W[1][0]*5.0+b[0]], [W[0][0]*0.0+W[1][0]*0.0+b[0]], [np.inf]]]}, test.internals['inout_1_2']['state'])
+ W = test.internals["inout_1_0"]["param"]["W"]
+ b = test.internals["inout_1_0"]["param"]["b"]
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_1_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_1_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_1_2"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[0.0]]], test.internals["inout_1_0"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_1_1"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_1_2"]["XY"]["target"])
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [0.0],
+ [0.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_0"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [0.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_1"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_2"]["state"],
+ )
# Replace instead of rolling
# self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0][0] * 4.0 + W[0][1][0] * 2.0 + b[0][0]],
# [W[0][0][0] * 6.0 + W[0][1][0] * 5.0 + b[0][0]], [0.0]]]},
@@ -722,43 +1374,111 @@ def test_training_values_fir_connect_linear_more_window(self):
# test.internals['inout_1_2']['state'])
with self.assertRaises(KeyError):
- test.internals['inout_2_0']
+ test.internals["inout_2_0"]
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, train_batch_size=2, prediction_samples=1)
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[3.0]],[[0.0]]], test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]],[[1.0]]], test.internals['inout_0_1']['XY']['target'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=1,
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]],
+ test.internals["inout_0_0"]["XY"]["in1"],
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]],
+ test.internals["inout_0_1"]["XY"]["in1"],
+ )
+ self.assertListEqual(
+ [[[3.0]], [[0.0]]], test.internals["inout_0_0"]["XY"]["target"]
+ )
+ self.assertListEqual(
+ [[[0.0]], [[1.0]]], test.internals["inout_0_1"]["XY"]["target"]
+ )
with self.assertRaises(KeyError):
- test.internals['inout_1_0']
+ test.internals["inout_1_0"]
test.neuralizeModel(clear_model=True)
- dataset3 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0], 'inout':[9,8,7,6,5,4,3,2]}
- test.loadData(name='dataset3', source=dataset3)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1,
- train_batch_size=1,
- prediction_samples=2)
- self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state'])
- self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state'])
- self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state'])
+ dataset3 = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]],
+ "target": [3, 4, 5, 1, 3, 0, 1, 0],
+ "inout": [9, 8, 7, 6, 5, 4, 3, 2],
+ }
+ test.loadData(name="dataset3", source=dataset3)
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=2,
+ )
+ self.assertDictEqual(
+ {"inout": [[[8.0], [7.0], [-15.0], [-13.0], [np.inf]]]},
+ test.internals["inout_0_0"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[7.0], [-15.0], [-13.0], [-30.0], [np.inf]]]},
+ test.internals["inout_0_1"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]},
+ test.internals["inout_0_2"]["state"],
+ )
# Replace insead of rolling
# self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [9.0]]]}, test.internals['inout_0_0']['state'])
# self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [8.0]]]}, test.internals['inout_0_1']['state'])
# self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [7.0]]]}, test.internals['inout_0_2']['state'])
- W = test.internals['inout_1_0']['param']['W']
- b = test.internals['inout_1_0']['param']['b']
- self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [np.inf]]]},
- test.internals['inout_1_0']['state'])
- self.assertAlmostEqual({'inout': [[[6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [np.inf]]]},
- test.internals['inout_1_1']['state'])
- self.assertAlmostEqual({'inout': [
- [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [np.inf]]]},
- test.internals['inout_1_2']['state'])
+ W = test.internals["inout_1_0"]["param"]["W"]
+ b = test.internals["inout_1_0"]["param"]["b"]
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [7.0],
+ [6.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_0"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [6.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_1"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]],
+ [np.inf],
+ ]
+ ]
+ },
+ test.internals["inout_1_2"]["state"],
+ )
# replace insead of rolling
# self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0][0] * 4.0 + W[0][1][0] * 2.0 + b[0][0]],
# [W[0][0][0] * 6.0 + W[0][1][0] * 5.0 + b[0][0]], [8.0]]]},
@@ -774,184 +1494,411 @@ def test_training_values_fir_connect_linear_more_window(self):
def test_training_values_fir_connect_train_linear_more_window(self):
NeuObj.clearNames()
- input1 = Input('in1', dimensions=2)
- W = Parameter('W', values=[[-1], [-5]])
- b = Parameter('b', values=[1])
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
lin_out = Linear(W=W, b=b)(input1.sw(2))
- output1 = Output('out1', lin_out)
+ output1 = Output("out1", lin_out)
- inout = Input('inout')
- a = Parameter('a', sw=2, values=[[4], [5]])
- a_big = Parameter('ab', sw=5, values=[[1], [2], [3], [4], [5]])
- output2 = Output('out2', Fir(W=a)(inout.sw(2)))
- output3 = Output('out3', Fir(W=a_big)(inout.sw(5)))
- output4 = Output('out4', Fir(W=a)(lin_out))
+ inout = Input("inout")
+ a = Parameter("a", sw=2, values=[[4], [5]])
+ a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]])
+ output2 = Output("out2", Fir(W=a)(inout.sw(2)))
+ output3 = Output("out3", Fir(W=a_big)(inout.sw(5)))
+ output4 = Output("out4", Fir(W=a)(lin_out))
- target = Input('target')
+ target = Input("target")
test = Modely(visualizer=None, seed=42, log_internal=True)
- test.addModel('model', [output1, output2, output3, output4])
- test.addMinimize('error2', target.last(), output2)
+ test.addModel("model", [output1, output2, output3, output4])
+ test.addMinimize("error2", target.last(), output2)
test.neuralizeModel()
# Dataset with only one sample
- dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1,3]}
- self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]],
- 'out2': [-96.0, -194.0, -179.0, -125.0],
- 'out3': [-96.0, -206.0, -235.0, -239.0],
- 'out4': [-96.0, -194.0, -179.0, -125.0]
- }, test(dataset, connect={'inout':'out1'}))
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[-1], [-5]], test.parameters['W'])
- self.assertListEqual([1], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'})
- self.assertListEqual([[6143], [5627]], test.parameters['W'])
- self.assertListEqual([2305], test.parameters['b'])
- self.assertListEqual([[-3836], [-3323]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ dataset = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2]],
+ "target": [3, 4, 5, 1, 3],
+ }
+ self.assertEqual(
+ {
+ "out1": [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]],
+ "out2": [-96.0, -194.0, -179.0, -125.0],
+ "out3": [-96.0, -206.0, -235.0, -239.0],
+ "out4": [-96.0, -194.0, -179.0, -125.0],
+ },
+ test(dataset, connect={"inout": "out1"}),
+ )
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[6143], [5627]], test.parameters["W"])
+ self.assertListEqual([2305], test.parameters["b"])
+ self.assertListEqual([[-3836], [-3323]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
with self.assertRaises(KeyError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10)
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, connect={'inout':'out1'})
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ connect={"inout": "out1"},
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout':'out1'})
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=1,
+ connect={"inout": "out1"},
+ )
# Data set with more samples
test.neuralizeModel(clear_model=True)
- dataset2 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0]}
- test.loadData(name='dataset2', source=dataset2)
+ dataset2 = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]],
+ "target": [3, 4, 5, 1, 3, 0, 1, 0],
+ }
+ test.loadData(name="dataset2", source=dataset2)
self.maxDiff = None
- self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0], [-13.0, -30.0], [-30.0, -28.0], [-28.0, 1.0]],
- 'out2': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
- 'out3': [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0],
- 'out4': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0]
- }, test(dataset2, connect={'inout':'out1'}))
+ self.assertEqual(
+ {
+ "out1": [
+ [-4.0, -16.0],
+ [-16.0, -26.0],
+ [-26.0, -15.0],
+ [-15.0, -13.0],
+ [-13.0, -30.0],
+ [-30.0, -28.0],
+ [-28.0, 1.0],
+ ],
+ "out2": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
+ "out3": [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0],
+ "out4": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0],
+ },
+ test(dataset2, connect={"inout": "out1"}),
+ )
# Use a train_batch_size of 4
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout':'out1'}) TODO add this test
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'})
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertListEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertListEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4, connect={'inout':'out1'})
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertListEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertListEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Use a small batch but with a prediction sample of 3 = to 4 samples
test.neuralizeModel(clear_model=True)
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4, connect={'inout':'out1'})
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout':'out1'})
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertListEqual([3142], test.parameters['b'])
- self.assertListEqual([[-7682], [-7457.5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=4,
+ connect={"inout": "out1"},
+ )
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertListEqual([3142], test.parameters["b"])
+ self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Different minimize
- test.removeMinimize('error2')
- test.addMinimize('error3', target.last(), output3)
+ test.removeMinimize("error2")
+ test.addMinimize("error3", target.last(), output3)
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[-1], [-5]], test.parameters['W'])
- self.assertListEqual([1], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab'])
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"])
# Use a train_batch_size of 4
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'})
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertListEqual([3142], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertListEqual([3142], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4, connect={'inout':'out1'})
- self.assertListEqual([[12779], [11678.5]], test.parameters['W'])
- self.assertListEqual([3142], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual([[12779], [11678.5]], test.parameters["W"])
+ self.assertListEqual([3142], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout':'out1'})
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]],test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_2']['XY']['in1'])
- self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]],test.internals['inout_0_3']['XY']['in1'])
- self.assertListEqual([[[3.0]]],test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]]],test.internals['inout_0_1']['XY']['target'])
- self.assertListEqual([[[1.0]]],test.internals['inout_0_2']['XY']['target'])
- self.assertListEqual([[[0.0]]],test.internals['inout_0_3']['XY']['target'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_0_3"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_3"]["XY"]["target"])
+ self.assertDictEqual(
+ {
+ "out1": [[[-15.0], [-13.0]]],
+ "out2": [[[-125.0]]],
+ "out3": [[[-125.0]]],
+ "out4": [[[-125.0]]],
+ },
+ test.internals["inout_0_0"]["out"],
+ )
self.assertDictEqual(
- {'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]},
- test.internals['inout_0_0']['out'])
+ {
+ "out1": [[[-13.0], [-30.0]]],
+ "out2": [[[-202.0]]],
+ "out3": [[[-247.0]]],
+ "out4": [[[-202.0]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
self.assertDictEqual(
- {'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]},
- test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
- 'out2': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
- 'out3': [[[(-15) * 3.0 + (4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
- 'out4': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]]},
- test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
- 'out2': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]],
- 'out3': [[[(-15) * 2.0 + (4 * -1 + 2 * -5 + 1.0) * 3.0 + (6 * -1 + 5 * -5 + 1.0) * 4.0 + (
- 4 * -1 + 5 * -5 + 1.0) * 5.0]]],
- 'out4': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]]},
- test.internals['inout_0_2']['out'])
- self.assertDictEqual({'out1': [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]],
- 'out2': [[[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]],
- 'out3': [[[(-15) * 1.0 + (4 * -1 + 2 * -5 + 1.0) * 2.0 + (6 * -1 + 5 * -5 + 1.0) * 3.0 + (
- 4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]],
- 'out4': [[[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]]},
- test.internals['inout_0_3']['out'])
- self.assertListEqual([[22273.5], [20993.0]], test.parameters['W'])
- self.assertListEqual([6154.0], test.parameters['b'])
- self.assertListEqual([[4], [5]], test.parameters['a'])
- self.assertListEqual([[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]],
- test.parameters['ab'])
+ {
+ "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
+ "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ "out3": [
+ [
+ [
+ (-15) * 3.0
+ + (4 * -1 + 2 * -5 + 1.0) * 4
+ + (6 * -1 + 5 * -5 + 1.0) * 5
+ ]
+ ]
+ ],
+ "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
+ "out2": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 2.0
+ + (4 * -1 + 2 * -5 + 1.0) * 3.0
+ + (6 * -1 + 5 * -5 + 1.0) * 4.0
+ + (4 * -1 + 5 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_2"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]],
+ "out2": [
+ [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 1.0
+ + (4 * -1 + 2 * -5 + 1.0) * 2.0
+ + (6 * -1 + 5 * -5 + 1.0) * 3.0
+ + (4 * -1 + 5 * -5 + 1.0) * 4.0
+ + (0 * -1 + 0 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_3"]["out"],
+ )
+ self.assertListEqual([[22273.5], [20993.0]], test.parameters["W"])
+ self.assertListEqual([6154.0], test.parameters["b"])
+ self.assertListEqual([[4], [5]], test.parameters["a"])
+ self.assertListEqual(
+ [[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]],
+ test.parameters["ab"],
+ )
with self.assertRaises(KeyError):
- test.internals['inout_1_0']
+ test.internals["inout_1_0"]
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1,
- train_batch_size=1,
- prediction_samples=2, connect={'inout':'out1'})
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]],test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_2']['XY']['in1'])
- self.assertListEqual([[[3.0]]],test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]]],test.internals['inout_0_1']['XY']['target'])
- self.assertListEqual([[[1.0]]],test.internals['inout_0_2']['XY']['target'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=2,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"])
+ self.assertDictEqual(
+ {
+ "out1": [[[-15.0], [-13.0]]],
+ "out2": [[[-125.0]]],
+ "out3": [[[-125.0]]],
+ "out4": [[[-125.0]]],
+ },
+ test.internals["inout_0_0"]["out"],
+ )
self.assertDictEqual(
- {'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]},
- test.internals['inout_0_0']['out'])
+ {
+ "out1": [[[-13.0], [-30.0]]],
+ "out2": [[[-202.0]]],
+ "out3": [[[-247.0]]],
+ "out4": [[[-202.0]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
self.assertDictEqual(
- {'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]},
- test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
- 'out2': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
- 'out3': [[[(-15) * 3.0 + (4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
- 'out4': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]]},
- test.internals['inout_0_1']['out'])
- self.assertDictEqual({'out1': [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
- 'out2': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]],
- 'out3': [[[(-15) * 2.0 + (4 * -1 + 2 * -5 + 1.0) * 3.0 + (6 * -1 + 5 * -5 + 1.0) * 4.0 + (
- 4 * -1 + 5 * -5 + 1.0) * 5.0]]],
- 'out4': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]]},
- test.internals['inout_0_2']['out'])
- W = test.internals['inout_1_0']['param']['W']
- b = test.internals['inout_1_0']['param']['b']
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_1_0']['XY']['in1'])
- self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_1_1']['XY']['in1'])
- self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]],test.internals['inout_1_2']['XY']['in1'])
- self.assertListEqual([[[0.0]]],test.internals['inout_1_0']['XY']['target'])
- self.assertListEqual([[[1.0]]],test.internals['inout_1_1']['XY']['target'])
- self.assertListEqual([[[0.0]]],test.internals['inout_1_2']['XY']['target'])
+ {
+ "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]],
+ "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ "out3": [
+ [
+ [
+ (-15) * 3.0
+ + (4 * -1 + 2 * -5 + 1.0) * 4
+ + (6 * -1 + 5 * -5 + 1.0) * 5
+ ]
+ ]
+ ],
+ "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]],
+ },
+ test.internals["inout_0_1"]["out"],
+ )
+ self.assertDictEqual(
+ {
+ "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]],
+ "out2": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ "out3": [
+ [
+ [
+ (-15) * 2.0
+ + (4 * -1 + 2 * -5 + 1.0) * 3.0
+ + (6 * -1 + 5 * -5 + 1.0) * 4.0
+ + (4 * -1 + 5 * -5 + 1.0) * 5.0
+ ]
+ ]
+ ],
+ "out4": [
+ [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]
+ ],
+ },
+ test.internals["inout_0_2"]["out"],
+ )
+ W = test.internals["inout_1_0"]["param"]["W"]
+ b = test.internals["inout_1_0"]["param"]["b"]
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_1_0"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_1_1"]["XY"]["in1"]
+ )
+ self.assertListEqual(
+ [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_1_2"]["XY"]["in1"]
+ )
+ self.assertListEqual([[[0.0]]], test.internals["inout_1_0"]["XY"]["target"])
+ self.assertListEqual([[[1.0]]], test.internals["inout_1_1"]["XY"]["target"])
+ self.assertListEqual([[[0.0]]], test.internals["inout_1_2"]["XY"]["target"])
# self.assertAlmostEqual({'inout': [[[0.0], [0.0], [0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
# [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]]]]},
# test.internals['inout_1_0']['state'])
@@ -963,48 +1910,117 @@ def test_training_values_fir_connect_train_linear_more_window(self):
# [[0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
# [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]]]]},
# test.internals['inout_1_2']['state'])
- self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_0']['state'])
- self.assertAlmostEqual({'inout': [[[0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_1']['state'])
- self.assertAlmostEqual({'inout': [
- [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_2']['state'])
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [0.0],
+ [0.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_0"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [0.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_1"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_2"]["state"],
+ )
with self.assertRaises(KeyError):
- test.internals['inout_2_0']
+ test.internals["inout_2_0"]
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1,
- train_batch_size=2, prediction_samples=1, connect={'inout':'out1'})
- self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_0']['XY']['in1'])
- self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_1']['XY']['in1'])
- self.assertListEqual([[[3.0]], [[0.0]]],test.internals['inout_0_0']['XY']['target'])
- self.assertListEqual([[[0.0]], [[1.0]]],test.internals['inout_0_1']['XY']['target'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=2,
+ prediction_samples=1,
+ connect={"inout": "out1"},
+ )
+ self.assertListEqual(
+ [[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]],
+ test.internals["inout_0_0"]["XY"]["in1"],
+ )
+ self.assertListEqual(
+ [[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]],
+ test.internals["inout_0_1"]["XY"]["in1"],
+ )
+ self.assertListEqual(
+ [[[3.0]], [[0.0]]], test.internals["inout_0_0"]["XY"]["target"]
+ )
+ self.assertListEqual(
+ [[[0.0]], [[1.0]]], test.internals["inout_0_1"]["XY"]["target"]
+ )
with self.assertRaises(KeyError):
- test.internals['inout_1_0']
+ test.internals["inout_1_0"]
test.neuralizeModel(clear_model=True)
- dataset3 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0], 'inout':[9,8,7,6,5,4,3,2]}
- test.loadData(name='dataset3', source=dataset3)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1,
- train_batch_size=1,
- prediction_samples=2, connect={'inout':'out1'})
+ dataset3 = {
+ "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]],
+ "target": [3, 4, 5, 1, 3, 0, 1, 0],
+ "inout": [9, 8, 7, 6, 5, 4, 3, 2],
+ }
+ test.loadData(name="dataset3", source=dataset3)
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ lr=0.01,
+ shuffle_data=False,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=2,
+ connect={"inout": "out1"},
+ )
# self.assertDictEqual({'inout': [[[8.0], [7.0], [6.0], [-15.0], [-13.0]]]}, test.internals['inout_0_0']['state'])
# self.assertDictEqual({'inout': [[[7.0], [6.0], [-15.0], [-13.0], [-30.0]]]}, test.internals['inout_0_1']['state'])
# self.assertDictEqual({'inout': [[[6.0], [-15.0], [-13.0], [-30.0], [-28.0]]]}, test.internals['inout_0_2']['state'])
- self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [float('inf')]]]}, test.internals['inout_0_0']['state'])
- self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [float('inf')]]]}, test.internals['inout_0_1']['state'])
- self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [float('inf')]]]}, test.internals['inout_0_2']['state'])
+ self.assertDictEqual(
+ {"inout": [[[8.0], [7.0], [-15.0], [-13.0], [float("inf")]]]},
+ test.internals["inout_0_0"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[7.0], [-15.0], [-13.0], [-30.0], [float("inf")]]]},
+ test.internals["inout_0_1"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [float("inf")]]]},
+ test.internals["inout_0_2"]["state"],
+ )
# Replace instead of rolling
# self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [9.0]]]}, test.internals['inout_0_0']['connect'])
# self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [8.0]]]}, test.internals['inout_0_1']['connect'])
# self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [7.0]]]}, test.internals['inout_0_2']['connect'])
- W = test.internals['inout_1_0']['param']['W']
- b = test.internals['inout_1_0']['param']['b']
+ W = test.internals["inout_1_0"]["param"]["W"]
+ b = test.internals["inout_1_0"]["param"]["b"]
# self.assertAlmostEqual({'inout': [[[7.0], [6.0], [5.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
# [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]]]]},
# test.internals['inout_1_0']['state'])
@@ -1017,249 +2033,396 @@ def test_training_values_fir_connect_train_linear_more_window(self):
# [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]]]]},
# test.internals['inout_1_2']['state'])
# Replace insead of rolling
- self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_0']['state'])
- self.assertAlmostEqual({'inout': [[[6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
- [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_1']['state'])
- self.assertAlmostEqual({'inout': [
- [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
- [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [float('inf')]]]},
- test.internals['inout_1_2']['state'])
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [7.0],
+ [6.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_0"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [6.0],
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_1"]["state"],
+ )
+ self.assertAlmostEqual(
+ {
+ "inout": [
+ [
+ [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]],
+ [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]],
+ [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]],
+ [float("inf")],
+ ]
+ ]
+ },
+ test.internals["inout_1_2"]["state"],
+ )
def test_training_values_fir_and_liner_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target_out1 = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
+ input1 = Input("in1")
+ target_out1 = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
relation1 = Fir(W=a)(input1.last())
relation1.closedLoop(input1)
- output1 = Output('out1', relation1)
+ output1 = Output("out1", relation1)
- input2 = Input('in2')
- target_out2 = Input('target2')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- relation2 = Linear(W=W,b=b)(input2.last())
+ input2 = Input("in2")
+ target_out2 = Input("target2")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ relation2 = Linear(W=W, b=b)(input2.last())
relation2.closedLoop(input2)
- output2 = Output('out2', relation2)
+ output2 = Output("out2", relation2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', target_out1.last(), output1)
- test.addMinimize('error2', target_out2.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", target_out1.last(), output1)
+ test.addMinimize("error2", target_out2.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test())
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1.0],'in2': [1.0]}))
- self.assertEqual({'out1': [1.0], 'out2': [3.0]}, test())
+ self.assertEqual({"out1": [0.0], "out2": [1.0]}, test())
+ self.assertEqual(
+ {"out1": [1.0], "out2": [2.0]}, test({"in1": [1.0], "in2": [1.0]})
+ )
+ self.assertEqual({"out1": [1.0], "out2": [3.0]}, test())
test.resetStates()
- self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}, test(prediction_samples=5, num_of_samples=6))
-
- dataset = {'in1': [1], 'in2': [1.0], 'target1': [3], 'target2': [3]}
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertEqual(
+ {
+ "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ },
+ test(prediction_samples=5, num_of_samples=6),
+ )
+
+ dataset = {"in1": [1], "in2": [1.0], "target1": [3], "target2": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
-
- dataset = {'in1': [1.0,1.0], 'in2': [1.0,1.0], 'target1': [3.0,3.0], 'target2': [3.0,3.0]}
- test.loadData(name='dataset2', source=dataset)
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2)
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+
+ dataset = {
+ "in1": [1.0, 1.0],
+ "in2": [1.0, 1.0],
+ "target1": [3.0, 3.0],
+ "target2": [3.0, 3.0],
+ }
+ test.loadData(name="dataset2", source=dataset)
test.neuralizeModel(clear_model=True)
# the out is 3.0 due the mean of the error is not the same of two epochs
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertListEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertListEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertListEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=2
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertListEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
def test_training_values_fir_and_liner_train_closed_loop(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target_out1 = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target_out1 = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1", Fir(W=a)(input1.last()))
- input2 = Input('in2')
- target_out2 = Input('target2')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=1)
- output2 = Output('out2', Linear(W=W,b=b)(input2.last()))
+ input2 = Input("in2")
+ target_out2 = Input("target2")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=1)
+ output2 = Output("out2", Linear(W=W, b=b)(input2.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', target_out1.last(), output1)
- test.addMinimize('error2', target_out2.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", target_out1.last(), output1)
+ test.addMinimize("error2", target_out2.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test(closed_loop={'in1':'out1','in2':'out2'}))
- self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1.0],'in2': [1.0]},closed_loop={'in1':'out1','in2':'out2'}))
+ self.assertEqual(
+ {"out1": [0.0], "out2": [1.0]},
+ test(closed_loop={"in1": "out1", "in2": "out2"}),
+ )
+ self.assertEqual(
+ {"out1": [1.0], "out2": [2.0]},
+ test(
+ {"in1": [1.0], "in2": [1.0]}, closed_loop={"in1": "out1", "in2": "out2"}
+ ),
+ )
# # The memory is reset for each call
- self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test(closed_loop={'in1':'out1', 'in2':'out2'}))
-
- dataset = {'in1': [1], 'in2': [1.0], 'target1': [3], 'target2': [3]}
- test.loadData(name='dataset', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertEqual(
+ {"out1": [0.0], "out2": [1.0]},
+ test(closed_loop={"in1": "out1", "in2": "out2"}),
+ )
+
+ dataset = {"in1": [1], "in2": [1.0], "target1": [3], "target2": [3]}
+ test.loadData(name="dataset", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=1,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1)
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
+ test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1)
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
-
- dataset = {'in1': [1.0,1.0], 'in2': [1.0,1.0], 'target1': [3.0,3.0], 'target2': [3.0,3.0]}
- test.loadData(name='dataset2', source=dataset)
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ optimizer="SGD",
+ splits=[100, 0, 0],
+ lr=1,
+ num_of_epochs=2,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+
+ dataset = {
+ "in1": [1.0, 1.0],
+ "in2": [1.0, 1.0],
+ "target1": [3.0, 3.0],
+ "target2": [3.0, 3.0],
+ }
+ test.loadData(name="dataset2", source=dataset)
test.neuralizeModel(clear_model=True)
# the out is 3.0 due the mean of the error is not the same of two epochs
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[3.0]], test.parameters['W'])
- self.assertEqual([3.0], test.parameters['b'])
- self.assertListEqual([[5.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[3.0]], test.parameters["W"])
+ self.assertEqual([3.0], test.parameters["b"])
+ self.assertListEqual([[5.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2, closed_loop={'in1':'out1','in2':'out2'})
- self.assertListEqual([[-3.0]], test.parameters['W'])
- self.assertEqual([-3.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=2,
+ closed_loop={"in1": "out1", "in2": "out2"},
+ )
+ self.assertListEqual([[-3.0]], test.parameters["W"])
+ self.assertEqual([-3.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
def test_training_values_fir_and_linear_closed_loop_more_prediction(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target_out1 = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
+ input1 = Input("in1")
+ target_out1 = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
relation1 = Fir(W=a)(input1.last())
relation1.closedLoop(input1)
- output1 = Output('out1',relation1)
+ output1 = Output("out1", relation1)
- input2 = Input('in2')
- target_out2 = Input('target2')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- relation2=Linear(W=W,b=b)(input2.last())
+ input2 = Input("in2")
+ target_out2 = Input("target2")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ relation2 = Linear(W=W, b=b)(input2.last())
relation2.closedLoop(input2)
- output2 = Output('out2', relation2)
+ output2 = Output("out2", relation2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', target_out1.last(), output1)
- test.addMinimize('error2', target_out2.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", target_out1.last(), output1)
+ test.addMinimize("error2", target_out2.last(), output2)
test.neuralizeModel()
- self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]},
- test(prediction_samples=5, num_of_samples=6))
- #self.assertEqual({'out1': [1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0]},
+ self.assertEqual(
+ {
+ "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
+ },
+ test(prediction_samples=5, num_of_samples=6),
+ )
+ # self.assertEqual({'out1': [1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0]},
# test({'in1':[1.0,2.0]},prediction_samples=5))
- self.assertEqual({'out1': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0]},
- test({'in1': [1.0, 2.0]}, prediction_samples=5, num_of_samples=7))
- #self.assertEqual({'out1': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [0.0, -1.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]},
+ self.assertEqual(
+ {
+ "out1": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],
+ "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0],
+ },
+ test({"in1": [1.0, 2.0]}, prediction_samples=5, num_of_samples=7),
+ )
+ # self.assertEqual({'out1': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [0.0, -1.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]},
# test({'in2':[-1.0,-2.0,-3.0]},prediction_samples=5))
- self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0]},
- test({'in2':[-1.0,-2.0,-3.0]}, prediction_samples=5, num_of_samples=8))
-
- dataset = {'in1': [0,2,7,1], 'in2': [-1,0,-3,7], 'target1': [3,4,5,1], 'target2': [-3,-4,-5,-1]}
- test.loadData(name='dataset2', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[-24.5]], test.parameters['W'])
- self.assertListEqual([-9.0], test.parameters['b'])
- self.assertListEqual([[-4.0]], test.parameters['a'])
+ self.assertEqual(
+ {
+ "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ "out2": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0],
+ },
+ test({"in2": [-1.0, -2.0, -3.0]}, prediction_samples=5, num_of_samples=8),
+ )
+
+ dataset = {
+ "in1": [0, 2, 7, 1],
+ "in2": [-1, 0, -3, 7],
+ "target1": [3, 4, 5, 1],
+ "target2": [-3, -4, -5, -1],
+ }
+ test.loadData(name="dataset2", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[-24.5]], test.parameters["W"])
+ self.assertListEqual([-9.0], test.parameters["b"])
+ self.assertListEqual([[-4.0]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1)
- self.assertListEqual([[-24.5]], test.parameters['W'])
- self.assertListEqual([-9.0], test.parameters['b'])
- self.assertListEqual([[-4.0]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1
+ )
+ self.assertListEqual([[-24.5]], test.parameters["W"])
+ self.assertListEqual([-9.0], test.parameters["b"])
+ self.assertListEqual([[-4.0]], test.parameters["a"])
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10)
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
# test.neuralizeModel(clear_model=True)
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1)
# self.assertListEqual([[[1.0]]], test.parameters['W'])
# self.assertListEqual([[1.0]], test.parameters['b'])
# self.assertListEqual([[1.0]], test.parameters['a'])
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([-24.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([-24.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
# test.neuralizeModel(clear_model=True)
# self.assertListEqual([[[1.0]]],test.parameters['W'])
@@ -1272,50 +2435,97 @@ def test_training_values_fir_and_linear_closed_loop_more_prediction(self):
def test_training_values_fir_and_linear_train_closed_loop_more_prediction(self):
NeuObj.clearNames()
- input1 = Input('in1')
- target_out1 = Input('target1')
- a = Parameter('a', sw=1, values=[[1]])
- output1 = Output('out1',Fir(W=a)(input1.last()))
+ input1 = Input("in1")
+ target_out1 = Input("target1")
+ a = Parameter("a", sw=1, values=[[1]])
+ output1 = Output("out1", Fir(W=a)(input1.last()))
- input2 = Input('in2')
- target_out2 = Input('target2')
- W = Parameter('W', values=[[1]])
- b = Parameter('b', values=[1])
- output2 = Output('out2', Linear(W=W,b=b)(input2.last()))
+ input2 = Input("in2")
+ target_out2 = Input("target2")
+ W = Parameter("W", values=[[1]])
+ b = Parameter("b", values=[1])
+ output2 = Output("out2", Linear(W=W, b=b)(input2.last()))
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', target_out1.last(), output1)
- test.addMinimize('error2', target_out2.last(), output2)
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", target_out1.last(), output1)
+ test.addMinimize("error2", target_out2.last(), output2)
test.neuralizeModel()
# self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]},
# test(prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, _num_of_samples=6))
- self.assertEqual({'out1': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0]},
- test({'in1':[1.0, 2.0]}, prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, num_of_samples=7))
- self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0]},
- test({'in2':[-1.0,-2.0,-3.0]}, prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, num_of_samples=8))
-
- dataset = {'in1': [0,2,7,1], 'in2': [-1,0,-3,7], 'target1': [3,4,5,1], 'target2': [-3,-4,-5,-1]}
- test.loadData(name='dataset2', source=dataset)
-
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, closed_loop={'in2':'out2','in1':'out1'})
- self.assertListEqual([[-24.5]], test.parameters['W'])
- self.assertListEqual([-9.0], test.parameters['b'])
- self.assertListEqual([[-4.0]], test.parameters['a'])
+ self.assertEqual(
+ {
+ "out1": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],
+ "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0],
+ },
+ test(
+ {"in1": [1.0, 2.0]},
+ prediction_samples=5,
+ closed_loop={"in2": "out2", "in1": "out1"},
+ num_of_samples=7,
+ ),
+ )
+ self.assertEqual(
+ {
+ "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ "out2": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0],
+ },
+ test(
+ {"in2": [-1.0, -2.0, -3.0]},
+ prediction_samples=5,
+ closed_loop={"in2": "out2", "in1": "out1"},
+ num_of_samples=8,
+ ),
+ )
+
+ dataset = {
+ "in1": [0, 2, 7, 1],
+ "in2": [-1, 0, -3, 7],
+ "target1": [3, 4, 5, 1],
+ "target2": [-3, -4, -5, -1],
+ }
+ test.loadData(name="dataset2", source=dataset)
+
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ closed_loop={"in2": "out2", "in1": "out1"},
+ )
+ self.assertListEqual([[-24.5]], test.parameters["W"])
+ self.assertListEqual([-9.0], test.parameters["b"])
+ self.assertListEqual([[-4.0]], test.parameters["a"])
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, closed_loop={'in2':'out2','in1':'out1'})
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ closed_loop={"in2": "out2", "in1": "out1"},
+ )
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, closed_loop={'in2':'out2','in1':'out1'}) # TODO add this test
test.neuralizeModel(clear_model=True)
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, closed_loop={'in2':'out2','in1':'out1'})
- self.assertListEqual([[1.0]], test.parameters['W'])
- self.assertListEqual([-24.0], test.parameters['b'])
- self.assertListEqual([[1.0]], test.parameters['a'])
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ closed_loop={"in2": "out2", "in1": "out1"},
+ )
+ self.assertListEqual([[1.0]], test.parameters["W"])
+ self.assertListEqual([-24.0], test.parameters["b"])
+ self.assertListEqual([[1.0]], test.parameters["a"])
# test.neuralizeModel(clear_model=True) # TODO add this test
# self.assertListEqual([[[1.0]]],test.parameters['W'])
@@ -1328,96 +2538,242 @@ def test_training_values_fir_and_linear_train_closed_loop_more_prediction(self):
def test_training_values_fir_and_liner_closed_loop_bigger_window(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=[1])
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
relation1 = Linear(W=W, b=b)(input1.sw(2))
- input2 = Input('in2')
- a = Parameter('a', sw=4, values=[[1,3],[2,4],[3,5],[4,6]])
- relation2 = Fir(output_dimension=2,W=a)(input2.sw(4))
+ input2 = Input("in2")
+ a = Parameter("a", sw=4, values=[[1, 3], [2, 4], [3, 5], [4, 6]])
+ relation2 = Fir(output_dimension=2, W=a)(input2.sw(4))
relation2.closedLoop(input1)
relation1.closedLoop(input2)
- output1 = Output('out1', relation1)
- output2 = Output('out2', relation2)
+ output1 = Output("out1", relation1)
+ output2 = Output("out2", relation2)
- target1 = Input('target1')
- target2 = Input('target2', dimensions=2)
+ target1 = Input("target1")
+ target2 = Input("target2", dimensions=2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', output1, target1.sw(2))
- test.addMinimize('error2', output2, target2.last())
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", output1, target1.sw(2))
+ test.addMinimize("error2", output2, target2.last())
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], 'out2': [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2, 2, 2]}))
-
- dataset = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2],
- 'target1': [-11, -17, -12, -20],
- 'target2': [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]]}
- test.loadData(name='dataset', source=dataset)
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
- 'out2': [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]]},
- test(dataset))
-
- self.assertListEqual([[-1],[-5]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a'])
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[83.0],[105.0]], test.parameters['W'])
- self.assertListEqual([6.0], test.parameters['b'])
- self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a'])
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
+ "out2": [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]],
+ },
+ test(
+ {
+ "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]],
+ "in2": [-10, -16, -5, 2, 2, 2],
+ }
+ ),
+ )
+
+ dataset = {
+ "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]],
+ "in2": [-10, -16, -5, 2],
+ "target1": [-11, -17, -12, -20],
+ "target2": [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]],
+ }
+ test.loadData(name="dataset", source=dataset)
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
+ "out2": [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]],
+ },
+ test(dataset),
+ )
+
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[83.0], [105.0]], test.parameters["W"])
+ self.assertListEqual([6.0], test.parameters["b"])
+ self.assertListEqual(
+ [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]],
+ test.parameters["a"],
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10)
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1)
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=1,
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error1':0})
- self.assertListEqual([[-1],[-5]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ minimize_gain={"error1": 0},
+ )
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual(
+ [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]],
+ test.parameters["a"],
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error2':0})
- self.assertListEqual([[83.0],[105.0]], test.parameters['W'])
- self.assertListEqual([6.0], test.parameters['b'])
- self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ minimize_gain={"error2": 0},
+ )
+ self.assertListEqual([[83.0], [105.0]], test.parameters["W"])
+ self.assertListEqual([6.0], test.parameters["b"])
+ self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- dataset2 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5],
- 'target1':[-11,-17,-12,-20,5,1,0],
- 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-33.0, -84.0],[-31.0, -84.0],[0.0, -84.0],[-31.0, 0.0]]}
- test.loadData(name='dataset2', source=dataset2)
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0],[-4.0,1.0],[1.0,-4.0],[-4.0,0.0]],
- 'out2': [[[-49, -107]], [[-8, -40]], [[-4, -10]], [[19, 33]], [[-11, -17]], [[-24, -44]]]},
- test(dataset2))
+ dataset2 = {
+ "in1": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ [4.0, 6.0],
+ [0.0, 1.0],
+ [0.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 0.0],
+ ],
+ "in2": [-10, -16, -5, 2, 3, -3, 5],
+ "target1": [-11, -17, -12, -20, 5, 1, 0],
+ "target2": [
+ [-34.0, -86.0],
+ [-31.0, -90.0],
+ [-32.0, -86.0],
+ [-33.0, -84.0],
+ [-31.0, -84.0],
+ [0.0, -84.0],
+ [-31.0, 0.0],
+ ],
+ }
+ test.loadData(name="dataset2", source=dataset2)
+ self.assertEqual(
+ {
+ "out1": [
+ [-10.0, -16.0],
+ [-16.0, -33.0],
+ [-33.0, -4.0],
+ [-4.0, 1.0],
+ [1.0, -4.0],
+ [-4.0, 0.0],
+ ],
+ "out2": [
+ [[-49, -107]],
+ [[-8, -40]],
+ [[-4, -10]],
+ [[19, 33]],
+ [[-11, -17]],
+ [[-24, -44]],
+ ],
+ },
+ test(dataset2),
+ )
# Use a train_batch_size of 4
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) # TODO Add this test
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0)
- self.assertListEqual([[20.0], [21.0]], test.parameters['W'])
- self.assertListEqual([2.75], test.parameters['b'])
- self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ )
+ self.assertListEqual([[20.0], [21.0]], test.parameters["W"])
+ self.assertListEqual([2.75], test.parameters["b"])
+ self.assertListEqual(
+ [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]],
+ test.parameters["a"],
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4)
- self.assertListEqual([[20.0], [21.0]], test.parameters['W'])
- self.assertListEqual([2.75], test.parameters['b'])
- self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ )
+ self.assertListEqual([[20.0], [21.0]], test.parameters["W"])
+ self.assertListEqual([2.75], test.parameters["b"])
+ self.assertListEqual(
+ [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]],
+ test.parameters["a"],
+ )
# Use a small batch but with a prediction sample of 3 = to 4 samples
- dataset3 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5],
- 'target1':[-11, -17, -30, -2, 582, 1421, -18975],
- 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-48, -106], [-140, -256], [2254, 3341], [7420, 11374]]}
- test.loadData(name='dataset3', source=dataset3)
+ dataset3 = {
+ "in1": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ [4.0, 6.0],
+ [0.0, 1.0],
+ [0.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 0.0],
+ ],
+ "in2": [-10, -16, -5, 2, 3, -3, 5],
+ "target1": [-11, -17, -30, -2, 582, 1421, -18975],
+ "target2": [
+ [-34.0, -86.0],
+ [-31.0, -90.0],
+ [-32.0, -86.0],
+ [-48, -106],
+ [-140, -256],
+ [2254, 3341],
+ [7420, 11374],
+ ],
+ }
+ test.loadData(name="dataset3", source=dataset3)
test.neuralizeModel(clear_model=True)
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4)
- test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3)
- self.assertListEqual([[-3010.5],[-5211.25]], test.parameters['W'])
- self.assertListEqual([199.5], test.parameters['b'])
- self.assertListEqual([[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=4,
+ )
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ )
+ self.assertListEqual([[-3010.5], [-5211.25]], test.parameters["W"])
+ self.assertListEqual([199.5], test.parameters["b"])
+ self.assertListEqual(
+ [[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]],
+ test.parameters["a"],
+ )
# test.neuralizeModel(clear_model=True)
# test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, num_of_epochs=2, train_batch_size=2, prediction_samples=2)
@@ -1427,90 +2783,246 @@ def test_training_values_fir_and_liner_closed_loop_bigger_window(self):
def test_training_values_fir_and_liner_train_closed_loop_bigger_window(self):
NeuObj.clearNames()
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[-1],[-5]])
- b = Parameter('b', values=[1])
- output1 = Output('out1', Linear(W=W,b=b)(input1.sw(2)))
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[-1], [-5]])
+ b = Parameter("b", values=[1])
+ output1 = Output("out1", Linear(W=W, b=b)(input1.sw(2)))
- input2 = Input('in2')
- a = Parameter('a', sw=4, values=[[1,3],[2,4],[3,5],[4,6]])
- output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(4)))
+ input2 = Input("in2")
+ a = Parameter("a", sw=4, values=[[1, 3], [2, 4], [3, 5], [4, 6]])
+ output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(4)))
- target1 = Input('target1')
- target2 = Input('target2', dimensions=2)
+ target1 = Input("target1")
+ target2 = Input("target2", dimensions=2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', output1, target1.sw(2))
- test.addMinimize('error2', output2, target2.last())
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", output1, target1.sw(2))
+ test.addMinimize("error2", output2, target2.last())
test.neuralizeModel()
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], 'out2': [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]]},
- test({'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2, 2, 2]},closed_loop={'in1':'out2','in2':'out1'}))
-
- dataset = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2],
- 'target1': [-11, -17, -12, -20],
- 'target2': [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]]}
- test.loadData(name='dataset', source=dataset)
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
- 'out2': [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]]},
- test(dataset,closed_loop={'in1':'out2','in2':'out1'}))
-
- self.assertListEqual([[-1],[-5]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a'])
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0,closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[83.0],[105.0]], test.parameters['W'])
- self.assertListEqual([6.0], test.parameters['b'])
- self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a'])
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
+ "out2": [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]],
+ },
+ test(
+ {
+ "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]],
+ "in2": [-10, -16, -5, 2, 2, 2],
+ },
+ closed_loop={"in1": "out2", "in2": "out1"},
+ ),
+ )
+
+ dataset = {
+ "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]],
+ "in2": [-10, -16, -5, 2],
+ "target1": [-11, -17, -12, -20],
+ "target2": [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]],
+ }
+ test.loadData(name="dataset", source=dataset)
+ self.assertEqual(
+ {
+ "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]],
+ "out2": [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]],
+ },
+ test(dataset, closed_loop={"in1": "out2", "in2": "out1"}),
+ )
+
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[83.0], [105.0]], test.parameters["W"])
+ self.assertListEqual([6.0], test.parameters["b"])
+ self.assertListEqual(
+ [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]],
+ test.parameters["a"],
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10,closed_loop={'in1':'out2','in2':'out1'})
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=10,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1,closed_loop={'in1':'out2','in2':'out1'})
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=1,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error1':0},closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[-1],[-5]], test.parameters['W'])
- self.assertListEqual([1.0], test.parameters['b'])
- self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ minimize_gain={"error1": 0},
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[-1], [-5]], test.parameters["W"])
+ self.assertListEqual([1.0], test.parameters["b"])
+ self.assertListEqual(
+ [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]],
+ test.parameters["a"],
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error2':0},closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[83.0],[105.0]], test.parameters['W'])
- self.assertListEqual([6.0], test.parameters['b'])
- self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ minimize_gain={"error2": 0},
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[83.0], [105.0]], test.parameters["W"])
+ self.assertListEqual([6.0], test.parameters["b"])
+ self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"])
test.neuralizeModel(clear_model=True)
- dataset2 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5],
- 'target1':[-11,-17,-12,-20,5,1,0],
- 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-33.0, -84.0],[-31.0, -84.0],[0.0, -84.0],[-31.0, 0.0]]}
- test.loadData(name='dataset2', source=dataset2)
- self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0],[-4.0,1.0],[1.0,-4.0],[-4.0,0.0]],
- 'out2': [[[-49, -107]], [[-8, -40]], [[-4, -10]], [[19, 33]], [[-11, -17]], [[-24, -44]]]},
- test(dataset2,closed_loop={'in1':'out2','in2':'out1'}))
+ dataset2 = {
+ "in1": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ [4.0, 6.0],
+ [0.0, 1.0],
+ [0.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 0.0],
+ ],
+ "in2": [-10, -16, -5, 2, 3, -3, 5],
+ "target1": [-11, -17, -12, -20, 5, 1, 0],
+ "target2": [
+ [-34.0, -86.0],
+ [-31.0, -90.0],
+ [-32.0, -86.0],
+ [-33.0, -84.0],
+ [-31.0, -84.0],
+ [0.0, -84.0],
+ [-31.0, 0.0],
+ ],
+ }
+ test.loadData(name="dataset2", source=dataset2)
+ self.assertEqual(
+ {
+ "out1": [
+ [-10.0, -16.0],
+ [-16.0, -33.0],
+ [-33.0, -4.0],
+ [-4.0, 1.0],
+ [1.0, -4.0],
+ [-4.0, 0.0],
+ ],
+ "out2": [
+ [[-49, -107]],
+ [[-8, -40]],
+ [[-4, -10]],
+ [[19, 33]],
+ [[-11, -17]],
+ [[-24, -44]],
+ ],
+ },
+ test(dataset2, closed_loop={"in1": "out2", "in2": "out1"}),
+ )
# Use a train_batch_size of 4
# test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, closed_loop={'in1':'out2','in2':'out1'}) # TODO Add this test
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[20.0], [21.0]], test.parameters['W'])
- self.assertListEqual([2.75], test.parameters['b'])
- self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ prediction_samples=0,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[20.0], [21.0]], test.parameters["W"])
+ self.assertListEqual([2.75], test.parameters["b"])
+ self.assertListEqual(
+ [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]],
+ test.parameters["a"],
+ )
test.neuralizeModel(clear_model=True)
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4,closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[20.0], [21.0]], test.parameters['W'])
- self.assertListEqual([2.75], test.parameters['b'])
- self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=4,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[20.0], [21.0]], test.parameters["W"])
+ self.assertListEqual([2.75], test.parameters["b"])
+ self.assertListEqual(
+ [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]],
+ test.parameters["a"],
+ )
# Use a small batch but with a prediction sample of 3 = to 4 samples
- dataset3 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5],
- 'target1':[-11, -17, -30, -2, 582, 1421, -18975],
- 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-48, -106], [-140, -256], [2254, 3341], [7420, 11374]]}
- test.loadData(name='dataset3', source=dataset3)
+ dataset3 = {
+ "in1": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ [4.0, 6.0],
+ [0.0, 1.0],
+ [0.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 0.0],
+ ],
+ "in2": [-10, -16, -5, 2, 3, -3, 5],
+ "target1": [-11, -17, -30, -2, 582, 1421, -18975],
+ "target2": [
+ [-34.0, -86.0],
+ [-31.0, -90.0],
+ [-32.0, -86.0],
+ [-48, -106],
+ [-140, -256],
+ [2254, 3341],
+ [7420, 11374],
+ ],
+ }
+ test.loadData(name="dataset3", source=dataset3)
test.neuralizeModel(clear_model=True)
with self.assertRaises(ValueError):
- test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4,closed_loop={'in1':'out2','in2':'out1'})
- test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3,closed_loop={'in1':'out2','in2':'out1'})
- self.assertListEqual([[-3010.5],[-5211.25]], test.parameters['W'])
- self.assertListEqual([199.5], test.parameters['b'])
- self.assertListEqual([[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], test.parameters['a'])
+ test.trainModel(
+ train_dataset="dataset2",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=4,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ test.trainModel(
+ train_dataset="dataset3",
+ optimizer="SGD",
+ lr=1,
+ num_of_epochs=1,
+ train_batch_size=1,
+ prediction_samples=3,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
+ self.assertListEqual([[-3010.5], [-5211.25]], test.parameters["W"])
+ self.assertListEqual([199.5], test.parameters["b"])
+ self.assertListEqual(
+ [[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]],
+ test.parameters["a"],
+ )
# test.neuralizeModel(clear_model=True)
# test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=2, train_batch_size=2, prediction_samples=2,closed_loop={'in1':'out2','in2':'out1'})
@@ -1520,105 +3032,230 @@ def test_training_values_fir_and_liner_train_closed_loop_bigger_window(self):
def test_train_compare_state_and_closed_loop(self):
NeuObj.clearNames()
- dataset = {'control': [-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2],
- 'target1': [-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2],
- 'target2': [[-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0],
- [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0]
- ]}
-
- feed = Input('control')
- input1 = Input('in1', dimensions=2)
- W = Parameter('W', values=[[0.1],[0.1]])
- b = Parameter('b', values=[0.1])
- output1 = Output('out1', feed.sw(2)+Linear(W=W, b=b)(input1.sw(2)))
-
- input2 = Input('in2')
- a = Parameter('a', sw=4, values=[[0.1,0.3],[0.2,0.4],[0.3,0.5],[0.4,0.6]])
- output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(4)))
-
- target1 = Input('target1')
- target2 = Input('target2', dimensions=2)
+ dataset = {
+ "control": [
+ -1,
+ -1,
+ -5,
+ 2,
+ -1,
+ -1,
+ -5,
+ 2,
+ -1,
+ -1,
+ -5,
+ 2,
+ -1,
+ -1,
+ -5,
+ 2,
+ -1,
+ -1,
+ -5,
+ 2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ ],
+ "target1": [
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ -1,
+ -1,
+ -1,
+ -2,
+ ],
+ "target2": [
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ [-1.0, -1.0],
+ [-1.0, -2.0],
+ ],
+ }
+
+ feed = Input("control")
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[0.1], [0.1]])
+ b = Parameter("b", values=[0.1])
+ output1 = Output("out1", feed.sw(2) + Linear(W=W, b=b)(input1.sw(2)))
+
+ input2 = Input("in2")
+ a = Parameter(
+ "a", sw=4, values=[[0.1, 0.3], [0.2, 0.4], [0.3, 0.5], [0.4, 0.6]]
+ )
+ output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(4)))
+
+ target1 = Input("target1")
+ target2 = Input("target2", dimensions=2)
test = Modely(visualizer=None, seed=42)
- test.addModel('model', [output1,output2])
- test.addMinimize('error1', output1, target1.sw(2))
- test.addMinimize('error2', output2, target2.last())
+ test.addModel("model", [output1, output2])
+ test.addMinimize("error1", output1, target1.sw(2))
+ test.addMinimize("error2", output2, target2.last())
test.neuralizeModel()
- test.loadData(name='dataset', source=dataset)
- test.trainModel(splits=[60,40,0], optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=10,
- closed_loop={'in1': 'out2', 'in2': 'out1'})
+ test.loadData(name="dataset", source=dataset)
+ test.trainModel(
+ splits=[60, 40, 0],
+ optimizer="SGD",
+ lr=0.001,
+ num_of_epochs=1,
+ prediction_samples=10,
+ closed_loop={"in1": "out2", "in2": "out1"},
+ )
NeuObj.clearNames()
- feed = Input('control')
- input1 = Input('in1',dimensions=2)
- W = Parameter('W', values=[[0.1],[0.1]])
- b = Parameter('b', values=[0.1])
+ feed = Input("control")
+ input1 = Input("in1", dimensions=2)
+ W = Parameter("W", values=[[0.1], [0.1]])
+ b = Parameter("b", values=[0.1])
relation1 = feed.sw(2) + Linear(W=W, b=b)(input1.sw(2))
- input2 = Input('in2')
- a = Parameter('a', sw=4, values=[[0.1,0.3],[0.2,0.4],[0.3,0.5],[0.4,0.6]])
+ input2 = Input("in2")
+ a = Parameter(
+ "a", sw=4, values=[[0.1, 0.3], [0.2, 0.4], [0.3, 0.5], [0.4, 0.6]]
+ )
relation2 = Fir(output_dimension=2, W=a)(input2.sw(4))
relation1.closedLoop(input2)
relation2.closedLoop(input1)
- output1 = Output('out1', relation1)
- output2 = Output('out2', relation2)
+ output1 = Output("out1", relation1)
+ output2 = Output("out2", relation2)
- target1 = Input('target1')
- target2 = Input('target2', dimensions=2)
+ target1 = Input("target1")
+ target2 = Input("target2", dimensions=2)
test2 = Modely(visualizer=None, seed=42)
- test2.addModel('model', [output1, output2])
- test2.addMinimize('error1', output1, target1.sw(2))
- test2.addMinimize('error2', output2, target2.last())
+ test2.addModel("model", [output1, output2])
+ test2.addMinimize("error1", output1, target1.sw(2))
+ test2.addMinimize("error2", output2, target2.last())
test2.neuralizeModel()
- test2.loadData(name='dataset', source=dataset)
- test2.trainModel(splits=[60,40,0], optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=10)
-
- self.assertListEqual(test2.parameters['W'], test.parameters['W'])
- self.assertListEqual(test2.parameters['a'], test.parameters['a'])
- self.assertListEqual(test2.parameters['b'], test.parameters['b'])
- self.assertListEqual(test2._training['error1']['train'] , test._training['error1']['train'])
- self.assertListEqual(test2._training['error1']['val'], test._training['error1']['val'])
- self.assertListEqual(test2._training['error2']['train'] , test._training['error2']['train'])
- self.assertListEqual(test2._training['error2']['val'], test._training['error2']['val'])
+ test2.loadData(name="dataset", source=dataset)
+ test2.trainModel(
+ splits=[60, 40, 0],
+ optimizer="SGD",
+ lr=0.001,
+ num_of_epochs=1,
+ prediction_samples=10,
+ )
+
+ self.assertListEqual(test2.parameters["W"], test.parameters["W"])
+ self.assertListEqual(test2.parameters["a"], test.parameters["a"])
+ self.assertListEqual(test2.parameters["b"], test.parameters["b"])
+ self.assertListEqual(
+ test2._training["error1"]["train"], test._training["error1"]["train"]
+ )
+ self.assertListEqual(
+ test2._training["error1"]["val"], test._training["error1"]["val"]
+ )
+ self.assertListEqual(
+ test2._training["error2"]["train"], test._training["error2"]["train"]
+ )
+ self.assertListEqual(
+ test2._training["error2"]["val"], test._training["error2"]["val"]
+ )
test2 = Modely(visualizer=None, seed=42, log_internal=True)
- test2.addModel('model', [output1, output2])
- test2.addMinimize('error1', output1, target1.sw(2))
- test2.addMinimize('error2', output2, target2.last())
+ test2.addModel("model", [output1, output2])
+ test2.addMinimize("error1", output1, target1.sw(2))
+ test2.addMinimize("error2", output2, target2.last())
test2.neuralizeModel()
- test2.loadData(name='dataset', source=dataset)
- test2.trainModel(splits=[100,0,0], train_batch_size=1, step=10, optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=3)
-
- self.assertEqual(len(test2.internals.keys()), (3+1)*((26+10)//(10+1)))
- #self.assertEqual(test2.internals)
+ test2.loadData(name="dataset", source=dataset)
+ test2.trainModel(
+ splits=[100, 0, 0],
+ train_batch_size=1,
+ step=10,
+ optimizer="SGD",
+ lr=0.001,
+ num_of_epochs=1,
+ prediction_samples=3,
+ )
+
+ self.assertEqual(len(test2.internals.keys()), (3 + 1) * ((26 + 10) // (10 + 1)))
+ # self.assertEqual(test2.internals)
def test_train_derivate_wrt_input_closed_loop(self):
NeuObj.clearNames()
- x = Input('x')
- x_target = Input('x_target')
- y = Input('y')
+ x = Input("x")
+ x_target = Input("x_target")
+ y = Input("y")
x_last = x.last()
y_last = y.last()
- p=Parameter('fir',sw=1,values=[[-0.5]])
+ p = Parameter("fir", sw=1, values=[[-0.5]])
fun = Sin(x_last) + Fir(W=p)(x_last) + Cos(y_last)
out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last)
out_der.closedLoop(x)
- out = Output('out', out_der)
+ out = Output("out", out_der)
m = Modely(visualizer=None, seed=7)
- m.addModel('model', [out])
- m.addMinimize('error', out, x_target.next())
+ m.addModel("model", [out])
+ m.addMinimize("error", out, x_target.next())
m.neuralizeModel()
K = 0
@@ -1634,39 +3271,46 @@ def fun_data(x, y, K):
x_data.append(x)
y_data.append(y)
- dataset = {'x': x_data, 'y': y_data, 'x_target': x_data}
- m.loadData('dataset', dataset)
- m.trainModel(lr=0.4, num_of_epochs=200, closed_loop={'y': 'out'}, prediction_samples=9)
- result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples=10)
- self.assertAlmostEqual([a.tolist() for a in x_data[0:10]],result['out'])
+ dataset = {"x": x_data, "y": y_data, "x_target": x_data}
+ m.loadData("dataset", dataset)
+ m.trainModel(
+ lr=0.4, num_of_epochs=200, closed_loop={"y": "out"}, prediction_samples=9
+ )
+ result = m(
+ {"x": [-0.2], "y": [0.5]},
+ closed_loop={"y": "out"},
+ num_of_samples=10,
+ prediction_samples=10,
+ )
+ self.assertAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"])
def test_train_derivate_wrt_input_connect(self):
NeuObj.clearNames()
- x = Input('x')
- y = Input('y')
+ x = Input("x")
+ y = Input("y")
x_last = x.last()
y_last = y.last()
- p1 = Parameter('p1', sw=1, values=[[0]])
+ p1 = Parameter("p1", sw=1, values=[[0]])
fun = Sin(x_last) + Fir(W=p1)(x_last) + Cos(y_last)
out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last)
- x2 = Input('x2')
- y2 = Input('y2')
+ x2 = Input("x2")
+ y2 = Input("y2")
x2_last = x2.last()
y2_last = y2.last()
- p2 = Parameter('p2', sw=1, values=[[0]])
+ p2 = Parameter("p2", sw=1, values=[[0]])
fun2 = Sin(x2_last) + Fir(W=p2)(x2_last) + Cos(y2_last)
out_der2 = Differentiate(fun2, x2_last) + Differentiate(fun2, y2_last)
out_der.connect(x2)
- out1 = Output('out1', out_der)
- out2 = Output('out2', out_der2)
+ out1 = Output("out1", out_der)
+ out2 = Output("out2", out_der2)
- target = Input('target')
+ target = Input("target")
m = Modely(visualizer=None, seed=5)
- m.addModel('model', [out1,out2])
- m.addMinimize('error', out_der2, target.last())
+ m.addModel("model", [out1, out2])
+ m.addMinimize("error", out_der2, target.last())
m.neuralizeModel()
K1 = -0.5
@@ -1676,76 +3320,121 @@ def fun_data(x, y, K):
return K + np.cos(x) - np.sin(y)
def fun_data2(x, y, K1, K2):
- return K2 + np.cos(fun_data(x,y,K1)) - np.sin(fun_data(x,y,K1))
+ return K2 + np.cos(fun_data(x, y, K1)) - np.sin(fun_data(x, y, K1))
target = []
import numpy as np
+
x = np.random.rand(100)
y = np.random.rand(100)
- for (xi,yi) in zip(x,y):
+ for xi, yi in zip(x, y):
r = fun_data2(xi, yi, K1, K2)
target.append(r)
- dataset = {'x': x.tolist(), 'y': y.tolist(), 'target': target}
- m.loadData('dataset', dataset)
- m.trainModel(lr=0.3, num_of_epochs=200, splits=[70,20,10], connect={'y2': 'out1'}, prediction_samples=9)
-
- result = m({'x': x.tolist(), 'y': y.tolist()}, connect={'y2':'out1'}, num_of_samples=10, prediction_samples=10)
- self.assertAlmostEqual([a.tolist() for a in target[0:10]],result['out2'])
+ dataset = {"x": x.tolist(), "y": y.tolist(), "target": target}
+ m.loadData("dataset", dataset)
+ m.trainModel(
+ lr=0.3,
+ num_of_epochs=200,
+ splits=[70, 20, 10],
+ connect={"y2": "out1"},
+ prediction_samples=9,
+ )
+
+ result = m(
+ {"x": x.tolist(), "y": y.tolist()},
+ connect={"y2": "out1"},
+ num_of_samples=10,
+ prediction_samples=10,
+ )
+ self.assertAlmostEqual([a.tolist() for a in target[0:10]], result["out2"])
def test_training_values_fir_connect_linear_more_window_2(self):
NeuObj.clearNames()
- input1 = Input('in1')
- W = Parameter('W', sw=4, values=[[1], [1], [1], [1]])
- b = Parameter('b', values=0)
+ input1 = Input("in1")
+ W = Parameter("W", sw=4, values=[[1], [1], [1], [1]])
+ b = Parameter("b", values=0)
lin_out = Fir(W=W, b=b)(input1.sw(4))
- inout = Input('inout')
+ inout = Input("inout")
lin_out.connect(inout)
- W1 = Parameter('W1', sw=4, values=[[1], [1], [1], [1]])
- b1 = Parameter('b1', values=0)
+ W1 = Parameter("W1", sw=4, values=[[1], [1], [1], [1]])
+ b1 = Parameter("b1", values=0)
fir_out = Fir(W=W1, b=b1)(inout.sw(4))
- output = Output('out', inout.sw(4))
- output1 = Output('out1', lin_out)
- output2 = Output('out2', fir_out)
+ output = Output("out", inout.sw(4))
+ output1 = Output("out1", lin_out)
+ output2 = Output("out2", fir_out)
- target = Input('target')
+ target = Input("target")
test = Modely(visualizer=None, seed=42, log_internal=True)
- test.addModel('model', [output, output1, output2])
- test.addMinimize('error2', target.last(), output2)
+ test.addModel("model", [output, output1, output2])
+ test.addMinimize("error2", target.last(), output2)
test.neuralizeModel()
# Dataset with only one sample
- dataset = {'in1': [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0],
- 'inout':[10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0],
- 'target': [10.0, 22.0, 34.0, 46.0, 58.0, 70.0, 82.0, 94.0, 106.0]}
- test.loadData(name='dataset', source=dataset)
-
+ dataset = {
+ "in1": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0],
+ "inout": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0],
+ "target": [10.0, 22.0, 34.0, 46.0, 58.0, 70.0, 82.0, 94.0, 106.0],
+ }
+ test.loadData(name="dataset", source=dataset)
# Use a small batch but with a prediction sample of 3 = to 4 samples
- self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters['W'])
- self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters['W1'])
- self.assertListEqual([0.0], test.parameters['b'])
- self.assertListEqual([0.0], test.parameters['b1'])
+ self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters["W"])
+ self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters["W1"])
+ self.assertListEqual([0.0], test.parameters["b"])
+ self.assertListEqual([0.0], test.parameters["b1"])
inference = test(inputs=dataset, prediction_samples=3)
- self.assertDictEqual({'out': [[10.0, 20.0, 30.0, 12.0], [20.0, 30.0, 12.0, 20.0], [30.0, 12.0, 20.0, 28.0], [12.0, 20.0, 28.0, 36.0], [50.0, 60.0, 70.0, 44.0], [60.0, 70.0, 44.0, 52.0]],
- 'out1': [12.0, 20.0, 28.0, 36.0, 44.0, 52.0],
- 'out2': [72.0, 82.0, 90.0, 96.0, 224.0, 226.0]},
- inference)
- test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.0001, train_batch_size=1, num_of_epochs=1, shuffle_data=False, prediction_samples=3)
- for i, (k,v) in enumerate(test.internals.items()):
+ self.assertDictEqual(
+ {
+ "out": [
+ [10.0, 20.0, 30.0, 12.0],
+ [20.0, 30.0, 12.0, 20.0],
+ [30.0, 12.0, 20.0, 28.0],
+ [12.0, 20.0, 28.0, 36.0],
+ [50.0, 60.0, 70.0, 44.0],
+ [60.0, 70.0, 44.0, 52.0],
+ ],
+ "out1": [12.0, 20.0, 28.0, 36.0, 44.0, 52.0],
+ "out2": [72.0, 82.0, 90.0, 96.0, 224.0, 226.0],
+ },
+ inference,
+ )
+ test.trainModel(
+ train_dataset="dataset",
+ optimizer="SGD",
+ lr=0.0001,
+ train_batch_size=1,
+ num_of_epochs=1,
+ shuffle_data=False,
+ prediction_samples=3,
+ )
+ for i, (k, v) in enumerate(test.internals.items()):
if i > 3:
break
- self.assertEqual(v['out']['out'][0][-1][0], v['out']['out1'][0][0][0])
- self.assertEqual(sum([x[0] for x in v['out']['out'][0]]), v['out']['out2'][0][0][0])
- self.assertDictEqual({'inout': [[[20.0], [30.0], [12.0], [float('inf')]]]}, test.internals['inout_0_0']['state'])
- self.assertDictEqual({'inout': [[[30.0], [12.0], [20.0], [float('inf')]]]}, test.internals['inout_0_1']['state'])
- self.assertDictEqual({'inout': [[[12.0], [20.0], [28.0], [float('inf')]]]}, test.internals['inout_0_2']['state'])
- self.assertDictEqual({'inout': [[[20.0], [28.0], [36.0], [float('inf')]]]}, test.internals['inout_0_3']['state'])
-
+ self.assertEqual(v["out"]["out"][0][-1][0], v["out"]["out1"][0][0][0])
+ self.assertEqual(
+ sum([x[0] for x in v["out"]["out"][0]]), v["out"]["out2"][0][0][0]
+ )
+ self.assertDictEqual(
+ {"inout": [[[20.0], [30.0], [12.0], [float("inf")]]]},
+ test.internals["inout_0_0"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[30.0], [12.0], [20.0], [float("inf")]]]},
+ test.internals["inout_0_1"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[12.0], [20.0], [28.0], [float("inf")]]]},
+ test.internals["inout_0_2"]["state"],
+ )
+ self.assertDictEqual(
+ {"inout": [[[20.0], [28.0], [36.0], [float("inf")]]]},
+ test.internals["inout_0_3"]["state"],
+ )
# def test_state_initialization(self):
# NeuObj.clearNames()
@@ -1782,4 +3471,4 @@ def test_training_values_fir_connect_linear_more_window_2(self):
# self.assertEqual(model.internals['inout_0_3']['out']['out'], [[[42.0]]])
# self.assertEqual(model.internals['inout_0_4']['out']['out'], [[[89.0]]])
# self.assertEqual(model.internals['inout_0_5']['out']['out'], [[[184.0]]])
- # self.assertEqual(model.internals['inout_1_0']['out']['out'], [[[6.0]]])
\ No newline at end of file
+ # self.assertEqual(model.internals['inout_1_0']['out']['out'], [[[6.0]]])
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e3d6665e..bc7f9c8b 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,4 +1,7 @@
-import unittest, os, sys, torch
+import unittest
+import os
+import sys
+import torch
from nnodely import *
from nnodely.support.mathutils import linear_interp
@@ -12,14 +15,21 @@
sys.path.append(os.getcwd())
+
class ModelyTrainingTest(unittest.TestCase):
# test the linear interpolation function with batches of input data with shape torch.Size([N, 1, 1])
def test_linear_interp_with_batched_input_1(self):
# x is a tensor of query points, and is a tensor of shape torch.Size([N, 1, 1])
- x = torch.tensor([[[-1.0]],[[0.15]],[[0.25]],[[0.35]],[[1.3]]])
+ x = torch.tensor([[[-1.0]], [[0.15]], [[0.25]], [[0.35]], [[1.3]]])
# x_data and y_data are tensors of shape torch.Size([Q, 1])
- x_data = torch.tensor([[0.0],[0.1],[0.2],[0.3],[0.4],[0.5],[0.6],[0.7],[0.8],[0.9]])
- y_data = torch.tensor([[0.5],[0.6],[0.7],[0.8],[0.9],[1.0],[1.1],[1.2],[1.3],[1.4]])
+ x_data = torch.tensor(
+ [[0.0], [0.1], [0.2], [0.3], [0.4], [0.5], [0.6], [0.7], [0.8], [0.9]]
+ )
+ y_data = torch.tensor(
+ [[0.5], [0.6], [0.7], [0.8], [0.9], [1.0], [1.1], [1.2], [1.3], [1.4]]
+ )
- y = linear_interp(x,x_data,y_data)
- self.assertEqual(y.shape, x.shape) # check that the output has the same shape as the input
+ y = linear_interp(x, x_data, y_data)
+ self.assertEqual(
+ y.shape, x.shape
+ ) # check that the output has the same shape as the input
diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py
index b7f50364..4a824d12 100644
--- a/tests/test_visualizer.py
+++ b/tests/test_visualizer.py
@@ -1,10 +1,9 @@
-import sys, io, os, unittest, torch
-import numpy as np
+import sys
+import os
+import unittest
from nnodely import *
-from nnodely.basic.relation import NeuObj
from nnodely.support.logger import logging, nnLogger
-from nnodely.support.jsonutils import plot_structure
log = nnLogger(__name__, logging.ERROR)
log.setAllLevel(logging.ERROR)
@@ -14,258 +13,319 @@
# 3 Tests
# Test of visualizers
-class ModelyTestVisualizer(unittest.TestCase):
- def __init__(self, *args, **kwargs):
- NeuObj.clearNames()
- super(ModelyTestVisualizer, self).__init__(*args, **kwargs)
-
- self.x = x = Input('x')
- self.y = y = Input('y')
- self.z = z = Input('z')
- self.a = a = Input('a', dimensions=2)
- self.b = b = Input('b', dimensions=2)
-
- ## create the relations
- def myFun(K1, p1, p2):
- return K1 * p1 * p2
-
- P_time = Parameter('P_time', dimensions=2, sw=5, values=[[0,0],[-0.1,0.1],[-0.2,0.2],[-0.3,0.3],[-0.4,0.4]])
- K_x = Parameter('k_x', dimensions=1, tw=1, init='init_constant', init_params={'value': 1})
- K_y = Parameter('k_y', dimensions=1, tw=1)
- w = Parameter('w', dimensions=1, tw=1, init='init_constant', init_params={'value': 1})
- t = Parameter('t', dimensions=1, tw=1)
- c_v = Constant('c_v', tw=1, values=[[1], [2]])
- c = 5
- w_5 = Parameter('w_5', dimensions=1, tw=5)
- t_5 = Parameter('t_5', dimensions=1, tw=5)
- c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
- c_5_2 = Constant('c_5_2', tw=5, values=c_5)
- parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v])
- parfun_y = ParamFun(myFun, parameters_and_constants=[K_y])
- parfun_zz = ParamFun(myFun)
- parfun_2d = ParamFun(myFun, parameters_and_constants=[K_x, K_x])
- parfun_3d = ParamFun(myFun, parameters_and_constants=[K_x])
- fir_w = Fir(W=w_5)(x.tw(5))
- fir_t = Fir(W=t_5)(y.tw(5))
- time_part = TimePart(x.tw(5), i=1, j=3)
- sample_select = SampleSelect(x.sw(5), i=1)
-
- def fuzzyfun(x):
- return torch.sin(x)
-
- def fuzzyfunth(x):
- return torch.tanh(x)
-
- fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1))
- fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1))
- fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions='Rectangular')(x.tw(1))
- fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun,fuzzyfunth])(x.tw(1))
- self.stream = fuzzyList
-
- self.out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)))
- self.out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
- self.out3 = Output('out3', Add(fir_w, fir_t))
- self.out4 = Output('out4', Linear(output_dimension=1)(fuzzy+fuzzyTriang+fuzzyRect+fuzzyList))
- self.out5 = Output('out5', Fir(time_part) + Fir(sample_select))
- self.out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy))
- self.out7 = Output('out7', parfun_zz(z.last()))
- self.out8 = Output('out8', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_zz(x.tw(5), t_5, c_5_2)))
- self.out9 = Output('out9', Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1),x.tw(1))))
- self.out10 = Output('out10', a.sw(5)+P_time)
- self.out11 = Output('out11', TimeConcatenate(TimeConcatenate(
- TimeConcatenate(Integrate(a.last()),Integrate(a.last())),
- TimeConcatenate(Integrate(a.last()),Integrate(a.last()))
- ),Integrate(a.last()))+P_time)
-
- def setUp(self):
- # Reindirizza stdout e stderr
- self._original_stdout = sys.stdout
- self._original_stderr = sys.stderr
- sys.stdout = io.StringIO()
- sys.stderr = io.StringIO()
-
- def tearDown(self):
- # Ripristina stdout e stderr
- sys.stdout = self._original_stdout
- sys.stderr = self._original_stderr
-
- def test_rper_of_objects(self):
- print(repr(self.x))
- print(repr(self.stream))
- print(repr(self.out9))
-
- def test_export_textvisualizer(self):
- t = TextVisualizer(5)
- test = Modely(visualizer=t, seed=42, workspace='./results')
- test.addModel('modelA', self.out)
- test.addModel('modelB', [self.out2, self.out3, self.out4])
- test.addModel('modelC', [self.out4, self.out5, self.out6])
- test.addModel('modelD', self.out7)
- test.addMinimize('error1', self.x.last(), self.out)
- test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse')
- test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse')
-
- test.neuralizeModel(0.5)
-
- data_x = np.arange(0.0, 5, 0.1)
- data_y = np.arange(0.0, 5, 0.1)
- a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 10, 'lr': 0.01}
- test.loadData(name='dataset', source=dataset) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model
- t.showMinimize('error1')
- t.showMinimize('error2')
- t.showMinimize('error3')
- test.trainModel(optimizer='SGD', training_params=params)
- t.showWeights()
- test.trainModel(optimizer='SGD', training_params=params, closed_loop={'x':'out'}, prediction_samples=1)
- test.saveModel()
- test.loadModel()
-
- test.neuralizeModel(0.5)
- test.exportPythonModel()
- test.importPythonModel()
- test.exportReport()
-
- test = Modely(visualizer='Standard')
- test.addModel('modelA', self.out)
- test.neuralizeModel(0.5)
- def test_export_mplvisualizer(self):
- m = MPLVisualizer(5)
- test = Modely(visualizer=m, seed=42)
- test.addModel('modelA', self.out)
- test.addModel('modelB', [self.out2, self.out3, self.out4])
- test.addModel('modelC', [self.out4, self.out5, self.out6])
- test.addModel('modelD', self.out7)
- test.addModel('modelE', self.out9)
- test.addMinimize('error1', self.x.last(), self.out)
- test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse')
- test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse')
-
- test.neuralizeModel(0.5)
-
- data_x = np.arange(0.0, 1000, 0.1)
- data_y = np.arange(0.0, 1000, 0.1)
- a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 10, 'lr': 0.01}
- test.loadData(name='dataset', source=dataset) # Create the dataset
- test.trainAndAnalyze(optimizer='SGD', training_params=params) # Train the traced model
- test.trainAndAnalyze(optimizer='SGD', training_params=params)
- m.closeResult()
- m.closeTraining()
- list_of_functions = list(test.json['Functions'].keys())
- try:
- for f in list_of_functions:
- m.showFunctions(f)
- except ValueError:
- pass
- with self.assertRaises(ValueError):
- m.showFunctions(list_of_functions[1])
- m.closeFunctions()
- test.trainAndAnalyze(optimizer='SGD', splits=[70, 20, 10], training_params=params, closed_loop={'x': 'out2'},
- prediction_samples=5)
- m.closeResult()
- m.closeTraining()
-
- def test_export_mplvisualizer2(self):
- clearNames(['x', 'F'])
- x = Input('x')
- F = Input('F')
- def myFun(K1, K2, p1, p2):
- import torch
- return p1 * K1 + p2 * torch.sin(K2)
-
- parfun = ParamFun(myFun)
- out = Output('fun', parfun(x.last(), F.last()))
- m = MPLVisualizer()
- example = Modely(visualizer=m)
- example.addModel('out', out)
- example.neuralizeModel()
- m.showFunctions(list(example.json['Functions'].keys()), xlim=[[-5, 5], [-1, 1]])
- m.closeFunctions()
-
- # @unittest.skipIf(
- # sys.platform.startswith("win"),
- # reason="MPLNotebookVisualizer ask for backend GUI not available in Windows CI"
- # )
- def test_export_mplnotebookvisualizer(self):
- m = MPLNotebookVisualizer(5, test=True)
- test = Modely(visualizer=m, seed=42)
- test.addModel('modelB', [self.out2, self.out3, self.out4])
- test.addModel('modelC', [self.out4, self.out5, self.out6])
- test.addModel('modelD', [self.out9])
- test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse')
- test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse')
-
- test.neuralizeModel(1)
-
- data_x = np.arange(0.0, 1000, 0.1)
- data_y = np.arange(0.0, 1000, 0.1)
- a, b = -1.0, 2.0
- dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y}
- params = {'num_of_epochs': 1, 'lr': 0.01}
- test.loadData(name='dataset', source=dataset) # Create the dataset
- test.trainAndAnalyze(optimizer='SGD', splits=[70,20,10], training_params=params) # Train the traced mode
- m.closePlots()
- list_of_functions = list(test.json['Functions'].keys())
- try:
- for f in list_of_functions:
- m.showFunctions(f)
- except ValueError:
- pass
- m.closePlots()
- test.trainAndAnalyze(optimizer='SGD', splits=[70, 20, 10], training_params=params, closed_loop={'x':'out2'}, prediction_samples=5)
- m.closePlots()
-
-
- def test_structure_plot(self):
- clearNames()
- X = Input('X')
- Y = Input('Y')
- Z = Input('Z')
- t_state = Input('t_state')
- k_state = Input('k_state')
-
- func1 = Fir(X.last()) + Fir(Y.last())
- func1.closedLoop(t_state)
- func2 = Fir(Z.last()) + t_state.last()
- func2.connect(k_state)
- func3 = Fir(k_state.last()) * Constant('g', sw=1, values=[[9.8]])
-
- out = Output('out', func1 + func2 + func3)
-
- example = Modely(visualizer=None)
- example.addModel('model', out)
- example.neuralizeModel()
- with self.assertRaises(ValueError):
- plot_structure(example.json, filename='results/structure_plot', library='invalid_library')
- plot_structure(example.json, filename='results/structure_plot', library='matplotlib', view=False)
- #plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False)
-
- def test_window_vector_plot(self):
- m = MPLNotebookVisualizer(5, test=True)
- test = Modely(visualizer=m, seed=42)
- test.addModel('modelA', self.out10)
- test.addMinimize('error1', self.b.sw(5), self.out10, loss_function='rmse')
- test.neuralizeModel()
- data_x = np.sin(np.arange(0.0, 5, 0.01))
- data_y = np.cos(np.arange(0.0, 5, 0.01))
- data_a = np.transpose(np.array([data_x,data_y]))
- dataset = {'a':data_a, 'b': data_a}
- test.loadData(name='dataset', source=dataset)
- test.analyzeModel()
-
- def test_window_vector_plot_recurrent(self):
- m = MPLNotebookVisualizer(5, test=True)
- test = Modely(visualizer=m, seed=42)
- test.addModel('modelA', self.out10)
- test.addMinimize('error1', self.b.sw(5), self.out11, loss_function='rmse')
- test.neuralizeModel(0.1)
- data_x = np.sin(np.arange(0.0, 5, 0.01))
- data_y = np.cos(np.arange(0.0, 5, 0.01))
- data_a = np.transpose(np.array([data_x,data_y]))
- dataset = {'a':data_a, 'b': data_a}
- test.loadData(name='dataset', source=dataset)
- test.analyzeModel(prediction_samples=20)
\ No newline at end of file
+class ModelyTestVisualizer(unittest.TestCase):
+ pass
+ # def __init__(self, *args, **kwargs):
+ # NeuObj.clearNames()
+ # super(ModelyTestVisualizer, self).__init__(*args, **kwargs)
+ #
+ # self.x = x = Input("x")
+ # self.y = y = Input("y")
+ # self.z = z = Input("z")
+ # self.a = a = Input("a", dimensions=2)
+ # self.b = b = Input("b", dimensions=2)
+ #
+ # ## create the relations
+ # def myFun(K1, p1, p2):
+ # return K1 * p1 * p2
+ #
+ # P_time = Parameter(
+ # "P_time",
+ # dimensions=2,
+ # sw=5,
+ # values=[[0, 0], [-0.1, 0.1], [-0.2, 0.2], [-0.3, 0.3], [-0.4, 0.4]],
+ # )
+ # K_x = Parameter(
+ # "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1}
+ # )
+ # K_y = Parameter("k_y", dimensions=1, tw=1)
+ # w = Parameter(
+ # "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1}
+ # )
+ # t = Parameter("t", dimensions=1, tw=1)
+ # c_v = Constant("c_v", tw=1, values=[[1], [2]])
+ # c = 5
+ # w_5 = Parameter("w_5", dimensions=1, tw=5)
+ # t_5 = Parameter("t_5", dimensions=1, tw=5)
+ # c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
+ # c_5_2 = Constant("c_5_2", tw=5, values=c_5)
+ # parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v])
+ # parfun_y = ParamFun(myFun, parameters_and_constants=[K_y])
+ # parfun_zz = ParamFun(myFun)
+ # parfun_2d = ParamFun(myFun, parameters_and_constants=[K_x, K_x])
+ # parfun_3d = ParamFun(myFun, parameters_and_constants=[K_x])
+ # fir_w = Fir(W=w_5)(x.tw(5))
+ # fir_t = Fir(W=t_5)(y.tw(5))
+ # time_part = TimePart(x.tw(5), i=1, j=3)
+ # sample_select = SampleSelect(x.sw(5), i=1)
+ #
+ # def fuzzyfun(x):
+ # return torch.sin(x)
+ #
+ # def fuzzyfunth(x):
+ # return torch.tanh(x)
+ #
+ # fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1))
+ # fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1))
+ # fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions="Rectangular")(x.tw(1))
+ # fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun, fuzzyfunth])(
+ # x.tw(1)
+ # )
+ # self.stream = fuzzyList
+ #
+ # self.out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)))
+ # self.out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c))
+ # self.out3 = Output("out3", Add(fir_w, fir_t))
+ # self.out4 = Output(
+ # "out4",
+ # Linear(output_dimension=1)(fuzzy + fuzzyTriang + fuzzyRect + fuzzyList),
+ # )
+ # self.out5 = Output("out5", Fir(time_part) + Fir(sample_select))
+ # self.out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy))
+ # self.out7 = Output("out7", parfun_zz(z.last()))
+ # self.out8 = Output(
+ # "out8",
+ # Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))
+ # + Fir(parfun_zz(x.tw(5), t_5, c_5_2)),
+ # )
+ # self.out9 = Output(
+ # "out9", Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1), x.tw(1)))
+ # )
+ # self.out10 = Output("out10", a.sw(5) + P_time)
+ # self.out11 = Output(
+ # "out11",
+ # TimeConcatenate(
+ # TimeConcatenate(
+ # TimeConcatenate(Integrate(a.last()), Integrate(a.last())),
+ # TimeConcatenate(Integrate(a.last()), Integrate(a.last())),
+ # ),
+ # Integrate(a.last()),
+ # )
+ # + P_time,
+ # )
+ #
+ # def setUp(self):
+ # # Reindirizza stdout e stderr
+ # self._original_stdout = sys.stdout
+ # self._original_stderr = sys.stderr
+ # sys.stdout = io.StringIO()
+ # sys.stderr = io.StringIO()
+ #
+ # def tearDown(self):
+ # # Ripristina stdout e stderr
+ # sys.stdout = self._original_stdout
+ # sys.stderr = self._original_stderr
+ #
+ # def test_rper_of_objects(self):
+ # print(repr(self.x))
+ # print(repr(self.stream))
+ # print(repr(self.out9))
+ #
+ # def test_export_textvisualizer(self):
+ # t = TextVisualizer(5)
+ # test = Modely(visualizer=t, seed=42, workspace="./results")
+ # test.addModel("modelA", self.out)
+ # test.addModel("modelB", [self.out2, self.out3, self.out4])
+ # test.addModel("modelC", [self.out4, self.out5, self.out6])
+ # test.addModel("modelD", self.out7)
+ # test.addMinimize("error1", self.x.last(), self.out)
+ # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse")
+ # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse")
+ #
+ # test.neuralizeModel(0.5)
+ #
+ # data_x = np.arange(0.0, 5, 0.1)
+ # data_y = np.arange(0.0, 5, 0.1)
+ # a, b = -1.0, 2.0
+ # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ # params = {"num_of_epochs": 10, "lr": 0.01}
+ # test.loadData(
+ # name="dataset", source=dataset
+ # ) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model
+ # t.showMinimize("error1")
+ # t.showMinimize("error2")
+ # t.showMinimize("error3")
+ # test.trainModel(optimizer="SGD", training_params=params)
+ # t.showWeights()
+ # test.trainModel(
+ # optimizer="SGD",
+ # training_params=params,
+ # closed_loop={"x": "out"},
+ # prediction_samples=1,
+ # )
+ # test.saveModel()
+ # test.loadModel()
+ #
+ # test.neuralizeModel(0.5)
+ # test.exportPythonModel()
+ # test.importPythonModel()
+ # test.exportReport()
+ #
+ # test = Modely(visualizer="Standard")
+ # test.addModel("modelA", self.out)
+ # test.neuralizeModel(0.5)
+ #
+ # def test_export_mplvisualizer(self):
+ # m = MPLVisualizer(5)
+ # test = Modely(visualizer=m, seed=42)
+ # test.addModel("modelA", self.out)
+ # test.addModel("modelB", [self.out2, self.out3, self.out4])
+ # test.addModel("modelC", [self.out4, self.out5, self.out6])
+ # test.addModel("modelD", self.out7)
+ # test.addModel("modelE", self.out9)
+ # test.addMinimize("error1", self.x.last(), self.out)
+ # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse")
+ # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse")
+ #
+ # test.neuralizeModel(0.5)
+ #
+ # data_x = np.arange(0.0, 1000, 0.1)
+ # data_y = np.arange(0.0, 1000, 0.1)
+ # a, b = -1.0, 2.0
+ # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ # params = {"num_of_epochs": 10, "lr": 0.01}
+ # test.loadData(name="dataset", source=dataset) # Create the dataset
+ # test.trainAndAnalyze(
+ # optimizer="SGD", training_params=params
+ # ) # Train the traced model
+ # test.trainAndAnalyze(optimizer="SGD", training_params=params)
+ # m.closeResult()
+ # m.closeTraining()
+ # list_of_functions = list(test.json["Functions"].keys())
+ # try:
+ # for f in list_of_functions:
+ # m.showFunctions(f)
+ # except ValueError:
+ # pass
+ # with self.assertRaises(ValueError):
+ # m.showFunctions(list_of_functions[1])
+ # m.closeFunctions()
+ # test.trainAndAnalyze(
+ # optimizer="SGD",
+ # splits=[70, 20, 10],
+ # training_params=params,
+ # closed_loop={"x": "out2"},
+ # prediction_samples=5,
+ # )
+ # m.closeResult()
+ # m.closeTraining()
+ #
+ # def test_export_mplvisualizer2(self):
+ # clearNames(["x", "F"])
+ # x = Input("x")
+ # F = Input("F")
+ #
+ # def myFun(K1, K2, p1, p2):
+ # import torch
+ #
+ # return p1 * K1 + p2 * torch.sin(K2)
+ #
+ # parfun = ParamFun(myFun)
+ # out = Output("fun", parfun(x.last(), F.last()))
+ # m = MPLVisualizer()
+ # example = Modely(visualizer=m)
+ # example.addModel("out", out)
+ # example.neuralizeModel()
+ # m.showFunctions(list(example.json["Functions"].keys()), xlim=[[-5, 5], [-1, 1]])
+ # m.closeFunctions()
+ #
+ # # @unittest.skipIf(
+ # # sys.platform.startswith("win"),
+ # # reason="MPLNotebookVisualizer ask for backend GUI not available in Windows CI"
+ # # )
+ # def test_export_mplnotebookvisualizer(self):
+ # m = MPLNotebookVisualizer(5, test=True)
+ # test = Modely(visualizer=m, seed=42)
+ # test.addModel("modelB", [self.out2, self.out3, self.out4])
+ # test.addModel("modelC", [self.out4, self.out5, self.out6])
+ # test.addModel("modelD", [self.out9])
+ # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse")
+ # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse")
+ #
+ # test.neuralizeModel(1)
+ #
+ # data_x = np.arange(0.0, 1000, 0.1)
+ # data_y = np.arange(0.0, 1000, 0.1)
+ # a, b = -1.0, 2.0
+ # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y}
+ # params = {"num_of_epochs": 1, "lr": 0.01}
+ # test.loadData(name="dataset", source=dataset) # Create the dataset
+ # test.trainAndAnalyze(
+ # optimizer="SGD", splits=[70, 20, 10], training_params=params
+ # ) # Train the traced mode
+ # m.closePlots()
+ # list_of_functions = list(test.json["Functions"].keys())
+ # try:
+ # for f in list_of_functions:
+ # m.showFunctions(f)
+ # except ValueError:
+ # pass
+ # m.closePlots()
+ # test.trainAndAnalyze(
+ # optimizer="SGD",
+ # splits=[70, 20, 10],
+ # training_params=params,
+ # closed_loop={"x": "out2"},
+ # prediction_samples=5,
+ # )
+ # m.closePlots()
+ #
+ # def test_structure_plot(self):
+ # clearNames()
+ # X = Input("X")
+ # Y = Input("Y")
+ # Z = Input("Z")
+ # t_state = Input("t_state")
+ # k_state = Input("k_state")
+ #
+ # func1 = Fir(X.last()) + Fir(Y.last())
+ # func1.closedLoop(t_state)
+ # func2 = Fir(Z.last()) + t_state.last()
+ # func2.connect(k_state)
+ # func3 = Fir(k_state.last()) * Constant("g", sw=1, values=[[9.8]])
+ #
+ # out = Output("out", func1 + func2 + func3)
+ #
+ # example = Modely(visualizer=None)
+ # example.addModel("model", out)
+ # example.neuralizeModel()
+ # with self.assertRaises(ValueError):
+ # plot_structure(
+ # example.json,
+ # filename="results/structure_plot",
+ # library="invalid_library",
+ # )
+ # plot_structure(
+ # example.json,
+ # filename="results/structure_plot",
+ # library="matplotlib",
+ # view=False,
+ # )
+ # # plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False)
+ #
+ # def test_window_vector_plot(self):
+ # m = MPLNotebookVisualizer(5, test=True)
+ # test = Modely(visualizer=m, seed=42)
+ # test.addModel("modelA", self.out10)
+ # test.addMinimize("error1", self.b.sw(5), self.out10, loss_function="rmse")
+ # test.neuralizeModel()
+ # data_x = np.sin(np.arange(0.0, 5, 0.01))
+ # data_y = np.cos(np.arange(0.0, 5, 0.01))
+ # data_a = np.transpose(np.array([data_x, data_y]))
+ # dataset = {"a": data_a, "b": data_a}
+ # test.loadData(name="dataset", source=dataset)
+ # test.analyzeModel()
+ #
+ # def test_window_vector_plot_recurrent(self):
+ # m = MPLNotebookVisualizer(5, test=True)
+ # test = Modely(visualizer=m, seed=42)
+ # test.addModel("modelA", self.out10)
+ # test.addMinimize("error1", self.b.sw(5), self.out11, loss_function="rmse")
+ # test.neuralizeModel(0.1)
+ # data_x = np.sin(np.arange(0.0, 5, 0.01))
+ # data_y = np.cos(np.arange(0.0, 5, 0.01))
+ # data_a = np.transpose(np.array([data_x, data_y]))
+ # dataset = {"a": data_a, "b": data_a}
+ # test.loadData(name="dataset", source=dataset)
+ # test.analyzeModel(prediction_samples=20)
diff --git a/tests/val_data/testdata.dta b/tests/val_data/testdata.dta
index 3175afdc..24f246c3 100644
--- a/tests/val_data/testdata.dta
+++ b/tests/val_data/testdata.dta
@@ -13,4 +13,3 @@ x1 y1 x2 y2 A1x A1y B1x B1y A2x A2y
0.809 0.820 0.337 0.466 - 0.350 1.375 0.577 1.208 - 0.575 1.375 0.706 2.220 - 0.274 0.857 12.533 0.080 -
0.810 0.819 0.342 0.460 - 0.350 1.375 0.574 1.204 - 0.575 1.375 0.703 2.217 - 0.274 0.850 12.543 0.090 -
0.812 0.818 0.348 0.453 - 0.350 1.375 0.571 1.199 - 0.575 1.375 0.699 2.214 - 0.274 0.842 12.556 0.100 -
-
diff --git a/tests/vector_data/vector_2.dta b/tests/vector_data/vector_2.dta
index bb9f5885..e5b91343 100644
--- a/tests/vector_data/vector_2.dta
+++ b/tests/vector_data/vector_2.dta
@@ -14,4 +14,3 @@ x1 x2 x3 x4 y1 y2 y3 NO NO NO NO k1
0.805 0.825 0.322 0.485 0.350 1.375 0.585 1.218 - 0.575 1.375 0.714 1.227 - 0.274 0.977 12.502 0.040
0.806 0.824 0.325 0.481 0.350 1.375 0.584 1.216 - 0.575 1.375 0.712 1.225 - 0.274 0.973 12.508 0.050
0.807 0.823 0.329 0.477 0.350 1.375 0.582 1.214 - 0.575 1.375 0.710 1.224 - 0.274 0.969 12.515 0.060
-
diff --git a/tests/vehicle_data/vehicle.csv b/tests/vehicle_data/vehicle.csv
index 5625a587..3d80578b 100644
--- a/tests/vehicle_data/vehicle.csv
+++ b/tests/vehicle_data/vehicle.csv
@@ -98,4 +98,4 @@
13.473333333333336,12.779200000000001,0.,5,-0.7460835566787409,-0.6602761975976819,-0.5746327099489577,-0.48922828574131927,-0.4061226600199461,-0.3259584416340431,-0.2506976788511679,-0.18109245144700026,-0.11566989947226602,-0.05551166846061051,0.,0.04959411345004128,0.09326429982348827,0.12985450593151882,0.16023045483507303,0.1832476556463689,0.20626485645772163,0.22928205726901751,0.2522992580803134,0.2753164588916661,0.298333659702962,0.09911902783658734
13.474444444444448,13.0928,0.,5,-0.7485232229184362,-0.6627158638373771,-0.5770452306900893,-0.4916408064824509,-0.4081821510060877,-0.32801793262018464,-0.25204581648097246,-0.18244058907686167,-0.11638159820341798,-0.05622336719181931,0.,0.04959411345004128,0.09414036583672214,0.13073057194475268,0.162020926674586,0.1850381274858819,0.2080553282972346,0.2310725291085305,0.25408972991982637,0.2771069307311791,0.30012413154247497,0.11169376341230414
13.496111111111112,13.014400000000002,0.,5,-0.7509649513013414,-0.6651575922202255,-0.5794597906294712,-0.4940553664218328,-0.41024338278987216,-0.3300791644039691,-0.2533950936325482,-0.1837898662284374,-0.11709389850244634,-0.05693566749079082,0.,0.04959411345004128,0.09501717235025353,0.13160737845828407,0.16381291192163872,0.1868301127329346,0.20984731354423047,0.2328645143555832,0.2558817151668791,0.2788989159782318,0.3019161167895277,0.1517397792930878
-13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794
\ No newline at end of file
+13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 00000000..20129039
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,2168 @@
+version = 1
+revision = 3
+requires-python = ">=3.10, <3.14"
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" },
+ { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" },
+ { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" },
+ { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" },
+ { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" },
+ { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" },
+ { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" },
+ { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" },
+ { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" },
+ { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
+ { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
+ { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
+ { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
+ { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
+ { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+]
+
+[[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.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+]
+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.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.4", 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/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.13.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" },
+ { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" },
+ { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" },
+ { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" },
+ { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" },
+ { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" },
+ { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" },
+ { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" },
+ { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" },
+ { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" },
+ { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" },
+ { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
+ { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
+ { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
+ { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
+ { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
+ { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
+ { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
+ { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[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 = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[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 = "filelock"
+version = "3.25.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.12.19"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.62.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" },
+ { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" },
+ { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" },
+ { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" },
+ { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" },
+ { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" },
+ { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" },
+ { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" },
+ { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" },
+ { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" },
+ { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" },
+]
+
+[[package]]
+name = "graphviz"
+version = "0.21"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+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 = "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 = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" },
+ { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" },
+ { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" },
+ { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" },
+ { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" },
+ { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" },
+ { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" },
+ { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" },
+ { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" },
+ { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" },
+ { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
+ { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
+ { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" },
+ { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
+]
+
+[[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" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+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/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 = "ml-dtypes"
+version = "0.5.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" },
+ { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" },
+ { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" },
+ { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" },
+ { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" },
+ { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "nnodely"
+version = "1.5.5.dev1"
+source = { editable = "." }
+dependencies = [
+ { name = "graphviz" },
+ { name = "matplotlib" },
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "onnx" },
+ { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "onnxruntime", version = "1.24.4", 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.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "reportlab" },
+ { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pre-commit" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "graphviz" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "numpy", marker = "python_full_version == '3.10.*' and platform_machine == 'x86_64'", specifier = "==1.26.4" },
+ { name = "onnx" },
+ { name = "onnxruntime" },
+ { name = "pandas" },
+ { name = "reportlab" },
+ { name = "torch", marker = "python_full_version != '3.10.*' or platform_machine != 'x86_64'", specifier = "==2.6.0" },
+ { name = "torch", marker = "python_full_version == '3.10.*' and platform_machine == 'x86_64'", specifier = "==2.2.2" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pre-commit", specifier = ">=4.5.1" },
+ { name = "pytest-cov", specifier = ">=7.1.0" },
+ { name = "ruff", specifier = ">=0.15.10" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" },
+ { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" },
+ { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" },
+ { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
+ { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+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.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
+ { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
+ { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
+ { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
+ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
+ { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.1.3.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774, upload-time = "2023-04-19T15:50:03.519Z" },
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.4.5.8"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805, upload-time = "2024-04-03T20:57:06.025Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.1.105"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015, upload-time = "2023-04-19T15:47:32.502Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957, upload-time = "2024-04-03T20:55:01.564Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.1.105"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734, upload-time = "2023-04-19T15:48:32.42Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306, upload-time = "2024-04-03T20:56:01.463Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.1.105"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596, upload-time = "2023-04-19T15:47:22.471Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737, upload-time = "2024-04-03T20:54:51.355Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "8.9.2.26"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.1.0.70"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.0.2.54"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161, upload-time = "2023-04-19T15:50:46Z" },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.2.1.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.2.106"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784, upload-time = "2023-04-19T15:51:04.804Z" },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.5.147"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206, upload-time = "2024-04-03T20:58:08.722Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.4.5.107"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "nvidia-cusparse-cu12", version = "12.1.0.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink-cu12", version = "12.9.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.6.1.9"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "nvidia-cusparse-cu12", version = "12.3.1.170", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.1.0.106"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", version = "12.9.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.3.1.170"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751, upload-time = "2024-07-23T02:35:53.074Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.19.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/00/d0d4e48aef772ad5aebcf70b73028f88db6e5640b36c38e90445b7a57c45/nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d", size = 165987969, upload-time = "2023-10-24T16:16:24.789Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.21.5"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414, upload-time = "2024-04-03T15:32:57.427Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810, upload-time = "2024-04-03T20:59:46.957Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.9.86"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.1.105"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138, upload-time = "2023-04-19T15:48:43.556Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144, upload-time = "2024-04-03T20:56:12.406Z" },
+]
+
+[[package]]
+name = "onnx"
+version = "1.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ml-dtypes" },
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "protobuf" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/28/a14b1845bf9302c3a787221e8f37cde4e7f930e10d95a8e22dd910aeb41d/onnx-1.21.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd", size = 17966899, upload-time = "2026-03-27T21:32:15.53Z" },
+ { url = "https://files.pythonhosted.org/packages/41/7b/788881bf022a4cfb7b0843782f88415ea51c805cee4a909dcf2e49bb8129/onnx-1.21.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a", size = 17534297, upload-time = "2026-03-27T21:32:18.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/51/eb64d4f2ec6caa98909aab5fbcfa24be9c059081e804bbb0012cc549ef89/onnx-1.21.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9", size = 17616697, upload-time = "2026-03-27T21:32:21Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/4e/6b1f7800dae3407dc850e7e59d591ed8c83e9b3401e4cd57a1f612e400c6/onnx-1.21.0-cp310-cp310-win32.whl", hash = "sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027", size = 16288893, upload-time = "2026-03-27T21:32:23.864Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/a8/89273e581d3943e20314af19b1596ab4d763f9c2eb07d4eaf4fb0593219b/onnx-1.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68", size = 16443416, upload-time = "2026-03-27T21:32:26.655Z" },
+ { url = "https://files.pythonhosted.org/packages/45/48/32e383aa6bc40b72a9fd419937aaa647078190c9bfccdc97b316d2dee687/onnx-1.21.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce", size = 17968053, upload-time = "2026-03-27T21:32:29.558Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/26/5726e8df7d36e96bb3c679912d1a86af42f393d77aa17d6b98a97d4289ce/onnx-1.21.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974", size = 17534821, upload-time = "2026-03-27T21:32:32.351Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2b/021dcd2dd50c3c71b7959d7368526da384a295c162fb4863f36057973f78/onnx-1.21.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda", size = 17616664, upload-time = "2026-03-27T21:32:34.921Z" },
+ { url = "https://files.pythonhosted.org/packages/12/00/afa32a46fa122a7ed42df1cfe8796922156a3725ba8fc581c4779c96e2fc/onnx-1.21.0-cp311-cp311-win32.whl", hash = "sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27", size = 16289035, upload-time = "2026-03-27T21:32:37.783Z" },
+ { url = "https://files.pythonhosted.org/packages/73/8d/483cc980a24d4c0131d0af06d0ff6a37fb08ae90a7848ece8cef645194f1/onnx-1.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d", size = 16443748, upload-time = "2026-03-27T21:32:40.337Z" },
+ { url = "https://files.pythonhosted.org/packages/38/78/9d06fd5aaaed1ec9cb8a3b70fbbf00c1bdc18db610771e96379f0ed58112/onnx-1.21.0-cp311-cp311-win_arm64.whl", hash = "sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d", size = 16406123, upload-time = "2026-03-27T21:32:45.159Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" },
+ { url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/89/0e1a9beb536401e2f45ac88735e123f2735e12fc7b56ff6c11727e097526/onnx-1.21.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f", size = 17975430, upload-time = "2026-03-27T21:33:07.005Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/46/e6dc71a7b3b317265591b20a5f71d0ff5c0d26c24e52283139dc90c66038/onnx-1.21.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8", size = 17537435, upload-time = "2026-03-27T21:33:09.765Z" },
+ { url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.24.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "flatbuffers", marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "packaging", marker = "python_full_version < '3.11'" },
+ { name = "protobuf", marker = "python_full_version < '3.11'" },
+ { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "sympy", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" },
+ { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" },
+ { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" },
+ { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" },
+ { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" },
+ { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.24.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "flatbuffers", marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+ { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" },
+ { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" },
+ { url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" },
+ { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" },
+ { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" },
+ { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" },
+]
+
+[[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.11' and platform_machine == 'x86_64'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { 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" },
+]
+
+[[package]]
+name = "pandas"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.4", 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/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" },
+ { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" },
+ { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" },
+ { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" },
+ { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" },
+ { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" },
+ { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" },
+ { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" },
+ { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" },
+ { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
+ { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
+ { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
+ { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
+ { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
+ { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
+ { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
+ { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
+ { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
+ { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
+ { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
+ { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
+ { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
+]
+
+[[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 = "pre-commit"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "7.34.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" },
+ { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" },
+ { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" },
+ { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" },
+ { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[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 = "pytest"
+version = "9.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
+]
+
+[[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 = "python-discovery"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2026.1.post1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
+]
+
+[[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" },
+]
+
+[[package]]
+name = "reportlab"
+version = "4.4.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "charset-normalizer" },
+ { name = "pillow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.15.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
+ { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
+ { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
+ { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
+ { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
+ { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "81.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
+]
+
+[[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 = "sympy"
+version = "1.13.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "mpmath", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040, upload-time = "2024-07-19T09:26:51.238Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177, upload-time = "2024-07-19T09:26:48.863Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "mpmath", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.2.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "filelock", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "fsspec", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "jinja2", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", version = "8.9.2.26", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", version = "11.0.2.54", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", version = "10.3.2.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", version = "11.4.5.107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", version = "12.1.0.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", version = "2.19.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "sympy", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+ { name = "triton", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/b3/1fcc3bccfddadfd6845dcbfe26eb4b099f1dfea5aa0e5cfb92b3c98dba5b/torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585", size = 755526581, upload-time = "2024-03-27T21:06:46.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/81/684d99e536b20e869a7c1222cf1dd233311fb05d3628e9570992bfb65760/torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5", size = 198579616, upload-time = "2024-03-27T21:10:15.41Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/55/7192974ab13e5e5577f45d14ce70d42f5a9a686b4f57bbe8c9ab45c4a61a/torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e", size = 150788930, upload-time = "2024-03-27T21:08:09.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/33/d7a6123231bd4d04c7005dde8507235772f3bc4622a25f3a88c016415d49/torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb", size = 755555407, upload-time = "2024-03-27T21:09:48.166Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/01/5ab75f138bf32d7a69df61e4997e24eccad87cc009f5fb7e2a31af8a4036/torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c", size = 198584125, upload-time = "2024-03-27T21:10:06.958Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/14/e105b8ef6d324e789c1589e95cb0ab63f3e07c2216d68b1178b7c21b7d2a/torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059", size = 150796474, upload-time = "2024-03-27T21:09:29.142Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/0c/d8f77363a7a3350c96e6c9db4ffb101d1c0487cc0b8cdaae1e4bfb2800ad/torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca", size = 755466713, upload-time = "2024-03-27T21:08:48.868Z" },
+ { url = "https://files.pythonhosted.org/packages/72/ce/beca89dcdcf4323880d3b959ef457a4c61a95483af250e6892fec9174162/torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea", size = 198528804, upload-time = "2024-03-27T21:09:14.691Z" },
+ { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270, upload-time = "2024-03-27T21:08:29.623Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.6.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "filelock", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+ { name = "fsspec", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+ { name = "jinja2", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" },
+ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", version = "9.1.0.70", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", version = "11.2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", version = "10.3.5.147", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", version = "11.6.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", version = "12.3.1.170", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", version = "2.21.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools", marker = "python_full_version >= '3.12'" },
+ { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+ { name = "triton", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561, upload-time = "2025-01-29T16:19:12.12Z" },
+ { url = "https://files.pythonhosted.org/packages/86/86/e661e229df2f5bfc6eab4c97deb1286d598bbeff31ab0cdb99b3c0d53c6f/torch-2.6.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c4f103a49830ce4c7561ef4434cc7926e5a5fe4e5eb100c19ab36ea1e2b634ab", size = 95751887, upload-time = "2025-01-29T16:27:50.77Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e0/5cb2f8493571f0a5a7273cd7078f191ac252a402b5fb9cb6091f14879109/torch-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:56eeaf2ecac90da5d9e35f7f35eb286da82673ec3c582e310a8d1631a1c02341", size = 204165139, upload-time = "2025-01-29T16:27:11.63Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/16/ea1b7842413a7b8a5aaa5e99e8eaf3da3183cc3ab345ad025a07ff636301/torch-2.6.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:09e06f9949e1a0518c5b09fe95295bc9661f219d9ecb6f9893e5123e10696628", size = 66520221, upload-time = "2025-01-29T16:22:18.862Z" },
+ { url = "https://files.pythonhosted.org/packages/78/a9/97cbbc97002fff0de394a2da2cdfa859481fdca36996d7bd845d50aa9d8d/torch-2.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:7979834102cd5b7a43cc64e87f2f3b14bd0e1458f06e9f88ffa386d07c7446e1", size = 766715424, upload-time = "2025-01-29T16:25:15.874Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fa/134ce8f8a7ea07f09588c9cc2cea0d69249efab977707cf67669431dcf5c/torch-2.6.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd0320411fe1a3b3fec7b4d3185aa7d0c52adac94480ab024b5c8f74a0bf1d", size = 95759416, upload-time = "2025-01-29T16:27:38.429Z" },
+ { url = "https://files.pythonhosted.org/packages/11/c5/2370d96b31eb1841c3a0883a492c15278a6718ccad61bb6a649c80d1d9eb/torch-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:46763dcb051180ce1ed23d1891d9b1598e07d051ce4c9d14307029809c4d64f7", size = 204164970, upload-time = "2025-01-29T16:26:16.182Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/fa/f33a4148c6fb46ca2a3f8de39c24d473822d5774d652b66ed9b1214da5f7/torch-2.6.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:94fc63b3b4bedd327af588696559f68c264440e2503cc9e6954019473d74ae21", size = 66530713, upload-time = "2025-01-29T16:26:38.881Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/35/0c52d708144c2deb595cd22819a609f78fdd699b95ff6f0ebcd456e3c7c1/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9", size = 766624563, upload-time = "2025-01-29T16:23:19.084Z" },
+ { url = "https://files.pythonhosted.org/packages/01/d6/455ab3fbb2c61c71c8842753b566012e1ed111e7a4c82e0e1c20d0c76b62/torch-2.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb", size = 95607867, upload-time = "2025-01-29T16:25:55.649Z" },
+ { url = "https://files.pythonhosted.org/packages/18/cf/ae99bd066571656185be0d88ee70abc58467b76f2f7c8bfeb48735a71fe6/torch-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239", size = 204120469, upload-time = "2025-01-29T16:24:01.821Z" },
+ { url = "https://files.pythonhosted.org/packages/81/b4/605ae4173aa37fb5aa14605d100ff31f4f5d49f617928c9f486bb3aaec08/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989", size = 66532538, upload-time = "2025-01-29T16:24:18.976Z" },
+ { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191, upload-time = "2025-01-29T16:17:26.26Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439, upload-time = "2025-01-29T16:21:21.061Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475, upload-time = "2025-01-29T16:21:55.394Z" },
+ { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783, upload-time = "2025-01-29T16:22:08.559Z" },
+]
+
+[[package]]
+name = "triton"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64'",
+]
+dependencies = [
+ { name = "filelock", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/05/ed974ce87fe8c8843855daa2136b3409ee1c126707ab54a8b72815c08b49/triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5", size = 167900779, upload-time = "2024-01-10T03:11:56.576Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/ac/3974caaa459bf2c3a244a84be8d17561f631f7d42af370fc311defeca2fb/triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0", size = 167928356, upload-time = "2024-01-10T03:12:05.923Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/49/2e1bbae4542b8f624e409540b4197e37ab22a88e8685e99debe721cc2b50/triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5", size = 167933985, upload-time = "2024-01-10T03:12:14.556Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/65/3ffa90e158a2c82f0716eee8d26a725d241549b7d7aaf7e4f44ac03ebd89/triton-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e54983cd51875855da7c68ec05c05cf8bb08df361b1d5b69e05e40b0c9bd62", size = 253090354, upload-time = "2025-01-22T19:12:21.872Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/2e/757d2280d4fefe7d33af7615124e7e298ae7b8e3bc4446cdb8e88b0f9bab/triton-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8009a1fb093ee8546495e96731336a33fb8856a38e45bb4ab6affd6dbc3ba220", size = 253157636, upload-time = "2025-01-22T19:12:51.322Z" },
+ { url = "https://files.pythonhosted.org/packages/06/00/59500052cb1cf8cf5316be93598946bc451f14072c6ff256904428eaf03c/triton-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c", size = 253159365, upload-time = "2025-01-22T19:13:24.648Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278, upload-time = "2025-01-22T19:13:54.221Z" },
+]
+
+[[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 = "2026.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "21.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" },
+]