diff --git a/.github/workflows/export_tutorials.yml b/.github/workflows/export_tutorials.yml new file mode 100644 index 000000000..4b009a122 --- /dev/null +++ b/.github/workflows/export_tutorials.yml @@ -0,0 +1,72 @@ +name: "Export Tutorials" + +on: + push: + branches: + - "**" # Run on push on all branches + paths: + - 'tutorials/**/*.ipynb' +jobs: + export_tutorials: + permissions: write-all + runs-on: ubuntu-latest + env: + TUTORIAL_TIMEOUT: 1200s + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + # Dependencies for tutorials + python3 -m pip install --upgrade pip .[tutorial] black[jupyter] + - name: Setup FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v2 + + - id: files + uses: jitterbit/get-changed-files@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + format: space-delimited + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - name: Export tutorials to .py and .html + run: | + set -x + for file in ${{ steps.files.outputs.all }}; do + if [[ $file == *.ipynb ]]; then + filename=$(basename $file) + pyfilename=$(echo ${filename%?????})py + timeout --signal=SIGKILL $TUTORIAL_TIMEOUT python -Xfrozen_modules=off -m jupyter nbconvert $file --to python --output $pyfilename --output-dir=$(dirname $file) + htmlfilename=$(echo ${filename%?????} | sed -e 's/-//g')html + htmldir="docs/source"/$(echo ${file%??????????????} | sed -e 's/-//g') + timeout --signal=SIGKILL $TUTORIAL_TIMEOUT python -Xfrozen_modules=off -m jupyter nbconvert --execute $file --to html --output $htmlfilename --output-dir=$htmldir + fi + done + set +x + + - name: Run formatter + run: black tutorials/ + + - uses: benjlevesque/short-sha@v2.1 + id: short-sha + + - name: Remove unwanted files + run: | + rm -rf build/ tutorials/tutorial4/data/ + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5.0.2 + with: + labels: maintenance + title: Export tutorial changed in ${{ steps.short-sha.outputs.sha }} + branch: export-tutorial-${{ steps.short-sha.outputs.sha }} + base: ${{ github.head_ref }} + commit-message: export tutorials changed in ${{ steps.short-sha.outputs.sha }} + delete-branch: true diff --git a/.github/workflows/testing_doc.yml b/.github/workflows/testing_doc.yml index e8b716dfa..5c368fb69 100644 --- a/.github/workflows/testing_doc.yml +++ b/.github/workflows/testing_doc.yml @@ -1,33 +1,21 @@ name: Test Sphinx Documentation Build on: - push: - branches: - - "master" - paths: - - 'docs/**' pull_request: branches: - "master" + - "0.2" paths: - 'docs/**' jobs: docs: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Set up Python + - name: Checkout Doc uses: ammaraskar/sphinx-action@7.4.7 with: - pre-build-command: "python3 -m pip install .[docs]" - docs-folder: "docs/" - - - name: Build Sphinx documentation - run: | - cd docs - make html - + pre-build-command: "python3 -m pip install .[doc]" + docs-folder: "docs/" \ No newline at end of file diff --git a/docs/source/_cite.rst b/docs/source/_cite.rst index 71d537931..786134b5b 100644 --- a/docs/source/_cite.rst +++ b/docs/source/_cite.rst @@ -1,7 +1,7 @@ Cite PINA ============== -If PINA has been significant in your research, and you would like to acknowledge the project in your academic publication, +If **PINA** has been significant in your research, and you would like to acknowledge the project in your academic publication, we suggest citing the following paper: *Coscia, D., Ivagnes, A., Demo, N., & Rozza, G. (2023). Physics-Informed Neural networks for Advanced modeling. Journal of Open Source Software, 8(87), 5352.* diff --git a/docs/source/_contributing.rst b/docs/source/_contributing.rst new file mode 100644 index 000000000..dbc06912b --- /dev/null +++ b/docs/source/_contributing.rst @@ -0,0 +1,100 @@ +Contributing to PINA +===================== + +First off, thanks for taking the time to contribute to **PINA**! 🎉 Your help makes the project better for everyone. This document outlines the process for contributing, reporting issues, suggesting features, and submitting pull requests. + +Table of Contents +------------------------ + +1. `How to Contribute`_ +2. `Reporting Bugs`_ +3. `Suggesting Enhancements`_ +4. `Pull Request Process`_ +5. `Code Style & Guidelines`_ +6. `Community Standards`_ + +How to Contribute +------------------------ + +You can contribute in several ways: + +- Reporting bugs +- Suggesting features/enhancements +- Submitting fixes or improvements via Pull Requests (PRs) +- Improving documentation + +We encourage all contributions, big or small! + +Reporting Bugs +------------------------ + +If you find a bug, please open an `issue `_ and include: + +- A clear and descriptive title +- Steps to reproduce the problem +- What you expected to happen +- What actually happened +- Any relevant logs, screenshots, or error messages +- Environment info (OS, Python version, dependencies, etc.) + +Suggesting Enhancements +------------------------ + +We welcome new ideas! If you have an idea to improve PINA: + +1. Check the `issue tracker `_ or the `discussions `_ to see if someone has already suggested it. +2. If not, open a new issue describing: + - The enhancement you'd like + - Why it would be useful + - Any ideas on how to implement it (optional but helpful) +3. If you are not sure about (something of) the enhancement, we suggest opening a discussion to collaborate on it with the PINA community. + +Pull Request Process +------------------------ + +Before submitting a PR: + +1. Ensure there’s an open issue related to your contribution (or create one). +2. `Fork `_ the repository and create a new branch from ``master``: + + .. code-block:: bash + + git checkout -b feature/my-feature + +3. Make your changes: + - Write clear, concise, and well-documented code + - Add or update tests where appropriate + - Update documentation if necessary +4. Verify your changes by running tests: + + .. code-block:: bash + + pytest + +5. Properly format your code. If you want to save time, simply run: + + .. code-block:: bash + + bash code_formatter.sh + +7. Submit a `pull request `_ with a clear explanation of your changes and reference the related issue if applicable. + +Pull Request Checklist + +1. Code follows the project’s style guidelines +2. Tests have been added or updated +3. Documentation has been updated if necessary +4. Pull request is linked to an open issue (if applicable) + +Code Style & Guidelines +------------------------ + +- Follow PEP8 for Python code. +- Use descriptive commit messages (e.g. ``Fix parser crash on empty input``). +- Write clear docstrings for public classes, methods, and functions. +- Keep functions small and focused; do one thing and do it well. + +Community Standards +------------------------ + +By participating in this project, you agree to abide by our Code of Conduct. We are committed to maintaining a welcoming and inclusive community. diff --git a/docs/source/_rst/_installation.rst b/docs/source/_installation.rst similarity index 100% rename from docs/source/_rst/_installation.rst rename to docs/source/_installation.rst diff --git a/docs/source/_rst/_code.rst b/docs/source/_rst/_code.rst index 16a42986f..c7cf79c39 100644 --- a/docs/source/_rst/_code.rst +++ b/docs/source/_rst/_code.rst @@ -11,22 +11,53 @@ The high-level structure of the package is depicted in our API. The pipeline to solve differential equations with PINA follows just five steps: - 1. Define the `Problem`_ the user aim to solve - 2. Generate data using built in `Geometries`_, or load high level simulation results as :doc:`LabelTensor ` + 1. Define the `Problems`_ the user aim to solve + 2. Generate data using built in `Geometrical Domains`_, or load high level simulation results as :doc:`LabelTensor ` 3. Choose or build one or more `Models`_ to solve the problem - 4. Choose a solver across PINA available `Solvers`_, or build one using the :doc:`SolverInterface ` - 5. Train the model with the PINA :doc:`Trainer `, enhance the train with `Callbacks`_ + 4. Choose a solver across PINA available `Solvers`_, or build one using the :doc:`SolverInterface ` + 5. Train the model with the PINA :doc:`Trainer `, enhance the train with `Callbacks`_ -PINA Features --------------- + +Trainer, Dataset and Datamodule +-------------------------------- .. toctree:: :titlesonly: - LabelTensor - Condition Trainer - Plotter + Dataset + DataModule + +Data Types +------------ +.. toctree:: + :titlesonly: + + LabelTensor + Graph + LabelBatch + +Graphs Structures +------------------ +.. toctree:: + :titlesonly: + + GraphBuilder + RadiusGraph + KNNGraph + + +Conditions +------------- +.. toctree:: + :titlesonly: + + ConditionInterface + Condition + DataCondition + DomainEquationCondition + InputEquationCondition + InputTargetCondition Solvers -------------- @@ -34,17 +65,19 @@ Solvers .. toctree:: :titlesonly: - SolverInterface - PINNInterface - PINN - GPINN - CausalPINN - CompetitivePINN - SAPINN - RBAPINN - Supervised solver - ReducedOrderModelSolver - GAROM + SolverInterface + SingleSolverInterface + MultiSolverInterface + PINNInterface + PINN + GradientPINN + CausalPINN + CompetitivePINN + SelfAdaptivePINN + RBAPINN + SupervisedSolver + ReducedOrderModelSolver + GAROM Models @@ -54,36 +87,60 @@ Models :titlesonly: :maxdepth: 5 - Network - KernelNeuralOperator - FeedForward - MultiFeedForward - ResidualFeedForward - Spline - DeepONet - MIONet - FourierIntegralKernel - FNO - AveragingNeuralOperator - LowRankNeuralOperator - -Layers + FeedForward + MultiFeedForward + ResidualFeedForward + Spline + DeepONet + MIONet + KernelNeuralOperator + FourierIntegralKernel + FNO + AveragingNeuralOperator + LowRankNeuralOperator + GraphNeuralOperator + GraphNeuralKernel + +Blocks ------------- .. toctree:: :titlesonly: - Residual layer - EnhancedLinear layer - Spectral convolution - Fourier layers - Averaging layer - Low Rank layer - Continuous convolution - Proper Orthogonal Decomposition - Periodic Boundary Condition Embedding - Fourier Feature Embedding - Radial Basis Function Interpolation + Residual Block + EnhancedLinear Block + Spectral Convolution Block + Fourier Block + Averaging Block + Low Rank Block + Graph Neural Operator Block + Continuous Convolution Interface + Continuous Convolution Block + Orthogonal Block + + +Reduction and Embeddings +-------------------------- + +.. toctree:: + :titlesonly: + + Proper Orthogonal Decomposition + Periodic Boundary Condition Embedding + Fourier Feature Embedding + Radial Basis Function Interpolation + +Optimizers and Schedulers +-------------------------- + +.. toctree:: + :titlesonly: + + Optimizer + Scheduler + TorchOptimizer + TorchScheduler + Adaptive Activation Functions ------------------------------- @@ -91,77 +148,97 @@ Adaptive Activation Functions .. toctree:: :titlesonly: - Adaptive Function Interface - Adaptive ReLU - Adaptive Sigmoid - Adaptive Tanh - Adaptive SiLU - Adaptive Mish - Adaptive ELU - Adaptive CELU - Adaptive GELU - Adaptive Softmin - Adaptive Softmax - Adaptive SIREN - Adaptive Exp + Adaptive Function Interface + Adaptive ReLU + Adaptive Sigmoid + Adaptive Tanh + Adaptive SiLU + Adaptive Mish + Adaptive ELU + Adaptive CELU + Adaptive GELU + Adaptive Softmin + Adaptive Softmax + Adaptive SIREN + Adaptive Exp -Equations and Operators -------------------------- +Equations and Differential Operators +--------------------------------------- .. toctree:: :titlesonly: - Equations - Differential Operators + EquationInterface + Equation + SystemEquation + Equation Factory + Differential Operators -Problem +Problems -------------- .. toctree:: :titlesonly: - AbstractProblem - SpatialProblem - TimeDependentProblem - ParametricProblem + AbstractProblem + InverseProblem + ParametricProblem + SpatialProblem + TimeDependentProblem -Geometries ------------------ +Problems Zoo +-------------- + +.. toctree:: + :titlesonly: + + AdvectionProblem + AllenCahnProblem + DiffusionReactionProblem + HelmholtzProblem + InversePoisson2DSquareProblem + Poisson2DSquareProblem + SupervisedProblem + + +Geometrical Domains +-------------------- .. toctree:: :titlesonly: - Location - CartesianDomain - EllipsoidDomain - SimplexDomain + Domain + CartesianDomain + EllipsoidDomain + SimplexDomain -Geometry set operations ------------------------- +Domain Operations +------------------ .. toctree:: :titlesonly: - OperationInterface - Union - Intersection - Difference - Exclusion + OperationInterface + Union + Intersection + Difference + Exclusion Callbacks --------------------- +----------- .. toctree:: :titlesonly: - Processing Callbacks - Optimizer Callbacks - Adaptive Refinment Callback + Processing callback + Optimizer callback + Refinment callback + Weighting callback -Metrics and Losses --------------------- +Losses and Weightings +--------------------- .. toctree:: :titlesonly: @@ -169,3 +246,5 @@ Metrics and Losses LossInterface LpLoss PowerLoss + WeightingInterface + ScalarWeighting diff --git a/docs/source/_rst/_contributing.rst b/docs/source/_rst/_contributing.rst deleted file mode 100644 index d527a0ebe..000000000 --- a/docs/source/_rst/_contributing.rst +++ /dev/null @@ -1,37 +0,0 @@ -How to contribute -================= - -We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. - -Submitting a patch ------------------- - - 1. It's generally best to start by opening a new issue describing the bug or - feature you're intending to fix. Even if you think it's relatively minor, - it's helpful to know what people are working on. Mention in the initial - issue that you are planning to work on that bug or feature so that it can - be assigned to you. - - 2. Follow the normal process of forking the project, and setup a new - branch to work in. It's important that each group of changes be done in - separate branches in order to ensure that a pull request only includes the - commits related to that bug or feature. - - 3. To ensure properly formatted code, please make sure to use 4 - spaces to indent the code. The easy way is to run on your bash the provided - script: ./code_formatter.sh. You should also run pylint over your code. - It's not strictly necessary that your code be completely "lint-free", - but this will help you find common style issues. - - 4. Any significant changes should almost always be accompanied by tests. The - project already has good test coverage, so look at some of the existing - tests if you're unsure how to go about it. We're using coveralls that - is an invaluable tools for seeing which parts of your code aren't being - exercised by your tests. - - 5. Do your best to have well-formed commit messages for each change. - This provides consistency throughout the project, and ensures that commit - messages are able to be formatted properly by various git tools. - - 6. Finally, push the commits to your fork and submit a pull request. Please, - remember to rebase properly in order to maintain a clean, linear git history. diff --git a/docs/source/_rst/_tutorial.rst b/docs/source/_rst/_tutorial.rst deleted file mode 100644 index 4e2d20504..000000000 --- a/docs/source/_rst/_tutorial.rst +++ /dev/null @@ -1,46 +0,0 @@ -PINA Tutorials -============== - -In this folder we collect useful tutorials in order to understand the principles and the potential of **PINA**. - -Getting started with PINA -------------------------- -.. toctree:: - :maxdepth: 3 - :titlesonly: - - Introduction to PINA for Physics Informed Neural Networks training - Introduction to PINA Equation class - PINA and PyTorch Lightning, training tips and visualizations - Building custom geometries with PINA Location class - - -Physics Informed Neural Networks --------------------------------- -.. toctree:: - :maxdepth: 3 - :titlesonly: - - Two dimensional Poisson problem using Extra Features Learning - Two dimensional Wave problem with hard constraint - Resolution of a 2D Poisson inverse problem - Periodic Boundary Conditions for Helmotz Equation - Multiscale PDE learning with Fourier Feature Network - -Neural Operator Learning ------------------------- -.. toctree:: - :maxdepth: 3 - :titlesonly: - - Two dimensional Darcy flow using the Fourier Neural Operator - Time dependent Kuramoto Sivashinsky equation using the Averaging Neural Operator - -Supervised Learning -------------------- -.. toctree:: - :maxdepth: 3 - :titlesonly: - - Unstructured convolutional autoencoder via continuous convolution - POD-RBF and POD-NN for reduced order modeling diff --git a/docs/source/_rst/adaptive_function/AdaptiveActivationFunctionInterface.rst b/docs/source/_rst/adaptive_function/AdaptiveActivationFunctionInterface.rst new file mode 100644 index 000000000..cf8b6551d --- /dev/null +++ b/docs/source/_rst/adaptive_function/AdaptiveActivationFunctionInterface.rst @@ -0,0 +1,8 @@ +AdaptiveActivationFunctionInterface +======================================= + +.. currentmodule:: pina.adaptive_function.adaptive_function_interface + +.. automodule:: pina.adaptive_function.adaptive_function_interface + :members: + :show-inheritance: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveCELU.rst b/docs/source/_rst/adaptive_function/AdaptiveCELU.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveCELU.rst rename to docs/source/_rst/adaptive_function/AdaptiveCELU.rst index 9736ee631..c4d6d5429 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveCELU.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveCELU.rst @@ -1,7 +1,7 @@ AdaptiveCELU ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveCELU :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveELU.rst b/docs/source/_rst/adaptive_function/AdaptiveELU.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveELU.rst rename to docs/source/_rst/adaptive_function/AdaptiveELU.rst index ad04717f1..aab273b08 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveELU.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveELU.rst @@ -1,7 +1,7 @@ AdaptiveELU =========== -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveELU :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveExp.rst b/docs/source/_rst/adaptive_function/AdaptiveExp.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveExp.rst rename to docs/source/_rst/adaptive_function/AdaptiveExp.rst index 7d07cd52d..a7ee52b20 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveExp.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveExp.rst @@ -1,7 +1,7 @@ AdaptiveExp =========== -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveExp :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveGELU.rst b/docs/source/_rst/adaptive_function/AdaptiveGELU.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveGELU.rst rename to docs/source/_rst/adaptive_function/AdaptiveGELU.rst index 86e587584..b4aef14dc 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveGELU.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveGELU.rst @@ -1,7 +1,7 @@ AdaptiveGELU ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveGELU :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveMish.rst b/docs/source/_rst/adaptive_function/AdaptiveMish.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveMish.rst rename to docs/source/_rst/adaptive_function/AdaptiveMish.rst index 4e1e3b435..d006df054 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveMish.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveMish.rst @@ -1,7 +1,7 @@ AdaptiveMish ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveMish :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveReLU.rst b/docs/source/_rst/adaptive_function/AdaptiveReLU.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveReLU.rst rename to docs/source/_rst/adaptive_function/AdaptiveReLU.rst index ea08c29a9..d0fe4de68 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveReLU.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveReLU.rst @@ -1,7 +1,7 @@ AdaptiveReLU ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveReLU :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveSIREN.rst b/docs/source/_rst/adaptive_function/AdaptiveSIREN.rst similarity index 72% rename from docs/source/_rst/adaptive_functions/AdaptiveSIREN.rst rename to docs/source/_rst/adaptive_function/AdaptiveSIREN.rst index 96133bdd8..9f132547b 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveSIREN.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveSIREN.rst @@ -1,7 +1,7 @@ AdaptiveSIREN ============= -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveSIREN :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveSiLU.rst b/docs/source/_rst/adaptive_function/AdaptiveSiLU.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveSiLU.rst rename to docs/source/_rst/adaptive_function/AdaptiveSiLU.rst index 2f359fded..722678611 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveSiLU.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveSiLU.rst @@ -1,7 +1,7 @@ AdaptiveSiLU ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveSiLU :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveSigmoid.rst b/docs/source/_rst/adaptive_function/AdaptiveSigmoid.rst similarity index 72% rename from docs/source/_rst/adaptive_functions/AdaptiveSigmoid.rst rename to docs/source/_rst/adaptive_function/AdaptiveSigmoid.rst index 6f495a8ed..6002ffb31 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveSigmoid.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveSigmoid.rst @@ -1,7 +1,7 @@ AdaptiveSigmoid =============== -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveSigmoid :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveSoftmax.rst b/docs/source/_rst/adaptive_function/AdaptiveSoftmax.rst similarity index 72% rename from docs/source/_rst/adaptive_functions/AdaptiveSoftmax.rst rename to docs/source/_rst/adaptive_function/AdaptiveSoftmax.rst index 5cab9c65c..c2b4c9f09 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveSoftmax.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveSoftmax.rst @@ -1,7 +1,7 @@ AdaptiveSoftmax =============== -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveSoftmax :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveSoftmin.rst b/docs/source/_rst/adaptive_function/AdaptiveSoftmin.rst similarity index 72% rename from docs/source/_rst/adaptive_functions/AdaptiveSoftmin.rst rename to docs/source/_rst/adaptive_function/AdaptiveSoftmin.rst index a0e6c94ae..5189cb391 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveSoftmin.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveSoftmin.rst @@ -1,7 +1,7 @@ AdaptiveSoftmin =============== -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveSoftmin :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveTanh.rst b/docs/source/_rst/adaptive_function/AdaptiveTanh.rst similarity index 71% rename from docs/source/_rst/adaptive_functions/AdaptiveTanh.rst rename to docs/source/_rst/adaptive_function/AdaptiveTanh.rst index 3e486512f..9a9b380a3 100644 --- a/docs/source/_rst/adaptive_functions/AdaptiveTanh.rst +++ b/docs/source/_rst/adaptive_function/AdaptiveTanh.rst @@ -1,7 +1,7 @@ AdaptiveTanh ============ -.. currentmodule:: pina.adaptive_functions.adaptive_func +.. currentmodule:: pina.adaptive_function.adaptive_function .. autoclass:: AdaptiveTanh :members: diff --git a/docs/source/_rst/adaptive_functions/AdaptiveFunctionInterface.rst b/docs/source/_rst/adaptive_functions/AdaptiveFunctionInterface.rst deleted file mode 100644 index 7cdf754b7..000000000 --- a/docs/source/_rst/adaptive_functions/AdaptiveFunctionInterface.rst +++ /dev/null @@ -1,8 +0,0 @@ -AdaptiveActivationFunctionInterface -======================================= - -.. currentmodule:: pina.adaptive_functions.adaptive_func_interface - -.. automodule:: pina.adaptive_functions.adaptive_func_interface - :members: - :show-inheritance: diff --git a/docs/source/_rst/callback/adaptive_refinment_callback.rst b/docs/source/_rst/callback/adaptive_refinment_callback.rst new file mode 100644 index 000000000..8afad6571 --- /dev/null +++ b/docs/source/_rst/callback/adaptive_refinment_callback.rst @@ -0,0 +1,7 @@ +Refinments callbacks +======================= + +.. currentmodule:: pina.callback.adaptive_refinement_callback +.. autoclass:: R3Refinement + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/callback/linear_weight_update_callback.rst b/docs/source/_rst/callback/linear_weight_update_callback.rst new file mode 100644 index 000000000..fe45b56e2 --- /dev/null +++ b/docs/source/_rst/callback/linear_weight_update_callback.rst @@ -0,0 +1,7 @@ +Weighting callbacks +======================== + +.. currentmodule:: pina.callback.linear_weight_update_callback +.. autoclass:: LinearWeightUpdate + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/callbacks/optimizer_callbacks.rst b/docs/source/_rst/callback/optimizer_callback.rst similarity index 66% rename from docs/source/_rst/callbacks/optimizer_callbacks.rst rename to docs/source/_rst/callback/optimizer_callback.rst index 7ee418fac..0afdc2669 100644 --- a/docs/source/_rst/callbacks/optimizer_callbacks.rst +++ b/docs/source/_rst/callback/optimizer_callback.rst @@ -1,7 +1,7 @@ Optimizer callbacks ===================== -.. currentmodule:: pina.callbacks.optimizer_callbacks +.. currentmodule:: pina.callback.optimizer_callback .. autoclass:: SwitchOptimizer :members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/callbacks/processing_callbacks.rst b/docs/source/_rst/callback/processing_callback.rst similarity index 76% rename from docs/source/_rst/callbacks/processing_callbacks.rst rename to docs/source/_rst/callback/processing_callback.rst index bd3bbc840..a06bb8b17 100644 --- a/docs/source/_rst/callbacks/processing_callbacks.rst +++ b/docs/source/_rst/callback/processing_callback.rst @@ -1,7 +1,7 @@ Processing callbacks ======================= -.. currentmodule:: pina.callbacks.processing_callbacks +.. currentmodule:: pina.callback.processing_callback .. autoclass:: MetricTracker :members: :show-inheritance: diff --git a/docs/source/_rst/callbacks/adaptive_refinment_callbacks.rst b/docs/source/_rst/callbacks/adaptive_refinment_callbacks.rst deleted file mode 100644 index 11b313ee0..000000000 --- a/docs/source/_rst/callbacks/adaptive_refinment_callbacks.rst +++ /dev/null @@ -1,7 +0,0 @@ -Adaptive Refinments callbacks -=============================== - -.. currentmodule:: pina.callbacks.adaptive_refinment_callbacks -.. autoclass:: R3Refinement - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition.rst b/docs/source/_rst/condition.rst deleted file mode 100644 index 088b966d6..000000000 --- a/docs/source/_rst/condition.rst +++ /dev/null @@ -1,7 +0,0 @@ -Condition -========= -.. currentmodule:: pina.condition - -.. autoclass:: Condition - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/condition.rst b/docs/source/_rst/condition/condition.rst new file mode 100644 index 000000000..51edfafff --- /dev/null +++ b/docs/source/_rst/condition/condition.rst @@ -0,0 +1,7 @@ +Conditions +============= +.. currentmodule:: pina.condition.condition + +.. autoclass:: Condition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/condition_interface.rst b/docs/source/_rst/condition/condition_interface.rst new file mode 100644 index 000000000..88459629b --- /dev/null +++ b/docs/source/_rst/condition/condition_interface.rst @@ -0,0 +1,7 @@ +ConditionInterface +====================== +.. currentmodule:: pina.condition.condition_interface + +.. autoclass:: ConditionInterface + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/data_condition.rst b/docs/source/_rst/condition/data_condition.rst new file mode 100644 index 000000000..b7c322ea1 --- /dev/null +++ b/docs/source/_rst/condition/data_condition.rst @@ -0,0 +1,15 @@ +Data Conditions +================== +.. currentmodule:: pina.condition.data_condition + +.. autoclass:: DataCondition + :members: + :show-inheritance: + +.. autoclass:: GraphDataCondition + :members: + :show-inheritance: + +.. autoclass:: TensorDataCondition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/domain_equation_condition.rst b/docs/source/_rst/condition/domain_equation_condition.rst new file mode 100644 index 000000000..505c8b839 --- /dev/null +++ b/docs/source/_rst/condition/domain_equation_condition.rst @@ -0,0 +1,7 @@ +Domain Equation Condition +=========================== +.. currentmodule:: pina.condition.domain_equation_condition + +.. autoclass:: DomainEquationCondition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/input_equation_condition.rst b/docs/source/_rst/condition/input_equation_condition.rst new file mode 100644 index 000000000..4f5450e93 --- /dev/null +++ b/docs/source/_rst/condition/input_equation_condition.rst @@ -0,0 +1,15 @@ +Input Equation Condition +=========================== +.. currentmodule:: pina.condition.input_equation_condition + +.. autoclass:: InputEquationCondition + :members: + :show-inheritance: + +.. autoclass:: InputTensorEquationCondition + :members: + :show-inheritance: + +.. autoclass:: InputGraphEquationCondition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/condition/input_target_condition.rst b/docs/source/_rst/condition/input_target_condition.rst new file mode 100644 index 000000000..960b7d6f4 --- /dev/null +++ b/docs/source/_rst/condition/input_target_condition.rst @@ -0,0 +1,23 @@ +Input Target Condition +=========================== +.. currentmodule:: pina.condition.input_target_condition + +.. autoclass:: InputTargetCondition + :members: + :show-inheritance: + +.. autoclass:: TensorInputTensorTargetCondition + :members: + :show-inheritance: + +.. autoclass:: TensorInputGraphTargetCondition + :members: + :show-inheritance: + +.. autoclass:: GraphInputTensorTargetCondition + :members: + :show-inheritance: + +.. autoclass:: GraphInputGraphTargetCondition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/data/data_module.rst b/docs/source/_rst/data/data_module.rst new file mode 100644 index 000000000..b7ffb14e0 --- /dev/null +++ b/docs/source/_rst/data/data_module.rst @@ -0,0 +1,15 @@ +DataModule +====================== +.. currentmodule:: pina.data.data_module + +.. autoclass:: Collator + :members: + :show-inheritance: + +.. autoclass:: PinaDataModule + :members: + :show-inheritance: + +.. autoclass:: PinaSampler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/data/dataset.rst b/docs/source/_rst/data/dataset.rst new file mode 100644 index 000000000..b49b41db1 --- /dev/null +++ b/docs/source/_rst/data/dataset.rst @@ -0,0 +1,19 @@ +Dataset +====================== +.. currentmodule:: pina.data.dataset + +.. autoclass:: PinaDataset + :members: + :show-inheritance: + +.. autoclass:: PinaDatasetFactory + :members: + :show-inheritance: + +.. autoclass:: PinaGraphDataset + :members: + :show-inheritance: + +.. autoclass:: PinaTensorDataset + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/geometry/cartesian.rst b/docs/source/_rst/domain/cartesian.rst similarity index 59% rename from docs/source/_rst/geometry/cartesian.rst rename to docs/source/_rst/domain/cartesian.rst index b57c02bb4..97f5e8974 100644 --- a/docs/source/_rst/geometry/cartesian.rst +++ b/docs/source/_rst/domain/cartesian.rst @@ -1,8 +1,8 @@ CartesianDomain ====================== -.. currentmodule:: pina.geometry.cartesian +.. currentmodule:: pina.domain.cartesian -.. automodule:: pina.geometry.cartesian +.. automodule:: pina.domain.cartesian .. autoclass:: CartesianDomain :members: diff --git a/docs/source/_rst/geometry/difference_domain.rst b/docs/source/_rst/domain/difference_domain.rst similarity index 50% rename from docs/source/_rst/geometry/difference_domain.rst rename to docs/source/_rst/domain/difference_domain.rst index fc0b29377..f25daa522 100644 --- a/docs/source/_rst/geometry/difference_domain.rst +++ b/docs/source/_rst/domain/difference_domain.rst @@ -1,8 +1,8 @@ Difference ====================== -.. currentmodule:: pina.geometry.difference_domain +.. currentmodule:: pina.domain.difference_domain -.. automodule:: pina.geometry.difference_domain +.. automodule:: pina.domain.difference_domain .. autoclass:: Difference :members: diff --git a/docs/source/_rst/domain/domain.rst b/docs/source/_rst/domain/domain.rst new file mode 100644 index 000000000..27adcf0bc --- /dev/null +++ b/docs/source/_rst/domain/domain.rst @@ -0,0 +1,9 @@ +Domain +=========== +.. currentmodule:: pina.domain.domain_interface + +.. automodule:: pina.domain.domain_interface + +.. autoclass:: DomainInterface + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/geometry/ellipsoid.rst b/docs/source/_rst/domain/ellipsoid.rst similarity index 59% rename from docs/source/_rst/geometry/ellipsoid.rst rename to docs/source/_rst/domain/ellipsoid.rst index 09af427ba..ee0d2b7a4 100644 --- a/docs/source/_rst/geometry/ellipsoid.rst +++ b/docs/source/_rst/domain/ellipsoid.rst @@ -1,8 +1,8 @@ EllipsoidDomain ====================== -.. currentmodule:: pina.geometry.ellipsoid +.. currentmodule:: pina.domain.ellipsoid -.. automodule:: pina.geometry.ellipsoid +.. automodule:: pina.domain.ellipsoid .. autoclass:: EllipsoidDomain :members: diff --git a/docs/source/_rst/geometry/exclusion_domain.rst b/docs/source/_rst/domain/exclusion_domain.rst similarity index 50% rename from docs/source/_rst/geometry/exclusion_domain.rst rename to docs/source/_rst/domain/exclusion_domain.rst index a07aafca1..8d18be199 100644 --- a/docs/source/_rst/geometry/exclusion_domain.rst +++ b/docs/source/_rst/domain/exclusion_domain.rst @@ -1,8 +1,8 @@ Exclusion ====================== -.. currentmodule:: pina.geometry.exclusion_domain +.. currentmodule:: pina.domain.exclusion_domain -.. automodule:: pina.geometry.exclusion_domain +.. automodule:: pina.domain.exclusion_domain .. autoclass:: Exclusion :members: diff --git a/docs/source/_rst/geometry/intersection_domain.rst b/docs/source/_rst/domain/intersection_domain.rst similarity index 50% rename from docs/source/_rst/geometry/intersection_domain.rst rename to docs/source/_rst/domain/intersection_domain.rst index a3c1356aa..8b2498661 100644 --- a/docs/source/_rst/geometry/intersection_domain.rst +++ b/docs/source/_rst/domain/intersection_domain.rst @@ -1,8 +1,8 @@ Intersection ====================== -.. currentmodule:: pina.geometry.intersection_domain +.. currentmodule:: pina.domain.intersection_domain -.. automodule:: pina.geometry.intersection_domain +.. automodule:: pina.domain.intersection_domain .. autoclass:: Intersection :members: diff --git a/docs/source/_rst/geometry/operation_interface.rst b/docs/source/_rst/domain/operation_interface.rst similarity index 52% rename from docs/source/_rst/geometry/operation_interface.rst rename to docs/source/_rst/domain/operation_interface.rst index 00a2d8467..0acd393dc 100644 --- a/docs/source/_rst/geometry/operation_interface.rst +++ b/docs/source/_rst/domain/operation_interface.rst @@ -1,8 +1,8 @@ OperationInterface ====================== -.. currentmodule:: pina.geometry.operation_interface +.. currentmodule:: pina.domain.operation_interface -.. automodule:: pina.geometry.operation_interface +.. automodule:: pina.domain.operation_interface .. autoclass:: OperationInterface :members: diff --git a/docs/source/_rst/geometry/simplex.rst b/docs/source/_rst/domain/simplex.rst similarity index 60% rename from docs/source/_rst/geometry/simplex.rst rename to docs/source/_rst/domain/simplex.rst index b5a83e44e..7accd7f84 100644 --- a/docs/source/_rst/geometry/simplex.rst +++ b/docs/source/_rst/domain/simplex.rst @@ -1,8 +1,8 @@ SimplexDomain ====================== -.. currentmodule:: pina.geometry.simplex +.. currentmodule:: pina.domain.simplex -.. automodule:: pina.geometry.simplex +.. automodule:: pina.domain.simplex .. autoclass:: SimplexDomain :members: diff --git a/docs/source/_rst/geometry/union_domain.rst b/docs/source/_rst/domain/union_domain.rst similarity index 50% rename from docs/source/_rst/geometry/union_domain.rst rename to docs/source/_rst/domain/union_domain.rst index ad172d792..921e430cf 100644 --- a/docs/source/_rst/geometry/union_domain.rst +++ b/docs/source/_rst/domain/union_domain.rst @@ -1,8 +1,8 @@ Union ====================== -.. currentmodule:: pina.geometry.union_domain +.. currentmodule:: pina.domain.union_domain -.. automodule:: pina.geometry.union_domain +.. automodule:: pina.domain.union_domain .. autoclass:: Union :members: diff --git a/docs/source/_rst/equation/equation.rst b/docs/source/_rst/equation/equation.rst new file mode 100644 index 000000000..33e19c957 --- /dev/null +++ b/docs/source/_rst/equation/equation.rst @@ -0,0 +1,7 @@ +Equation +========== + +.. currentmodule:: pina.equation.equation +.. autoclass:: Equation + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equation/equation_factory.rst b/docs/source/_rst/equation/equation_factory.rst new file mode 100644 index 000000000..cf5d430d3 --- /dev/null +++ b/docs/source/_rst/equation/equation_factory.rst @@ -0,0 +1,19 @@ +Equation Factory +================== + +.. currentmodule:: pina.equation.equation_factory +.. autoclass:: FixedValue + :members: + :show-inheritance: + +.. autoclass:: FixedGradient + :members: + :show-inheritance: + +.. autoclass:: FixedFlux + :members: + :show-inheritance: + +.. autoclass:: Laplace + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equation/equation_interface.rst b/docs/source/_rst/equation/equation_interface.rst new file mode 100644 index 000000000..cde7b0012 --- /dev/null +++ b/docs/source/_rst/equation/equation_interface.rst @@ -0,0 +1,7 @@ +Equation Interface +==================== + +.. currentmodule:: pina.equation.equation_interface +.. autoclass:: EquationInterface + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equation/system_equation.rst b/docs/source/_rst/equation/system_equation.rst new file mode 100644 index 000000000..33c931cd9 --- /dev/null +++ b/docs/source/_rst/equation/system_equation.rst @@ -0,0 +1,7 @@ +System Equation +================= + +.. currentmodule:: pina.equation.system_equation +.. autoclass:: SystemEquation + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equations.rst b/docs/source/_rst/equations.rst deleted file mode 100644 index 6826dde21..000000000 --- a/docs/source/_rst/equations.rst +++ /dev/null @@ -1,42 +0,0 @@ -Equations -========== -Equations are used in PINA to make easy the training. During problem definition -each `equation` passed to a `Condition` object must be an `Equation` or `SystemEquation`. -An `Equation` is simply a wrapper over callable python functions, while `SystemEquation` is -a wrapper arounf a list of callable python functions. We provide a wide rage of already implemented -equations to ease the code writing, such as `FixedValue`, `Laplace`, and many more. - - -.. currentmodule:: pina.equation.equation_interface -.. autoclass:: EquationInterface - :members: - :show-inheritance: - -.. currentmodule:: pina.equation.equation -.. autoclass:: Equation - :members: - :show-inheritance: - - -.. currentmodule:: pina.equation.system_equation -.. autoclass:: SystemEquation - :members: - :show-inheritance: - - -.. currentmodule:: pina.equation.equation_factory -.. autoclass:: FixedValue - :members: - :show-inheritance: - -.. autoclass:: FixedGradient - :members: - :show-inheritance: - -.. autoclass:: FixedFlux - :members: - :show-inheritance: - -.. autoclass:: Laplace - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/geometry/location.rst b/docs/source/_rst/geometry/location.rst deleted file mode 100644 index 5d680a1e4..000000000 --- a/docs/source/_rst/geometry/location.rst +++ /dev/null @@ -1,9 +0,0 @@ -Location -==================== -.. currentmodule:: pina.geometry.location - -.. automodule:: pina.geometry.location - -.. autoclass:: Location - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/graph/graph.rst b/docs/source/_rst/graph/graph.rst new file mode 100644 index 000000000..1921f83e0 --- /dev/null +++ b/docs/source/_rst/graph/graph.rst @@ -0,0 +1,9 @@ +Graph +=========== +.. currentmodule:: pina.graph + + +.. autoclass:: Graph + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/graph/graph_builder.rst b/docs/source/_rst/graph/graph_builder.rst new file mode 100644 index 000000000..2508aecb7 --- /dev/null +++ b/docs/source/_rst/graph/graph_builder.rst @@ -0,0 +1,9 @@ +GraphBuilder +============== +.. currentmodule:: pina.graph + + +.. autoclass:: GraphBuilder + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/graph/knn_graph.rst b/docs/source/_rst/graph/knn_graph.rst new file mode 100644 index 000000000..8ef0b190b --- /dev/null +++ b/docs/source/_rst/graph/knn_graph.rst @@ -0,0 +1,9 @@ +KNNGraph +=========== +.. currentmodule:: pina.graph + + +.. autoclass:: KNNGraph + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/graph/label_batch.rst b/docs/source/_rst/graph/label_batch.rst new file mode 100644 index 000000000..7cd4d2684 --- /dev/null +++ b/docs/source/_rst/graph/label_batch.rst @@ -0,0 +1,9 @@ +LabelBatch +=========== +.. currentmodule:: pina.graph + + +.. autoclass:: LabelBatch + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/graph/radius_graph.rst b/docs/source/_rst/graph/radius_graph.rst new file mode 100644 index 000000000..7414d2dc1 --- /dev/null +++ b/docs/source/_rst/graph/radius_graph.rst @@ -0,0 +1,9 @@ +RadiusGraph +============= +.. currentmodule:: pina.graph + + +.. autoclass:: RadiusGraph + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/layers/avno_layer.rst b/docs/source/_rst/layers/avno_layer.rst deleted file mode 100644 index 38d7ccbe2..000000000 --- a/docs/source/_rst/layers/avno_layer.rst +++ /dev/null @@ -1,8 +0,0 @@ -Averaging layers -==================== -.. currentmodule:: pina.model.layers.avno_layer - -.. autoclass:: AVNOBlock - :members: - :show-inheritance: - :noindex: diff --git a/docs/source/_rst/layers/convolution.rst b/docs/source/_rst/layers/convolution.rst deleted file mode 100644 index 3089fea47..000000000 --- a/docs/source/_rst/layers/convolution.rst +++ /dev/null @@ -1,8 +0,0 @@ -Continuous convolution -========================= -.. currentmodule:: pina.model.layers.convolution_2d - -.. autoclass:: ContinuousConvBlock - :members: - :show-inheritance: - :noindex: diff --git a/docs/source/_rst/layers/enhanced_linear.rst b/docs/source/_rst/layers/enhanced_linear.rst deleted file mode 100644 index ba30960e6..000000000 --- a/docs/source/_rst/layers/enhanced_linear.rst +++ /dev/null @@ -1,8 +0,0 @@ -EnhancedLinear -================= -.. currentmodule:: pina.model.layers.residual - -.. autoclass:: EnhancedLinear - :members: - :show-inheritance: - :noindex: \ No newline at end of file diff --git a/docs/source/_rst/layers/lowrank_layer.rst b/docs/source/_rst/layers/lowrank_layer.rst deleted file mode 100644 index 6e72feb68..000000000 --- a/docs/source/_rst/layers/lowrank_layer.rst +++ /dev/null @@ -1,8 +0,0 @@ -Low Rank layer -==================== -.. currentmodule:: pina.model.layers.lowrank_layer - -.. autoclass:: LowRankBlock - :members: - :show-inheritance: - :noindex: diff --git a/docs/source/_rst/layers/pod.rst b/docs/source/_rst/layers/pod.rst deleted file mode 100644 index 041be9973..000000000 --- a/docs/source/_rst/layers/pod.rst +++ /dev/null @@ -1,7 +0,0 @@ -PODBlock -====================== -.. currentmodule:: pina.model.layers.pod - -.. autoclass:: PODBlock - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/layers/rbf_layer.rst b/docs/source/_rst/layers/rbf_layer.rst deleted file mode 100644 index 8736d1a2b..000000000 --- a/docs/source/_rst/layers/rbf_layer.rst +++ /dev/null @@ -1,7 +0,0 @@ -RBFBlock -====================== -.. currentmodule:: pina.model.layers.rbf_layer - -.. autoclass:: RBFBlock - :members: - :show-inheritance: diff --git a/docs/source/_rst/loss/loss_interface.rst b/docs/source/_rst/loss/loss_interface.rst index 6d4827d15..8ff78c01e 100644 --- a/docs/source/_rst/loss/loss_interface.rst +++ b/docs/source/_rst/loss/loss_interface.rst @@ -1,8 +1,8 @@ -LpLoss +LossInterface =============== -.. currentmodule:: pina.loss +.. currentmodule:: pina.loss.loss_interface -.. automodule:: pina.loss +.. automodule:: pina.loss.loss_interface .. autoclass:: LossInterface :members: diff --git a/docs/source/_rst/loss/lploss.rst b/docs/source/_rst/loss/lploss.rst index f95d1877c..37dfdfe3c 100644 --- a/docs/source/_rst/loss/lploss.rst +++ b/docs/source/_rst/loss/lploss.rst @@ -1,9 +1,6 @@ LpLoss =============== -.. currentmodule:: pina.loss - -.. automodule:: pina.loss - :no-index: +.. currentmodule:: pina.loss.lp_loss .. autoclass:: LpLoss :members: diff --git a/docs/source/_rst/loss/powerloss.rst b/docs/source/_rst/loss/powerloss.rst index 0b1a7d91b..e4dee43b8 100644 --- a/docs/source/_rst/loss/powerloss.rst +++ b/docs/source/_rst/loss/powerloss.rst @@ -1,9 +1,6 @@ PowerLoss ==================== -.. currentmodule:: pina.loss - -.. automodule:: pina.loss - :no-index: +.. currentmodule:: pina.loss.power_loss .. autoclass:: PowerLoss :members: diff --git a/docs/source/_rst/loss/scalar_weighting.rst b/docs/source/_rst/loss/scalar_weighting.rst new file mode 100644 index 000000000..5ee82a785 --- /dev/null +++ b/docs/source/_rst/loss/scalar_weighting.rst @@ -0,0 +1,9 @@ +ScalarWeighting +=================== +.. currentmodule:: pina.loss.scalar_weighting + +.. automodule:: pina.loss.scalar_weighting + +.. autoclass:: ScalarWeighting + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/loss/weighting_interface.rst b/docs/source/_rst/loss/weighting_interface.rst new file mode 100644 index 000000000..2b0fa1bdc --- /dev/null +++ b/docs/source/_rst/loss/weighting_interface.rst @@ -0,0 +1,9 @@ +WeightingInterface +=================== +.. currentmodule:: pina.loss.weighting_interface + +.. automodule:: pina.loss.weighting_interface + +.. autoclass:: WeightingInterface + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/models/avno.rst b/docs/source/_rst/model/average_neural_operator.rst similarity index 70% rename from docs/source/_rst/models/avno.rst rename to docs/source/_rst/model/average_neural_operator.rst index a083f6fdc..02211e9a8 100644 --- a/docs/source/_rst/models/avno.rst +++ b/docs/source/_rst/model/average_neural_operator.rst @@ -1,6 +1,6 @@ Averaging Neural Operator ============================== -.. currentmodule:: pina.model.avno +.. currentmodule:: pina.model.average_neural_operator .. autoclass:: AveragingNeuralOperator :members: diff --git a/docs/source/_rst/model/block/average_neural_operator_block.rst b/docs/source/_rst/model/block/average_neural_operator_block.rst new file mode 100644 index 000000000..0072ec9d0 --- /dev/null +++ b/docs/source/_rst/model/block/average_neural_operator_block.rst @@ -0,0 +1,8 @@ +Averaging Neural Operator Block +================================== +.. currentmodule:: pina.model.block.average_neural_operator_block + +.. autoclass:: AVNOBlock + :members: + :show-inheritance: + :noindex: diff --git a/docs/source/_rst/model/block/convolution.rst b/docs/source/_rst/model/block/convolution.rst new file mode 100644 index 000000000..4033d5d56 --- /dev/null +++ b/docs/source/_rst/model/block/convolution.rst @@ -0,0 +1,8 @@ +Continuous Convolution Block +=============================== +.. currentmodule:: pina.model.block.convolution_2d + +.. autoclass:: ContinuousConvBlock + :members: + :show-inheritance: + :noindex: diff --git a/docs/source/_rst/model/block/convolution_interface.rst b/docs/source/_rst/model/block/convolution_interface.rst new file mode 100644 index 000000000..f8e61c16c --- /dev/null +++ b/docs/source/_rst/model/block/convolution_interface.rst @@ -0,0 +1,8 @@ +Continuous Convolution Interface +================================== +.. currentmodule:: pina.model.block.convolution + +.. autoclass:: BaseContinuousConv + :members: + :show-inheritance: + :noindex: diff --git a/docs/source/_rst/model/block/enhanced_linear.rst b/docs/source/_rst/model/block/enhanced_linear.rst new file mode 100644 index 000000000..d08cf79bf --- /dev/null +++ b/docs/source/_rst/model/block/enhanced_linear.rst @@ -0,0 +1,8 @@ +EnhancedLinear Block +===================== +.. currentmodule:: pina.model.block.residual + +.. autoclass:: EnhancedLinear + :members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/docs/source/_rst/layers/fourier.rst b/docs/source/_rst/model/block/fourier_block.rst similarity index 63% rename from docs/source/_rst/layers/fourier.rst rename to docs/source/_rst/model/block/fourier_block.rst index 132170069..c0fff4deb 100644 --- a/docs/source/_rst/layers/fourier.rst +++ b/docs/source/_rst/model/block/fourier_block.rst @@ -1,6 +1,6 @@ -Fourier Layers -=================== -.. currentmodule:: pina.model.layers.fourier +Fourier Neural Operator Block +====================================== +.. currentmodule:: pina.model.block.fourier_block .. autoclass:: FourierBlock1D diff --git a/docs/source/_rst/layers/fourier_embedding.rst b/docs/source/_rst/model/block/fourier_embedding.rst similarity index 75% rename from docs/source/_rst/layers/fourier_embedding.rst rename to docs/source/_rst/model/block/fourier_embedding.rst index f48cef150..77eb3960c 100644 --- a/docs/source/_rst/layers/fourier_embedding.rst +++ b/docs/source/_rst/model/block/fourier_embedding.rst @@ -1,6 +1,6 @@ Fourier Feature Embedding ======================================= -.. currentmodule:: pina.model.layers.embedding +.. currentmodule:: pina.model.block.embedding .. autoclass:: FourierFeatureEmbedding :members: diff --git a/docs/source/_rst/model/block/gno_block.rst b/docs/source/_rst/model/block/gno_block.rst new file mode 100644 index 000000000..19a532bab --- /dev/null +++ b/docs/source/_rst/model/block/gno_block.rst @@ -0,0 +1,8 @@ +Graph Neural Operator Block +=============================== +.. currentmodule:: pina.model.block.gno_block + +.. autoclass:: GNOBlock + :members: + :show-inheritance: + :noindex: diff --git a/docs/source/_rst/model/block/low_rank_block.rst b/docs/source/_rst/model/block/low_rank_block.rst new file mode 100644 index 000000000..366068f79 --- /dev/null +++ b/docs/source/_rst/model/block/low_rank_block.rst @@ -0,0 +1,8 @@ +Low Rank Neural Operator Block +================================= +.. currentmodule:: pina.model.block.low_rank_block + +.. autoclass:: LowRankBlock + :members: + :show-inheritance: + :noindex: diff --git a/docs/source/_rst/layers/orthogonal.rst b/docs/source/_rst/model/block/orthogonal.rst similarity index 59% rename from docs/source/_rst/layers/orthogonal.rst rename to docs/source/_rst/model/block/orthogonal.rst index 6dfc4009b..21d12998a 100644 --- a/docs/source/_rst/layers/orthogonal.rst +++ b/docs/source/_rst/model/block/orthogonal.rst @@ -1,6 +1,6 @@ -OrthogonalBlock +Orthogonal Block ====================== -.. currentmodule:: pina.model.layers.orthogonal +.. currentmodule:: pina.model.block.orthogonal .. autoclass:: OrthogonalBlock :members: diff --git a/docs/source/_rst/layers/pbc_embedding.rst b/docs/source/_rst/model/block/pbc_embedding.rst similarity index 77% rename from docs/source/_rst/layers/pbc_embedding.rst rename to docs/source/_rst/model/block/pbc_embedding.rst index d4d202314..f469644af 100644 --- a/docs/source/_rst/layers/pbc_embedding.rst +++ b/docs/source/_rst/model/block/pbc_embedding.rst @@ -1,6 +1,6 @@ Periodic Boundary Condition Embedding ======================================= -.. currentmodule:: pina.model.layers.embedding +.. currentmodule:: pina.model.block.embedding .. autoclass:: PeriodicBoundaryEmbedding :members: diff --git a/docs/source/_rst/model/block/pod_block.rst b/docs/source/_rst/model/block/pod_block.rst new file mode 100644 index 000000000..4b66e2c97 --- /dev/null +++ b/docs/source/_rst/model/block/pod_block.rst @@ -0,0 +1,7 @@ +Proper Orthogonal Decomposition Block +============================================ +.. currentmodule:: pina.model.block.pod_block + +.. autoclass:: PODBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/model/block/rbf_block.rst b/docs/source/_rst/model/block/rbf_block.rst new file mode 100644 index 000000000..545f14d08 --- /dev/null +++ b/docs/source/_rst/model/block/rbf_block.rst @@ -0,0 +1,7 @@ +Radias Basis Function Block +============================= +.. currentmodule:: pina.model.block.rbf_block + +.. autoclass:: RBFBlock + :members: + :show-inheritance: diff --git a/docs/source/_rst/layers/residual.rst b/docs/source/_rst/model/block/residual.rst similarity index 58% rename from docs/source/_rst/layers/residual.rst rename to docs/source/_rst/model/block/residual.rst index 1af11e5b8..69741c74c 100644 --- a/docs/source/_rst/layers/residual.rst +++ b/docs/source/_rst/model/block/residual.rst @@ -1,6 +1,6 @@ -Residual layer +Residual Block =================== -.. currentmodule:: pina.model.layers.residual +.. currentmodule:: pina.model.block.residual .. autoclass:: ResidualBlock :members: diff --git a/docs/source/_rst/layers/spectral.rst b/docs/source/_rst/model/block/spectral.rst similarity index 68% rename from docs/source/_rst/layers/spectral.rst rename to docs/source/_rst/model/block/spectral.rst index 5635ba27c..3c80f3dd8 100644 --- a/docs/source/_rst/layers/spectral.rst +++ b/docs/source/_rst/model/block/spectral.rst @@ -1,6 +1,6 @@ -Spectral Convolution -====================== -.. currentmodule:: pina.model.layers.spectral +Spectral Convolution Block +============================ +.. currentmodule:: pina.model.block.spectral .. autoclass:: SpectralConvBlock1D :members: diff --git a/docs/source/_rst/models/deeponet.rst b/docs/source/_rst/model/deeponet.rst similarity index 100% rename from docs/source/_rst/models/deeponet.rst rename to docs/source/_rst/model/deeponet.rst diff --git a/docs/source/_rst/models/fnn.rst b/docs/source/_rst/model/feed_forward.rst similarity index 100% rename from docs/source/_rst/models/fnn.rst rename to docs/source/_rst/model/feed_forward.rst diff --git a/docs/source/_rst/models/fourier_kernel.rst b/docs/source/_rst/model/fourier_integral_kernel.rst similarity index 68% rename from docs/source/_rst/models/fourier_kernel.rst rename to docs/source/_rst/model/fourier_integral_kernel.rst index e45ba174d..b1fb484fe 100644 --- a/docs/source/_rst/models/fourier_kernel.rst +++ b/docs/source/_rst/model/fourier_integral_kernel.rst @@ -1,6 +1,6 @@ FourierIntegralKernel ========================= -.. currentmodule:: pina.model.fno +.. currentmodule:: pina.model.fourier_neural_operator .. autoclass:: FourierIntegralKernel :members: diff --git a/docs/source/_rst/models/fno.rst b/docs/source/_rst/model/fourier_neural_operator.rst similarity index 56% rename from docs/source/_rst/models/fno.rst rename to docs/source/_rst/model/fourier_neural_operator.rst index 3d102b3ad..e77494fd0 100644 --- a/docs/source/_rst/models/fno.rst +++ b/docs/source/_rst/model/fourier_neural_operator.rst @@ -1,6 +1,6 @@ FNO =========== -.. currentmodule:: pina.model.fno +.. currentmodule:: pina.model.fourier_neural_operator .. autoclass:: FNO :members: diff --git a/docs/source/_rst/model/graph_neural_operator.rst b/docs/source/_rst/model/graph_neural_operator.rst new file mode 100644 index 000000000..fbb8600e5 --- /dev/null +++ b/docs/source/_rst/model/graph_neural_operator.rst @@ -0,0 +1,7 @@ +GraphNeuralOperator +======================= +.. currentmodule:: pina.model.graph_neural_operator + +.. autoclass:: GraphNeuralOperator + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/model/graph_neural_operator_integral_kernel.rst b/docs/source/_rst/model/graph_neural_operator_integral_kernel.rst new file mode 100644 index 000000000..cf15a31a5 --- /dev/null +++ b/docs/source/_rst/model/graph_neural_operator_integral_kernel.rst @@ -0,0 +1,7 @@ +GraphNeuralKernel +======================= +.. currentmodule:: pina.model.graph_neural_operator + +.. autoclass:: GraphNeuralKernel + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/models/base_no.rst b/docs/source/_rst/model/kernel_neural_operator.rst similarity index 68% rename from docs/source/_rst/models/base_no.rst rename to docs/source/_rst/model/kernel_neural_operator.rst index 772261c5c..d693afac5 100644 --- a/docs/source/_rst/models/base_no.rst +++ b/docs/source/_rst/model/kernel_neural_operator.rst @@ -1,6 +1,6 @@ KernelNeuralOperator ======================= -.. currentmodule:: pina.model.base_no +.. currentmodule:: pina.model.kernel_neural_operator .. autoclass:: KernelNeuralOperator :members: diff --git a/docs/source/_rst/models/lno.rst b/docs/source/_rst/model/low_rank_neural_operator.rst similarity index 69% rename from docs/source/_rst/models/lno.rst rename to docs/source/_rst/model/low_rank_neural_operator.rst index f3f8277dd..22fe7cc93 100644 --- a/docs/source/_rst/models/lno.rst +++ b/docs/source/_rst/model/low_rank_neural_operator.rst @@ -1,6 +1,6 @@ Low Rank Neural Operator ============================== -.. currentmodule:: pina.model.lno +.. currentmodule:: pina.model.low_rank_neural_operator .. autoclass:: LowRankNeuralOperator :members: diff --git a/docs/source/_rst/models/mionet.rst b/docs/source/_rst/model/mionet.rst similarity index 100% rename from docs/source/_rst/models/mionet.rst rename to docs/source/_rst/model/mionet.rst diff --git a/docs/source/_rst/models/multifeedforward.rst b/docs/source/_rst/model/multi_feed_forward.rst similarity index 100% rename from docs/source/_rst/models/multifeedforward.rst rename to docs/source/_rst/model/multi_feed_forward.rst diff --git a/docs/source/_rst/models/fnn_residual.rst b/docs/source/_rst/model/residual_feed_forward.rst similarity index 100% rename from docs/source/_rst/models/fnn_residual.rst rename to docs/source/_rst/model/residual_feed_forward.rst diff --git a/docs/source/_rst/models/spline.rst b/docs/source/_rst/model/spline.rst similarity index 100% rename from docs/source/_rst/models/spline.rst rename to docs/source/_rst/model/spline.rst diff --git a/docs/source/_rst/models/network.rst b/docs/source/_rst/models/network.rst deleted file mode 100644 index 4df9e194b..000000000 --- a/docs/source/_rst/models/network.rst +++ /dev/null @@ -1,8 +0,0 @@ -Network -================ - -.. automodule:: pina.model.network - -.. autoclass:: Network - :members: - :show-inheritance: diff --git a/docs/source/_rst/operator.rst b/docs/source/_rst/operator.rst new file mode 100644 index 000000000..42746a6f8 --- /dev/null +++ b/docs/source/_rst/operator.rst @@ -0,0 +1,8 @@ +Operators +=========== + +.. currentmodule:: pina.operator + +.. automodule:: pina.operator + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/operators.rst b/docs/source/_rst/operators.rst deleted file mode 100644 index 59f7c7a79..000000000 --- a/docs/source/_rst/operators.rst +++ /dev/null @@ -1,8 +0,0 @@ -Operators -=========== - -.. currentmodule:: pina.operators - -.. automodule:: pina.operators - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/optim/optimizer_interface.rst b/docs/source/_rst/optim/optimizer_interface.rst new file mode 100644 index 000000000..88c18e8f5 --- /dev/null +++ b/docs/source/_rst/optim/optimizer_interface.rst @@ -0,0 +1,7 @@ +Optimizer +============ +.. currentmodule:: pina.optim.optimizer_interface + +.. autoclass:: Optimizer + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/optim/scheduler_interface.rst b/docs/source/_rst/optim/scheduler_interface.rst new file mode 100644 index 000000000..ab8ee292e --- /dev/null +++ b/docs/source/_rst/optim/scheduler_interface.rst @@ -0,0 +1,7 @@ +Scheduler +============= +.. currentmodule:: pina.optim.scheduler_interface + +.. autoclass:: Scheduler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/optim/torch_optimizer.rst b/docs/source/_rst/optim/torch_optimizer.rst new file mode 100644 index 000000000..3e6c9d912 --- /dev/null +++ b/docs/source/_rst/optim/torch_optimizer.rst @@ -0,0 +1,7 @@ +TorchOptimizer +=============== +.. currentmodule:: pina.optim.torch_optimizer + +.. autoclass:: TorchOptimizer + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/optim/torch_scheduler.rst b/docs/source/_rst/optim/torch_scheduler.rst new file mode 100644 index 000000000..5c3e4df36 --- /dev/null +++ b/docs/source/_rst/optim/torch_scheduler.rst @@ -0,0 +1,7 @@ +TorchScheduler +=============== +.. currentmodule:: pina.optim.torch_scheduler + +.. autoclass:: TorchScheduler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/plotter.rst b/docs/source/_rst/plotter.rst deleted file mode 100644 index b6e94a717..000000000 --- a/docs/source/_rst/plotter.rst +++ /dev/null @@ -1,8 +0,0 @@ -Plotter -=========== -.. currentmodule:: pina.plotter - -.. automodule:: pina.plotter - :members: - :show-inheritance: - :noindex: diff --git a/docs/source/_rst/problem/abstractproblem.rst b/docs/source/_rst/problem/abstract_problem.rst similarity index 100% rename from docs/source/_rst/problem/abstractproblem.rst rename to docs/source/_rst/problem/abstract_problem.rst diff --git a/docs/source/_rst/problem/inverse_problem.rst b/docs/source/_rst/problem/inverse_problem.rst new file mode 100644 index 000000000..5ce306ffc --- /dev/null +++ b/docs/source/_rst/problem/inverse_problem.rst @@ -0,0 +1,9 @@ +InverseProblem +============== +.. currentmodule:: pina.problem.inverse_problem + +.. automodule:: pina.problem.inverse_problem + +.. autoclass:: InverseProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/parametricproblem.rst b/docs/source/_rst/problem/parametric_problem.rst similarity index 100% rename from docs/source/_rst/problem/parametricproblem.rst rename to docs/source/_rst/problem/parametric_problem.rst diff --git a/docs/source/_rst/problem/spatialproblem.rst b/docs/source/_rst/problem/spatial_problem.rst similarity index 100% rename from docs/source/_rst/problem/spatialproblem.rst rename to docs/source/_rst/problem/spatial_problem.rst diff --git a/docs/source/_rst/problem/timedepproblem.rst b/docs/source/_rst/problem/time_dependent_problem.rst similarity index 52% rename from docs/source/_rst/problem/timedepproblem.rst rename to docs/source/_rst/problem/time_dependent_problem.rst index 93b8cb50b..db94121c2 100644 --- a/docs/source/_rst/problem/timedepproblem.rst +++ b/docs/source/_rst/problem/time_dependent_problem.rst @@ -1,8 +1,8 @@ TimeDependentProblem ==================== -.. currentmodule:: pina.problem.timedep_problem +.. currentmodule:: pina.problem.time_dependent_problem -.. automodule:: pina.problem.timedep_problem +.. automodule:: pina.problem.time_dependent_problem .. autoclass:: TimeDependentProblem :members: diff --git a/docs/source/_rst/problem/zoo/advection.rst b/docs/source/_rst/problem/zoo/advection.rst new file mode 100644 index 000000000..b83cc9d99 --- /dev/null +++ b/docs/source/_rst/problem/zoo/advection.rst @@ -0,0 +1,9 @@ +AdvectionProblem +================== +.. currentmodule:: pina.problem.zoo.advection + +.. automodule:: pina.problem.zoo.advection + +.. autoclass:: AdvectionProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/allen_cahn.rst b/docs/source/_rst/problem/zoo/allen_cahn.rst new file mode 100644 index 000000000..ada3465d1 --- /dev/null +++ b/docs/source/_rst/problem/zoo/allen_cahn.rst @@ -0,0 +1,9 @@ +AllenCahnProblem +================== +.. currentmodule:: pina.problem.zoo.allen_cahn + +.. automodule:: pina.problem.zoo.allen_cahn + +.. autoclass:: AllenCahnProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/diffusion_reaction.rst b/docs/source/_rst/problem/zoo/diffusion_reaction.rst new file mode 100644 index 000000000..0cad0fd67 --- /dev/null +++ b/docs/source/_rst/problem/zoo/diffusion_reaction.rst @@ -0,0 +1,9 @@ +DiffusionReactionProblem +========================= +.. currentmodule:: pina.problem.zoo.diffusion_reaction + +.. automodule:: pina.problem.zoo.diffusion_reaction + +.. autoclass:: DiffusionReactionProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/helmholtz.rst b/docs/source/_rst/problem/zoo/helmholtz.rst new file mode 100644 index 000000000..af4ec7dbc --- /dev/null +++ b/docs/source/_rst/problem/zoo/helmholtz.rst @@ -0,0 +1,9 @@ +HelmholtzProblem +================== +.. currentmodule:: pina.problem.zoo.helmholtz + +.. automodule:: pina.problem.zoo.helmholtz + +.. autoclass:: HelmholtzProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst b/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst new file mode 100644 index 000000000..727c17b47 --- /dev/null +++ b/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst @@ -0,0 +1,9 @@ +InversePoisson2DSquareProblem +============================== +.. currentmodule:: pina.problem.zoo.inverse_poisson_2d_square + +.. automodule:: pina.problem.zoo.inverse_poisson_2d_square + +.. autoclass:: InversePoisson2DSquareProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/poisson_2d_square.rst b/docs/source/_rst/problem/zoo/poisson_2d_square.rst new file mode 100644 index 000000000..718c33ccc --- /dev/null +++ b/docs/source/_rst/problem/zoo/poisson_2d_square.rst @@ -0,0 +1,9 @@ +Poisson2DSquareProblem +======================== +.. currentmodule:: pina.problem.zoo.poisson_2d_square + +.. automodule:: pina.problem.zoo.poisson_2d_square + +.. autoclass:: Poisson2DSquareProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/supervised_problem.rst b/docs/source/_rst/problem/zoo/supervised_problem.rst new file mode 100644 index 000000000..aad7d5aa5 --- /dev/null +++ b/docs/source/_rst/problem/zoo/supervised_problem.rst @@ -0,0 +1,9 @@ +SupervisedProblem +================== +.. currentmodule:: pina.problem.zoo.supervised_problem + +.. automodule:: pina.problem.zoo.supervised_problem + +.. autoclass:: SupervisedProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/solvers/garom.rst b/docs/source/_rst/solver/garom.rst similarity index 64% rename from docs/source/_rst/solvers/garom.rst rename to docs/source/_rst/solver/garom.rst index 5fcd97f5c..0e5820f6f 100644 --- a/docs/source/_rst/solvers/garom.rst +++ b/docs/source/_rst/solver/garom.rst @@ -1,6 +1,6 @@ GAROM ====== -.. currentmodule:: pina.solvers.garom +.. currentmodule:: pina.solver.garom .. autoclass:: GAROM :members: diff --git a/docs/source/_rst/solver/multi_solver_interface.rst b/docs/source/_rst/solver/multi_solver_interface.rst new file mode 100644 index 000000000..7f68c83a4 --- /dev/null +++ b/docs/source/_rst/solver/multi_solver_interface.rst @@ -0,0 +1,8 @@ +MultiSolverInterface +====================== +.. currentmodule:: pina.solver.solver + +.. autoclass:: MultiSolverInterface + :show-inheritance: + :members: + diff --git a/docs/source/_rst/solvers/causalpinn.rst b/docs/source/_rst/solver/physic_informed_solver/causal_pinn.rst similarity index 56% rename from docs/source/_rst/solvers/causalpinn.rst rename to docs/source/_rst/solver/physic_informed_solver/causal_pinn.rst index 28f7f15ea..489900111 100644 --- a/docs/source/_rst/solvers/causalpinn.rst +++ b/docs/source/_rst/solver/physic_informed_solver/causal_pinn.rst @@ -1,6 +1,6 @@ CausalPINN ============== -.. currentmodule:: pina.solvers.pinns.causalpinn +.. currentmodule:: pina.solver.physic_informed_solver.causal_pinn .. autoclass:: CausalPINN :members: diff --git a/docs/source/_rst/solvers/competitivepinn.rst b/docs/source/_rst/solver/physic_informed_solver/competitive_pinn.rst similarity index 58% rename from docs/source/_rst/solvers/competitivepinn.rst rename to docs/source/_rst/solver/physic_informed_solver/competitive_pinn.rst index 2bbe242b7..5bfa431a8 100644 --- a/docs/source/_rst/solvers/competitivepinn.rst +++ b/docs/source/_rst/solver/physic_informed_solver/competitive_pinn.rst @@ -1,6 +1,6 @@ CompetitivePINN ================= -.. currentmodule:: pina.solvers.pinns.competitive_pinn +.. currentmodule:: pina.solver.physic_informed_solver.competitive_pinn .. autoclass:: CompetitivePINN :members: diff --git a/docs/source/_rst/solver/physic_informed_solver/gradient_pinn.rst b/docs/source/_rst/solver/physic_informed_solver/gradient_pinn.rst new file mode 100644 index 000000000..ea7039311 --- /dev/null +++ b/docs/source/_rst/solver/physic_informed_solver/gradient_pinn.rst @@ -0,0 +1,7 @@ +GradientPINN +============== +.. currentmodule:: pina.solver.physic_informed_solver.gradient_pinn + +.. autoclass:: GradientPINN + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/solvers/pinn.rst b/docs/source/_rst/solver/physic_informed_solver/pinn.rst similarity index 53% rename from docs/source/_rst/solvers/pinn.rst rename to docs/source/_rst/solver/physic_informed_solver/pinn.rst index e1c2b59cd..974cddd06 100644 --- a/docs/source/_rst/solvers/pinn.rst +++ b/docs/source/_rst/solver/physic_informed_solver/pinn.rst @@ -1,6 +1,6 @@ PINN ====== -.. currentmodule:: pina.solvers.pinns.pinn +.. currentmodule:: pina.solver.physic_informed_solver.pinn .. autoclass:: PINN :members: diff --git a/docs/source/_rst/solvers/basepinn.rst b/docs/source/_rst/solver/physic_informed_solver/pinn_interface.rst similarity index 58% rename from docs/source/_rst/solvers/basepinn.rst rename to docs/source/_rst/solver/physic_informed_solver/pinn_interface.rst index c6507953d..e9b83ac03 100644 --- a/docs/source/_rst/solvers/basepinn.rst +++ b/docs/source/_rst/solver/physic_informed_solver/pinn_interface.rst @@ -1,6 +1,6 @@ PINNInterface ================= -.. currentmodule:: pina.solvers.pinns.basepinn +.. currentmodule:: pina.solver.physic_informed_solver.pinn_interface .. autoclass:: PINNInterface :members: diff --git a/docs/source/_rst/solvers/rba_pinn.rst b/docs/source/_rst/solver/physic_informed_solver/rba_pinn.rst similarity index 54% rename from docs/source/_rst/solvers/rba_pinn.rst rename to docs/source/_rst/solver/physic_informed_solver/rba_pinn.rst index b964ccef6..63d72f299 100644 --- a/docs/source/_rst/solvers/rba_pinn.rst +++ b/docs/source/_rst/solver/physic_informed_solver/rba_pinn.rst @@ -1,6 +1,6 @@ RBAPINN ======== -.. currentmodule:: pina.solvers.pinns.rbapinn +.. currentmodule:: pina.solver.physic_informed_solver.rba_pinn .. autoclass:: RBAPINN :members: diff --git a/docs/source/_rst/solver/physic_informed_solver/self_adaptive_pinn.rst b/docs/source/_rst/solver/physic_informed_solver/self_adaptive_pinn.rst new file mode 100644 index 000000000..dd242bb15 --- /dev/null +++ b/docs/source/_rst/solver/physic_informed_solver/self_adaptive_pinn.rst @@ -0,0 +1,7 @@ +SelfAdaptivePINN +================== +.. currentmodule:: pina.solver.physic_informed_solver.self_adaptive_pinn + +.. autoclass:: SelfAdaptivePINN + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/solvers/rom.rst b/docs/source/_rst/solver/reduced_order_model.rst similarity index 71% rename from docs/source/_rst/solvers/rom.rst rename to docs/source/_rst/solver/reduced_order_model.rst index 3ee534bb5..33a909515 100644 --- a/docs/source/_rst/solvers/rom.rst +++ b/docs/source/_rst/solver/reduced_order_model.rst @@ -1,6 +1,6 @@ ReducedOrderModelSolver ========================== -.. currentmodule:: pina.solvers.rom +.. currentmodule:: pina.solver.reduced_order_model .. autoclass:: ReducedOrderModelSolver :members: diff --git a/docs/source/_rst/solver/single_solver_interface.rst b/docs/source/_rst/solver/single_solver_interface.rst new file mode 100644 index 000000000..5b85f11b5 --- /dev/null +++ b/docs/source/_rst/solver/single_solver_interface.rst @@ -0,0 +1,8 @@ +SingleSolverInterface +====================== +.. currentmodule:: pina.solver.solver + +.. autoclass:: SingleSolverInterface + :show-inheritance: + :members: + diff --git a/docs/source/_rst/solvers/solver_interface.rst b/docs/source/_rst/solver/solver_interface.rst similarity index 70% rename from docs/source/_rst/solvers/solver_interface.rst rename to docs/source/_rst/solver/solver_interface.rst index 363e1dbb2..9bb11783e 100644 --- a/docs/source/_rst/solvers/solver_interface.rst +++ b/docs/source/_rst/solver/solver_interface.rst @@ -1,7 +1,8 @@ SolverInterface ================= -.. currentmodule:: pina.solvers.solver +.. currentmodule:: pina.solver.solver .. autoclass:: SolverInterface :show-inheritance: :members: + diff --git a/docs/source/_rst/solvers/supervised.rst b/docs/source/_rst/solver/supervised.rst similarity index 70% rename from docs/source/_rst/solvers/supervised.rst rename to docs/source/_rst/solver/supervised.rst index 895759e9e..19978f9a0 100644 --- a/docs/source/_rst/solvers/supervised.rst +++ b/docs/source/_rst/solver/supervised.rst @@ -1,6 +1,6 @@ SupervisedSolver =================== -.. currentmodule:: pina.solvers.supervised +.. currentmodule:: pina.solver.supervised .. autoclass:: SupervisedSolver :members: diff --git a/docs/source/_rst/solvers/gpinn.rst b/docs/source/_rst/solvers/gpinn.rst deleted file mode 100644 index ee076a5d7..000000000 --- a/docs/source/_rst/solvers/gpinn.rst +++ /dev/null @@ -1,7 +0,0 @@ -GPINN -====== -.. currentmodule:: pina.solvers.pinns.gpinn - -.. autoclass:: GPINN - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/solvers/sapinn.rst b/docs/source/_rst/solvers/sapinn.rst deleted file mode 100644 index b20891fff..000000000 --- a/docs/source/_rst/solvers/sapinn.rst +++ /dev/null @@ -1,7 +0,0 @@ -SAPINN -====== -.. currentmodule:: pina.solvers.pinns.sapinn - -.. autoclass:: SAPINN - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/tutorials/tutorial1/tutorial.rst b/docs/source/_rst/tutorials/tutorial1/tutorial.rst deleted file mode 100644 index d15cb6360..000000000 --- a/docs/source/_rst/tutorials/tutorial1/tutorial.rst +++ /dev/null @@ -1,385 +0,0 @@ -Tutorial: Physics Informed Neural Networks on PINA -================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial1/tutorial.ipynb - -In this tutorial, we will demonstrate a typical use case of **PINA** on -a toy problem, following the standard API procedure. - -.. raw:: html - -

- -.. raw:: html - -

- -Specifically, the tutorial aims to introduce the following topics: - -- Explaining how to build **PINA** Problems, -- Showing how to generate data for ``PINN`` training - -These are the two main steps needed **before** starting the modelling -optimization (choose model and solver, and train). We will show each -step in detail, and at the end, we will solve a simple Ordinary -Differential Equation (ODE) problem using the ``PINN`` solver. - -Build a PINA problem --------------------- - -Problem definition in the **PINA** framework is done by building a -python ``class``, which inherits from one or more problem classes -(``SpatialProblem``, ``TimeDependentProblem``, ``ParametricProblem``, …) -depending on the nature of the problem. Below is an example: - -Simple Ordinary Differential Equation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Consider the following: - -.. math:: - - - \begin{equation} - \begin{cases} - \frac{d}{dx}u(x) &= u(x) \quad x\in(0,1)\\ - u(x=0) &= 1 \\ - \end{cases} - \end{equation} - -with the analytical solution :math:`u(x) = e^x`. In this case, our ODE -depends only on the spatial variable :math:`x\in(0,1)` , meaning that -our ``Problem`` class is going to be inherited from the -``SpatialProblem`` class: - -.. code:: python - - from pina.problem import SpatialProblem - from pina.geometry import CartesianProblem - - class SimpleODE(SpatialProblem): - - output_variables = ['u'] - spatial_domain = CartesianProblem({'x': [0, 1]}) - - # other stuff ... - -Notice that we define ``output_variables`` as a list of symbols, -indicating the output variables of our equation (in this case only -:math:`u`), this is done because in **PINA** the ``torch.Tensor``\ s are -labelled, allowing the user maximal flexibility for the manipulation of -the tensor. The ``spatial_domain`` variable indicates where the sample -points are going to be sampled in the domain, in this case -:math:`x\in[0,1]`. - -What if our equation is also time-dependent? In this case, our ``class`` -will inherit from both ``SpatialProblem`` and ``TimeDependentProblem``: - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - from pina.problem import SpatialProblem, TimeDependentProblem - from pina.geometry import CartesianDomain - - class TimeSpaceODE(SpatialProblem, TimeDependentProblem): - - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1]}) - temporal_domain = CartesianDomain({'t': [0, 1]}) - - # other stuff ... - - -where we have included the ``temporal_domain`` variable, indicating the -time domain wanted for the solution. - -In summary, using **PINA**, we can initialize a problem with a class -which inherits from different base classes: ``SpatialProblem``, -``TimeDependentProblem``, ``ParametricProblem``, and so on depending on -the type of problem we are considering. Here are some examples (more on -the official documentation): - -* ``SpatialProblem`` :math:`\rightarrow` a differential equation with spatial variable(s) ``spatial_domain`` -* ``TimeDependentProblem`` :math:`\rightarrow` a time-dependent differential equation with temporal variable(s) ``temporal_domain`` -* ``ParametricProblem`` :math:`\rightarrow` a parametrized differential equation with parametric variable(s) ``parameter_domain`` -* ``AbstractProblem`` :math:`\rightarrow` any **PINA** problem inherits from here - -Write the problem class -~~~~~~~~~~~~~~~~~~~~~~~ - -Once the ``Problem`` class is initialized, we need to represent the -differential equation in **PINA**. In order to do this, we need to load -the **PINA** operators from ``pina.operators`` module. Again, we’ll -consider Equation (1) and represent it in **PINA**: - -.. code:: ipython3 - - from pina.problem import SpatialProblem - from pina.operators import grad - from pina import Condition - from pina.geometry import CartesianDomain - from pina.equation import Equation, FixedValue - - import torch - - - class SimpleODE(SpatialProblem): - - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1]}) - - # defining the ode equation - def ode_equation(input_, output_): - - # computing the derivative - u_x = grad(output_, input_, components=['u'], d=['x']) - - # extracting the u input variable - u = output_.extract(['u']) - - # calculate the residual and return it - return u_x - u - - # conditions to hold - conditions = { - 'x0': Condition(location=CartesianDomain({'x': 0.}), equation=FixedValue(1)), # We fix initial condition to value 1 - 'D': Condition(location=CartesianDomain({'x': [0, 1]}), equation=Equation(ode_equation)), # We wrap the python equation using Equation - } - - # sampled points (see below) - input_pts = None - - # defining the true solution - def truth_solution(self, pts): - return torch.exp(pts.extract(['x'])) - - problem = SimpleODE() - -After we define the ``Problem`` class, we need to write different class -methods, where each method is a function returning a residual. These -functions are the ones minimized during PINN optimization, given the -initial conditions. For example, in the domain :math:`[0,1]`, the ODE -equation (``ode_equation``) must be satisfied. We represent this by -returning the difference between subtracting the variable ``u`` from its -gradient (the residual), which we hope to minimize to 0. This is done -for all conditions. Notice that we do not pass directly a ``python`` -function, but an ``Equation`` object, which is initialized with the -``python`` function. This is done so that all the computations and -internal checks are done inside **PINA**. - -Once we have defined the function, we need to tell the neural network -where these methods are to be applied. To do so, we use the -``Condition`` class. In the ``Condition`` class, we pass the location -points and the equation we want minimized on those points (other -possibilities are allowed, see the documentation for reference). - -Finally, it’s possible to define a ``truth_solution`` function, which -can be useful if we want to plot the results and see how the real -solution compares to the expected (true) solution. Notice that the -``truth_solution`` function is a method of the ``PINN`` class, but it is -not mandatory for problem definition. - -Generate data -------------- - -Data for training can come in form of direct numerical simulation -results, or points in the domains. In case we perform unsupervised -learning, we just need the collocation points for training, i.e. points -where we want to evaluate the neural network. Sampling point in **PINA** -is very easy, here we show three examples using the -``.discretise_domain`` method of the ``AbstractProblem`` class. - -.. code:: ipython3 - - # sampling 20 points in [0, 1] through discretization in all locations - problem.discretise_domain(n=20, mode='grid', variables=['x'], locations='all') - - # sampling 20 points in (0, 1) through latin hypercube sampling in D, and 1 point in x0 - problem.discretise_domain(n=20, mode='latin', variables=['x'], locations=['D']) - problem.discretise_domain(n=1, mode='random', variables=['x'], locations=['x0']) - - # sampling 20 points in (0, 1) randomly - problem.discretise_domain(n=20, mode='random', variables=['x']) - -We are going to use latin hypercube points for sampling. We need to -sample in all the conditions domains. In our case we sample in ``D`` and -``x0``. - -.. code:: ipython3 - - # sampling for training - problem.discretise_domain(1, 'random', locations=['x0']) - problem.discretise_domain(20, 'lh', locations=['D']) - -The points are saved in a python ``dict``, and can be accessed by -calling the attribute ``input_pts`` of the problem - -.. code:: ipython3 - - print('Input points:', problem.input_pts) - print('Input points labels:', problem.input_pts['D'].labels) - - -.. parsed-literal:: - - Input points: {'x0': LabelTensor([[[0.]]]), 'D': LabelTensor([[[0.7644]], - [[0.2028]], - [[0.1789]], - [[0.4294]], - [[0.3239]], - [[0.6531]], - [[0.1406]], - [[0.6062]], - [[0.4969]], - [[0.7429]], - [[0.8681]], - [[0.3800]], - [[0.5357]], - [[0.0152]], - [[0.9679]], - [[0.8101]], - [[0.0662]], - [[0.9095]], - [[0.2503]], - [[0.5580]]])} - Input points labels: ['x'] - - -To visualize the sampled points we can use the ``.plot_samples`` method -of the ``Plotter`` class - -.. code:: ipython3 - - from pina import Plotter - - pl = Plotter() - pl.plot_samples(problem=problem) - - - -.. image:: tutorial_files/tutorial_16_0.png - - -Perform a small training ------------------------- - -Once we have defined the problem and generated the data we can start the -modelling. Here we will choose a ``FeedForward`` neural network -available in ``pina.model``, and we will train using the ``PINN`` solver -from ``pina.solvers``. We highlight that this training is fairly simple, -for more advanced stuff consider the tutorials in the **Physics Informed -Neural Networks** section of **Tutorials**. For training we use the -``Trainer`` class from ``pina.trainer``. Here we show a very short -training and some method for plotting the results. Notice that by -default all relevant metrics (e.g. MSE error during training) are going -to be tracked using a ``lightining`` logger, by default ``CSVLogger``. -If you want to track the metric by yourself without a logger, use -``pina.callbacks.MetricTracker``. - -.. code:: ipython3 - - from pina import Trainer - from pina.solvers import PINN - from pina.model import FeedForward - from pina.callbacks import MetricTracker - - - # build the model - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - - # create the PINN object - pinn = PINN(problem, model) - - # create the trainer - trainer = Trainer(solver=pinn, max_epochs=1500, callbacks=[MetricTracker()], accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - - # train - trainer.train() - -After the training we can inspect trainer logged metrics (by default -**PINA** logs mean square error residual loss). The logged metrics can -be accessed online using one of the ``Lightinig`` loggers. The final -loss can be accessed by ``trainer.logged_metrics`` - -.. code:: ipython3 - - # inspecting final loss - trainer.logged_metrics - - - - -.. parsed-literal:: - - {'x0_loss': tensor(1.0674e-05), - 'D_loss': tensor(0.0008), - 'mean_loss': tensor(0.0004)} - - - -By using the ``Plotter`` class from **PINA** we can also do some -quatitative plots of the solution. - -.. code:: ipython3 - - # plotting the solution - pl.plot(solver=pinn) - - - -.. image:: tutorial_files/tutorial_23_1.png - - - -.. parsed-literal:: - -
- - -The solution is overlapped with the actual one, and they are barely -indistinguishable. We can also plot easily the loss: - -.. code:: ipython3 - - pl.plot_loss(trainer=trainer, label = 'mean_loss', logy=True) - - - -.. image:: tutorial_files/tutorial_25_0.png - - -As we can see the loss has not reached a minimum, suggesting that we -could train for longer - -What’s next? ------------- - -Congratulations on completing the introductory tutorial of **PINA**! -There are several directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. Train the network using other types of models (see ``pina.model``) - -3. GPU training and speed benchmarking - -4. Many more… - - diff --git a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_16_0.png b/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_16_0.png deleted file mode 100644 index 3c906354f..000000000 Binary files a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_16_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_23_1.png b/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_23_1.png deleted file mode 100644 index e4d92c2ea..000000000 Binary files a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_23_1.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_25_0.png b/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_25_0.png deleted file mode 100644 index 64bd43af4..000000000 Binary files a/docs/source/_rst/tutorials/tutorial1/tutorial_files/tutorial_25_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial10/tutorial.rst b/docs/source/_rst/tutorials/tutorial10/tutorial.rst deleted file mode 100644 index 469235447..000000000 --- a/docs/source/_rst/tutorials/tutorial10/tutorial.rst +++ /dev/null @@ -1,366 +0,0 @@ -Tutorial: Averaging Neural Operator for solving Kuramoto Sivashinsky equation -============================================================================= - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial10/tutorial.ipynb - -In this tutorial we will build a Neural Operator using the -``AveragingNeuralOperator`` model and the ``SupervisedSolver``. At the -end of the tutorial you will be able to train a Neural Operator for -learning the operator of time dependent PDEs. - -First of all, some useful imports. Note we use ``scipy`` for i/o -operations. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - !mkdir "data" - !wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS.mat" -O "data/Data_KS.mat" - !wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS2.mat" -O "data/Data_KS2.mat" - - - import torch - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - from scipy import io - from pina import Condition, LabelTensor - from pina.problem import AbstractProblem - from pina.model import AveragingNeuralOperator - from pina.solvers import SupervisedSolver - from pina.trainer import Trainer - -Data Generation ---------------- - -We will focus on solving a specific PDE, the **Kuramoto Sivashinsky** -(KS) equation. The KS PDE is a fourth-order nonlinear PDE with the -following form: - -.. math:: - - - \frac{\partial u}{\partial t}(x,t) = -u(x,t)\frac{\partial u}{\partial x}(x,t)- \frac{\partial^{4}u}{\partial x^{4}}(x,t) - \frac{\partial^{2}u}{\partial x^{2}}(x,t). - -In the above :math:`x\in \Omega=[0, 64]` represents a spatial location, -:math:`t\in\mathbb{T}=[0,50]` the time and :math:`u(x, t)` is the value -of the function :math:`u:\Omega \times\mathbb{T}\in\mathbb{R}`. We -indicate with :math:`\mathbb{U}` a suitable space for :math:`u`, i.e. we -have that the solution :math:`u\in\mathbb{U}`. - -We impose Dirichlet boundary conditions on the derivative of :math:`u` -on the border of the domain :math:`\partial \Omega` - -.. math:: - - - \frac{\partial u}{\partial x}(x,t)=0 \quad \forall (x,t)\in \partial \Omega\times\mathbb{T}. - - -Initial conditions are sampled from a distribution over truncated -Fourier series with random coefficients -:math:`\{A_k, \ell_k, \phi_k\}_k` as - -.. math:: - - - u(x,0) = \sum_{k=1}^N A_k \sin(2 \pi \ell_k x / L + \phi_k) \ , - -where :math:`A_k \in [-0.4, -0.3]`, :math:`\ell_k = 2`, -:math:`\phi_k = 2\pi \quad \forall k=1,\dots,N`. - -We have already generated some data for differenti initial conditions, -and our objective will be to build a Neural Operator that, given -:math:`u(x, t)` will output :math:`u(x, t+\delta)`, where :math:`\delta` -is a fixed time step. We will come back on the Neural Operator -architecture, for now we first need to import the data. - -**Note:** *The numerical integration is obtained by using pseudospectral -method for spatial derivative discratization and implicit Runge Kutta 5 -for temporal dynamics.* - -.. code:: ipython3 - - # load data - data=io.loadmat("dat/Data_KS.mat") - - # converting to label tensor - initial_cond_train = LabelTensor(torch.tensor(data['initial_cond_train'], dtype=torch.float), ['t','x','u0']) - initial_cond_test = LabelTensor(torch.tensor(data['initial_cond_test'], dtype=torch.float), ['t','x','u0']) - sol_train = LabelTensor(torch.tensor(data['sol_train'], dtype=torch.float), ['u']) - sol_test = LabelTensor(torch.tensor(data['sol_test'], dtype=torch.float), ['u']) - - print('Data Loaded') - print(f' shape initial condition: {initial_cond_train.shape}') - print(f' shape solution: {sol_train.shape}') - - -.. parsed-literal:: - - Data Loaded - shape initial condition: torch.Size([100, 12800, 3]) - shape solution: torch.Size([100, 12800, 1]) - - -The data are saved in the form ``B \times N \times D``, where ``B`` is -the batch_size (basically how many initial conditions we sample), ``N`` -the number of points in the mesh (which is the product of the -discretization in ``x`` timese the one in ``t``), and ``D`` the -dimension of the problem (in this case we have three variables -``[u, t, x]``). - -We are now going to plot some trajectories! - -.. code:: ipython3 - - # helper function - def plot_trajectory(coords, real, no_sol=None): - # find the x-t shapes - dim_x = len(torch.unique(coords.extract('x'))) - dim_t = len(torch.unique(coords.extract('t'))) - # if we don't have the Neural Operator solution we simply plot the real one - if no_sol is None: - fig, axs = plt.subplots(1, 1, figsize=(15, 5), sharex=True, sharey=True) - c = axs.imshow(real.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto') - axs.set_title('Real solution') - fig.colorbar(c, ax=axs) - axs.set_xlabel('t') - axs.set_ylabel('x') - # otherwise we plot the real one, the Neural Operator one, and their difference - else: - fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharex=True, sharey=True) - axs[0].imshow(real.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto') - axs[0].set_title('Real solution') - axs[1].imshow(no_sol.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto') - axs[1].set_title('NO solution') - c = axs[2].imshow((real - no_sol).abs().reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto') - axs[2].set_title('Absolute difference') - fig.colorbar(c, ax=axs.ravel().tolist()) - for ax in axs: - ax.set_xlabel('t') - ax.set_ylabel('x') - plt.show() - - # a sample trajectory (we use the sample 5, feel free to change) - sample_number = 20 - plot_trajectory(coords=initial_cond_train[sample_number].extract(['x', 't']), - real=sol_train[sample_number].extract('u')) - - - - -.. image:: tutorial_files/tutorial_5_0.png - - -As we can see, as the time progresses the solution becomes chaotic, -which makes it really hard to learn! We will now focus on building a -Neural Operator using the ``SupervisedSolver`` class to tackle the -problem. - -Averaging Neural Operator -------------------------- - -We will build a neural operator :math:`\texttt{NO}` which takes the -solution at time :math:`t=0` for any :math:`x\in\Omega`, the time -:math:`(t)` at which we want to compute the solution, and gives back the -solution to the KS equation :math:`u(x, t)`, mathematically: - -.. math:: - - - \texttt{NO}_\theta : \mathbb{U} \rightarrow \mathbb{U}, - -such that - -.. math:: - - - \texttt{NO}_\theta[u(t=0)](x, t) \rightarrow u(x, t). - -There are many ways on approximating the following operator, e.g. by 2D -`FNO `__ (for -regular meshes), a -`DeepOnet `__, -`Continuous Convolutional Neural -Operator `__, -`MIONet `__. In -this tutorial we will use the *Averaging Neural Operator* presented in -`The Nonlocal Neural Operator: Universal -Approximation `__ which is a `Kernel -Neural -Operator `__ -with integral kernel: - -.. math:: - - - K(v) = \sigma\left(Wv(x) + b + \frac{1}{|\Omega|}\int_\Omega v(y)dy\right) - -where: - -- :math:`v(x)\in\mathbb{R}^{\rm{emb}}` is the update for a function - :math:`v` with :math:`\mathbb{R}^{\rm{emb}}` the embedding (hidden) - size -- :math:`\sigma` is a non-linear activation -- :math:`W\in\mathbb{R}^{\rm{emb}\times\rm{emb}}` is a tunable matrix. -- :math:`b\in\mathbb{R}^{\rm{emb}}` is a tunable bias. - -If PINA many Kernel Neural Operators are already implemented, and the -modular componets of the `Kernel Neural -Operator `__ -class permits to create new ones by composing base kernel layers. - -**Note:**\ \* We will use the already built class\* -``AveragingNeuralOperator``, *as constructive excercise try to use the* -`KernelNeuralOperator `__ -*class for building a kernel neural operator from scratch. You might -employ the different layers that we have in pina, e.g.* -`FeedForward `__, -*and* -`AveragingNeuralOperator `__ -*layers*. - -.. code:: ipython3 - - class SIREN(torch.nn.Module): - def forward(self, x): - return torch.sin(x) - - embedding_dimesion = 40 # hyperparameter embedding dimension - input_dimension = 3 # ['u', 'x', 't'] - number_of_coordinates = 2 # ['x', 't'] - lifting_net = torch.nn.Linear(input_dimension, embedding_dimesion) # simple linear layers for lifting and projecting nets - projecting_net = torch.nn.Linear(embedding_dimesion + number_of_coordinates, 1) - model = AveragingNeuralOperator(lifting_net=lifting_net, - projecting_net=projecting_net, - coordinates_indices=['x', 't'], - field_indices=['u0'], - n_layers=4, - func=SIREN - ) - -Super easy! Notice that we use the ``SIREN`` activation function, more -on `Implicit Neural Representations with Periodic Activation -Functions `__. - -Solving the KS problem ----------------------- - -We will now focus on solving the KS equation using the -``SupervisedSolver`` class and the ``AveragingNeuralOperator`` model. As -done in the `FNO -tutorial `__ -we now create the ``NeuralOperatorProblem`` class with -``AbstractProblem``. - -.. code:: ipython3 - - # expected running time ~ 1 minute - - class NeuralOperatorProblem(AbstractProblem): - input_variables = initial_cond_train.labels - output_variables = sol_train.labels - conditions = {'data' : Condition(input_points=initial_cond_train, - output_points=sol_train)} - - - # initialize problem - problem = NeuralOperatorProblem() - # initialize solver - solver = SupervisedSolver(problem=problem, model=model,optimizer_kwargs={"lr":0.001}) - # train, only CPU and avoid model summary at beginning of training (optional) - trainer = Trainer(solver=solver, max_epochs=40, accelerator='cpu', enable_model_summary=False, log_every_n_steps=-1, batch_size=5) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - -.. parsed-literal:: - - Epoch 39: 100%|██████████| 20/20 [00:01<00:00, 13.59it/s, v_num=3, mean_loss=0.118] - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=40` reached. - - -.. parsed-literal:: - - Epoch 39: 100%|██████████| 20/20 [00:01<00:00, 13.56it/s, v_num=3, mean_loss=0.118] - - -We can now see some plots for the solutions - -.. code:: ipython3 - - sample_number = 2 - no_sol = solver(initial_cond_test) - plot_trajectory(coords=initial_cond_test[sample_number].extract(['x', 't']), - real=sol_test[sample_number].extract('u'), - no_sol=no_sol[5]) - - - -.. image:: tutorial_files/tutorial_11_0.png - - -As we can see we can obtain nice result considering the small trainint -time and the difficulty of the problem! Let’s see how the training and -testing error: - -.. code:: ipython3 - - from pina.loss import PowerLoss - - error_metric = PowerLoss(p=2) # we use the MSE loss - - with torch.no_grad(): - no_sol_train = solver(initial_cond_train) - err_train = error_metric(sol_train.extract('u'), no_sol_train).mean() # we average the error over trajectories - no_sol_test = solver(initial_cond_test) - err_test = error_metric(sol_test.extract('u'),no_sol_test).mean() # we average the error over trajectories - print(f'Training error: {float(err_train):.3f}') - print(f'Testing error: {float(err_test):.3f}') - - -.. parsed-literal:: - - Training error: 0.128 - Testing error: 0.119 - - -as we can see the error is pretty small, which agrees with what we can -see from the previous plots. - -What’s next? ------------- - -Now you know how to solve a time dependent neural operator problem in -**PINA**! There are multiple directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. We left a more challenging dataset - `Data_KS2.mat `__ where - :math:`A_k \in [-0.5, 0.5]`, :math:`\ell_k \in [1, 2, 3]`, - :math:`\phi_k \in [0, 2\pi]` for loger training - -3. Compare the performance between the different neural operators (you - can even try to implement your favourite one!) diff --git a/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_11_0.png b/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_11_0.png deleted file mode 100644 index 2f7e8cc0a..000000000 Binary files a/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_11_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_5_0.png b/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_5_0.png deleted file mode 100644 index 0b355c37a..000000000 Binary files a/docs/source/_rst/tutorials/tutorial10/tutorial_files/tutorial_5_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial11/logging.png b/docs/source/_rst/tutorials/tutorial11/logging.png deleted file mode 100644 index c4b421e19..000000000 Binary files a/docs/source/_rst/tutorials/tutorial11/logging.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial11/tutorial.rst b/docs/source/_rst/tutorials/tutorial11/tutorial.rst deleted file mode 100644 index daed289c4..000000000 --- a/docs/source/_rst/tutorials/tutorial11/tutorial.rst +++ /dev/null @@ -1,550 +0,0 @@ -Tutorial: PINA and PyTorch Lightning, training tips and visualizations -====================================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial11/tutorial.ipynb - -In this tutorial, we will delve deeper into the functionality of the -``Trainer`` class, which serves as the cornerstone for training **PINA** -`Solvers `__. -The ``Trainer`` class offers a plethora of features aimed at improving -model accuracy, reducing training time and memory usage, facilitating -logging visualization, and more thanks to the amazing job done by the PyTorch Lightning team! -Our leading example will revolve around solving the ``SimpleODE`` -problem, as outlined in the `Introduction to PINA for Physics Informed -Neural Networks -training `__. -If you haven’t already explored it, we highly recommend doing so before -diving into this tutorial. -Let’s start by importing useful modules, define the ``SimpleODE`` -problem and the ``PINN`` solver. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - - from pina import Condition, Trainer - from pina.solvers import PINN - from pina.model import FeedForward - from pina.problem import SpatialProblem - from pina.operators import grad - from pina.geometry import CartesianDomain - from pina.equation import Equation, FixedValue - - class SimpleODE(SpatialProblem): - - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1]}) - - # defining the ode equation - def ode_equation(input_, output_): - u_x = grad(output_, input_, components=['u'], d=['x']) - u = output_.extract(['u']) - return u_x - u - - # conditions to hold - conditions = { - 'x0': Condition(location=CartesianDomain({'x': 0.}), equation=FixedValue(1)), # We fix initial condition to value 1 - 'D': Condition(location=CartesianDomain({'x': [0, 1]}), equation=Equation(ode_equation)), # We wrap the python equation using Equation - } - - # defining the true solution - def truth_solution(self, pts): - return torch.exp(pts.extract(['x'])) - - - # sampling for training - problem = SimpleODE() - problem.discretise_domain(1, 'random', locations=['x0']) - problem.discretise_domain(20, 'lh', locations=['D']) - - # build the model - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - - # create the PINN object - pinn = PINN(problem, model) - -Till now we just followed the extact step of the previous tutorials. The -``Trainer`` object can be initialized by simiply passing the ``PINN`` -solver - -.. code:: ipython3 - - trainer = Trainer(solver=pinn) - - -.. parsed-literal:: - - GPU available: True (mps), used: True - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - -Trainer Accelerator -------------------- - -When creating the trainer, **by defualt** the ``Trainer`` will choose -the most performing ``accelerator`` for training which is available in -your system, ranked as follow: - -1. `TPU `__ -2. `IPU `__ -3. `HPU `__ -4. `GPU `__ or `MPS `__ -5. CPU - -For setting manually the ``accelerator`` run: - -- ``accelerator = {'gpu', 'cpu', 'hpu', 'mps', 'cpu', 'ipu'}`` sets the - accelerator to a specific one - -.. code:: ipython3 - - trainer = Trainer(solver=pinn, - accelerator='cpu') - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - -as you can see, even if in the used system ``GPU`` is available, it is -not used since we set ``accelerator='cpu'``. - -Trainer Logging ---------------- - -In **PINA** you can log metrics in different ways. The simplest approach -is to use the ``MetricTraker`` class from ``pina.callbacks`` as seen in -the `Introduction to PINA for Physics Informed Neural Networks -training `__ -tutorial. - -However, expecially when we need to train multiple times to get an -average of the loss across multiple runs, ``pytorch_lightning.loggers`` -might be useful. Here we will use ``TensorBoardLogger`` (more on -`logging `__ -here), but you can choose the one you prefer (or make your own one). - -We will now import ``TensorBoardLogger``, do three runs of training and -then visualize the results. Notice we set ``enable_model_summary=False`` -to avoid model summary specifications (e.g. number of parameters), set -it to true if needed. - -.. code:: ipython3 - - from pytorch_lightning.loggers import TensorBoardLogger - - # three run of training, by default it trains for 1000 epochs - # we reinitialize the model each time otherwise the same parameters will be optimized - for _ in range(3): - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - logger=TensorBoardLogger(save_dir='simpleode'), - enable_model_summary=False) - trainer.train() - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - `Trainer.fit` stopped: `max_epochs=1000` reached. - Epoch 999: 100%|██████████| 1/1 [00:00<00:00, 133.46it/s, v_num=6, x0_loss=1.48e-5, D_loss=0.000655, mean_loss=0.000335] - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - `Trainer.fit` stopped: `max_epochs=1000` reached. - Epoch 999: 100%|██████████| 1/1 [00:00<00:00, 154.49it/s, v_num=7, x0_loss=6.21e-6, D_loss=0.000221, mean_loss=0.000114] - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - `Trainer.fit` stopped: `max_epochs=1000` reached. - Epoch 999: 100%|██████████| 1/1 [00:00<00:00, 62.60it/s, v_num=8, x0_loss=1.44e-5, D_loss=0.000572, mean_loss=0.000293] - - -We can now visualize the logs by simply running -``tensorboard --logdir=simpleode/`` on terminal, you should obtain a -webpage as the one shown below: - -.. image:: logging.png - -as you can see, by default, **PINA** logs the losses which are shown in -the progress bar, as well as the number of epochs. You can always insert -more loggings by either defining a **callback** (`more on -callbacks `__), -or inheriting the solver and modify the programs with different -**hooks** (`more on -hooks `__). - -Trainer Callbacks ------------------ - -Whenever we need to access certain steps of the training for logging, do -static modifications (i.e. not changing the ``Solver``) or updating -``Problem`` hyperparameters (static variables), we can use -``Callabacks``. Notice that ``Callbacks`` allow you to add arbitrary -self-contained programs to your training. At specific points during the -flow of execution (hooks), the Callback interface allows you to design -programs that encapsulate a full set of functionality. It de-couples -functionality that does not need to be in **PINA** ``Solver``\ s. -Lightning has a callback system to execute them when needed. Callbacks -should capture NON-ESSENTIAL logic that is NOT required for your -lightning module to run. - -The following are best practices when using/designing callbacks. - -- Callbacks should be isolated in their functionality. -- Your callback should not rely on the behavior of other callbacks in - order to work properly. -- Do not manually call methods from the callback. -- Directly calling methods (eg. on_validation_end) is strongly - discouraged. -- Whenever possible, your callbacks should not depend on the order in - which they are executed. - -We will try now to implement a naive version of ``MetricTraker`` to show -how callbacks work. Notice that this is a very easy application of -callbacks, fortunately in **PINA** we already provide more advanced -callbacks in ``pina.callbacks``. - -.. raw:: html - - - -.. code:: ipython3 - - from pytorch_lightning.callbacks import Callback - import torch - - # define a simple callback - class NaiveMetricTracker(Callback): - def __init__(self): - self.saved_metrics = [] - - def on_train_epoch_end(self, trainer, __): # function called at the end of each epoch - self.saved_metrics.append( - {key: value for key, value in trainer.logged_metrics.items()} - ) - -Let’s see the results when applyed to the ``SimpleODE`` problem. You can -define callbacks when initializing the ``Trainer`` by the ``callbacks`` -argument, which expects a list of callbacks. - -.. code:: ipython3 - - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - enable_model_summary=False, - callbacks=[NaiveMetricTracker()]) # adding a callbacks - trainer.train() - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - `Trainer.fit` stopped: `max_epochs=1000` reached. - Epoch 999: 100%|██████████| 1/1 [00:00<00:00, 149.27it/s, v_num=1, x0_loss=7.27e-5, D_loss=0.0016, mean_loss=0.000838] - - -We can easily access the data by calling -``trainer.callbacks[0].saved_metrics`` (notice the zero representing the -first callback in the list given at initialization). - -.. code:: ipython3 - - trainer.callbacks[0].saved_metrics[:3] # only the first three epochs - - - - -.. parsed-literal:: - - [{'x0_loss': tensor(0.9141), - 'D_loss': tensor(0.0304), - 'mean_loss': tensor(0.4722)}, - {'x0_loss': tensor(0.8906), - 'D_loss': tensor(0.0287), - 'mean_loss': tensor(0.4596)}, - {'x0_loss': tensor(0.8674), - 'D_loss': tensor(0.0274), - 'mean_loss': tensor(0.4474)}] - - - -PyTorch Lightning also has some built in ``Callbacks`` which can be used -in **PINA**, `here an extensive -list `__. - -We can for example try the ``EarlyStopping`` routine, which -automatically stops the training when a specific metric converged (here -the ``mean_loss``). In order to let the training keep going forever set -``max_epochs=-1``. - -.. code:: ipython3 - - # ~2 mins - from pytorch_lightning.callbacks import EarlyStopping - - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - max_epochs = -1, - enable_model_summary=False, - callbacks=[EarlyStopping('mean_loss')]) # adding a callbacks - trainer.train() - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - Epoch 6157: 100%|██████████| 1/1 [00:00<00:00, 139.84it/s, v_num=9, x0_loss=4.21e-9, D_loss=9.93e-6, mean_loss=4.97e-6] - - -As we can see the model automatically stop when the logging metric -stopped improving! - -Trainer Tips to Boost Accuracy, Save Memory and Speed Up Training ------------------------------------------------------------------ - -Untill now we have seen how to choose the right ``accelerator``, how to -log and visualize the results, and how to interface with the program in -order to add specific parts of code at specific points by ``callbacks``. -Now, we well focus on how boost your training by saving memory and -speeding it up, while mantaining the same or even better degree of -accuracy! - -There are several built in methods developed in PyTorch Lightning which -can be applied straight forward in **PINA**, here we report some: - -- `Stochastic Weight - Averaging `__ - to boost accuracy -- `Gradient - Clippling `__ to - reduce computational time (and improve accuracy) -- `Gradient - Accumulation `__ - to save memory consumption -- `Mixed Precision - Training `__ - to save memory consumption - -We will just demonstrate how to use the first two, and see the results -compared to a standard training. We use the -`Timer `__ -callback from ``pytorch_lightning.callbacks`` to take the times. Let’s -start by training a simple model without any optimization (train for -2000 epochs). - -.. code:: ipython3 - - from pytorch_lightning.callbacks import Timer - from pytorch_lightning import seed_everything - - # setting the seed for reproducibility - seed_everything(42, workers=True) - - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - deterministic=True, # setting deterministic=True ensure reproducibility when a seed is imposed - max_epochs = 2000, - enable_model_summary=False, - callbacks=[Timer()]) # adding a callbacks - trainer.train() - print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') - - -.. parsed-literal:: - - Seed set to 42 - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - - `Trainer.fit` stopped: `max_epochs=2000` reached. - Epoch 1999: 100%|██████████| 1/1 [00:00<00:00, 163.58it/s, v_num=31, x0_loss=1.12e-6, D_loss=0.000127, mean_loss=6.4e-5] - Total training time 17.36381 s - - -Now we do the same but with StochasticWeightAveraging - -.. code:: ipython3 - - from pytorch_lightning.callbacks import StochasticWeightAveraging - - # setting the seed for reproducibility - seed_everything(42, workers=True) - - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - deterministic=True, - max_epochs = 2000, - enable_model_summary=False, - callbacks=[Timer(), - StochasticWeightAveraging(swa_lrs=0.005)]) # adding StochasticWeightAveraging callbacks - trainer.train() - print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') - - -.. parsed-literal:: - - Seed set to 42 - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - - Epoch 1598: 100%|██████████| 1/1 [00:00<00:00, 210.04it/s, v_num=47, x0_loss=4.17e-6, D_loss=0.000204, mean_loss=0.000104] - Swapping scheduler `ConstantLR` for `SWALR` - `Trainer.fit` stopped: `max_epochs=2000` reached. - Epoch 1999: 100%|██████████| 1/1 [00:00<00:00, 120.85it/s, v_num=47, x0_loss=1.56e-7, D_loss=7.49e-5, mean_loss=3.75e-5] - Total training time 17.10627 s - - -As you can see, the training time does not change at all! Notice that -around epoch ``1600`` the scheduler is switched from the defalut one -``ConstantLR`` to the Stochastic Weight Average Learning Rate -(``SWALR``). This is because by default ``StochasticWeightAveraging`` -will be activated after ``int(swa_epoch_start * max_epochs)`` with -``swa_epoch_start=0.7`` by default. Finally, the final ``mean_loss`` is -lower when ``StochasticWeightAveraging`` is used. - -We will now now do the same but clippling the gradient to be relatively -small. - -.. code:: ipython3 - - # setting the seed for reproducibility - seed_everything(42, workers=True) - - model = FeedForward( - layers=[10, 10], - func=torch.nn.Tanh, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model) - trainer = Trainer(solver=pinn, - accelerator='cpu', - max_epochs = 2000, - enable_model_summary=False, - gradient_clip_val=0.1, # clipping the gradient - callbacks=[Timer(), - StochasticWeightAveraging(swa_lrs=0.005)]) - trainer.train() - print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') - - -.. parsed-literal:: - - Seed set to 42 - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - Epoch 1598: 100%|██████████| 1/1 [00:00<00:00, 261.80it/s, v_num=46, x0_loss=9e-8, D_loss=2.39e-5, mean_loss=1.2e-5] - Swapping scheduler `ConstantLR` for `SWALR` - `Trainer.fit` stopped: `max_epochs=2000` reached. - Epoch 1999: 100%|██████████| 1/1 [00:00<00:00, 148.99it/s, v_num=46, x0_loss=7.08e-7, D_loss=1.77e-5, mean_loss=9.19e-6] - Total training time 17.01149 s - - -As we can see we by applying gradient clipping we were able to even -obtain lower error! - -What’s next? ------------- - -Now you know how to use efficiently the ``Trainer`` class **PINA**! -There are multiple directions you can go now: - -1. Explore training times on different devices (e.g.) ``TPU`` - -2. Try to reduce memory cost by mixed precision training and gradient - accumulation (especially useful when training Neural Operators) - -3. Benchmark ``Trainer`` speed for different precisions. diff --git a/docs/source/_rst/tutorials/tutorial12/tutorial.rst b/docs/source/_rst/tutorials/tutorial12/tutorial.rst deleted file mode 100644 index 054213259..000000000 --- a/docs/source/_rst/tutorials/tutorial12/tutorial.rst +++ /dev/null @@ -1,176 +0,0 @@ -Tutorial: The ``Equation`` Class -================================ - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial12/tutorial.ipynb - -In this tutorial, we will show how to use the ``Equation`` Class in -PINA. Specifically, we will see how use the Class and its inherited -classes to enforce residuals minimization in PINNs. - -Example: The Burgers 1D equation --------------------------------- - -We will start implementing the viscous Burgers 1D problem Class, -described as follows: - -.. math:: - - - \begin{equation} - \begin{cases} - \frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} &= \nu \frac{\partial^2 u}{ \partial x^2}, \quad x\in(0,1), \quad t>0\\ - u(x,0) &= -\sin (\pi x)\\ - u(x,t) &= 0 \quad x = \pm 1\\ - \end{cases} - \end{equation} - -where we set :math:`\nu = \frac{0.01}{\pi}` . - -In the class that models this problem we will see in action the -``Equation`` class and one of its inherited classes, the ``FixedValue`` -class. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - #useful imports - from pina.problem import SpatialProblem, TimeDependentProblem - from pina.equation import Equation, FixedValue, FixedGradient, FixedFlux - from pina.geometry import CartesianDomain - import torch - from pina.operators import grad, laplacian - from pina import Condition - - - -.. code:: ipython3 - - class Burgers1D(TimeDependentProblem, SpatialProblem): - - # define the burger equation - def burger_equation(input_, output_): - du = grad(output_, input_) - ddu = grad(du, input_, components=['dudx']) - return ( - du.extract(['dudt']) + - output_.extract(['u'])*du.extract(['dudx']) - - (0.01/torch.pi)*ddu.extract(['ddudxdx']) - ) - - # define initial condition - def initial_condition(input_, output_): - u_expected = -torch.sin(torch.pi*input_.extract(['x'])) - return output_.extract(['u']) - u_expected - - # assign output/ spatial and temporal variables - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [-1, 1]}) - temporal_domain = CartesianDomain({'t': [0, 1]}) - - # problem condition statement - conditions = { - 'gamma1': Condition(location=CartesianDomain({'x': -1, 't': [0, 1]}), equation=FixedValue(0.)), - 'gamma2': Condition(location=CartesianDomain({'x': 1, 't': [0, 1]}), equation=FixedValue(0.)), - 't0': Condition(location=CartesianDomain({'x': [-1, 1], 't': 0}), equation=Equation(initial_condition)), - 'D': Condition(location=CartesianDomain({'x': [-1, 1], 't': [0, 1]}), equation=Equation(burger_equation)), - } - -The ``Equation`` class takes as input a function (in this case it -happens twice, with ``initial_condition`` and ``burger_equation``) which -computes a residual of an equation, such as a PDE. In a problem class -such as the one above, the ``Equation`` class with such a given input is -passed as a parameter in the specified ``Condition``. - -The ``FixedValue`` class takes as input a value of same dimensions of -the output functions; this class can be used to enforced a fixed value -for a specific condition, e.g. Dirichlet boundary conditions, as it -happens for instance in our example. - -Once the equations are set as above in the problem conditions, the PINN -solver will aim to minimize the residuals described in each equation in -the training phase. - -Available classes of equations include also: - ``FixedGradient`` and -``FixedFlux``: they work analogously to ``FixedValue`` class, where we -can require a constant value to be enforced, respectively, on the -gradient of the solution or the divergence of the solution; - -``Laplace``: it can be used to enforce the laplacian of the solution to -be zero; - ``SystemEquation``: we can enforce multiple conditions on the -same subdomain through this class, passing a list of residual equations -defined in the problem. - -Defining a new Equation class ------------------------------ - -``Equation`` classes can be also inherited to define a new class. As -example, we can see how to rewrite the above problem introducing a new -class ``Burgers1D``; during the class call, we can pass the viscosity -parameter :math:`\nu`: - -.. code:: ipython3 - - class Burgers1DEquation(Equation): - - def __init__(self, nu = 0.): - """ - Burgers1D class. This class can be - used to enforce the solution u to solve the viscous Burgers 1D Equation. - - :param torch.float32 nu: the viscosity coefficient. Default value is set to 0. - """ - self.nu = nu - - def equation(input_, output_): - return grad(output_, input_, d='t') +\ - output_*grad(output_, input_, d='x') -\ - self.nu*laplacian(output_, input_, d='x') - - - super().__init__(equation) - -Now we can just pass the above class as input for the last condition, -setting :math:`\nu= \frac{0.01}{\pi}`: - -.. code:: ipython3 - - class Burgers1D(TimeDependentProblem, SpatialProblem): - - # define initial condition - def initial_condition(input_, output_): - u_expected = -torch.sin(torch.pi*input_.extract(['x'])) - return output_.extract(['u']) - u_expected - - # assign output/ spatial and temporal variables - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [-1, 1]}) - temporal_domain = CartesianDomain({'t': [0, 1]}) - - # problem condition statement - conditions = { - 'gamma1': Condition(location=CartesianDomain({'x': -1, 't': [0, 1]}), equation=FixedValue(0.)), - 'gamma2': Condition(location=CartesianDomain({'x': 1, 't': [0, 1]}), equation=FixedValue(0.)), - 't0': Condition(location=CartesianDomain({'x': [-1, 1], 't': 0}), equation=Equation(initial_condition)), - 'D': Condition(location=CartesianDomain({'x': [-1, 1], 't': [0, 1]}), equation=Burgers1DEquation(0.01/torch.pi)), - } - -What’s next? ------------- - -Congratulations on completing the ``Equation`` class tutorial of -**PINA**! As we have seen, you can build new classes that inherits -``Equation`` to store more complex equations, as the Burgers 1D -equation, only requiring to pass the characteristic coefficients of the -problem. From now on, you can: - define additional complex equation -classes (e.g. ``SchrodingerEquation``, ``NavierStokeEquation``..) - -define more ``FixedOperator`` (e.g. ``FixedCurl``) diff --git a/docs/source/_rst/tutorials/tutorial13/tutorial.rst b/docs/source/_rst/tutorials/tutorial13/tutorial.rst deleted file mode 100644 index 1b932909f..000000000 --- a/docs/source/_rst/tutorials/tutorial13/tutorial.rst +++ /dev/null @@ -1,327 +0,0 @@ -Tutorial: Multiscale PDE learning with Fourier Feature Network -============================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial13/tutorial.ipynb - -This tutorial presents how to solve with Physics-Informed Neural -Networks (PINNs) a PDE characterized by multiscale behaviour, as -presented in `On the eigenvector bias of Fourier feature networks: From -regression to solving multi-scale PDEs with physics-informed neural -networks `__. - -First of all, some useful imports. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - - from pina import Condition, Plotter, Trainer, Plotter - from pina.problem import SpatialProblem - from pina.operators import laplacian - from pina.solvers import PINN, SAPINN - from pina.model.layers import FourierFeatureEmbedding - from pina.loss import LpLoss - from pina.geometry import CartesianDomain - from pina.equation import Equation, FixedValue - from pina.model import FeedForward - - -Multiscale Problem ------------------- - -We begin by presenting the problem which also can be found in Section 2 -of `On the eigenvector bias of Fourier feature networks: From regression -to solving multi-scale PDEs with physics-informed neural -networks `__. The -one-dimensional Poisson problem we aim to solve is mathematically -written as: - -.. math:: - - \begin{equation} - \begin{cases} - \Delta u (x) + f(x) = 0 \quad x \in [0,1], \\ - u(x) = 0 \quad x \in \partial[0,1], \\ - \end{cases} - \end{equation} - -We impose the solution as -:math:`u(x) = \sin(2\pi x) + 0.1 \sin(50\pi x)` and obtain the force -term -:math:`f(x) = (2\pi)^2 \sin(2\pi x) + 0.1 (50 \pi)^2 \sin(50\pi x)`. -Though this example is simple and pedagogical, it is worth noting that -the solution exhibits low frequency in the macro-scale and high -frequency in the micro-scale, which resembles many practical scenarios. - -In **PINA** this problem is written, as always, as a class `see here for -a tutorial on the Problem -class `__. -Below you can find the ``Poisson`` problem which is mathmatically -described above. - -.. code:: ipython3 - - class Poisson(SpatialProblem): - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1]}) - - def poisson_equation(input_, output_): - x = input_.extract('x') - u_xx = laplacian(output_, input_, components=['u'], d=['x']) - f = ((2*torch.pi)**2)*torch.sin(2*torch.pi*x) + 0.1*((50*torch.pi)**2)*torch.sin(50*torch.pi*x) - return u_xx + f - - # here we write the problem conditions - conditions = { - 'gamma0' : Condition(location=CartesianDomain({'x': 0}), - equation=FixedValue(0)), - 'gamma1' : Condition(location=CartesianDomain({'x': 1}), - equation=FixedValue(0)), - 'D': Condition(location=spatial_domain, - equation=Equation(poisson_equation)), - } - - def truth_solution(self, x): - return torch.sin(2*torch.pi*x) + 0.1*torch.sin(50*torch.pi*x) - - problem = Poisson() - - # let's discretise the domain - problem.discretise_domain(128, 'grid') - -A standard PINN approach would be to fit this model using a Feed Forward -(fully connected) Neural Network. For a conventional fully-connected -neural network is easy to approximate a function :math:`u`, given -sufficient data inside the computational domain. However solving -high-frequency or multi-scale problems presents great challenges to -PINNs especially when the number of data cannot capture the different -scales. - -Below we run a simulation using the ``PINN`` solver and the self -adaptive ``SAPINN`` solver, using a -``FeedForward`` model. We used a ``MultiStepLR`` scheduler to decrease the learning rate -slowly during training (it takes around 2 minutes to run on CPU). - -.. code:: ipython3 - - # training with PINN and visualize results - pinn = PINN(problem=problem, - model=FeedForward(input_dimensions=1, output_dimensions=1, layers=[100, 100, 100]), - scheduler=torch.optim.lr_scheduler.MultiStepLR, - scheduler_kwargs={'milestones' : [1000, 2000, 3000, 4000], 'gamma':0.9}) - trainer = Trainer(pinn, max_epochs=5000, accelerator='cpu', enable_model_summary=False) - trainer.train() - - # training with PINN and visualize results - sapinn = SAPINN(problem=problem, - model=FeedForward(input_dimensions=1, output_dimensions=1, layers=[100, 100, 100]), - scheduler_model=torch.optim.lr_scheduler.MultiStepLR, - scheduler_model_kwargs={'milestones' : [1000, 2000, 3000, 4000], 'gamma':0.9}) - trainer_sapinn = Trainer(sapinn, max_epochs=5000, accelerator='cpu', enable_model_summary=False) - trainer_sapinn.train() - - # plot results - pl = Plotter() - pl.plot(pinn, title='PINN Solution') - pl.plot(sapinn, title='Self Adaptive PINN Solution') - - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - Epoch 4999: 100%|██████████| 1/1 [00:00<00:00, 97.66it/s, v_num=69, gamma0_loss=2.61e+3, gamma1_loss=2.61e+3, D_loss=409.0, mean_loss=1.88e+3] - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - Epoch 4999: 100%|██████████| 1/1 [00:00<00:00, 65.77it/s, v_num=70, gamma0_loss=151.0, gamma1_loss=148.0, D_loss=6.38e+5, mean_loss=2.13e+5] - - - -.. image:: tutorial_files/tutorial_5_8.png - - - -.. image:: tutorial_files/tutorial_5_9.png - - -We can clearly see that the solution has not been learned by the two -different solvers. Indeed the big problem is not in the optimization -strategy (i.e. the solver), but in the model used to solve the problem. -A simple ``FeedForward`` network can hardly handle multiscales if not -enough collocation points are used! - -We can also compute the :math:`l_2` relative error for the ``PINN`` and -``SAPINN`` solutions: - -.. code:: ipython3 - - # l2 loss from PINA losses - l2_loss = LpLoss(p=2, relative=True) - - # sample new test points - pts = pts = problem.spatial_domain.sample(100, 'grid') - print(f'Relative l2 error PINN {l2_loss(pinn(pts), problem.truth_solution(pts)).item():.2%}') - print(f'Relative l2 error SAPINN {l2_loss(sapinn(pts), problem.truth_solution(pts)).item():.2%}') - - -.. parsed-literal:: - - Relative l2 error PINN 95.76% - Relative l2 error SAPINN 124.26% - - -Which is indeed very high! - -Fourier Feature Embedding in PINA ---------------------------------- - -Fourier Feature Embedding is a way to transform the input features, to -help the network in learning multiscale variations in the output. It was -first introduced in `On the eigenvector bias of Fourier feature -networks: From regression to solving multi-scale PDEs with -physics-informed neural -networks `__ showing great -results for multiscale problems. The basic idea is to map the input -:math:`\mathbf{x}` into an embedding :math:`\tilde{\mathbf{x}}` where: - -.. math:: \tilde{\mathbf{x}} =\left[\cos\left( \mathbf{B} \mathbf{x} \right), \sin\left( \mathbf{B} \mathbf{x} \right)\right] - -and :math:`\mathbf{B}_{ij} \sim \mathcal{N}(0, \sigma^2)`. This simple -operation allow the network to learn on multiple scales! - -In PINA we already have implemented the feature as a ``layer`` called -```FourierFeatureEmbedding`` `__. -Below we will build the *Multi-scale Fourier Feature Architecture*. In -this architecture multiple Fourier feature embeddings (initialized with -different :math:`\sigma`) are applied to input coordinates and then -passed through the same fully-connected neural network, before the -outputs are finally concatenated with a linear layer. - -.. code:: ipython3 - - class MultiscaleFourierNet(torch.nn.Module): - def __init__(self): - super().__init__() - self.embedding1 = FourierFeatureEmbedding(input_dimension=1, - output_dimension=100, - sigma=1) - self.embedding2 = FourierFeatureEmbedding(input_dimension=1, - output_dimension=100, - sigma=10) - self.layers = FeedForward(input_dimensions=100, output_dimensions=100, layers=[100]) - self.final_layer = torch.nn.Linear(2*100, 1) - - def forward(self, x): - e1 = self.layers(self.embedding1(x)) - e2 = self.layers(self.embedding2(x)) - return self.final_layer(torch.cat([e1, e2], dim=-1)) - - MultiscaleFourierNet() - - - - -.. parsed-literal:: - - MultiscaleFourierNet( - (embedding1): FourierFeatureEmbedding() - (embedding2): FourierFeatureEmbedding() - (layers): FeedForward( - (model): Sequential( - (0): Linear(in_features=100, out_features=100, bias=True) - (1): Tanh() - (2): Linear(in_features=100, out_features=100, bias=True) - ) - ) - (final_layer): Linear(in_features=200, out_features=1, bias=True) - ) - - - -We will train the ``MultiscaleFourierNet`` with the ``PINN`` solver (and -feel free to try also with our PINN variants (``SAPINN``, ``GPINN``, -``CompetitivePINN``, …). - -.. code:: ipython3 - - multiscale_pinn = PINN(problem=problem, - model=MultiscaleFourierNet(), - scheduler=torch.optim.lr_scheduler.MultiStepLR, - scheduler_kwargs={'milestones' : [1000, 2000, 3000, 4000], 'gamma':0.9}) - trainer = Trainer(multiscale_pinn, max_epochs=5000, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - Epoch 4999: 100%|██████████| 1/1 [00:00<00:00, 72.21it/s, v_num=71, gamma0_loss=3.91e-5, gamma1_loss=3.91e-5, D_loss=0.000151, mean_loss=0.000113] - - -Let us now plot the solution and compute the relative :math:`l_2` again! - -.. code:: ipython3 - - # plot the solution - pl.plot(multiscale_pinn, title='Solution PINN with MultiscaleFourierNet') - - # sample new test points - pts = pts = problem.spatial_domain.sample(100, 'grid') - print(f'Relative l2 error PINN with MultiscaleFourierNet {l2_loss(multiscale_pinn(pts), problem.truth_solution(pts)).item():.2%}') - - - -.. image:: tutorial_files/tutorial_15_0.png - - -.. parsed-literal:: - - Relative l2 error PINN with MultiscaleFourierNet 2.72% - - -It is pretty clear that the network has learned the correct solution, -with also a very law error. Obviously a longer training and a more -expressive neural network could improve the results! - -What’s next? ------------- - -Congratulations on completing the one dimensional Poisson tutorial of -**PINA** using ``FourierFeatureEmbedding``! There are multiple -directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. Understand the role of ``sigma`` in ``FourierFeatureEmbedding`` (see - original paper for a nice reference) - -3. Code the *Spatio-temporal multi-scale Fourier feature architecture* - for a more complex time dependent PDE (section 3 of the original - reference) - -4. Many more… diff --git a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_15_0.png b/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_15_0.png deleted file mode 100644 index c6f0e508a..000000000 Binary files a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_15_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_8.png b/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_8.png deleted file mode 100644 index 470a5715a..000000000 Binary files a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_8.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_9.png b/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_9.png deleted file mode 100644 index 1cfc02b1c..000000000 Binary files a/docs/source/_rst/tutorials/tutorial13/tutorial_files/tutorial_5_9.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial2/tutorial.rst b/docs/source/_rst/tutorials/tutorial2/tutorial.rst deleted file mode 100644 index 9ed0eae56..000000000 --- a/docs/source/_rst/tutorials/tutorial2/tutorial.rst +++ /dev/null @@ -1,385 +0,0 @@ -Tutorial: Two dimensional Poisson problem using Extra Features Learning -======================================================================= - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial2/tutorial.ipynb - - -This tutorial presents how to solve with Physics-Informed Neural -Networks (PINNs) a 2D Poisson problem with Dirichlet boundary -conditions. We will train with standard PINN’s training, and with -extrafeatures. For more insights on extrafeature learning please read -`An extended physics informed neural network for preliminary analysis of -parametric optimal control -problems `__. - -First of all, some useful imports. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - from torch.nn import Softplus - - from pina.problem import SpatialProblem - from pina.operators import laplacian - from pina.model import FeedForward - from pina.solvers import PINN - from pina.trainer import Trainer - from pina.plotter import Plotter - from pina.geometry import CartesianDomain - from pina.equation import Equation, FixedValue - from pina import Condition, LabelTensor - from pina.callbacks import MetricTracker - -The problem definition ----------------------- - -The two-dimensional Poisson problem is mathematically written as: - -.. math:: - - \begin{equation} - \begin{cases} - \Delta u = \sin{(\pi x)} \sin{(\pi y)} \text{ in } D, \\ - u = 0 \text{ on } \Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4, - \end{cases} - \end{equation} - -where :math:`D` is a square domain :math:`[0,1]^2`, and -:math:`\Gamma_i`, with :math:`i=1,...,4`, are the boundaries of the -square. - -The Poisson problem is written in **PINA** code as a class. The -equations are written as *conditions* that should be satisfied in the -corresponding domains. The *truth_solution* is the exact solution which -will be compared with the predicted one. - -.. code:: ipython3 - - class Poisson(SpatialProblem): - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) - - def laplace_equation(input_, output_): - force_term = (torch.sin(input_.extract(['x'])*torch.pi) * - torch.sin(input_.extract(['y'])*torch.pi)) - laplacian_u = laplacian(output_, input_, components=['u'], d=['x', 'y']) - return laplacian_u - force_term - - # here we write the problem conditions - conditions = { - 'gamma1': Condition(location=CartesianDomain({'x': [0, 1], 'y': 1}), equation=FixedValue(0.)), - 'gamma2': Condition(location=CartesianDomain({'x': [0, 1], 'y': 0}), equation=FixedValue(0.)), - 'gamma3': Condition(location=CartesianDomain({'x': 1, 'y': [0, 1]}), equation=FixedValue(0.)), - 'gamma4': Condition(location=CartesianDomain({'x': 0, 'y': [0, 1]}), equation=FixedValue(0.)), - 'D': Condition(location=CartesianDomain({'x': [0, 1], 'y': [0, 1]}), equation=Equation(laplace_equation)), - } - - def poisson_sol(self, pts): - return -( - torch.sin(pts.extract(['x'])*torch.pi)* - torch.sin(pts.extract(['y'])*torch.pi) - )/(2*torch.pi**2) - - truth_solution = poisson_sol - - problem = Poisson() - - # let's discretise the domain - problem.discretise_domain(25, 'grid', locations=['D']) - problem.discretise_domain(25, 'grid', locations=['gamma1', 'gamma2', 'gamma3', 'gamma4']) - -Solving the problem with standard PINNs ---------------------------------------- - -After the problem, the feed-forward neural network is defined, through -the class ``FeedForward``. This neural network takes as input the -coordinates (in this case :math:`x` and :math:`y`) and provides the -unkwown field of the Poisson problem. The residual of the equations are -evaluated at several sampling points (which the user can manipulate -using the method ``CartesianDomain_pts``) and the loss minimized by the -neural network is the sum of the residuals. - -In this tutorial, the neural network is composed by two hidden layers of -10 neurons each, and it is trained for 1000 epochs with a learning rate -of 0.006 and :math:`l_2` weight regularization set to :math:`10^{-7}`. -These parameters can be modified as desired. We use the -``MetricTracker`` class to track the metrics during training. - -.. code:: ipython3 - - # make model + solver + trainer - model = FeedForward( - layers=[10, 10], - func=Softplus, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - pinn = PINN(problem, model, optimizer_kwargs={'lr':0.006, 'weight_decay':1e-8}) - trainer = Trainer(pinn, max_epochs=1000, callbacks=[MetricTracker()], accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - - # train - trainer.train() - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 105.33it/s, v_num=3, gamma1_loss=5.29e-5, gamma2_loss=4.09e-5, gamma3_loss=4.73e-5, gamma4_loss=4.18e-5, D_loss=0.00134, mean_loss=0.000304] - - -Now the ``Plotter`` class is used to plot the results. The solution -predicted by the neural network is plotted on the left, the exact one is -represented at the center and on the right the error between the exact -and the predicted solutions is showed. - -.. code:: ipython3 - - plotter = Plotter() - plotter.plot(solver=pinn) - - - -.. image:: tutorial_files/tutorial_9_0.png - - -Solving the problem with extra-features PINNs ---------------------------------------------- - -Now, the same problem is solved in a different way. A new neural network -is now defined, with an additional input variable, named extra-feature, -which coincides with the forcing term in the Laplace equation. The set -of input variables to the neural network is: - -.. math:: - - \begin{equation} - [x, y, k(x, y)], \text{ with } k(x, y)=\sin{(\pi x)}\sin{(\pi y)}, - \end{equation} - -where :math:`x` and :math:`y` are the spatial coordinates and -:math:`k(x, y)` is the added feature. - -This feature is initialized in the class ``SinSin``, which needs to be -inherited by the ``torch.nn.Module`` class and to have the ``forward`` -method. After declaring such feature, we can just incorporate in the -``FeedForward`` class thanks to the ``extra_features`` argument. **NB**: -``extra_features`` always needs a ``list`` as input, you you have one -feature just encapsulated it in a class, as in the next cell. - -Finally, we perform the same training as before: the problem is -``Poisson``, the network is composed by the same number of neurons and -optimizer parameters are equal to previous test, the only change is the -new extra feature. - -.. code:: ipython3 - - class SinSin(torch.nn.Module): - """Feature: sin(x)*sin(y)""" - def __init__(self): - super().__init__() - - def forward(self, x): - t = (torch.sin(x.extract(['x'])*torch.pi) * - torch.sin(x.extract(['y'])*torch.pi)) - return LabelTensor(t, ['sin(x)sin(y)']) - - - # make model + solver + trainer - model_feat = FeedForward( - layers=[10, 10], - func=Softplus, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables)+1 - ) - pinn_feat = PINN(problem, model_feat, extra_features=[SinSin()], optimizer_kwargs={'lr':0.006, 'weight_decay':1e-8}) - trainer_feat = Trainer(pinn_feat, max_epochs=1000, callbacks=[MetricTracker()], accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - - # train - trainer_feat.train() - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 85.62it/s, v_num=4, gamma1_loss=2.54e-7, gamma2_loss=2.17e-7, gamma3_loss=1.94e-7, gamma4_loss=2.69e-7, D_loss=9.2e-6, mean_loss=2.03e-6] - - -The predicted and exact solutions and the error between them are -represented below. We can easily note that now our network, having -almost the same condition as before, is able to reach additional order -of magnitudes in accuracy. - -.. code:: ipython3 - - plotter.plot(solver=pinn_feat) - - - -.. image:: tutorial_files/tutorial_14_0.png - - -Solving the problem with learnable extra-features PINNs -------------------------------------------------------- - -We can still do better! - -Another way to exploit the extra features is the addition of learnable -parameter inside them. In this way, the added parameters are learned -during the training phase of the neural network. In this case, we use: - -.. math:: - - \begin{equation} - k(x, \mathbf{y}) = \beta \sin{(\alpha x)} \sin{(\alpha y)}, - \end{equation} - -where :math:`\alpha` and :math:`\beta` are the abovementioned -parameters. Their implementation is quite trivial: by using the class -``torch.nn.Parameter`` we cam define all the learnable parameters we -need, and they are managed by ``autograd`` module! - -.. code:: ipython3 - - class SinSinAB(torch.nn.Module): - """ """ - def __init__(self): - super().__init__() - self.alpha = torch.nn.Parameter(torch.tensor([1.0])) - self.beta = torch.nn.Parameter(torch.tensor([1.0])) - - - def forward(self, x): - t = ( - self.beta*torch.sin(self.alpha*x.extract(['x'])*torch.pi)* - torch.sin(self.alpha*x.extract(['y'])*torch.pi) - ) - return LabelTensor(t, ['b*sin(a*x)sin(a*y)']) - - - # make model + solver + trainer - model_lean= FeedForward( - layers=[10, 10], - func=Softplus, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables)+1 - ) - pinn_lean = PINN(problem, model_lean, extra_features=[SinSinAB()], optimizer_kwargs={'lr':0.006, 'weight_decay':1e-8}) - trainer_learn = Trainer(pinn_lean, max_epochs=1000, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - - # train - trainer_learn.train() - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 85.94it/s, v_num=5, gamma1_loss=3.26e-8, gamma2_loss=7.84e-8, gamma3_loss=1.13e-7, gamma4_loss=3.02e-8, D_loss=2.66e-6, mean_loss=5.82e-7] - - -Umh, the final loss is not appreciabily better than previous model (with -static extra features), despite the usage of learnable parameters. This -is mainly due to the over-parametrization of the network: there are many -parameter to optimize during the training, and the model in unable to -understand automatically that only the parameters of the extra feature -(and not the weights/bias of the FFN) should be tuned in order to fit -our problem. A longer training can be helpful, but in this case the -faster way to reach machine precision for solving the Poisson problem is -removing all the hidden layers in the ``FeedForward``, keeping only the -:math:`\alpha` and :math:`\beta` parameters of the extra feature. - -.. code:: ipython3 - - # make model + solver + trainer - model_lean= FeedForward( - layers=[], - func=Softplus, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables)+1 - ) - pinn_learn = PINN(problem, model_lean, extra_features=[SinSinAB()], optimizer_kwargs={'lr':0.01, 'weight_decay':1e-8}) - trainer_learn = Trainer(pinn_learn, max_epochs=1000, callbacks=[MetricTracker()], accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - - # train - trainer_learn.train() - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 98.81it/s, v_num=6, gamma1_loss=2.55e-16, gamma2_loss=4.76e-17, gamma3_loss=2.55e-16, gamma4_loss=4.76e-17, D_loss=1.74e-13, mean_loss=3.5e-14] - - -In such a way, the model is able to reach a very high accuracy! Of -course, this is a toy problem for understanding the usage of extra -features: similar precision could be obtained if the extra features are -very similar to the true solution. The analyzed Poisson problem shows a -forcing term very close to the solution, resulting in a perfect problem -to address with such an approach. - -We conclude here by showing the graphical comparison of the unknown -field and the loss trend for all the test cases presented here: the -standard PINN, PINN with extra features, and PINN with learnable extra -features. - -.. code:: ipython3 - - plotter.plot(solver=pinn_learn) - - - -.. image:: tutorial_files/tutorial_21_0.png - - -Let us compare the training losses for the various types of training - -.. code:: ipython3 - - plotter.plot_loss(trainer, logy=True, label='Standard') - plotter.plot_loss(trainer_feat, logy=True,label='Static Features') - plotter.plot_loss(trainer_learn, logy=True, label='Learnable Features') - - - - -.. image:: tutorial_files/tutorial_23_0.png - - -What’s next? ------------- - -Nice you have completed the two dimensional Poisson tutorial of -**PINA**! There are multiple directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. Propose new types of extrafeatures and see how they affect the - learning - -3. Exploit extrafeature training in more complex problems - -4. Many more… diff --git a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_14_0.png b/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_14_0.png deleted file mode 100644 index 4974131c8..000000000 Binary files a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_14_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_21_0.png b/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_21_0.png deleted file mode 100644 index acaece688..000000000 Binary files a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_21_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_23_0.png b/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_23_0.png deleted file mode 100644 index 5960e46b7..000000000 Binary files a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_23_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_9_0.png b/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_9_0.png deleted file mode 100644 index 4dd8b3be5..000000000 Binary files a/docs/source/_rst/tutorials/tutorial2/tutorial_files/tutorial_9_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial.rst b/docs/source/_rst/tutorials/tutorial3/tutorial.rst deleted file mode 100644 index 54172f423..000000000 --- a/docs/source/_rst/tutorials/tutorial3/tutorial.rst +++ /dev/null @@ -1,335 +0,0 @@ -Tutorial: Two dimensional Wave problem with hard constraint -=========================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial3/tutorial.ipynb - - -In this tutorial we present how to solve the wave equation using hard -constraint PINNs. For doing so we will build a costum ``torch`` model -and pass it to the ``PINN`` solver. - -First of all, some useful imports. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - - from pina.problem import SpatialProblem, TimeDependentProblem - from pina.operators import laplacian, grad - from pina.geometry import CartesianDomain - from pina.solvers import PINN - from pina.trainer import Trainer - from pina.equation import Equation - from pina.equation.equation_factory import FixedValue - from pina import Condition, Plotter - -The problem definition ----------------------- - -The problem is written in the following form: - -.. math:: - \begin{equation} - \begin{cases} - \Delta u(x,y,t) = \frac{\partial^2}{\partial t^2} u(x,y,t) \quad \text{in } D, \\\\ - u(x, y, t=0) = \sin(\pi x)\sin(\pi y), \\\\ - u(x, y, t) = 0 \quad \text{on } \Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4, - \end{cases} - \end{equation} - -where :math:`D` is a square domain :math:`[0,1]^2`, and -:math:`\Gamma_i`, with :math:`i=1,...,4`, are the boundaries of the -square, and the velocity in the standard wave equation is fixed to one. - -Now, the wave problem is written in PINA code as a class, inheriting -from ``SpatialProblem`` and ``TimeDependentProblem`` since we deal with -spatial, and time dependent variables. The equations are written as -``conditions`` that should be satisfied in the corresponding domains. -``truth_solution`` is the exact solution which will be compared with the -predicted one. - -.. code:: ipython3 - - class Wave(TimeDependentProblem, SpatialProblem): - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) - temporal_domain = CartesianDomain({'t': [0, 1]}) - - def wave_equation(input_, output_): - u_t = grad(output_, input_, components=['u'], d=['t']) - u_tt = grad(u_t, input_, components=['dudt'], d=['t']) - nabla_u = laplacian(output_, input_, components=['u'], d=['x', 'y']) - return nabla_u - u_tt - - def initial_condition(input_, output_): - u_expected = (torch.sin(torch.pi*input_.extract(['x'])) * - torch.sin(torch.pi*input_.extract(['y']))) - return output_.extract(['u']) - u_expected - - conditions = { - 'gamma1': Condition(location=CartesianDomain({'x': [0, 1], 'y': 1, 't': [0, 1]}), equation=FixedValue(0.)), - 'gamma2': Condition(location=CartesianDomain({'x': [0, 1], 'y': 0, 't': [0, 1]}), equation=FixedValue(0.)), - 'gamma3': Condition(location=CartesianDomain({'x': 1, 'y': [0, 1], 't': [0, 1]}), equation=FixedValue(0.)), - 'gamma4': Condition(location=CartesianDomain({'x': 0, 'y': [0, 1], 't': [0, 1]}), equation=FixedValue(0.)), - 't0': Condition(location=CartesianDomain({'x': [0, 1], 'y': [0, 1], 't': 0}), equation=Equation(initial_condition)), - 'D': Condition(location=CartesianDomain({'x': [0, 1], 'y': [0, 1], 't': [0, 1]}), equation=Equation(wave_equation)), - } - - def wave_sol(self, pts): - return (torch.sin(torch.pi*pts.extract(['x'])) * - torch.sin(torch.pi*pts.extract(['y'])) * - torch.cos(torch.sqrt(torch.tensor(2.))*torch.pi*pts.extract(['t']))) - - truth_solution = wave_sol - - problem = Wave() - -Hard Constraint Model ---------------------- - -After the problem, a **torch** model is needed to solve the PINN. -Usually, many models are already implemented in **PINA**, but the user -has the possibility to build his/her own model in ``torch``. The hard -constraint we impose is on the boundary of the spatial domain. -Specifically, our solution is written as: - -.. math:: u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t), - -where :math:`NN` is the neural net output. This neural network takes as -input the coordinates (in this case :math:`x`, :math:`y` and :math:`t`) -and provides the unknown field :math:`u`. By construction, it is zero on -the boundaries. The residuals of the equations are evaluated at several -sampling points (which the user can manipulate using the method -``discretise_domain``) and the loss minimized by the neural network is -the sum of the residuals. - -.. code:: ipython3 - - class HardMLP(torch.nn.Module): - - def __init__(self, input_dim, output_dim): - super().__init__() - - self.layers = torch.nn.Sequential(torch.nn.Linear(input_dim, 40), - torch.nn.ReLU(), - torch.nn.Linear(40, 40), - torch.nn.ReLU(), - torch.nn.Linear(40, output_dim)) - - # here in the foward we implement the hard constraints - def forward(self, x): - hard = x.extract(['x'])*(1-x.extract(['x']))*x.extract(['y'])*(1-x.extract(['y'])) - return hard*self.layers(x) - -Train and Inference -------------------- - -In this tutorial, the neural network is trained for 1000 epochs with a -learning rate of 0.001 (default in ``PINN``). Training takes -approximately 3 minutes. - -.. code:: ipython3 - - # generate the data - problem.discretise_domain(1000, 'random', locations=['D', 't0', 'gamma1', 'gamma2', 'gamma3', 'gamma4']) - - # crete the solver - pinn = PINN(problem, HardMLP(len(problem.input_variables), len(problem.output_variables))) - - # create trainer and train - trainer = Trainer(pinn, max_epochs=1000, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 68.69it/s, v_num=0, gamma1_loss=0.000, gamma2_loss=0.000, gamma3_loss=0.000, gamma4_loss=0.000, t0_loss=0.0419, D_loss=0.0307, mean_loss=0.0121] - - -Notice that the loss on the boundaries of the spatial domain is exactly -zero, as expected! After the training is completed one can now plot some -results using the ``Plotter`` class of **PINA**. - -.. code:: ipython3 - - plotter = Plotter() - - # plotting at fixed time t = 0.0 - print('Plotting at t=0') - plotter.plot(pinn, fixed_variables={'t': 0.0}) - - # plotting at fixed time t = 0.5 - print('Plotting at t=0.5') - plotter.plot(pinn, fixed_variables={'t': 0.5}) - - # plotting at fixed time t = 1. - print('Plotting at t=1') - plotter.plot(pinn, fixed_variables={'t': 1.0}) - - -.. parsed-literal:: - - Plotting at t=0 - - - -.. image:: tutorial_files/tutorial_13_1.png - - -.. parsed-literal:: - - Plotting at t=0.5 - - - -.. image:: tutorial_files/tutorial_13_3.png - - -.. parsed-literal:: - - Plotting at t=1 - - - -.. image:: tutorial_files/tutorial_13_5.png - - -The results are not so great, and we can clearly see that as time -progress the solution get worse…. Can we do better? - -A valid option is to impose the initial condition as hard constraint as -well. Specifically, our solution is written as: - -.. math:: u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t)\cdot t + \cos(\sqrt{2}\pi t)\sin(\pi x)\sin(\pi y), - -Let us build the network first - -.. code:: ipython3 - - class HardMLPtime(torch.nn.Module): - - def __init__(self, input_dim, output_dim): - super().__init__() - - self.layers = torch.nn.Sequential(torch.nn.Linear(input_dim, 40), - torch.nn.ReLU(), - torch.nn.Linear(40, 40), - torch.nn.ReLU(), - torch.nn.Linear(40, output_dim)) - - # here in the foward we implement the hard constraints - def forward(self, x): - hard_space = x.extract(['x'])*(1-x.extract(['x']))*x.extract(['y'])*(1-x.extract(['y'])) - hard_t = torch.sin(torch.pi*x.extract(['x'])) * torch.sin(torch.pi*x.extract(['y'])) * torch.cos(torch.sqrt(torch.tensor(2.))*torch.pi*x.extract(['t'])) - return hard_space * self.layers(x) * x.extract(['t']) + hard_t - -Now let’s train with the same configuration as thre previous test - -.. code:: ipython3 - - # generate the data - problem.discretise_domain(1000, 'random', locations=['D', 't0', 'gamma1', 'gamma2', 'gamma3', 'gamma4']) - - # crete the solver - pinn = PINN(problem, HardMLPtime(len(problem.input_variables), len(problem.output_variables))) - - # create trainer and train - trainer = Trainer(pinn, max_epochs=1000, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=1000` reached. - - -.. parsed-literal:: - - Epoch 999: : 1it [00:00, 45.78it/s, v_num=1, gamma1_loss=1.97e-15, gamma2_loss=0.000, gamma3_loss=2.14e-15, gamma4_loss=0.000, t0_loss=0.000, D_loss=1.25e-7, mean_loss=2.09e-8] - - -We can clearly see that the loss is way lower now. Let’s plot the -results - -.. code:: ipython3 - - plotter = Plotter() - - # plotting at fixed time t = 0.0 - print('Plotting at t=0') - plotter.plot(pinn, fixed_variables={'t': 0.0}) - - # plotting at fixed time t = 0.5 - print('Plotting at t=0.5') - plotter.plot(pinn, fixed_variables={'t': 0.5}) - - # plotting at fixed time t = 1. - print('Plotting at t=1') - plotter.plot(pinn, fixed_variables={'t': 1.0}) - - -.. parsed-literal:: - - Plotting at t=0 - - - -.. image:: tutorial_files/tutorial_19_1.png - - -.. parsed-literal:: - - Plotting at t=0.5 - - - -.. image:: tutorial_files/tutorial_19_3.png - - -.. parsed-literal:: - - Plotting at t=1 - - - -.. image:: tutorial_files/tutorial_19_5.png - - -We can see now that the results are way better! This is due to the fact -that previously the network was not learning correctly the initial -conditon, leading to a poor solution when the time evolved. By imposing -the initial condition the network is able to correctly solve the -problem. - -What’s next? ------------- - -Nice you have completed the two dimensional Wave tutorial of **PINA**! -There are multiple directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. Propose new types of hard constraints in time, e.g.  - - .. math:: u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t)(1-\exp(-t)) + \cos(\sqrt{2}\pi t)sin(\pi x)\sin(\pi y), - -3. Exploit extrafeature training for model 1 and 2 - -4. Many more… diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_1.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_1.png deleted file mode 100644 index 795610ffb..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_1.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_3.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_3.png deleted file mode 100644 index c260215b0..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_3.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_5.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_5.png deleted file mode 100644 index ebd27a0d2..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_13_5.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_1.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_1.png deleted file mode 100644 index c9ed12fd8..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_1.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_3.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_3.png deleted file mode 100644 index 2523fcf29..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_3.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_5.png b/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_5.png deleted file mode 100644 index c6448a698..000000000 Binary files a/docs/source/_rst/tutorials/tutorial3/tutorial_files/tutorial_19_5.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial4/tutorial.rst b/docs/source/_rst/tutorials/tutorial4/tutorial.rst deleted file mode 100644 index 2900c3e88..000000000 --- a/docs/source/_rst/tutorials/tutorial4/tutorial.rst +++ /dev/null @@ -1,820 +0,0 @@ -Tutorial: Unstructured convolutional autoencoder via continuous convolution -=========================================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial4/tutorial.ipynb - -In this tutorial, we will show how to use the Continuous Convolutional -Filter, and how to build common Deep Learning architectures with it. The -implementation of the filter follows the original work `A Continuous -Convolutional Trainable Filter for Modelling Unstructured -Data `__. - -First of all we import the modules needed for the tutorial: - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - from pina.problem import AbstractProblem - from pina.solvers import SupervisedSolver - from pina.trainer import Trainer - from pina import Condition, LabelTensor - from pina.model.layers import ContinuousConvBlock - import torchvision # for MNIST dataset - from pina.model import FeedForward # for building AE and MNIST classification - -The tutorial is structured as follow: - -* `Continuous filter background <#continuous-filter-background>`__: understand how the convolutional filter works and how to use it. -* `Building a MNIST Classifier <#building-a-mnist-classifier>`__: show how to build a simple - classifier using the MNIST dataset and how to combine a continuous - convolutional layer with a feedforward neural network. -* `Building a Continuous Convolutional Autoencoder <#building-a-continuous-convolutional-autoencoder>`__: show - show to use the continuous filter to work with unstructured data for - autoencoding and up-sampling. - -Continuous filter background ----------------------------- - -As reported by the authors in the original paper: in contrast to -discrete convolution, continuous convolution is mathematically defined -as: - -.. math:: - - - \mathcal{I}_{\rm{out}}(\mathbf{x}) = \int_{\mathcal{X}} \mathcal{I}(\mathbf{x} + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{\tau}) d\mathbf{\tau}, - -where :math:`\mathcal{K} : \mathcal{X} \rightarrow \mathbb{R}` is the -*continuous filter* function, and -:math:`\mathcal{I} : \Omega \subset \mathbb{R}^N \rightarrow \mathbb{R}` -is the input function. The continuous filter function is approximated -using a FeedForward Neural Network, thus trainable during the training -phase. The way in which the integral is approximated can be different, -currently on **PINA** we approximate it using a simple sum, as suggested -by the authors. Thus, given :math:`\{\mathbf{x}_i\}_{i=1}^{n}` points in -:math:`\mathbb{R}^N` of the input function mapped on the -:math:`\mathcal{X}` filter domain, we approximate the above equation as: - -.. math:: - - - \mathcal{I}_{\rm{out}}(\mathbf{\tilde{x}}_i) = \sum_{{\mathbf{x}_i}\in\mathcal{X}} \mathcal{I}(\mathbf{x}_i + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{x}_i), - -where :math:`\mathbf{\tau} \in \mathcal{S}`, with :math:`\mathcal{S}` -the set of available strides, corresponds to the current stride position -of the filter, and :math:`\mathbf{\tilde{x}}_i` points are obtained by -taking the centroid of the filter position mapped on the :math:`\Omega` -domain. - -We will now try to pratically see how to work with the filter. From the -above definition we see that what is needed is: 1. A domain and a -function defined on that domain (the input) 2. A stride, corresponding -to the positions where the filter needs to be :math:`\rightarrow` -``stride`` variable in ``ContinuousConv`` 3. The filter rectangular -domain :math:`\rightarrow` ``filter_dim`` variable in ``ContinuousConv`` - -Input function -~~~~~~~~~~~~~~ - -The input function for the continuous filter is defined as a tensor of -shape: - -.. math:: [B \times N_{in} \times N \times D] - -\ where :math:`B` is the batch_size, :math:`N_{in}` is the number of -input fields, :math:`N` the number of points in the mesh, :math:`D` the -dimension of the problem. In particular: \* :math:`D` is the number of -spatial variables + 1. The last column must contain the field value. For -example for 2D problems :math:`D=3` and the tensor will be something -like ``[first coordinate, second coordinate, field value]`` \* -:math:`N_{in}` represents the number of vectorial function presented. -For example a vectorial function :math:`f = [f_1, f_2]` will have -:math:`N_{in}=2` - -Let’s see an example to clear the ideas. We will be verbose to explain -in details the input form. We wish to create the function: - -.. math:: - - - f(x, y) = [\sin(\pi x) \sin(\pi y), -\sin(\pi x) \sin(\pi y)] \quad (x,y)\in[0,1]\times[0,1] - -using a batch size of one. - -.. code:: ipython3 - - # batch size fixed to 1 - batch_size = 1 - - # points in the mesh fixed to 200 - N = 200 - - # vectorial 2 dimensional function, number_input_fileds=2 - number_input_fileds = 2 - - # 2 dimensional spatial variables, D = 2 + 1 = 3 - D = 3 - - # create the function f domain as random 2d points in [0, 1] - domain = torch.rand(size=(batch_size, number_input_fileds, N, D-1)) - print(f"Domain has shape: {domain.shape}") - - # create the functions - pi = torch.acos(torch.tensor([-1.])) # pi value - f1 = torch.sin(pi * domain[:, 0, :, 0]) * torch.sin(pi * domain[:, 0, :, 1]) - f2 = - torch.sin(pi * domain[:, 1, :, 0]) * torch.sin(pi * domain[:, 1, :, 1]) - - # stacking the input domain and field values - data = torch.empty(size=(batch_size, number_input_fileds, N, D)) - data[..., :-1] = domain # copy the domain - data[:, 0, :, -1] = f1 # copy first field value - data[:, 1, :, -1] = f1 # copy second field value - print(f"Filter input data has shape: {data.shape}") - - -.. parsed-literal:: - - Domain has shape: torch.Size([1, 2, 200, 2]) - Filter input data has shape: torch.Size([1, 2, 200, 3]) - - -Stride -~~~~~~ - -The stride is passed as a dictionary ``stride`` which tells the filter -where to go. Here is an example for the :math:`[0,1]\times[0,5]` domain: - -.. code:: python - - # stride definition - stride = {"domain": [1, 5], - "start": [0, 0], - "jump": [0.1, 0.3], - "direction": [1, 1], - } - -This tells the filter: - -1. ``domain``: square domain (the only implemented) :math:`[0,1]\times[0,5]`. The minimum value is always zero, - while the maximum is specified by the user -2. ``start``: start position - of the filter, coordinate :math:`(0, 0)` -3. ``jump``: the jumps of the - centroid of the filter to the next position :math:`(0.1, 0.3)` -4. ``direction``: the directions of the jump, with ``1 = right``, - ``0 = no jump``,\ ``-1 = left`` with respect to the current position - -**Note** - -We are planning to release the possibility to directly pass a list of -possible strides! - -Filter definition -~~~~~~~~~~~~~~~~~ - -Having defined all the previous blocks we are able to construct the -continuous filter. Suppose we would like to get an ouput with only one field, and let us -fix the filter dimension to be :math:`[0.1, 0.1]`. - -.. code:: ipython3 - - # filter dim - filter_dim = [0.1, 0.1] - - # stride - stride = {"domain": [1, 1], - "start": [0, 0], - "jump": [0.08, 0.08], - "direction": [1, 1], - } - - # creating the filter - cConv = ContinuousConvBlock(input_numb_field=number_input_fileds, - output_numb_field=1, - filter_dim=filter_dim, - stride=stride) - - -That’s it! In just one line of code we have created the continuous -convolutional filter. By default the ``pina.model.FeedForward`` neural -network is intitialised, more on the -`documentation `__. In -case the mesh doesn’t change during training we can set the ``optimize`` -flag equals to ``True``, to exploit optimizations for finding the points -to convolve. - -.. code:: ipython3 - - # creating the filter + optimization - cConv = ContinuousConvBlock(input_numb_field=number_input_fileds, - output_numb_field=1, - filter_dim=filter_dim, - stride=stride, - optimize=True) - - -Let’s try to do a forward pass - -.. code:: ipython3 - - print(f"Filter input data has shape: {data.shape}") - - #input to the filter - output = cConv(data) - - print(f"Filter output data has shape: {output.shape}") - - -.. parsed-literal:: - - Filter input data has shape: torch.Size([1, 2, 200, 3]) - Filter output data has shape: torch.Size([1, 1, 169, 3]) - - -If we don’t want to use the default ``FeedForward`` neural network, we -can pass a specified torch model in the ``model`` keyword as follow: - -.. code:: ipython3 - - class SimpleKernel(torch.nn.Module): - def __init__(self) -> None: - super().__init__() - self. model = torch.nn.Sequential( - torch.nn.Linear(2, 20), - torch.nn.ReLU(), - torch.nn.Linear(20, 20), - torch.nn.ReLU(), - torch.nn.Linear(20, 1)) - - def forward(self, x): - return self.model(x) - - - cConv = ContinuousConvBlock(input_numb_field=number_input_fileds, - output_numb_field=1, - filter_dim=filter_dim, - stride=stride, - optimize=True, - model=SimpleKernel) - - -Notice that we pass the class and not an already built object! - -Building a MNIST Classifier ---------------------------- - -Let’s see how we can build a MNIST classifier using a continuous -convolutional filter. We will use the MNIST dataset from PyTorch. In -order to keep small training times we use only 6000 samples for training -and 1000 samples for testing. - -.. code:: ipython3 - - from torch.utils.data import DataLoader, SubsetRandomSampler - - numb_training = 6000 # get just 6000 images for training - numb_testing= 1000 # get just 1000 images for training - seed = 111 # for reproducibility - batch_size = 8 # setting batch size - - # setting the seed - torch.manual_seed(seed) - - # downloading the dataset - train_data = torchvision.datasets.MNIST('./data/', train=True, download=True, - transform=torchvision.transforms.Compose([ - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize( - (0.1307,), (0.3081,)) - ])) - subsample_train_indices = torch.randperm(len(train_data))[:numb_training] - train_loader = DataLoader(train_data, batch_size=batch_size, - sampler=SubsetRandomSampler(subsample_train_indices)) - - test_data = torchvision.datasets.MNIST('./data/', train=False, download=True, - transform=torchvision.transforms.Compose([ - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize( - (0.1307,), (0.3081,)) - ])) - subsample_test_indices = torch.randperm(len(train_data))[:numb_testing] - test_loader = DataLoader(train_data, batch_size=batch_size, - sampler=SubsetRandomSampler(subsample_train_indices)) - -Let’s now build a simple classifier. The MNIST dataset is composed by -vectors of shape ``[batch, 1, 28, 28]``, but we can image them as one -field functions where the pixels :math:`ij` are the coordinate -:math:`x=i, y=j` in a :math:`[0, 27]\times[0,27]` domain, and the pixels -value are the field values. We just need a function to transform the -regular tensor in a tensor compatible for the continuous filter: - -.. code:: ipython3 - - def transform_input(x): - batch_size = x.shape[0] - dim_grid = tuple(x.shape[:-3:-1]) - - # creating the n dimensional mesh grid for a single channel image - values_mesh = [torch.arange(0, dim).float() for dim in dim_grid] - mesh = torch.meshgrid(values_mesh) - coordinates_mesh = [x.reshape(-1, 1) for x in mesh] - coordinates = torch.cat(coordinates_mesh, dim=1).unsqueeze( - 0).repeat((batch_size, 1, 1)).unsqueeze(1) - - return torch.cat((coordinates, x.flatten(2).unsqueeze(-1)), dim=-1) - - - # let's try it out - image, s = next(iter(train_loader)) - print(f"Original MNIST image shape: {image.shape}") - - image_transformed = transform_input(image) - print(f"Transformed MNIST image shape: {image_transformed.shape}") - - - -.. parsed-literal:: - - Original MNIST image shape: torch.Size([8, 1, 28, 28]) - Transformed MNIST image shape: torch.Size([8, 1, 784, 3]) - - -We can now build a simple classifier! We will use just one convolutional -filter followed by a feedforward neural network - -.. code:: ipython3 - - # setting the seed - torch.manual_seed(seed) - - class ContinuousClassifier(torch.nn.Module): - def __init__(self): - super().__init__() - - # number of classes for classification - numb_class = 10 - - # convolutional block - self.convolution = ContinuousConvBlock(input_numb_field=1, - output_numb_field=4, - stride={"domain": [27, 27], - "start": [0, 0], - "jumps": [4, 4], - "direction": [1, 1.], - }, - filter_dim=[4, 4], - optimize=True) - # feedforward net - self.nn = FeedForward(input_dimensions=196, - output_dimensions=numb_class, - layers=[120, 64], - func=torch.nn.ReLU) - - def forward(self, x): - # transform input + convolution - x = transform_input(x) - x = self.convolution(x) - # feed forward classification - return self.nn(x[..., -1].flatten(1)) - - - net = ContinuousClassifier() - -Let’s try to train it using a simple pytorch training loop. We train for -juts 1 epoch using Adam optimizer with a :math:`0.001` learning rate. - -.. code:: ipython3 - - # setting the seed - torch.manual_seed(seed) - - # optimizer and loss function - optimizer = torch.optim.Adam(net.parameters(), lr=0.001) - criterion = torch.nn.CrossEntropyLoss() - - for epoch in range(1): # loop over the dataset multiple times - - running_loss = 0.0 - for i, data in enumerate(train_loader, 0): - # get the inputs; data is a list of [inputs, labels] - inputs, labels = data - - # zero the parameter gradients - optimizer.zero_grad() - - # forward + backward + optimize - outputs = net(inputs) - loss = criterion(outputs, labels) - loss.backward() - optimizer.step() - - # print statistics - running_loss += loss.item() - if i % 50 == 49: - print( - f'batch [{i + 1}/{numb_training//batch_size}] loss[{running_loss / 500:.3f}]') - running_loss = 0.0 - - -.. parsed-literal:: - - batch [50/750] loss[0.161] - batch [100/750] loss[0.073] - batch [150/750] loss[0.063] - batch [200/750] loss[0.051] - batch [250/750] loss[0.044] - batch [300/750] loss[0.050] - batch [350/750] loss[0.053] - batch [400/750] loss[0.049] - batch [450/750] loss[0.046] - batch [500/750] loss[0.034] - batch [550/750] loss[0.036] - batch [600/750] loss[0.040] - batch [650/750] loss[0.028] - batch [700/750] loss[0.040] - batch [750/750] loss[0.040] - - -Let’s see the performance on the train set! - -.. code:: ipython3 - - correct = 0 - total = 0 - with torch.no_grad(): - for data in test_loader: - images, labels = data - # calculate outputs by running images through the network - outputs = net(images) - # the class with the highest energy is what we choose as prediction - _, predicted = torch.max(outputs.data, 1) - total += labels.size(0) - correct += (predicted == labels).sum().item() - - print( - f'Accuracy of the network on the 1000 test images: {(correct / total):.3%}') - - - -.. parsed-literal:: - - Accuracy of the network on the 1000 test images: 92.733% - - -As we can see we have very good performance for having traing only for 1 -epoch! Nevertheless, we are still using structured data… Let’s see how -we can build an autoencoder for unstructured data now. - -Building a Continuous Convolutional Autoencoder ------------------------------------------------ - -Just as toy problem, we will now build an autoencoder for the following -function :math:`f(x,y)=\sin(\pi x)\sin(\pi y)` on the unit circle domain -centered in :math:`(0.5, 0.5)`. We will also see the ability to -up-sample (once trained) the results without retraining. Let’s first -create the input and visualize it, we will use firstly a mesh of -:math:`100` points. - -.. code:: ipython3 - - # create inputs - def circle_grid(N=100): - """Generate points withing a unit 2D circle centered in (0.5, 0.5) - - :param N: number of points - :type N: float - :return: [x, y] array of points - :rtype: torch.tensor - """ - - PI = torch.acos(torch.zeros(1)).item() * 2 - R = 0.5 - centerX = 0.5 - centerY = 0.5 - - r = R * torch.sqrt(torch.rand(N)) - theta = torch.rand(N) * 2 * PI - - x = centerX + r * torch.cos(theta) - y = centerY + r * torch.sin(theta) - - return torch.stack([x, y]).T - - # create the grid - grid = circle_grid(500) - - # create input - input_data = torch.empty(size=(1, 1, grid.shape[0], 3)) - input_data[0, 0, :, :-1] = grid - input_data[0, 0, :, -1] = torch.sin(pi * grid[:, 0]) * torch.sin(pi * grid[:, 1]) - - # visualize data - plt.title("Training sample with 500 points") - plt.scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1]) - plt.colorbar() - plt.show() - - - - -.. image:: tutorial_files/tutorial_32_0.png - - -Let’s now build a simple autoencoder using the continuous convolutional -filter. The data is clearly unstructured and a simple convolutional -filter might not work without projecting or interpolating first. Let’s -first build and ``Encoder`` and ``Decoder`` class, and then a -``Autoencoder`` class that contains both. - -.. code:: ipython3 - - class Encoder(torch.nn.Module): - def __init__(self, hidden_dimension): - super().__init__() - - # convolutional block - self.convolution = ContinuousConvBlock(input_numb_field=1, - output_numb_field=2, - stride={"domain": [1, 1], - "start": [0, 0], - "jumps": [0.05, 0.05], - "direction": [1, 1.], - }, - filter_dim=[0.15, 0.15], - optimize=True) - # feedforward net - self.nn = FeedForward(input_dimensions=400, - output_dimensions=hidden_dimension, - layers=[240, 120]) - - def forward(self, x): - # convolution - x = self.convolution(x) - # feed forward pass - return self.nn(x[..., -1]) - - - class Decoder(torch.nn.Module): - def __init__(self, hidden_dimension): - super().__init__() - - # convolutional block - self.convolution = ContinuousConvBlock(input_numb_field=2, - output_numb_field=1, - stride={"domain": [1, 1], - "start": [0, 0], - "jumps": [0.05, 0.05], - "direction": [1, 1.], - }, - filter_dim=[0.15, 0.15], - optimize=True) - # feedforward net - self.nn = FeedForward(input_dimensions=hidden_dimension, - output_dimensions=400, - layers=[120, 240]) - - def forward(self, weights, grid): - # feed forward pass - x = self.nn(weights) - # transpose convolution - return torch.sigmoid(self.convolution.transpose(x, grid)) - - -Very good! Notice that in the ``Decoder`` class in the ``forward`` pass -we have used the ``.transpose()`` method of the -``ContinuousConvolution`` class. This method accepts the ``weights`` for -upsampling and the ``grid`` on where to upsample. Let’s now build the -autoencoder! We set the hidden dimension in the ``hidden_dimension`` -variable. We apply the sigmoid on the output since the field value is -between :math:`[0, 1]`. - -.. code:: ipython3 - - class Autoencoder(torch.nn.Module): - def __init__(self, hidden_dimension=10): - super().__init__() - - self.encoder = Encoder(hidden_dimension) - self.decoder = Decoder(hidden_dimension) - - def forward(self, x): - # saving grid for later upsampling - grid = x.clone().detach() - # encoder - weights = self.encoder(x) - # decoder - out = self.decoder(weights, grid) - return out - - net = Autoencoder() - -Let’s now train the autoencoder, minimizing the mean square error loss -and optimizing using Adam. We use the ``SupervisedSolver`` as solver, -and the problem is a simple problem created by inheriting from -``AbstractProblem``. It takes approximately two minutes to train on CPU. - -.. code:: ipython3 - - # define the problem - class CircleProblem(AbstractProblem): - input_variables = ['x', 'y', 'f'] - output_variables = input_variables - conditions = {'data' : Condition(input_points=LabelTensor(input_data, input_variables), output_points=LabelTensor(input_data, output_variables))} - - # define the solver - solver = SupervisedSolver(problem=CircleProblem(), model=net, loss=torch.nn.MSELoss()) - - # train - trainer = Trainer(solver, max_epochs=150, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=150` reached. - - -Let’s visualize the two solutions side by side! - -.. code:: ipython3 - - net.eval() - - # get output and detach from computational graph for plotting - output = net(input_data).detach() - - # visualize data - fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) - pic1 = axes[0].scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1]) - axes[0].set_title("Real") - fig.colorbar(pic1) - plt.subplot(1, 2, 2) - pic2 = axes[1].scatter(grid[:, 0], grid[:, 1], c=output[0, 0, :, -1]) - axes[1].set_title("Autoencoder") - fig.colorbar(pic2) - plt.tight_layout() - plt.show() - - - - -.. image:: tutorial_files/tutorial_40_0.png - - -As we can see the two are really similar! We can compute the :math:`l_2` -error quite easily as well: - -.. code:: ipython3 - - def l2_error(input_, target): - return torch.linalg.norm(input_-target, ord=2)/torch.linalg.norm(input_, ord=2) - - - print(f'l2 error: {l2_error(input_data[0, 0, :, -1], output[0, 0, :, -1]):.2%}') - - -.. parsed-literal:: - - l2 error: 4.32% - - -More or less :math:`4\%` in :math:`l_2` error, which is really low -considering the fact that we use just **one** convolutional layer and a -simple feedforward to decrease the dimension. Let’s see now some -peculiarity of the filter. - -Filter for upsampling -~~~~~~~~~~~~~~~~~~~~~ - -Suppose we have already the hidden dimension and we want to upsample on -a differen grid with more points. Let’s see how to do it: - -.. code:: ipython3 - - # setting the seed - torch.manual_seed(seed) - - grid2 = circle_grid(1500) # triple number of points - input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3)) - input_data2[0, 0, :, :-1] = grid2 - input_data2[0, 0, :, -1] = torch.sin(pi * - grid2[:, 0]) * torch.sin(pi * grid2[:, 1]) - - # get the hidden dimension representation from original input - latent = net.encoder(input_data) - - # upsample on the second input_data2 - output = net.decoder(latent, input_data2).detach() - - # show the picture - fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) - pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1]) - axes[0].set_title("Real") - fig.colorbar(pic1) - plt.subplot(1, 2, 2) - pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1]) - axes[1].set_title("Up-sampling") - fig.colorbar(pic2) - plt.tight_layout() - plt.show() - - - - -.. image:: tutorial_files/tutorial_45_0.png - - -As we can see we have a very good approximation of the original -function, even thought some noise is present. Let’s calculate the error -now: - -.. code:: ipython3 - - print(f'l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}') - - -.. parsed-literal:: - - l2 error: 8.49% - - -Autoencoding at different resolution -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the previous example we already had the hidden dimension (of original -input) and we used it to upsample. Sometimes however we have a more fine -mesh solution and we simply want to encode it. This can be done without -retraining! This procedure can be useful in case we have many points in -the mesh and just a smaller part of them are needed for training. Let’s -see the results of this: - -.. code:: ipython3 - - # setting the seed - torch.manual_seed(seed) - - grid2 = circle_grid(3500) # very fine mesh - input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3)) - input_data2[0, 0, :, :-1] = grid2 - input_data2[0, 0, :, -1] = torch.sin(pi * - grid2[:, 0]) * torch.sin(pi * grid2[:, 1]) - - # get the hidden dimension representation from more fine mesh input - latent = net.encoder(input_data2) - - # upsample on the second input_data2 - output = net.decoder(latent, input_data2).detach() - - # show the picture - fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) - pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1]) - axes[0].set_title("Real") - fig.colorbar(pic1) - plt.subplot(1, 2, 2) - pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1]) - axes[1].set_title("Autoencoder not re-trained") - fig.colorbar(pic2) - plt.tight_layout() - plt.show() - - # calculate l2 error - print( - f'l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}') - - - - -.. image:: tutorial_files/tutorial_49_0.png - - -.. parsed-literal:: - - l2 error: 8.59% - - -What’s next? ------------- - -We have shown the basic usage of a convolutional filter. There are -additional extensions possible: - -1. Train using Physics Informed strategies - -2. Use the filter to build an unstructured convolutional autoencoder for - reduced order modelling - -3. Many more… diff --git a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_32_0.png b/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_32_0.png deleted file mode 100644 index 229df2733..000000000 Binary files a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_32_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_40_0.png b/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_40_0.png deleted file mode 100644 index 55dea5bdd..000000000 Binary files a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_40_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_45_0.png b/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_45_0.png deleted file mode 100644 index a3246f925..000000000 Binary files a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_45_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_49_0.png b/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_49_0.png deleted file mode 100644 index 9a15d8705..000000000 Binary files a/docs/source/_rst/tutorials/tutorial4/tutorial_files/tutorial_49_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial5/tutorial.rst b/docs/source/_rst/tutorials/tutorial5/tutorial.rst deleted file mode 100644 index 59eb62a8a..000000000 --- a/docs/source/_rst/tutorials/tutorial5/tutorial.rst +++ /dev/null @@ -1,249 +0,0 @@ -Tutorial: Two dimensional Darcy flow using the Fourier Neural Operator -====================================================================== - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial5/tutorial.ipynb - -In this tutorial we are going to solve the Darcy flow problem in two -dimensions, presented in `Fourier Neural Operator for Parametric Partial -Differential Equation `__. -First of all we import the modules needed for the tutorial. Importing -``scipy`` is needed for input output operations. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - !pip install scipy - # get the data - !wget https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial5/Data_Darcy.mat - - - # !pip install scipy # install scipy - from scipy import io - import torch - from pina.model import FNO, FeedForward # let's import some models - from pina import Condition, LabelTensor - from pina.solvers import SupervisedSolver - from pina.trainer import Trainer - from pina.problem import AbstractProblem - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - -Data Generation ---------------- - -We will focus on solving the a specfic PDE, the **Darcy Flow** equation. -The Darcy PDE is a second order, elliptic PDE with the following form: - -.. math:: - - - -\nabla\cdot(k(x, y)\nabla u(x, y)) = f(x) \quad (x, y) \in D. - -Specifically, :math:`u` is the flow pressure, :math:`k` is the -permeability field and :math:`f` is the forcing function. The Darcy flow -can parameterize a variety of systems including flow through porous -media, elastic materials and heat conduction. Here you will define the -domain as a 2D unit square Dirichlet boundary conditions. The dataset is -taken from the authors original reference. - -.. code:: ipython3 - - # download the dataset - data = io.loadmat("Data_Darcy.mat") - - # extract data (we use only 100 data for train) - k_train = LabelTensor(torch.tensor(data['k_train'], dtype=torch.float).unsqueeze(-1), ['u0']) - u_train = LabelTensor(torch.tensor(data['u_train'], dtype=torch.float).unsqueeze(-1), ['u']) - k_test = LabelTensor(torch.tensor(data['k_test'], dtype=torch.float).unsqueeze(-1), ['u0']) - u_test= LabelTensor(torch.tensor(data['u_test'], dtype=torch.float).unsqueeze(-1), ['u']) - x = torch.tensor(data['x'], dtype=torch.float)[0] - y = torch.tensor(data['y'], dtype=torch.float)[0] - -Let’s visualize some data - -.. code:: ipython3 - - plt.subplot(1, 2, 1) - plt.title('permeability') - plt.imshow(k_train.squeeze(-1)[0]) - plt.subplot(1, 2, 2) - plt.title('field solution') - plt.imshow(u_train.squeeze(-1)[0]) - plt.show() - - - -.. image:: tutorial_files/tutorial_6_0.png - - -We now create the neural operator class. It is a very simple class, -inheriting from ``AbstractProblem``. - -.. code:: ipython3 - - class NeuralOperatorSolver(AbstractProblem): - input_variables = k_train.labels - output_variables = u_train.labels - conditions = {'data' : Condition(input_points=k_train, - output_points=u_train)} - - # make problem - problem = NeuralOperatorSolver() - -Solving the problem with a FeedForward Neural Network ------------------------------------------------------ - -We will first solve the problem using a Feedforward neural network. We -will use the ``SupervisedSolver`` for solving the problem, since we are -training using supervised learning. - -.. code:: ipython3 - - # make model - model = FeedForward(input_dimensions=1, output_dimensions=1) - - - # make solver - solver = SupervisedSolver(problem=problem, model=model) - - # make the trainer and train - trainer = Trainer(solver=solver, max_epochs=10, accelerator='cpu', enable_model_summary=False, batch_size=10) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - - -.. parsed-literal:: - - GPU available: False, used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - -.. parsed-literal:: - - Epoch 9: : 100it [00:00, 357.28it/s, v_num=1, mean_loss=0.108] - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=10` reached. - - -.. parsed-literal:: - - Epoch 9: : 100it [00:00, 354.81it/s, v_num=1, mean_loss=0.108] - - -The final loss is pretty high… We can calculate the error by importing -``LpLoss``. - -.. code:: ipython3 - - from pina.loss import LpLoss - - # make the metric - metric_err = LpLoss(relative=True) - - - err = float(metric_err(u_train.squeeze(-1), solver.neural_net(k_train).squeeze(-1)).mean())*100 - print(f'Final error training {err:.2f}%') - - err = float(metric_err(u_test.squeeze(-1), solver.neural_net(k_test).squeeze(-1)).mean())*100 - print(f'Final error testing {err:.2f}%') - - -.. parsed-literal:: - - Final error training 56.04% - Final error testing 56.01% - - -Solving the problem with a Fuorier Neural Operator (FNO) --------------------------------------------------------- - -We will now move to solve the problem using a FNO. Since we are learning -operator this approach is better suited, as we shall see. - -.. code:: ipython3 - - # make model - lifting_net = torch.nn.Linear(1, 24) - projecting_net = torch.nn.Linear(24, 1) - model = FNO(lifting_net=lifting_net, - projecting_net=projecting_net, - n_modes=8, - dimensions=2, - inner_size=24, - padding=8) - - - # make solver - solver = SupervisedSolver(problem=problem, model=model) - - # make the trainer and train - trainer = Trainer(solver=solver, max_epochs=10, accelerator='cpu', enable_model_summary=False, batch_size=10) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - - -.. parsed-literal:: - - GPU available: False, used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - - -.. parsed-literal:: - - Epoch 0: : 0it [00:00, ?it/s]Epoch 9: : 100it [00:02, 47.76it/s, v_num=4, mean_loss=0.00106] - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=10` reached. - - -.. parsed-literal:: - - Epoch 9: : 100it [00:02, 47.65it/s, v_num=4, mean_loss=0.00106] - - -We can clearly see that the final loss is lower. Let’s see in testing.. -Notice that the number of parameters is way higher than a -``FeedForward`` network. We suggest to use GPU or TPU for a speed up in -training, when many data samples are used. - -.. code:: ipython3 - - err = float(metric_err(u_train.squeeze(-1), solver.neural_net(k_train).squeeze(-1)).mean())*100 - print(f'Final error training {err:.2f}%') - - err = float(metric_err(u_test.squeeze(-1), solver.neural_net(k_test).squeeze(-1)).mean())*100 - print(f'Final error testing {err:.2f}%') - - -.. parsed-literal:: - - Final error training 4.83% - Final error testing 5.16% - - -As we can see the loss is way lower! - -What’s next? ------------- - -We have made a very simple example on how to use the ``FNO`` for -learning neural operator. Currently in **PINA** we implement 1D/2D/3D -cases. We suggest to extend the tutorial using more complex problems and -train for longer, to see the full potential of neural operators. diff --git a/docs/source/_rst/tutorials/tutorial5/tutorial_files/tutorial_6_0.png b/docs/source/_rst/tutorials/tutorial5/tutorial_files/tutorial_6_0.png deleted file mode 100644 index fec83e2c2..000000000 Binary files a/docs/source/_rst/tutorials/tutorial5/tutorial_files/tutorial_6_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial.rst b/docs/source/_rst/tutorials/tutorial6/tutorial.rst deleted file mode 100644 index d021adf8b..000000000 --- a/docs/source/_rst/tutorials/tutorial6/tutorial.rst +++ /dev/null @@ -1,330 +0,0 @@ -Tutorial: Building custom geometries with PINA ``Location`` class -================================================================= - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial6/tutorial.ipynb - -In this tutorial we will show how to use geometries in PINA. -Specifically, the tutorial will include how to create geometries and how -to visualize them. The topics covered are: - -- Creating CartesianDomains and EllipsoidDomains -- Getting the Union and Difference of Geometries -- Sampling points in the domain (and visualize them) - -We import the relevant modules first. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - from pina.geometry import EllipsoidDomain, Difference, CartesianDomain, Union, SimplexDomain - from pina.label_tensor import LabelTensor - - def plot_scatter(ax, pts, title): - ax.title.set_text(title) - ax.scatter(pts.extract('x'), pts.extract('y'), color='blue', alpha=0.5) - -Built-in Geometries -------------------- - -We will create one cartesian and two ellipsoids. For the sake of -simplicity, we show here the 2-dimensional, but it’s trivial the -extension to 3D (and higher) cases. The geometries allows also the -generation of samples belonging to the boundary. So, we will create one -ellipsoid with the border and one without. - -.. code:: ipython3 - - cartesian = CartesianDomain({'x': [0, 2], 'y': [0, 2]}) - ellipsoid_no_border = EllipsoidDomain({'x': [1, 3], 'y': [1, 3]}) - ellipsoid_border = EllipsoidDomain({'x': [2, 4], 'y': [2, 4]}, sample_surface=True) - -The ``{'x': [0, 2], 'y': [0, 2]}`` are the bounds of the -``CartesianDomain`` being created. - -To visualize these shapes, we need to sample points on them. We will use -the ``sample`` method of the ``CartesianDomain`` and ``EllipsoidDomain`` -classes. This method takes a ``n`` argument which is the number of -points to sample. It also takes different modes to sample such as -random. - -.. code:: ipython3 - - cartesian_samples = cartesian.sample(n=1000, mode='random') - ellipsoid_no_border_samples = ellipsoid_no_border.sample(n=1000, mode='random') - ellipsoid_border_samples = ellipsoid_border.sample(n=1000, mode='random') - -We can see the samples of each of the geometries to see what we are -working with. - -.. code:: ipython3 - - print(f"Cartesian Samples: {cartesian_samples}") - print(f"Ellipsoid No Border Samples: {ellipsoid_no_border_samples}") - print(f"Ellipsoid Border Samples: {ellipsoid_border_samples}") - - -.. parsed-literal:: - - Cartesian Samples: labels(['x', 'y']) - LabelTensor([[[0.2300, 1.6698]], - [[1.7785, 0.4063]], - [[1.5143, 1.8979]], - ..., - [[0.0905, 1.4660]], - [[0.8176, 1.7357]], - [[0.0475, 0.0170]]]) - Ellipsoid No Border Samples: labels(['x', 'y']) - LabelTensor([[[1.9341, 2.0182]], - [[1.5503, 1.8426]], - [[2.0392, 1.7597]], - ..., - [[1.8976, 2.2859]], - [[1.8015, 2.0012]], - [[2.2713, 2.2355]]]) - Ellipsoid Border Samples: labels(['x', 'y']) - LabelTensor([[[3.3413, 3.9400]], - [[3.9573, 2.7108]], - [[3.8341, 2.4484]], - ..., - [[2.7251, 2.0385]], - [[3.8654, 2.4990]], - [[3.2292, 3.9734]]]) - - -Notice how these are all ``LabelTensor`` objects. You can read more -about these in the -`documentation `__. -At a very high level, they are tensors where each element in a tensor -has a label that we can access by doing ``.labels``. We can -also access the values of the tensor by doing -``.extract(['x'])``. - -We are now ready to visualize the samples using matplotlib. - -.. code:: ipython3 - - fig, axs = plt.subplots(1, 3, figsize=(16, 4)) - pts_list = [cartesian_samples, ellipsoid_no_border_samples, ellipsoid_border_samples] - title_list = ['Cartesian Domain', 'Ellipsoid Domain', 'Ellipsoid Border Domain'] - for ax, pts, title in zip(axs, pts_list, title_list): - plot_scatter(ax, pts, title) - - - -.. image:: tutorial_files/tutorial_10_0.png - - -We have now created, sampled, and visualized our first geometries! We -can see that the ``EllipsoidDomain`` with the border has a border around -it. We can also see that the ``EllipsoidDomain`` without the border is -just the ellipse. We can also see that the ``CartesianDomain`` is just a -square. - -Simplex Domain -~~~~~~~~~~~~~~ - -Among the built-in shapes, we quickly show here the usage of -``SimplexDomain``, which can be used for polygonal domains! - -.. code:: ipython3 - - import torch - spatial_domain = SimplexDomain( - [ - LabelTensor(torch.tensor([[0, 0]]), labels=["x", "y"]), - LabelTensor(torch.tensor([[1, 1]]), labels=["x", "y"]), - LabelTensor(torch.tensor([[0, 2]]), labels=["x", "y"]), - ] - ) - - spatial_domain2 = SimplexDomain( - [ - LabelTensor(torch.tensor([[ 0., -2.]]), labels=["x", "y"]), - LabelTensor(torch.tensor([[-.5, -.5]]), labels=["x", "y"]), - LabelTensor(torch.tensor([[-2., 0.]]), labels=["x", "y"]), - ] - ) - - pts = spatial_domain2.sample(100) - fig, axs = plt.subplots(1, 2, figsize=(16, 6)) - for domain, ax in zip([spatial_domain, spatial_domain2], axs): - pts = domain.sample(1000) - plot_scatter(ax, pts, 'Simplex Domain') - - - -.. image:: tutorial_files/tutorial_13_0.png - - -Boolean Operations ------------------- - -To create complex shapes we can use the boolean operations, for example -to merge two default geometries. We need to simply use the ``Union`` -class: it takes a list of geometries and returns the union of them. - -Let’s create three unions. Firstly, it will be a union of ``cartesian`` -and ``ellipsoid_no_border``. Next, it will be a union of -``ellipse_no_border`` and ``ellipse_border``. Lastly, it will be a union -of all three geometries. - -.. code:: ipython3 - - cart_ellipse_nb_union = Union([cartesian, ellipsoid_no_border]) - cart_ellipse_b_union = Union([cartesian, ellipsoid_border]) - three_domain_union = Union([cartesian, ellipsoid_no_border, ellipsoid_border]) - -We can of course sample points over the new geometries, by using the -``sample`` method as before. We highlihgt that the available sample -strategy here is only *random*. - -.. code:: ipython3 - - c_e_nb_u_points = cart_ellipse_nb_union.sample(n=2000, mode='random') - c_e_b_u_points = cart_ellipse_b_union.sample(n=2000, mode='random') - three_domain_union_points = three_domain_union.sample(n=3000, mode='random') - -We can plot the samples of each of the unions to see what we are working -with. - -.. code:: ipython3 - - fig, axs = plt.subplots(1, 3, figsize=(16, 4)) - pts_list = [c_e_nb_u_points, c_e_b_u_points, three_domain_union_points] - title_list = ['Cartesian with Ellipsoid No Border Union', 'Cartesian with Ellipsoid Border Union', 'Three Domain Union'] - for ax, pts, title in zip(axs, pts_list, title_list): - plot_scatter(ax, pts, title) - - - -.. image:: tutorial_files/tutorial_20_0.png - - -Now, we will find the differences of the geometries. We will find the -difference of ``cartesian`` and ``ellipsoid_no_border``. - -.. code:: ipython3 - - cart_ellipse_nb_difference = Difference([cartesian, ellipsoid_no_border]) - c_e_nb_d_points = cart_ellipse_nb_difference.sample(n=2000, mode='random') - - fig, ax = plt.subplots(1, 1, figsize=(8, 6)) - plot_scatter(ax, c_e_nb_d_points, 'Difference') - - - -.. image:: tutorial_files/tutorial_22_0.png - - -Create Custom Location ----------------------- - -We will take a look on how to create our own geometry. The one we will -try to make is a heart defined by the function - -.. math:: (x^2+y^2-1)^3-x^2y^3 \le 0 - -Let’s start by importing what we will need to create our own geometry -based on this equation. - -.. code:: ipython3 - - import torch - from pina import Location - from pina import LabelTensor - import random - -Next, we will create the ``Heart(Location)`` class and initialize it. - -.. code:: ipython3 - - class Heart(Location): - """Implementation of the Heart Domain.""" - - def __init__(self, sample_border=False): - super().__init__() - - -Because the ``Location`` class we are inherting from requires both a -``sample`` method and ``is_inside`` method, we will create them and just -add in “pass” for the moment. - -.. code:: ipython3 - - class Heart(Location): - """Implementation of the Heart Domain.""" - - def __init__(self, sample_border=False): - super().__init__() - - def is_inside(self): - pass - - def sample(self): - pass - -Now we have the skeleton for our ``Heart`` class. The ``sample`` -method is where most of the work is done so let’s fill it out. - -.. code:: ipython3 - - - class Heart(Location): - """Implementation of the Heart Domain.""" - - def __init__(self, sample_border=False): - super().__init__() - - def is_inside(self): - pass - - def sample(self, n, mode='random', variables='all'): - sampled_points = [] - - while len(sampled_points) < n: - x = torch.rand(1)*3.-1.5 - y = torch.rand(1)*3.-1.5 - if ((x**2 + y**2 - 1)**3 - (x**2)*(y**3)) <= 0: - sampled_points.append([x.item(), y.item()]) - - return LabelTensor(torch.tensor(sampled_points), labels=['x','y']) - -To create the Heart geometry we simply run: - -.. code:: ipython3 - - heart = Heart() - -To sample from the Heart geometry we simply run: - -.. code:: ipython3 - - pts_heart = heart.sample(1500) - - fig, ax = plt.subplots() - plot_scatter(ax, pts_heart, 'Heart Domain') - - - -.. image:: tutorial_files/tutorial_36_0.png - - -What’s next? ------------- - -We have made a very simple tutorial on how to build custom geometries -and use domain operation to compose base geometries. Now you can play -around with different geometries and build your own! diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_10_0.png b/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_10_0.png deleted file mode 100644 index b253ffa17..000000000 Binary files a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_10_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_13_0.png b/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_13_0.png deleted file mode 100644 index a64e90b13..000000000 Binary files a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_13_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_20_0.png b/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_20_0.png deleted file mode 100644 index 42862ad68..000000000 Binary files a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_20_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_22_0.png b/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_22_0.png deleted file mode 100644 index 5a573bbdb..000000000 Binary files a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_22_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_36_0.png b/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_36_0.png deleted file mode 100644 index 85846024f..000000000 Binary files a/docs/source/_rst/tutorials/tutorial6/tutorial_files/tutorial_36_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial7/tutorial.rst b/docs/source/_rst/tutorials/tutorial7/tutorial.rst deleted file mode 100644 index ac5ace30e..000000000 --- a/docs/source/_rst/tutorials/tutorial7/tutorial.rst +++ /dev/null @@ -1,240 +0,0 @@ -Tutorial: Resolution of an inverse problem -============================================ - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial7/tutorial.ipynb - -Introduction to the inverse problem -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This tutorial shows how to solve an inverse Poisson problem with -Physics-Informed Neural Networks. The problem definition is that of a -Poisson problem with homogeneous boundary conditions and it reads: - -.. math:: - - \begin{equation} - \begin{cases} - \Delta u = e^{-2(x-\mu_1)^2-2(y-\mu_2)^2} \text{ in } \Omega\, ,\\ - u = 0 \text{ on }\partial \Omega,\\ - u(\mu_1, \mu_2) = \text{ data} - \end{cases} - \end{equation} - -where :math:`\Omega` is a square domain -:math:`[-2, 2] \times [-2, 2]`, and -:math:`\partial \Omega=\Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4` -is the union of the boundaries of the domain. - -This kind of problem, namely the “inverse problem”, has two main goals: - -* find the solution :math:`u` that satisfies the Poisson equation -* find the unknown parameters (:math:`\mu_1`, :math:`\mu_2`) that better fit some given data (third equation in the system above). - -In order to achieve both the goals we will need to define an -``InverseProblem`` in PINA. Let’s start with useful imports. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - # get the data - !mkdir "data" - !wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pinn_solution_0.5_0.5" -O "data/pinn_solution_0.5_0.5" - !wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pts_0.5_0.5" -O "data/pts_0.5_0.5" - - - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - import torch - from pytorch_lightning.callbacks import Callback - from pina.problem import SpatialProblem, InverseProblem - from pina.operators import laplacian - from pina.model import FeedForward - from pina.equation import Equation, FixedValue - from pina import Condition, Trainer - from pina.solvers import PINN - from pina.geometry import CartesianDomain - -Then, we import the pre-saved data, for (:math:`\mu_1`, -:math:`\mu_2`)=(:math:`0.5`, :math:`0.5`). These two values are the -optimal parameters that we want to find through the neural network -training. In particular, we import the ``input_points``\ (the spatial -coordinates), and the ``output_points`` (the corresponding :math:`u` -values evaluated at the ``input_points``). - -.. code:: ipython3 - - data_output = torch.load('data/pinn_solution_0.5_0.5').detach() - data_input = torch.load('data/pts_0.5_0.5') - -Moreover, let’s plot also the data points and the reference solution: -this is the expected output of the neural network. - -.. code:: ipython3 - - points = data_input.extract(['x', 'y']).detach().numpy() - truth = data_output.detach().numpy() - - plt.scatter(points[:, 0], points[:, 1], c=truth, s=8) - plt.axis('equal') - plt.colorbar() - plt.show() - - - -.. image:: tutorial_files/output_8_0.png - - -Inverse problem definition in PINA -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Then, we initialize the Poisson problem, that is inherited from the -``SpatialProblem`` and from the ``InverseProblem`` classes. We here have -to define all the variables, and the domain where our unknown parameters -(:math:`\mu_1`, :math:`\mu_2`) belong. Notice that the laplace equation -takes as inputs also the unknown variables, that will be treated as -parameters that the neural network optimizes during the training -process. - -.. code:: ipython3 - - ### Define ranges of variables - x_min = -2 - x_max = 2 - y_min = -2 - y_max = 2 - - class Poisson(SpatialProblem, InverseProblem): - ''' - Problem definition for the Poisson equation. - ''' - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [x_min, x_max], 'y': [y_min, y_max]}) - # define the ranges for the parameters - unknown_parameter_domain = CartesianDomain({'mu1': [-1, 1], 'mu2': [-1, 1]}) - - def laplace_equation(input_, output_, params_): - ''' - Laplace equation with a force term. - ''' - force_term = torch.exp( - - 2*(input_.extract(['x']) - params_['mu1'])**2 - - 2*(input_.extract(['y']) - params_['mu2'])**2) - delta_u = laplacian(output_, input_, components=['u'], d=['x', 'y']) - - return delta_u - force_term - - # define the conditions for the loss (boundary conditions, equation, data) - conditions = { - 'gamma1': Condition(location=CartesianDomain({'x': [x_min, x_max], - 'y': y_max}), - equation=FixedValue(0.0, components=['u'])), - 'gamma2': Condition(location=CartesianDomain({'x': [x_min, x_max], 'y': y_min - }), - equation=FixedValue(0.0, components=['u'])), - 'gamma3': Condition(location=CartesianDomain({'x': x_max, 'y': [y_min, y_max] - }), - equation=FixedValue(0.0, components=['u'])), - 'gamma4': Condition(location=CartesianDomain({'x': x_min, 'y': [y_min, y_max] - }), - equation=FixedValue(0.0, components=['u'])), - 'D': Condition(location=CartesianDomain({'x': [x_min, x_max], 'y': [y_min, y_max] - }), - equation=Equation(laplace_equation)), - 'data': Condition(input_points=data_input.extract(['x', 'y']), output_points=data_output) - } - - problem = Poisson() - -Then, we define the model of the neural network we want to use. Here we -used a model which impose hard constrains on the boundary conditions, as -also done in the Wave tutorial! - -.. code:: ipython3 - - model = FeedForward( - layers=[20, 20, 20], - func=torch.nn.Softplus, - output_dimensions=len(problem.output_variables), - input_dimensions=len(problem.input_variables) - ) - -After that, we discretize the spatial domain. - -.. code:: ipython3 - - problem.discretise_domain(20, 'grid', locations=['D'], variables=['x', 'y']) - problem.discretise_domain(1000, 'random', locations=['gamma1', 'gamma2', - 'gamma3', 'gamma4'], variables=['x', 'y']) - -Here, we define a simple callback for the trainer. We use this callback -to save the parameters predicted by the neural network during the -training. The parameters are saved every 100 epochs as ``torch`` tensors -in a specified directory (``tmp_dir`` in our case). The goal is to read -the saved parameters after training and plot their trend across the -epochs. - -.. code:: ipython3 - - # temporary directory for saving logs of training - tmp_dir = "tmp_poisson_inverse" - - class SaveParameters(Callback): - ''' - Callback to save the parameters of the model every 100 epochs. - ''' - def on_train_epoch_end(self, trainer, __): - if trainer.current_epoch % 100 == 99: - torch.save(trainer.solver.problem.unknown_parameters, '{}/parameters_epoch{}'.format(tmp_dir, trainer.current_epoch)) - -Then, we define the ``PINN`` object and train the solver using the -``Trainer``. - -.. code:: ipython3 - - ### train the problem with PINN - max_epochs = 5000 - pinn = PINN(problem, model, optimizer_kwargs={'lr':0.005}) - # define the trainer for the solver - trainer = Trainer(solver=pinn, accelerator='cpu', max_epochs=max_epochs, - default_root_dir=tmp_dir, callbacks=[SaveParameters()]) - trainer.train() - -One can now see how the parameters vary during the training by reading -the saved solution and plotting them. The plot shows that the parameters -stabilize to their true value before reaching the epoch :math:`1000`! - -.. code:: ipython3 - - epochs_saved = range(99, max_epochs, 100) - parameters = torch.empty((int(max_epochs/100), 2)) - for i, epoch in enumerate(epochs_saved): - params_torch = torch.load('{}/parameters_epoch{}'.format(tmp_dir, epoch)) - for e, var in enumerate(pinn.problem.unknown_variables): - parameters[i, e] = params_torch[var].data - - # Plot parameters - plt.close() - plt.plot(epochs_saved, parameters[:, 0], label='mu1', marker='o') - plt.plot(epochs_saved, parameters[:, 1], label='mu2', marker='s') - plt.ylim(-1, 1) - plt.grid() - plt.legend() - plt.xlabel('Epoch') - plt.ylabel('Parameter value') - plt.show() - - - -.. image:: tutorial_files/output_21_0.png - - diff --git a/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_21_0.png b/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_21_0.png deleted file mode 100644 index 39f313bf3..000000000 Binary files a/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_21_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_8_0.png b/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_8_0.png deleted file mode 100644 index 4f706c373..000000000 Binary files a/docs/source/_rst/tutorials/tutorial7/tutorial_files/output_8_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial8/tutorial.rst b/docs/source/_rst/tutorials/tutorial8/tutorial.rst deleted file mode 100644 index 6be60b4e6..000000000 --- a/docs/source/_rst/tutorials/tutorial8/tutorial.rst +++ /dev/null @@ -1,403 +0,0 @@ -Tutorial: Reduced order model (POD-RBF or POD-NN) for parametric problems -========================================================================= - -|Open In Colab| - -.. |Open In Colab| image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial8/tutorial.ipynb - -The tutorial aims to show how to employ the **PINA** library in order to -apply a reduced order modeling technique [1]. Such methodologies have -several similarities with machine learning approaches, since the main -goal consists in predicting the solution of differential equations -(typically parametric PDEs) in a real-time fashion. - -In particular we are going to use the Proper Orthogonal Decomposition -with either Radial Basis Function Interpolation(POD-RBF) or Neural -Network (POD-NN) [2]. Here we basically perform a dimensional reduction -using the POD approach, and approximating the parametric solution -manifold (at the reduced space) using an interpolation (RBF) or a -regression technique (NN). In this example, we use a simple multilayer -perceptron, but the plenty of different architectures can be plugged as -well. - -References -^^^^^^^^^^ - -1. Rozza G., Stabile G., Ballarin F. (2022). Advanced Reduced Order - Methods and Applications in Computational Fluid Dynamics, Society for - Industrial and Applied Mathematics. -2. Hesthaven, J. S., & Ubbiali, S. (2018). Non-intrusive reduced order - modeling of nonlinear problems using neural networks. Journal of - Computational Physics, 363, 55-78. - -Let’s start with the necessary imports. It’s important to note the -minimum PINA version to run this tutorial is the ``0.1``. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - %matplotlib inline - - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - import torch - import pina - - from pina.geometry import CartesianDomain - - from pina.problem import ParametricProblem - from pina.model.layers import PODBlock, RBFBlock - from pina import Condition, LabelTensor, Trainer - from pina.model import FeedForward - from pina.solvers import SupervisedSolver - - print(f'We are using PINA version {pina.__version__}') - - -.. parsed-literal:: - - We are using PINA version 0.1.1 - - -We exploit the `Smithers `__ library to -collect the parametric snapshots. In particular, we use the -``NavierStokesDataset`` class that contains a set of parametric -solutions of the Navier-Stokes equations in a 2D L-shape domain. The -parameter is the inflow velocity. The dataset is composed by 500 -snapshots of the velocity (along :math:`x`, :math:`y`, and the -magnitude) and pressure fields, and the corresponding parameter values. - -To visually check the snapshots, let’s plot also the data points and the -reference solution: this is the expected output of our model. - -.. code:: ipython3 - - from smithers.dataset import NavierStokesDataset - dataset = NavierStokesDataset() - - fig, axs = plt.subplots(1, 4, figsize=(14, 3)) - for ax, p, u in zip(axs, dataset.params[:4], dataset.snapshots['mag(v)'][:4]): - ax.tricontourf(dataset.triang, u, levels=16) - ax.set_title(f'$\mu$ = {p[0]:.2f}') - - - -.. image:: tutorial_files/tutorial_5_0.png - - -The *snapshots* - aka the numerical solutions computed for several -parameters - and the corresponding parameters are the only data we need -to train the model, in order to predict the solution for any new test -parameter. To properly validate the accuracy, we initially split the 500 -snapshots into the training dataset (90% of the original data) and the -testing one (the reamining 10%). It must be said that, to plug the -snapshots into **PINA**, we have to cast them to ``LabelTensor`` -objects. - -.. code:: ipython3 - - u = torch.tensor(dataset.snapshots['mag(v)']).float() - p = torch.tensor(dataset.params).float() - - p = LabelTensor(p, labels=['mu']) - u = LabelTensor(u, labels=[f's{i}' for i in range(u.shape[1])]) - - ratio_train_test = 0.9 - n = u.shape - n_train = int(u.shape[0] * ratio_train_test) - n_test = u - n_train - u_train, u_test = u[:n_train], u[n_train:] - p_train, p_test = p[:n_train], p[n_train:] - -It is now time to define the problem! We inherit from -``ParametricProblem`` (since the space invariant typically of this -methodology), just defining a simple *input-output* condition. - -.. code:: ipython3 - - class SnapshotProblem(ParametricProblem): - output_variables = [f's{i}' for i in range(u.shape[1])] - parameter_domain = CartesianDomain({'mu': [0, 100]}) - - conditions = { - 'io': Condition(input_points=p_train, output_points=u_train) - } - - poisson_problem = SnapshotProblem() - -We can then build a ``PODRBF`` model (using a Radial Basis Function -interpolation as approximation) and a ``PODNN`` approach (using an MLP -architecture as approximation). - -POD-RBF reduced order model ---------------------------- - -Then, we define the model we want to use, with the POD (``PODBlock``) -and the RBF (``RBFBlock``) objects. - -.. code:: ipython3 - - class PODRBF(torch.nn.Module): - """ - Proper orthogonal decomposition with Radial Basis Function interpolation model. - """ - - def __init__(self, pod_rank, rbf_kernel): - """ - - """ - super().__init__() - - self.pod = PODBlock(pod_rank) - self.rbf = RBFBlock(kernel=rbf_kernel) - - - def forward(self, x): - """ - Defines the computation performed at every call. - - :param x: The tensor to apply the forward pass. - :type x: torch.Tensor - :return: the output computed by the model. - :rtype: torch.Tensor - """ - coefficents = self.rbf(x) - return self.pod.expand(coefficents) - - def fit(self, p, x): - """ - Call the :meth:`pina.model.layers.PODBlock.fit` method of the - :attr:`pina.model.layers.PODBlock` attribute to perform the POD, - and the :meth:`pina.model.layers.RBFBlock.fit` method of the - :attr:`pina.model.layers.RBFBlock` attribute to fit the interpolation. - """ - self.pod.fit(x) - self.rbf.fit(p, self.pod.reduce(x)) - -We can then fit the model and ask it to predict the required field for -unseen values of the parameters. Note that this model does not need a -``Trainer`` since it does not include any neural network or learnable -parameters. - -.. code:: ipython3 - - pod_rbf = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline') - pod_rbf.fit(p_train, u_train) - -.. code:: ipython3 - - u_test_rbf = pod_rbf(p_test) - u_train_rbf = pod_rbf(p_train) - - relative_error_train = torch.norm(u_train_rbf - u_train)/torch.norm(u_train) - relative_error_test = torch.norm(u_test_rbf - u_test)/torch.norm(u_test) - - print('Error summary for POD-RBF model:') - print(f' Train: {relative_error_train.item():e}') - print(f' Test: {relative_error_test.item():e}') - - -.. parsed-literal:: - - Error summary for POD-RBF model: - Train: 1.287801e-03 - Test: 1.217041e-03 - - -POD-NN reduced order model --------------------------- - -.. code:: ipython3 - - class PODNN(torch.nn.Module): - """ - Proper orthogonal decomposition with neural network model. - """ - - def __init__(self, pod_rank, layers, func): - """ - - """ - super().__init__() - - self.pod = PODBlock(pod_rank) - self.nn = FeedForward( - input_dimensions=1, - output_dimensions=pod_rank, - layers=layers, - func=func - ) - - - def forward(self, x): - """ - Defines the computation performed at every call. - - :param x: The tensor to apply the forward pass. - :type x: torch.Tensor - :return: the output computed by the model. - :rtype: torch.Tensor - """ - coefficents = self.nn(x) - return self.pod.expand(coefficents) - - def fit_pod(self, x): - """ - Just call the :meth:`pina.model.layers.PODBlock.fit` method of the - :attr:`pina.model.layers.PODBlock` attribute. - """ - self.pod.fit(x) - -We highlight that the POD modes are directly computed by means of the -singular value decomposition (computed over the input data), and not -trained using the backpropagation approach. Only the weights of the MLP -are actually trained during the optimization loop. - -.. code:: ipython3 - - pod_nn = PODNN(pod_rank=20, layers=[10, 10, 10], func=torch.nn.Tanh) - pod_nn.fit_pod(u_train) - - pod_nn_stokes = SupervisedSolver( - problem=poisson_problem, - model=pod_nn, - optimizer=torch.optim.Adam, - optimizer_kwargs={'lr': 0.0001}) - -Now that we have set the ``Problem`` and the ``Model``, we have just to -train the model and use it for predicting the test snapshots. - -.. code:: ipython3 - - trainer = Trainer( - solver=pod_nn_stokes, - max_epochs=1000, - batch_size=100, - log_every_n_steps=5, - accelerator='cpu') - trainer.train() - - -.. parsed-literal:: - - GPU available: True (cuda), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - /u/a/aivagnes/anaconda3/lib/python3.8/site-packages/pytorch_lightning/trainer/setup.py:187: GPU available but not used. You can set it by doing `Trainer(accelerator='gpu')`. - - | Name | Type | Params - ---------------------------------------- - 0 | _loss | MSELoss | 0 - 1 | _neural_net | Network | 460 - ---------------------------------------- - 460 Trainable params - 0 Non-trainable params - 460 Total params - 0.002 Total estimated model params size (MB) - /u/a/aivagnes/anaconda3/lib/python3.8/site-packages/torch/cuda/__init__.py:152: UserWarning: - Found GPU0 Quadro K600 which is of cuda capability 3.0. - PyTorch no longer supports this GPU because it is too old. - The minimum cuda capability supported by this library is 3.7. - - warnings.warn(old_gpu_warn % (d, name, major, minor, min_arch // 10, min_arch % 10)) - - - -.. parsed-literal:: - - Training: | | 0/? [00:00`__. - -First of all, some useful imports. - -.. code:: ipython3 - - ## routine needed to run the notebook on Google Colab - try: - import google.colab - IN_COLAB = True - except: - IN_COLAB = False - if IN_COLAB: - !pip install "pina-mathlab" - - import torch - import matplotlib.pyplot as plt - plt.style.use('tableau-colorblind10') - - from pina import Condition, Plotter - from pina.problem import SpatialProblem - from pina.operators import laplacian - from pina.model import FeedForward - from pina.model.layers import PeriodicBoundaryEmbedding # The PBC module - from pina.solvers import PINN - from pina.trainer import Trainer - from pina.geometry import CartesianDomain - from pina.equation import Equation - -The problem definition ----------------------- - -The one-dimensional Helmotz problem is mathematically written as: - -.. math:: - - - \begin{cases} - \frac{d^2}{dx^2}u(x) - \lambda u(x) -f(x) &= 0 \quad x\in(0,2)\\ - u^{(m)}(x=0) - u^{(m)}(x=2) &= 0 \quad m\in[0, 1, \cdots]\\ - \end{cases} - -In this case we are asking the solution to be :math:`C^{\infty}` -periodic with period :math:`2`, on the inifite domain -:math:`x\in(-\infty, \infty)`. Notice that the classical PINN would need -inifinite conditions to evaluate the PBC loss function, one for each -derivative, which is of course infeasable… A possible solution, -diverging from the original PINN formulation, is to use *coordinates -augmentation*. In coordinates augmentation you seek for a coordinates -transformation :math:`v` such that :math:`x\rightarrow v(x)` such that -the periodicity condition -:math:`u^{(m)}(x=0) - u^{(m)}(x=2) = 0 \quad, m\in[0, 1, \cdots]` is satisfied. - -For demonstration porpuses the problem specifics are -:math:`\lambda=-10\pi^2`, and -:math:`f(x)=-6\pi^2\sin(3\pi x)\cos(\pi x)` which gives a solution that -can be computed analytically :math:`u(x) = \sin(\pi x)\cos(3\pi x)`. - -.. code:: ipython3 - - class Helmotz(SpatialProblem): - output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 2]}) - - def helmotz_equation(input_, output_): - x = input_.extract('x') - u_xx = laplacian(output_, input_, components=['u'], d=['x']) - f = - 6.*torch.pi**2 * torch.sin(3*torch.pi*x)*torch.cos(torch.pi*x) - lambda_ = - 10. * torch.pi ** 2 - return u_xx - lambda_ * output_ - f - - # here we write the problem conditions - conditions = { - 'D': Condition(location=spatial_domain, - equation=Equation(helmotz_equation)), - } - - def helmotz_sol(self, pts): - return torch.sin(torch.pi * pts) * torch.cos(3. * torch.pi * pts) - - truth_solution = helmotz_sol - - problem = Helmotz() - - # let's discretise the domain - problem.discretise_domain(200, 'grid', locations=['D']) - -As usual the Helmotz problem is written in **PINA** code as a class. The -equations are written as ``conditions`` that should be satisfied in the -corresponding domains. The ``truth_solution`` is the exact solution -which will be compared with the predicted one. We used latin hypercube -sampling for choosing the collocation points. - -Solving the problem with a Periodic Network -------------------------------------------- - -Any :math:`\mathcal{C}^{\infty}` periodic function -:math:`u : \mathbb{R} \rightarrow \mathbb{R}` with period -:math:`L\in\mathbb{N}` can be constructed by composition of an arbitrary -smooth function :math:`f : \mathbb{R}^n \rightarrow \mathbb{R}` and a -given smooth periodic function -:math:`v : \mathbb{R} \rightarrow \mathbb{R}^n` with period :math:`L`, -that is :math:`u(x) = f(v(x))`. The formulation is generalizable for -arbitrary dimension, see `A method for representing periodic functions -and enforcing exactly periodic boundary conditions with deep neural -networks `__. - -In our case, we rewrite -:math:`v(x) = \left[1, \cos\left(\frac{2\pi}{L} x\right), \sin\left(\frac{2\pi}{L} x\right)\right]`, -i.e the coordinates augmentation, and -:math:`f(\cdot) = NN_{\theta}(\cdot)` i.e. a neural network. The -resulting neural network obtained by composing :math:`f` with :math:`v` -gives the PINN approximate solution, that is -:math:`u(x) \approx u_{\theta}(x)=NN_{\theta}(v(x))`. - -In **PINA** this translates in using the ``PeriodicBoundaryEmbedding`` layer for -:math:`v`, and any ``pina.model`` for :math:`NN_{\theta}`. Let’s see it -in action! - -.. code:: ipython3 - - # we encapsulate all modules in a torch.nn.Sequential container - model = torch.nn.Sequential(PeriodicBoundaryEmbedding(input_dimension=1, - periods=2), - FeedForward(input_dimensions=3, # output of PeriodicBoundaryEmbedding = 3 * input_dimension - output_dimensions=1, - layers=[10, 10])) - -As simple as that! Notice in higher dimension you can specify different -periods for all dimensions using a dictionary, -e.g. ``periods={'x':2, 'y':3, ...}`` would indicate a periodicity of -:math:`2` in :math:`x`, :math:`3` in :math:`y`, and so on… - -We will now sole the problem as usually with the ``PINN`` and -``Trainer`` class. - -.. code:: ipython3 - - pinn = PINN(problem=problem, model=model) - trainer = Trainer(pinn, max_epochs=5000, accelerator='cpu', enable_model_summary=False) # we train on CPU and avoid model summary at beginning of training (optional) - trainer.train() - - -.. parsed-literal:: - - GPU available: True (mps), used: False - TPU available: False, using: 0 TPU cores - IPU available: False, using: 0 IPUs - HPU available: False, using: 0 HPUs - -.. parsed-literal:: - - `Trainer.fit` stopped: `max_epochs=5000` reached. - - -.. parsed-literal:: - - Epoch 4999: 100%|██████████| 1/1 [00:00<00:00, 155.47it/s, v_num=20, D_loss=0.0123, mean_loss=0.0123] - - -We are going to plot the solution now! - -.. code:: ipython3 - - pl = Plotter() - pl.plot(pinn) - - - -.. image:: tutorial_files/tutorial_11_0.png - - -Great, they overlap perfectly! This seeams a good result, considering -the simple neural network used to some this (complex) problem. We will -now test the neural network on the domain :math:`[-4, 4]` without -retraining. In principle the periodicity should be present since the -:math:`v` function ensures the periodicity in :math:`(-\infty, \infty)`. - -.. code:: ipython3 - - # plotting solution - with torch.no_grad(): - # Notice here we put [-4, 4]!!! - new_domain = CartesianDomain({'x' : [0, 4]}) - x = new_domain.sample(1000, mode='grid') - fig, axes = plt.subplots(1, 3, figsize=(15, 5)) - # Plot 1 - axes[0].plot(x, problem.truth_solution(x), label=r'$u(x)$', color='blue') - axes[0].set_title(r'True solution $u(x)$') - axes[0].legend(loc="upper right") - # Plot 2 - axes[1].plot(x, pinn(x), label=r'$u_{\theta}(x)$', color='green') - axes[1].set_title(r'PINN solution $u_{\theta}(x)$') - axes[1].legend(loc="upper right") - # Plot 3 - diff = torch.abs(problem.truth_solution(x) - pinn(x)) - axes[2].plot(x, diff, label=r'$|u(x) - u_{\theta}(x)|$', color='red') - axes[2].set_title(r'Absolute difference $|u(x) - u_{\theta}(x)|$') - axes[2].legend(loc="upper right") - # Adjust layout - plt.tight_layout() - # Show the plots - plt.show() - - - -.. image:: tutorial_files/tutorial_13_0.png - - -It is pretty clear that the network is periodic, with also the error -following a periodic pattern. Obviusly a longer training, and a more -expressive neural network could improve the results! - -What’s next? ------------- - -Nice you have completed the one dimensional Helmotz tutorial of -**PINA**! There are multiple directions you can go now: - -1. Train the network for longer or with different layer sizes and assert - the finaly accuracy - -2. Apply the ``PeriodicBoundaryEmbedding`` layer for a time-dependent problem (see - reference in the documentation) - -3. Exploit extrafeature training ? - -4. Many more… diff --git a/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_11_0.png b/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_11_0.png deleted file mode 100644 index baf10c71f..000000000 Binary files a/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_11_0.png and /dev/null differ diff --git a/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_13_0.png b/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_13_0.png deleted file mode 100644 index 4178e8274..000000000 Binary files a/docs/source/_rst/tutorials/tutorial9/tutorial_files/tutorial_13_0.png and /dev/null differ diff --git a/docs/source/_team.rst b/docs/source/_team.rst index 973274d1b..287f11fcc 100644 --- a/docs/source/_team.rst +++ b/docs/source/_team.rst @@ -1,10 +1,14 @@ PINA Team ============== -PINA is currently developed in the `SISSA MathLab `_, in collaboration with `Fast Computing `_. +**PINA** is currently developed in the `SISSA MathLab `_, in collaboration with `Fast Computing `_. -A significant part of PINA has been written either as a by-product for other projects people were funded for, or by people on university-funded positions. -There are probably many of such projects that have led to some development of PINA. We are very grateful for this support! +.. figure:: index_files/fast_mathlab.png + :align: center + :width: 500 + +A significant part of **PINA** has been written either as a by-product for other projects people were funded for, or by people on university-funded positions. +There are probably many of such projects that have led to some development of **PINA**. We are very grateful for this support! In particular, we acknowledge the following sources of support with great gratitude: * `H2020 ERC CoG 2015 AROMA-CFD project 681447 `_, P.I. Professor `Prof. Gianluigi Rozza `_ at `SISSA MathLab `_. @@ -12,11 +16,12 @@ In particular, we acknowledge the following sources of support with great gratit .. figure:: index_files/foudings.png :align: center - :width: 400 + :width: 500 We also acknowledge the contribuition of `Maria Strazzullo `_ in the early developments of the package. A special -thank goeas to all the students and researchers from different universities which contributed to the package. Finally we warmly thank all the -`contributors `_! +thank goeas to all the students and researchers from different universities which contributed to the package. +Finally we warmly thank all the +`contributors `_ which are the real heart of **PINA**! .. figure:: index_files/university_dev_pina.png :align: center diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html new file mode 100644 index 000000000..c1bc42107 --- /dev/null +++ b/docs/source/_templates/layout.html @@ -0,0 +1,17 @@ +{% extends "!layout.html" %} + +{%- block footer %} + +{%- endblock %} \ No newline at end of file diff --git a/docs/source/_tutorial.rst b/docs/source/_tutorial.rst new file mode 100644 index 000000000..745e575ba --- /dev/null +++ b/docs/source/_tutorial.rst @@ -0,0 +1,35 @@ +PINA Tutorials +====================== + + +In this folder we collect useful tutorials in order to understand the principles and the potential of **PINA**. + +Getting started with PINA +------------------------- + +- `Introduction to PINA for Physics Informed Neural Networks training `_ +- `Introduction to PINA Equation class `_ +- `PINA and PyTorch Lightning, training tips and visualizations `_ +- `Building custom geometries with PINA Location class `_ + + +Physics Informed Neural Networks +-------------------------------- + +- `Two dimensional Poisson problem using Extra Features Learning `_ +- `Two dimensional Wave problem with hard constraint `_ +- `Resolution of a 2D Poisson inverse problem `_ +- `Periodic Boundary Conditions for Helmotz Equation `_ +- `Multiscale PDE learning with Fourier Feature Network `_ + +Neural Operator Learning +------------------------ + +- `Two dimensional Darcy flow using the Fourier Neural Operator `_ +- `Time dependent Kuramoto Sivashinsky equation using the Averaging Neural Operator `_ + +Supervised Learning +------------------- + +- `Unstructured convolutional autoencoder via continuous convolution `_ +- `POD-RBF and POD-NN for reduced order modeling `_ diff --git a/docs/source/conf.py b/docs/source/conf.py index d0ddc09a5..7d982236f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,89 +14,72 @@ import sys import os -import sphinx_rtd_theme +import time import importlib.metadata # -- Project information ----------------------------------------------------- -_DISTRIBUTION_METADATA = importlib.metadata.metadata('pina-mathlab') -project = _DISTRIBUTION_METADATA['Name'] -copyright = _DISTRIBUTION_METADATA['License-File'] -author = "PINA contributors" -version = _DISTRIBUTION_METADATA['Version'] +_DISTRIBUTION_METADATA = importlib.metadata.metadata("pina-mathlab") +project = _DISTRIBUTION_METADATA["Name"] +copyright = f'2021-{time.strftime("%Y")}' +author = "PINA Contributors" +version = _DISTRIBUTION_METADATA["Version"] -sys.path.insert(0, os.path.abspath('../sphinx_extensions')) # extension to remove paramref link from lightinig +sys.path.insert(0, os.path.abspath("../sphinx_extensions")) # -- General configuration ------------------------------------------------ extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', - 'sphinx.ext.intersphinx', - 'paramref_extension', # this extension is made to remove paramref links from lightining doc - 'sphinx_copybutton', - 'sphinx_design' + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "sphinx.ext.intersphinx", + "paramref_extension", # this extension is made to remove paramref links from lightining doc + "sphinx_copybutton", + "sphinx_design", ] -# The root document. -root_doc = 'index' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'docstrings', 'nextgen', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["build", "docstrings", "nextgen", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = 'literal' +default_role = "literal" # Generate the API documentation when building autosummary_generate = True numpydoc_show_class_members = False intersphinx_mapping = { - 'python': ('http://docs.python.org/3', None), - 'matplotlib': ('https://matplotlib.org/stable', None), - 'torch': ('https://pytorch.org/docs/stable/', None), - 'lightning.pytorch': ("https://lightning.ai/docs/pytorch/stable/", None), - } - -nitpicky = True -nitpick_ignore = [ - # ('py:meth', 'lightning.pytorch.core.module.LightningModule.log'), - # ('py:meth', 'lightning.pytorch.core.module.LightningModule.log_dict'), - # ('py:exc', 'MisconfigurationException'), - # ('py:func', 'torch.inference_mode'), - # ('py:func', 'torch.no_grad'), - # ('py:class', 'torch.utils.data.DistributedSampler'), - # ('py:class', 'pina.model.layers.convolution.BaseContinuousConv'), - # ('py:class', 'Module'), - # ('py:class', 'torch.nn.modules.loss._Loss'), # TO FIX - # ('py:class', 'torch.optim.LRScheduler'), # TO FIX - - ] - + "python": ("http://docs.python.org/3", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "torch": ("https://pytorch.org/docs/stable/", None), + "lightning.pytorch": ("https://lightning.ai/docs/pytorch/stable/", None), + "torch_geometric": ( + "https://pytorch-geometric.readthedocs.io/en/latest/", + None, + ), +} # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # autoclass -autoclass_content = 'both' +autoclass_content = "both" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -105,10 +88,9 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -# # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -122,7 +104,7 @@ add_module_names = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sortins as "systems = False @@ -143,18 +125,11 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'pydata_sphinx_theme' +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. html_logo = "index_files/PINA_logo.png" html_theme_options = { "icon_links": [ @@ -162,7 +137,7 @@ "name": "GitHub", "url": "https://github.com/mathLab/PINA", "icon": "fab fa-github", - "type": "fontawesome", + "type": "fontawesome", }, { "name": "Twitter", @@ -172,7 +147,7 @@ }, { "name": "Email", - "url": "mailto:pina.mathlab@gmail.com", + "url": "mailto:pina.mathlab@gmail.com", "icon": "fas fa-envelope", "type": "fontawesome", }, @@ -183,9 +158,13 @@ "header_links_before_dropdown": 8, } +html_context = { + "default_mode": "light", +} + # If not ''i, a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = "%b %d, %Y" # If false, no index is generated. html_use_index = True @@ -197,40 +176,52 @@ html_show_copyright = True # Output file base name for HTML help builder. -htmlhelp_basename = 'pinadoc' +htmlhelp_basename = "pinadoc" + +# Link to external html files +html_extra_path = ["tutorials"] + +# Avoid side bar for html files +html_sidebars = { + "_tutorial": [], + "_team": [], + "_cite": [], + "_contributing": [], + "_installation": [], + "_LICENSE": [], +} # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - 'papersize': 'a4paper', - + "papersize": "a4paper", # The font size ('10pt', '11pt' or '12pt'). - 'pointsize': '20pt', - + "pointsize": "20pt", # Additional stuff for the LaTeX preamble. - 'preamble': '', - + "preamble": "", # Latex figure (float) alignment - 'figure_align': 'htbp', + "figure_align": "htbp", } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pina.tex', u'PINA Documentation', - u'PINA contributors', 'manual'), + ( + master_doc, + "pina.tex", + "PINA Documentation", + "PINA contributors", + "manual", + ), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'pina', u'PINA Documentation', - [author], 1) -] +man_pages = [(master_doc, "pina", "PINA Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -238,11 +229,19 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'pina', u'PINA Documentation', - author, 'pina', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "pina", + "PINA Documentation", + author, + "pina", + "Miscellaneous", + ), ] # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" + +# Do consider meth ending with _ (needed for in-place methods of torch) +strip_signature_backslash = True diff --git a/docs/source/index.rst b/docs/source/index.rst index c84307923..fbebe0aff 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,32 +9,32 @@ Welcome to PINA’s documentation! .. grid-item:: .. image:: index_files/tutorial_13_3.png - :target: _rst/tutorials/tutorial2/tutorial.html + :target: tutorial2/tutorial.html .. grid-item:: .. image:: index_files/tutorial_32_0.png - :target: _rst/tutorials/tutorial4/tutorial.html + :target: tutorial4/tutorial.html .. grid-item:: .. image:: index_files/tutorial_13_01.png - :target: _rst/tutorials/tutorial9/tutorial.html + :target: tutorial9/tutorial.html .. grid-item:: .. image:: index_files/tutorial_36_0.png - :target: _rst/tutorials/tutorial6/tutorial.html + :target: tutorial6/tutorial.html .. grid-item:: .. image:: index_files/tutorial_15_0.png - :target: _rst/tutorials/tutorial13/tutorial.html + :target: tutorial13/tutorial.html .. grid-item:: .. image:: index_files/tutorial_5_0.png - :target: _rst/tutorials/tutorial10/tutorial.html + :target: tutorial10/tutorial.html .. grid:: 1 1 3 3 @@ -45,7 +45,7 @@ Welcome to PINA’s documentation! an open-source Python library providing an intuitive interface for solving differential equations using PINNs, NOs or both together. - Based on `PyTorch `_ and `PyTorchLightning `_, **PINA** offers a simple and intuitive way to formalize a specific (differential) problem + Based on `PyTorch `_, `PyTorchLightning `_, and `PyG `_, **PINA** offers a simple and intuitive way to formalize a specific (differential) problem and solve it using neural networks . The approximated solution of a differential equation can be implemented using PINA in a few lines of code thanks to the intuitive and user-friendly interface. @@ -63,11 +63,11 @@ Welcome to PINA’s documentation! .. toctree:: :maxdepth: 1 - Installing <_rst/_installation> - Tutorial <_rst/_tutorial> API <_rst/_code> + Tutorial <_tutorial> + Installing <_installation> Team & Foundings <_team.rst> - Contributing <_rst/_contributing> + Contributing <_contributing> License <_LICENSE.rst> Cite PINA <_cite.rst> diff --git a/docs/source/index_files/fast_mathlab.png b/docs/source/index_files/fast_mathlab.png new file mode 100644 index 000000000..cccce6512 Binary files /dev/null and b/docs/source/index_files/fast_mathlab.png differ diff --git a/docs/source/tutorials/tutorial1/tutorial.html b/docs/source/tutorials/tutorial1/tutorial.html new file mode 100644 index 000000000..7dea1b1d5 --- /dev/null +++ b/docs/source/tutorials/tutorial1/tutorial.html @@ -0,0 +1,8324 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial10/tutorial.html b/docs/source/tutorials/tutorial10/tutorial.html new file mode 100644 index 000000000..a292490b5 --- /dev/null +++ b/docs/source/tutorials/tutorial10/tutorial.html @@ -0,0 +1,8071 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial11/tutorial.html b/docs/source/tutorials/tutorial11/tutorial.html new file mode 100644 index 000000000..ecd5c1144 --- /dev/null +++ b/docs/source/tutorials/tutorial11/tutorial.html @@ -0,0 +1,8663 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial13/tutorial.html b/docs/source/tutorials/tutorial13/tutorial.html new file mode 100644 index 000000000..e16b822d0 --- /dev/null +++ b/docs/source/tutorials/tutorial13/tutorial.html @@ -0,0 +1,8149 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial14/tutorial.html b/docs/source/tutorials/tutorial14/tutorial.html new file mode 100644 index 000000000..27ee7738f --- /dev/null +++ b/docs/source/tutorials/tutorial14/tutorial.html @@ -0,0 +1,8212 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/source/tutorials/tutorial2/tutorial.html b/docs/source/tutorials/tutorial2/tutorial.html new file mode 100644 index 000000000..3d7761941 --- /dev/null +++ b/docs/source/tutorials/tutorial2/tutorial.html @@ -0,0 +1,8429 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial3/tutorial.html b/docs/source/tutorials/tutorial3/tutorial.html new file mode 100644 index 000000000..31bbedcfc --- /dev/null +++ b/docs/source/tutorials/tutorial3/tutorial.html @@ -0,0 +1,8256 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial4/tutorial.html b/docs/source/tutorials/tutorial4/tutorial.html new file mode 100644 index 000000000..5874dfb6f --- /dev/null +++ b/docs/source/tutorials/tutorial4/tutorial.html @@ -0,0 +1,9052 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial5/tutorial.html b/docs/source/tutorials/tutorial5/tutorial.html new file mode 100644 index 000000000..a77145d3a --- /dev/null +++ b/docs/source/tutorials/tutorial5/tutorial.html @@ -0,0 +1,8039 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial6/tutorial.html b/docs/source/tutorials/tutorial6/tutorial.html new file mode 100644 index 000000000..e6d8d1bc6 --- /dev/null +++ b/docs/source/tutorials/tutorial6/tutorial.html @@ -0,0 +1,8222 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/source/tutorials/tutorial7/tutorial.html b/docs/source/tutorials/tutorial7/tutorial.html new file mode 100644 index 000000000..b7b0ce317 --- /dev/null +++ b/docs/source/tutorials/tutorial7/tutorial.html @@ -0,0 +1,8090 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial8/tutorial.html b/docs/source/tutorials/tutorial8/tutorial.html new file mode 100644 index 000000000..610f0b170 --- /dev/null +++ b/docs/source/tutorials/tutorial8/tutorial.html @@ -0,0 +1,8227 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/source/tutorials/tutorial9/tutorial.html b/docs/source/tutorials/tutorial9/tutorial.html new file mode 100644 index 000000000..24a5b775b --- /dev/null +++ b/docs/source/tutorials/tutorial9/tutorial.html @@ -0,0 +1,8008 @@ + + + + + +tutorial + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + diff --git a/docs/sphinx_extensions/paramref_extension.py b/docs/sphinx_extensions/paramref_extension.py index 3b722845a..e4f939675 100644 --- a/docs/sphinx_extensions/paramref_extension.py +++ b/docs/sphinx_extensions/paramref_extension.py @@ -1,11 +1,12 @@ from docutils import nodes from docutils.parsers.rst.roles import register_local_role + def paramref_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # Simply replace :paramref: with :param: new_role = nodes.literal(text=text[1:]) return [new_role], [] -def setup(app): - register_local_role('paramref', paramref_role) +def setup(app): + register_local_role("paramref", paramref_role) diff --git a/pina/__init__.py b/pina/__init__.py index f8624144a..2cbe7f3bb 100644 --- a/pina/__init__.py +++ b/pina/__init__.py @@ -1,6 +1,4 @@ -""" -Module for the Pina library. -""" +"""Module for the Pina library.""" __all__ = [ "Trainer", diff --git a/pina/adaptive_function/__init__.py b/pina/adaptive_function/__init__.py index 093739247..d53c5f368 100644 --- a/pina/adaptive_function/__init__.py +++ b/pina/adaptive_function/__init__.py @@ -1,6 +1,4 @@ -""" -Adaptive Activation Functions Module. -""" +"""Adaptive Activation Functions Module.""" __all__ = [ "AdaptiveActivationFunctionInterface", diff --git a/pina/adaptive_function/adaptive_function.py b/pina/adaptive_function/adaptive_function.py index a88fe804e..e6f86a549 100644 --- a/pina/adaptive_function/adaptive_function.py +++ b/pina/adaptive_function/adaptive_function.py @@ -1,4 +1,4 @@ -"""Module for adaptive functions.""" +"""Module for the Adaptive Functions.""" import torch from ..utils import check_consistency diff --git a/pina/adaptive_function/adaptive_function_interface.py b/pina/adaptive_function/adaptive_function_interface.py index 365caf6f3..a655fdbd7 100644 --- a/pina/adaptive_function/adaptive_function_interface.py +++ b/pina/adaptive_function/adaptive_function_interface.py @@ -1,4 +1,4 @@ -"""Module for adaptive functions.""" +"""Module for the Adaptive Function interface.""" from abc import ABCMeta import torch @@ -7,9 +7,7 @@ class AdaptiveActivationFunctionInterface(torch.nn.Module, metaclass=ABCMeta): r""" - The - :class:`~pina.adaptive_function.adaptive_func_interface.\ - AdaptiveActivationFunctionInterface` + The :class:`AdaptiveActivationFunctionInterface` class makes a :class:`torch.nn.Module` activation function into an adaptive trainable activation function. If one wants to create an adpative activation function, this class must be use as base class. diff --git a/pina/adaptive_functions/__init__.py b/pina/adaptive_functions/__init__.py index ce00ac998..6df3338c0 100644 --- a/pina/adaptive_functions/__init__.py +++ b/pina/adaptive_functions/__init__.py @@ -1,6 +1,4 @@ -""" -Old module for adaptive functions. Deprecated in 0.2.0. -""" +"""Old module for adaptive functions. Deprecated in 0.2.0.""" import warnings diff --git a/pina/callback/__init__.py b/pina/callback/__init__.py index 4cf51479a..421071a2c 100644 --- a/pina/callback/__init__.py +++ b/pina/callback/__init__.py @@ -1,3 +1,5 @@ +"""Module for the Pina Callbacks.""" + __all__ = [ "SwitchOptimizer", "R3Refinement", diff --git a/pina/callback/adaptive_refinement_callback.py b/pina/callback/adaptive_refinement_callback.py index 9b2932f95..84ac0cfcc 100644 --- a/pina/callback/adaptive_refinement_callback.py +++ b/pina/callback/adaptive_refinement_callback.py @@ -1,4 +1,4 @@ -"""PINA Callbacks Implementations""" +"""Module for the R3Refinement callback.""" import importlib.metadata import torch diff --git a/pina/callback/linear_weight_update_callback.py b/pina/callback/linear_weight_update_callback.py index da5431bf7..8f81063f9 100644 --- a/pina/callback/linear_weight_update_callback.py +++ b/pina/callback/linear_weight_update_callback.py @@ -1,4 +1,4 @@ -"""PINA Callbacks Implementations""" +"""Module for the LinearWeightUpdate callback.""" import warnings from lightning.pytorch.callbacks import Callback @@ -18,7 +18,7 @@ def __init__( """ Callback initialization. - :param int target_epoch: The epoch at which the weight of the condition + param int target_epoch: The epoch at which the weight of the condition should reach the target value. :param str condition_name: The name of the condition whose weight should be adjusted. diff --git a/pina/callback/optimizer_callback.py b/pina/callback/optimizer_callback.py index 6b77b3d9a..fb2770a43 100644 --- a/pina/callback/optimizer_callback.py +++ b/pina/callback/optimizer_callback.py @@ -1,4 +1,4 @@ -"""PINA Callbacks Implementations""" +"""Module for the SwitchOptimizer callback.""" from lightning.pytorch.callbacks import Callback from ..optim import TorchOptimizer diff --git a/pina/callback/processing_callback.py b/pina/callback/processing_callback.py index 79c5b9c13..244c7266d 100644 --- a/pina/callback/processing_callback.py +++ b/pina/callback/processing_callback.py @@ -1,4 +1,4 @@ -"""PINA Callbacks Implementations""" +"""Module for the Processing Callbacks.""" import copy import torch diff --git a/pina/callbacks/__init__.py b/pina/callbacks/__init__.py index 76c5021ff..69f8782f6 100644 --- a/pina/callbacks/__init__.py +++ b/pina/callbacks/__init__.py @@ -1,6 +1,4 @@ -""" -Old module for callbacks. Deprecated in 0.2.0. -""" +"""Old module for callbacks. Deprecated in 0.2.0.""" import warnings diff --git a/pina/collector.py b/pina/collector.py index ab42111c1..db7296f3d 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -1,6 +1,4 @@ -""" -Module for the Collector class. -""" +"""Module for the Collector class.""" from .graph import Graph from .utils import check_consistency @@ -8,15 +6,18 @@ class Collector: """ - Collector class for collecting data from the problem. + Collector class for retrieving data from different conditions in the + problem. """ def __init__(self, problem): - """ " + """ Initialize the Collector class, by creating a hook between the collector - and the problem and initializing the data collections. + and the problem and initializing the data collections (dictionary where + data will be stored). - :param AbstractProblem problem: The problem to collect data from. + :param pina.problem.abstract_problem.AbstractProblem problem: The + problem to collect data from. """ # creating a hook between collector and problem self.problem = problem @@ -34,7 +35,12 @@ def __init__(self, problem): @property def full(self): """ - Return True if all conditions are ready. + Returns ``True`` if the collector is full. The collector is considered + full if all conditions have entries in the ``data_collection`` + dictionary. + + :return: ``True`` if all conditions are ready, ``False`` otherwise. + :rtype: bool """ return all(self._is_conditions_ready.values()) @@ -42,20 +48,20 @@ def full(self): @full.setter def full(self, value): """ - Set the full property of the collector. Admit only boolean values. + Set the ``_full`` variable. - :param bool value: The value to set the full property to. + :param bool value: The value to set the ``_full`` variable. """ + check_consistency(value, bool) self._full = value @property def data_collections(self): """ - Return the data collections, created by combining together all condition - in the problem. + Return the data collections (dictionary where data is stored). - :return: The data collections. + :return: The data collections where the data is stored. :rtype: dict """ @@ -64,20 +70,20 @@ def data_collections(self): @property def problem(self): """ - Property that return the problem connected to the collector. + Problem connected to the collector. - :return: The problem connected to the collector. - :rtype: AbstractProblem + :return: The problem from which the data is collected. + :rtype: pina.problem.abstract_problem.AbstractProblem """ return self._problem @problem.setter def problem(self, value): """ - Return the problem connected to the collector. + Set the problem connected to the collector. - return: The problem connected to the collector. - rtype: AbstractProblem + :param pina.problem.abstract_problem.AbstractProblem value: The problem + to connect to the collector. """ self._problem = value @@ -109,8 +115,11 @@ def store_fixed_data(self): def store_sample_domains(self): """ Store inside data collections the sampled data of the problem. These - comes from the conditions that require sampling. + comes from the conditions that require sampling (e.g. + :class:`~pina.condition.domain_equation_condition.\ + DomainEquationCondition`). """ + for condition_name in self.problem.conditions: condition = self.problem.conditions[condition_name] if not hasattr(condition, "domain"): diff --git a/pina/condition/__init__.py b/pina/condition/__init__.py index 36f401130..4e57811fb 100644 --- a/pina/condition/__init__.py +++ b/pina/condition/__init__.py @@ -1,6 +1,4 @@ -""" -Module for conditions. -""" +"""Module for PINA Conditions classes.""" __all__ = [ "Condition", diff --git a/pina/condition/condition.py b/pina/condition/condition.py index 53744b471..05a377eab 100644 --- a/pina/condition/condition.py +++ b/pina/condition/condition.py @@ -1,4 +1,4 @@ -"""Condition module.""" +"""Module for the Condition class.""" import warnings from .data_condition import DataCondition @@ -29,31 +29,51 @@ def warning_function(new, old): class Condition: """ - The class ``Condition`` is used to represent the constraints (physical - equations, boundary conditions, etc.) that should be satisfied in the - problem at hand. Condition objects are used to formulate the - PINA :obj:`pina.problem.abstract_problem.AbstractProblem` object. - Conditions can be specified in four ways: - - 1. By specifying the input and output points of the condition; in such a - case, the model is trained to produce the output points given the input - points. Those points can either be torch.Tensor, LabelTensors, Graph - - 2. By specifying the location and the equation of the condition; in such - a case, the model is trained to minimize the equation residual by - evaluating it at some samples of the location. - - 3. By specifying the input points and the equation of the condition; in - such a case, the model is trained to minimize the equation residual by - evaluating it at the passed input points. The input points must be - a LabelTensor. - - 4. By specifying only the data matrix; in such a case the model is - trained with an unsupervised costum loss and uses the data in training. - Additionaly conditioning variables can be passed, whenever the model - has extra conditioning variable it depends on. - - Example:: + Represents constraints (such as physical equations, boundary conditions, + etc.) that must be satisfied in a given problem. Condition objects are used + to formulate the PINA + :class:`~pina.problem.abstract_problem.AbstractProblem` object. + + There are different types of conditions: + + - :class:`~pina.condition.input_target_condition.InputTargetCondition`: + Defined by specifying both the input and the target of the condition. In + this case, the model is trained to produce the target given the input. The + input and output data must be one of the :class:`torch.Tensor`, + :class:`~pina.label_tensor.LabelTensor`, + :class:`~torch_geometric.data.Data`, or :class:`~pina.graph.Graph`. + Different implementations exist depending on the type of input and target. + For more details, see + :class:`~pina.condition.input_target_condition.InputTargetCondition`. + + - :class:`~pina.condition.domain_equation_condition.DomainEquationCondition` + : Defined by specifying both the domain and the equation of the condition. + Here, the model is trained to minimize the equation residual by evaluating + it at sampled points within the domain. + + - :class:`~pina.condition.input_equation_condition.InputEquationCondition`: + Defined by specifying the input and the equation of the condition. In this + case, the model is trained to minimize the equation residual by evaluating + it at the provided input. The input must be either a + :class:`~pina.label_tensor.LabelTensor` or a :class:`~pina.graph.Graph`. + Different implementations exist depending on the type of input. For more + details, see + :class:`~pina.condition.input_equation_condition.InputEquationCondition`. + + - :class:`~pina.condition.data_condition.DataCondition`: + Defined by specifying only the input. In this case, the model is trained + with an unsupervised custom loss while using the provided data during + training. The input data must be one of :class:`torch.Tensor`, + :class:`~pina.label_tensor.LabelTensor`, + :class:`~torch_geometric.data.Data`, or :class:`~pina.graph.Graph`. + Additionally, conditional variables can be provided when the model + depends on extra parameters. These conditional variables must be either + :class:`torch.Tensor` or :class:`~pina.label_tensor.LabelTensor`. + Different implementations exist depending on the type of input. + For more details, see + :class:`~pina.condition.data_condition.DataCondition`. + + :Example: >>> from pina import Condition >>> condition = Condition( @@ -85,6 +105,15 @@ class Condition: ) def __new__(cls, *args, **kwargs): + """ + Instantiate the appropriate Condition object based on the keyword + arguments passed. + + :raises ValueError: If no keyword arguments are passed. + :raises ValueError: If the keyword arguments are invalid. + :return: The appropriate Condition object. + :rtype: ConditionInterface + """ if len(args) != 0: raise ValueError( diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index 4d748c3d7..9869c1e0c 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -1,6 +1,4 @@ -""" -Module that defines the ConditionInterface class. -""" +"""Module for the Condition interface.""" from abc import ABCMeta from torch_geometric.data import Data @@ -11,9 +9,15 @@ class ConditionInterface(metaclass=ABCMeta): """ Abstract class which defines a common interface for all the conditions. + It defined a common interface for all the conditions. + """ def __init__(self): + """ + Initialize the ConditionInterface object. + """ + self._problem = None @property @@ -21,17 +25,46 @@ def problem(self): """ Return the problem to which the condition is associated. - :return: Problem to which the condition is associated - :rtype: pina.problem.AbstractProblem + :return: Problem to which the condition is associated. + :rtype: ~pina.problem.abstract_problem.AbstractProblem """ return self._problem @problem.setter def problem(self, value): + """ + Set the problem to which the condition is associated. + + :param pina.problem.abstract_problem.AbstractProblem value: Problem to + which the condition is associated + """ self._problem = value @staticmethod def _check_graph_list_consistency(data_list): + """ + Check the consistency of the list of Data/Graph objects. It performs + the following checks: + + 1. All elements in the list must be of the same type (either Data or + Graph). + 2. All elements in the list must have the same keys. + 3. The type of each tensor must be consistent across all elements in + the list. + 4. If the tensor is a LabelTensor, the labels must be consistent across + all elements in the list. + + :param data_list: List of Data/Graph objects to check + :type data_list: list[Data] | list[Graph] | tuple[Data] | tuple[Graph] + + :raises ValueError: If the input types are invalid. + :raises ValueError: If all elements in the list do not have the same + keys. + :raises ValueError: If the type of each tensor is not consistent across + all elements in the list. + :raises ValueError: If the labels of the LabelTensors are not consistent + across all elements in the list. + """ # If the data is a Graph or Data object, return (do not need to check # anything) diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index 156015777..4ecd0aefb 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -1,6 +1,4 @@ -""" -DataCondition class -""" +"""Module for the DataCondition class.""" import torch from torch_geometric.data import Data @@ -11,10 +9,14 @@ class DataCondition(ConditionInterface): """ - Condition for data. This condition must be used every - time a Unsupervised Loss is needed in the Solver. The conditionalvariable - can be passed as extra-input when the model learns a conditional - distribution + Condition defined by input data and conditional variables. It can be used + in unsupervised learning problems. Based on the type of the input, + different condition implementations are available: + + - :class:`TensorDataCondition`: For :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor` input data. + - :class:`GraphDataCondition`: For :class:`~pina.graph.Graph` or + :class:`~torch_geometric.data.Data` input data. """ __slots__ = ["input", "conditional_variables"] @@ -23,18 +25,23 @@ class DataCondition(ConditionInterface): def __new__(cls, input, conditional_variables=None): """ - Instanciate the correct subclass of DataCondition by checking the type - of the input data (input and conditional_variables). - - :param input: torch.Tensor or Graph/Data object containing the input - data - :type input: torch.Tensor or Graph or Data - :param conditional_variables: torch.Tensor or LabelTensor containing - the conditional variables - :type conditional_variables: torch.Tensor or LabelTensor - :return: DataCondition subclass - :rtype: TensorDataCondition or GraphDataCondition + Instantiate the appropriate subclass of :class:`DataCondition` based on + the type of ``input``. + + :param input: Input data for the condition. + :type input: torch.Tensor | LabelTensor | Graph | + Data | list[Graph] | list[Data] | tuple[Graph] | tuple[Data] + :param conditional_variables: Conditional variables for the condition. + :type conditional_variables: torch.Tensor | LabelTensor, optional + :return: Subclass of DataCondition. + :rtype: pina.condition.data_condition.TensorDataCondition | + pina.condition.data_condition.GraphDataCondition + + :raises ValueError: If input is not of type :class:`torch.Tensor`, + :class:`~pina.label_tensor.LabelTensor`, :class:`~pina.graph.Graph`, + or :class:`~torch_geometric.data.Data`. """ + if cls != DataCondition: return super().__new__(cls) if isinstance(input, (torch.Tensor, LabelTensor)): @@ -48,21 +55,26 @@ def __new__(cls, input, conditional_variables=None): raise ValueError( "Invalid input types. " - "Please provide either Data or Graph objects." + "Please provide either torch_geometric.data.Data or Graph objects." ) def __init__(self, input, conditional_variables=None): """ - Initialize the DataCondition, storing the input and conditional + Initialize the object by storing the input and conditional variables (if any). - :param input: torch.Tensor or Graph/Data object containing the input - data - :type input: torch.Tensor or Graph or Data - :param conditional_variables: torch.Tensor or LabelTensor containing - the conditional variables - :type conditional_variables: torch.Tensor or LabelTensor + :param input: Input data for the condition. + :type input: torch.Tensor | LabelTensor | Graph | Data | list[Graph] | + list[Data] | tuple[Graph] | tuple[Data] + :param conditional_variables: Conditional variables for the condition. + :type conditional_variables: torch.Tensor | LabelTensor + + .. note:: + If ``input`` consists of a list of :class:`~pina.graph.Graph` or + :class:`~torch_geometric.data.Data`, all elements must have the same + structure (keys and data types) """ + super().__init__() self.input = input self.conditional_variables = conditional_variables @@ -70,11 +82,13 @@ def __init__(self, input, conditional_variables=None): class TensorDataCondition(DataCondition): """ - DataCondition for torch.Tensor input data + DataCondition for :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor` input data """ class GraphDataCondition(DataCondition): """ - DataCondition for Graph/Data input data + DataCondition for :class:`~pina.graph.Graph` or + :class:`~torch_geometric.data.Data` input data """ diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index aad9d9f82..ee2b5074e 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -1,6 +1,4 @@ -""" -DomainEquationCondition class definition. -""" +"""Module for the DomainEquationCondition class.""" from .condition_interface import ConditionInterface from ..utils import check_consistency @@ -10,19 +8,20 @@ class DomainEquationCondition(ConditionInterface): """ - Condition for domain/equation data. This condition must be used every - time a Physics Informed Loss is needed in the Solver. + Condition defined by a domain and an equation. It can be used in Physics + Informed problems. Before using this condition, make sure that input data + are correctly sampled from the domain. """ __slots__ = ["domain", "equation"] def __init__(self, domain, equation): """ - Initialize the DomainEquationCondition, storing the domain and equation. + Initialise the object by storing the domain and equation. - :param DomainInterface domain: Domain object containing the domain data + :param DomainInterface domain: Domain object containing the domain data. :param EquationInterface equation: Equation object containing the - equation data + equation data. """ super().__init__() self.domain = domain diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index 9a267a382..a803a8815 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -1,6 +1,4 @@ -""" -Module to define InputEquationCondition class and its subclasses. -""" +"""Module for the InputEquationCondition class and its subclasses.""" from torch_geometric.data import Data from .condition_interface import ConditionInterface @@ -12,8 +10,14 @@ class InputEquationCondition(ConditionInterface): """ - Condition for input/equation data. This condition must be used every - time a Physics Informed Loss is needed in the Solver. + Condition defined by input data and an equation. This condition can be + used in a Physics Informed problems. Based on the type of the input, + different condition implementations are available: + + - :class:`InputTensorEquationCondition`: For \ + :class:`~pina.label_tensor.LabelTensor` input data. + - :class:`InputGraphEquationCondition`: For :class:`~pina.graph.Graph` \ + input data. """ __slots__ = ["input", "equation"] @@ -22,15 +26,20 @@ class InputEquationCondition(ConditionInterface): def __new__(cls, input, equation): """ - Instanciate the correct subclass of InputEquationCondition by checking - the type of the input data (only `input`). + Instantiate the appropriate subclass of :class:`InputEquationCondition` + based on the type of ``input``. - :param input: torch.Tensor or Graph/Data object containing the input - :type input: torch.Tensor or Graph or Data + :param input: Input data for the condition. + :type input: LabelTensor | Graph | list[Graph] | tuple[Graph] :param EquationInterface equation: Equation object containing the - equation function - :return: InputEquationCondition subclass - :rtype: InputTensorEquationCondition or InputGraphEquationCondition + equation function. + :return: Subclass of InputEquationCondition, based on the input type. + :rtype: pina.condition.input_equation_condition. + InputTensorEquationCondition | + pina.condition.input_equation_condition.InputGraphEquationCondition + + :raises ValueError: If input is not of type + :class:`~pina.label_tensor.LabelTensor`, :class:`~pina.graph.Graph`. """ # If the class is already a subclass, return the instance @@ -54,13 +63,20 @@ def __new__(cls, input, equation): def __init__(self, input, equation): """ - Initialize the InputEquationCondition by storing the input and equation. + Initialize the object by storing the input data and equation object. - :param input: torch.Tensor or Graph/Data object containing the input - :type input: torch.Tensor or Graph or Data + :param input: Input data for the condition. + :type input: LabelTensor | Graph | + list[Graph] | tuple[Graph] :param EquationInterface equation: Equation object containing the - equation function + equation function. + + .. note:: + If ``input`` consists of a list of :class:`~pina.graph.Graph` or + :class:`~torch_geometric.data.Data`, all elements must have the same + structure (keys and data types) """ + super().__init__() self.input = input self.equation = equation @@ -78,23 +94,29 @@ def __setattr__(self, key, value): class InputTensorEquationCondition(InputEquationCondition): """ - InputEquationCondition subclass for LabelTensor input data. + InputEquationCondition subclass for :class:`~pina.label_tensor.LabelTensor` + input data. """ class InputGraphEquationCondition(InputEquationCondition): """ - InputEquationCondition subclass for Graph input data. + InputEquationCondition subclass for :class:`~pina.graph.Graph` input data. """ @staticmethod def _check_label_tensor(input): """ - Check if the input is a LabelTensor. + Check if at least one :class:`~pina.label_tensor.LabelTensor` is present + in the :class:`~pina.graph.Graph` object. - :param input: input data - :type input: torch.Tensor or Graph or Data + :param input: Input data. + :type input: torch.Tensor | Graph | Data + + :raises ValueError: If the input data object does not contain at least + one LabelTensor. """ + # Store the fist element of the list/tuple if input is a list/tuple # it is anougth to check the first element because all elements must # have the same type and structure (already checked) diff --git a/pina/condition/input_target_condition.py b/pina/condition/input_target_condition.py index 70e09bce2..d39fb28ca 100644 --- a/pina/condition/input_target_condition.py +++ b/pina/condition/input_target_condition.py @@ -11,8 +11,21 @@ class InputTargetCondition(ConditionInterface): """ - Condition for domain/equation data. This condition must be used every - time a Physics Informed or a Supervised Loss is needed in the Solver. + Condition defined by input and target data. This condition can be used in + both supervised learning and Physics-informed problems. Based on the type of + the input and target, different condition implementations are available: + + - :class:`TensorInputTensorTargetCondition`: For :class:`torch.Tensor` or \ + :class:`~pina.label_tensor.LabelTensor` input and target data. + - :class:`TensorInputGraphTargetCondition`: For :class:`torch.Tensor` or \ + :class:`~pina.label_tensor.LabelTensor` input and \ + :class:`~pina.graph.Graph` or :class:`torch_geometric.data.Data` \ + target data. + - :class:`GraphInputTensorTargetCondition`: For :class:`~pina.graph.Graph` \ + or :class:`~torch_geometric.data.Data` input and :class:`torch.Tensor` \ + or :class:`~pina.label_tensor.LabelTensor` target data. + - :class:`GraphInputGraphTargetCondition`: For :class:`~pina.graph.Graph` \ + or :class:`~torch_geometric.data.Data` input and target data. """ __slots__ = ["input", "target"] @@ -21,17 +34,27 @@ class InputTargetCondition(ConditionInterface): def __new__(cls, input, target): """ - Instanciate the correct subclass of InputTargetCondition by checking the - type of the input and target data. - - :param input: torch.Tensor or Graph/Data object containing the input - :type input: torch.Tensor or Graph or Data - :param target: torch.Tensor or Graph/Data object containing the target - :type target: torch.Tensor or Graph or Data - :return: InputTargetCondition subclass - :rtype: TensorInputTensorTargetCondition or - TensorInputGraphTargetCondition or GraphInputTensorTargetCondition - or GraphInputGraphTargetCondition + Instantiate the appropriate subclass of InputTargetCondition based on + the types of input and target data. + + :param input: Input data for the condition. + :type input: torch.Tensor | LabelTensor | Graph | Data | list[Graph] | + list[Data] | tuple[Graph] | tuple[Data] + :param target: Target data for the condition. + :type target: torch.Tensor | LabelTensor | Graph | Data | list[Graph] | + list[Data] | tuple[Graph] | tuple[Data] + :return: Subclass of InputTargetCondition + :rtype: pina.condition.input_target_condition. + TensorInputTensorTargetCondition | + pina.condition.input_target_condition. + TensorInputGraphTargetCondition | + pina.condition.input_target_condition. + GraphInputTensorTargetCondition | + pina.condition.input_target_condition.GraphInputGraphTargetCondition + + :raises ValueError: If ``input`` and/or ``target`` are not of type + :class:`torch.Tensor`, :class:`~pina.label_tensor.LabelTensor`, + :class:`~pina.graph.Graph`, or :class:`~torch_geometric.data.Data`. """ if cls != InputTargetCondition: return super().__new__(cls) @@ -65,19 +88,28 @@ def __new__(cls, input, target): raise ValueError( "Invalid input/target types. " - "Please provide either Data, Graph, LabelTensor or torch.Tensor " - "objects." + "Please provide either torch_geometric.data.Data, Graph, " + "LabelTensor or torch.Tensor objects." ) def __init__(self, input, target): """ - Initialize the InputTargetCondition, storing the input and target data. - - :param input: torch.Tensor or Graph/Data object containing the input - :type input: torch.Tensor or Graph or Data - :param target: torch.Tensor or Graph/Data object containing the target - :type target: torch.Tensor or Graph or Data + Initialize the object by storing the ``input`` and ``target`` data. + + :param input: Input data for the condition. + :type input: torch.Tensor | LabelTensor | Graph | Data | list[Graph] | + list[Data] | tuple[Graph] | tuple[Data] + :param target: Target data for the condition. + :type target: torch.Tensor | LabelTensor | Graph | Data | list[Graph] | + list[Data] | tuple[Graph] | tuple[Data] + + .. note:: + If either input or target consists of a list of + :class:~pina.graph.Graph or :class:~torch_geometric.data.Data + objects, all elements must have the same structure (matching + keys and data types). """ + super().__init__() self._check_input_target_len(input, target) self.input = input @@ -97,25 +129,30 @@ def _check_input_target_len(input, target): class TensorInputTensorTargetCondition(InputTargetCondition): """ - InputTargetCondition subclass for torch.Tensor input and target data. + InputTargetCondition subclass for :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor` ``input`` and ``target`` data. """ class TensorInputGraphTargetCondition(InputTargetCondition): """ - InputTargetCondition subclass for torch.Tensor input and Graph/Data target + InputTargetCondition subclass for :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor` ``input`` and + :class:`~pina.graph.Graph` or :class:`~torch_geometric.data.Data` `target` data. """ class GraphInputTensorTargetCondition(InputTargetCondition): """ - InputTargetCondition subclass for Graph/Data input and torch.Tensor target - data. + InputTargetCondition subclass for :class:`~pina.graph.Graph` o + :class:`~torch_geometric.data.Data` ``input`` and :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor` ``target`` data. """ class GraphInputGraphTargetCondition(InputTargetCondition): """ - InputTargetCondition subclass for Graph/Data input and target data. + InputTargetCondition subclass for :class:`~pina.graph.Graph`/ + :class:`~torch_geometric.data.Data` ``input`` and ``target`` data. """ diff --git a/pina/data/__init__.py b/pina/data/__init__.py index 4c1418863..70e100011 100644 --- a/pina/data/__init__.py +++ b/pina/data/__init__.py @@ -1,6 +1,4 @@ -""" -Import data classes -""" +"""Module for data, data module, and dataset.""" __all__ = ["PinaDataModule", "PinaDataset"] diff --git a/pina/data/data_module.py b/pina/data/data_module.py index f68bbc70f..6f3c751a6 100644 --- a/pina/data/data_module.py +++ b/pina/data/data_module.py @@ -16,23 +16,25 @@ class DummyDataloader: - """ " - Dummy dataloader used when batch size is None. It callects all the data - in self.dataset and returns it when it is called a single batch. - """ def __init__(self, dataset): """ - param dataset: The dataset object to be processed. - :notes: - - **Distributed Environment**: - - Divides the dataset across processes using the - rank and world size. - - Fetches only the portion of data corresponding to - the current process. - - **Non-Distributed Environment**: - - Fetches the entire dataset. + Prepare a dataloader object that returns the entire dataset in a single + batch. Depending on the number of GPUs, the dataset is managed + as follows: + + - **Distributed Environment** (multiple GPUs): Divides dataset across + processes using the rank and world size. Fetches only portion of + data corresponding to the current process. + - **Non-Distributed Environment** (single GPU): Fetches the entire + dataset. + + :param PinaDataset dataset: The dataset object to be processed. + + .. note:: + This dataloader is used when the batch size is ``None``. """ + if ( torch.distributed.is_available() and torch.distributed.is_initialized() @@ -64,29 +66,75 @@ def __next__(self): class Collator: """ - Class used to collate the batch + This callable class is used to collate the data points fetched from the + dataset. The collation is performed based on the type of dataset used and + on the batching strategy. """ - def __init__(self, max_conditions_lengths, dataset=None): + def __init__( + self, max_conditions_lengths, automatic_batching, dataset=None + ): + """ + Initialize the object, setting the collate function based on whether + automatic batching is enabled or not. + + :param dict max_conditions_lengths: ``dict`` containing the maximum + number of data points to consider in a single batch for + each condition. + :param bool automatic_batching: Whether to enable automatic batching. + If ``True``, automatic PyTorch batching + is performed, which consists of extracting one element at a time + from the dataset and collating them into a batch. This is useful + when the dataset is too large to fit into memory. On the other hand, + if ``False``, the items are retrieved from the dataset all at once + avoind the overhead of collating them into a batch and reducing the + __getitem__ calls to the dataset. This is useful when the dataset + fits into memory. Avoid using automatic batching when ``batch_size`` + is large. Default is ``False``. + :param PinaDataset dataset: The dataset where the data is stored. + """ + self.max_conditions_lengths = max_conditions_lengths + # Set the collate function based on the batching strategy + # collate_pina_dataloader is used when automatic batching is disabled + # collate_torch_dataloader is used when automatic batching is enabled self.callable_function = ( - self._collate_custom_dataloader - if max_conditions_lengths is None - else (self._collate_standard_dataloader) + self._collate_torch_dataloader + if automatic_batching + else (self._collate_pina_dataloader) ) self.dataset = dataset + + # Set the function which performs the actual collation if isinstance(self.dataset, PinaTensorDataset): + # If the dataset is a PinaTensorDataset, use this collate function self._collate = self._collate_tensor_dataset else: + # If the dataset is a PinaDataset, use this collate function self._collate = self._collate_graph_dataset - def _collate_custom_dataloader(self, batch): + def _collate_pina_dataloader(self, batch): + """ + Function used to create a batch when automatic batching is disabled. + + :param list[int] batch: List of integers representing the indices of + the data points to be fetched. + :return: Dictionary containing the data points fetched from the dataset. + :rtype: dict + """ + # Call the fetch_from_idx_list method of the dataset return self.dataset.fetch_from_idx_list(batch) - def _collate_standard_dataloader(self, batch): + def _collate_torch_dataloader(self, batch): """ Function used to collate the batch + + :param list[dict] batch: List of retrieved data. + :return: Dictionary containing the data points fetched from the dataset, + collated. + :rtype: dict """ + batch_dict = {} if isinstance(batch, dict): return batch @@ -112,6 +160,19 @@ def _collate_standard_dataloader(self, batch): @staticmethod def _collate_tensor_dataset(data_list): + """ + Function used to collate the data when the dataset is a + :class:`~pina.data.dataset.PinaTensorDataset`. + + :param data_list: Elements to be collated. + :type data_list: list[torch.Tensor] | list[LabelTensor] + :return: Batch of data. + :rtype: dict + + :raises RuntimeError: If the data is not a :class:`torch.Tensor` or a + :class:`~pina.label_tensor.LabelTensor`. + """ + if isinstance(data_list[0], LabelTensor): return LabelTensor.stack(data_list) if isinstance(data_list[0], torch.Tensor): @@ -119,24 +180,59 @@ def _collate_tensor_dataset(data_list): raise RuntimeError("Data must be Tensors or LabelTensor ") def _collate_graph_dataset(self, data_list): + """ + Function used to collate data when the dataset is a + :class:`~pina.data.dataset.PinaGraphDataset`. + + :param data_list: Elememts to be collated. + :type data_list: list[Data] | list[Graph] + :return: Batch of data. + :rtype: dict + + :raises RuntimeError: If the data is not a + :class:`~torch_geometric.data.Data` or a :class:`~pina.graph.Graph`. + """ if isinstance(data_list[0], LabelTensor): return LabelTensor.cat(data_list) if isinstance(data_list[0], torch.Tensor): return torch.cat(data_list) if isinstance(data_list[0], Data): - return self.dataset.create_graph_batch(data_list) - raise RuntimeError("Data must be Tensors or LabelTensor or pyG Data") + return self.dataset.create_batch(data_list) + raise RuntimeError( + "Data must be Tensors or LabelTensor or pyG " + "torch_geometric.data.Data" + ) def __call__(self, batch): + """ + Perform the collation of data fetched from the dataset. The behavoior + of the function is set based on the batching strategy during class + initialization. + + :param batch: List of retrieved data or sampled indices. + :type batch: list[int] | list[dict] + :return: Dictionary containing colleted data fetched from the dataset. + :rtype: dict + """ + return self.callable_function(batch) class PinaSampler: """ - Class used to create the sampler instance. + This class is used to create the sampler instance based on the shuffle + parameter and the environment in which the code is running. """ def __new__(cls, dataset, shuffle): + """ + Instantiate and initialize the sampler. + + :param PinaDataset dataset: The dataset from which to sample. + :param bool shuffle: Whether to shuffle the dataset. + :return: The sampler instance. + :rtype: :class:`torch.utils.data.Sampler` + """ if ( torch.distributed.is_available() @@ -153,8 +249,9 @@ def __new__(cls, dataset, shuffle): class PinaDataModule(LightningDataModule): """ - This class extend LightningDataModule, allowing proper creation and - management of different types of Datasets defined in PINA + This class extends :class:`~lightning.pytorch.core.LightningDataModule`, + allowing proper creation and management of different types of datasets + defined in PINA. """ def __init__( @@ -171,31 +268,39 @@ def __init__( pin_memory=False, ): """ - Initialize the object, creating datasets based on the input problem. - - :param problem: The problem defining the dataset. - :type problem: AbstractProblem - :param train_size: Fraction or number of elements in the training split. - :type train_size: float - :param test_size: Fraction or number of elements in the test split. - :type test_size: float - :param val_size: Fraction or number of elements in the validation split. - :type val_size: float - :param batch_size: Batch size used for training. If None, the entire - dataset is used per batch. - :type batch_size: int or None - :param shuffle: Whether to shuffle the dataset before splitting. - :type shuffle: bool - :param repeat: Whether to repeat the dataset indefinitely. - :type repeat: bool + Initialize the object and creating datasets based on the input problem. + + :param AbstractProblem problem: The problem containing the data on which + to create the datasets and dataloaders. + :param float train_size: Fraction of elements in the training split. It + must be in the range [0, 1]. + :param float test_size: Fraction of elements in the test split. It must + be in the range [0, 1]. + :param float val_size: Fraction of elements in the validation split. It + must be in the range [0, 1]. + :param int batch_size: The batch size used for training. If ``None``, + the entire dataset is returned in a single batch. + Default is ``None``. + :param bool shuffle: Whether to shuffle the dataset before splitting. + Default ``True``. + :param bool repeat: Whether to repeat the dataset indefinitely. + Default ``False``. :param automatic_batching: Whether to enable automatic batching. - :type automatic_batching: bool - :param num_workers: Number of worker threads for data loading. - Default 0 (serial loading) - :type num_workers: int - :param pin_memory: Whether to use pinned memory for faster data - transfer to GPU. (Default False) - :type pin_memory: bool + Default ``False``. + :param int num_workers: Number of worker threads for data loading. + Default ``0`` (serial loading). + :param bool pin_memory: Whether to use pinned memory for faster data + transfer to GPU. Default ``False``. + + :raises ValueError: If at least one of the splits is negative. + :raises ValueError: If the sum of the splits is different from 1. + + .. seealso:: + For more information on multi-process data loading, see: + https://pytorch.org/docs/stable/data.html#multi-process-data-loading + + For details on memory pinning, see: + https://pytorch.org/docs/stable/data.html#memory-pinning """ super().__init__() @@ -204,6 +309,8 @@ def __init__( self.shuffle = shuffle self.repeat = repeat self.automatic_batching = automatic_batching + + # If batch size is None, num_workers has no effect if batch_size is None and num_workers != 0: warnings.warn( "Setting num_workers when batch_size is None has no effect on " @@ -212,6 +319,8 @@ def __init__( self.num_workers = 0 else: self.num_workers = num_workers + + # If batch size is None, pin_memory has no effect if batch_size is None and pin_memory: warnings.warn( "Setting pin_memory to True has no effect when " @@ -235,16 +344,22 @@ def __init__( splits_dict["train"] = train_size self.train_dataset = None else: + # Use the super method to create the train dataloader which + # raises NotImplementedError self.train_dataloader = super().train_dataloader if test_size > 0: splits_dict["test"] = test_size self.test_dataset = None else: + # Use the super method to create the train dataloader which + # raises NotImplementedError self.test_dataloader = super().test_dataloader if val_size > 0: splits_dict["val"] = val_size self.val_dataset = None else: + # Use the super method to create the train dataloader which + # raises NotImplementedError self.val_dataloader = super().val_dataloader self.collector_splits = self._create_splits(collector, splits_dict) @@ -252,7 +367,13 @@ def __init__( def setup(self, stage=None): """ - Perform the splitting of the dataset + Create the dataset objects for the given stage. + If the stage is "fit", the training and validation datasets are created. + If the stage is "test", the testing dataset is created. + + :param str stage: The stage for which to perform the dataset setup. + + :raises ValueError: If the stage is neither "fit" nor "test". """ if stage == "fit" or stage is None: self.train_dataset = PinaDatasetFactory( @@ -280,8 +401,18 @@ def setup(self, stage=None): raise ValueError("stage must be either 'fit' or 'test'.") @staticmethod - def _split_condition(condition_dict, splits_dict): - len_condition = len(condition_dict["input"]) + def _split_condition(single_condition_dict, splits_dict): + """ + Split the condition into different stages. + + :param dict single_condition_dict: The condition to be split. + :param dict splits_dict: The dictionary containing the number of + elements in each stage. + :return: A dictionary containing the split condition. + :rtype: dict + """ + + len_condition = len(single_condition_dict["input"]) lengths = [ int(len_condition * length) for length in splits_dict.values() @@ -300,7 +431,7 @@ def _split_condition(condition_dict, splits_dict): for stage, stage_len in splits_dict.items(): to_return_dict[stage] = { k: v[offset : offset + stage_len] - for k, v in condition_dict.items() + for k, v in single_condition_dict.items() if k != "equation" # Equations are NEVER dataloaded } @@ -312,7 +443,13 @@ def _split_condition(condition_dict, splits_dict): def _create_splits(self, collector, splits_dict): """ - Create the dataset objects putting data + Create the dataset objects putting data in the correct splits. + + :param Collector collector: The collector object containing the data. + :param dict splits_dict: The dictionary containing the number of + elements in each stage. + :return: The dictionary containing the dataset objects. + :rtype: dict """ # ----------- Auxiliary function ------------ @@ -348,6 +485,15 @@ def _apply_shuffle(condition_dict, len_data): return dataset_dict def _create_dataloader(self, split, dataset): + """ " + Create the dataloader for the given split. + + :param str split: The split on which to create the dataloader. + :param str dataset: The dataset to be used for the dataloader. + :return: The dataloader for the given split. + :rtype: torch.utils.data.DataLoader + """ + shuffle = self.shuffle if split == "train" else False # Suppress the warning about num_workers. # In many cases, especially for PINNs, @@ -365,10 +511,14 @@ def _create_dataloader(self, split, dataset): sampler = PinaSampler(dataset, shuffle) if self.automatic_batching: collate = Collator( - self.find_max_conditions_lengths(split), dataset=dataset + self.find_max_conditions_lengths(split), + self.automatic_batching, + dataset=dataset, ) else: - collate = Collator(None, dataset=dataset) + collate = Collator( + None, self.automatic_batching, dataset=dataset + ) return DataLoader( dataset, self.batch_size, @@ -385,13 +535,13 @@ def _create_dataloader(self, split, dataset): def find_max_conditions_lengths(self, split): """ - Define the maximum length of the conditions. + Define the maximum length for each conditions. - :param split: The splits of the dataset. - :type split: dict - :return: The maximum length of the conditions. + :param dict split: The split of the dataset. + :return: The maximum length per condition. :rtype: dict """ + max_conditions_lengths = {} for k, v in self.collector_splits[split].items(): if self.batch_size is None: @@ -406,31 +556,60 @@ def find_max_conditions_lengths(self, split): def val_dataloader(self): """ - Create the validation dataloader + Create the validation dataloader. + + :return: The validation dataloader + :rtype: torch.utils.data.DataLoader """ return self._create_dataloader("val", self.val_dataset) def train_dataloader(self): """ Create the training dataloader + + :return: The training dataloader + :rtype: torch.utils.data.DataLoader """ return self._create_dataloader("train", self.train_dataset) def test_dataloader(self): """ Create the testing dataloader + + :return: The testing dataloader + :rtype: torch.utils.data.DataLoader """ return self._create_dataloader("test", self.test_dataset) @staticmethod def _transfer_batch_to_device_dummy(batch, device, dataloader_idx): + """ + Transfer the batch to the device. This method is used when the batch + size is None: batch has already been transferred to the device. + + :param list[tuple] batch: List of tuple where the first element of the + tuple is the condition name and the second element is the data. + :param torch.device device: Device to which the batch is transferred. + :param int dataloader_idx: Index of the dataloader. + :return: The batch transferred to the device. + :rtype: list[tuple] + """ + return batch def _transfer_batch_to_device(self, batch, device, dataloader_idx): """ Transfer the batch to the device. This method is called in the training loop and is used to transfer the batch to the device. + + :param dict batch: The batch to be transferred to the device. + :param torch.device device: The device to which the batch is + transferred. + :param int dataloader_idx: The index of the dataloader. + :return: The batch transferred to the device. + :rtype: list[tuple] """ + batch = [ ( k, @@ -446,8 +625,18 @@ def _transfer_batch_to_device(self, batch, device, dataloader_idx): @staticmethod def _check_slit_sizes(train_size, test_size, val_size): """ - Check if the splits are correct + Check if the splits are correct. The splits sizes must be positive and + the sum of the splits must be 1. + + :param float train_size: The size of the training split. + :param float test_size: The size of the testing split. + :param float val_size: The size of the validation split. + + :raises ValueError: If at least one of the splits is negative. + :raises ValueError: If the sum of the splits is different + from 1. """ + if train_size < 0 or test_size < 0 or val_size < 0: raise ValueError("The splits must be positive") if abs(train_size + test_size + val_size - 1) > 1e-6: @@ -456,13 +645,17 @@ def _check_slit_sizes(train_size, test_size, val_size): @property def input(self): """ - # TODO + Return all the input points coming from all the datasets. + + :return: The input points for training. + :rtype: dict """ + to_return = {} if hasattr(self, "train_dataset") and self.train_dataset is not None: to_return["train"] = self.train_dataset.input if hasattr(self, "val_dataset") and self.val_dataset is not None: to_return["val"] = self.val_dataset.input if hasattr(self, "test_dataset") and self.test_dataset is not None: - to_return = self.test_dataset.input + to_return["test"] = self.test_dataset.input return to_return diff --git a/pina/data/dataset.py b/pina/data/dataset.py index 3174b4bb0..54c15564d 100644 --- a/pina/data/dataset.py +++ b/pina/data/dataset.py @@ -1,8 +1,6 @@ -""" -This module provide basic data management functionalities -""" +"""Module for the PINA dataset classes.""" -from abc import abstractmethod +from abc import abstractmethod, ABC from torch.utils.data import Dataset from torch_geometric.data import Data from ..graph import Graph, LabelBatch @@ -10,13 +8,35 @@ class PinaDatasetFactory: """ - Factory class for the PINA dataset. Depending on the type inside the - conditions it creates a different dataset object: - - PinaTensorDataset for torch.Tensor - - PinaGraphDataset for list of torch_geometric.data.Data objects + Factory class for the PINA dataset. + + Depending on the data type inside the conditions, it instanciate an object + belonging to the appropriate subclass of + :class:`~pina.data.dataset.PinaDataset`. The possible subclasses are: + + - :class:`~pina.data.dataset.PinaTensorDataset`, for handling \ + :class:`torch.Tensor` and :class:`~pina.label_tensor.LabelTensor` data. + - :class:`~pina.data.dataset.PinaGraphDataset`, for handling \ + :class:`~pina.graph.Graph` and :class:`~torch_geometric.data.Data` data. """ def __new__(cls, conditions_dict, **kwargs): + """ + Instantiate the appropriate subclass of + :class:`~pina.data.dataset.PinaDataset`. + + If a graph is present in the conditions, returns a + :class:`~pina.data.dataset.PinaGraphDataset`, otherwise returns a + :class:`~pina.data.dataset.PinaTensorDataset`. + + :param dict conditions_dict: Dictionary containing all the conditions + to be included in the dataset instance. + :return: A subclass of :class:`~pina.data.dataset.PinaDataset`. + :rtype: PinaTensorDataset | PinaGraphDataset + + :raises ValueError: If an empty dictionary is provided. + """ + # Check if conditions_dict is empty if len(conditions_dict) == 0: raise ValueError("No conditions provided") @@ -31,21 +51,51 @@ def __new__(cls, conditions_dict, **kwargs): @staticmethod def _is_graph_dataset(conditions_dict): + """ + Check if a graph is present in the conditions (at least one time). + + :param conditions_dict: Dictionary containing the conditions. + :type conditions_dict: dict + :return: True if a graph is present in the conditions, False otherwise. + :rtype: bool + """ + + # Iterate over the conditions dictionary for v in conditions_dict.values(): + # Iterate over the values of the current condition for cond in v.values(): - if isinstance(cond, (Data, Graph, list)): + # Check if the current value is a list of Data objects + if isinstance(cond, (Data, Graph, list, tuple)): return True return False -class PinaDataset(Dataset): +class PinaDataset(Dataset, ABC): """ - Abstract class for the PINA dataset + Abstract class for the PINA dataset which extends the PyTorch + :class:`~torch.utils.data.Dataset` class. It defines the common interface + for :class:`~pina.data.dataset.PinaTensorDataset` and + :class:`~pina.data.dataset.PinaGraphDataset` classes. """ def __init__( self, conditions_dict, max_conditions_lengths, automatic_batching ): + """ + Initialize the instance by storing the conditions dictionary, the + maximum number of items per conditions to consider, and the automatic + batching flag. + + :param dict conditions_dict: A dictionary mapping condition names to + their respective data. Each key represents a condition name, and the + corresponding value is a dictionary containing the associated data. + :param dict max_conditions_lengths: Maximum number of data points that + can be included in a single batch per condition. + :param bool automatic_batching: Indicates whether PyTorch automatic + batching is enabled in + :class:`~pina.data.data_module.PinaDataModule`. + """ + # Store the conditions dictionary self.conditions_dict = conditions_dict # Store the maximum number of conditions to consider @@ -63,7 +113,13 @@ def __init__( self._getitem_func = self._getitem_dummy def _get_max_len(self): - """""" + """ + Returns the length of the longest condition in the dataset. + + :return: Length of the longest condition in the dataset. + :rtype: int + """ + max_len = 0 for condition in self.conditions_dict.values(): max_len = max(max_len, len(condition["input"])) @@ -76,10 +132,28 @@ def __getitem__(self, idx): return self._getitem_func(idx) def _getitem_dummy(self, idx): + """ + Return the index itself. This is used when automatic batching is + disabled to postpone the data retrieval to the dataloader. + + :param int idx: Index. + :return: Index. + :rtype: int + """ + # If automatic batching is disabled, return the data at the given index return idx def _getitem_int(self, idx): + """ + Return the data at the given index in the dataset. This is used when + automatic batching is enabled. + + :param int idx: Index. + :return: A dictionary containing the data at the given index. + :rtype: dict + """ + # If automatic batching is enabled, return the data at the given index return { k: {k_data: v[k_data][idx % len(v["input"])] for k_data in v.keys()} @@ -88,23 +162,24 @@ def _getitem_int(self, idx): def get_all_data(self): """ - Return all data in the dataset + Return all data in the dataset. - :return: All data in the dataset + :return: A dictionary containing all the data in the dataset. :rtype: dict """ + index = list(range(len(self))) return self.fetch_from_idx_list(index) def fetch_from_idx_list(self, idx): """ - Return data from the dataset given a list of indices + Return data from the dataset given a list of indices. - :param idx: List of indices - :type idx: list - :return: Data from the dataset + :param list[int] idx: List of indices. + :return: A dictionary containing the data at the given indices. :rtype: dict """ + to_return_dict = {} for condition, data in self.conditions_dict.items(): # Get the indices for the current condition @@ -121,62 +196,113 @@ def fetch_from_idx_list(self, idx): @abstractmethod def _retrive_data(self, data, idx_list): - pass + """ + Abstract method to retrieve data from the dataset given a list of + indices. + """ class PinaTensorDataset(PinaDataset): """ - Class for the PINA dataset with torch.Tensor data + Dataset class for the PINA dataset with :class:`torch.Tensor` and + :class:`~pina.label_tensor.LabelTensor` data. """ # Override _retrive_data method for torch.Tensor data def _retrive_data(self, data, idx_list): + """ + Retrieve data from the dataset given a list of indices. + + :param dict data: Dictionary containing the data + (only :class:`torch.Tensor` or + :class:`~pina.label_tensor.LabelTensor`). + :param list[int] idx_list: indices to retrieve. + :return: Dictionary containing the data at the given indices. + :rtype: dict + """ + return {k: v[idx_list] for k, v in data.items()} @property def input(self): """ - Method to return input points for training. + Return the input data for the dataset. + + :return: Dictionary containing the input points. + :rtype: dict """ return {k: v["input"] for k, v in self.conditions_dict.items()} class PinaGraphDataset(PinaDataset): """ - Class for the PINA dataset with torch_geometric.data.Data data + Dataset class for the PINA dataset with :class:`~torch_geometric.data.Data` + and :class:`~pina.graph.Graph` data. """ - def _create_graph_batch_from_list(self, data): + def _create_graph_batch(self, data): + """ + Create a LabelBatch object from a list of + :class:`~torch_geometric.data.Data` objects. + + :param data: List of items to collate in a single batch. + :type data: list[Data] | list[Graph] + :return: LabelBatch object all the graph collated in a single batch + disconnected graphs. + :rtype: LabelBatch + """ batch = LabelBatch.from_data_list(data) return batch - def _create_output_batch(self, data): + def _create_tensor_batch(self, data): + """ + Reshape properly ``data`` tensor to be processed handle by the graph + based models. + + :param data: torch.Tensor object of shape ``(N, ...)`` where ``N`` is + the number of data objects. + :type data: torch.Tensor | LabelTensor + :return: Reshaped tensor object. + :rtype: torch.Tensor | LabelTensor + """ out = data.reshape(-1, *data.shape[2:]) return out - def create_graph_batch(self, data): + def create_batch(self, data): """ - Create a Batch object from a list of Data objects. + Create a Batch object from a list of :class:`~torch_geometric.data.Data` + objects. - :param data: List of Data objects - :type data: list - :return: Batch object - :rtype: Batch or PinaBatch + :param data: List of items to collate in a single batch. + :type data: list[Data] | list[Graph] + :return: Batch object. + :rtype: :class:`~torch_geometric.data.Batch` + | :class:`~pina.graph.LabelBatch` """ + if isinstance(data[0], Data): - return self._create_graph_batch_from_list(data) - return self._create_output_batch(data) + return self._create_graph_batch(data) + return self._create_tensor_batch(data) # Override _retrive_data method for graph handling def _retrive_data(self, data, idx_list): + """ + Retrieve data from the dataset given a list of indices. + + :param dict data: Dictionary containing the data. + :param list[int] idx_list: List of indices to retrieve. + :return: Dictionary containing the data at the given indices. + :rtype: dict + """ + # Return the data from the current condition # If the data is a list of Data objects, create a Batch object # If the data is a list of torch.Tensor objects, create a torch.Tensor return { k: ( - self._create_graph_batch_from_list([v[i] for i in idx_list]) + self._create_graph_batch([v[i] for i in idx_list]) if isinstance(v, list) - else self._create_output_batch(v[idx_list]) + else self._create_tensor_batch(v[idx_list]) ) for k, v in data.items() } diff --git a/pina/domain/__init__.py b/pina/domain/__init__.py index 45aade718..cf0f03b90 100644 --- a/pina/domain/__init__.py +++ b/pina/domain/__init__.py @@ -1,6 +1,4 @@ -""" -This module contains the domain classes. -""" +"""Module to create and handle domains.""" __all__ = [ "DomainInterface", diff --git a/pina/domain/cartesian.py b/pina/domain/cartesian.py index 0d96080b6..4e6f3b9b0 100644 --- a/pina/domain/cartesian.py +++ b/pina/domain/cartesian.py @@ -1,4 +1,4 @@ -"""Module for CartesianDomain class.""" +"""Module for the Cartesian Domain.""" import torch @@ -8,14 +8,19 @@ class CartesianDomain(DomainInterface): - """PINA implementation of Hypercube domain.""" + """ + Implementation of the hypercube domain. + """ def __init__(self, cartesian_dict): """ - :param cartesian_dict: A dictionary with dict-key a string representing - the input variables for the pinn, and dict-value a list with - the domain extrema. - :type cartesian_dict: dict + Initialization of the :class:`CartesianDomain` class. + + :param dict cartesian_dict: A dictionary where the keys are the + variable names and the values are the domain extrema. The domain + extrema can be either a list with two elements or a single number. + If the domain extrema is a single number, the variable is fixed to + that value. :Example: >>> spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) @@ -33,22 +38,30 @@ def __init__(self, cartesian_dict): @property def sample_modes(self): + """ + List of available sampling modes. + + :return: List of available sampling modes. + :rtype: list[str] + """ return ["random", "grid", "lh", "chebyshev", "latin"] @property def variables(self): - """Spatial variables. + """ + List of variables of the domain. - :return: Spatial variables defined in ``__init__()`` + :return: List of variables of the domain. :rtype: list[str] """ return sorted(list(self.fixed_.keys()) + list(self.range_.keys())) def update(self, new_domain): - """Adding new dimensions on the ``CartesianDomain`` + """ + Add new dimensions to an existing :class:`CartesianDomain` object. - :param CartesianDomain new_domain: A new ``CartesianDomain`` object - to merge + :param CartesianDomain new_domain: New domain to be added to an existing + :class:`CartesianDomain` object. :Example: >>> spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) @@ -63,24 +76,20 @@ def update(self, new_domain): self.range_.update(new_domain.range_) def _sample_range(self, n, mode, bounds): - """Rescale the samples to the correct bounds + """ + Rescale the samples to fit within the specified bounds. - :param n: Number of points to sample, see Note below - for reference. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. - Available modes include: random sampling, ``random``; - latin hypercube sampling, ``latin`` or ``lh``; - chebyshev sampling, ``chebyshev``; grid sampling ``grid``. - :type mode: str - :param bounds: Bounds to rescale the samples. - :type bounds: torch.Tensor + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + :param torch.Tensor bounds: Bounds of the domain. + :raises RuntimeError: Wrong bounds initialization. + :raises ValueError: Invalid sampling mode. :return: Rescaled sample points. :rtype: torch.Tensor """ dim = bounds.shape[0] if mode in ["chebyshev", "grid"] and dim != 1: - raise RuntimeError("Something wrong in Cartesian...") + raise RuntimeError("Wrong bounds initialization") if mode == "random": pts = torch.rand(size=(n, dim)) @@ -88,7 +97,6 @@ def _sample_range(self, n, mode, bounds): pts = chebyshev_roots(n).mul(0.5).add(0.5).reshape(-1, 1) elif mode == "grid": pts = torch.linspace(0, 1, n).reshape(-1, 1) - # elif mode == 'lh' or mode == 'latin': elif mode in ["lh", "latin"]: pts = torch_lhs(n, dim) else: @@ -97,36 +105,34 @@ def _sample_range(self, n, mode, bounds): return pts * (bounds[:, 1] - bounds[:, 0]) + bounds[:, 0] def sample(self, n, mode="random", variables="all"): - """Sample routine. + """ + Sampling routine. - :param n: Number of points to sample, see Note below - for reference. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. - Available modes include: random sampling, ``random``; + :param int n: Number of points to sample, see Note below for reference. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``; latin hypercube sampling, ``latin`` or ``lh``; chebyshev sampling, ``chebyshev``; grid sampling ``grid``. - :type mode: str - :param variables: pinn variable to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + :param list[str] variables: variables to be sampled. Default is ``all``. + :return: Sampled points. :rtype: LabelTensor .. note:: - The total number of points sampled in case of multiple variables - is not ``n``, and it depends on the chosen ``mode``. If ``mode`` is - 'grid' or ``chebyshev``, the points are sampled independentely - across the variables and the results crossed together, i.e. the - final number of points is ``n`` to the power of the number of - variables. If 'mode' is 'random', ``lh`` or ``latin``, the variables - are sampled all together, and the final number of points + When multiple variables are involved, the total number of sampled + points may differ from ``n``, depending on the chosen ``mode``. + If ``mode`` is ``grid`` or ``chebyshev``, points are sampled + independently for each variable and then combined, resulting in a + total number of points equal to ``n`` raised to the power of the + number of variables. If 'mode' is 'random', ``lh`` or ``latin``, + all variables are sampled together, and the total number of points + remains ``n``. .. warning:: - The extrema values of Span are always sampled only for ``grid`` - mode. + The extrema of CartesianDomain are only sampled when using the + ``grid`` mode. :Example: - >>> spatial_domain = Span({'x': [0, 1], 'y': [0, 1]}) + >>> spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) >>> spatial_domain.sample(n=4, mode='random') tensor([[0.0108, 0.7643], [0.4477, 0.8015], @@ -152,7 +158,15 @@ def sample(self, n, mode="random", variables="all"): """ def _1d_sampler(n, mode, variables): - """Sample independentely the variables and cross the results""" + """ + Sample each variable independently. + + :param int n: Number of points to sample. + :param str mode: Sampling method. + :param list[str] variables: variables to be sampled. + :return: Sampled points. + :rtype: list[LabelTensor] + """ tmp = [] for variable in variables: if variable in self.range_: @@ -181,19 +195,14 @@ def _1d_sampler(n, mode, variables): return result def _Nd_sampler(n, mode, variables): - """Sample all the variables together - - :param n: Number of points to sample. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. - Available modes include: random sampling, ``random``; - latin hypercube sampling, ``latin`` or ``lh``; - chebyshev sampling, ``chebyshev``; grid sampling ``grid``. - :type mode: str. - :param variables: pinn variable to be sampled, defaults to ``all``. - :type variables: str or list[str]. - :return: Sample points. - :rtype: list[torch.Tensor] + """ + Sample all variables together. + + :param int n: Number of points to sample. + :param str mode: Sampling method. + :param list[str] variables: variables to be sampled. + :return: Sampled points. + :rtype: list[LabelTensor] """ pairs = [(k, v) for k, v in self.range_.items() if k in variables] keys, values = map(list, zip(*pairs)) @@ -215,13 +224,12 @@ def _Nd_sampler(n, mode, variables): return result def _single_points_sample(n, variables): - """Sample a single point in one dimension. + """ + Sample a single point in one dimension. - :param n: Number of points to sample. - :type n: int - :param variables: Variables to sample from. - :type variables: list[str] - :return: Sample points. + :param int n: Number of points to sample. + :param list[str] variables: variables to be sampled. + :return: Sampled points. :rtype: list[torch.Tensor] """ tmp = [] @@ -256,14 +264,14 @@ def _single_points_sample(n, variables): raise ValueError(f"mode={mode} is not valid.") def is_inside(self, point, check_border=False): - """Check if a point is inside the ellipsoid. - - :param point: Point to be checked - :type point: LabelTensor - :param check_border: Check if the point is also on the frontier - of the hypercube, default ``False``. - :type check_border: bool - :return: Returning ``True`` if the point is inside, ``False`` otherwise. + """ + Check if a point is inside the hypercube. + + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the hypercube. Default is ``False``. + :return: ``True`` if the point is inside the domain, + ``False`` otherwise. :rtype: bool """ is_inside = [] diff --git a/pina/domain/difference_domain.py b/pina/domain/difference_domain.py index fc5056396..4ea7b5278 100644 --- a/pina/domain/difference_domain.py +++ b/pina/domain/difference_domain.py @@ -1,4 +1,4 @@ -"""Module for Difference class.""" +"""Module for the Difference Operation.""" import torch from .operation_interface import OperationInterface @@ -6,44 +6,46 @@ class Difference(OperationInterface): - """ - PINA implementation of Difference of Domains. - """ + r""" + Implementation of the difference operation between of a list of domains. - def __init__(self, geometries): - r""" - Given two sets :math:`A` and :math:`B` then the - domain difference is defined as: + Given two sets :math:`A` and :math:`B`, define the difference of the two + sets as: - .. math:: - A - B = \{x \mid x \in A \land x \not\in B\}, + .. math:: + A - B = \{x \mid x \in A \land x \not\in B\}, - with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N` - the dimension of the geometry space. + where :math:`x` is a point in :math:`\mathbb{R}^N`. + """ + + def __init__(self, geometries): + """ + Initialization of the :class:`Difference` class. - :param list geometries: A list of geometries from ``pina.geometry`` - such as ``EllipsoidDomain`` or ``CartesianDomain``. The first - geometry in the list is the geometry from which points are - sampled. The rest of the geometries are the geometries that - are excluded from the first geometry to find the difference. + :param list[DomainInterface] geometries: A list of instances of the + :class:`~pina.domain.domain_interface.DomainInterface` class on + which the difference operation is performed. The first domain in the + list serves as the base from which points are sampled, while the + remaining domains define the regions to be excluded from the base + domain to compute the difference. :Example: >>> # Create two ellipsoid domains >>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]}) >>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]}) - >>> # Create a Difference of the ellipsoid domains + >>> # Define the difference between the domains >>> difference = Difference([ellipsoid1, ellipsoid2]) """ super().__init__(geometries) def is_inside(self, point, check_border=False): """ - Check if a point is inside the ``Difference`` domain. + Check if a point is inside the resulting domain. - :param point: Point to be checked. - :type point: torch.Tensor - :param bool check_border: If ``True``, the border is considered inside. - :return: ``True`` if the point is inside the Exclusion domain, + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the domain. Default is ``False``. + :return: ``True`` if the point is inside the domain, ``False`` otherwise. :rtype: bool """ @@ -54,21 +56,21 @@ def is_inside(self, point, check_border=False): def sample(self, n, mode="random", variables="all"): """ - Sample routine for ``Difference`` domain. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. Available - modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``; + :param list[str] variables: variables to be sampled. Default is ``all``. + :raises NotImplementedError: If the sampling method is not implemented. + :return: Sampled points. :rtype: LabelTensor :Example: >>> # Create two Cartesian domains >>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]}) >>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]}) - >>> # Create a Difference of the ellipsoid domains + >>> # Define the difference between the domains >>> difference = Difference([cartesian1, cartesian2]) >>> # Sampling >>> difference.sample(n=5) diff --git a/pina/domain/domain_interface.py b/pina/domain/domain_interface.py index 2db6fe5d3..7f693e3da 100644 --- a/pina/domain/domain_interface.py +++ b/pina/domain/domain_interface.py @@ -1,12 +1,12 @@ -"""Module for the DomainInterface class.""" +"""Module for the Domain Interface.""" from abc import ABCMeta, abstractmethod class DomainInterface(metaclass=ABCMeta): """ - Abstract Location class. - Any geometry entity should inherit from this class. + Abstract base class for geometric domains. All specific domain types should + inherit from this class. """ available_sampling_modes = ["random", "grid", "lh", "chebyshev", "latin"] @@ -15,20 +15,24 @@ class DomainInterface(metaclass=ABCMeta): @abstractmethod def sample_modes(self): """ - Abstract method returing available samples modes for the Domain. + Abstract method defining sampling methods. """ @property @abstractmethod def variables(self): """ - Abstract method returing Domain variables. + Abstract method returning the domain variables. """ @sample_modes.setter def sample_modes(self, values): """ - TODO + Setter for the sample_modes property. + + :param values: Sampling modes to be set. + :type values: str | list[str] + :raises TypeError: Invalid sampling mode. """ if not isinstance(values, (list, tuple)): values = [values] @@ -43,18 +47,15 @@ def sample_modes(self, values): @abstractmethod def sample(self): """ - Abstract method for sampling a point from the location. To be - implemented in the child class. + Abstract method for the sampling routine. """ @abstractmethod def is_inside(self, point, check_border=False): """ - Abstract method for checking if a point is inside the location. To be - implemented in the child class. + Abstract method for checking if a point is inside the domain. - :param torch.Tensor point: A tensor point to be checked. - :param bool check_border: A boolean that determines whether the border - of the location is considered checked to be considered inside or - not. Defaults to ``False``. + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the domain. Default is ``False``. """ diff --git a/pina/domain/ellipsoid.py b/pina/domain/ellipsoid.py index 120cbf6b1..4b75be8e2 100644 --- a/pina/domain/ellipsoid.py +++ b/pina/domain/ellipsoid.py @@ -1,6 +1,4 @@ -""" -Module for the Ellipsoid domain. -""" +"""Module for the Ellipsoid Domain.""" import torch from .domain_interface import DomainInterface @@ -9,34 +7,36 @@ class EllipsoidDomain(DomainInterface): - """PINA implementation of Ellipsoid domain.""" + """ + Implementation of the ellipsoid domain. + """ def __init__(self, ellipsoid_dict, sample_surface=False): - """PINA implementation of Ellipsoid domain. - - :param ellipsoid_dict: A dictionary with dict-key a string representing - the input variables for the pinn, and dict-value a list with - the domain extrema. - :type ellipsoid_dict: dict - :param sample_surface: A variable for choosing sample strategies. If - ``sample_surface=True`` only samples on the ellipsoid surface - frontier are taken. If ``sample_surface=False`` only samples on - the ellipsoid interior are taken, defaults to ``False``. - :type sample_surface: bool + """ + Initialization of the :class:`EllipsoidDomain` class. + + :param dict ellipsoid_dict: A dictionary where the keys are the variable + names and the values are the domain extrema. + :param bool sample_surface: A flag to choose the sampling strategy. + If ``True``, samples are taken from the surface of the ellipsoid. + If ``False``, samples are taken from the interior of the ellipsoid. + Default is ``False``. + :raises TypeError: If the input dictionary is not correctly formatted. .. warning:: - Sampling for dimensions greater or equal to 10 could result - in a shrinking of the ellipsoid, which degrades the quality - of the samples. For dimensions higher than 10, other algorithms - for sampling should be used, such as: Dezert, Jean, and Christian - Musso. "An efficient method for generating points uniformly - distributed in hyperellipsoids." Proceedings of the Workshop on - Estimation, Tracking and Fusion: A Tribute to Yaakov Bar-Shalom. - Vol. 7. No. 8. 2001. + Sampling for dimensions greater or equal to 10 could result in a + shrinkage of the ellipsoid, which degrades the quality of the + samples. For dimensions higher than 10, see the following reference. + + .. seealso:: + **Original reference**: Dezert, Jean, and Musso, Christian. + *An efficient method for generating points uniformly distributed + in hyperellipsoids.* + Proceedings of the Workshop on Estimation, Tracking and Fusion: + A Tribute to Yaakov Bar-Shalom. 2001. :Example: >>> spatial_domain = Ellipsoid({'x':[-1, 1], 'y':[-1,1]}) - """ self.fixed_ = {} self.range_ = {} @@ -75,34 +75,41 @@ def __init__(self, ellipsoid_dict, sample_surface=False): @property def sample_modes(self): + """ + List of available sampling modes. + + :return: List of available sampling modes. + :rtype: list[str] + """ return ["random"] @property def variables(self): - """Spatial variables. + """ + List of variables of the domain. - :return: Spatial variables defined in '__init__()' + :return: List of variables of the domain. :rtype: list[str] """ return sorted(list(self.fixed_.keys()) + list(self.range_.keys())) def is_inside(self, point, check_border=False): - """Check if a point is inside the ellipsoid domain. + """ + Check if a point is inside the ellipsoid. + + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the ellipsoid. Default is ``False``. + :raises ValueError: If the labels of the point are different from those + passed in the ``__init__`` method. + :return: ``True`` if the point is inside the domain, + ``False`` otherwise. + :rtype: bool .. note:: - When ``sample_surface`` in the ``__init()__`` - is set to ``True``, then the method only checks - points on the surface, and not inside the domain. - - :param point: Point to be checked. - :type point: LabelTensor - :param check_border: Check if the point is also on the frontier - of the ellipsoid, default ``False``. - :type check_border: bool - :return: Returning True if the point is inside, ``False`` otherwise. - :rtype: bool + When ``sample_surface=True`` in the ``__init__`` method, this method + checks only those points lying on the surface of the ellipsoid. """ - # small check that point is labeltensor check_consistency(point, LabelTensor) @@ -142,15 +149,12 @@ def is_inside(self, point, check_border=False): return bool(eqn < 0) def _sample_range(self, n, mode, variables): - """Rescale the samples to the correct bounds. - - :param n: Number of points to sample in the ellipsoid. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. - Available modes include: random sampling, ``random``. - :type mode: str, optional - :param variables: Variables to be rescaled in the samples. - :type variables: torch.Tensor + """ + Rescale the samples to fit within the specified bounds. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + :param list[str] variables: variables whose samples must be rescaled. :return: Rescaled sample points. :rtype: torch.Tensor """ @@ -202,19 +206,20 @@ def _sample_range(self, n, mode, variables): return pts def sample(self, n, mode="random", variables="all"): - """Sample routine. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. - Available modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + """ + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``. + :param list[str] variables: variables to be sampled. Default is ``all``. + :raises NotImplementedError: If the sampling mode is not implemented. + :return: Sampled points. :rtype: LabelTensor :Example: - >>> elips = Ellipsoid({'x':[1, 0], 'y':1}) - >>> elips.sample(n=6) + >>> ellipsoid = Ellipsoid({'x':[1, 0], 'y':1}) + >>> ellipsoid.sample(n=6) tensor([[0.4872, 1.0000], [0.2977, 1.0000], [0.0422, 1.0000], @@ -224,19 +229,14 @@ def sample(self, n, mode="random", variables="all"): """ def _Nd_sampler(n, mode, variables): - """Sample all the variables together - - :param n: Number of points to sample. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. - Available modes include: random sampling, ``random``; - latin hypercube sampling, 'latin' or 'lh'; - chebyshev sampling, 'chebyshev'; grid sampling 'grid'. - :type mode: str, optional. - :param variables: pinn variable to be sampled, defaults to ``all``. - :type variables: str or list[str], optional. - :return: Sample points. - :rtype: list[torch.Tensor] + """ + Sample all variables together. + + :param int n: Number of points to sample. + :param str mode: Sampling method. + :param list[str] variables: variables to be sampled. + :return: Sampled points. + :rtype: list[LabelTensor] """ pairs = [(k, v) for k, v in self.range_.items() if k in variables] keys, _ = map(list, zip(*pairs)) @@ -258,13 +258,12 @@ def _Nd_sampler(n, mode, variables): return result def _single_points_sample(n, variables): - """Sample a single point in one dimension. + """ + Sample a single point in one dimension. - :param n: Number of points to sample. - :type n: int - :param variables: Variables to sample from. - :type variables: list[str] - :return: Sample points. + :param int n: Number of points to sample. + :param list[str] variables: variables to be sampled. + :return: Sampled points. :rtype: list[torch.Tensor] """ tmp = [] diff --git a/pina/domain/exclusion_domain.py b/pina/domain/exclusion_domain.py index 0d25d7378..4a61e415d 100644 --- a/pina/domain/exclusion_domain.py +++ b/pina/domain/exclusion_domain.py @@ -1,4 +1,4 @@ -"""Module for Exclusion class.""" +"""Module for the Exclusion Operation.""" import random import torch @@ -7,42 +7,44 @@ class Exclusion(OperationInterface): - """ - PINA implementation of Exclusion of Domains. - """ + r""" + Implementation of the exclusion operation between of a list of domains. - def __init__(self, geometries): - r""" - Given two sets :math:`A` and :math:`B` then the - domain difference is defined as: + Given two sets :math:`A` and :math:`B`, define the exclusion of the two + sets as: + + .. math:: + A \setminus B = \{x \mid x \in A \land x \in B \land + x \not\in(A \lor B)\}, - .. math:: - A \setminus B = \{x \mid x \in A \land x \in B \land - x \not\in(A \lor B)\}, + where :math:`x` is a point in :math:`\mathbb{R}^N`. + """ - with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N` - the dimension of the geometry space. + def __init__(self, geometries): + """ + Initialization of the :class:`Exclusion` class. - :param list geometries: A list of geometries from ``pina.geometry`` - such as ``EllipsoidDomain`` or ``CartesianDomain``. + :param list[DomainInterface] geometries: A list of instances of the + :class:`~pina.domain.domain_interface.DomainInterface` class on + which the exclusion operation is performed. :Example: >>> # Create two ellipsoid domains >>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]}) >>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]}) - >>> # Create a Exclusion of the ellipsoid domains + >>> # Define the exclusion between the domains >>> exclusion = Exclusion([ellipsoid1, ellipsoid2]) """ super().__init__(geometries) def is_inside(self, point, check_border=False): """ - Check if a point is inside the ``Exclusion`` domain. + Check if a point is inside the resulting domain. - :param point: Point to be checked. - :type point: torch.Tensor - :param bool check_border: If ``True``, the border is considered inside. - :return: ``True`` if the point is inside the Exclusion domain, + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the domain. Default is ``False``. + :return: ``True`` if the point is inside the domain, ``False`` otherwise. :rtype: bool """ @@ -54,21 +56,21 @@ def is_inside(self, point, check_border=False): def sample(self, n, mode="random", variables="all"): """ - Sample routine for ``Exclusion`` domain. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. Available - modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``; + :param list[str] variables: variables to be sampled. Default is ``all``. + :raises NotImplementedError: If the sampling method is not implemented. + :return: Sampled points. :rtype: LabelTensor :Example: >>> # Create two Cartesian domains >>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]}) >>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]}) - >>> # Create a Exclusion of the ellipsoid domains + >>> # Define the exclusion between the domains >>> Exclusion = Exclusion([cartesian1, cartesian2]) >>> # Sample >>> Exclusion.sample(n=5) @@ -79,7 +81,6 @@ def sample(self, n, mode="random", variables="all"): [0.1978, 0.3526]]) >>> len(Exclusion.sample(n=5) 5 - """ if mode not in self.sample_modes: raise NotImplementedError( diff --git a/pina/domain/intersection_domain.py b/pina/domain/intersection_domain.py index 69388b002..0921ff381 100644 --- a/pina/domain/intersection_domain.py +++ b/pina/domain/intersection_domain.py @@ -1,4 +1,4 @@ -"""Module for Intersection class.""" +"""Module for the Intersection Operation.""" import random import torch @@ -7,44 +7,44 @@ class Intersection(OperationInterface): - """ - PINA implementation of Intersection of Domains. - """ + r""" + Implementation of the intersection operation between of a list of domains. - def __init__(self, geometries): - r""" - Given two sets :math:`A` and :math:`B` then the - domain difference is defined as: + Given two sets :math:`A` and :math:`B`, define the intersection of the two + sets as: + + .. math:: + A \cap B = \{x \mid x \in A \land x \in B\}, - .. math:: - A \cap B = \{x \mid x \in A \land x \in B\}, + where :math:`x` is a point in :math:`\mathbb{R}^N`. + """ - with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N` - the dimension of the geometry space. + def __init__(self, geometries): + """ + Initialization of the :class:`Intersection` class. - :param list geometries: A list of geometries from ``pina.geometry`` - such as ``EllipsoidDomain`` or ``CartesianDomain``. The intersection - will be taken between all the geometries in the list. The resulting - geometry will be the intersection of all the geometries in the list. + :param list[DomainInterface] geometries: A list of instances of the + :class:`~pina.domain.domain_interface.DomainInterface` class on + which the intersection operation is performed. :Example: >>> # Create two ellipsoid domains >>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]}) >>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]}) - >>> # Create a Intersection of the ellipsoid domains + >>> # Define the intersection of the domains >>> intersection = Intersection([ellipsoid1, ellipsoid2]) """ super().__init__(geometries) def is_inside(self, point, check_border=False): """ - Check if a point is inside the ``Intersection`` domain. + Check if a point is inside the resulting domain. - :param point: Point to be checked. - :type point: torch.Tensor - :param bool check_border: If ``True``, the border is considered inside. - :return: ``True`` if the point is inside the Intersection domain, - ``False`` otherwise. + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the domain. Default is ``False``. + :return: ``True`` if the point is inside the domain, + ``False`` otherwise. :rtype: bool """ flag = 0 @@ -55,21 +55,21 @@ def is_inside(self, point, check_border=False): def sample(self, n, mode="random", variables="all"): """ - Sample routine for ``Intersection`` domain. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. Available - modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``; + :param list[str] variables: variables to be sampled. Default is ``all``. + :raises NotImplementedError: If the sampling method is not implemented. + :return: Sampled points. :rtype: LabelTensor :Example: >>> # Create two Cartesian domains >>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]}) >>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]}) - >>> # Create a Intersection of the ellipsoid domains + >>> # Define the intersection of the domains >>> intersection = Intersection([cartesian1, cartesian2]) >>> # Sample >>> intersection.sample(n=5) @@ -80,7 +80,6 @@ def sample(self, n, mode="random", variables="all"): [1.9902, 1.4458]]) >>> len(intersection.sample(n=5) 5 - """ if mode not in self.sample_modes: raise NotImplementedError( diff --git a/pina/domain/operation_interface.py b/pina/domain/operation_interface.py index 5a5c3c169..8cce9698a 100644 --- a/pina/domain/operation_interface.py +++ b/pina/domain/operation_interface.py @@ -1,4 +1,4 @@ -"""Module for OperationInterface class.""" +"""Module for the Operation Interface.""" from abc import ABCMeta, abstractmethod from .domain_interface import DomainInterface @@ -7,15 +7,16 @@ class OperationInterface(DomainInterface, metaclass=ABCMeta): """ - Abstract class for set domains operations. + Abstract class for set operations defined on geometric domains. """ def __init__(self, geometries): """ - Any geometry operation entity must inherit from this class. + Initialization of the :class:`OperationInterface` class. - :param list geometries: A list of geometries from ``pina.geometry`` - such as ``EllipsoidDomain`` or ``CartesianDomain``. + :param list[DomainInterface] geometries: A list of instances of the + :class:`~pina.domain.domain_interface.DomainInterface` class on + which the set operation is performed. """ # check consistency geometries check_consistency(geometries, DomainInterface) @@ -29,21 +30,30 @@ def __init__(self, geometries): @property def sample_modes(self): + """ + List of available sampling modes. + + :return: List of available sampling modes. + :rtype: list[str] + """ return ["random"] @property def geometries(self): """ - The geometries to perform set operation. + The domains on which to perform the set operation. + + :return: The domains on which to perform the set operation. + :rtype: list[DomainInterface] """ return self._geometries @property def variables(self): """ - Spatial variables of the domain. + List of variables of the domain. - :return: All the variables defined in ``__init__`` in order. + :return: List of variables of the domain. :rtype: list[str] """ variables = [] @@ -54,22 +64,24 @@ def variables(self): @abstractmethod def is_inside(self, point, check_border=False): """ - Check if a point is inside the resulting domain after - a set operation is applied. + Abstract method to check if a point lies inside the resulting domain + after performing the set operation. - :param point: Point to be checked. - :type point: torch.Tensor - :param bool check_border: If ``True``, the border is considered inside. - :return: ``True`` if the point is inside the Intersection domain, + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the resulting domain. Default is ``False``. + :return: ``True`` if the point is inside the domain, ``False`` otherwise. :rtype: bool """ def _check_dimensions(self, geometries): - """Check if the dimensions of the geometries are consistent. + """ + Check if the dimensions of the geometries are consistent. - :param geometries: Geometries to be checked. - :type geometries: list[Location] + :param list[DomainInterface] geometries: Domains to be checked. + :raises NotImplementedError: If the dimensions of the geometries are not + consistent. """ for geometry in geometries: if geometry.variables != geometries[0].variables: diff --git a/pina/domain/simplex.py b/pina/domain/simplex.py index 7c1deee6f..cc496daee 100644 --- a/pina/domain/simplex.py +++ b/pina/domain/simplex.py @@ -1,6 +1,4 @@ -""" -Module for Simplex Domain. -""" +"""Module for the Simplex Domain.""" import torch from .domain_interface import DomainInterface @@ -10,27 +8,28 @@ class SimplexDomain(DomainInterface): - """PINA implementation of a Simplex.""" + """ + Implementation of the simplex domain. + """ def __init__(self, simplex_matrix, sample_surface=False): """ - :param simplex_matrix: A matrix of LabelTensor objects representing - a vertex of the simplex (a tensor), and the coordinates of the - point (a list of labels). - - :type simplex_matrix: list[LabelTensor] - :param sample_surface: A variable for choosing sample strategies. If - ``sample_surface=True`` only samples on the Simplex surface - frontier are taken. If ``sample_surface=False``, no such criteria - is followed. - - :type sample_surface: bool + Initialization of the :class:`SimplexDomain` class. + + :param list[LabelTensor] simplex_matrix: A matrix representing the + vertices of the simplex. + :param bool sample_surface: A flag to choose the sampling strategy. + If ``True``, samples are taken only from the surface of the simplex. + If ``False``, samples are taken from the interior of the simplex. + Default is ``False``. + :raises ValueError: If the labels of the vertices don't match. + :raises ValueError: If the number of vertices is not equal to the + dimension of the simplex plus one. .. warning:: - Sampling for dimensions greater or equal to 10 could result - in a shrinking of the simplex, which degrades the quality - of the samples. For dimensions higher than 10, other algorithms - for sampling should be used. + Sampling for dimensions greater or equal to 10 could result in a + shrinkage of the simplex, which degrades the quality of the samples. + For dimensions higher than 10, use other sampling algorithms. :Example: >>> spatial_domain = SimplexDomain( @@ -77,18 +76,30 @@ def __init__(self, simplex_matrix, sample_surface=False): @property def sample_modes(self): + """ + List of available sampling modes. + + :return: List of available sampling modes. + :rtype: list[str] + """ return ["random"] @property def variables(self): + """ + List of variables of the domain. + + :return: List of variables of the domain. + :rtype: list[str] + """ return sorted(self._vertices_matrix.labels) def _build_cartesian(self, vertices): """ - Build Cartesian border for Simplex domain to be used in sampling. - :param vertex_matrix: matrix of vertices - :type vertices: list[list] - :return: Cartesian border for triangular domain + Build the cartesian border for a simplex domain to be used in sampling. + + :param list[LabelTensor] vertices: list of vertices defining the domain. + :return: The cartesian border for the simplex domain. :rtype: CartesianDomain """ @@ -105,22 +116,17 @@ def _build_cartesian(self, vertices): def is_inside(self, point, check_border=False): """ - Check if a point is inside the simplex. - Uses the algorithm described involving barycentric coordinates: - https://en.wikipedia.org/wiki/Barycentric_coordinate_system. - - :param point: Point to be checked. - :type point: LabelTensor - :param check_border: Check if the point is also on the frontier - of the simplex, default ``False``. - :type check_border: bool - :return: Returning ``True`` if the point is inside, ``False`` otherwise. + Check if a point is inside the simplex. It uses an algorithm involving + barycentric coordinates. + + :param LabelTensor point: Point to be checked. + :param check_border: If ``True``, the border is considered inside + the simplex. Default is ``False``. + :raises ValueError: If the labels of the point are different from those + passed in the ``__init__`` method. + :return: ``True`` if the point is inside the domain, + ``False`` otherwise. :rtype: bool - - .. note:: - When ``sample_surface`` in the ``__init()__`` - is set to ``True``, then the method only checks - points on the surface, and not inside the domain. """ if not all(label in self.variables for label in point.labels): @@ -134,7 +140,6 @@ def is_inside(self, point, check_border=False): point_shift = point_shift.tensor.reshape(-1, 1) # compute barycentric coordinates - lambda_ = torch.linalg.solve( self._vectors_shifted * 1.0, point_shift * 1.0 ) @@ -151,13 +156,13 @@ def is_inside(self, point, check_border=False): def _sample_interior_randomly(self, n, variables): """ - Randomly sample points inside a simplex of arbitrary - dimension, without the boundary. - :param int n: Number of points to sample in the shape. - :param variables: pinn variable to be sampled, defaults to ``all``. - :type variables: str or list[str], optional - :return: Returns tensor of n sampled points. - :rtype: torch.Tensor + Sample at random points from the interior of the simplex. Boundaries are + excluded from this sampling routine. + + :param int n: Number of points to sample. + :param list[str] variables: variables to be sampled. + :return: Sampled points. + :rtype: list[torch.Tensor] """ # =============== For Developers ================ # @@ -182,10 +187,10 @@ def _sample_interior_randomly(self, n, variables): def _sample_boundary_randomly(self, n): """ - Randomly sample points on the boundary of a simplex - of arbitrary dimensions. - :param int n: Number of points to sample in the shape. - :return: Returns tensor of n sampled points + Sample at random points from the boundary of the simplex. + + :param int n: Number of points to sample. + :return: Sampled points. :rtype: torch.Tensor """ @@ -221,20 +226,19 @@ def _sample_boundary_randomly(self, n): def sample(self, n, mode="random", variables="all"): """ - Sample n points from Simplex domain. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. Available - modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``. + :param list[str] variables: variables to be sampled. Default is ``all``. + :raises NotImplementedError: If the sampling method is not implemented. + :return: Sampled points. :rtype: LabelTensor .. warning:: - When ``sample_surface = True`` in the initialization, all - the variables are sampled, despite passing different once - in ``variables``. + When ``sample_surface=True``, all variables are sampled, + ignoring the ``variables`` parameter. """ if variables == "all": diff --git a/pina/domain/union_domain.py b/pina/domain/union_domain.py index ecf6c63c2..5c3e96f3f 100644 --- a/pina/domain/union_domain.py +++ b/pina/domain/union_domain.py @@ -1,4 +1,4 @@ -"""Module for Union class.""" +"""Module for the Union Operation.""" import random import torch @@ -7,51 +7,52 @@ class Union(OperationInterface): - """ - Union of Domains. - """ + r""" + Implementation of the union operation between of a list of domains. - def __init__(self, geometries): - r""" - PINA implementation of Unions of Domains. - Given two sets :math:`A` and :math:`B` then the - domain difference is defined as: + Given two sets :math:`A` and :math:`B`, define the union of the two sets as: - .. math:: - A \cup B = \{x \mid x \in A \lor x \in B\}, + .. math:: + A \cup B = \{x \mid x \in A \lor x \in B\}, - with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N` - the dimension of the geometry space. + where :math:`x` is a point in :math:`\mathbb{R}^N`. + """ + + def __init__(self, geometries): + """ + Initialization of the :class:`Union` class. - :param list geometries: A list of geometries from ``pina.geometry`` - such as ``EllipsoidDomain`` or ``CartesianDomain``. + :param list[DomainInterface] geometries: A list of instances of the + :class:`~pina.domain.domain_interface.DomainInterface` class on + which the union operation is performed. :Example: >>> # Create two ellipsoid domains >>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]}) >>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]}) - >>> # Create a union of the ellipsoid domains - >>> union = GeometryUnion([ellipsoid1, ellipsoid2]) - + >>> # Define the union of the domains + >>> union = Union([ellipsoid1, ellipsoid2]) """ super().__init__(geometries) @property def sample_modes(self): + """ + List of available sampling modes. + """ self.sample_modes = list( set(geom.sample_modes for geom in self.geometries) ) def is_inside(self, point, check_border=False): """ - Check if a point is inside the ``Union`` domain. - - :param point: Point to be checked. - :type point: LabelTensor - :param check_border: Check if the point is also on the frontier - of the ellipsoid, default ``False``. - :type check_border: bool - :return: Returning ``True`` if the point is inside, ``False`` otherwise. + Check if a point is inside the resulting domain. + + :param LabelTensor point: Point to be checked. + :param bool check_border: If ``True``, the border is considered inside + the domain. Default is ``False``. + :return: ``True`` if the point is inside the domain, + ``False`` otherwise. :rtype: bool """ for geometry in self.geometries: @@ -61,21 +62,20 @@ def is_inside(self, point, check_border=False): def sample(self, n, mode="random", variables="all"): """ - Sample routine for ``Union`` domain. - - :param int n: Number of points to sample in the shape. - :param str mode: Mode for sampling, defaults to ``random``. Available - modes include: ``random``. - :param variables: Variables to be sampled, defaults to ``all``. - :type variables: str | list[str] - :return: Returns ``LabelTensor`` of n sampled points. + Sampling routine. + + :param int n: Number of points to sample. + :param str mode: Sampling method. Default is ``random``. + Available modes: random sampling, ``random``; + :param list[str] variables: variables to be sampled. Default is ``all``. + :return: Sampled points. :rtype: LabelTensor :Example: - >>> # Create two ellipsoid domains + >>> # Create two cartesian domains >>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]}) >>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]}) - >>> # Create a union of the ellipsoid domains + >>> # Define the union of the domains >>> union = Union([cartesian1, cartesian2]) >>> # Sample >>> union.sample(n=5) diff --git a/pina/equation/__init__.py b/pina/equation/__init__.py index 87168146b..07ab74239 100644 --- a/pina/equation/__init__.py +++ b/pina/equation/__init__.py @@ -1,6 +1,4 @@ -""" -Module for defining equations and system of equations. -""" +"""Module to define equations and systems of equations.""" __all__ = [ "SystemEquation", diff --git a/pina/equation/equation.py b/pina/equation/equation.py index 7306ea8ca..60b538e11 100644 --- a/pina/equation/equation.py +++ b/pina/equation/equation.py @@ -1,21 +1,23 @@ -"""Module for Equation.""" +"""Module for the Equation.""" from .equation_interface import EquationInterface class Equation(EquationInterface): """ - Equation class for specifing any equation in PINA. + Implementation of the Equation class. Every ``equation`` passed to a + :class:`~pina.condition.condition.Condition` object must be either an + instance of :class:`Equation` or + :class:`~pina.equation.system_equation.SystemEquation`. """ def __init__(self, equation): """ - Each ``equation`` passed to a ``Condition`` object - must be an ``Equation`` or ``SystemEquation``. + Initialization of the :class:`Equation` class. - :param equation: A ``torch`` callable equation to - evaluate the residual. - :type equation: Callable + :param Callable equation: A ``torch`` callable function used to compute + the residual of a mathematical equation. + :raises ValueError: If the equation is not a callable function. """ if not callable(equation): raise ValueError( @@ -27,20 +29,17 @@ def __init__(self, equation): def residual(self, input_, output_, params_=None): """ - Residual computation of the equation. - - :param LabelTensor input_: Input points to evaluate the equation. - :param LabelTensor output_: Output vectors given by a model (e.g, - a ``FeedForward`` model). - :param dict params_: Dictionary of parameters related to the inverse - problem (if any). - If the equation is not related to an ``InverseProblem``, the - parameters are initialized to ``None`` and the residual is - computed as ``equation(input_, output_)``. - Otherwise, the parameters are automatically initialized in the - ranges specified by the user. - - :return: The residual evaluation of the specified equation. + Compute the residual of the equation. + + :param LabelTensor input_: Input points where the equation is evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :param dict params_: Dictionary of unknown parameters, associated with a + :class:`~pina.problem.inverse_problem.InverseProblem` instance. + If the equation is not related to a + :class:`~pina.problem.inverse_problem.InverseProblem` instance, the + parameters must be initialized to ``None``. Default is ``None``. + :return: The computed residual of the equation. :rtype: LabelTensor """ if params_ is None: diff --git a/pina/equation/equation_factory.py b/pina/equation/equation_factory.py index cc271092f..879990ae9 100644 --- a/pina/equation/equation_factory.py +++ b/pina/equation/equation_factory.py @@ -1,4 +1,4 @@ -"""Module for defining different equations.""" +"""Module for defining various general equations.""" from .equation import Equation from ..operator import grad, div, laplacian @@ -6,24 +6,32 @@ class FixedValue(Equation): """ - Fixed Value Equation class. + Equation to enforce a fixed value. Can be used to enforce Dirichlet Boundary + conditions. """ def __init__(self, value, components=None): """ - This class can be - used to enforced a fixed value for a specific - condition, e.g. Dirichlet Boundary conditions. - - :param float value: Value to be mantained fixed. - :param list(str) components: the name of the output - variables to calculate the gradient for. It should - be a subset of the output labels. If ``None``, - all the output variables are considered. + Initialization of the :class:`FixedValue` class. + + :param float value: The fixed value to be enforced. + :param list[str] components: The name of the output variables for which + the fixed value condition is applied. It should be a subset of the + output labels. If ``None``, all output variables are considered. Default is ``None``. """ def equation(input_, output_): + """ + Definition of the equation to enforce a fixed value. + + :param LabelTensor input_: Input points where the equation is + evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :return: The computed residual of the equation. + :rtype: LabelTensor + """ if components is None: return output_ - value return output_.extract(components) - value @@ -33,27 +41,35 @@ def equation(input_, output_): class FixedGradient(Equation): """ - Fixed Gradient Equation class. + Equation to enforce a fixed gradient for a specific condition. """ def __init__(self, value, components=None, d=None): """ - This class can beused to enforced a fixed gradient for a specific - condition. - - :param float value: Value to be mantained fixed. - :param list(str) components: the name of the output - variables to calculate the gradient for. It should - be a subset of the output labels. If ``None``, - all the output variables are considered. + Initialization of the :class:`FixedGradient` class. + + :param float value: The fixed value to be enforced to the gradient. + :param list[str] components: The name of the output variables for which + the fixed gradient condition is applied. It should be a subset of + the output labels. If ``None``, all output variables are considered. + Default is ``None``. + :param list[str] d: The name of the input variables on which the + gradient is computed. It should be a subset of the input labels. + If ``None``, all the input variables are considered. Default is ``None``. - :param list(str) d: the name of the input variables on - which the gradient is calculated. d should be a subset - of the input labels. If ``None``, all the input variables - are considered. Default is ``None``. """ def equation(input_, output_): + """ + Definition of the equation to enforce a fixed gradient. + + :param LabelTensor input_: Input points where the equation is + evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :return: The computed residual of the equation. + :rtype: LabelTensor + """ return grad(output_, input_, components=components, d=d) - value super().__init__(equation) @@ -61,27 +77,34 @@ def equation(input_, output_): class FixedFlux(Equation): """ - Fixed Flux Equation class. + Equation to enforce a fixed flux, or divergence, for a specific condition. """ def __init__(self, value, components=None, d=None): """ - This class can be used to enforced a fixed flux for a specific - condition. - - :param float value: Value to be mantained fixed. - :param list(str) components: the name of the output - variables to calculate the flux for. It should - be a subset of the output labels. If ``None``, - all the output variables are considered. + Initialization of the :class:`FixedFlux` class. + + :param float value: The fixed value to be enforced to the flux. + :param list[str] components: The name of the output variables for which + the fixed flux condition is applied. It should be a subset of the + output labels. If ``None``, all output variables are considered. Default is ``None``. - :param list(str) d: the name of the input variables on - which the flux is calculated. d should be a subset - of the input labels. If ``None``, all the input variables - are considered. Default is ``None``. + :param list[str] d: The name of the input variables on which the flux + is computed. It should be a subset of the input labels. If ``None``, + all the input variables are considered. Default is ``None``. """ def equation(input_, output_): + """ + Definition of the equation to enforce a fixed flux. + + :param LabelTensor input_: Input points where the equation is + evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :return: The computed residual of the equation. + :rtype: LabelTensor + """ return div(output_, input_, components=components, d=d) - value super().__init__(equation) @@ -89,27 +112,34 @@ def equation(input_, output_): class Laplace(Equation): """ - Laplace Equation class. + Equation to enforce a null laplacian for a specific condition. """ def __init__(self, components=None, d=None): """ - This class can be - used to enforced a Laplace equation for a specific - condition (force term set to zero). - - :param list(str) components: the name of the output - variables to calculate the flux for. It should - be a subset of the output labels. If ``None``, - all the output variables are considered. + Initialization of the :class:`Laplace` class. + + :param list[str] components: The name of the output variables for which + the null laplace condition is applied. It should be a subset of the + output labels. If ``None``, all output variables are considered. + Default is ``None``. + :param list[str] d: The name of the input variables on which the + laplacian is computed. It should be a subset of the input labels. + If ``None``, all the input variables are considered. Default is ``None``. - :param list(str) d: the name of the input variables on - which the flux is calculated. d should be a subset - of the input labels. If ``None``, all the input variables - are considered. Default is ``None``. """ def equation(input_, output_): + """ + Definition of the equation to enforce a null laplacian. + + :param LabelTensor input_: Input points where the equation is + evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :return: The computed residual of the equation. + :rtype: LabelTensor + """ return laplacian(output_, input_, components=components, d=d) super().__init__(equation) diff --git a/pina/equation/equation_interface.py b/pina/equation/equation_interface.py index 6c25418b1..f1cc74754 100644 --- a/pina/equation/equation_interface.py +++ b/pina/equation/equation_interface.py @@ -1,28 +1,35 @@ -"""Module for EquationInterface class""" +"""Module for the Equation Interface.""" from abc import ABCMeta, abstractmethod class EquationInterface(metaclass=ABCMeta): """ - The abstract `AbstractProblem` class. All the class defining a PINA Problem - should be inheritied from this class. + Abstract base class for equations. - In the definition of a PINA problem, the fundamental elements are: - the output variables, the condition(s), and the domain(s) where the - conditions are applied. + Equations in PINA simplify the training process. When defining a problem, + each equation passed to a :class:`~pina.condition.condition.Condition` + object must be either an :class:`~pina.equation.equation.Equation` or a + :class:`~pina.equation.system_equation.SystemEquation` instance. + + An :class:`~pina.equation.equation.Equation` is a wrapper for a callable + function, while :class:`~pina.equation.system_equation.SystemEquation` + wraps a list of callable functions. To streamline code writing, PINA + provides a diverse set of pre-implemented equations, such as + :class:`~pina.equation.equation_factory.FixedValue`, + :class:`~pina.equation.equation_factory.FixedGradient`, and many others. """ @abstractmethod def residual(self, input_, output_, params_): """ - Residual computation of the equation. + Abstract method to compute the residual of an equation. - :param LabelTensor input_: Input points to evaluate the equation. - :param LabelTensor output_: Output vectors given by my model (e.g., - a ``FeedForward`` model). - :param dict params_: Dictionary of unknown parameters, eventually - related to an ``InverseProblem``. - :return: The residual evaluation of the specified equation. + :param LabelTensor input_: Input points where the equation is evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :param dict params_: Dictionary of unknown parameters, associated with a + :class:`~pina.problem.inverse_problem.InverseProblem` instance. + :return: The computed residual of the equation. :rtype: LabelTensor """ diff --git a/pina/equation/system_equation.py b/pina/equation/system_equation.py index 4200199b9..d51ba9408 100644 --- a/pina/equation/system_equation.py +++ b/pina/equation/system_equation.py @@ -1,4 +1,4 @@ -"""Module for SystemEquation.""" +"""Module for the System of Equation.""" import torch from .equation_interface import EquationInterface @@ -8,25 +8,26 @@ class SystemEquation(EquationInterface): """ - System of Equation class for specifing any system - of equations in PINA. + Implementation of the System of Equations. Every ``equation`` passed to a + :class:`~pina.condition.condition.Condition` object must be either a + :class:`~pina.equation.equation.Equation` or a + :class:`~pina.equation.system_equation.SystemEquation` instance. """ def __init__(self, list_equation, reduction=None): """ - Each ``equation`` passed to a ``Condition`` object - must be an ``Equation`` or ``SystemEquation``. - A ``SystemEquation`` is specified by a list of - equations. + Initialization of the :class:`SystemEquation` class. - :param Callable equation: A ``torch`` callable equation to - evaluate the residual - :param str reduction: Specifies the reduction to apply to the output: - None | ``mean`` | ``sum`` | callable. None: no reduction - will be applied, ``mean``: the output sum will be divided - by the number of elements in the output, ``sum``: the output will - be summed. *callable* is a callable function to perform reduction, - no checks guaranteed. Default: None. + :param Callable equation: A ``torch`` callable function used to compute + the residual of a mathematical equation. + :param str reduction: The reduction method to aggregate the residuals of + each equation. Available options are: ``None``, ``mean``, ``sum``, + ``callable``. + If ``None``, no reduction is applied. If ``mean``, the output sum is + divided by the number of elements in the output. If ``sum``, the + output is summed. ``callable`` is a user-defined callable function + to perform reduction, no checks guaranteed. Default is ``None``. + :raises NotImplementedError: If the reduction is not implemented. """ check_consistency([list_equation], list) @@ -49,22 +50,21 @@ def __init__(self, list_equation, reduction=None): def residual(self, input_, output_, params_=None): """ - Residual computation for the equations of the system. + Compute the residual for each equation in the system of equations and + aggregate it according to the ``reduction`` specified in the + ``__init__`` method. - :param LabelTensor input_: Input points to evaluate the system of - equations. - :param LabelTensor output_: Output vectors given by a model (e.g, - a ``FeedForward`` model). - :param dict params_: Dictionary of parameters related to the inverse - problem (if any). - If the equation is not related to an ``InverseProblem``, the - parameters are initialized to ``None`` and the residual is - computed as ``equation(input_, output_)``. - Otherwise, the parameters are automatically initialized in the - ranges specified by the user. + :param LabelTensor input_: Input points where each equation of the + system is evaluated. + :param LabelTensor output_: Output tensor, eventually produced by a + :class:`torch.nn.Module` instance. + :param dict params_: Dictionary of unknown parameters, associated with a + :class:`~pina.problem.inverse_problem.InverseProblem` instance. + If the equation is not related to a + :class:`~pina.problem.inverse_problem.InverseProblem` instance, the + parameters must be initialized to ``None``. Default is ``None``. - :return: The residual evaluation of the specified system of equations, - aggregated by the ``reduction`` defined in the ``__init__``. + :return: The aggregated residuals of the system of equations. :rtype: LabelTensor """ residual = torch.hstack( diff --git a/pina/geometry/__init__.py b/pina/geometry/__init__.py index 9ed4c0145..762820ac6 100644 --- a/pina/geometry/__init__.py +++ b/pina/geometry/__init__.py @@ -1,7 +1,4 @@ -""" -Old module for geometry related classes and functions. -Deprecated in 0.2.0. -""" +"""Old module for geometry classes and functions. Deprecated in 0.2.0.""" import warnings diff --git a/pina/graph.py b/pina/graph.py index 39d4bcfd5..1340ed69a 100644 --- a/pina/graph.py +++ b/pina/graph.py @@ -1,6 +1,4 @@ -""" -This module provides an interface to build torch_geometric.data.Data objects. -""" +"""Module to build Graph objects and perform operations on them.""" import torch from torch_geometric.data import Data, Batch @@ -11,7 +9,8 @@ class Graph(Data): """ - A class to build torch_geometric.data.Data objects. + Extends :class:`~torch_geometric.data.Data` class to include additional + checks and functionlities. """ def __new__( @@ -19,8 +18,12 @@ def __new__( **kwargs, ): """ - :param kwargs: Parameters to construct the Graph object. - :return: A new instance of the Graph class. + Create a new instance of the :class:`~pina.graph.Graph` class by + checking the consistency of the input data and storing the attributes. + + :param dict kwargs: Parameters used to initialize the + :class:`~pina.graph.Graph` object. + :return: A new instance of the :class:`~pina.graph.Graph` class. :rtype: Graph """ # create class instance @@ -42,23 +45,25 @@ def __init__( **kwargs, ): """ - Initialize the Graph object. + Initialize the object by setting the node features, edge index, + edge attributes, and positions. The edge index is preprocessed to make + the graph undirected if required. For more details, see the + :meth:`torch_geometric.data.Data` - :param x: Optional tensor of node features (N, F) where F is the number - of features per node. + :param x: Optional tensor of node features ``(N, F)`` where ``F`` is the + number of features per node. :type x: torch.Tensor, LabelTensor - :param torch.Tensor edge_index: A tensor of shape (2, E) representing - the indices of the graph's edges. - :param pos: A tensor of shape (N, D) representing the positions of N - points in D-dimensional space. + :param torch.Tensor edge_index: A tensor of shape ``(2, E)`` + representing the indices of the graph's edges. + :param pos: A tensor of shape ``(N, D)`` representing the positions of + ``N`` points in ``D``-dimensional space. :type pos: torch.Tensor | LabelTensor - :param edge_attr: Optional tensor of edge_featured (E, F') where F' is - the number of edge features + :param edge_attr: Optional tensor of edge_featured ``(E, F')`` where + ``F'`` is the number of edge features + :type edge_attr: torch.Tensor | LabelTensor :param bool undirected: Whether to make the graph undirected - :param kwargs: Additional keyword arguments passed to the - `torch_geometric.data.Data` class constructor. If the argument - is a `torch.Tensor` or `LabelTensor`, it is included in the Data - object as a graph parameter. + :param dict kwargs: Additional keyword arguments passed to the + :class:`~torch_geometric.data.Data` class constructor. """ # preprocessing self._preprocess_edge_index(edge_index, undirected) @@ -69,6 +74,11 @@ def __init__( ) def _check_type_consistency(self, **kwargs): + """ + Check the consistency of the types of the input data. + + :param dict kwargs: Attributes to be checked for consistency. + """ # default types, specified in cls.__new__, by default they are Nont # if specified in **kwargs they get override x, pos, edge_index, edge_attr = None, None, None, None @@ -93,6 +103,7 @@ def _check_pos_consistency(pos): """ Check if the position tensor is consistent. :param torch.Tensor pos: The position tensor. + :raises ValueError: If the position tensor is not consistent. """ if pos is not None: check_consistency(pos, (torch.Tensor, LabelTensor)) @@ -103,7 +114,9 @@ def _check_pos_consistency(pos): def _check_edge_index_consistency(edge_index): """ Check if the edge index is consistent. + :param torch.Tensor edge_index: The edge index tensor. + :raises ValueError: If the edge index tensor is not consistent. """ check_consistency(edge_index, (torch.Tensor, LabelTensor)) if edge_index.ndim != 2: @@ -114,10 +127,13 @@ def _check_edge_index_consistency(edge_index): @staticmethod def _check_edge_attr_consistency(edge_attr, edge_index): """ - Check if the edge attr is consistent. - :param torch.Tensor edge_attr: The edge attribute tensor. + Check if the edge attribute tensor is consistent in type and shape + with the edge index. + :param edge_attr: The edge attribute tensor. + :type edge_attr: torch.Tensor | LabelTensor :param torch.Tensor edge_index: The edge index tensor. + :raises ValueError: If the edge attribute tensor is not consistent. """ if edge_attr is not None: check_consistency(edge_attr, (torch.Tensor, LabelTensor)) @@ -134,9 +150,14 @@ def _check_edge_attr_consistency(edge_attr, edge_index): @staticmethod def _check_x_consistency(x, pos=None): """ - Check if the input tensor x is consistent with the position tensor pos. - :param torch.Tensor x: The input tensor. - :param torch.Tensor pos: The position tensor. + Check if the input tensor x is consistent with the position tensor + `pos`. + + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :param pos: The position tensor. + :type pos: torch.Tensor | LabelTensor + :raises ValueError: If the input tensor is not consistent. """ if x is not None: check_consistency(x, (torch.Tensor, LabelTensor)) @@ -145,14 +166,12 @@ def _check_x_consistency(x, pos=None): if pos is not None: if x.size(0) != pos.size(0): raise ValueError("Inconsistent number of nodes.") - if pos is not None: - if x.size(0) != pos.size(0): - raise ValueError("Inconsistent number of nodes.") @staticmethod def _preprocess_edge_index(edge_index, undirected): """ - Preprocess the edge index. + Preprocess the edge index to make the graph undirected (if required). + :param torch.Tensor edge_index: The edge index. :param bool undirected: Whether the graph is undirected. :return: The preprocessed edge index. @@ -164,10 +183,10 @@ def _preprocess_edge_index(edge_index, undirected): def extract(self, labels, attr="x"): """ - Perform extraction of labels on node features (x) + Perform extraction of labels from the attribute specified by `attr`. :param labels: Labels to extract - :type labels: list[str] | tuple[str] | str + :type labels: list[str] | tuple[str] | str | dict :return: Batch object with extraction performed on x :rtype: PinaBatch """ @@ -180,7 +199,7 @@ def extract(self, labels, attr="x"): class GraphBuilder: """ - A class that allows the simple definition of Graph instances. + A class that allows an easy definition of :class:`Graph` instances. """ def __new__( @@ -193,24 +212,28 @@ def __new__( **kwargs, ): """ - Creates a new instance of the Graph class. - - :param pos: A tensor of shape (N, D) representing the positions of N - points in D-dimensional space. - :type pos: torch.Tensor | LabelTensor - :param edge_index: A tensor of shape (2, E) representing the indices of - the graph's edges. + Compute the edge attributes and create a new instance of the + :class:`~pina.graph.Graph` class. + + :param pos: A tensor of shape ``(N, D)`` representing the positions of + ``N`` points in ``D``-dimensional space. + :type pos: torch.Tensor or LabelTensor + :param edge_index: A tensor of shape ``(2, E)`` representing the indices + of the graph's edges. :type edge_index: torch.Tensor - :param x: Optional tensor of node features (N, F) where F is the number - of features per node. - :type x: torch.Tensor, LabelTensor - :param bool edge_attr: Optional edge attributes (E, F) where F is the - number of features per edge. - :param callable custom_edge_func: A custom function to compute edge - attributes. - :param kwargs: Additional keyword arguments passed to the Graph class - constructor. - :return: A Graph instance constructed using the provided information. + :param x: Optional tensor of node features of shape ``(N, F)``, where + ``F`` is the number of features per node. + :type x: torch.Tensor | LabelTensor, optional + :param edge_attr: Optional tensor of edge attributes of shape ``(E, F)`` + , where ``F`` is the number of features per edge. + :type edge_attr: torch.Tensor, optional + :param custom_edge_func: A custom function to compute edge attributes. + If provided, overrides ``edge_attr``. + :type custom_edge_func: Callable, optional + :param kwargs: Additional keyword arguments passed to the + :class:`~pina.graph.Graph` class constructor. + :return: A :class:`~pina.graph.Graph` instance constructed using the + provided information. :rtype: Graph """ edge_attr = cls._create_edge_attr( @@ -226,6 +249,18 @@ def __new__( @staticmethod def _create_edge_attr(pos, edge_index, edge_attr, func): + """ + Create the edge attributes based on the input parameters. + + :param pos: Positions of the points. + :type pos: torch.Tensor | LabelTensor + :param torch.Tensor edge_index: Edge indices. + :param bool edge_attr: Whether to compute the edge attributes. + :param Callable func: Function to compute the edge attributes. + :raises ValueError: If ``func`` is not a function. + :return: The edge attributes. + :rtype: torch.Tensor | LabelTensor | None + """ check_consistency(edge_attr, bool) if edge_attr: if is_function(func): @@ -235,6 +270,15 @@ def _create_edge_attr(pos, edge_index, edge_attr, func): @staticmethod def _build_edge_attr(pos, edge_index): + """ + Default function to compute the edge attributes. + + :param pos: Positions of the points. + :type pos: torch.Tensor | LabelTensor + :param torch.Tensor edge_index: Edge indices. + :return: The edge attributes. + :rtype: torch.Tensor + """ return ( (pos[edge_index[0]] - pos[edge_index[1]]) .abs() @@ -244,23 +288,24 @@ def _build_edge_attr(pos, edge_index): class RadiusGraph(GraphBuilder): """ - A class to build a radius graph. + Extends the :class:`~pina.graph.GraphBuilder` class to compute + ``edge_index`` based on a radius. Each point is connected to all the points + within the radius. """ def __new__(cls, pos, radius, **kwargs): """ - Creates a new instance of the Graph class using a radius-based graph - construction. + Instantiate the :class:`~pina.graph.Graph` class by computing the + ``edge_index`` based on the radius provided. - :param pos: A tensor of shape (N, D) representing the positions of N - points in D-dimensional space. + :param pos: A tensor of shape ``(N, D)`` representing the positions of + ``N`` points in ``D``-dimensional space. :type pos: torch.Tensor | LabelTensor :param float radius: The radius within which points are connected. - :Keyword Arguments: - The additional keyword arguments to be passed to GraphBuilder - and Graph classes - :return: Graph instance containg the information passed in input and - the computed edge_index + :param dict kwargs: The additional keyword arguments to be passed to + :class:`GraphBuilder` and :class:`Graph` classes. + :return: A :class:`~pina.graph.Graph` instance with the computed + ``edge_index``. :rtype: Graph """ edge_index = cls.compute_radius_graph(pos, radius) @@ -269,15 +314,16 @@ def __new__(cls, pos, radius, **kwargs): @staticmethod def compute_radius_graph(points, radius): """ - Computes a radius-based graph for a given set of points. + Computes the ``edge_index`` based on the radius. Each point is connected + to all the points within the radius. - :param points: A tensor of shape (N, D) representing the positions of - N points in D-dimensional space. + :param points: A tensor of shape ``(N, D)`` representing the positions + of ``N`` points in ``D``-dimensional space. :type points: torch.Tensor | LabelTensor - :param float radius: The number of nearest neighbors to find for each - point. - :rtype torch.Tensor: A tensor of shape (2, E), where E is the number of - edges, representing the edge indices of the KNN graph. + :param float radius: The radius within which points are connected. + :return: A tensor of shape ``(2, E)``, with ``E`` number of edges, + representing the edge indices of the graph. + :rtype: torch.Tensor """ dist = torch.cdist(points, points, p=2) return ( @@ -289,25 +335,25 @@ def compute_radius_graph(points, radius): class KNNGraph(GraphBuilder): """ - A class to build a KNN graph. + Extends the :class:`~pina.graph.GraphBuilder` class to compute + ``edge_index`` based on a K-nearest neighbors algorithm. """ def __new__(cls, pos, neighbours, **kwargs): """ - Creates a new instance of the Graph class using k-nearest neighbors - to compute edge_index. + Instantiate the :class:`~pina.graph.Graph` class by computing the + ``edge_index`` based on the K-nearest neighbors algorithm. - :param pos: A tensor of shape (N, D) representing the positions of N - points in D-dimensional space. + :param pos: A tensor of shape ``(N, D)`` representing the positions of + ``N`` points in ``D``-dimensional space. :type pos: torch.Tensor | LabelTensor :param int neighbours: The number of nearest neighbors to consider when building the graph. - :Keyword Arguments: - The additional keyword arguments to be passed to GraphBuilder - and Graph classes + :param dict kwargs: The additional keyword arguments to be passed to + :class:`GraphBuilder` and :class:`Graph` classes. - :return: Graph instance containg the information passed in input and - the computed edge_index + :return: A :class:`~pina.graph.Graph` instance with the computed + ``edge_index``. :rtype: Graph """ @@ -315,34 +361,47 @@ def __new__(cls, pos, neighbours, **kwargs): return super().__new__(cls, pos=pos, edge_index=edge_index, **kwargs) @staticmethod - def compute_knn_graph(points, k): + def compute_knn_graph(points, neighbours): """ - Computes the edge_index based k-nearest neighbors graph algorithm + Computes the ``edge_index`` based on the K-nearest neighbors algorithm. - :param points: A tensor of shape (N, D) representing the positions of - N points in D-dimensional space. + :param points: A tensor of shape ``(N, D)`` representing the positions + of ``N`` points in ``D``-dimensional space. :type points: torch.Tensor | LabelTensor - :param int k: The number of nearest neighbors to find for each point. - :rtype torch.Tensor: A tensor of shape (2, E), where E is the number of - edges, representing the edge indices of the KNN graph. + :param int neighbours: The number of nearest neighbors to consider when + building the graph. + :return: A tensor of shape ``(2, E)``, with ``E`` number of edges, + representing the edge indices of the graph. + :rtype: torch.Tensor """ dist = torch.cdist(points, points, p=2) - knn_indices = torch.topk(dist, k=k + 1, largest=False).indices[:, 1:] - row = torch.arange(points.size(0)).repeat_interleave(k) + knn_indices = torch.topk(dist, k=neighbours + 1, largest=False).indices[ + :, 1: + ] + row = torch.arange(points.size(0)).repeat_interleave(neighbours) col = knn_indices.flatten() return torch.stack([row, col], dim=0).as_subclass(torch.Tensor) class LabelBatch(Batch): """ - Add extract function to torch_geometric Batch object + Extends the :class:`~torch_geometric.data.Batch` class to include + :class:`~pina.label_tensor.LabelTensor` objects. """ @classmethod def from_data_list(cls, data_list): """ - Create a Batch object from a list of Data objects. + Create a Batch object from a list of :class:`~torch_geometric.data.Data` + or :class:`~pina.graph.Graph` objects. + + :param data_list: List of :class:`~torch_geometric.data.Data` or + :class:`~pina.graph.Graph` objects. + :type data_list: list[Data] | list[Graph] + :return: A :class:`~torch_geometric.data.Batch` object containing + the input data. + :rtype: :class:`~torch_geometric.data.Batch` """ # Store the labels of Data/Graph objects (all data have the same labels) # If the data do not contain labels, labels is an empty dictionary, diff --git a/pina/label_tensor.py b/pina/label_tensor.py index cce141c12..3ff1e79d2 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -6,10 +6,25 @@ class LabelTensor(torch.Tensor): - """Torch tensor with a label for any column.""" + """ + Extension of the :class:`torch.Tensor` class that includes labels for + each dimension. + """ @staticmethod def __new__(cls, x, labels, *args, **kwargs): + """ + Create a new instance of the :class:`~pina.label_tensor.LabelTensor` + class. + + :param torch.Tensor x: :class:`torch.tensor` instance to be casted as a + :class:`~pina.label_tensor.LabelTensor`. + :param labels: Labels to assign to the tensor. + :type labels: str | list[str] | dict + :return: The instance of the :class:`~pina.label_tensor.LabelTensor` + class. + :rtype: LabelTensor + """ if isinstance(x, LabelTensor): return x @@ -18,23 +33,46 @@ def __new__(cls, x, labels, *args, **kwargs): @property def tensor(self): """ - Give the tensor part of the LabelTensor. + Returns the tensor part of the :class:`~pina.label_tensor.LabelTensor` + object. - :return: tensor part of the LabelTensor + :return: Tensor part of the :class:`~pina.label_tensor.LabelTensor`. :rtype: torch.Tensor """ + return self.as_subclass(Tensor) def __init__(self, x, labels): """ - Construct a `LabelTensor` by passing a dict of the labels + Initialize the :class:`~pina.label_tensor.LabelTensor` instance, by + checking the consistency of the labels and the tensor. Specifically, the + labels must match the following conditions: + + - At each dimension, the number of labels must match the size of the \ + dimension. + - At each dimension, the labels must be unique. + + The labels can be passed in the following formats: :Example: >>> from pina import LabelTensor >>> tensor = LabelTensor( >>> torch.rand((2000, 3)), - {1: {"name": "space"['a', 'b', 'c']) - + ... {1: {"name": "space", "dof": ['a', 'b', 'c']) + >>> tensor = LabelTensor( + >>> torch.rand((2000, 3)), + ... ["a", "b", "c"]) + + The keys of the dictionary are the dimension indices, and the values are + dictionaries containing the labels and the name of the dimension. If + the labels are passed as a list, these are assigned to the last + dimension. + + :param torch.Tensor x: The tensor to be casted as a + :class:`~pina.label_tensor.LabelTensor`. + :param labels: Labels to assign to the tensor. + :type labels: str | list[str] | dict + :raises ValueError: If the labels are not consistent with the tensor. """ super().__init__() if labels is not None: @@ -42,23 +80,14 @@ def __init__(self, x, labels): else: self._labels = {} - @property - def labels(self): - """Property decorator for labels - - :return: labels of self - :rtype: list - """ - if self.ndim - 1 in self._labels: - return self._labels[self.ndim - 1]["dof"] - return None - @property def full_labels(self): - """Property decorator for labels + """ + Returns the full labels of the tensor, even for the dimensions that are + not labeled. - :return: labels of self - :rtype: list + :return: The full labels of the tensor + :rtype: dict """ to_return_dict = {} shape_tensor = self.shape @@ -71,21 +100,41 @@ def full_labels(self): @property def stored_labels(self): - """Property decorator for labels + """ + Returns the labels stored inside the instance. - :return: labels of self - :rtype: list + :return: The labels stored inside the instance. + :rtype: dict """ return self._labels + @property + def labels(self): + """ + Returns the labels of the last dimension of the instance. + + :return: labels of last dimension + :rtype: list + """ + if self.ndim - 1 in self._labels: + return self._labels[self.ndim - 1]["dof"] + return None + @labels.setter def labels(self, labels): - """ " - Set properly the parameter _labels + """ + Set labels stored insider the instance by checking the type of the + input labels and handling it accordingly. The following types are + accepted: + + - **list**: The list of labels is assigned to the last dimension. + - **dict**: The dictionary of labels is assigned to the tensor. + - **str**: The string is assigned to the last dimension. :param labels: Labels to assign to the class variable _labels. - :type: labels: str | list(str) | dict + :type labels: str | list[str] | dict """ + if not hasattr(self, "_labels"): self._labels = {} if isinstance(labels, dict): @@ -98,19 +147,19 @@ def labels(self, labels): else: raise ValueError("labels must be list, dict or string.") - def _init_labels_from_dict(self, labels: dict): + def _init_labels_from_dict(self, labels): """ - Update the internal label representation according to the values + Store the internal label representation according to the values passed as input. - :param labels: The label(s) to update. - :type labels: dict + :param dict labels: The label(s) to update. :raises ValueError: If the dof list contains duplicates or the number of dof does not match the tensor shape. """ + tensor_shape = self.shape - def validate_dof(dof_list, dim_size: int): + def validate_dof(dof_list, dim_size): """Validate the 'dof' list for uniqueness and size.""" if len(dof_list) != len(set(dof_list)): raise ValueError("dof must be unique") @@ -152,11 +201,12 @@ def validate_dof(dof_list, dim_size: int): def _init_labels_from_list(self, labels): """ Given a list of dof, this method update the internal label - representation + representation by assigning the dof to the last dimension. :param labels: The label(s) to update. :type labels: list """ + # Create a dict with labels last_dim_labels = { self.ndim - 1: {"dof": labels, "name": self.ndim - 1} @@ -165,12 +215,27 @@ def _init_labels_from_list(self, labels): def extract(self, labels_to_extract): """ - Extract the subset of the original tensor by returning all the columns - corresponding to the passed ``label_to_extract``. + Extract the subset of the original tensor by returning all the positions + corresponding to the passed ``label_to_extract``. If + ``label_to_extract`` is a dictionary, the keys are the dimension names + and the values are the labels to extract. If a single label or a list + of labels is passed, the last dimension is considered. + + :Example: + >>> from pina import LabelTensor + >>> labels = {1: {'dof': ["a", "b", "c"], 'name': 'space'}} + >>> tensor = LabelTensor(torch.rand((2000, 3)), labels) + >>> tensor.extract("a") + >>> tensor.extract(["a", "b"]) + >>> tensor.extract({"space": ["a", "b"]}) :param labels_to_extract: The label(s) to extract. - :type labels_to_extract: str | list(str) | tuple(str) - :raises TypeError: Labels are not ``str``. + :type labels_to_extract: str | list[str] | tuple[str] | dict + :return: The extracted tensor with the updated labels. + :rtype: LabelTensor + + :raises TypeError: Labels are not ``str``, ``list[str]`` or ``dict`` + properly setted. :raises ValueError: Label to extract is not in the labels ``list``. """ @@ -231,8 +296,14 @@ def get_label_indices(dim_labels, labels_te): def __str__(self): """ - returns a string with the representation of the class + The string representation of the + :class:`~pina.label_tensor.LabelTensor`. + + :return: String representation of the + :class:`~pina.label_tensor.LabelTensor` instance. + :rtype: str """ + s = "" for key, value in self._labels.items(): s += f"{key}: {value}\n" @@ -243,18 +314,20 @@ def __str__(self): @staticmethod def cat(tensors, dim=0): """ - Stack a list of tensors. For example, given a tensor `a` of shape - `(n,m,dof)` and a tensor `b` of dimension `(n',m,dof)` - the resulting tensor is of shape `(n+n',m,dof)` + Concatenate a list of tensors along a specified dimension. For more + details, see :meth:`torch.cat`. + + :param list[LabelTensor] tensors: + :class:`~pina.label_tensor.LabelTensor` instances to concatenate + :param int dim: Dimensions on which you want to perform the operation + (default is 0) + :return: A new :class:`LabelTensor` instance obtained by concatenating + the input instances. - :param tensors: tensors to concatenate - :type tensors: list of LabelTensor - :param dim: dimensions on which you want to perform the operation - (default is 0) - :type dim: int :rtype: LabelTensor - :raises ValueError: either number dof or dimensions names differ + :raises ValueError: either number dof or dimensions names differ. """ + if not tensors: return [] # Handle empty list if len(tensors) == 1: @@ -295,15 +368,16 @@ def cat(tensors, dim=0): @staticmethod def stack(tensors): """ - Stacks a list of tensors along a new dimension. + Stacks a list of tensors along a new dimension. For more details, see + :meth:`torch.stack`. - :param tensors: A list of tensors to stack. All tensors must have the - same shape. - :type tensors: list of LabelTensor - :return: A new tensor obtained by stacking the input tensors, - with the updated labels. + :param list[LabelTensor] tensors: A list of tensors to stack. + All tensors must have the same shape. + :return: A new :class:`~pina.label_tensor.LabelTensor` instance obtained + by stacking the input tensors. :rtype: LabelTensor """ + # Perform stacking in torch new_tensor = torch.stack(tensors) @@ -315,17 +389,18 @@ def stack(tensors): def requires_grad_(self, mode=True): """ - Override the requires_grad_ method to update the labels in the new - tensor. + Override the :meth:`~torch.Tensor.requires_grad_` method to handle + the labels in the new tensor. + For more details, see :meth:`~torch.Tensor.requires_grad_`. - :param mode: A boolean value indicating whether the tensor should track - gradients.If `True`, the tensor will track gradients; if ` - False`, it will not. - :type mode: bool, optional (default is `True`) - :return: The tensor itself with the updated `requires_grad` state and - retained labels. + :param bool mode: A boolean value indicating whether the tensor should + track gradients.If `True`, the tensor will track gradients; + if `False`, it will not. + :return: The :class:`~pina.label_tensor.LabelTensor` itself with the + updated ``requires_grad`` state and retained labels. :rtype: LabelTensor """ + lt = super().requires_grad_(mode) lt._labels = self._labels return lt @@ -333,30 +408,39 @@ def requires_grad_(self, mode=True): @property def dtype(self): """ - Give the dtype of the tensor. + Give the ``dtype`` of the tensor. For more details, see + :meth:`torch.dtype`. - :return: dtype of the tensor + :return: The data type of the tensor. :rtype: torch.dtype """ + return super().dtype def to(self, *args, **kwargs): """ Performs Tensor dtype and/or device conversion. For more details, see :meth:`torch.Tensor.to`. + + :return: A new :class:`~pina.label_tensor.LabelTensor` instance with the + updated dtype and/or device and retained labels. + :rtype: LabelTensor """ + lt = super().to(*args, **kwargs) lt._labels = self._labels return lt def clone(self, *args, **kwargs): """ - Clone the LabelTensor. For more details, see + Clone the :class:`~pina.label_tensor.LabelTensor`. For more details, see :meth:`torch.Tensor.clone`. - :return: A copy of the tensor. + :return: A new :class:`~pina.label_tensor.LabelTensor` instance with the + same data and labels but allocated in a different memory location. :rtype: LabelTensor """ + out = LabelTensor( super().clone(*args, **kwargs), deepcopy(self._labels) ) @@ -365,22 +449,24 @@ def clone(self, *args, **kwargs): def append(self, tensor, mode="std"): """ Appends a given tensor to the current tensor along the last dimension. + This method supports two types of appending operations: - This method allows for two types of appending operations: - 1. **Standard append** ("std"): Concatenates the tensors along the - last dimension. - 2. **Cross append** ("cross"): Repeats the current tensor and the new - tensor in a cross-product manner, then concatenates them. + 1. **Standard append** ("std"): Concatenates the input tensor with the \ + current tensor along the last dimension. + 2. **Cross append** ("cross"): Creates a cross-product of the current \ + tensor and the input tensor. - :param LabelTensor tensor: The tensor to append. - :param mode: The append mode to use. Defaults to "std". + :param tensor: The tensor to append to the current tensor. + :type tensor: LabelTensor + :param mode: The append mode to use. Defaults to ``st``. :type mode: str, optional - :return: The new tensor obtained by appending the input tensor - (either 'std' or 'cross'). + :return: A new :class:`LabelTensor` instance obtained by appending the + input tensor. :rtype: LabelTensor :raises ValueError: If the mode is not "std" or "cross". """ + if mode == "std": # Call cat on last dimension new_label_tensor = LabelTensor.cat( @@ -404,30 +490,36 @@ def append(self, tensor, mode="std"): raise ValueError('mode must be either "std" or "cross"') @staticmethod - def vstack(label_tensors): + def vstack(tensors): """ - Stack tensors vertically. For more details, see - :meth:`torch.vstack`. + Stack tensors vertically. For more details, see :meth:`torch.vstack`. - :param list(LabelTensor) label_tensors: the tensors to stack. They need - to have equal labels. - :return: the stacked tensor + :param list of LabelTensor label_tensors: The + :class:`~pina.label_tensor.LabelTensor` instances to stack. They + need to have equal labels. + :return: A new :class:`~pina.label_tensor.LabelTensor` instance obtained + by stacking the input tensors vertically. :rtype: LabelTensor """ - return LabelTensor.cat(label_tensors, dim=0) + + return LabelTensor.cat(tensors, dim=0) # This method is used to update labels def _update_single_label( self, old_labels, to_update_labels, index, dim, to_update_dim ): """ - Update the labels of the tensor by selecting only the labels - :param old_labels: labels from which retrieve data - :param to_update_labels: labels to update - :param index: index of dof to retain - :param dim: label index - :return: + Update the labels of the tensor based on the index (or list of indices). + + :param dict old_labels: Labels from which retrieve data. + :param dict to_update_labels: Labels to update. + :param index: Index of dof to retain. + :type index: int | slice | list[int] | tuple[int] | torch.Tensor + :param int dim: The dimension to update. + + :raises: ValueError: If the index type is not supported. """ + old_dof = old_labels[to_update_dim]["dof"] label_name = old_labels[dim]["name"] # Handle slicing @@ -460,16 +552,22 @@ def _update_single_label( def __getitem__(self, index): """ " - Override the __getitem__ method to handle the labels of the tensor. - Perform the __getitem__ operation on the tensor and update the labels. + Override the __getitem__ method to handle the labels of the + :class:`~pina.label_tensor.LabelTensor` instance. It first performs + __getitem__ operation on the :class:`torch.Tensor` part of the instance, + then updates the labels based on the index. :param index: The index used to access the item - :type index: Union[int, str, tuple, list] - :return: A tensor-like object with updated labels. + :type index: int | str | tuple of int | list ot int | torch.Tensor + :return: A new :class:`~pina.label_tensor.LabelTensor` instance obtained + `__getitem__` operation on :class:`torch.Tensor` part of the + instance, with the updated labels. :rtype: LabelTensor + :raises KeyError: If an invalid label index is provided. :raises IndexError: If an invalid index is accessed in the tensor. """ + # Handle string index if isinstance(index, str) or ( isinstance(index, (tuple, list)) @@ -516,12 +614,11 @@ def __getitem__(self, index): def sort_labels(self, dim=None): """ - Sorts the labels along a specified dimension and returns a new tensor - with sorted labels. + Sort the labels along the specified dimension and apply. It applies the + same sorting to the tensor part of the instance. - :param dim: The dimension along which to sort the labels. If `None`, - the last dimension (`ndim - 1`) is used. - :type dim: int, optional + :param int dim: The dimension along which to sort the labels. + If ``None``, the last dimension is used. :return: A new tensor with sorted labels along the specified dimension. :rtype: LabelTensor """ @@ -543,13 +640,15 @@ def arg_sort(lst): def __deepcopy__(self, memo): """ - Creates a deep copy of the object. + Creates a deep copy of the object. For more details, see + :meth:`copy.deepcopy`. :param memo: LabelTensor object to be copied. :type memo: LabelTensor :return: A deep copy of the original LabelTensor object. :rtype: LabelTensor """ + cls = self.__class__ result = cls(deepcopy(self.tensor), deepcopy(self.stored_labels)) return result @@ -557,10 +656,10 @@ def __deepcopy__(self, memo): def permute(self, *dims): """ Permutes the dimensions of the tensor and the associated labels - accordingly. + accordingly. For more details, see :meth:`torch.Tensor.permute`. :param dims: The dimensions to permute the tensor to. - :type dims: tuple, list + :type dims: tuple[int] | list[int] :return: A new object with permuted dimensions and reordered labels. :rtype: LabelTensor """ @@ -579,11 +678,12 @@ def permute(self, *dims): def detach(self): """ Detaches the tensor from the computation graph and retains the stored - labels. + labels. For more details, see :meth:`torch.Tensor.detach`. :return: A new tensor detached from the computation graph. :rtype: LabelTensor """ + lt = super().detach() # Copy the labels to the new tensor only if present @@ -594,14 +694,16 @@ def detach(self): @staticmethod def summation(tensors): """ - Computes the summation of a list of tensors. + Computes the summation of a list of + :class:`~pina.label_tensor.LabelTensor` instances. + - :param tensors: A list of tensors to sum. All tensors must have the same - shape and labels. - :type tensors: list of LabelTensor + :param list[LabelTensor] tensors: A list of tensors to sum. All + tensors must have the same shape and labels. :return: A new `LabelTensor` containing the element-wise sum of the input tensors. :rtype: LabelTensor + :raises ValueError: If the input `tensors` list is empty. :raises RuntimeError: If the tensors have different shapes and/or mismatched labels. @@ -637,12 +739,14 @@ def summation(tensors): def reshape(self, *shape): """ Override the reshape method to update the labels of the tensor. + For more details, see :meth:`torch.Tensor.reshape`. - :param shape: The new shape of the tensor. - :type shape: tuple - :return: A tensor-like object with updated labels. + :param tuple of int shape: The new shape of the tensor. + :return: A new :class:`~pina.label_tensor.LabelTensor` instance with the + updated shape and labels. :rtype: LabelTensor """ + # As for now the reshape method is used only in the context of the # dataset, the labels are not tensor = super().reshape(*shape) diff --git a/pina/loss/__init__.py b/pina/loss/__init__.py index 178b84782..4c57f9be0 100644 --- a/pina/loss/__init__.py +++ b/pina/loss/__init__.py @@ -1,6 +1,4 @@ -""" -Module for loss functions and weighting functions. -""" +"""Module for loss functions and weighting functions.""" __all__ = [ "LossInterface", diff --git a/pina/loss/loss_interface.py b/pina/loss/loss_interface.py index 227e2a6f6..728c9f77e 100644 --- a/pina/loss/loss_interface.py +++ b/pina/loss/loss_interface.py @@ -1,4 +1,4 @@ -"""Module for Loss Interface""" +"""Module for the Loss Interface.""" from abc import ABCMeta, abstractmethod from torch.nn.modules.loss import _Loss @@ -7,45 +7,37 @@ class LossInterface(_Loss, metaclass=ABCMeta): """ - The abstract ``LossInterface`` class. All the class defining a PINA Loss - should be inheritied from this class. + Abstract base class for all losses. All classes defining a loss function + should inherit from this interface. """ def __init__(self, reduction="mean"): """ - :param str reduction: Specifies the reduction to apply to the output: - ``none`` | ``mean`` | ``sum``. When ``none``: no reduction - will be applied, ``mean``: the sum of the output will be divided - by the number of elements in the output, ``sum``: the output will - be summed. Note: ``size_average`` and ``reduce`` are in the - process of being deprecated, and in the meantime, specifying either - of those two args will override ``reduction``. Default: ``mean``. + Initialization of the :class:`LossInterface` class. + + :param str reduction: The reduction method for the loss. + Available options: ``none``, ``mean``, ``sum``. + If ``none``, no reduction is applied. If ``mean``, the sum of the + loss values is divided by the number of values. If ``sum``, the loss + values are summed. Default is ``mean``. """ super().__init__(reduction=reduction, size_average=None, reduce=None) @abstractmethod def forward(self, input, target): - """Forward method for loss function. + """ + Forward method of the loss function. :param torch.Tensor input: Input tensor from real data. :param torch.Tensor target: Model tensor output. - :return: Loss evaluation. - :rtype: torch.Tensor """ def _reduction(self, loss): - """Simple helper function to check reduction - - :param reduction: Specifies the reduction to apply to the output: - ``none`` | ``mean`` | ``sum``. When ``none``: no reduction - will be applied, ``mean``: the sum of the output will be divided - by the number of elements in the output, ``sum``: the output will - be summed. Note: ``size_average`` and ``reduce`` are in the - process of being deprecated, and in the meantime, specifying either - of those two args will override ``reduction``. Default: ``mean``. - :type reduction: str - :param loss: Loss tensor for each element. - :type loss: torch.Tensor + """ + Apply the reduction to the loss. + + :param torch.Tensor loss: The tensor containing the pointwise losses. + :raises ValueError: If the reduction method is not valid. :return: Reduced loss. :rtype: torch.Tensor """ diff --git a/pina/loss/lp_loss.py b/pina/loss/lp_loss.py index 03f447350..f535a5b6f 100644 --- a/pina/loss/lp_loss.py +++ b/pina/loss/lp_loss.py @@ -1,4 +1,4 @@ -"""Module for LpLoss class""" +"""Module for the LpLoss class.""" import torch @@ -8,26 +8,26 @@ class LpLoss(LossInterface): r""" - The Lp loss implementation class. Creates a criterion that measures - the Lp error between each element in the input :math:`x` and + Implementation of the Lp Loss. It defines a criterion to measures the + pointwise Lp error between values in the input :math:`x` and values in the target :math:`y`. - The unreduced (i.e. with ``reduction`` set to ``none``) loss can - be described as: + If ``reduction`` is set to ``none``, the loss can be written as: .. math:: \ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = \left[\sum_{i=1}^{D} \left| x_n^i - y_n^i \right|^p \right], - If ``'relative'`` is set to true: + If ``relative`` is set to ``True``, the relative Lp error is computed: .. math:: \ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = \frac{ [\sum_{i=1}^{D} | x_n^i - y_n^i|^p] } {[\sum_{i=1}^{D}|y_n^i|^p]}, - where :math:`N` is the batch size. If ``reduction`` is not ``none`` - (default ``mean``), then: + where :math:`N` is the batch size. + + If ``reduction`` is not ``none``, then: .. math:: \ell(x, y) = @@ -35,30 +35,21 @@ class LpLoss(LossInterface): \operatorname{mean}(L), & \text{if reduction} = \text{`mean';}\\ \operatorname{sum}(L), & \text{if reduction} = \text{`sum'.} \end{cases} - - :math:`x` and :math:`y` are tensors of arbitrary shapes with a total - of :math:`n` elements each. - - The sum operation still operates over all the elements, and divides by - :math:`n`. - - The division by :math:`n` can be avoided if one sets ``reduction`` to - ``sum``. """ def __init__(self, p=2, reduction="mean", relative=False): """ - :param int p: Degree of Lp norm. It specifies the type of norm to - be calculated. See `list of possible orders in torch linalg - `torch.linalg.norm `_ - for possible degrees. Default 2 (euclidean norm). - :param str reduction: Specifies the reduction to apply to the output: - ``none`` | ``mean`` | ``sum``. ``none``: no reduction - will be applied, ``mean``: the sum of the output will be divided - by the number of elements in the output, ``sum``: the output will - be summed. - :param bool relative: Specifies if relative error should be computed. + Initialization of the :class:`LpLoss` class. + + :param int p: Degree of the Lp norm. It specifies the norm to be + computed. Default is ``2`` (euclidean norm). + :param str reduction: The reduction method for the loss. + Available options: ``none``, ``mean``, ``sum``. + If ``none``, no reduction is applied. If ``mean``, the sum of the + loss values is divided by the number of values. If ``sum``, the loss + values are summed. Default is ``mean``. + :param bool relative: If ``True``, the relative error is computed. + Default is ``False``. """ super().__init__(reduction=reduction) @@ -70,7 +61,8 @@ def __init__(self, p=2, reduction="mean", relative=False): self.relative = relative def forward(self, input, target): - """Forward method for loss function. + """ + Forward method of the loss function. :param torch.Tensor input: Input tensor from real data. :param torch.Tensor target: Model tensor output. diff --git a/pina/loss/power_loss.py b/pina/loss/power_loss.py index 695ef4d32..1edbf4f86 100644 --- a/pina/loss/power_loss.py +++ b/pina/loss/power_loss.py @@ -1,4 +1,4 @@ -"""Module for PowerLoss class""" +"""Module for the PowerLoss class.""" import torch @@ -8,27 +8,27 @@ class PowerLoss(LossInterface): r""" - The PowerLoss loss implementation class. Creates a criterion that measures - the error between each element in the input :math:`x` and - target :math:`y` powered to a specific integer. + Implementation of the Power Loss. It defines a criterion to measures the + pointwise error between values in the input :math:`x` and values in the + target :math:`y`. - The unreduced (i.e. with ``reduction`` set to ``none``) loss can - be described as: + If ``reduction`` is set to ``none``, the loss can be written as: .. math:: \ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = \frac{1}{D}\left[\sum_{i=1}^{D} \left| x_n^i - y_n^i \right|^p\right], - If ``'relative'`` is set to true: + If ``relative`` is set to ``True``, the relative error is computed: .. math:: \ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = \frac{ \sum_{i=1}^{D} | x_n^i - y_n^i|^p } {\sum_{i=1}^{D}|y_n^i|^p}, - where :math:`N` is the batch size. If ``reduction`` is not ``none`` - (default ``mean``), then: + where :math:`N` is the batch size. + + If ``reduction`` is not ``none``, then: .. math:: \ell(x, y) = @@ -36,30 +36,21 @@ class PowerLoss(LossInterface): \operatorname{mean}(L), & \text{if reduction} = \text{`mean';}\\ \operatorname{sum}(L), & \text{if reduction} = \text{`sum'.} \end{cases} - - :math:`x` and :math:`y` are tensors of arbitrary shapes with a total - of :math:`n` elements each. - - The sum operation still operates over all the elements, and divides by - :math:`n`. - - The division by :math:`n` can be avoided if one sets ``reduction`` to - ``sum``. """ def __init__(self, p=2, reduction="mean", relative=False): """ - :param int p: Degree of Lp norm. It specifies the type of norm to - be calculated. See `list of possible orders in torch linalg - `_ to - see the possible degrees. Default 2 (euclidean norm). - :param str reduction: Specifies the reduction to apply to the output: - ``none`` | ``mean`` | ``sum``. When ``none``: no reduction - will be applied, ``mean``: the sum of the output will be divided - by the number of elements in the output, ``sum``: the output will - be summed. - :param bool relative: Specifies if relative error should be computed. + Initialization of the :class:`PowerLoss` class. + + :param int p: Degree of the Lp norm. It specifies the norm to be + computed. Default is ``2`` (euclidean norm). + :param str reduction: The reduction method for the loss. + Available options: ``none``, ``mean``, ``sum``. + If ``none``, no reduction is applied. If ``mean``, the sum of the + loss values is divided by the number of values. If ``sum``, the loss + values are summed. Default is ``mean``. + :param bool relative: If ``True``, the relative error is computed. + Default is ``False``. """ super().__init__(reduction=reduction) @@ -71,7 +62,8 @@ def __init__(self, p=2, reduction="mean", relative=False): self.relative = relative def forward(self, input, target): - """Forward method for loss function. + """ + Forward method of the loss function. :param torch.Tensor input: Input tensor from real data. :param torch.Tensor target: Model tensor output. diff --git a/pina/loss/scalar_weighting.py b/pina/loss/scalar_weighting.py index 3273dea77..6bc093c7d 100644 --- a/pina/loss/scalar_weighting.py +++ b/pina/loss/scalar_weighting.py @@ -1,20 +1,41 @@ -"""Module for Loss Interface""" +"""Module for the Scalar Weighting.""" from .weighting_interface import WeightingInterface from ..utils import check_consistency class _NoWeighting(WeightingInterface): + """ + Weighting scheme that does not apply any weighting to the losses. + """ + def aggregate(self, losses): + """ + Aggregate the losses. + + :param dict losses: The dictionary of losses. + :return: The aggregated losses. + :rtype: torch.Tensor + """ return sum(losses.values()) class ScalarWeighting(WeightingInterface): """ - TODO + Weighting scheme that assigns a scalar weight to each loss term. """ def __init__(self, weights): + """ + Initialization of the :class:`ScalarWeighting` class. + + :param weights: The weights to be assigned to each loss term. + If a single scalar value is provided, it is assigned to all loss + terms. If a dictionary is provided, the keys are the conditions and + the values are the weights. If a condition is not present in the + dictionary, the default value is used. + :type weights: float | int | dict + """ super().__init__() check_consistency([weights], (float, dict, int)) if isinstance(weights, (float, int)): @@ -28,8 +49,8 @@ def aggregate(self, losses): """ Aggregate the losses. - :param dict(torch.Tensor) losses: The dictionary of losses. - :return: The losses aggregation. It should be a scalar Tensor. + :param dict losses: The dictionary of losses. + :return: The aggregated losses. :rtype: torch.Tensor """ return sum( diff --git a/pina/loss/weighting_interface.py b/pina/loss/weighting_interface.py index 56a17b8ef..8b8cb2f28 100644 --- a/pina/loss/weighting_interface.py +++ b/pina/loss/weighting_interface.py @@ -1,14 +1,18 @@ -"""Module for Loss Interface""" +"""Module for the Weighting Interface.""" from abc import ABCMeta, abstractmethod class WeightingInterface(metaclass=ABCMeta): """ - The ``weightingInterface`` class. TODO + Abstract base class for all loss weighting schemas. All weighting schemas + should inherit from this class. """ def __init__(self): + """ + Initialization of the :class:`WeightingInterface` class. + """ self.condition_names = None @abstractmethod @@ -16,7 +20,5 @@ def aggregate(self, losses): """ Aggregate the losses. - :param dict(torch.Tensor) input: The dictionary of losses. - :return: The losses aggregation. It should be a scalar Tensor. - :rtype: torch.Tensor + :param dict losses: The dictionary of losses. """ diff --git a/pina/model/__init__.py b/pina/model/__init__.py index ac36fcb16..606dde7d5 100644 --- a/pina/model/__init__.py +++ b/pina/model/__init__.py @@ -1,6 +1,4 @@ -""" -Module containing the neural network models. -""" +"""Module for the Neural model classes.""" __all__ = [ "FeedForward", diff --git a/pina/model/average_neural_operator.py b/pina/model/average_neural_operator.py index 1b1bcfe02..6019b96c6 100644 --- a/pina/model/average_neural_operator.py +++ b/pina/model/average_neural_operator.py @@ -1,4 +1,4 @@ -"""Module Averaging Neural Operator.""" +"""Module for the Averaging Neural Operator model class.""" import torch from torch import nn @@ -9,19 +9,17 @@ class AveragingNeuralOperator(KernelNeuralOperator): """ - Implementation of Averaging Neural Operator. + Averaging Neural Operator model class. - Averaging Neural Operator is a general architecture for - learning Operators. Unlike traditional machine learning methods - AveragingNeuralOperator is designed to map entire functions - to other functions. It can be trained with Supervised learning strategies. - AveragingNeuralOperator does convolution by performing a field average. + The Averaging Neural Operator is a general architecture for learning + operators, which map functions to functions. It can be trained both with + Supervised and Physics-Informed learning strategies. The Averaging Neural + Operator performs convolution by means of a field average. .. seealso:: - **Original reference**: Lanthaler S. Li, Z., Kovachki, - Stuart, A. (2020). *The Nonlocal Neural Operator: - Universal Approximation*. + **Original reference**: Lanthaler S., Li, Z., Stuart, A. (2020). + *The Nonlocal Neural Operator: Universal Approximation*. DOI: `arXiv preprint arXiv:2304.13221. `_ """ @@ -36,21 +34,26 @@ def __init__( func=nn.GELU, ): """ - :param torch.nn.Module lifting_net: The neural network for lifting - the input. It must take as input the input field and the coordinates - at which the input field is avaluated. The output of the lifting - net is chosen as embedding dimension of the problem - :param torch.nn.Module projecting_net: The neural network for - projecting the output. It must take as input the embedding dimension - (output of the ``lifting_net``) plus the dimension - of the coordinates. - :param list[str] field_indices: the label of the fields - in the input tensor. - :param list[str] coordinates_indices: the label of the - coordinates in the input tensor. - :param int n_layers: number of hidden layers. Default is 4. - :param torch.nn.Module func: the activation function to use, - default to torch.nn.GELU. + Initialization of the :class:`AveragingNeuralOperator` class. + + :param torch.nn.Module lifting_net: The lifting neural network mapping + the input to its hidden dimension. It must take as input the input + field and the coordinates at which the input field is evaluated. + :param torch.nn.Module projecting_net: The projection neural network + mapping the hidden representation to the output function. It must + take as input the embedding dimension plus the dimension of the + coordinates. + :param list[str] field_indices: The labels of the fields in the input + tensor. + :param list[str] coordinates_indices: The labels of the coordinates in + the input tensor. + :param int n_layers: The number of hidden layers. Default is ``4``. + :param torch.nn.Module func: The activation function to use. + Default is :class:`torch.nn.GELU`. + :raises ValueError: If the input dimension does not match with the + labels of the fields and coordinates. + :raises ValueError: If the input dimension of the projecting network + does not match with the hidden dimension of the lifting network. """ # check consistency @@ -93,19 +96,20 @@ def __init__( def forward(self, x): r""" - Forward computation for Averaging Neural Operator. It performs a - lifting of the input by the ``lifting_net``. Then different layers - of Averaging Neural Operator Blocks are applied. - Finally the output is projected to the final dimensionality - by the ``projecting_net``. - - :param torch.Tensor x: The input tensor for fourier block, - depending on ``dimension`` in the initialization. It expects - a tensor :math:`B \times N \times D`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem, i.e. the sum - of ``len(coordinates_indices)+len(field_indices)``. - :return: The output tensor obtained from Average Neural Operator. + Forward pass for the :class:`AveragingNeuralOperator` model. + + The ``lifting_net`` maps the input to the hidden dimension. + Then, several layers of + :class:`~pina.model.block.average_neural_operator_block.AVNOBlock` are + applied. Finally, the ``projection_net`` maps the hidden representation + to the output function. + + :param LabelTensor x: The input tensor for performing the computation. + It expects a tensor :math:`B \times N \times D`, where :math:`B` is + the batch_size, :math:`N` the number of points in the mesh, + :math:`D` the dimension of the problem, i.e. the sum + of ``len(coordinates_indices)`` and ``len(field_indices)``. + :return: The output tensor. :rtype: torch.Tensor """ points_tmp = x.extract(self.coordinates_indices) diff --git a/pina/model/block/__init__.py b/pina/model/block/__init__.py index 9b6bac309..c40135b7e 100644 --- a/pina/model/block/__init__.py +++ b/pina/model/block/__init__.py @@ -1,6 +1,4 @@ -""" -Module containing the building blocks for models. -""" +"""Module for the building blocks of the neural models.""" __all__ = [ "ContinuousConvBlock", diff --git a/pina/model/block/average_neural_operator_block.py b/pina/model/block/average_neural_operator_block.py index 010e80bc7..91379abeb 100644 --- a/pina/model/block/average_neural_operator_block.py +++ b/pina/model/block/average_neural_operator_block.py @@ -1,4 +1,4 @@ -"""Module for Averaging Neural Operator Layer class.""" +"""Module for the Averaging Neural Operator Block class.""" import torch from torch import nn @@ -7,12 +7,12 @@ class AVNOBlock(nn.Module): r""" - The PINA implementation of the inner layer of the Averaging Neural Operator. + The inner block of the Averaging Neural Operator. The operator layer performs an affine transformation where the convolution is approximated with a local average. Given the input function - :math:`v(x)\in\mathbb{R}^{\rm{emb}}` the layer computes - the operator update :math:`K(v)` as: + :math:`v(x)\in\mathbb{R}^{\rm{emb}}` the layer computes the operator update + :math:`K(v)` as: .. math:: K(v) = \sigma\left(Wv(x) + b + \frac{1}{|\mathcal{A}|}\int v(y)dy\right) @@ -28,18 +28,20 @@ class AVNOBlock(nn.Module): .. seealso:: - **Original reference**: Lanthaler S. Li, Z., Kovachki, - Stuart, A. (2020). *The Nonlocal Neural Operator: Universal - Approximation*. + **Original reference**: Lanthaler S., Li, Z., Stuart, A. (2020). + *The Nonlocal Neural Operator: Universal Approximation*. DOI: `arXiv preprint arXiv:2304.13221. `_ - """ def __init__(self, hidden_size=100, func=nn.GELU): """ - :param int hidden_size: Size of the hidden layer, defaults to 100. - :param func: The activation function, default to nn.GELU. + Initialization of the :class:`AVNOBlock` class. + + :param int hidden_size: The size of the hidden layer. + Defaults is ``100``. + :param func: The activation function. + Default is :class:`torch.nn.GELU`. """ super().__init__() @@ -52,17 +54,11 @@ def __init__(self, hidden_size=100, func=nn.GELU): def forward(self, x): r""" - Forward pass of the layer, it performs a sum of local average - and an affine transformation of the field. + Forward pass of the block. It performs a sum of local average and an + affine transformation of the field. - :param torch.Tensor x: The input tensor for performing the - computation. It expects a tensor :math:`B \times N \times D`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem. In particular - :math:`D` is the codomain of the function :math:`v`. For example - a scalar function has :math:`D=1`, a 4-dimensional vector function - :math:`D=4`. - :return: The output tensor obtained from Average Neural Operator Block. + :param torch.Tensor x: The input tensor for performing the computation. + :return: The output tensor. :rtype: torch.Tensor """ return self._func(self._nn(x) + torch.mean(x, dim=1, keepdim=True)) diff --git a/pina/model/block/convolution.py b/pina/model/block/convolution.py index 1849399fe..666f66a66 100644 --- a/pina/model/block/convolution.py +++ b/pina/model/block/convolution.py @@ -1,4 +1,4 @@ -"""Module for Base Continuous Convolution class.""" +"""Module for the Base Continuous Convolution class.""" from abc import ABCMeta, abstractmethod import torch @@ -7,8 +7,32 @@ class BaseContinuousConv(torch.nn.Module, metaclass=ABCMeta): - """ - Abstract class + r""" + Base Class for Continuous Convolution. + + The class expects the input to be in the form: + :math:`[B \times N_{in} \times N \times D]`, where :math:`B` is the + batch_size, :math:`N_{in}` is the number of input fields, :math:`N` + the number of points in the mesh, :math:`D` the dimension of the problem. + In particular: + + * :math:`D` is the number of spatial variables + 1. The last column must + contain the field value. + * :math:`N_{in}` represents the number of function components. + For instance, a vectorial function :math:`f = [f_1, f_2]` has + :math:`N_{in}=2`. + + :Note + A 2-dimensional vector-valued function defined on a 3-dimensional input + evaluated on a 100 points input mesh and batch size of 8 is represented + as a tensor of shape ``[8, 2, 100, 4]``, where the columns + ``[:, 0, :, -1]`` and ``[:, 1, :, -1]`` represent the first and second, + components of the function, respectively. + + The algorithm returns a tensor of shape: + :math:`[B \times N_{out} \times N \times D]`, where :math:`B` is the + batch_size, :math:`N_{out}` is the number of output fields, :math:`N` + the number of points in the mesh, :math:`D` the dimension of the problem. """ def __init__( @@ -22,56 +46,30 @@ def __init__( no_overlap=False, ): """ - Base Class for Continuous Convolution. - - The algorithm expects input to be in the form: - $$[B \times N_{in} \times N \times D]$$ - where $B$ is the batch_size, $N_{in}$ is the number of input - fields, $N$ the number of points in the mesh, $D$ the dimension - of the problem. In particular: - * $D$ is the number of spatial variables + 1. The last column must - contain the field value. For example for 2D problems $D=3$ and - the tensor will be something like `[first coordinate, second - coordinate, field value]`. - * $N_{in}$ represents the number of vectorial function presented. - For example a vectorial function $f = [f_1, f_2]$ will have - $N_{in}=2$. - - :Note - A 2-dimensional vectorial function $N_{in}=2$ of 3-dimensional - input $D=3+1=4$ with 100 points input mesh and batch size of 8 - is represented as a tensor `[8, 2, 100, 4]`, where the columns - `[:, 0, :, -1]` and `[:, 1, :, -1]` represent the first and - second filed value respectively - - The algorithm returns a tensor of shape: - $$[B \times N_{out} \times N' \times D]$$ - where $B$ is the batch_size, $N_{out}$ is the number of output - fields, $N'$ the number of points in the mesh, $D$ the dimension - of the problem. - - :param input_numb_field: number of fields in the input - :type input_numb_field: int - :param output_numb_field: number of fields in the output - :type output_numb_field: int - :param filter_dim: dimension of the filter - :type filter_dim: tuple/ list - :param stride: stride for the filter - :type stride: dict - :param model: neural network for inner parametrization, - defaults to None. - :type model: torch.nn.Module, optional - :param optimize: flag for performing optimization on the continuous - filter, defaults to False. The flag `optimize=True` should be - used only when the scatter datapoints are fixed through the - training. If torch model is in `.eval()` mode, the flag is - automatically set to False always. - :type optimize: bool, optional - :param no_overlap: flag for performing optimization on the transpose - continuous filter, defaults to False. The flag set to `True` should - be used only when the filter positions do not overlap for different - strides. RuntimeError will raise in case of non-compatible strides. - :type no_overlap: bool, optional + Initialization of the :class:`BaseContinuousConv` class. + + :param int input_numb_field: The number of input fields. + :param int output_numb_field: The number of input fields. + :param filter_dim: The shape of the filter. + :type filter_dim: list[int] | tuple[int] + :param dict stride: The stride of the filter. + :param torch.nn.Module model: The neural network for inner + parametrization. Default is ``None``. + :param bool optimize: If ``True``, optimization is performed on the + continuous filter. It should be used only when the training points + are fixed. If ``model`` is in ``eval`` mode, it is reset to + ``False``. Default is ``False``. + :param bool no_overlap: If ``True``, optimization is performed on the + transposed continuous filter. It should be used only when the filter + positions do not overlap for different strides. + Default is ``False``. + :raises ValueError: If ``input_numb_field`` is not an integer. + :raises ValueError: If ``output_numb_field`` is not an integer. + :raises ValueError: If ``filter_dim`` is not a list or tuple. + :raises ValueError: If ``stride`` is not a dictionary. + :raises ValueError: If ``optimize`` is not a boolean. + :raises ValueError: If ``no_overlap`` is not a boolean. + :raises NotImplementedError: If ``no_overlap`` is ``True``. """ super().__init__() @@ -119,12 +117,17 @@ def __init__( class DefaultKernel(torch.nn.Module): """ - TODO + The default kernel. """ def __init__(self, input_dim, output_dim): """ - TODO + Initialization of the :class:`DefaultKernel` class. + + :param int input_dim: The input dimension. + :param int output_dim: The output dimension. + :raises ValueError: If ``input_dim`` is not an integer. + :raises ValueError: If ``output_dim`` is not an integer. """ super().__init__() assert isinstance(input_dim, int) @@ -139,65 +142,93 @@ def __init__(self, input_dim, output_dim): def forward(self, x): """ - TODO + Forward pass. + + :param torch.Tensor x: The input data. + :return: The output data. + :rtype: torch.Tensor """ return self._model(x) @property def net(self): """ - TODO + The neural network for inner parametrization. + + :return: The neural network. + :rtype: torch.nn.Module """ return self._net @property def stride(self): """ - TODO + The stride of the filter. + + :return: The stride of the filter. + :rtype: dict """ return self._stride @property def filter_dim(self): """ - TODO + The shape of the filter. + + :return: The shape of the filter. + :rtype: torch.Tensor """ return self._dim @property def input_numb_field(self): """ - TODO + The number of input fields. + + :return: The number of input fields. + :rtype: int """ return self._input_numb_field @property def output_numb_field(self): """ - TODO + The number of output fields. + + :return: The number of output fields. + :rtype: int """ return self._output_numb_field @abstractmethod def forward(self, X): """ - TODO + Forward pass. + + :param torch.Tensor X: The input data. """ @abstractmethod def transpose_overlap(self, X): """ - TODO + Transpose the convolution with overlap. + + :param torch.Tensor X: The input data. """ @abstractmethod def transpose_no_overlap(self, X): """ - TODO + Transpose the convolution without overlap. + + :param torch.Tensor X: The input data. """ @abstractmethod def _initialize_convolution(self, X, type_): """ - TODO + Initialize the convolution. + + :param torch.Tensor X: The input data. + :param str type_: The type of initialization. """ diff --git a/pina/model/block/convolution_2d.py b/pina/model/block/convolution_2d.py index 68df175d3..825ae613b 100644 --- a/pina/model/block/convolution_2d.py +++ b/pina/model/block/convolution_2d.py @@ -1,4 +1,4 @@ -"""Module for Continuous Convolution class""" +"""Module for the Continuous Convolution class.""" import torch from .convolution import BaseContinuousConv @@ -7,14 +7,14 @@ class ContinuousConvBlock(BaseContinuousConv): - """ - Implementation of Continuous Convolutional operator. + r""" + Continuous Convolutional block. - The algorithm expects input to be in the form: - :math:`[B, N_{in}, N, D]` - where :math:`B` is the batch_size, :math:`N_{in}` is the number of input - fields, :math:`N` the number of points in the mesh, :math:`D` the dimension - of the problem. In particular: + The class expects the input to be in the form: + :math:`[B \times N_{in} \times N \times D]`, where :math:`B` is the + batch_size, :math:`N_{in}` is the number of input fields, :math:`N` + the number of points in the mesh, :math:`D` the dimension of the problem. + In particular: * :math:`D` is the number of spatial variables + 1. The last column must contain the field value. For example for 2D problems :math:`D=3` and @@ -26,11 +26,11 @@ class ContinuousConvBlock(BaseContinuousConv): .. seealso:: - **Original reference**: Coscia, D., Meneghetti, L., Demo, N. et al. - *A continuous convolutional trainable filter for modelling - unstructured data*. Comput Mech 72, 253–265 (2023). + **Original reference**: + Coscia, D., Meneghetti, L., Demo, N. et al. + *A continuous convolutional trainable filter for modelling unstructured + data*. Comput Mech 72, 253-265 (2023). DOI ``_ - """ def __init__( @@ -44,53 +44,48 @@ def __init__( no_overlap=False, ): """ - :param input_numb_field: Number of fields :math:`N_{in}` in the input. - :type input_numb_field: int - :param output_numb_field: Number of fields :math:`N_{out}` in the - output. - :type output_numb_field: int - :param filter_dim: Dimension of the filter. - :type filter_dim: tuple(int) | list(int) - :param stride: Stride for the filter. - :type stride: dict - :param model: Neural network for inner parametrization, - defaults to ``None``. If None, a default multilayer perceptron - of width three and size twenty with ReLU activation is used. - :type model: torch.nn.Module - :param optimize: Flag for performing optimization on the continuous - filter, defaults to False. The flag `optimize=True` should be - used only when the scatter datapoints are fixed through the - training. If torch model is in ``.eval()`` mode, the flag is - automatically set to False always. - :type optimize: bool - :param no_overlap: Flag for performing optimization on the transpose - continuous filter, defaults to False. The flag set to `True` should - be used only when the filter positions do not overlap for different - strides. RuntimeError will raise in case of non-compatible strides. - :type no_overlap: bool + Initialization of the :class:`ContinuousConvBlock` class. + + :param int input_numb_field: The number of input fields. + :param int output_numb_field: The number of input fields. + :param filter_dim: The shape of the filter. + :type filter_dim: list[int] | tuple[int] + :param dict stride: The stride of the filter. + :param torch.nn.Module model: The neural network for inner + parametrization. Default is ``None``. + :param bool optimize: If ``True``, optimization is performed on the + continuous filter. It should be used only when the training points + are fixed. If ``model`` is in ``eval`` mode, it is reset to + ``False``. Default is ``False``. + :param bool no_overlap: If ``True``, optimization is performed on the + transposed continuous filter. It should be used only when the filter + positions do not overlap for different strides. + Default is ``False``. .. note:: - Using `optimize=True` the filter can be use either in `forward` - or in `transpose` mode, not both. If `optimize=False` the same - filter can be used for both `transpose` and `forward` modes. + If ``optimize=True``, the filter can be use either in ``forward`` + or in ``transpose`` mode, not both. :Example: >>> class MLP(torch.nn.Module): - def __init__(self) -> None: - super().__init__() - self. model = torch.nn.Sequential( - torch.nn.Linear(2, 8), - torch.nn.ReLU(), - torch.nn.Linear(8, 8), - torch.nn.ReLU(), - torch.nn.Linear(8, 1)) - def forward(self, x): - return self.model(x) + ... def __init__(self) -> None: + ... super().__init__() + ... self. model = torch.nn.Sequential( + ... torch.nn.Linear(2, 8), + ... torch.nn.ReLU(), + ... torch.nn.Linear(8, 8), + ... torch.nn.ReLU(), + ... torch.nn.Linear(8, 1) + ... ) + ... def forward(self, x): + ... return self.model(x) >>> dim = [3, 3] - >>> stride = {"domain": [10, 10], - "start": [0, 0], - "jumps": [3, 3], - "direction": [1, 1.]} + >>> stride = { + ... "domain": [10, 10], + ... "start": [0, 0], + ... "jumps": [3, 3], + ... "direction": [1, 1.] + ... } >>> conv = ContinuousConv2D(1, 2, dim, stride, MLP) >>> conv ContinuousConv2D( @@ -116,7 +111,6 @@ def forward(self, x): ) ) """ - super().__init__( input_numb_field=input_numb_field, output_numb_field=output_numb_field, @@ -143,13 +137,13 @@ def forward(self, x): def _spawn_networks(self, model): """ - Private method to create a collection of kernels + Create a collection of kernels - :param model: A :class:`torch.nn.Module` model in form of Object class. - :type model: torch.nn.Module - :return: List of :class:`torch.nn.Module` models. + :param torch.nn.Module model: A neural network model. + :raises ValueError: If the model is not a subclass of + ``torch.nn.Module``. + :return: A list of models. :rtype: torch.nn.ModuleList - """ nets = [] if self._net is None: @@ -176,13 +170,11 @@ def _spawn_networks(self, model): def _extract_mapped_points(self, batch_idx, index, x): """ - Priviate method to extract mapped points in the filter + Extract mapped points in the filter. - :param x: Input tensor of shape ``[channel, N, dim]`` - :type x: torch.Tensor + :param torch.Tensor x: Input tensor of shape ``[channel, N, dim]`` :return: Mapped points and indeces for each channel, - :rtype: torch.Tensor, list - + :rtype: tuple """ mapped_points = [] indeces_channels = [] @@ -218,11 +210,9 @@ def _extract_mapped_points(self, batch_idx, index, x): def _find_index(self, X): """ - Private method to extract indeces for convolution. - - :param X: Input tensor, as in ContinuousConvBlock ``__init__``. - :type X: torch.Tensor + Extract indeces for convolution. + :param torch.Tensor X: The input tensor. """ # append the index for each stride index = [] @@ -236,11 +226,9 @@ def _find_index(self, X): def _make_grid_forward(self, X): """ - Private method to create forward convolution grid. - - :param X: Input tensor, as in ContinuousConvBlock docstring. - :type X: torch.Tensor + Create forward convolution grid. + :param torch.Tensor X: The input tensor. """ # filter dimension + number of points in output grid filter_dim = len(self._dim) @@ -264,12 +252,9 @@ def _make_grid_forward(self, X): def _make_grid_transpose(self, X): """ - Private method to create transpose convolution grid. - - :param X: Input tensor, as in ContinuousConvBlock docstring. - :type X: torch.Tensor - + Create transpose convolution grid. + :param torch.Tensor X: The input tensor. """ # initialize to all zeros tmp = torch.zeros_like(X).as_subclass(torch.Tensor) @@ -280,14 +265,12 @@ def _make_grid_transpose(self, X): def _make_grid(self, X, type_): """ - Private method to create convolution grid. - - :param X: Input tensor, as in ContinuousConvBlock docstring. - :type X: torch.Tensor - :param type: Type of convolution, ``['forward', 'inverse']`` the - possibilities. - :type type: str + Create convolution grid. + :param torch.Tensor X: The input tensor. + :param str type_: The type of convolution. + Available options are: ``forward`` and ``inverse``. + :raises TypeError: If the type is not in the available options. """ # choose the type of convolution if type_ == "forward": @@ -300,15 +283,12 @@ def _make_grid(self, X, type_): def _initialize_convolution(self, X, type_="forward"): """ - Private method to intialize the convolution. - The convolution is initialized by setting a grid and - calculate the index for finding the points inside the - filter. - - :param X: Input tensor, as in ContinuousConvBlock docstring. - :type X: torch.Tensor - :param str type: type of convolution, ``['forward', 'inverse'] ``the - possibilities. + Initialize the convolution by setting a grid and computing the index to + find the points inside the filter. + + :param torch.Tensor X: The input tensor. + :param str type_: The type of convolution. Available options are: + ``forward`` and ``inverse``. Default is ``forward``. """ # variable for the convolution @@ -319,11 +299,10 @@ def _initialize_convolution(self, X, type_="forward"): def forward(self, X): """ - Forward pass in the convolutional layer. + Forward pass. - :param x: Input data for the convolution :math:`[B, N_{in}, N, D]`. - :type x: torch.Tensor - :return: Convolution output :math:`[B, N_{out}, N, D]`. + :param torch.Tensor x: The input tensor. + :return: The output tensor. :rtype: torch.Tensor """ @@ -381,25 +360,14 @@ def forward(self, X): def transpose_no_overlap(self, integrals, X): """ - Transpose pass in the layer for no-overlapping filters - - :param integrals: Weights for the transpose convolution. Shape - :math:`[B, N_{in}, N]` - where B is the batch_size, :math`N_{in}` is the number of input - fields, :math:`N` the number of points in the mesh, D the dimension - of the problem. - :type integral: torch.tensor - :param X: Input data. Expect tensor of shape - :math:`[B, N_{in}, M, D]` where :math:`B` is the batch_size, - :math`N_{in}`is the number of input fields, :math:`M` the number of - points - in the mesh, :math:`D` the dimension of the problem. - :type X: torch.Tensor - :return: Feed forward transpose convolution. Tensor of shape - :math:`[B, N_{out}, M, D]` where :math:`B` is the batch_size, - :math`N_{out}`is the number of input fields, :math:`M` the number of - points - in the mesh, :math:`D` the dimension of the problem. + Transpose pass in the layer for no-overlapping filters. + + :param torch.Tensor integrals: The weights for the transpose convolution. + Expected shape :math:`[B, N_{in}, N]`. + :param torch.Tensor X: The input data. + Expected shape :math:`[B, N_{in}, M, D]`. + :return: Feed forward transpose convolution. + Expected shape: :math:`[B, N_{out}, M, D]`. :rtype: torch.Tensor .. note:: @@ -466,25 +434,14 @@ def transpose_no_overlap(self, integrals, X): def transpose_overlap(self, integrals, X): """ - Transpose pass in the layer for overlapping filters - - :param integrals: Weights for the transpose convolution. Shape - :math:`[B, N_{in}, N]` - where B is the batch_size, :math`N_{in}` is the number of input - fields, :math:`N` the number of points in the mesh, D the dimension - of the problem. - :type integral: torch.tensor - :param X: Input data. Expect tensor of shape - :math:`[B, N_{in}, M, D]` where :math:`B` is the batch_size, - :math`N_{in}`is the number of input fields, :math:`M` the number of - points - in the mesh, :math:`D` the dimension of the problem. - :type X: torch.Tensor - :return: Feed forward transpose convolution. Tensor of shape - :math:`[B, N_{out}, M, D]` where :math:`B` is the batch_size, - :math`N_{out}`is the number of input fields, :math:`M` the number of - points - in the mesh, :math:`D` the dimension of the problem. + Transpose pass in the layer for overlapping filters. + + :param torch.Tensor integrals: The weights for the transpose convolution. + Expected shape :math:`[B, N_{in}, N]`. + :param torch.Tensor X: The input data. + Expected shape :math:`[B, N_{in}, M, D]`. + :return: Feed forward transpose convolution. + Expected shape: :math:`[B, N_{out}, M, D]`. :rtype: torch.Tensor .. note:: This function is automatically called when ``.transpose()`` diff --git a/pina/model/block/embedding.py b/pina/model/block/embedding.py index 270ca1d05..1e44ec143 100644 --- a/pina/model/block/embedding.py +++ b/pina/model/block/embedding.py @@ -1,4 +1,4 @@ -"""Embedding modulus.""" +"""Modules for the the Embedding blocks.""" import torch from pina.utils import check_consistency @@ -6,20 +6,18 @@ class PeriodicBoundaryEmbedding(torch.nn.Module): r""" - Imposing hard constraint periodic boundary conditions by embedding the + Enforcing hard-constrained periodic boundary conditions by embedding the input. - A periodic function :math:`u:\mathbb{R}^{\rm{in}} - \rightarrow\mathbb{R}^{\rm{out}}` periodic in the spatial - coordinates :math:`\mathbf{x}` with periods :math:`\mathbf{L}` is such that: + A function :math:`u:\mathbb{R}^{\rm{in}} \rightarrow\mathbb{R}^{\rm{out}}` + is periodic with respect to the spatial coordinates :math:`\mathbf{x}` + with period :math:`\mathbf{L}` if: .. math:: u(\mathbf{x}) = u(\mathbf{x} + n \mathbf{L})\;\; \forall n\in\mathbb{N}. - The :meth:`PeriodicBoundaryEmbedding` augments the input such that the - periodic conditonsis guarantee. The input is augmented by the following - formula: + The :class:`PeriodicBoundaryEmbedding` augments the input as follows: .. math:: \mathbf{x} \rightarrow \tilde{\mathbf{x}} = \left[1, @@ -32,44 +30,48 @@ class PeriodicBoundaryEmbedding(torch.nn.Module): .. seealso:: **Original reference**: - 1. Dong, Suchuan, and Naxian Ni (2021). *A method for representing - periodic functions and enforcing exactly periodic boundary - conditions with deep neural networks*. Journal of Computational - Physics 435, 110242. + 1. Dong, Suchuan, and Naxian Ni (2021). + *A method for representing periodic functions and enforcing + exactly periodic boundary conditions with deep neural networks*. + Journal of Computational Physics 435, 110242. DOI: `10.1016/j.jcp.2021.110242. `_ - 2. Wang, S., Sankaran, S., Wang, H., & Perdikaris, P. (2023). *An - expert's guide to training physics-informed neural networks*. + 2. Wang, S., Sankaran, S., Wang, H., & Perdikaris, P. (2023). + *An expert's guide to training physics-informed neural + networks*. DOI: `arXiv preprint arXiv:2308.0846. `_ + .. warning:: - The embedding is a truncated fourier expansion, and only ensures - function PBC and not for its derivatives. Ensuring approximate - periodicity in - the derivatives of :math:`u` can be done, and extensive - tests have shown (also in the reference papers) that this implementation - can correctly compute the PBC on the derivatives up to the order - :math:`\sim 2,3`, while it is not guarantee the periodicity for - :math:`>3`. The PINA code is tested only for function PBC and not for - its derivatives. + The embedding is a truncated fourier expansion, and enforces periodic + boundary conditions only for the function, and not for its derivatives. + Enforcement of the approximate periodicity in the derivatives can be + performed. Extensive tests have shown (see referenced papers) that this + implementation can correctly enforce the periodic boundary conditions on + the derivatives up to the order :math:`\sim 2,3`. This is not guaranteed + for orders :math:`>3`. The PINA module is tested only for periodic + boundary conditions on the function itself. """ def __init__(self, input_dimension, periods, output_dimension=None): """ - :param int input_dimension: The dimension of the input tensor, it can - be checked with `tensor.ndim` method. - :param float | int | dict periods: The periodicity in each dimension for - the input data. If ``float`` or ``int`` is passed, - the period is assumed constant for all the dimensions of the data. - If a ``dict`` is passed the `dict.values` represent periods, - while the ``dict.keys`` represent the dimension where the - periodicity is applied. The `dict.keys` can either be `int` - if working with ``torch.Tensor`` or ``str`` if - working with ``LabelTensor``. + Initialization of the :class:`PeriodicBoundaryEmbedding` block. + + :param int input_dimension: The dimension of the input tensor. + :param periods: The periodicity with respect to each dimension for the + input data. If ``float`` or ``int`` is passed, the period is assumed + to be constant over all the dimensions of the data. If a ``dict`` is + passed the `dict.values` represent periods, while the ``dict.keys`` + represent the dimension where the periodicity is enforced. + The `dict.keys` can either be `int` if working with + :class:`torch.Tensor`, or ``str`` if working with + :class:`pina.label_tensor.LabelTensor`. + :type periods: float | int | dict :param int output_dimension: The dimension of the output after the - fourier embedding. If not ``None`` a ``torch.nn.Linear`` layer + fourier embedding. If not ``None``, a :class:`torch.nn.Linear` layer is applied to the fourier embedding output to match the desired - dimensionality, default ``None``. + dimensionality. Default is ``None``. + :raises TypeError: If the periods dict is not consistent. """ super().__init__() @@ -98,9 +100,10 @@ def __init__(self, input_dimension, periods, output_dimension=None): def forward(self, x): """ - Forward pass to compute the periodic boundary conditions embedding. + Forward pass. - :param torch.Tensor x: Input tensor. + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor :return: Periodic embedding of the input. :rtype: torch.Tensor """ @@ -125,12 +128,16 @@ def forward(self, x): def _get_vars(self, x, indeces): """ - Get variables from input tensor ordered by specific indeces. - - :param torch.Tensor x: The input tensor to extract. - :param list[int] | list[str] indeces: List of indeces to extract. - :return: The extracted tensor given the indeces. - :rtype: torch.Tensor + Get the variables from input tensor ordered by specific indeces. + + :param x: The input tensor from which to extract. + :type x: torch.Tensor | LabelTensor + :param indeces: The indeces to extract. + :type indeces: list[int] | list[str] + :raises RuntimeError: If the indeces are not consistent. + :raises RuntimeError: If the extraction is not possible. + :return: The extracted tensor. + :rtype: torch.Tensor | LabelTensor """ if isinstance(indeces[0], str): try: @@ -146,75 +153,79 @@ def _get_vars(self, x, indeces): return x[..., indeces] else: raise RuntimeError( - "Not able to extract right indeces for tensor." + "Not able to extract correct indeces for tensor." " For more information refer to warning in the documentation." ) @property def period(self): """ - The period of the periodic function to approximate. + The period of the function. + + :return: The period of the function. + :rtype: dict | float | int """ return self._period class FourierFeatureEmbedding(torch.nn.Module): - """ - Fourier Feature Embedding class for encoding input features - using random Fourier features. + r""" + Fourier Feature Embedding class to encode the input features using random + Fourier features. + + This class applies a Fourier transformation to the input features, which can + help in learning high-frequency variations in data. The class supports + multiscale feature embedding, creating embeddings for each scale specified + by the ``sigma`` parameter. + + The Fourier Feature Embedding augments the input features as follows + (3.10 of original paper): + + .. math:: + \mathbf{x} \rightarrow \tilde{\mathbf{x}} = \left[ + \cos\left( \mathbf{B} \mathbf{x} \right), + \sin\left( \mathbf{B} \mathbf{x} \right)\right], + + where :math:`\mathbf{B}_{ij} \sim \mathcal{N}(0, \sigma^2)`. + + If multiple ``sigma`` are passed, the resulting embeddings are concateneted: + + .. math:: + \mathbf{x} \rightarrow \tilde{\mathbf{x}} = \left[ + \cos\left( \mathbf{B}^1 \mathbf{x} \right), + \sin\left( \mathbf{B}^1 \mathbf{x} \right), + \cos\left( \mathbf{B}^2 \mathbf{x} \right), + \sin\left( \mathbf{B}^3 \mathbf{x} \right), + \dots, + \cos\left( \mathbf{B}^M \mathbf{x} \right), + \sin\left( \mathbf{B}^M \mathbf{x} \right)\right], + + where :math:`\mathbf{B}^k_{ij} \sim \mathcal{N}(0, \sigma_k^2) \quad k \in + (1, \dots, M)`. + + .. seealso:: + **Original reference**: + Wang, S., Wang, H., and Perdikaris, P. (2021). + *On the eigenvector bias of Fourier feature networks: From regression to + solving multi-scale PDEs with physics-informed neural networks.* + Computer Methods in Applied Mechanics and Engineering 384 (2021): + 113938. + DOI: `10.1016/j.cma.2021.113938. + `_ """ def __init__(self, input_dimension, output_dimension, sigma): - r""" - This class applies a Fourier transformation to the input features, - which can help in learning high-frequency variations in data. - If multiple sigma are provided, the class - supports multiscale feature embedding, creating embeddings for - each scale specified by the sigma. - - The :obj:`FourierFeatureEmbedding` augments the input - by the following formula (3.10 of original paper): - - .. math:: - \mathbf{x} \rightarrow \tilde{\mathbf{x}} = \left[ - \cos\left( \mathbf{B} \mathbf{x} \right), - \sin\left( \mathbf{B} \mathbf{x} \right)\right], - - where :math:`\mathbf{B}_{ij} \sim \mathcal{N}(0, \sigma^2)`. - - In case multiple ``sigma`` are passed, the resulting embeddings - are concateneted: - - .. math:: - \mathbf{x} \rightarrow \tilde{\mathbf{x}} = \left[ - \cos\left( \mathbf{B}^1 \mathbf{x} \right), - \sin\left( \mathbf{B}^1 \mathbf{x} \right), - \cos\left( \mathbf{B}^2 \mathbf{x} \right), - \sin\left( \mathbf{B}^3 \mathbf{x} \right), - \dots, - \cos\left( \mathbf{B}^M \mathbf{x} \right), - \sin\left( \mathbf{B}^M \mathbf{x} \right)\right], - - where :math:`\mathbf{B}^k_{ij} \sim \mathcal{N}(0, \sigma_k^2) \quad - k \in (1, \dots, M)`. - - .. seealso:: - **Original reference**: - Wang, Sifan, Hanwen Wang, and Paris Perdikaris. *On the eigenvector - bias of Fourier feature networks: From regression to solving - multi-scale PDEs with physics-informed neural networks.* - Computer Methods in Applied Mechanics and - Engineering 384 (2021): 113938. - DOI: `10.1016/j.cma.2021.113938. - `_ - - :param int input_dimension: The input vector dimension of the layer. - :param int output_dimension: The output dimension of the layer. The - output is obtained as a concatenation of the cosine and sine - embedding, hence it must be a multiple of two (even number). - :param int | float sigma: The standard deviation used for the - Fourier Embedding. This value must reflect the granularity of the - scale in the differential equation solution. + """ + Initialization of the :class:`FourierFeatureEmbedding` block. + + :param int input_dimension: The dimension of the input tensor. + :param int output_dimension: The dimension of the output tensor. The + output is obtained as a concatenation of cosine and sine embeddings. + :param sigma: The standard deviation used for the Fourier Embedding. + This value must reflect the granularity of the scale in the + differential equation solution. + :type sigma: float | int + :raises RuntimeError: If the output dimension is not an even number. """ super().__init__() @@ -242,10 +253,11 @@ def __init__(self, input_dimension, output_dimension, sigma): def forward(self, x): """ - Forward pass to compute the fourier embedding. + Forward pass. - :param torch.Tensor x: Input tensor. - :return: Fourier embeddings of the input. + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :return: Fourier embedding of the input. :rtype: torch.Tensor """ # compute random matrix multiplication @@ -259,6 +271,9 @@ def forward(self, x): @property def sigma(self): """ - Returning the variance of the sampled matrix for Fourier Embedding. + The standard deviation used for the Fourier Embedding. + + :return: The standard deviation used for the Fourier Embedding. + :rtype: float | int """ return self._sigma diff --git a/pina/model/block/fourier_block.py b/pina/model/block/fourier_block.py index 06f7efb16..2983c840a 100644 --- a/pina/model/block/fourier_block.py +++ b/pina/model/block/fourier_block.py @@ -1,6 +1,4 @@ -""" -Module for Fourier Block implementation. -""" +"""Module for the Fourier Neural Operator Block class.""" import torch from torch import nn @@ -15,15 +13,19 @@ class FourierBlock1D(nn.Module): """ - Fourier block implementation for three dimensional - input tensor. The combination of Fourier blocks - make up the Fourier Neural Operator + The inner block of the Fourier Neural Operator for 1-dimensional input + tensors. + + The module computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. The output is then added to a Linear tranformation of the input in + the physical space. Finally an activation function is applied to the output. .. seealso:: **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., - Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). *Fourier - neural operator for parametric partial differential equations*. + Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). + *Fourier neural operator for parametric partial differential equations*. DOI: `arXiv preprint arXiv:2010.08895. `_ @@ -36,22 +38,16 @@ def __init__( n_modes, activation=torch.nn.Tanh, ): - """ - PINA implementation of Fourier block one dimension. The module computes - the spectral convolution of the input with a linear kernel in the - fourier space, and then it maps the input back to the physical - space. The output is then added to a Linear tranformation of the - input in the physical space. Finally an activation function is - applied to the output. - - The block expects an input of size ``[batch, input_numb_fields, N]`` - and returns an output of size ``[batch, output_numb_fields, N]``. + r""" + Initialization of the :class:`FourierBlock1D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param list | tuple n_modes: Number of modes to select for each - dimension. It must be at most equal to the ``floor(N/2)+1``. + :param n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`. + :type n_modes: list[int] | tuple[int] :param torch.nn.Module activation: The activation function. + Default is :class:`torch.nn.Tanh`. """ super().__init__() @@ -70,15 +66,11 @@ def __init__( def forward(self, x): """ - Forward computation for Fourier Block. It performs a spectral - convolution and a linear transformation of the input and sum the - results. - - :param x: The input tensor for fourier block, expect of size - ``[batch, input_numb_fields, x]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - fourier block of size ``[batch, output_numb_fields, x]``. + Forward pass of the block. It performs a spectral convolution and a + linear transformation of the input. Then, it sums the results. + + :param torch.Tensor x: The input tensor for performing the computation. + :return: The output tensor. :rtype: torch.Tensor """ return self._activation(self._spectral_conv(x) + self._linear(x)) @@ -86,18 +78,21 @@ def forward(self, x): class FourierBlock2D(nn.Module): """ - Fourier block implementation for two dimensional - input tensor. The combination of Fourier blocks - make up the Fourier Neural Operator + The inner block of the Fourier Neural Operator for 2-dimensional input + tensors. - .. seealso:: + The module computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. The output is then added to a Linear tranformation of the input in + the physical space. Finally an activation function is applied to the output. - **Original reference**: Li, Zongyi, et al. - *Fourier neural operator for parametric partial - differential equations*. arXiv preprint - arXiv:2010.08895 (2020) - `_. + .. seealso:: + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., + Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). + *Fourier neural operator for parametric partial differential equations*. + DOI: `arXiv preprint arXiv:2010.08895. + `_ """ def __init__( @@ -107,24 +102,17 @@ def __init__( n_modes, activation=torch.nn.Tanh, ): - """ - PINA implementation of Fourier block two dimensions. The module computes - the spectral convolution of the input with a linear kernel in the - fourier space, and then it maps the input back to the physical - space. The output is then added to a Linear tranformation of the - input in the physical space. Finally an activation function is - applied to the output. - - The block expects an input of size - ``[batch, input_numb_fields, Nx, Ny]`` and returns an output of size - ``[batch, output_numb_fields, Nx, Ny]``. + r""" + Initialization of the :class:`FourierBlock2D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param list | tuple n_modes: Number of modes to select for each - dimension. It must be at most equal to the ``floor(Nx/2)+1`` - and ``floor(Ny/2)+1``. + :param n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`, + :math:`\floor(Ny/2)+1`. + :type n_modes: list[int] | tuple[int] :param torch.nn.Module activation: The activation function. + Default is :class:`torch.nn.Tanh`. """ super().__init__() @@ -142,15 +130,11 @@ def __init__( def forward(self, x): """ - Forward computation for Fourier Block. It performs a spectral - convolution and a linear transformation of the input and sum the - results. - - :param x: The input tensor for fourier block, expect of size - ``[batch, input_numb_fields, x, y]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - fourier block of size ``[batch, output_numb_fields, x, y, z]``. + Forward pass of the block. It performs a spectral convolution and a + linear transformation of the input. Then, it sums the results. + + :param torch.Tensor x: The input tensor for performing the computation. + :return: The output tensor. :rtype: torch.Tensor """ return self._activation(self._spectral_conv(x) + self._linear(x)) @@ -158,18 +142,21 @@ def forward(self, x): class FourierBlock3D(nn.Module): """ - Fourier block implementation for three dimensional - input tensor. The combination of Fourier blocks - make up the Fourier Neural Operator + The inner block of the Fourier Neural Operator for 3-dimensional input + tensors. - .. seealso:: + The module computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. The output is then added to a Linear tranformation of the input in + the physical space. Finally an activation function is applied to the output. - **Original reference**: Li, Zongyi, et al. - *Fourier neural operator for parametric partial - differential equations*. arXiv preprint - arXiv:2010.08895 (2020) - `_. + .. seealso:: + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., + Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). + *Fourier neural operator for parametric partial differential equations*. + DOI: `arXiv preprint arXiv:2010.08895. + `_ """ def __init__( @@ -179,24 +166,17 @@ def __init__( n_modes, activation=torch.nn.Tanh, ): - """ - PINA implementation of Fourier block three dimensions. The module - computes the spectral convolution of the input with a linear kernel in - the fourier space, and then it maps the input back to the physical - space. The output is then added to a Linear tranformation of the - input in the physical space. Finally an activation function is - applied to the output. - - The block expects an input of size - ``[batch, input_numb_fields, Nx, Ny, Nz]`` and returns an output of size - ``[batch, output_numb_fields, Nx, Ny, Nz]``. + r""" + Initialization of the :class:`FourierBlock3D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param list | tuple n_modes: Number of modes to select for each - dimension. It must be at most equal to the ``floor(Nx/2)+1``, - ``floor(Ny/2)+1`` and ``floor(Nz/2)+1``. + :param n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`, + :math:`\floor(Ny/2)+1`, :math:`\floor(Nz/2)+1`. + :type n_modes: list[int] | tuple[int] :param torch.nn.Module activation: The activation function. + Default is :class:`torch.nn.Tanh`. """ super().__init__() @@ -214,15 +194,11 @@ def __init__( def forward(self, x): """ - Forward computation for Fourier Block. It performs a spectral - convolution and a linear transformation of the input and sum the - results. - - :param x: The input tensor for fourier block, expect of size - ``[batch, input_numb_fields, x, y, z]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - fourier block of size ``[batch, output_numb_fields, x, y, z]``. + Forward pass of the block. It performs a spectral convolution and a + linear transformation of the input. Then, it sums the results. + + :param torch.Tensor x: The input tensor for performing the computation. + :return: The output tensor. :rtype: torch.Tensor """ return self._activation(self._spectral_conv(x) + self._linear(x)) diff --git a/pina/model/block/gno_block.py b/pina/model/block/gno_block.py index c1d470dfa..600803463 100644 --- a/pina/model/block/gno_block.py +++ b/pina/model/block/gno_block.py @@ -1,6 +1,4 @@ -""" -Module containing the Graph Integral Layer class. -""" +"""Module for the Graph Neural Operator Block class.""" import torch from torch_geometric.nn import MessagePassing @@ -8,7 +6,7 @@ class GNOBlock(MessagePassing): """ - Graph Neural Operator (GNO) Block using PyG MessagePassing. + The inner block of the Graph Neural Operator, based on Message Passing. """ def __init__( @@ -22,11 +20,22 @@ def __init__( external_func=None, ): """ - Initialize the GNOBlock. - - :param width: Hidden dimension of node features. - :param edges_features: Number of edge features. - :param n_layers: Number of layers in edge transformation MLP. + Initialization of the :class:`GNOBlock` class. + + :param int width: The width of the kernel. + :param int edge_features: The number of edge features. + :param int n_layers: The number of kernel layers. Default is ``2``. + :param layers: A list specifying the number of neurons for each layer + of the neural network. If not ``None``, it overrides the + ``inner_size`` and ``n_layers``parameters. Default is ``None``. + :type layers: list[int] | tuple[int] + :param int inner_size: The size of the inner layer. Default is ``None``. + :param torch.nn.Module internal_func: The activation function applied to + the output of each layer. If ``None``, it uses the + :class:`torch.nn.Tanh` activation. Default is ``None``. + :param torch.nn.Module external_func: The activation function applied to + the output of the block. If ``None``, it uses the + :class:`torch.nn.Tanh`. activation. Default is ``None``. """ from ...model.feed_forward import FeedForward @@ -51,12 +60,13 @@ def __init__( def message_and_aggregate(self, edge_index, x, edge_attr): """ - Combines message and aggregation. + Combine messages and perform aggregation. - :param edge_index: COO format edge indices. - :param x: Node feature matrix [num_nodes, width]. - :param edge_attr: Edge features [num_edges, edge_dim]. - :return: Aggregated messages. + :param torch.Tensor edge_index: The edge index. + :param torch.Tensor x: The node feature matrix. + :param torch.Tensor edge_attr: The edge features. + :return: The aggregated messages. + :rtype: torch.Tensor """ # Edge features are transformed into a matrix of shape # [num_edges, width, width] @@ -68,27 +78,33 @@ def message_and_aggregate(self, edge_index, x, edge_attr): def edge_update(self, edge_attr): """ - Updates edge features. + Update edge features. + + :param torch.Tensor edge_attr: The edge features. + :return: The updated edge features. + :rtype: torch.Tensor """ return edge_attr def update(self, aggr_out, x): """ - Updates node features. + Update node features. - :param aggr_out: Aggregated messages. - :param x: Node feature matrix. - :return: Updated node features. + :param torch.Tensor aggr_out: The aggregated messages. + :param torch.Tensor x: The node feature matrix. + :return: The updated node features. + :rtype: torch.Tensor """ return aggr_out + self.W(x) def forward(self, x, edge_index, edge_attr): """ - Forward pass of the GNOBlock. + Forward pass of the block. - :param x: Node features. - :param edge_index: Edge indices. - :param edge_attr: Edge features. - :return: Updated node features. + :param torch.Tensor x: The node features. + :param torch.Tensor edge_index: The edge indeces. + :param torch.Tensor edge_attr: The edge features. + :return: The updated node features. + :rtype: torch.Tensor """ return self.func(self.propagate(edge_index, x=x, edge_attr=edge_attr)) diff --git a/pina/model/block/integral.py b/pina/model/block/integral.py index 5b54bb76c..0bab4f07a 100644 --- a/pina/model/block/integral.py +++ b/pina/model/block/integral.py @@ -1,23 +1,22 @@ -""" -Module for performing integral for continuous convolution -""" +"""Module to perform integration for continuous convolution.""" import torch class Integral: """ - Integral class for continous convolution + Class allowing integration for continous convolution. """ def __init__(self, param): """ - Initialize the integral class + Initializzation of the :class:`Integral` class. - :param param: type of continuous convolution + :param param: The type of continuous convolution. :type param: string + :raises TypeError: If the parameter is neither ``discrete`` + nor ``continuous``. """ - if param == "discrete": self.make_integral = self.integral_param_disc elif param == "continuous": @@ -26,46 +25,47 @@ def __init__(self, param): raise TypeError def __call__(self, *args, **kwds): + """ + Call the integral function + + :param list args: Arguments for the integral function. + :param dict kwds: Keyword arguments for the integral function. + :return: The integral of the input. + :rtype: torch.tensor + """ return self.make_integral(*args, **kwds) def _prepend_zero(self, x): - """Create bins for performing integral + """ + Create bins to perform integration. - :param x: input tensor - :type x: torch.tensor - :return: bins for integrals - :rtype: torch.tensor + :param torch.Tensor x: The input tensor. + :return: The bins for the integral. + :rtype: torch.Tensor """ return torch.cat((torch.zeros(1, dtype=x.dtype, device=x.device), x)) def integral_param_disc(self, x, y, idx): - """Perform discretize integral - with discrete parameters + """ + Perform discrete integration with discrete parameters. - :param x: input vector - :type x: torch.tensor - :param y: input vector - :type y: torch.tensor - :param idx: indeces for different strides - :type idx: list - :return: integral - :rtype: torch.tensor + :param torch.Tensor x: The first input tensor. + :param torch.Tensor y: The second input tensor. + :param list[int] idx: The indices for different strides. + :return: The discrete integral. + :rtype: torch.Tensor """ cs_idxes = self._prepend_zero(torch.cumsum(torch.tensor(idx), 0)) cs = self._prepend_zero(torch.cumsum(x.flatten() * y.flatten(), 0)) return cs[cs_idxes[1:]] - cs[cs_idxes[:-1]] def integral_param_cont(self, x, y, idx): - """Perform discretize integral for continuous convolution - with continuous parameters + """ + Perform continuous integration with continuous parameters. - :param x: input vector - :type x: torch.tensor - :param y: input vector - :type y: torch.tensor - :param idx: indeces for different strides - :type idx: list - :return: integral - :rtype: torch.tensor + :param torch.Tensor x: The first input tensor. + :param torch.Tensor y: The second input tensor. + :param list[int] idx: The indices for different strides. + :raises NotImplementedError: The method is not implemented. """ raise NotImplementedError diff --git a/pina/model/block/low_rank_block.py b/pina/model/block/low_rank_block.py index 06b59d7dc..1e8925d95 100644 --- a/pina/model/block/low_rank_block.py +++ b/pina/model/block/low_rank_block.py @@ -1,4 +1,4 @@ -"""Module for Averaging Neural Operator Layer class.""" +"""Module for the Low Rank Neural Operator Block class.""" import torch @@ -6,30 +6,8 @@ class LowRankBlock(torch.nn.Module): - r""" - The PINA implementation of the inner layer of the Averaging Neural Operator. - - The operator layer performs an affine transformation where the convolution - is approximated with a local average. Given the input function - :math:`v(x)\in\mathbb{R}^{\rm{emb}}` the layer computes - the operator update :math:`K(v)` as: - - .. math:: - K(v) = \sigma\left(Wv(x) + b + \sum_{i=1}^r \langle - \psi^{(i)} , v(x) \rangle \phi^{(i)} \right) - - where: - - * :math:`\mathbb{R}^{\rm{emb}}` is the embedding (hidden) size - corresponding to the ``hidden_size`` object - * :math:`\sigma` is a non-linear activation, corresponding to the - ``func`` object - * :math:`W\in\mathbb{R}^{\rm{emb}\times\rm{emb}}` is a tunable matrix. - * :math:`b\in\mathbb{R}^{\rm{emb}}` is a tunable bias. - * :math:`\psi^{(i)}\in\mathbb{R}^{\rm{emb}}` and - :math:`\phi^{(i)}\in\mathbb{R}^{\rm{emb}}` are :math:`r` a low rank - basis functions mapping. - * :math:`b\in\mathbb{R}^{\rm{emb}}` is a tunable bias. + """ + The inner block of the Low Rank Neural Operator. .. seealso:: @@ -38,7 +16,6 @@ class LowRankBlock(torch.nn.Module): (2023). *Neural operator: Learning maps between function spaces with applications to PDEs*. Journal of Machine Learning Research, 24(89), 1-97. - """ def __init__( @@ -51,30 +28,25 @@ def __init__( func=torch.nn.Tanh, bias=True, ): - """ - :param int input_dimensions: The number of input components of the - model. - Expected tensor shape of the form :math:`(*, d)`, where * - means any number of dimensions including none, - and :math:`d` the ``input_dimensions``. - :param int embedding_dimenion: Size of the embedding dimension of the - field. - :param int rank: The rank number of the basis approximation components - of the model. Expected tensor shape of the form :math:`(*, 2d)`, - where * means any number of dimensions including none, - and :math:`2d` the ``rank`` for both basis functions. - :param int inner_size: Number of neurons in the hidden layer(s) for the - basis function network. Default is 20. - :param int n_layers: Number of hidden layers. for the - basis function network. Default is 2. - :param func: The activation function to use for the - basis function network. If a single - :class:`torch.nn.Module` is passed, this is used as - activation function after any layers, except the last one. - If a list of Modules is passed, - they are used as activation functions at any layers, in order. - :param bool bias: If ``True`` the MLP will consider some bias for the - basis function network. + r""" + Initialization of the :class:`LowRankBlock` class. + + :param int input_dimensions: The input dimension of the field. + :param int embedding_dimenion: The embedding dimension of the field. + :param int rank: The rank of the low rank approximation. The expected + value is :math:`2d`, where :math:`d` is the rank of each basis + function. + :param int inner_size: The number of neurons for each hidden layer in + the basis function neural network. Default is ``20``. + :param int n_layers: The number of hidden layers in the basis function + neural network. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param bool bias: If ``True`` bias is considered for the basis function + neural network. Default is ``True``. """ super().__init__() from ..feed_forward import FeedForward @@ -96,26 +68,16 @@ def __init__( def forward(self, x, coords): r""" - Forward pass of the layer, it performs an affine transformation of - the field, and a low rank approximation by - doing a dot product of the basis - :math:`\psi^{(i)}` with the filed vector :math:`v`, and use this - coefficients to expand :math:`\phi^{(i)}` evaluated in the - spatial input :math:`x`. - - :param torch.Tensor x: The input tensor for performing the - computation. It expects a tensor :math:`B \times N \times D`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem. In particular - :math:`D` is the codomain of the function :math:`v`. For example - a scalar function has :math:`D=1`, a 4-dimensional vector function - :math:`D=4`. - :param torch.Tensor coords: The coordinates in which the field is - evaluated for performing the computation. It expects a - tensor :math:`B \times N \times d`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the domain. - :return: The output tensor obtained from Average Neural Operator Block. + Forward pass of the block. It performs an affine transformation of the + field, followed by a low rank approximation. The latter is performed by + means of a dot product of the basis :math:`\psi^{(i)}` with the vector + field :math:`v` to compute coefficients used to expand + :math:`\phi^{(i)}`, evaluated in the spatial input :math:`x`. + + :param torch.Tensor x: The input tensor for performing the computation. + :param torch.Tensor coords: The coordinates for which the field is + evaluated to perform the computation. + :return: The output tensor. :rtype: torch.Tensor """ # extract basis @@ -138,5 +100,8 @@ def forward(self, x, coords): def rank(self): """ The basis rank. + + :return: The basis rank. + :rtype: int """ return self._rank diff --git a/pina/model/block/orthogonal.py b/pina/model/block/orthogonal.py index 32a060719..cd45b3c72 100644 --- a/pina/model/block/orthogonal.py +++ b/pina/model/block/orthogonal.py @@ -1,4 +1,4 @@ -"""Module for OrthogonalBlock.""" +"""Module for the Orthogonal Block class.""" import torch from ...utils import check_consistency @@ -6,21 +6,24 @@ class OrthogonalBlock(torch.nn.Module): """ - Module to make the input orthonormal. - The module takes a tensor of size :math:`[N, M]` and returns a tensor of - size :math:`[N, M]` where the columns are orthonormal. The block performs a - Gram Schmidt orthogonalization process for the input, see + Orthogonal Block. + + This block transforms an input tensor of shape :math:`[N, M]` into a tensor + of the same shape whose columns are orthonormal. The block performs the + Gram Schmidt orthogonalization, see `here ` for details. """ def __init__(self, dim=-1, requires_grad=True): """ - Initialize the OrthogonalBlock module. + Initialization of the :class:`OrthogonalBlock` class. - :param int dim: The dimension where to orthogonalize. - :param bool requires_grad: If autograd should record operations on - the returned tensor, defaults to True. + :param int dim: The dimension on which orthogonalization is performed. + If ``-1``, the orthogonalization is performed on the last dimension. + Default is ``-1``. + :param bool requires_grad: If ``True``, the gradients are computed + during the backward pass. Default is ``True`` """ super().__init__() # store dim @@ -31,14 +34,13 @@ def __init__(self, dim=-1, requires_grad=True): def forward(self, X): """ - Forward pass of the OrthogonalBlock module using a Gram-Schmidt - algorithm. - - :raises Warning: If the dimension is greater than the other dimensions. + Forward pass. - :param torch.Tensor X: The input tensor to orthogonalize. The input must - be of dimensions :math:`[N, M]`. + :param torch.Tensor X: The input tensor to orthogonalize. + :raises Warning: If the chosen dimension is greater than the other + dimensions in the input. :return: The orthonormal tensor. + :rtype: torch.Tensor """ # check dim is less than all the other dimensions if X.shape[self.dim] > min(X.shape): @@ -65,13 +67,12 @@ def forward(self, X): def _differentiable_copy(self, result, idx, value): """ - Perform a differentiable copy operation on a tensor. + Perform a differentiable copy operation. - :param torch.Tensor result: The tensor where values will be copied to. + :param torch.Tensor result: The tensor where values are be copied to. :param int idx: The index along the specified dimension where the - value will be copied. - :param torch.Tensor value: The tensor value to copy into the - result tensor. + values are copied. + :param torch.Tensor value: The tensor value to copy into ``result``. :return: A new tensor with the copied values. :rtype: torch.Tensor """ @@ -82,7 +83,7 @@ def _differentiable_copy(self, result, idx, value): @property def dim(self): """ - Get the dimension along which operations are performed. + The dimension along which operations are performed. :return: The current dimension value. :rtype: int @@ -94,10 +95,11 @@ def dim(self, value): """ Set the dimension along which operations are performed. - :param value: The dimension to be set, which must be 0, 1, or -1. + :param value: The dimension to be set. Must be either ``0``, ``1``, or + ``-1``. :type value: int - :raises IndexError: If the provided dimension is not in the - range [-1, 1]. + :raises IndexError: If the provided dimension is not ``0``, ``1``, or + ``-1``. """ # check consistency check_consistency(value, int) @@ -115,7 +117,7 @@ def requires_grad(self): Indicates whether gradient computation is required for operations on the tensors. - :return: True if gradients are required, False otherwise. + :return: ``True`` if gradients are required, ``False`` otherwise. :rtype: bool """ return self._requires_grad diff --git a/pina/model/block/pod_block.py b/pina/model/block/pod_block.py index aff359ffe..203a035bf 100644 --- a/pina/model/block/pod_block.py +++ b/pina/model/block/pod_block.py @@ -5,23 +5,26 @@ class PODBlock(torch.nn.Module): """ - POD layer: it projects the input field on the proper orthogonal - decomposition basis. It needs to be fitted to the data before being used - with the method :meth:`fit`, which invokes the singular value decomposition. - The layer is not trainable. + Proper Orthogonal Decomposition block. + + This block projects the input field on the proper orthogonal decomposition + basis. Before being used, it must be fitted to the data with the ``fit`` + method, which invokes the singular value decomposition. This block is not + trainable. .. note:: All the POD modes are stored in memory, avoiding to recompute them when - the rank changes but increasing the memory usage. + the rank changes, leading to increased memory usage. """ def __init__(self, rank, scale_coefficients=True): """ - Build the POD layer with the given rank. + Initialization of the :class:`PODBlock` class. :param int rank: The rank of the POD layer. - :param bool scale_coefficients: If True, the coefficients are scaled + :param bool scale_coefficients: If ``True``, the coefficients are scaled after the projection to have zero mean and unit variance. + Default is ``True``. """ super().__init__() self.__scale_coefficients = scale_coefficients @@ -34,12 +37,19 @@ def rank(self): """ The rank of the POD layer. + :return: The rank of the POD layer. :rtype: int """ return self._rank @rank.setter def rank(self, value): + """ + Set the rank of the POD layer. + + :param int value: The new rank of the POD layer. + :raises ValueError: If the rank is not a positive integer. + """ if value < 1 or not isinstance(value, int): raise ValueError("The rank must be positive integer") @@ -48,9 +58,10 @@ def rank(self, value): @property def basis(self): """ - The POD basis. It is a matrix whose columns are the first `self.rank` - POD modes. + The POD basis. It is a matrix whose columns are the first ``rank`` POD + modes. + :return: The POD basis. :rtype: torch.Tensor """ if self._basis is None: @@ -61,9 +72,11 @@ def basis(self): @property def scaler(self): """ - The scaler. It is a dictionary with the keys `'mean'` and `'std'` that - store the mean and the standard deviation of the coefficients. + Return the scaler dictionary, having keys ``mean`` and ``std`` + corresponding to the mean and the standard deviation of the + coefficients, respectively. + :return: The scaler dictionary. :rtype: dict """ if self._scaler is None: @@ -77,9 +90,9 @@ def scaler(self): @property def scale_coefficients(self): """ - If True, the coefficients are scaled after the projection to have zero - mean and unit variance. + The flag indicating if the coefficients are scaled after the projection. + :return: The flag indicating if the coefficients are scaled. :rtype: bool """ return self.__scale_coefficients @@ -87,10 +100,10 @@ def scale_coefficients(self): def fit(self, X): """ Set the POD basis by performing the singular value decomposition of the - given tensor. If `self.scale_coefficients` is True, the coefficients + given tensor. If ``self.scale_coefficients`` is True, the coefficients are scaled after the projection to have zero mean and unit variance. - :param torch.Tensor X: The tensor to be reduced. + :param torch.Tensor X: The input tensor to be reduced. """ self._fit_pod(X) @@ -99,10 +112,8 @@ def fit(self, X): def _fit_scaler(self, coeffs): """ - Private merhod that computes the mean and the standard deviation of the - given coefficients, allowing to scale them to have zero mean and unit - variance. Mean and standard deviation are stored in the private member - `_scaler`. + Compute the mean and the standard deviation of the given coefficients, + which are then stored in ``self._scaler``. :param torch.Tensor coeffs: The coefficients to be scaled. """ @@ -113,8 +124,8 @@ def _fit_scaler(self, coeffs): def _fit_pod(self, X): """ - Private method that computes the POD basis of the given tensor and - stores it in the private member `_basis`. + Compute the POD basis of the given tensor, which is then stored in + ``self._basis``. :param torch.Tensor X: The tensor to be reduced. """ @@ -125,9 +136,7 @@ def _fit_pod(self, X): def forward(self, X): """ - The forward pass of the POD layer. By default it executes the - :meth:`reduce` method, reducing the input tensor to its POD - representation. The POD layer needs to be fitted before being used. + The forward pass of the POD layer. :param torch.Tensor X: The input tensor to be reduced. :return: The reduced tensor. @@ -137,10 +146,11 @@ def forward(self, X): def reduce(self, X): """ - Reduce the input tensor to its POD representation. The POD layer needs - to be fitted before being used. + Reduce the input tensor to its POD representation. The POD layer must + be fitted before being used. :param torch.Tensor X: The input tensor to be reduced. + :raises RuntimeError: If the POD layer is not fitted. :return: The reduced tensor. :rtype: torch.Tensor """ @@ -165,6 +175,7 @@ def expand(self, coeff): to be fitted before being used. :param torch.Tensor coeff: The coefficients to be expanded. + :raises RuntimeError: If the POD layer is not fitted. :return: The expanded tensor. :rtype: torch.Tensor """ diff --git a/pina/model/block/rbf_block.py b/pina/model/block/rbf_block.py index e088d00d9..8001381bc 100644 --- a/pina/model/block/rbf_block.py +++ b/pina/model/block/rbf_block.py @@ -1,4 +1,4 @@ -"""Module for Radial Basis Function Interpolation layer.""" +"""Module for the Radial Basis Function Interpolation layer.""" import math import warnings @@ -10,6 +10,10 @@ def linear(r): """ Linear radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The linear radial basis function. + :rtype: torch.Tensor """ return -r @@ -17,6 +21,11 @@ def linear(r): def thin_plate_spline(r, eps=1e-7): """ Thin plate spline radial basis function. + + :param torch.Tensor r: Distance between points. + :param float eps: Small value to avoid log(0). + :return: The thin plate spline radial basis function. + :rtype: torch.Tensor """ r = torch.clamp(r, min=eps) return r**2 * torch.log(r) @@ -25,6 +34,10 @@ def thin_plate_spline(r, eps=1e-7): def cubic(r): """ Cubic radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The cubic radial basis function. + :rtype: torch.Tensor """ return r**3 @@ -32,6 +45,10 @@ def cubic(r): def quintic(r): """ Quintic radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The quintic radial basis function. + :rtype: torch.Tensor """ return -(r**5) @@ -39,6 +56,10 @@ def quintic(r): def multiquadric(r): """ Multiquadric radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The multiquadric radial basis function. + :rtype: torch.Tensor """ return -torch.sqrt(r**2 + 1) @@ -46,6 +67,10 @@ def multiquadric(r): def inverse_multiquadric(r): """ Inverse multiquadric radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The inverse multiquadric radial basis function. + :rtype: torch.Tensor """ return 1 / torch.sqrt(r**2 + 1) @@ -53,6 +78,10 @@ def inverse_multiquadric(r): def inverse_quadratic(r): """ Inverse quadratic radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The inverse quadratic radial basis function. + :rtype: torch.Tensor """ return 1 / (r**2 + 1) @@ -60,6 +89,10 @@ def inverse_quadratic(r): def gaussian(r): """ Gaussian radial basis function. + + :param torch.Tensor r: Distance between points. + :return: The gaussian radial basis function. + :rtype: torch.Tensor """ return torch.exp(-(r**2)) @@ -88,13 +121,14 @@ def gaussian(r): class RBFBlock(torch.nn.Module): """ - Radial Basis Function (RBF) interpolation layer. It need to be fitted with - the data with the method :meth:`fit`, before it can be used to interpolate - new points. The layer is not trainable. + Radial Basis Function (RBF) interpolation layer. + + The user needs to fit the model with the data, before using it to + interpolate new points. The layer is not trainable. .. note:: - It reproduces the implementation of ``scipy.interpolate.RBFBlock`` and - it is inspired from the implementation in `torchrbf. + It reproduces the implementation of :class:`scipy.interpolate.RBFBlock` + and it is inspired from the implementation in `torchrbf. `_ """ @@ -107,24 +141,25 @@ def __init__( degree=None, ): """ - :param int neighbors: Number of neighbors to use for the - interpolation. - If ``None``, use all data points. - :param float smoothing: Smoothing parameter for the interpolation. - if 0.0, the interpolation is exact and no smoothing is applied. - :param str kernel: Radial basis function to use. Must be one of - ``linear``, ``thin_plate_spline``, ``cubic``, ``quintic``, - ``multiquadric``, ``inverse_multiquadric``, ``inverse_quadratic``, - or ``gaussian``. - :param float epsilon: Shape parameter that scaled the input to - the RBF. This defaults to 1 for kernels in ``scale_invariant`` - dictionary, and must be specified for other kernels. - :param int degree: Degree of the added polynomial. - For some kernels, there exists a minimum degree of the polynomial - such that the RBF is well-posed. Those minimum degrees are specified - in the `min_degree_funcs` dictionary above. If `degree` is less than - the minimum degree, a warning is raised and the degree is set to the - minimum value. + Initialization of the :class:`RBFBlock` class. + + :param int neighbors: The number of neighbors used for interpolation. + If ``None``, all data are used. + :param float smoothing: The moothing parameter for the interpolation. + If ``0.0``, the interpolation is exact and no smoothing is applied. + :param str kernel: The radial basis function to use. + The available kernels are: ``linear``, ``thin_plate_spline``, + ``cubic``, ``quintic``, ``multiquadric``, ``inverse_multiquadric``, + ``inverse_quadratic``, or ``gaussian``. + :param float epsilon: The shape parameter that scales the input to the + RBF. Default is ``1`` for kernels in the ``scale_invariant`` + dictionary, while it must be specified for other kernels. + :param int degree: The degree of the polynomial. Some kernels require a + minimum degree of the polynomial to ensure that the RBF is well + defined. These minimum degrees are specified in the + ``min_degree_funcs`` dictionary. If ``degree`` is less than the + minimum degree required, a warning is raised and the degree is set + to the minimum value. """ super().__init__() @@ -151,27 +186,39 @@ def __init__( @property def smoothing(self): """ - Smoothing parameter for the interpolation. + The smoothing parameter for the interpolation. + :return: The smoothing parameter. :rtype: float """ return self._smoothing @smoothing.setter def smoothing(self, value): + """ + Set the smoothing parameter for the interpolation. + + :param float value: The smoothing parameter. + """ self._smoothing = value @property def kernel(self): """ - Radial basis function to use. + The Radial basis function. + :return: The radial basis function. :rtype: str """ return self._kernel @kernel.setter def kernel(self, value): + """ + Set the radial basis function. + + :param str value: The radial basis function. + """ if value not in radial_functions: raise ValueError(f"Unknown kernel: {value}") self._kernel = value.lower() @@ -179,14 +226,22 @@ def kernel(self, value): @property def epsilon(self): """ - Shape parameter that scaled the input to the RBF. + The shape parameter that scales the input to the RBF. + :return: The shape parameter. :rtype: float """ return self._epsilon @epsilon.setter def epsilon(self, value): + """ + Set the shape parameter. + + :param float value: The shape parameter. + :raises ValueError: If the kernel requires an epsilon and it is not + specified. + """ if value is None: if self.kernel in scale_invariant: value = 1.0 @@ -199,14 +254,23 @@ def epsilon(self, value): @property def degree(self): """ - Degree of the added polynomial. + The degree of the polynomial. + :return: The degree of the polynomial. :rtype: int """ return self._degree @degree.setter def degree(self, value): + """ + Set the degree of the polynomial. + + :param int value: The degree of the polynomial. + :raises UserWarning: If the degree is less than the minimum required + for the kernel. + :raises ValueError: If the degree is less than -1. + """ min_degree = min_degree_funcs.get(self.kernel, -1) if value is None: value = max(min_degree, 0) @@ -223,6 +287,13 @@ def degree(self, value): self._degree = value def _check_data(self, y, d): + """ + Check the data consistency. + + :param torch.Tensor y: The tensor of data points. + :param torch.Tensor d: The tensor of data values. + :raises ValueError: If the data is not consistent. + """ if y.ndim != 2: raise ValueError("y must be a 2-dimensional tensor.") @@ -241,8 +312,11 @@ def fit(self, y, d): """ Fit the RBF interpolator to the data. - :param torch.Tensor y: (n, d) tensor of data points. - :param torch.Tensor d: (n, m) tensor of data values. + :param torch.Tensor y: The tensor of data points. + :param torch.Tensor d: The tensor of data values. + :raises NotImplementedError: If the neighbors are not ``None``. + :raises ValueError: If the data is not compatible with the requested + degree. """ self._check_data(y, d) @@ -252,7 +326,7 @@ def fit(self, y, d): if self.neighbors is None: nobs = self.y.shape[0] else: - raise NotImplementedError("neighbors currently not supported") + raise NotImplementedError("Neighbors currently not supported") powers = RBFBlock.monomial_powers(self.y.shape[1], self.degree).to( y.device @@ -276,12 +350,14 @@ def fit(self, y, d): def forward(self, x): """ - Returns the interpolated data at the given points `x`. - - :param torch.Tensor x: `(n, d)` tensor of points at which - to query the interpolator - - :rtype: `(n, m)` torch.Tensor of interpolated data. + Forward pass. + + :param torch.Tensor x: The tensor of points to interpolate. + :raises ValueError: If the input is not a 2-dimensional tensor. + :raises ValueError: If the second dimension of the input is not the same + as the second dimension of the data. + :return: The interpolated data. + :rtype: torch.Tensor """ if x.ndim != 2: raise ValueError("`x` must be a 2-dimensional tensor.") @@ -309,25 +385,25 @@ def forward(self, x): @staticmethod def kernel_vector(x, y, kernel_func): """ - Evaluate radial functions with centers `y` for all points in `x`. + Evaluate for all points ``x`` the radial functions with center ``y``. - :param torch.Tensor x: `(n, d)` tensor of points. - :param torch.Tensor y: `(m, d)` tensor of centers. + :param torch.Tensor x: The tensor of points. + :param torch.Tensor y: The tensor of centers. :param str kernel_func: Radial basis function to use. - - :rtype: `(n, m)` torch.Tensor of radial function values. + :return: The radial function values. + :rtype: torch.Tensor """ return kernel_func(torch.cdist(x, y)) @staticmethod def polynomial_matrix(x, powers): """ - Evaluate monomials at `x` with given `powers`. + Evaluate monomials of power ``powers`` at points ``x``. - :param torch.Tensor x: `(n, d)` tensor of points. - :param torch.Tensor powers: `(r, d)` tensor of powers for each monomial. - - :rtype: `(n, r)` torch.Tensor of monomial values. + :param torch.Tensor x: The tensor of points. + :param torch.Tensor powers: The tensor of powers for each monomial. + :return: The monomial values. + :rtype: torch.Tensor """ x_ = torch.repeat_interleave(x, repeats=powers.shape[0], dim=0) powers_ = powers.repeat(x.shape[0], 1) @@ -336,12 +412,12 @@ def polynomial_matrix(x, powers): @staticmethod def kernel_matrix(x, kernel_func): """ - Returns radial function values for all pairs of points in `x`. - - :param torch.Tensor x: `(n, d`) tensor of points. - :param str kernel_func: Radial basis function to use. + Return the radial function values for all pairs of points in ``x``. - :rtype: `(n, n`) torch.Tensor of radial function values. + :param torch.Tensor x: The tensor of points. + :param str kernel_func: The radial basis function to use. + :return: The radial function values. + :rtype: torch.Tensor """ return kernel_func(torch.cdist(x, x)) @@ -350,12 +426,10 @@ def monomial_powers(ndim, degree): """ Return the powers for each monomial in a polynomial. - :param int ndim: Number of variables in the polynomial. - :param int degree: Degree of the polynomial. - - :rtype: `(nmonos, ndim)` torch.Tensor where each row contains the powers - for each variable in a monomial. - + :param int ndim: The number of variables in the polynomial. + :param int degree: The degree of the polynomial. + :return: The powers for each monomial. + :rtype: torch.Tensor """ nmonos = math.comb(degree + ndim, ndim) out = torch.zeros((nmonos, ndim), dtype=torch.int32) @@ -372,16 +446,16 @@ def build(y, d, smoothing, kernel, epsilon, powers): """ Build the RBF linear system. - :param torch.Tensor y: (n, d) tensor of data points. - :param torch.Tensor d: (n, m) tensor of data values. - :param torch.Tensor smoothing: (n,) tensor of smoothing parameters. - :param str kernel: Radial basis function to use. - :param float epsilon: Shape parameter that scaled the input to the RBF. - :param torch.Tensor powers: (r, d) tensor of powers for each monomial. - - :rtype: (lhs, rhs, shift, scale) where `lhs` and `rhs` are the - left-hand side and right-hand side of the linear system, and - `shift` and `scale` are the shift and scale parameters. + :param torch.Tensor y: The tensor of data points. + :param torch.Tensor d: The tensor of data values. + :param torch.Tensor smoothing: The tensor of smoothing parameters. + :param str kernel: The radial basis function to use. + :param float epsilon: The shape parameter that scales the input to the + RBF. + :param torch.Tensor powers: The tensor of powers for each monomial. + :return: The left-hand side and right-hand side of the linear system, + and the shift and scale parameters. + :rtype: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] """ p = d.shape[0] s = d.shape[1] @@ -413,21 +487,20 @@ def build(y, d, smoothing, kernel, epsilon, powers): @staticmethod def solve(y, d, smoothing, kernel, epsilon, powers): """ - Build then solve the RBF linear system. + Build and solve the RBF linear system. - :param torch.Tensor y: (n, d) tensor of data points. - :param torch.Tensor d: (n, m) tensor of data values. - :param torch.Tensor smoothing: (n,) tensor of smoothing parameters. - - :param str kernel: Radial basis function to use. - :param float epsilon: Shape parameter that scaled the input to the RBF. - :param torch.Tensor powers: (r, d) tensor of powers for each monomial. + :param torch.Tensor y: The tensor of data points. + :param torch.Tensor d: The tensor of data values. + :param torch.Tensor smoothing: The tensor of smoothing parameters. + :param str kernel: The radial basis function to use. + :param float epsilon: The shape parameter that scaled the input to the + RBF. + :param torch.Tensor powers: The tensor of powers for each monomial. :raises ValueError: If the linear system is singular. - - :rtype: (shift, scale, coeffs) where `shift` and `scale` are the - shift and scale parameters, and `coeffs` are the coefficients - of the interpolator + :return: The shift and scale parameters, and the coefficients of the + interpolator. + :rtype: tuple[torch.Tensor, torch.Tensor, torch.Tensor] """ lhs, rhs, shift, scale = RBFBlock.build( diff --git a/pina/model/block/residual.py b/pina/model/block/residual.py index db9f4f2b6..f109ce03d 100644 --- a/pina/model/block/residual.py +++ b/pina/model/block/residual.py @@ -1,6 +1,4 @@ -""" -TODO: Add title. -""" +"""Module for residual blocks and enhanced linear layers.""" import torch from torch import nn @@ -8,16 +6,16 @@ class ResidualBlock(nn.Module): - """Residual block base class. Implementation of a residual block. + """ + Residual block class. .. seealso:: **Original reference**: He, Kaiming, et al. *Deep residual learning for image recognition.* - Proceedings of the IEEE conference on computer vision - and pattern recognition. 2016.. + Proceedings of the IEEE conference on computer vision and pattern + recognition. 2016. DOI: ``_. - """ def __init__( @@ -29,18 +27,15 @@ def __init__( activation=torch.nn.ReLU(), ): """ - Initializes the ResidualBlock module. - - :param int input_dim: Dimension of the input to pass to the - feedforward linear layer. - :param int output_dim: Dimension of the output from the - residual layer. - :param int hidden_dim: Hidden dimension for mapping the input - (first block). - :param bool spectral_norm: Apply spectral normalization to feedforward - layers, defaults to False. - :param torch.nn.Module activation: Cctivation function after first - block. + Initialization of the :class:`ResidualBlock` class. + + :param int input_dim: The input dimension. + :param int output_dim: The output dimension. + :param int hidden_dim: The hidden dimension. + :param bool spectral_norm: If ``True``, the spectral normalization is + applied to the feedforward layers. Default is ``False``. + :param torch.nn.Module activation: The activation function. + Default is :class:`torch.nn.ReLU`. """ super().__init__() @@ -64,10 +59,11 @@ def __init__( self._l3 = self._spect_norm(nn.Linear(input_dim, output_dim)) def forward(self, x): - """Forward pass for residual block layer. + """ + Forward pass. - :param torch.Tensor x: Input tensor for the residual layer. - :return: Output tensor for the residual layer. + :param torch.Tensor x: The input tensor. + :return: The output tensor. :rtype: torch.Tensor """ y = self._activation(self._l1(x)) @@ -76,10 +72,10 @@ def forward(self, x): return y + x def _spect_norm(self, x): - """Perform spectral norm on the layers. + """ + Perform spectral normalization on the network layers. - :param x: A torch.nn.Module Linear layer - :type x: torch.nn.Module + :param torch.nn.Module x: A :class:`torch.nn.Linear` layer. :return: The spectral norm of the layer :rtype: torch.nn.Module """ @@ -88,37 +84,31 @@ def _spect_norm(self, x): class EnhancedLinear(torch.nn.Module): """ - A wrapper class for enhancing a linear layer with activation and/or dropout. - - :param layer: The linear layer to be enhanced. - :type layer: torch.nn.Module - :param activation: The activation function to be applied after the linear - layer. - :type activation: torch.nn.Module - :param dropout: The dropout probability to be applied after the activation - (if provided). - :type dropout: float - - :Example: - - >>> linear_layer = torch.nn.Linear(10, 20) - >>> activation = torch.nn.ReLU() - >>> dropout_prob = 0.5 - >>> enhanced_linear = EnhancedLinear(linear_layer, activation, dropout_prob) + Enhanced Linear layer class. + + This class is a wrapper for enhancing a linear layer with activation and/or + dropout. """ def __init__(self, layer, activation=None, dropout=None): """ - Initializes the EnhancedLinear module. - - :param layer: The linear layer to be enhanced. - :type layer: torch.nn.Module - :param activation: The activation function to be applied after the - linear layer. - :type activation: torch.nn.Module - :param dropout: The dropout probability to be applied after the - activation (if provided). - :type dropout: float + Initialization of the :class:`EnhancedLinear` class. + + :param torch.nn.Module layer: The linear layer to be enhanced. + :param torch.nn.Module activation: The activation function. Default is + ``None``. + :param float dropout: The dropout probability. Default is ``None``. + + :Example: + + >>> linear_layer = torch.nn.Linear(10, 20) + >>> activation = torch.nn.ReLU() + >>> dropout_prob = 0.5 + >>> enhanced_linear = EnhancedLinear( + ... linear_layer, + ... activation, + ... dropout_prob + ... ) """ super().__init__() @@ -146,23 +136,19 @@ def __init__(self, layer, activation=None, dropout=None): def forward(self, x): """ - Forward pass through the enhanced linear module. + Forward pass. - :param x: Input tensor. - :type x: torch.Tensor - - :return: Output tensor after passing through the enhanced linear module. + :param torch.Tensor x: The input tensor. + :return: The output tensor. :rtype: torch.Tensor """ return self._model(x) def _drop(self, p): """ - Applies dropout with probability p. - - :param p: Dropout probability. - :type p: float + Apply dropout with probability p. + :param float p: Dropout probability. :return: Dropout layer with the specified probability. :rtype: torch.nn.Dropout """ diff --git a/pina/model/block/spectral.py b/pina/model/block/spectral.py index ba581a982..aae915a42 100644 --- a/pina/model/block/spectral.py +++ b/pina/model/block/spectral.py @@ -1,6 +1,4 @@ -""" -TODO: Add title. -""" +"""Module for spectral convolution blocks.""" import torch from torch import nn @@ -10,24 +8,23 @@ ######## 1D Spectral Convolution ########### class SpectralConvBlock1D(nn.Module): """ - PINA implementation of Spectral Convolution Block for one - dimensional tensors. + Spectral Convolution Block for one-dimensional tensors. + + This class computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. + The block expects an input of size [``batch``, ``input_numb_fields``, ``N``] + and returns an output of size [``batch``, ``output_numb_fields``, ``N``]. """ def __init__(self, input_numb_fields, output_numb_fields, n_modes): - """ - The module computes the spectral convolution of the input with a linear - kernel in the - fourier space, and then it maps the input back to the physical - space. - - The block expects an input of size ``[batch, input_numb_fields, N]`` - and returns an output of size ``[batch, output_numb_fields, N]``. + r""" + Initialization of the :class:`SpectralConvBlock1D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param int n_modes: Number of modes to select, it must be at most equal - to the ``floor(N/2)+1``. + :param int n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`. """ super().__init__() @@ -54,30 +51,26 @@ def __init__(self, input_numb_fields, output_numb_fields, n_modes): def _compute_mult1d(self, input, weights): """ - Compute the matrix multiplication of the input - with the linear kernel weights. - - :param input: The input tensor, expect of size - ``[batch, input_numb_fields, x]``. - :type input: torch.Tensor - :param weights: The kernel weights, expect of - size ``[input_numb_fields, output_numb_fields, x]``. - :type weights: torch.Tensor - :return: The matrix multiplication of the input - with the linear kernel weights. + Compute the matrix multiplication of the input and the linear kernel + weights. + + :param torch.Tensor input: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``N``]. + :param torch.Tensor weights: The kernel weights. Expected of size + [``input_numb_fields``, ``output_numb_fields``, ``N``]. + :return: The result of the matrix multiplication. :rtype: torch.Tensor """ return torch.einsum("bix,iox->box", input, weights) def forward(self, x): """ - Forward computation for Spectral Convolution. + Forward pass. - :param x: The input tensor, expect of size - ``[batch, input_numb_fields, x]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - spectral convolution of size ``[batch, output_numb_fields, x]``. + :param torch.Tensor x: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``N``]. + :return: The input tensor. Expected of size + [``batch``, ``output_numb_fields``, ``N``]. :rtype: torch.Tensor """ batch_size = x.shape[0] @@ -104,26 +97,29 @@ def forward(self, x): ######## 2D Spectral Convolution ########### class SpectralConvBlock2D(nn.Module): """ - PINA implementation of spectral convolution block for two - dimensional tensors. + Spectral Convolution Block for two-dimensional tensors. + + This class computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. + The block expects an input of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``] + and returns an output of size + [``batch``, ``output_numb_fields``, ``Nx``, ``Ny``]. """ def __init__(self, input_numb_fields, output_numb_fields, n_modes): - """ - The module computes the spectral convolution of the input with a linear - kernel in the - fourier space, and then it maps the input back to the physical - space. - - The block expects an input of size - ``[batch, input_numb_fields, Nx, Ny]`` - and returns an output of size ``[batch, output_numb_fields, Nx, Ny]``. + r""" + Initialization of the :class:`SpectralConvBlock2D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param list | tuple n_modes: Number of modes to select for each - dimension. It must be at most equal to the ``floor(Nx/2)+1`` and - ``floor(Ny/2)+1``. + :param n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`, + :math:`\floor(Ny/2)+1`. + :type n_modes: list[int] | tuple[int] + :raises ValueError: If the number of modes is not consistent. + :raises ValueError: If the number of modes is not a list or tuple. """ super().__init__() @@ -178,30 +174,26 @@ def __init__(self, input_numb_fields, output_numb_fields, n_modes): def _compute_mult2d(self, input, weights): """ - Compute the matrix multiplication of the input - with the linear kernel weights. - - :param input: The input tensor, expect of size - ``[batch, input_numb_fields, x, y]``. - :type input: torch.Tensor - :param weights: The kernel weights, expect of - size ``[input_numb_fields, output_numb_fields, x, y]``. - :type weights: torch.Tensor - :return: The matrix multiplication of the input - with the linear kernel weights. + Compute the matrix multiplication of the input and the linear kernel + weights. + + :param torch.Tensor input: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``]. + :param torch.Tensor weights: The kernel weights. Expected of size + [``input_numb_fields``, ``output_numb_fields``, ``Nx``, ``Ny``]. + :return: The result of the matrix multiplication. :rtype: torch.Tensor """ return torch.einsum("bixy,ioxy->boxy", input, weights) def forward(self, x): """ - Forward computation for Spectral Convolution. + Forward pass. - :param x: The input tensor, expect of size - ``[batch, input_numb_fields, x, y]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - spectral convolution of size ``[batch, output_numb_fields, x, y]``. + :param torch.Tensor x: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``]. + :return: The input tensor. Expected of size + [``batch``, ``output_numb_fields``, ``Nx``, ``Ny``]. :rtype: torch.Tensor """ @@ -235,27 +227,29 @@ def forward(self, x): ######## 3D Spectral Convolution ########### class SpectralConvBlock3D(nn.Module): """ - PINA implementation of spectral convolution block for three - dimensional tensors. + Spectral Convolution Block for three-dimensional tensors. + + This class computes the spectral convolution of the input with a linear + kernel in the fourier space, and then it maps the input back to the physical + space. + The block expects an input of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``, ``Nz``] + and returns an output of size + [``batch``, ``output_numb_fields``, ``Nx``, ``Ny``, ``Nz``]. """ def __init__(self, input_numb_fields, output_numb_fields, n_modes): - """ - The module computes the spectral convolution of the input with a - linear kernel in the - fourier space, and then it maps the input back to the physical - space. - - The block expects an input of size - ``[batch, input_numb_fields, Nx, Ny, Nz]`` - and returns an output of size - ``[batch, output_numb_fields, Nx, Ny, Nz]``. + r""" + Initialization of the :class:`SpectralConvBlock3D` class. :param int input_numb_fields: The number of channels for the input. :param int output_numb_fields: The number of channels for the output. - :param list | tuple n_modes: Number of modes to select for each - dimension. It must be at most equal to the ``floor(Nx/2)+1``, - ``floor(Ny/2)+1`` and ``floor(Nz/2)+1``. + :param n_modes: The number of modes to select for each dimension. + It must be at most equal to :math:`\floor(Nx/2)+1`, + :math:`\floor(Ny/2)+1`, :math:`\floor(Nz/2)+1`. + :type n_modes: list[int] | tuple[int] + :raises ValueError: If the number of modes is not consistent. + :raises ValueError: If the number of modes is not a list or tuple. """ super().__init__() @@ -334,31 +328,27 @@ def __init__(self, input_numb_fields, output_numb_fields, n_modes): def _compute_mult3d(self, input, weights): """ - Compute the matrix multiplication of the input - with the linear kernel weights. - - :param input: The input tensor, expect of size - ``[batch, input_numb_fields, x, y, z]``. - :type input: torch.Tensor - :param weights: The kernel weights, expect of - size ``[input_numb_fields, output_numb_fields, x, y, z]``. - :type weights: torch.Tensor - :return: The matrix multiplication of the input - with the linear kernel weights. + Compute the matrix multiplication of the input and the linear kernel + weights. + + :param torch.Tensor input: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``, ``Nz``]. + :param torch.Tensor weights: The kernel weights. Expected of size + [``input_numb_fields``, ``output_numb_fields``, ``Nx``, ``Ny``, + ``Nz``]. + :return: The result of the matrix multiplication. :rtype: torch.Tensor """ return torch.einsum("bixyz,ioxyz->boxyz", input, weights) def forward(self, x): """ - Forward computation for Spectral Convolution. - - :param x: The input tensor, expect of size - ``[batch, input_numb_fields, x, y, z]``. - :type x: torch.Tensor - :return: The output tensor obtained from the - spectral convolution of size - ``[batch, output_numb_fields, x, y, z]``. + Forward pass. + + :param torch.Tensor x: The input tensor. Expected of size + [``batch``, ``input_numb_fields``, ``Nx``, ``Ny``, ``Nz``]. + :return: The input tensor. Expected of size + [``batch``, ``output_numb_fields``, ``Nx``, ``Ny``, ``Nz``]. :rtype: torch.Tensor """ diff --git a/pina/model/block/stride.py b/pina/model/block/stride.py index 34f433109..2a26faf07 100644 --- a/pina/model/block/stride.py +++ b/pina/model/block/stride.py @@ -1,20 +1,20 @@ -""" -TODO: Add description -""" +"""Module for the Stride class.""" import torch class Stride: """ - TODO + Stride class for continous convolution. """ def __init__(self, dict_): - """Stride class for continous convolution + """ + Initialization of the :class:`Stride` class. - :param param: type of continuous convolution - :type param: string + :param dict dict_: Dictionary having as keys the domain size ``domain``, + the starting position of the filter ``start``, the jump size for the + filter ``jump``, and the direction of the filter ``direction``. """ self._dict_stride = dict_ @@ -22,52 +22,50 @@ def __init__(self, dict_): self._stride_discrete = self._create_stride_discrete(dict_) def _create_stride_discrete(self, my_dict): - """Creating the list for applying the filter - - :param my_dict: Dictionary with the following arguments: - domain size, starting position of the filter, jump size - for the filter and direction of the filter - :type my_dict: dict - :raises IndexError: Values in the dict must have all same length - :raises ValueError: Domain values must be greater than 0 - :raises ValueError: Direction must be either equal to 1, -1 or 0 - :raises IndexError: Direction and jumps must have zero in the same - index - :return: list of positions for the filter - :rtype: list - :Example: + """ + Create a tensor of positions where to apply the filter. + + :param dict my_dict_: Dictionary having as keys the domain size + ``domain``, the starting position of the filter ``start``, the jump + size for the filter ``jump``, and the direction of the filter + ``direction``. + :raises IndexError: Values in the dict must have all same length. + :raises ValueError: Domain values must be greater than 0. + :raises ValueError: Direction must be either equal to ``1``, ``-1`` or + ``0``. + :raises IndexError: Direction and jumps must be zero in the same index. + :return: The positions for the filter + :rtype: torch.Tensor + :Example: - >>> stride = {"domain": [4, 4], - "start": [-4, 2], - "jump": [2, 2], - "direction": [1, 1], - } - >>> create_stride(stride) - [[-4.0, 2.0], [-4.0, 4.0], [-2.0, 2.0], [-2.0, 4.0]] + >>> stride_dict = { + ... "domain": [4, 4], + ... "start": [-4, 2], + ... "jump": [2, 2], + ... "direction": [1, 1], + ... } + >>> Stride(stride_dict) """ - # we must check boundaries of the input as well - domain, start, jumps, direction = my_dict.values() # checking - if not all(len(s) == len(domain) for s in my_dict.values()): - raise IndexError("values in the dict must have all same length") + raise IndexError("Values in the dict must have all same length") if not all(v >= 0 for v in domain): - raise ValueError("domain values must be greater than 0") + raise ValueError("Domain values must be greater than 0") if not all(v in (0, -1, 1) for v in direction): - raise ValueError("direction must be either equal to 1, -1 or 0") + raise ValueError("Direction must be either equal to 1, -1 or 0") seq_jumps = [i for i, e in enumerate(jumps) if e == 0] seq_direction = [i for i, e in enumerate(direction) if e == 0] if seq_direction != seq_jumps: raise IndexError( - "direction and jumps must have zero in the same index" + "Direction and jumps must have zero in the same index" ) if seq_jumps: diff --git a/pina/model/block/utils_convolution.py b/pina/model/block/utils_convolution.py index d8e30fed9..88e0baf6c 100644 --- a/pina/model/block/utils_convolution.py +++ b/pina/model/block/utils_convolution.py @@ -1,13 +1,17 @@ -""" -TODO -""" +"""Module for utility functions for the convolutional layer.""" import torch def check_point(x, current_stride, dim): """ - TODO + Check if the point is in the current stride. + + :param torch.Tensor x: The input data. + :param int current_stride: The current stride. + :param int dim: The shape of the filter. + :return: The indeces of the points in the current stride. + :rtype: torch.Tensor """ max_stride = current_stride + dim indeces = torch.logical_and( @@ -17,13 +21,12 @@ def check_point(x, current_stride, dim): def map_points_(x, filter_position): - """Mapping function n dimensional case + """ + The mapping function for n-dimensional case. - :param x: input data of two dimension - :type x: torch.tensor - :param filter_position: position of the filter - :type dim: list[numeric] - :return: data mapped inplace + :param torch.Tensor x: The two-dimensional input data. + :param list[int] filter_position: The position of the filter. + :return: The data mapped in-place. :rtype: torch.tensor """ x.add_(-filter_position) @@ -32,14 +35,20 @@ def map_points_(x, filter_position): def optimizing(f): - """Decorator for calling a function just once + """ + Decorator to call the function only once. :param f: python function - :type f: function + :type f: Callable """ def wrapper(*args, **kwargs): + """ + Wrapper function. + :param args: The arguments of the function. + :param kwargs: The keyword arguments of the function. + """ if kwargs["type_"] == "forward": if not wrapper.has_run_inverse: wrapper.has_run_inverse = True diff --git a/pina/model/deeponet.py b/pina/model/deeponet.py index 29891fad9..6da161665 100644 --- a/pina/model/deeponet.py +++ b/pina/model/deeponet.py @@ -1,4 +1,4 @@ -"""Module for DeepONet model""" +"""Module for the DeepONet and MIONet model classes.""" from functools import partial import torch @@ -8,22 +8,18 @@ class MIONet(torch.nn.Module): """ - The PINA implementation of MIONet network. + MIONet model class. - MIONet is a general architecture for learning Operators defined - on the tensor product of Banach spaces. Unlike traditional machine - learning methods MIONet is designed to map entire functions to other - functions. It can be trained both with Physics Informed or Supervised - learning strategies. + The MIONet is a general architecture for learning operators, which map + functions to functions. It can be trained with both Supervised and + Physics-Informed learning strategies. .. seealso:: - **Original reference**: Jin, Pengzhan, Shuai Meng, and Lu Lu. + **Original reference**: Jin, P., Meng, S., and Lu L. (2022). *MIONet: Learning multiple-input operators via tensor product.* SIAM Journal on Scientific Computing 44.6 (2022): A3490-A351 - DOI: `10.1137/22M1477751 - `_ - + DOI: `10.1137/22M1477751 `_ """ def __init__( @@ -35,42 +31,44 @@ def __init__( translation=True, ): """ - :param dict networks: The neural networks to use as - models. The ``dict`` takes as key a neural network, and - as value the list of indeces to extract from the input variable - in the forward pass of the neural network. If a list of ``int`` - is passed, the corresponding columns of the inner most entries are - extracted. - If a list of ``str`` is passed the variables of the corresponding - :py:obj:`pina.label_tensor.LabelTensor`are extracted. The - ``torch.nn.Module`` model has to take as input a - :py:obj:`pina.label_tensor.LabelTensor` or :class:`torch.Tensor`. - Default implementation consist of different branch nets and one + Initialization of the :class:`MIONet` class. + + :param dict networks: The neural networks to use as models. The ``dict`` + takes as key a neural network, and as value the list of indeces to + extract from the input variable in the forward pass of the neural + network. If a ``list[int]`` is passed, the corresponding columns of + the inner most entries are extracted. If a ``list[str]`` is passed + the variables of the corresponding + :class:`~pina.label_tensor.LabelTensor` are extracted. + Each :class:`torch.nn.Module` model has to take as input either a + :class:`~pina.label_tensor.LabelTensor` or a :class:`torch.Tensor`. + Default implementation consists of several branch nets and one trunk nets. - :param str or Callable aggregator: Aggregator to be used to aggregate - partial results from the modules in `nets`. Partial results are - aggregated component-wise. Available aggregators include - sum: ``+``, product: ``*``, mean: ``mean``, min: ``min``, max: - ``max``. - :param str or Callable reduction: Reduction to be used to reduce - the aggregated result of the modules in `nets` to the desired output - dimension. Available reductions include - sum: ``+``, product: ``*``, mean: ``mean``, min: ``min``, max: - ``max``. - :param bool or Callable scale: Scaling the final output before returning - the forward pass, default ``True``. - :param bool or Callable translation: Translating the final output before - returning the forward pass, default ``True``. + :param aggregator: The aggregator to be used to aggregate component-wise + partial results from the modules in ``networks``. Available + aggregators include: sum: ``+``, product: ``*``, mean: ``mean``, + min: ``min``, max: ``max``. Default is ``*``. + :type aggregator: str or Callable + :param reduction: The reduction to be used to reduce the aggregated + result of the modules in ``networks`` to the desired output + dimension. Available reductions include: sum: ``+``, product: ``*``, + mean: ``mean``, min: ``min``, max: ``max``. Default is ``+``. + :type reduction: str or Callable + :param bool scale: If ``True``, the final output is scaled before being + returned in the forward pass. Default is ``True``. + :param bool translation: If ``True``, the final output is translated + before being returned in the forward pass. Default is ``True``. + :raises ValueError: If the passed networks have not the same output + dimension. .. warning:: - In the forward pass we do not check if the input is instance of - :py:obj:`pina.label_tensor.LabelTensor` or :class:`torch.Tensor`. - A general rule is that for a :py:obj:`pina.label_tensor.LabelTensor` - input both list of integers and list of strings can be passed for - ``input_indeces_branch_net``and ``input_indeces_trunk_net``. - Differently, for a :class:`torch.Tensor` only a list of integers - can be passed for ``input_indeces_branch_net``and - ``input_indeces_trunk_net``. + No checks are performed in the forward pass to verify if the input + is instance of either :class:`~pina.label_tensor.LabelTensor` or + :class:`torch.Tensor`. In general, in case of a + :class:`~pina.label_tensor.LabelTensor`, both a ``list[int]`` or a + ``list[str]`` can be passed as ``networks`` dict values. + Differently, in case of a :class:`torch.Tensor`, only a + ``list[int]`` can be passed as ``networks`` dict values. :Example: >>> branch_net1 = FeedForward(input_dimensons=1, @@ -162,6 +160,10 @@ def _symbol_functions(**kwargs): """ Return a dictionary of functions that can be used as aggregators or reductions. + + :param dict kwargs: Additional parameters. + :return: A dictionary of functions. + :rtype: dict """ return { "+": partial(torch.sum, **kwargs), @@ -172,6 +174,13 @@ def _symbol_functions(**kwargs): } def _init_aggregator(self, aggregator): + """ + Initialize the aggregator. + + :param aggregator: The aggregator to be used to aggregate. + :type aggregator: str or Callable + :raises ValueError: If the aggregator is not supported. + """ aggregator_funcs = self._symbol_functions(dim=2) if aggregator in aggregator_funcs: aggregator_func = aggregator_funcs[aggregator] @@ -184,6 +193,13 @@ def _init_aggregator(self, aggregator): self._aggregator_type = aggregator def _init_reduction(self, reduction): + """ + Initialize the reduction. + + :param reduction: The reduction to be used. + :type reduction: str or Callable + :raises ValueError: If the reduction is not supported. + """ reduction_funcs = self._symbol_functions(dim=-1) if reduction in reduction_funcs: reduction_func = reduction_funcs[reduction] @@ -196,6 +212,18 @@ def _init_reduction(self, reduction): self._reduction_type = reduction def _get_vars(self, x, indeces): + """ + Extract the variables from the input tensor. + + :param x: The input tensor. + :type x: LabelTensor | torch.Tensor + :param indeces: The indeces to extract. + :type indeces: list[int] | list[str] + :raises RuntimeError: If failing to extract the variables. + :raises RuntimeError: If failing to extract the right indeces. + :return: The extracted variables. + :rtype: LabelTensor | torch.Tensor + """ if isinstance(indeces[0], str): try: return x.extract(indeces) @@ -216,12 +244,12 @@ def _get_vars(self, x, indeces): def forward(self, x): """ - Defines the computation performed at every call. + Forward pass for the :class:`MIONet` model. - :param LabelTensor or torch.Tensor x: The input tensor for the forward - call. - :return: The output computed by the DeepONet model. - :rtype: LabelTensor or torch.Tensor + :param x: The input tensor. + :type x: LabelTensor | torch.Tensor + :return: The output tensor. + :rtype: LabelTensor | torch.Tensor """ # forward pass @@ -248,13 +276,19 @@ def forward(self, x): def aggregator(self): """ The aggregator function. + + :return: The aggregator function. + :rtype: str or Callable """ return self._aggregator @property def reduction(self): """ - The translation factor. + The reduction function. + + :return: The reduction function. + :rtype: str or Callable """ return self._reduction @@ -262,13 +296,19 @@ def reduction(self): def scale(self): """ The scale factor. + + :return: The scale factor. + :rtype: torch.Tensor """ return self._scale @property def translation(self): """ - The translation factor for MIONet. + The translation factor. + + :return: The translation factor. + :rtype: torch.Tensor """ return self._trasl @@ -276,6 +316,9 @@ def translation(self): def indeces_variables_extracted(self): """ The input indeces for each model in form of list. + + :return: The indeces for each model. + :rtype: list """ return self._indeces @@ -283,24 +326,27 @@ def indeces_variables_extracted(self): def model(self): """ The models in form of list. + + :return: The models. + :rtype: list[torch.nn.Module] """ return self._indeces class DeepONet(MIONet): """ - The PINA implementation of DeepONet network. + DeepONet model class. - DeepONet is a general architecture for learning Operators. Unlike - traditional machine learning methods DeepONet is designed to map - entire functions to other functions. It can be trained both with - Physics Informed or Supervised learning strategies. + The MIONet is a general architecture for learning operators, which map + functions to functions. It can be trained with both Supervised and + Physics-Informed learning strategies. .. seealso:: - **Original reference**: Lu, L., Jin, P., Pang, G. et al. *Learning - nonlinear operators via DeepONet based on the universal approximation - theorem of operator*. Nat Mach Intell 3, 218–229 (2021). + **Original reference**: Lu, L., Jin, P., Pang, G. et al. + *Learning nonlinear operators via DeepONet based on the universal + approximation theorem of operator*. + Nat Mach Intell 3, 218-229 (2021). DOI: `10.1038/s42256-021-00302-5 `_ @@ -318,42 +364,44 @@ def __init__( translation=True, ): """ + Initialization of the :class:`DeepONet` class. + :param torch.nn.Module branch_net: The neural network to use as branch - model. It has to take as input a - :py:obj:`pina.label_tensor.LabelTensor` or :class:`torch.Tensor`. - The number of dimensions of the output has to be the same of the - ``trunk_net``. + model. It has to take as input either a + :class:`~pina.label_tensor.LabelTensor` or a :class:`torch.Tensor`. + The output dimension has to be the same as that of ``trunk_net``. :param torch.nn.Module trunk_net: The neural network to use as trunk - model. It has to take as input a - :py:obj:`pina.label_tensor.LabelTensor` or :class:`torch.Tensor`. - The number of dimensions of the output has to be the same of the - ``branch_net``. - :param list(int) or list(str) input_indeces_branch_net: List of indeces - to extract from the input variable in the forward pass for the - branch net. If a list of ``int`` is passed, the corresponding - columns of the inner most entries are extracted. If a list of - ``str`` is passed the variables of the corresponding - :py:obj:`pina.label_tensor.LabelTensor` are extracted. - :param list(int) or list(str) input_indeces_trunk_net: List of indeces - to extract from the input variable in the forward pass for the - trunk net. If a list of ``int`` is passed, the corresponding columns - of the inner most entries are extracted. If a list of ``str`` is - passed the variables of the corresponding - :py:obj:`pina.label_tensor.LabelTensor` are extracted. - :param str or Callable aggregator: Aggregator to be used to aggregate - partial results from the modules in `nets`. Partial results are - aggregated component-wise. Available aggregators include - sum: ``+``, product: ``*``, mean: ``mean``, min: ``min``, - max: ``max``. - :param str or Callable reduction: Reduction to be used to reduce - the aggregated result of the modules in `nets` to the desired output - dimension. Available reductions include - sum: ``+``, product: ``*``, mean: ``mean``, min: ``min``, - max: ``max``. - :param bool or Callable scale: Scaling the final output before returning - the forward pass, default True. - :param bool or Callable translation: Translating the final output before - returning the forward pass, default True. + model. It has to take as input either a + :class:`~pina.label_tensor.LabelTensor` or a :class:`torch.Tensor`. + The output dimension has to be the same as that of ``branch_net``. + :param input_indeces_branch_net: List of indeces to extract from the + input variable of the ``branch_net``. + If a list of ``int`` is passed, the corresponding columns of the + inner most entries are extracted. If a list of ``str`` is passed the + variables of the corresponding + :class:`~pina.label_tensor.LabelTensor` are extracted. + :type input_indeces_branch_net: list[int] | list[str] + :param input_indeces_trunk_net: List of indeces to extract from the + input variable of the ``trunk_net``. + If a list of ``int`` is passed, the corresponding columns of the + inner most entries are extracted. If a list of ``str`` is passed the + variables of the corresponding + :class:`~pina.label_tensor.LabelTensor` are extracted. + :type input_indeces_trunk_net: list[int] | list[str] + :param aggregator: The aggregator to be used to aggregate component-wise + partial results from the modules in ``networks``. Available + aggregators include: sum: ``+``, product: ``*``, mean: ``mean``, + min: ``min``, max: ``max``. Default is ``*``. + :type aggregator: str or Callable + :param reduction: The reduction to be used to reduce the aggregated + result of the modules in ``networks`` to the desired output + dimension. Available reductions include: sum: ``+``, product: ``*``, + mean: ``mean``, min: ``min``, max: ``max``. Default is ``+``. + :type reduction: str or Callable + :param bool scale: If ``True``, the final output is scaled before being + returned in the forward pass. Default is ``True``. + :param bool translation: If ``True``, the final output is translated + before being returned in the forward pass. Default is ``True``. .. warning:: In the forward pass we do not check if the input is instance of @@ -365,6 +413,15 @@ def __init__( be passed for ``input_indeces_branch_net`` and ``input_indeces_trunk_net``. + .. warning:: + No checks are performed in the forward pass to verify if the input + is instance of either :class:`~pina.label_tensor.LabelTensor` or + :class:`torch.Tensor`. In general, in case of a + :class:`~pina.label_tensor.LabelTensor`, both a ``list[int]`` or a + ``list[str]`` can be passed as ``input_indeces_branch_net`` and + ``input_indeces_trunk_net``. Differently, in case of a + :class:`torch.Tensor`, only a ``list[int]`` can be passed. + :Example: >>> branch_net = FeedForward(input_dimensons=1, ... output_dimensions=10) @@ -411,25 +468,31 @@ def __init__( def forward(self, x): """ - Defines the computation performed at every call. + Forward pass for the :class:`DeepONet` model. - :param LabelTensor or torch.Tensor x: The input tensor for the forward - call. - :return: The output computed by the DeepONet model. - :rtype: LabelTensor or torch.Tensor + :param x: The input tensor. + :type x: LabelTensor | torch.Tensor + :return: The output tensor. + :rtype: LabelTensor | torch.Tensor """ return super().forward(x) @property def branch_net(self): """ - The branch net for DeepONet. + The branch net of the DeepONet. + + :return: The branch net. + :rtype: torch.nn.Module """ return self.models[0] @property def trunk_net(self): """ - The trunk net for DeepONet. + The trunk net of the DeepONet. + + :return: The trunk net. + :rtype: torch.nn.Module """ return self.models[1] diff --git a/pina/model/feed_forward.py b/pina/model/feed_forward.py index a10652d49..a1651b38b 100644 --- a/pina/model/feed_forward.py +++ b/pina/model/feed_forward.py @@ -1,4 +1,4 @@ -"""Module for FeedForward model""" +"""Module for the Feed Forward model class.""" import torch from torch import nn @@ -8,28 +8,8 @@ class FeedForward(torch.nn.Module): """ - The PINA implementation of feedforward network, also refered as multilayer - perceptron. - - :param int input_dimensions: The number of input components of the model. - Expected tensor shape of the form :math:`(*, d)`, where * - means any number of dimensions including none, and :math:`d` the - ``input_dimensions``. - :param int output_dimensions: The number of output components of the model. - Expected tensor shape of the form :math:`(*, d)`, where * - means any number of dimensions including none, and :math:`d` the - ``output_dimensions``. - :param int inner_size: number of neurons in the hidden layer(s). Default is - 20. - :param int n_layers: number of hidden layers. Default is 2. - :param torch.nn.Module func: the activation function to use. If a single - :class:`torch.nn.Module` is passed, this is used as activation function - after any layers, except the last one. If a list of Modules is passed, - they are used as activation functions at any layers, in order. - :param list(int) | tuple(int) layers: a list containing the number of - neurons for any hidden layers. If specified, the parameters ``n_layers`` - and ``inner_size`` are not considered. - :param bool bias: If ``True`` the MLP will consider some bias. + Feed Forward neural network model class, also known as Multi-layer + Perceptron. """ def __init__( @@ -42,7 +22,36 @@ def __init__( layers=None, bias=True, ): - """ """ + """ + Initialization of the :class:`FeedForward` class. + + :param int input_dimensions: The number of input components. + The expected tensor shape is :math:`(*, d)`, where * + represents any number of preceding dimensions (including none), and + :math:`d` corresponds to ``input_dimensions``. + :param int output_dimensions: The number of output components . + The expected tensor shape is :math:`(*, d)`, where * + represents any number of preceding dimensions (including none), and + :math:`d` corresponds to ``output_dimensions``. + :param int inner_size: The number of neurons for each hidden layer. + Default is ``20``. + :param int n_layers: The number of hidden layers. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param list[int] layers: The list of the dimension of inner layers. + If ``None``, ``n_layers`` of dimension ``inner_size`` are used. + Otherwise, it overrides the values passed to ``n_layers`` and + ``inner_size``. Default is ``None``. + :param bool bias: If ``True`` bias is considered for the basis function + neural network. Default is ``True``. + :raises ValueError: If the input dimension is not an integer. + :raises ValueError: If the output dimension is not an integer. + :raises RuntimeError: If the number of layers and functions are + inconsistent. + """ super().__init__() if not isinstance(input_dimensions, int): @@ -71,7 +80,7 @@ def __init__( self.functions = [func for _ in range(len(self.layers) - 1)] if len(self.layers) != len(self.functions) + 1: - raise RuntimeError("uncosistent number of layers and functions") + raise RuntimeError("Incosistent number of layers and functions") unique_list = [] for layer, func_ in zip(self.layers[:-1], self.functions): @@ -84,52 +93,31 @@ def __init__( def forward(self, x): """ - Defines the computation performed at every call. + Forward pass for the :class:`FeedForward` model. - :param x: The tensor to apply the forward pass. - :type x: torch.Tensor - :return: the output computed by the model. - :rtype: torch.Tensor + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :return: The output tensor. + :rtype: torch.Tensor | LabelTensor """ return self.model(x) class ResidualFeedForward(torch.nn.Module): """ - The PINA implementation of feedforward network, also with skipped connection - and transformer network, as presented in **Understanding and mitigating - gradient pathologies in physics-informed neural networks** + Residual Feed Forward neural network model class. + + The model is composed of a series of linear layers with a residual + connection between themm as presented in the following: .. seealso:: - **Original reference**: Wang, Sifan, Yujun Teng, and Paris Perdikaris. + **Original reference**: Wang, S., Teng, Y., and Perdikaris, P. (2021). *Understanding and mitigating gradient flow pathologies in - physics-informed neural networks*. SIAM Journal on Scientific Computing - 43.5 (2021): A3055-A3081. + physics-informed neural networks*. + SIAM Journal on Scientific Computing 43.5 (2021): A3055-A3081. DOI: `10.1137/20M1318043 `_ - - - :param int input_dimensions: The number of input components of the model. - Expected tensor shape of the form :math:`(*, d)`, where * - means any number of dimensions including none, and :math:`d` the - ``input_dimensions``. - :param int output_dimensions: The number of output components of the model. - Expected tensor shape of the form :math:`(*, d)`, where * - means any number of dimensions including none, and :math:`d` the - ``output_dimensions``. - :param int inner_size: number of neurons in the hidden layer(s). Default is - 20. - :param int n_layers: number of hidden layers. Default is 2. - :param torch.nn.Module func: the activation function to use. If a single - :class:`torch.nn.Module` is passed, this is used as activation function - after any layers, except the last one. If a list of Modules is passed, - they are used as activation functions at any layers, in order. - :param bool bias: If ``True`` the MLP will consider some bias. - :param list | tuple transformer_nets: a list or tuple containing the two - torch.nn.Module which act as transformer network. The input dimension - of the network must be the same as ``input_dimensions``, and the output - dimension must be the same as ``inner_size``. """ def __init__( @@ -142,7 +130,37 @@ def __init__( bias=True, transformer_nets=None, ): - """ """ + """ + Initialization of the :class:`ResidualFeedForward` class. + + :param int input_dimensions: The number of input components. + The expected tensor shape is :math:`(*, d)`, where * + represents any number of preceding dimensions (including none), and + :math:`d` corresponds to ``input_dimensions``. + :param int output_dimensions: The number of output components . + The expected tensor shape is :math:`(*, d)`, where * + represents any number of preceding dimensions (including none), and + :math:`d` corresponds to ``output_dimensions``. + :param int inner_size: The number of neurons for each hidden layer. + Default is ``20``. + :param int n_layers: The number of hidden layers. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param bool bias: If ``True`` bias is considered for the basis function + neural network. Default is ``True``. + :param transformer_nets: The two :class:`torch.nn.Module` acting as + transformer network. The input dimension of both networks must be + equal to ``input_dimensions``, and the output dimension must be + equal to ``inner_size``. If ``None``, two + :class:`~pina.model.block.residual.EnhancedLinear` layers are used. + Default is ``None``. + :type transformer_nets: list[torch.nn.Module] | tuple[torch.nn.Module] + :raises RuntimeError: If the number of layers and functions are + inconsistent. + """ super().__init__() # check type consistency @@ -179,7 +197,7 @@ def __init__( self.functions = [func() for _ in range(len(self.layers))] if len(self.layers) != len(self.functions): - raise RuntimeError("uncosistent number of layers and functions") + raise RuntimeError("Incosistent number of layers and functions") unique_list = [] for layer, func_ in zip(self.layers, self.functions): @@ -188,12 +206,12 @@ def __init__( def forward(self, x): """ - Defines the computation performed at every call. + Forward pass for the :class:`ResidualFeedForward` model. - :param x: The tensor to apply the forward pass. - :type x: torch.Tensor - :return: the output computed by the model. - :rtype: torch.Tensor + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :return: The output tensor. + :rtype: torch.Tensor | LabelTensor """ # enhance the input with transformer input_ = [] @@ -210,6 +228,26 @@ def forward(self, x): @staticmethod def _check_transformer_nets(transformer_nets, input_dimensions, inner_size): + """ + Check the transformer networks consistency. + + :param transformer_nets: The two :class:`torch.nn.Module` acting as + transformer network. + :type transformer_nets: list[torch.nn.Module] | tuple[torch.nn.Module] + :param int input_dimensions: The number of input components. + :param int inner_size: The number of neurons for each hidden layer. + :raises ValueError: If the passed ``transformer_nets`` is not a list of + length two. + :raises ValueError: If the passed ``transformer_nets`` is not a list of + :class:`torch.nn.Module`. + :raises ValueError: If the input dimension of the transformer network + is incompatible with the input dimension of the model. + :raises ValueError: If the output dimension of the transformer network + is incompatible with the inner size of the model. + :raises RuntimeError: If unexpected error occurs. + :return: The two :class:`torch.nn.Module` acting as transformer network. + :rtype: list[torch.nn.Module] | tuple[torch.nn.Module] + """ # check transformer nets if transformer_nets is None: transformer_nets = [ diff --git a/pina/model/fourier_neural_operator.py b/pina/model/fourier_neural_operator.py index 59578aee6..e1336c999 100644 --- a/pina/model/fourier_neural_operator.py +++ b/pina/model/fourier_neural_operator.py @@ -1,6 +1,4 @@ -""" -Fourier Neural Operator Module. -""" +"""Module for the Fourier Neural Operator model class.""" import warnings import torch @@ -13,18 +11,16 @@ class FourierIntegralKernel(torch.nn.Module): """ - Implementation of Fourier Integral Kernel network. + Fourier Integral Kernel model class. - This class implements the Fourier Integral Kernel network, which is a - PINA implementation of Fourier Neural Operator kernel network. - It performs global convolution by operating in the Fourier space. + This class implements the Fourier Integral Kernel network, which + performs global convolution in the Fourier space. .. seealso:: - **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, - K., Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. - (2020). *Fourier neural operator for parametric partial - differential equations*. + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., Liu, + B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). + *Fourier neural operator for parametric partial differential equations*. DOI: `arXiv preprint arXiv:2010.08895. `_ """ @@ -43,16 +39,31 @@ def __init__( layers=None, ): """ - :param int input_numb_fields: Number of input fields. - :param int output_numb_fields: Number of output fields. - :param int | list[int] n_modes: Number of modes. - :param int dimensions: Number of dimensions (1, 2, or 3). - :param int padding: Padding size, defaults to 8. - :param str padding_type: Type of padding, defaults to "constant". - :param int inner_size: Inner size, defaults to 20. - :param int n_layers: Number of layers, defaults to 2. - :param torch.nn.Module func: Activation function, defaults to nn.Tanh. - :param list[int] layers: List of layer sizes, defaults to None. + Initialization of the :class:`FourierIntegralKernel` class. + + :param int input_numb_fields: The number of input fields. + :param int output_numb_fields: The number of output fields. + :param n_modes: The number of modes. + :type n_modes: int | list[int] + :param int dimensions: The number of dimensions. It can be set to ``1``, + ``2``, or ``3``. Default is ``3``. + :param int padding: The padding size. Default is ``8``. + :param str padding_type: The padding strategy. Default is ``constant``. + :param int inner_size: The inner size. Default is ``20``. + :param int n_layers: The number of layers. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param list[int] layers: The list of the dimension of inner layers. + If ``None``, ``n_layers`` of dimension ``inner_size`` are used. + Otherwise, it overrides the values passed to ``n_layers`` and + ``inner_size``. Default is ``None``. + :raises RuntimeError: If the number of layers and functions are + inconsistent. + :raises RunTimeError: If the number of layers and modes are + inconsistent. """ super().__init__() @@ -84,7 +95,7 @@ def __init__( if isinstance(func, list): if len(layers) != len(func): raise RuntimeError( - "Uncosistent number of layers and functions." + "Inconsistent number of layers and functions." ) _functions = func else: @@ -96,9 +107,7 @@ def __init__( if all(isinstance(i, list) for i in n_modes) and len(layers) != len( n_modes ): - raise RuntimeError( - "Uncosistent number of layers and functions." - ) + raise RuntimeError("Inconsistent number of layers and modes.") if all(isinstance(i, int) for i in n_modes): n_modes = [n_modes] * len(layers) else: @@ -129,19 +138,17 @@ def __init__( def forward(self, x): """ - Forward computation for Fourier Neural Operator. It performs a - lifting of the input by the ``lifting_net``. Then different layers - of Fourier Blocks are applied. Finally the output is projected - to the final dimensionality by the ``projecting_net``. - - :param torch.Tensor x: The input tensor for fourier block, - depending on ``dimension`` in the initialization. - In particular it is expected: + Forward pass for the :class:`FourierIntegralKernel` model. + :param x: The input tensor for performing the computation. Depending + on the ``dimensions`` in the initialization, it expects a tensor + with the following shapes: * 1D tensors: ``[batch, X, channels]`` * 2D tensors: ``[batch, X, Y, channels]`` * 3D tensors: ``[batch, X, Y, Z, channels]`` - :return: The output tensor obtained from the kernels convolution. + :type x: torch.Tensor | LabelTensor + :raises Warning: If a LabelTensor is passed as input. + :return: The output tensor. :rtype: torch.Tensor """ if isinstance(x, LabelTensor): @@ -181,6 +188,22 @@ def _check_consistency( layers, n_modes, ): + """ + Check the consistency of the input parameters. + + + :param int dimensions: The number of dimensions. + :param int padding: The padding size. + :param str padding_type: The padding strategy. + :param int inner_size: The inner size. + :param int n_layers: The number of layers. + :param func: The activation function. + :type func: torch.nn.Module | list[torch.nn.Module] + :param list[int] layers: The list of the dimension of inner layers. + :param n_modes: The number of modes. + :type n_modes: int | list[int] + :raises ValueError: If the input is not consistent. + """ check_consistency(dimensions, int) check_consistency(padding, int) check_consistency(padding_type, str) @@ -201,6 +224,15 @@ def _check_consistency( @staticmethod def _get_fourier_block(dimensions): + """ + Retrieve the Fourier Block class based on the number of dimensions. + + :param int dimensions: The number of dimensions. + :raises NotImplementedError: If the number of dimensions is not 1, 2, + or 3. + :return: The Fourier Block class. + :rtype: FourierBlock1D | FourierBlock2D | FourierBlock3D + """ if dimensions == 1: return FourierBlock1D if dimensions == 2: @@ -212,20 +244,18 @@ def _get_fourier_block(dimensions): class FNO(KernelNeuralOperator): """ - The PINA implementation of Fourier Neural Operator network. + Fourier Neural Operator model class. - Fourier Neural Operator (FNO) is a general architecture for - learning Operators. Unlike traditional machine learning methods - FNO is designed to map entire functions to other functions. It - can be trained with Supervised learning strategies. FNO does global - convolution by performing the operation on the Fourier space. + The Fourier Neural Operator (FNO) is a general architecture for learning + operators, which map functions to functions. It can be trained both with + Supervised and Physics_Informed learning strategies. The Fourier Neural + Operator performs global convolution in the Fourier space. .. seealso:: - **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, - K., Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. - (2020). *Fourier neural operator for parametric partial - differential equations*. + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., + Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2020). + *Fourier neural operator for parametric partial differential equations*. DOI: `arXiv preprint arXiv:2010.08895. `_ """ @@ -244,18 +274,27 @@ def __init__( layers=None, ): """ - :param torch.nn.Module lifting_net: The neural network for lifting - the input. - :param torch.nn.Module projecting_net: The neural network for - projecting the output. - :param int | list[int] n_modes: Number of modes. - :param int dimensions: Number of dimensions (1, 2, or 3). - :param int padding: Padding size, defaults to 8. - :param str padding_type: Type of padding, defaults to `constant`. - :param int inner_size: Inner size, defaults to 20. - :param int n_layers: Number of layers, defaults to 2. - :param torch.nn.Module func: Activation function, defaults to nn.Tanh. - :param list[int] layers: List of layer sizes, defaults to None. + :param torch.nn.Module lifting_net: The lifting neural network mapping + the input to its hidden dimension. + :param torch.nn.Module projecting_net: The projection neural network + mapping the hidden representation to the output function. + :param n_modes: The number of modes. + :type n_modes: int | list[int] + :param int dimensions: The number of dimensions. It can be set to ``1``, + ``2``, or ``3``. Default is ``3``. + :param int padding: The padding size. Default is ``8``. + :param str padding_type: The padding strategy. Default is ``constant``. + :param int inner_size: The inner size. Default is ``20``. + :param int n_layers: The number of layers. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param list[int] layers: The list of the dimension of inner layers. + If ``None``, ``n_layers`` of dimension ``inner_size`` are used. + Otherwise, it overrides the values passed to ``n_layers`` and + ``inner_size``. Default is ``None``. """ lifting_operator_out = lifting_net( torch.rand(size=next(lifting_net.parameters()).size()) @@ -279,19 +318,23 @@ def __init__( def forward(self, x): """ - Forward computation for Fourier Neural Operator. It performs a - lifting of the input by the ``lifting_net``. Then different layers - of Fourier Blocks are applied. Finally the output is projected - to the final dimensionality by the ``projecting_net``. + Forward pass for the :class:`FourierNeuralOperator` model. - :param torch.Tensor x: The input tensor for fourier block, - depending on ``dimension`` in the initialization. In - particular it is expected: + The ``lifting_net`` maps the input to the hidden dimension. + Then, several layers of Fourier blocks are applied. Finally, the + ``projection_net`` maps the hidden representation to the output + function. + + :param x: The input tensor for performing the computation. Depending + on the ``dimensions`` in the initialization, it expects a tensor + with the following shapes: * 1D tensors: ``[batch, X, channels]`` * 2D tensors: ``[batch, X, Y, channels]`` * 3D tensors: ``[batch, X, Y, Z, channels]`` - :return: The output tensor obtained from FNO. + + :type x: torch.Tensor | LabelTensor + :return: The output tensor. :rtype: torch.Tensor """ diff --git a/pina/model/graph_neural_operator.py b/pina/model/graph_neural_operator.py index b0233f1a6..3cb5cdd31 100644 --- a/pina/model/graph_neural_operator.py +++ b/pina/model/graph_neural_operator.py @@ -1,6 +1,4 @@ -""" -Module for the Graph Neural Operator and Graph Neural Kernel. -""" +"""Module for the Graph Neural Operator model class.""" import torch from torch.nn import Tanh @@ -10,7 +8,17 @@ class GraphNeuralKernel(torch.nn.Module): """ - TODO add docstring + Graph Neural Operator kernel model class. + + This class implements the Graph Neural Operator kernel network. + + .. seealso:: + + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., + Liu, B., Bhattacharya, K., Stuart, A., Anandkumar, A. (2020). + *Neural Operator: Graph Kernel Network for Partial Differential + Equations*. + DOI: `arXiv preprint arXiv:2003.03485 `_ """ def __init__( @@ -26,28 +34,24 @@ def __init__( shared_weights=False, ): """ - The Graph Neural Kernel constructor. - - :param width: The width of the kernel. - :type width: int - :param edge_features: The number of edge features. - :type edge_features: int - :param n_layers: The number of kernel layers. - :type n_layers: int - :param internal_n_layers: The number of layers the FF Neural Network - internal to each Kernel Layer. - :type internal_n_layers: int - :param internal_layers: Number of neurons of hidden layers(s) in the - FF Neural Network inside for each Kernel Layer. - :type internal_layers: list | tuple - :param internal_func: The activation function used inside the - computation of the representation of the edge features in the - Graph Integral Layer. - :param external_func: The activation function applied to the output of - the Graph Integral Layer. - :type external_func: torch.nn.Module - :param shared_weights: If ``True`` the weights of the Graph Integral - Layers are shared. + Initialization of the :class:`GraphNeuralKernel` class. + + :param int width: The width of the kernel. + :param int edge_features: The number of edge features. + :param int n_layers: The number of kernel layers. Default is ``2``. + :param int internal_n_layers: The number of layers of the neural network + inside each kernel layer. Default is ``0``. + :param internal_layers: The number of neurons for each layer of the + neural network inside each kernel layer. Default is ``None``. + :type internal_layers: list[int] | tuple[int] + :param torch.nn.Module internal_func: The activation function used + inside each kernel layer. If ``None``, it uses the + :class:`torch.nn.Tanh` activation. Default is ``None``. + :param torch.nn.Module external_func: The activation function applied to + the output of the each kernel layer. If ``None``, it uses the + :class:`torch.nn.Tanh` activation. Default is ``None``. + :param bool shared_weights: If ``True``, the weights of each kernel + layer are shared. Default is ``False``. """ super().__init__() if external_func is None: @@ -85,11 +89,33 @@ def __init__( self._forward_func = self._forward_unshared def _forward_unshared(self, x, edge_index, edge_attr): + """ + Forward pass for the Graph Neural Kernel with unshared weights. + + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :param torch.Tensor edge_index: The edge index. + :param edge_attr: The edge attributes. + :type edge_attr: torch.Tensor | LabelTensor + :return: The output tensor. + :rtype: torch.Tensor + """ for layer in self.layers: x = layer(x, edge_index, edge_attr) return x def _forward_shared(self, x, edge_index, edge_attr): + """ + Forward pass for the Graph Neural Kernel with shared weights. + + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :param torch.Tensor edge_index: The edge index. + :param edge_attr: The edge attributes. + :type edge_attr: torch.Tensor | LabelTensor + :return: The output tensor. + :rtype: torch.Tensor + """ for _ in range(self.n_layers): x = self.layers(x, edge_index, edge_attr) return x @@ -98,19 +124,34 @@ def forward(self, x, edge_index, edge_attr): """ The forward pass of the Graph Neural Kernel. - :param x: The input batch. - :type x: torch.Tensor - :param edge_index: The edge index. - :type edge_index: torch.Tensor + :param x: The input tensor. + :type x: torch.Tensor | LabelTensor + :param torch.Tensor edge_index: The edge index. :param edge_attr: The edge attributes. - :type edge_attr: torch.Tensor + :type edge_attr: torch.Tensor | LabelTensor + :return: The output tensor. + :rtype: torch.Tensor """ return self._forward_func(x, edge_index, edge_attr) class GraphNeuralOperator(KernelNeuralOperator): """ - TODO add docstring + Graph Neural Operator model class. + + The Graph Neural Operator is a general architecture for learning operators, + which map functions to functions. It can be trained both with Supervised + and Physics-Informed learning strategies. The Graph Neural Operator performs + graph convolution by means of a Graph Neural Kernel. + + .. seealso:: + + **Original reference**: Li, Z., Kovachki, N., Azizzadenesheli, K., + Liu, B., Bhattacharya, K., Stuart, A., Anandkumar, A. (2020). + *Neural Operator: Graph Kernel Network for Partial Differential + Equations*. + DOI: `arXiv preprint arXiv:2003.03485. + `_ """ def __init__( @@ -127,34 +168,29 @@ def __init__( shared_weights=True, ): """ - The Graph Neural Operator constructor. - - :param lifting_operator: The lifting operator mapping the node features - to its hidden dimension. - :type lifting_operator: torch.nn.Module - :param projection_operator: The projection operator mapping the hidden - representation of the nodes features to the output function. - :type projection_operator: torch.nn.Module - :param edge_features: Number of edge features. - :type edge_features: int - :param n_layers: The number of kernel layers. - :type n_layers: int - :param internal_n_layers: The number of layers the Feed Forward Neural - Network internal to each Kernel Layer. - :type internal_n_layers: int - :param internal_layers: Number of neurons of hidden layers(s) in the - FF Neural Network inside for each Kernel Layer. - :type internal_layers: list | tuple - :param internal_func: The activation function used inside the - computation of the representation of the edge features in the - Graph Integral Layer. - :type internal_func: torch.nn.Module - :param external_func: The activation function applied to the output of - the Graph Integral Kernel. - :type external_func: torch.nn.Module - :param shared_weights: If ``True`` the weights of the Graph Integral - Layers are shared. - :type shared_weights: bool + Initialization of the :class:`GraphNeuralOperator` class. + + :param torch.nn.Module lifting_operator: The lifting neural network + mapping the input to its hidden dimension. + :param torch.nn.Module projection_operator: The projection neural + network mapping the hidden representation to the output function. + :param int edge_features: The number of edge features. + :param int n_layers: The number of kernel layers. Default is ``10``. + :param int internal_n_layers: The number of layers of the neural network + inside each kernel layer. Default is ``0``. + :param int inner_size: The size of the hidden layers of the neural + network inside each kernel layer. Default is ``None``. + :param internal_layers: The number of neurons for each layer of the + neural network inside each kernel layer. Default is ``None``. + :type internal_layers: list[int] | tuple[int] + :param torch.nn.Module internal_func: The activation function used + inside each kernel layer. If ``None``, it uses the + :class:`torch.nn.Tanh`. activation. Default is ``None``. + :param torch.nn.Module external_func: The activation function applied to + the output of the each kernel layer. If ``None``, it uses the + :class:`torch.nn.Tanh`. activation. Default is ``None``. + :param bool shared_weights: If ``True``, the weights of each kernel + layer are shared. Default is ``False``. """ if internal_func is None: @@ -182,8 +218,9 @@ def forward(self, x): """ The forward pass of the Graph Neural Operator. - :param x: The input batch. - :type x: torch_geometric.data.Batch + :param torch_geometric.data.Batch x: The input graph. + :return: The output tensor. + :rtype: torch.Tensor """ x, edge_index, edge_attr = x.x, x.edge_index, x.edge_attr x = self.lifting_operator(x) diff --git a/pina/model/kernel_neural_operator.py b/pina/model/kernel_neural_operator.py index 62e3aa0b5..e3cb790e5 100644 --- a/pina/model/kernel_neural_operator.py +++ b/pina/model/kernel_neural_operator.py @@ -1,6 +1,4 @@ -""" -Kernel Neural Operator Module. -""" +"""Module for the Kernel Neural Operator model class.""" import torch from ..utils import check_consistency @@ -8,13 +6,14 @@ class KernelNeuralOperator(torch.nn.Module): r""" - Base class for composing Neural Operators with integral kernels. + Base class for Neural Operators with integral kernels. - This is a base class for composing neural operator with multiple - integral kernels. All neural operator models defined in PINA inherit - from this class. The structure is inspired by the work of Kovachki, N. - et al. see Figure 2 of the reference for extra details. The Neural - Operators inheriting from this class can be written as: + This class serves as a foundation for building Neural Operators that + incorporate multiple integral kernels. All Neural Operator models in + PINA inherit from this class. The design follows the framework proposed + by Kovachki et al., as illustrated in Figure 2 of their work. + + Neural Operators derived from this class can be expressed as: .. math:: G_\theta := P \circ K_m \circ \cdot \circ K_1 \circ L @@ -40,15 +39,18 @@ class KernelNeuralOperator(torch.nn.Module): **Original reference**: Kovachki, N., Li, Z., Liu, B., Azizzadenesheli, K., Bhattacharya, K., Stuart, A., & Anandkumar, A. - (2023). *Neural operator: Learning maps between function - spaces with applications to PDEs*. Journal of Machine Learning - Research, 24(89), 1-97. + (2023). + *Neural operator: Learning maps between function spaces with + applications to PDEs*. + Journal of Machine Learning Research, 24(89), 1-97. """ def __init__(self, lifting_operator, integral_kernels, projection_operator): """ - :param torch.nn.Module lifting_operator: The lifting operator - mapping the input to its hidden dimension. + Initialization of the :class:`KernelNeuralOperator` class. + + :param torch.nn.Module lifting_operator: The lifting operator mapping + the input to its hidden dimension. :param torch.nn.Module integral_kernels: List of integral kernels mapping each hidden representation to the next one. :param torch.nn.Module projection_operator: The projection operator @@ -64,16 +66,19 @@ def __init__(self, lifting_operator, integral_kernels, projection_operator): @property def lifting_operator(self): """ - The lifting operator property. + The lifting operator module. + + :return: The lifting operator module. + :rtype: torch.nn.Module """ return self._lifting_operator @lifting_operator.setter def lifting_operator(self, value): """ - The lifting operator setter + Set the lifting operator module. - :param torch.nn.Module value: The lifting operator torch module. + :param torch.nn.Module value: The lifting operator module. """ check_consistency(value, torch.nn.Module) self._lifting_operator = value @@ -81,16 +86,19 @@ def lifting_operator(self, value): @property def projection_operator(self): """ - The projection operator property. + The projection operator module. + + :return: The projection operator module. + :rtype: torch.nn.Module """ return self._projection_operator @projection_operator.setter def projection_operator(self, value): """ - The projection operator setter + Set the projection operator module. - :param torch.nn.Module value: The projection operator torch module. + :param torch.nn.Module value: The projection operator module. """ check_consistency(value, torch.nn.Module) self._projection_operator = value @@ -98,37 +106,41 @@ def projection_operator(self, value): @property def integral_kernels(self): """ - The integral kernels operator property. + The integral kernels operator module. + + :return: The integral kernels operator module. + :rtype: torch.nn.Module """ return self._integral_kernels @integral_kernels.setter def integral_kernels(self, value): """ - The integral kernels operator setter + Set the integral kernels operator module. - :param torch.nn.Module value: The integral kernels operator torch - module. + :param torch.nn.Module value: The integral kernels operator module. """ check_consistency(value, torch.nn.Module) self._integral_kernels = value def forward(self, x): r""" - Forward computation for Base Neural Operator. It performs a - lifting of the input by the ``lifting_operator``. - Then different layers integral kernels are applied using - ``integral_kernels``. Finally the output is projected - to the final dimensionality by the ``projection_operator``. - - :param torch.Tensor x: The input tensor for performing the - computation. It expects a tensor :math:`B \times N \times D`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem. In particular - :math:`D` is the number of spatial/paramtric/temporal variables - plus the field variables. For example for 2D problems with 2 - output\ variables :math:`D=4`. - :return: The output tensor obtained from the NO. + Forward pass for the :class:`KernelNeuralOperator` model. + + The ``lifting_operator`` maps the input to the hidden dimension. + The ``integral_kernels`` apply the integral kernels to the hidden + representation. The ``projection_operator`` maps the hidden + representation to the output function. + + :param x: The input tensor for performing the computation. It expects + a tensor :math:`B \times N \times D`, where :math:`B` is the + batch_size, :math:`N` the number of points in the mesh, and + :math:`D` the dimension of the problem. In particular, :math:`D` + is the number of spatial, parametric, and/or temporal variables + plus the field variables. For instance, for 2D problems with 2 + output variables, :math:`D=4`. + :type x: torch.Tensor | LabelTensor + :return: The output tensor. :rtype: torch.Tensor """ x = self.lifting_operator(x) diff --git a/pina/model/layers/__init__.py b/pina/model/layers/__init__.py index 2faa66ad5..aeef265c9 100644 --- a/pina/model/layers/__init__.py +++ b/pina/model/layers/__init__.py @@ -1,6 +1,4 @@ -""" -Old layers module, deprecated in 0.2.0. -""" +"""Old layers module, deprecated in 0.2.0.""" import warnings diff --git a/pina/model/low_rank_neural_operator.py b/pina/model/low_rank_neural_operator.py index 376b8a907..1a7082dff 100644 --- a/pina/model/low_rank_neural_operator.py +++ b/pina/model/low_rank_neural_operator.py @@ -1,4 +1,4 @@ -"""Module LowRank Neural Operator.""" +"""Module for the Low Rank Neural Operator model class.""" import torch from torch import nn @@ -11,23 +11,20 @@ class LowRankNeuralOperator(KernelNeuralOperator): """ - Implementation of LowRank Neural Operator. + Low Rank Neural Operator model class. - LowRank Neural Operator is a general architecture for - learning Operators. Unlike traditional machine learning methods - LowRankNeuralOperator is designed to map entire functions - to other functions. It can be trained with Supervised or PINN based - learning strategies. - LowRankNeuralOperator does convolution by performing a low rank - approximation, see :class:`~pina.model.block.lowrank_layer.LowRankBlock`. + The Low Rank Neural Operator is a general architecture for learning + operators, which map functions to functions. It can be trained both with + Supervised and Physics-Informed learning strategies. The Low Rank Neural + Operator performs convolution by means of a low rank approximation. .. seealso:: - **Original reference**: Kovachki, N., Li, Z., Liu, B., - Azizzadenesheli, K., Bhattacharya, K., Stuart, A., & Anandkumar, A. - (2023). *Neural operator: Learning maps between function - spaces with applications to PDEs*. Journal of Machine Learning - Research, 24(89), 1-97. + **Original reference**: Kovachki, N., Li, Z., Liu, B., Azizzadenesheli, + K., Bhattacharya, K., Stuart, A., & Anandkumar, A. (2023). + *Neural operator: Learning maps between function spaces with + applications to PDEs*. + Journal of Machine Learning Research, 24(89), 1-97. """ def __init__( @@ -44,32 +41,36 @@ def __init__( bias=True, ): """ - :param torch.nn.Module lifting_net: The neural network for lifting - the input. It must take as input the input field and the coordinates - at which the input field is avaluated. The output of the lifting - net is chosen as embedding dimension of the problem - :param torch.nn.Module projecting_net: The neural network for - projecting the output. It must take as input the embedding dimension - (output of the ``lifting_net``) plus the dimension - of the coordinates. - :param list[str] field_indices: the label of the fields - in the input tensor. - :param list[str] coordinates_indices: the label of the - coordinates in the input tensor. - :param int n_kernel_layers: number of hidden kernel layers. - Default is 4. - :param int inner_size: Number of neurons in the hidden layer(s) for the - basis function network. Default is 20. - :param int n_layers: Number of hidden layers. for the - basis function network. Default is 2. - :param func: The activation function to use for the - basis function network. If a single - :class:`torch.nn.Module` is passed, this is used as - activation function after any layers, except the last one. - If a list of Modules is passed, - they are used as activation functions at any layers, in order. - :param bool bias: If ``True`` the MLP will consider some bias for the - basis function network. + Initialization of the :class:`LowRankNeuralOperator` class. + + :param torch.nn.Module lifting_net: The lifting neural network mapping + the input to its hidden dimension. It must take as input the input + field and the coordinates at which the input field is evaluated. + :param torch.nn.Module projecting_net: The projection neural network + mapping the hidden representation to the output function. It must + take as input the embedding dimension plus the dimension of the + coordinates. + :param list[str] field_indices: The labels of the fields in the input + tensor. + :param list[str] coordinates_indices: The labels of the coordinates in + the input tensor. + :param int n_kernel_layers: The number of hidden kernel layers. + :param int rank: The rank of the low rank approximation. + :param int inner_size: The number of neurons for each hidden layer in + the basis function neural network. Default is ``20``. + :param int n_layers: The number of hidden layers in the basis function + neural network. Default is ``2``. + :param func: The activation function. If a list is passed, it must have + the same length as ``n_layers``. If a single function is passed, it + is used for all layers, except for the last one. + Default is :class:`torch.nn.Tanh`. + :type func: torch.nn.Module | list[torch.nn.Module] + :param bool bias: If ``True`` bias is considered for the basis function + neural network. Default is ``True``. + :raises ValueError: If the input dimension does not match with the + labels of the fields and coordinates. + :raises ValueError: If the input dimension of the projecting network + does not match with the hidden dimension of the lifting network. """ # check consistency @@ -122,19 +123,20 @@ def __init__( def forward(self, x): r""" - Forward computation for LowRank Neural Operator. It performs a - lifting of the input by the ``lifting_net``. Then different layers - of LowRank Neural Operator Blocks are applied. - Finally the output is projected to the final dimensionality - by the ``projecting_net``. - - :param torch.Tensor x: The input tensor for fourier block, - depending on ``dimension`` in the initialization. It expects - a tensor :math:`B \times N \times D`, - where :math:`B` is the batch_size, :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem, i.e. the sum - of ``len(coordinates_indices)+len(field_indices)``. - :return: The output tensor obtained from Average Neural Operator. + Forward pass for the :class:`LowRankNeuralOperator` model. + + The ``lifting_net`` maps the input to the hidden dimension. + Then, several layers of + :class:`~pina.model.block.low_rank_block.LowRankBlock` are + applied. Finally, the ``projecting_net`` maps the hidden representation + to the output function. + + :param LabelTensor x: The input tensor for performing the computation. + It expects a tensor :math:`B \times N \times D`, where :math:`B` is + the batch_size, :math:`N` the number of points in the mesh, + :math:`D` the dimension of the problem, i.e. the sum + of ``len(coordinates_indices)`` and ``len(field_indices)``. + :return: The output tensor. :rtype: torch.Tensor """ # extract points diff --git a/pina/model/multi_feed_forward.py b/pina/model/multi_feed_forward.py index 7fdbf31c4..f2f149ca6 100644 --- a/pina/model/multi_feed_forward.py +++ b/pina/model/multi_feed_forward.py @@ -1,4 +1,4 @@ -"""Module for Multi FeedForward model""" +"""Module for the Multi Feed Forward model class.""" from abc import ABC, abstractmethod import torch @@ -7,16 +7,21 @@ class MultiFeedForward(torch.nn.Module, ABC): """ - The PINA implementation of MultiFeedForward network. + Multi Feed Forward neural network model class. - This model allows to create a network with multiple FeedForward combined - together. The user has to define the `forward` method choosing how to - combine the different FeedForward networks. - - :param dict ffn_dict: dictionary of FeedForward networks. + This model allows to create a network with multiple Feed Forward neural + networks combined together. The user is required to define the ``forward`` + method to choose how to combine the networks. """ def __init__(self, ffn_dict): + """ + Initialization of the :class:`MultiFeedForward` class. + + :param dict ffn_dict: A dictionary containing the Feed Forward neural + networks to be combined. + :raises TypeError: If the input is not a dictionary. + """ super().__init__() if not isinstance(ffn_dict, dict): @@ -28,5 +33,8 @@ def __init__(self, ffn_dict): @abstractmethod def forward(self, *args, **kwargs): """ - TODO: Docstring + Forward pass for the :class:`MultiFeedForward` model. + + The user is required to define this method to choose how to combine the + networks. """ diff --git a/pina/model/spline.py b/pina/model/spline.py index 36596901f..c22c7937c 100644 --- a/pina/model/spline.py +++ b/pina/model/spline.py @@ -1,19 +1,26 @@ -"""Module for Spline model""" +"""Module for the Spline model class.""" import torch from ..utils import check_consistency class Spline(torch.nn.Module): - """TODO: Docstring for Spline.""" + """ + Spline model class. + """ def __init__(self, order=4, knots=None, control_points=None) -> None: """ - Spline model. - - :param int order: the order of the spline. - :param torch.Tensor knots: the knot vector. - :param torch.Tensor control_points: the control points. + Initialization of the :class:`Spline` class. + + :param int order: The order of the spline. Default is ``4``. + :param torch.Tensor knots: The tensor representing knots. If ``None``, + the knots will be initialized automatically. Default is ``None``. + :param torch.Tensor control_points: The control points. Default is + ``None``. + :raises ValueError: If the order is negative. + :raises ValueError: If both knots and control points are ``None``. + :raises ValueError: If the knot tensor is not one-dimensional. """ super().__init__() @@ -63,13 +70,13 @@ def __init__(self, order=4, knots=None, control_points=None) -> None: def basis(self, x, k, i, t): """ - Recursive function to compute the basis functions of the spline. + Recursive method to compute the basis functions of the spline. - :param torch.Tensor x: points to be evaluated. - :param int k: spline degree - :param int i: the index of the interval - :param torch.Tensor t: vector of knots - :return: the basis functions evaluated at x + :param torch.Tensor x: The points to be evaluated. + :param int k: The spline degree. + :param int i: The index of the interval. + :param torch.Tensor t: The tensor of knots. + :return: The basis functions evaluated at x :rtype: torch.Tensor """ @@ -100,11 +107,23 @@ def basis(self, x, k, i, t): @property def control_points(self): - """TODO: Docstring for control_points.""" + """ + The control points of the spline. + + :return: The control points. + :rtype: torch.Tensor + """ return self._control_points @control_points.setter def control_points(self, value): + """ + Set the control points of the spline. + + :param value: The control points. + :type value: torch.Tensor | dict + :raises ValueError: If invalid value is passed. + """ if isinstance(value, dict): if "n" not in value: raise ValueError("Invalid value for control_points") @@ -118,11 +137,23 @@ def control_points(self, value): @property def knots(self): - """TODO: Docstring for knots.""" + """ + The knots of the spline. + + :return: The knots. + :rtype: torch.Tensor + """ return self._knots @knots.setter def knots(self, value): + """ + Set the knots of the spline. + + :param value: The knots. + :type value: torch.Tensor | dict + :raises ValueError: If invalid value is passed. + """ if isinstance(value, dict): type_ = value.get("type", "auto") @@ -152,10 +183,10 @@ def knots(self, value): def forward(self, x): """ - Forward pass of the spline model. + Forward pass for the :class:`Spline` model. - :param torch.Tensor x: points to be evaluated. - :return: the spline evaluated at x + :param torch.Tensor x: The input tensor. + :return: The output tensor. :rtype: torch.Tensor """ t = self.knots diff --git a/pina/operator.py b/pina/operator.py index 32c565851..68e2cb409 100644 --- a/pina/operator.py +++ b/pina/operator.py @@ -1,11 +1,15 @@ """ -Module for operator vectorize implementation. Differential operator are used to -write any differential problem. These operator are implemented to work on -different accellerators: CPU, GPU, TPU or MPS. All operator take as input a -tensor onto which computing the operator, a tensor with respect to which -computing the operator, the name of the output variables to calculate the -operator for (in case of multidimensional functions), and the variables name -on which the operator is calculated. +Module for vectorized differential operators implementation. + +Differential operators are used to define differential problems and are +implemented to run efficiently on various accelerators, including CPU, GPU, TPU, +and MPS. + +Each differential operator takes the following inputs: +- A tensor on which the operator is applied. +- A tensor with respect to which the operator is computed. +- The names of the output variables for which the operator is evaluated. +- The names of the variables with respect to which the operator is computed. """ import torch @@ -14,39 +18,44 @@ def grad(output_, input_, components=None, d=None): """ - Perform gradient operation. The operator works for vectorial and scalar - functions, with multiple input coordinates. - - :param LabelTensor output_: the output tensor onto which computing the - gradient. - :param LabelTensor input_: the input tensor with respect to which computing - the gradient. - :param list(str) components: the name of the output variables to calculate - the gradient for. It should be a subset of the output labels. If None, - all the output variables are considered. Default is None. - :param list(str) d: the name of the input variables on which the gradient is - calculated. d should be a subset of the input labels. If None, all the - input variables are considered. Default is None. - - :return: the gradient tensor. + Compute the gradient of the ``output_`` with respect to the ``input``. + + This operator supports both vector-valued and scalar-valued functions with + one or multiple input coordinates. + + :param LabelTensor output_: The output tensor on which the gradient is + computed. + :param LabelTensor input_: The input tensor with respect to which the + gradient is computed. + :param list[str] components: The names of the output variables for which to + compute the gradient. It must be a subset of the output labels. + If ``None``, all output variables are considered. Default is ``None``. + :param list[str] d: The names of the input variables with respect to which + the gradient is computed. It must be a subset of the input labels. + If ``None``, all input variables are considered. Default is ``None``. + :raises TypeError: If the input tensor is not a LabelTensor. + :raises RuntimeError: If the output is a scalar field and the components + are not equal to the output labels. + :raises NotImplementedError: If the output is neither a vector field nor a + scalar field. + :return: The computed gradient tensor. :rtype: LabelTensor """ def grad_scalar_output(output_, input_, d): """ - Perform gradient operation for a scalar output. - - :param LabelTensor output_: the output tensor onto which computing the - gradient. It has to be a column tensor. - :param LabelTensor input_: the input tensor with respect to which - computing the gradient. - :param list(str) d: the name of the input variables on which the - gradient is calculated. d should be a subset of the input labels. If - None, all the input variables are considered. Default is None. - - :raises RuntimeError: a vectorial function is passed. - :raises RuntimeError: missing derivative labels. - :return: the gradient tensor. + Compute the gradient of a scalar-valued ``output_``. + + :param LabelTensor output_: The output tensor on which the gradient is + computed. It must be a column tensor. + :param LabelTensor input_: The input tensor with respect to which the + gradient is computed. + :param list[str] d: The names of the input variables with respect to + which the gradient is computed. It must be a subset of the input + labels. If ``None``, all input variables are considered. + :raises RuntimeError: If a vectorial function is passed. + :raises RuntimeError: If missing derivative labels. + :return: The computed gradient tensor. :rtype: LabelTensor """ @@ -102,25 +111,26 @@ def grad_scalar_output(output_, input_, d): def div(output_, input_, components=None, d=None): """ - Perform divergence operation. The operator works for vectorial functions, - with multiple input coordinates. - - :param LabelTensor output_: the output tensor onto which computing the - divergence. - :param LabelTensor input_: the input tensor with respect to which computing - the divergence. - :param list(str) components: the name of the output variables to calculate - the divergence for. It should be a subset of the output labels. If None, - all the output variables are considered. Default is None. - :param list(str) d: the name of the input variables on which the divergence - is calculated. d should be a subset of the input labels. If None, all - the input variables are considered. Default is None. - - :raises TypeError: div operator works only for LabelTensor. - :raises ValueError: div operator works only for vector fields. - :raises ValueError: div operator must derive all components with - respect to all coordinates. - :return: the divergenge tensor. + Compute the divergence of the ``output_`` with respect to ``input``. + + This operator supports vector-valued functions with multiple input + coordinates. + + :param LabelTensor output_: The output tensor on which the divergence is + computed. + :param LabelTensor input_: The input tensor with respect to which the + divergence is computed. + :param list[str] components: The names of the output variables for which to + compute the divergence. It must be a subset of the output labels. + If ``None``, all output variables are considered. Default is ``None``. + :param list[str] d: The names of the input variables with respect to which + the divergence is computed. It must be a subset of the input labels. + If ``None``, all input variables are considered. Default is ``None``. + :raises TypeError: If the input tensor is not a LabelTensor. + :raises ValueError: If the output is a scalar field. + :raises ValueError: If the number of components is not equal to the number + of input variables. + :return: The computed divergence tensor. :rtype: LabelTensor """ if not isinstance(input_, LabelTensor): @@ -151,42 +161,43 @@ def div(output_, input_, components=None, d=None): def laplacian(output_, input_, components=None, d=None, method="std"): """ - Compute Laplace operator. The operator works for vectorial and - scalar functions, with multiple input coordinates. - - :param LabelTensor output_: the output tensor onto which computing the - Laplacian. - :param LabelTensor input_: the input tensor with respect to which computing - the Laplacian. - :param list(str) components: the name of the output variables to calculate - the Laplacian for. It should be a subset of the output labels. If None, - all the output variables are considered. Default is None. - :param list(str) d: the name of the input variables on which the Laplacian - is calculated. d should be a subset of the input labels. If None, all - the input variables are considered. Default is None. - :param str method: used method to calculate Laplacian, defaults to 'std'. - - :raises NotImplementedError: 'divgrad' not implemented as method. - :return: The tensor containing the result of the Laplacian operator. + Compute the laplacian of the ``output_`` with respect to ``input``. + + This operator supports both vector-valued and scalar-valued functions with + one or multiple input coordinates. + + :param LabelTensor output_: The output tensor on which the laplacian is + computed. + :param LabelTensor input_: The input tensor with respect to which the + laplacian is computed. + :param list[str] components: The names of the output variables for which to + compute the laplacian. It must be a subset of the output labels. + If ``None``, all output variables are considered. Default is ``None``. + :param list[str] d: The names of the input variables with respect to which + the laplacian is computed. It must be a subset of the input labels. + If ``None``, all input variables are considered. Default is ``None``. + :param str method: The method used to compute the Laplacian. Default is + ``std``. + :raises NotImplementedError: If ``std=divgrad``. + :return: The computed laplacian tensor. :rtype: LabelTensor """ def scalar_laplace(output_, input_, components, d): """ - Compute Laplace operator for a scalar output. - - :param LabelTensor output_: the output tensor onto which computing the - Laplacian. It has to be a column tensor. - :param LabelTensor input_: the input tensor with respect to which - computing the Laplacian. - :param list(str) components: the name of the output variables to - calculate the Laplacian for. It should be a subset of the output - labels. If None, all the output variables are considered. - :param list(str) d: the name of the input variables on which the - Laplacian is computed. d should be a subset of the input labels. - If None, all the input variables are considered. Default is None. - - :return: The tensor containing the result of the Laplacian operator. + Compute the laplacian of a scalar-valued ``output_``. + + :param LabelTensor output_: The output tensor on which the laplacian is + computed. It must be a column tensor. + :param LabelTensor input_: The input tensor with respect to which the + laplacian is computed. + :param list[str] components: The names of the output variables for which + to compute the laplacian. It must be a subset of the output labels. + If ``None``, all output variables are considered. + :param list[str] d: The names of the input variables with respect to + which the laplacian is computed. It must be a subset of the input + labels. If ``None``, all input variables are considered. + :return: The computed laplacian tensor. :rtype: LabelTensor """ @@ -230,22 +241,23 @@ def scalar_laplace(output_, input_, components, d): def advection(output_, input_, velocity_field, components=None, d=None): """ - Perform advection operation. The operator works for vectorial functions, - with multiple input coordinates. - - :param LabelTensor output_: the output tensor onto which computing the - advection. - :param LabelTensor input_: the input tensor with respect to which computing - the advection. - :param str velocity_field: the name of the output variables which is used - as velocity field. It should be a subset of the output labels. - :param list(str) components: the name of the output variables to calculate - the Laplacian for. It should be a subset of the output labels. If None, - all the output variables are considered. Default is None. - :param list(str) d: the name of the input variables on which the advection - is calculated. d should be a subset of the input labels. If None, all - the input variables are considered. Default is None. - :return: the tensor containing the result of the advection operator. + Perform the advection operation on the ``output_`` with respect to the + ``input``. This operator support vector-valued functions with multiple input + coordinates. + + :param LabelTensor output_: The output tensor on which the advection is + computed. + :param LabelTensor input_: the input tensor with respect to which advection + is computed. + :param str velocity_field: The name of the output variable used as velocity + field. It must be chosen among the output labels. + :param list[str] components: The names of the output variables for which + to compute the advection. It must be a subset of the output labels. + If ``None``, all output variables are considered. Default is ``None``. + :param list[str] d: The names of the input variables with respect to which + the advection is computed. It must be a subset of the input labels. + If ``None``, all input variables are considered. Default is ``None``. + :return: The computed advection tensor. :rtype: LabelTensor """ if d is None: diff --git a/pina/operators.py b/pina/operators.py index a995d4436..cb2fb5e00 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -1,6 +1,4 @@ -""" -Old module for operators. Deprecated in 0.2.0. -""" +"""Old module for operators. Deprecated in 0.2.0.""" import warnings diff --git a/pina/optim/__init__.py b/pina/optim/__init__.py index 38301bb60..8266c8ca1 100644 --- a/pina/optim/__init__.py +++ b/pina/optim/__init__.py @@ -1,4 +1,4 @@ -"""Module for Optimizer class.""" +"""Module for the Optimizers and Schedulers.""" __all__ = [ "Optimizer", diff --git a/pina/optim/optimizer_interface.py b/pina/optim/optimizer_interface.py index d61ef4b59..5f2fbe66a 100644 --- a/pina/optim/optimizer_interface.py +++ b/pina/optim/optimizer_interface.py @@ -1,24 +1,23 @@ -"""Module for PINA Optimizer.""" +"""Module for the PINA Optimizer.""" from abc import ABCMeta, abstractmethod class Optimizer(metaclass=ABCMeta): """ - TODO - :param metaclass: _description_, defaults to ABCMeta - :type metaclass: _type_, optional + Abstract base class for defining an optimizer. All specific optimizers + should inherit form this class and implement the required methods. """ @property @abstractmethod def instance(self): """ - TODO + Abstract property to retrieve the optimizer instance. """ @abstractmethod def hook(self): """ - TODO + Abstract method to define the hook logic for the optimizer. """ diff --git a/pina/optim/scheduler_interface.py b/pina/optim/scheduler_interface.py index ddb515cd0..5ae5d8b99 100644 --- a/pina/optim/scheduler_interface.py +++ b/pina/optim/scheduler_interface.py @@ -1,25 +1,23 @@ -"""Module for PINA Scheduler.""" +"""Module for the PINA Scheduler.""" from abc import ABCMeta, abstractmethod class Scheduler(metaclass=ABCMeta): """ - TODO - - :param metaclass: _description_, defaults to ABCMeta - :type metaclass: _type_, optional + Abstract base class for defining a scheduler. All specific schedulers should + inherit form this class and implement the required methods. """ @property @abstractmethod def instance(self): """ - TODO + Abstract property to retrieve the scheduler instance. """ @abstractmethod def hook(self): """ - TODO + Abstract method to define the hook logic for the scheduler. """ diff --git a/pina/optim/torch_optimizer.py b/pina/optim/torch_optimizer.py index 74b53379b..7163c295e 100644 --- a/pina/optim/torch_optimizer.py +++ b/pina/optim/torch_optimizer.py @@ -1,4 +1,4 @@ -"""Module for PINA Torch Optimizer""" +"""Module for the PINA Torch Optimizer""" import torch @@ -8,18 +8,18 @@ class TorchOptimizer(Optimizer): """ - TODO - - :param Optimizer: _description_ - :type Optimizer: _type_ + A wrapper class for using PyTorch optimizers. """ def __init__(self, optimizer_class, **kwargs): """ - TODO + Initialization of the :class:`TorchOptimizer` class. - :param optimizer_class: _description_ - :type optimizer_class: _type_ + :param torch.optim.Optimizer optimizer_class: A + :class:`torch.optim.Optimizer` class. + :param dict kwargs: Additional parameters passed to ``optimizer_class``, + see more + `here `_. """ check_consistency(optimizer_class, torch.optim.Optimizer, subclass=True) @@ -29,10 +29,9 @@ def __init__(self, optimizer_class, **kwargs): def hook(self, parameters): """ - TODO + Initialize the optimizer instance with the given parameters. - :param parameters: _description_ - :type parameters: _type_ + :param dict parameters: The parameters of the model to be optimized. """ self._optimizer_instance = self.optimizer_class( parameters, **self.kwargs @@ -41,6 +40,9 @@ def hook(self, parameters): @property def instance(self): """ - Optimizer instance. + Get the optimizer instance. + + :return: The optimizer instance. + :rtype: torch.optim.Optimizer """ return self._optimizer_instance diff --git a/pina/optim/torch_scheduler.py b/pina/optim/torch_scheduler.py index 41c589c32..ff12300a1 100644 --- a/pina/optim/torch_scheduler.py +++ b/pina/optim/torch_scheduler.py @@ -1,4 +1,4 @@ -"""Module for PINA Torch Optimizer""" +"""Module for the PINA Torch Optimizer""" try: from torch.optim.lr_scheduler import LRScheduler # torch >= 2.0 @@ -14,18 +14,18 @@ class TorchScheduler(Scheduler): """ - TODO - - :param Scheduler: _description_ - :type Scheduler: _type_ + A wrapper class for using PyTorch schedulers. """ def __init__(self, scheduler_class, **kwargs): """ - TODO + Initialization of the :class:`TorchScheduler` class. - :param scheduler_class: _description_ - :type scheduler_class: _type_ + :param torch.optim.LRScheduler scheduler_class: A + :class:`torch.optim.LRScheduler` class. + :param dict kwargs: Additional parameters passed to ``scheduler_class``, + see more + `here _`. """ check_consistency(scheduler_class, LRScheduler, subclass=True) @@ -35,10 +35,9 @@ def __init__(self, scheduler_class, **kwargs): def hook(self, optimizer): """ - TODO + Initialize the scheduler instance with the given parameters. - :param optimizer: _description_ - :type optimizer: _type_ + :param dict parameters: The parameters of the optimizer. """ check_consistency(optimizer, Optimizer) self._scheduler_instance = self.scheduler_class( @@ -48,6 +47,9 @@ def hook(self, optimizer): @property def instance(self): """ - Scheduler instance. + Get the scheduler instance. + + :return: The scheduelr instance. + :rtype: torch.optim.LRScheduler """ return self._scheduler_instance diff --git a/pina/plotter.py b/pina/plotter.py index 263d98300..fcd4dedba 100644 --- a/pina/plotter.py +++ b/pina/plotter.py @@ -1,3 +1,3 @@ """Module for Plotter""" -raise ImportError("'pina.plotter' is deprecated and can not be imported.") +raise ImportError("'pina.plotter' is deprecated and cannot be imported.") diff --git a/pina/problem/__init__.py b/pina/problem/__init__.py index 3174082d6..e95f99703 100644 --- a/pina/problem/__init__.py +++ b/pina/problem/__init__.py @@ -1,4 +1,4 @@ -"""Module for Problems.""" +"""Module for the Problems.""" __all__ = [ "AbstractProblem", diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index 43058a6e2..5f601acff 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -1,4 +1,4 @@ -"""Module for AbstractProblem class""" +"""Module for the AbstractProblem class.""" from abc import ABCMeta, abstractmethod from copy import deepcopy @@ -11,20 +11,16 @@ class AbstractProblem(metaclass=ABCMeta): """ - The abstract `AbstractProblem` class. All the class defining a PINA Problem - should be inherited from this class. + Abstract base class for PINA problems. All specific problem types should + inherit from this class. - In the definition of a PINA problem, the fundamental elements are: - the output variables, the condition(s), and the domain(s) where the - conditions are applied. + A PINA problem is defined by key components, which typically include output + variables, conditions, and domains over which the conditions are applied. """ def __init__(self): """ - TODO - - :return: _description_ - :rtype: _type_ + Initialization of the :class:`AbstractProblem` class. """ self._discretised_domains = {} # create collector to manage problem data @@ -48,20 +44,19 @@ def __init__(self): @property def batching_dimension(self): """ - TODO + Get batching dimension. - :return: _description_ - :rtype: _type_ + :return: The batching dimension. + :rtype: int """ return self._batching_dimension @batching_dimension.setter def batching_dimension(self, value): """ - TODO + Set the batching dimension. - :return: _description_ - :rtype: _type_ + :param int value: The batching dimension. """ self._batching_dimension = value @@ -69,10 +64,11 @@ def batching_dimension(self, value): @property def input_pts(self): """ - TODO + Return a dictionary mapping condition names to their corresponding + input points. - :return: _description_ - :rtype: _type_ + :return: The input points of the problem. + :rtype: dict """ to_return = {} for cond_name, cond in self.conditions.items(): @@ -85,21 +81,21 @@ def input_pts(self): @property def discretised_domains(self): """ - TODO + Return a dictionary mapping domains to their corresponding sampled + points. - :return: _description_ - :rtype: _type_ + :return: The discretised domains. + :rtype: dict """ return self._discretised_domains def __deepcopy__(self, memo): """ - Implements deepcopy for the - :class:`~pina.problem.abstract_problem.AbstractProblem` class. + Perform a deep copy of the :class:`AbstractProblem` instance. - :param dict memo: Memory dictionary, to avoid excess copy - :return: The deep copy of the - :class:`~pina.problem.abstract_problem.AbstractProblem` class + :param dict memo: A dictionary used to track objects already copied + during the deep copy process to prevent redundant copies. + :return: A deep copy of the :class:`AbstractProblem` instance. :rtype: AbstractProblem """ cls = self.__class__ @@ -114,7 +110,7 @@ def are_all_domains_discretised(self): """ Check if all the domains are discretised. - :return: True if all the domains are discretised, False otherwise + :return: ``True`` if all domains are discretised, ``False`` otherwise. :rtype: bool """ return all( @@ -124,12 +120,10 @@ def are_all_domains_discretised(self): @property def input_variables(self): """ - The input variables of the AbstractProblem, whose type depends on the - type of domain (spatial, temporal, and parameter). - - :return: the input variables of self - :rtype: list + Get the input variables of the problem. + :return: The input variables of the problem. + :rtype: list[str] """ variables = [] @@ -144,20 +138,29 @@ def input_variables(self): @input_variables.setter def input_variables(self, variables): + """ + Set the input variables of the AbstractProblem. + + :param list[str] variables: The input variables of the problem. + :raises RuntimeError: Not implemented. + """ raise RuntimeError @property @abstractmethod def output_variables(self): """ - The output variables of the problem. + Get the output variables of the problem. """ @property @abstractmethod def conditions(self): """ - The conditions of the problem. + Get the conditions of the problem. + + :return: The conditions of the problem. + :rtype: dict """ return self.conditions @@ -165,33 +168,55 @@ def discretise_domain( self, n=None, mode="random", domains="all", sample_rules=None ): """ - Generate a set of points to span the `Location` of all the conditions of - the problem. + Discretize the problem's domains by sampling a specified number of + points according to the selected sampling mode. - :param n: Number of points to sample, see Note below - for reference. - :type n: int - :param mode: Mode for sampling, defaults to ``random``. + :param int n: The number of points to sample. + :param mode: The sampling method. Default is ``random``. Available modes include: random sampling, ``random``; latin hypercube sampling, ``latin`` or ``lh``; chebyshev sampling, ``chebyshev``; grid sampling ``grid``. - :param variables: variable(s) to sample, defaults to 'all'. - :type variables: str | list[str] - :param domains: Domain from where to sample, defaults to 'all'. + :param domains: The domains from which to sample. Default is ``all``. :type domains: str | list[str] + :param dict sample_rules: A dictionary defining custom sampling rules + for input variables. If provided, it must contain a dictionary + specifying the sampling rule for each variable, overriding the + ``n`` and ``mode`` arguments. Each key must correspond to the + input variables from + :meth:~pina.problem.AbstractProblem.input_variables, and its value + should be another dictionary with + two keys: ``n`` (number of points to sample) and ``mode`` + (sampling method). Defaults to None. + :raises RuntimeError: If both ``n`` and ``sample_rules`` are specified. + :raises RuntimeError: If neither ``n`` nor ``sample_rules`` are set. :Example: - >>> pinn.discretise_domain(n=10, mode='grid') - >>> pinn.discretise_domain(n=10, mode='grid', domain=['bound1']) - >>> pinn.discretise_domain(n=10, mode='grid', variables=['x']) + >>> problem.discretise_domain(n=10, mode='grid') + >>> problem.discretise_domain(n=10, mode='grid', domains=['gamma1']) + >>> problem.discretise_domain( + ... sample_rules={ + ... 'x': {'n': 10, 'mode': 'grid'}, + ... 'y': {'n': 100, 'mode': 'grid'} + ... }, + ... domains=['D'] + ... ) .. warning:: ``random`` is currently the only implemented ``mode`` for all - geometries, i.e. ``EllipsoidDomain``, ``CartesianDomain``, - ``SimplexDomain`` and the geometries compositions ``Union``, - ``Difference``, ``Exclusion``, ``Intersection``. The - modes ``latin`` or ``lh``, ``chebyshev``, ``grid`` are only - implemented for ``CartesianDomain``. + geometries, i.e. :class:`~pina.domain.ellipsoid.EllipsoidDomain`, + :class:`~pina.domain.cartesian.CartesianDomain`, + :class:`~pina.domain.simplex.SimplexDomain`, and geometry + compositions :class:`~pina.domain.union_domain.Union`, + :class:`~pina.domain.difference_domain.Difference`, + :class:`~pina.domain.exclusion_domain.Exclusion`, and + :class:`~pina.domain.intersection_domain.Intersection`. + The modes ``latin`` or ``lh``, ``chebyshev``, ``grid`` are only + implemented for :class:`~pina.domain.cartesian.CartesianDomain`. + + .. warning:: + If custom discretisation is applied by setting ``sample_rules`` not + to ``None``, then the discretised domain must be of class + :class:`~pina.domain.cartesian.CartesianDomain` """ # check consistecy n, mode, variables, locations @@ -218,12 +243,31 @@ def discretise_domain( raise RuntimeError("You have to specify either n or sample_rules.") def _apply_default_discretization(self, n, mode, domains): + """ + Apply default discretization to the problem's domains. + + :param int n: The number of points to sample. + :param mode: The sampling method. + :param domains: The domains from which to sample. + :type domains: str | list[str] + """ for domain in domains: self.discretised_domains[domain] = ( self.domains[domain].sample(n, mode).sort_labels() ) def _apply_custom_discretization(self, sample_rules, domains): + """ + Apply custom discretization to the problem's domains. + + :param dict sample_rules: A dictionary of custom sampling rules. + :param domains: The domains from which to sample. + :type domains: str | list[str] + :raises RuntimeError: If the keys of the sample_rules dictionary are not + the same as the input variables. + :raises RuntimeError: If custom discretisation is applied on a domain + that is not a CartesianDomain. + """ if sorted(list(sample_rules.keys())) != sorted(self.input_variables): raise RuntimeError( "The keys of the sample_rules dictionary must be the same as " @@ -247,10 +291,10 @@ def _apply_custom_discretization(self, sample_rules, domains): def add_points(self, new_points_dict): """ - Add input points to a sampled condition - :param new_points_dict: Dictionary of input points (condition_name: - LabelTensor) - :raises RuntimeError: if at least one condition is not already sampled + Add new points to an already sampled domain. + + :param dict new_points_dict: The dictionary mapping new points to their + corresponding domain. """ for k, v in new_points_dict.items(): self.discretised_domains[k] = LabelTensor.vstack( diff --git a/pina/problem/inverse_problem.py b/pina/problem/inverse_problem.py index bd7570112..231d01441 100644 --- a/pina/problem/inverse_problem.py +++ b/pina/problem/inverse_problem.py @@ -1,4 +1,4 @@ -"""Module for the ParametricProblem class""" +"""Module for the InverseProblem class.""" from abc import abstractmethod import torch @@ -7,19 +7,14 @@ class InverseProblem(AbstractProblem): """ - The class for the definition of inverse problems, i.e., problems - with unknown parameters that have to be learned during the training process - from given data. - - Here's an example of a spatial inverse ODE problem, i.e., a spatial - ODE problem with an unknown parameter `alpha` as coefficient of the - derivative term. - - :Example: - TODO + Class for defining inverse problems, where the objective is to determine + unknown parameters through training, based on given data. """ def __init__(self): + """ + Initialization of the :class:`InverseProblem` class. + """ super().__init__() # storing unknown_parameters for optimization self.unknown_parameters = {} @@ -33,23 +28,34 @@ def __init__(self): @abstractmethod def unknown_parameter_domain(self): """ - The parameters' domain of the problem. + The domain of the unknown parameters of the problem. """ @property def unknown_variables(self): """ - The parameters of the problem. + Get the unknown variables of the problem. + + :return: The unknown variables of the problem. + :rtype: list[str] """ return self.unknown_parameter_domain.variables @property def unknown_parameters(self): """ - The parameters of the problem. + Get the unknown parameters of the problem. + + :return: The unknown parameters of the problem. + :rtype: torch.nn.Parameter """ return self.__unknown_parameters @unknown_parameters.setter def unknown_parameters(self, value): + """ + Set the unknown parameters of the problem. + + :param torch.nn.Parameter value: The unknown parameters of the problem. + """ self.__unknown_parameters = value diff --git a/pina/problem/parametric_problem.py b/pina/problem/parametric_problem.py index e12c42ef1..e361074b3 100644 --- a/pina/problem/parametric_problem.py +++ b/pina/problem/parametric_problem.py @@ -1,4 +1,4 @@ -"""Module for the ParametricProblem class""" +"""Module for the ParametricProblem class.""" from abc import abstractmethod @@ -7,26 +7,23 @@ class ParametricProblem(AbstractProblem): """ - The class for the definition of parametric problems, i.e., problems - with parameters among the input variables. - - Here's an example of a spatial parametric ODE problem, i.e., a spatial - ODE problem with an additional parameter `alpha` as coefficient of the - derivative term. - - :Example: - TODO + Class for defining parametric problems, where certain input variables are + treated as parameters that can vary, allowing the model to adapt to + different scenarios based on the chosen parameters. """ @abstractmethod def parameter_domain(self): """ - The parameters' domain of the problem. + The domain of the parameters of the problem. """ @property def parameters(self): """ - The parameters' variables of the problem. + Get the parameters of the problem. + + :return: The parameters of the problem. + :rtype: list[str] """ return self.parameter_domain.variables diff --git a/pina/problem/spatial_problem.py b/pina/problem/spatial_problem.py index 1e5434e65..608e31691 100644 --- a/pina/problem/spatial_problem.py +++ b/pina/problem/spatial_problem.py @@ -1,4 +1,4 @@ -"""Module for the SpatialProblem class""" +"""Module for the SpatialProblem class.""" from abc import abstractmethod @@ -7,13 +7,8 @@ class SpatialProblem(AbstractProblem): """ - The class for the definition of spatial problems, i.e., for problems - with spatial input variables. - - Here's an example of a spatial 1-dimensional ODE problem. - - :Example: - TODO + Class for defining spatial problems, where the problem domain is defined in + terms of spatial variables. """ @abstractmethod @@ -25,6 +20,9 @@ def spatial_domain(self): @property def spatial_variables(self): """ - The spatial input variables of the problem. + Get the spatial input variables of the problem. + + :return: The spatial input variables of the problem. + :rtype: list[str] """ return self.spatial_domain.variables diff --git a/pina/problem/time_dependent_problem.py b/pina/problem/time_dependent_problem.py index 3d06b689b..ea2ad7d54 100644 --- a/pina/problem/time_dependent_problem.py +++ b/pina/problem/time_dependent_problem.py @@ -1,4 +1,4 @@ -"""Module for the TimeDependentProblem class""" +"""Module for the TimeDependentProblem class.""" from abc import abstractmethod @@ -7,13 +7,8 @@ class TimeDependentProblem(AbstractProblem): """ - The class for the definition of time-dependent problems, i.e., for problems - depending on time. - - Here's an example of a 1D wave problem. - - :Example: - TODO + Class for defining time-dependent problems, where the system's behavior + changes with respect to time. """ @abstractmethod @@ -25,6 +20,9 @@ def temporal_domain(self): @property def temporal_variable(self): """ - The time variable of the problem. + Get the time variable of the problem. + + :return: The time variable of the problem. + :rtype: list[str] """ return self.temporal_domain.variables diff --git a/pina/problem/zoo/__init__.py b/pina/problem/zoo/__init__.py index 6e3d58e52..e129c2cb3 100644 --- a/pina/problem/zoo/__init__.py +++ b/pina/problem/zoo/__init__.py @@ -1,4 +1,4 @@ -"""TODO""" +"""Module for implemented problems.""" __all__ = [ "SupervisedProblem", diff --git a/pina/problem/zoo/advection.py b/pina/problem/zoo/advection.py index 32c6afe78..a2e801562 100644 --- a/pina/problem/zoo/advection.py +++ b/pina/problem/zoo/advection.py @@ -16,7 +16,7 @@ class AdvectionEquation(Equation): def __init__(self, c): """ - Initialize the advection equation. + Initialization of the :class:`AdvectionEquation`. :param c: The advection velocity parameter. :type c: float | int @@ -63,6 +63,9 @@ class AdvectionProblem(SpatialProblem, TimeDependentProblem): training physics-informed neural networks*. arXiv preprint arXiv:2308.08468 (2023). DOI: `arXiv:2308.08468 `_. + + :Example: + >>> problem = AdvectionProblem(c=1.0) """ output_variables = ["u"] @@ -80,7 +83,7 @@ class AdvectionProblem(SpatialProblem, TimeDependentProblem): def __init__(self, c=1.0): """ - Initialize the advection problem. + Initialization of the :class:`AdvectionProblem`. :param c: The advection velocity parameter. :type c: float | int diff --git a/pina/problem/zoo/allen_cahn.py b/pina/problem/zoo/allen_cahn.py index e4a9c4c41..4e05eaf68 100644 --- a/pina/problem/zoo/allen_cahn.py +++ b/pina/problem/zoo/allen_cahn.py @@ -49,6 +49,9 @@ class AllenCahnProblem(TimeDependentProblem, SpatialProblem): Computer Methods in Applied Mechanics and Engineering 421 (2024): 116805 DOI: `10.1016/ j.cma.2024.116805 `_. + + :Example: + >>> problem = AllenCahnProblem() """ output_variables = ["u"] diff --git a/pina/problem/zoo/diffusion_reaction.py b/pina/problem/zoo/diffusion_reaction.py index 6d6485dda..d7a26c59a 100644 --- a/pina/problem/zoo/diffusion_reaction.py +++ b/pina/problem/zoo/diffusion_reaction.py @@ -59,6 +59,9 @@ class DiffusionReactionProblem(TimeDependentProblem, SpatialProblem): **Original reference**: Si, Chenhao, et al. *Complex Physics-Informed Neural Network.* arXiv preprint arXiv:2502.04917 (2025). DOI: `arXiv:2502.04917 `_. + + :Example: + >>> problem = DiffusionReactionProblem() """ output_variables = ["u"] diff --git a/pina/problem/zoo/helmholtz.py b/pina/problem/zoo/helmholtz.py index 8564d8200..34d389319 100644 --- a/pina/problem/zoo/helmholtz.py +++ b/pina/problem/zoo/helmholtz.py @@ -16,7 +16,7 @@ class HelmholtzEquation(Equation): def __init__(self, alpha): """ - Initialize the Helmholtz equation. + Initialization of the :class:`HelmholtzEquation` class. :param alpha: Parameter of the forcing term. :type alpha: float | int @@ -53,6 +53,9 @@ class HelmholtzProblem(SpatialProblem): **Original reference**: Si, Chenhao, et al. *Complex Physics-Informed Neural Network.* arXiv preprint arXiv:2502.04917 (2025). DOI: `arXiv:2502.04917 `_. + + :Example: + >>> problem = HelmholtzProblem() """ output_variables = ["u"] @@ -75,7 +78,7 @@ class HelmholtzProblem(SpatialProblem): def __init__(self, alpha=3.0): """ - Initialize the Helmholtz problem. + Initialization of the :class:`HelmholtzProblem` class. :param alpha: Parameter of the forcing term. :type alpha: float | int diff --git a/pina/problem/zoo/inverse_poisson_2d_square.py b/pina/problem/zoo/inverse_poisson_2d_square.py index 16b4ec1d9..f112ebfc0 100644 --- a/pina/problem/zoo/inverse_poisson_2d_square.py +++ b/pina/problem/zoo/inverse_poisson_2d_square.py @@ -1,8 +1,10 @@ """Formulation of the inverse Poisson problem in a square domain.""" -import os +import requests import torch +from io import BytesIO from ... import Condition +from ... import LabelTensor from ...operator import laplacian from ...domain import CartesianDomain from ...equation import Equation, FixedValue @@ -27,21 +29,27 @@ def laplace_equation(input_, output_, params_): return delta_u - force_term -# Absolute path to the data directory -data_dir = os.path.abspath( - os.path.join( - os.path.dirname(__file__), "../../../tutorials/tutorial7/data/" - ) +# URL of the file +url = "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pts_0.5_0.5" +# Download the file +response = requests.get(url) +response.raise_for_status() +file_like_object = BytesIO(response.content) +# Set the data +input_data = LabelTensor( + torch.load(file_like_object, weights_only=False).tensor.detach(), + ["x", "y", "mu1", "mu2"], ) -# Load input data -input_data = torch.load( - f=os.path.join(data_dir, "pts_0.5_0.5"), weights_only=False -).extract(["x", "y"]) - -# Load output data -output_data = torch.load( - f=os.path.join(data_dir, "pinn_solution_0.5_0.5"), weights_only=False +# URL of the file +url = "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pinn_solution_0.5_0.5" +# Download the file +response = requests.get(url) +response.raise_for_status() +file_like_object = BytesIO(response.content) +# Set the data +output_data = LabelTensor( + torch.load(file_like_object, weights_only=False).tensor.detach(), ["u"] ) @@ -50,6 +58,9 @@ class InversePoisson2DSquareProblem(SpatialProblem, InverseProblem): Implementation of the inverse 2-dimensional Poisson problem in the square domain :math:`[0, 1] \times [0, 1]`, with unknown parameter domain :math:`[-1, 1] \times [-1, 1]`. + + :Example: + >>> problem = InversePoisson2DSquareProblem() """ output_variables = ["u"] diff --git a/pina/problem/zoo/poisson_2d_square.py b/pina/problem/zoo/poisson_2d_square.py index fef0b2e61..c6644c462 100644 --- a/pina/problem/zoo/poisson_2d_square.py +++ b/pina/problem/zoo/poisson_2d_square.py @@ -30,6 +30,9 @@ class Poisson2DSquareProblem(SpatialProblem): r""" Implementation of the 2-dimensional Poisson problem in the square domain :math:`[0, 1] \times [0, 1]`. + + :Example: + >>> problem = Poisson2DSquareProblem() """ output_variables = ["u"] diff --git a/pina/problem/zoo/supervised_problem.py b/pina/problem/zoo/supervised_problem.py index 45c75a1c6..3fe683f13 100644 --- a/pina/problem/zoo/supervised_problem.py +++ b/pina/problem/zoo/supervised_problem.py @@ -2,12 +2,11 @@ from ..abstract_problem import AbstractProblem from ... import Condition -from ... import LabelTensor class SupervisedProblem(AbstractProblem): """ - Definition of a supervised learning problem in PINA. + Definition of a supervised-learning problem. This class provides a simple way to define a supervised problem using a single condition of type @@ -28,7 +27,7 @@ def __init__( self, input_, output_, input_variables=None, output_variables=None ): """ - Initialize the SupervisedProblem class. + Initialization of the :class:`SupervisedProblem` class. :param input_: Input data of the problem. :type input_: torch.Tensor | LabelTensor | Graph | Data diff --git a/pina/solver/__init__.py b/pina/solver/__init__.py index 7a10cf9fa..cdca62db2 100644 --- a/pina/solver/__init__.py +++ b/pina/solver/__init__.py @@ -1,6 +1,4 @@ -""" -TODO -""" +"""Module for the solver classes.""" __all__ = [ "SolverInterface", diff --git a/pina/solver/garom.py b/pina/solver/garom.py index d023cf890..2f763a700 100644 --- a/pina/solver/garom.py +++ b/pina/solver/garom.py @@ -1,4 +1,4 @@ -"""Module for GAROM""" +"""Module for the GAROM solver.""" import torch from torch.nn.modules.loss import _Loss @@ -10,9 +10,9 @@ class GAROM(MultiSolverInterface): """ - GAROM solver class. This class implements Generative Adversarial - Reduced Order Model solver, using user specified ``models`` to solve - a specific order reduction``problem``. + GAROM solver class. This class implements Generative Adversarial Reduced + Order Model solver, using user specified ``models`` to solve a specific + order reduction ``problem``. .. seealso:: @@ -39,40 +39,34 @@ def __init__( regularizer=False, ): """ - :param AbstractProblem problem: The formualation of the problem. - :param torch.nn.Module generator: The neural network model to use - for the generator. - :param torch.nn.Module discriminator: The neural network model to use + Initialization of the :class:`GAROM` class. + + :param AbstractProblem problem: The formulation of the problem. + :param torch.nn.Module generator: The generator model. + :param torch.nn.Module discriminator: The discriminator model. + :param torch.nn.Module loss: The loss function to be minimized. + If ``None``, :class:`~pina.loss.power_loss.PowerLoss` with ``p=1`` + is used. Default is ``None``. + :param Optimizer optimizer_generator: The optimizer for the generator. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Optimizer optimizer_discriminator: The optimizer for the + discriminator. If `None`, the :class:`torch.optim.Adam` optimizer is + used. Default is ``None``. + :param Scheduler scheduler_generator: The learning rate scheduler for + the generator. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param Scheduler scheduler_discriminator: The learning rate scheduler for the discriminator. - :param torch.nn.Module loss: The loss function used as minimizer, - default ``None``. If ``loss`` is ``None`` the defualt - ``PowerLoss(p=1)`` is used, as in the original paper. - :param Optimizer optimizer_generator: The neural - network optimizer to use for the generator network - , default is `torch.optim.Adam`. - :param Optimizer optimizer_discriminator: The neural - network optimizer to use for the discriminator network - , default is `torch.optim.Adam`. - :param Scheduler scheduler_generator: Learning - rate scheduler for the generator. - :param Scheduler scheduler_discriminator: Learning - rate scheduler for the discriminator. - :param dict scheduler_discriminator_kwargs: LR scheduler constructor - keyword args. - :param gamma: Ratio of expected loss for generator and discriminator, - defaults to 0.3. - :type gamma: float - :param lambda_k: Learning rate for control theory optimization, - defaults to 0.001. - :type lambda_k: float - :param regularizer: Regularization term in the GAROM loss, - defaults to False. - :type regularizer: bool - - .. warning:: - The algorithm works only for data-driven model. Hence in the - ``problem`` definition the codition must only contain ``input`` - (e.g. coefficient parameters, time parameters), and ``target``. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param float gamma: Ratio of expected loss for generator and + discriminator. Default is ``0.3``. + :param float lambda_k: Learning rate for control theory optimization. + Default is ``0.001``. + :param bool regularizer: If ``True``, uses a regularization term in the + GAROM loss. Default is ``False``. """ # set loss @@ -112,20 +106,16 @@ def __init__( def forward(self, x, mc_steps=20, variance=False): """ - Forward step for GAROM solver + Forward pass implementation. - :param x: The input tensor. - :type x: torch.Tensor - :param mc_steps: Number of montecarlo samples to approximate the - expected value, defaults to 20. - :type mc_steps: int - :param variance: Returining also the sample variance of the solution, - defaults to False. - :type variance: bool + :param torch.Tensor x: The input tensor. + :param int mc_steps: Number of Montecarlo samples to approximate the + expected value. Default is ``20``. + :param bool variance: If ``True``, the method returns also the variance + of the solution. Default is ``False``. :return: The expected value of the generator distribution. If - ``variance=True`` also the - sample variance is returned. - :rtype: torch.Tensor | tuple(torch.Tensor, torch.Tensor) + ``variance=True``, the method returns also the variance. + :rtype: torch.Tensor | tuple[torch.Tensor, torch.Tensor] """ # sampling @@ -142,13 +132,24 @@ def forward(self, x, mc_steps=20, variance=False): return mean def sample(self, x): - """TODO""" + """ + Sample from the generator distribution. + + :param torch.Tensor x: The input tensor. + :return: The generated sample. + :rtype: torch.Tensor + """ # sampling return self.generator(x) def _train_generator(self, parameters, snapshots): """ - Private method to train the generator network. + Train the generator model. + + :param torch.Tensor parameters: The input tensor. + :param torch.Tensor snapshots: The target tensor. + :return: The residual loss and the generator loss. + :rtype: tuple[torch.Tensor, torch.Tensor] """ optimizer = self.optimizer_generator optimizer.zero_grad() @@ -170,16 +171,14 @@ def _train_generator(self, parameters, snapshots): def on_train_batch_end(self, outputs, batch, batch_idx): """ - This method is called at the end of each training batch, and ovverides - the PytorchLightining implementation for logging the checkpoints. + This method is called at the end of each training batch and overrides + the PyTorch Lightning implementation to log checkpoints. - :param torch.Tensor outputs: The output from the model for the - current batch. - :param tuple batch: The current batch of data. + :param torch.Tensor outputs: The ``model``'s output for the current + batch. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. :param int batch_idx: The index of the current batch. - :return: Whatever is returned by the parent - method ``on_train_batch_end``. - :rtype: Any """ # increase by one the counter of optimization to save loggers ( @@ -190,7 +189,12 @@ def on_train_batch_end(self, outputs, batch, batch_idx): def _train_discriminator(self, parameters, snapshots): """ - Private method to train the discriminator network. + Train the discriminator model. + + :param torch.Tensor parameters: The input tensor. + :param torch.Tensor snapshots: The target tensor. + :return: The residual loss and the generator loss. + :rtype: tuple[torch.Tensor, torch.Tensor] """ optimizer = self.optimizer_discriminator optimizer.zero_grad() @@ -215,8 +219,15 @@ def _train_discriminator(self, parameters, snapshots): def _update_weights(self, d_loss_real, d_loss_fake): """ - Private method to Update the weights of the generator and discriminator - networks. + Update the weights of the generator and discriminator models. + + :param torch.Tensor d_loss_real: The discriminator loss computed on + dataset samples. + :param torch.Tensor d_loss_fake: The discriminator loss computed on + generated samples. + :return: The difference between the loss computed on the dataset samples + and the loss computed on the generated samples. + :rtype: torch.Tensor """ diff = torch.mean(self.gamma * d_loss_real - d_loss_fake) @@ -227,12 +238,15 @@ def _update_weights(self, d_loss_real, d_loss_fake): return diff def optimization_cycle(self, batch): - """GAROM solver training step. - - :param batch: The batch element in the dataloader. - :type batch: tuple - :return: The sum of the loss functions. - :rtype: LabelTensor + """ + The optimization cycle for the GAROM solver. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict + containing the condition name and the associated scalar loss. + :rtype: dict """ condition_loss = {} for condition_name, points in batch: @@ -258,6 +272,14 @@ def optimization_cycle(self, batch): return condition_loss def validation_step(self, batch): + """ + The validation step for the PINN solver. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The loss of the validation step. + :rtype: torch.Tensor + """ condition_loss = {} for condition_name, points in batch: parameters, snapshots = ( @@ -273,6 +295,14 @@ def validation_step(self, batch): return loss def test_step(self, batch): + """ + The test step for the PINN solver. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The loss of the test step. + :rtype: torch.Tensor + """ condition_loss = {} for condition_name, points in batch: parameters, snapshots = ( @@ -289,30 +319,60 @@ def test_step(self, batch): @property def generator(self): - """TODO""" + """ + The generator model. + + :return: The generator model. + :rtype: torch.nn.Module + """ return self.models[0] @property def discriminator(self): - """TODO""" + """ + The discriminator model. + + :return: The discriminator model. + :rtype: torch.nn.Module + """ return self.models[1] @property def optimizer_generator(self): - """TODO""" + """ + The optimizer for the generator. + + :return: The optimizer for the generator. + :rtype: Optimizer + """ return self.optimizers[0].instance @property def optimizer_discriminator(self): - """TODO""" + """ + The optimizer for the discriminator. + + :return: The optimizer for the discriminator. + :rtype: Optimizer + """ return self.optimizers[1].instance @property def scheduler_generator(self): - """TODO""" + """ + The scheduler for the generator. + + :return: The scheduler for the generator. + :rtype: Scheduler + """ return self.schedulers[0].instance @property def scheduler_discriminator(self): - """TODO""" + """ + The scheduler for the discriminator. + + :return: The scheduler for the discriminator. + :rtype: Scheduler + """ return self.schedulers[1].instance diff --git a/pina/solver/physic_informed_solver/__init__.py b/pina/solver/physic_informed_solver/__init__.py index ce14f85fa..f0fb8ebcd 100644 --- a/pina/solver/physic_informed_solver/__init__.py +++ b/pina/solver/physic_informed_solver/__init__.py @@ -1,4 +1,4 @@ -"""TODO""" +"""Module for the Physics-Informed solvers.""" __all__ = [ "PINNInterface", diff --git a/pina/solver/physic_informed_solver/causal_pinn.py b/pina/solver/physic_informed_solver/causal_pinn.py index 36e24a06d..1fb102a05 100644 --- a/pina/solver/physic_informed_solver/causal_pinn.py +++ b/pina/solver/physic_informed_solver/causal_pinn.py @@ -1,4 +1,4 @@ -"""Module for Causal PINN.""" +"""Module for the Causal PINN solver.""" import torch @@ -9,14 +9,13 @@ class CausalPINN(PINN): r""" - Causal Physics Informed Neural Network (CausalPINN) solver class. - This class implements Causal Physics Informed Neural - Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + Causal Physics-Informed Neural Network (CausalPINN) solver class. + This class implements the Causal Physics-Informed Neural Network solver, + using a user specified ``model`` to solve a specific ``problem``. + It can be used to solve both forward and inverse problems. - The Causal Physics Informed Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Causal Physics-Informed Neural Network solver aims to find the solution + :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential problem: .. math:: @@ -26,7 +25,7 @@ class CausalPINN(PINN): \mathbf{x}\in\partial\Omega \end{cases} - minimizing the loss function + minimizing the loss function: .. math:: \mathcal{L}_{\rm{problem}} = \frac{1}{N_t}\sum_{i=1}^{N_t} @@ -45,26 +44,25 @@ class CausalPINN(PINN): .. math:: \omega_i = \exp\left(\epsilon \sum_{k=1}^{i-1}\mathcal{L}_r(t_k)\right). - :math:`\epsilon` is an hyperparameter, default set to :math:`100`, while - :math:`\mathcal{L}` is a specific loss function, - default Mean Square Error: + :math:`\epsilon` is an hyperparameter, set by default to :math:`100`, while + :math:`\mathcal{L}` is a specific loss function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. - .. seealso:: **Original reference**: Wang, Sifan, Shyam Sankaran, and Paris - Perdikaris. "Respecting causality for training physics-informed - neural networks." Computer Methods in Applied Mechanics - and Engineering 421 (2024): 116813. - DOI `10.1016 `_. + Perdikaris. + *Respecting causality for training physics-informed + neural networks.* + Computer Methods in Applied Mechanics and Engineering 421 (2024):116813. + DOI: `10.1016 `_. .. note:: - This class can only work for problems inheriting - from at least - :class:`~pina.problem.timedep_problem.TimeDependentProblem` class. + This class is only compatible with problems that inherit from the + :class:`~pina.problem.time_dependent_problem.TimeDependentProblem` + class. """ def __init__( @@ -78,17 +76,25 @@ def __init__( eps=100, ): """ - :param torch.nn.Module model: The neural network model to use. - :param AbstractProblem problem: The formulation of the problem. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default `None`. - :param torch.optim.LRScheduler scheduler: Learning rate scheduler; - default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. - :param float eps: The exponential decay parameter; default `100`. + Initialization of the :class:`CausalPINN` class. + + :param AbstractProblem problem: The problem to be solved. It must + inherit from at least + :class:`~pina.problem.time_dependent_problem.TimeDependentProblem`. + :param torch.nn.Module model: The neural network model to be used. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param torch.optim.LRScheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. + :param float eps: The exponential decay parameter. Default is ``100``. + :raises ValueError: If the problem is not a TimeDependentProblem. """ super().__init__( model=model, @@ -110,14 +116,12 @@ def __init__( def loss_phys(self, samples, equation): """ - Computes the physics loss for the Causal PINN solver based on given - samples and equation. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ # split sequentially ordered time tensors into chunks @@ -146,13 +150,16 @@ def loss_phys(self, samples, equation): def eps(self): """ The exponential decay parameter. + + :return: The exponential decay parameter. + :rtype: float """ return self._eps @eps.setter def eps(self, value): """ - Setter method for the eps parameter. + Set the exponential decay parameter. :param float value: The exponential decay parameter. """ @@ -161,10 +168,10 @@ def eps(self, value): def _sort_label_tensor(self, tensor): """ - Sorts the label tensor based on time variables. + Sort the tensor with respect to the temporal variables. - :param LabelTensor tensor: The label tensor to be sorted. - :return: The sorted label tensor based on time variables. + :param LabelTensor tensor: The tensor to be sorted. + :return: The tensor sorted with respect to the temporal variables. :rtype: LabelTensor """ # labels input tensors @@ -179,11 +186,12 @@ def _sort_label_tensor(self, tensor): def _split_tensor_into_chunks(self, tensor): """ - Splits the label tensor into chunks based on time. + Split the tensor into chunks based on time. - :param LabelTensor tensor: The label tensor to be split. - :return: Tuple containing the chunks and the original labels. - :rtype: Tuple[List[LabelTensor], List] + :param LabelTensor tensor: The tensor to be split. + :return: A tuple containing the list of tensor chunks and the + corresponding labels. + :rtype: tuple[list[LabelTensor], list[str]] """ # extract labels labels = tensor.labels @@ -199,7 +207,7 @@ def _split_tensor_into_chunks(self, tensor): def _compute_weights(self, loss): """ - Computes the weights for the physics loss based on the cumulative loss. + Compute the weights for the physics loss based on the cumulative loss. :param LabelTensor loss: The physics loss values. :return: The computed weights for the physics loss. diff --git a/pina/solver/physic_informed_solver/competitive_pinn.py b/pina/solver/physic_informed_solver/competitive_pinn.py index 0073ad905..058c53f40 100644 --- a/pina/solver/physic_informed_solver/competitive_pinn.py +++ b/pina/solver/physic_informed_solver/competitive_pinn.py @@ -1,4 +1,4 @@ -"""Module for Competitive PINN.""" +"""Module for the Competitive PINN solver.""" import copy import torch @@ -10,14 +10,14 @@ class CompetitivePINN(PINNInterface, MultiSolverInterface): r""" - Competitive Physics Informed Neural Network (PINN) solver class. - This class implements Competitive Physics Informed Neural - Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + Competitive Physics-Informed Neural Network (CompetitivePINN) solver class. + This class implements the Competitive Physics-Informed Neural Network + solver, using a user specified ``model`` to solve a specific ``problem``. + It can be used to solve both forward and inverse problems. - The Competitive Physics Informed Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Competitive Physics-Informed Neural Network solver aims to find the + solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential + problem: .. math:: @@ -27,18 +27,18 @@ class CompetitivePINN(PINNInterface, MultiSolverInterface): \mathbf{x}\in\partial\Omega \end{cases} - with a minimization (on ``model`` parameters) maximation ( - on ``discriminator`` parameters) of the loss function + minimizing the loss function with respect to the model parameters, while + maximizing it with respect to the discriminator parameters: .. math:: \mathcal{L}_{\rm{problem}} = \frac{1}{N}\sum_{i=1}^N \mathcal{L}(D(\mathbf{x}_i)\mathcal{A}[\mathbf{u}](\mathbf{x}_i))+ \frac{1}{N}\sum_{i=1}^N - \mathcal{L}(D(\mathbf{x}_i)\mathcal{B}[\mathbf{u}](\mathbf{x}_i)) + \mathcal{L}(D(\mathbf{x}_i)\mathcal{B}[\mathbf{u}](\mathbf{x}_i)), - where :math:`D` is the discriminator network, which tries to find the points - where the network performs worst, and :math:`\mathcal{L}` is a specific loss - function, default Mean Square Error: + where :math:D is the discriminator network, which identifies the points + where the model performs worst, and :math:\mathcal{L} is a specific loss + function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. @@ -46,13 +46,9 @@ class CompetitivePINN(PINNInterface, MultiSolverInterface): .. seealso:: **Original reference**: Zeng, Qi, et al. - "Competitive physics informed networks." International Conference on - Learning Representations, ICLR 2022 + *Competitive physics informed networks.* + International Conference on Learning Representations, ICLR 2022 `OpenReview Preprint `_. - - .. warning:: - This solver does not currently support the possibility to pass - ``extra_feature``. """ def __init__( @@ -68,24 +64,32 @@ def __init__( loss=None, ): """ - :param AbstractProblem problem: The formulation of the problem. - :param torch.nn.Module model: The neural network model to use - for the model. - :param torch.nn.Module discriminator: The neural network model to use - for the discriminator. If ``None``, the discriminator network will - have the same architecture as the model network. - :param torch.optim.Optimizer optimizer_model: The neural network - optimizer to use for the model network; default `None`. - :param torch.optim.Optimizer optimizer_discriminator: The neural network - optimizer to use for the discriminator network; default `None`. - :param torch.optim.LRScheduler scheduler_model: Learning rate scheduler - for the model; default `None`. - :param torch.optim.LRScheduler scheduler_discriminator: Learning rate - scheduler for the discriminator; default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. + Initialization of the :class:`CompetitivePINN` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The neural network model to be used. + :param torch.nn.Module discriminator: The discriminator to be used. + If `None`, the discriminator is a deepcopy of the ``model``. + Default is ``None``. + :param torch.optim.Optimizer optimizer_model: The optimizer of the + ``model``. If `None`, the :class:`torch.optim.Adam` optimizer is + used. Default is ``None``. + :param torch.optim.Optimizer optimizer_discriminator: The optimizer of + the ``discriminator``. If `None`, the :class:`torch.optim.Adam` + optimizer is used. Default is ``None``. + :param Scheduler scheduler_model: Learning rate scheduler for the + ``model``. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param Scheduler scheduler_discriminator: Learning rate scheduler for + the ``discriminator``. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. """ if discriminator is None: discriminator = copy.deepcopy(model) @@ -103,15 +107,11 @@ def __init__( self.automatic_optimization = False def forward(self, x): - r""" - Forward pass implementation for the PINN solver. It returns the function - evaluation :math:`\mathbf{u}(\mathbf{x})` at the control points - :math:`\mathbf{x}`. - - :param LabelTensor x: Input tensor for the PINN solver. It expects - a tensor :math:`N \times D`, where :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem, - :return: PINN solution evaluated at contro points. + """ + Forward pass. + + :param LabelTensor x: Input tensor. + :return: The output of the neural network. :rtype: LabelTensor """ return self.neural_net(x) @@ -120,9 +120,9 @@ def training_step(self, batch): """ Solver training step, overridden to perform manual optimization. - :param batch: The batch element in the dataloader. - :type batch: tuple - :return: The sum of the loss functions. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The aggregated loss. :rtype: LabelTensor """ # train model @@ -139,14 +139,12 @@ def training_step(self, batch): def loss_phys(self, samples, equation): """ - Computes the physics loss for the Competitive PINN solver based on given - samples and equation. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ # Compute discriminator bets @@ -165,10 +163,10 @@ def loss_phys(self, samples, equation): def configure_optimizers(self): """ - Optimizer configuration for the Competitive PINN solver. + Optimizer configuration. :return: The optimizers and the schedulers - :rtype: tuple(list, list) + :rtype: tuple[list[Optimizer], list[Scheduler]] """ # If the problem is an InverseProblem, add the unknown parameters # to the parameters to be optimized @@ -198,16 +196,14 @@ def configure_optimizers(self): def on_train_batch_end(self, outputs, batch, batch_idx): """ - This method is called at the end of each training batch, and ovverides - the PytorchLightining implementation for logging the checkpoints. + This method is called at the end of each training batch and overrides + the PyTorch Lightning implementation to log checkpoints. - :param torch.Tensor outputs: The output from the model for the - current batch. - :param tuple batch: The current batch of data. + :param torch.Tensor outputs: The ``model``'s output for the current + batch. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. :param int batch_idx: The index of the current batch. - :return: Whatever is returned by the parent - method ``on_train_batch_end``. - :rtype: Any """ # increase by one the counter of optimization to save loggers ( @@ -219,9 +215,9 @@ def on_train_batch_end(self, outputs, batch, batch_idx): @property def neural_net(self): """ - Returns the neural network model. + The model. - :return: The neural network model. + :return: The model. :rtype: torch.nn.Module """ return self.models[0] @@ -229,9 +225,9 @@ def neural_net(self): @property def discriminator(self): """ - Returns the discriminator model (if applicable). + The discriminator. - :return: The discriminator model. + :return: The discriminator. :rtype: torch.nn.Module """ return self.models[1] @@ -239,39 +235,39 @@ def discriminator(self): @property def optimizer_model(self): """ - Returns the optimizer associated with the neural network model. + The optimizer associated to the model. - :return: The optimizer for the neural network model. - :rtype: torch.optim.Optimizer + :return: The optimizer for the model. + :rtype: Optimizer """ return self.optimizers[0] @property def optimizer_discriminator(self): """ - Returns the optimizer associated with the discriminator (if applicable). + The optimizer associated to the discriminator. :return: The optimizer for the discriminator. - :rtype: torch.optim.Optimizer + :rtype: Optimizer """ return self.optimizers[1] @property def scheduler_model(self): """ - Returns the scheduler associated with the neural network model. + The scheduler associated to the model. - :return: The scheduler for the neural network model. - :rtype: torch.optim.lr_scheduler._LRScheduler + :return: The scheduler for the model. + :rtype: Scheduler """ return self.schedulers[0] @property def scheduler_discriminator(self): """ - Returns the scheduler associated with the discriminator (if applicable). + The scheduler associated to the discriminator. :return: The scheduler for the discriminator. - :rtype: torch.optim.lr_scheduler._LRScheduler + :rtype: Scheduler """ return self.schedulers[1] diff --git a/pina/solver/physic_informed_solver/gradient_pinn.py b/pina/solver/physic_informed_solver/gradient_pinn.py index 22ebb2f17..4ac2b4c69 100644 --- a/pina/solver/physic_informed_solver/gradient_pinn.py +++ b/pina/solver/physic_informed_solver/gradient_pinn.py @@ -1,4 +1,4 @@ -"""Module for Gradient PINN.""" +"""Module for the Gradient PINN solver.""" import torch @@ -9,14 +9,14 @@ class GradientPINN(PINN): r""" - Gradient Physics Informed Neural Network (GradientPINN) solver class. - This class implements Gradient Physics Informed Neural - Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + Gradient Physics-Informed Neural Network (GradientPINN) solver class. + This class implements the Gradient Physics-Informed Neural Network solver, + using a user specified ``model`` to solve a specific ``problem``. + It can be used to solve both forward and inverse problems. - The Gradient Physics Informed Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Gradient Physics-Informed Neural Network solver aims to find the + solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential + problem: .. math:: @@ -26,7 +26,7 @@ class GradientPINN(PINN): \mathbf{x}\in\partial\Omega \end{cases} - minimizing the loss function + minimizing the loss function; .. math:: \mathcal{L}_{\rm{problem}} =& \frac{1}{N}\sum_{i=1}^N @@ -39,24 +39,22 @@ class GradientPINN(PINN): \nabla_{\mathbf{x}}\mathcal{L}(\mathcal{B}[\mathbf{u}](\mathbf{x}_i)) - where :math:`\mathcal{L}` is a specific loss function, - default Mean Square Error: + where :math:`\mathcal{L}` is a specific loss function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. .. seealso:: - **Original reference**: Yu, Jeremy, et al. "Gradient-enhanced - physics-informed neural networks for forward and inverse - PDE problems." Computer Methods in Applied Mechanics - and Engineering 393 (2022): 114823. + **Original reference**: Yu, Jeremy, et al. + *Gradient-enhanced physics-informed neural networks for forward and + inverse PDE problems.* + Computer Methods in Applied Mechanics and Engineering 393 (2022):114823. DOI: `10.1016 `_. .. note:: - This class can only work for problems inheriting - from at least :class:`~pina.problem.spatial_problem.SpatialProblem` - class. + This class is only compatible with problems that inherit from the + :class:`~pina.problem.spatial_problem.SpatialProblem` class. """ def __init__( @@ -69,19 +67,25 @@ def __init__( loss=None, ): """ - :param torch.nn.Module model: The neural network model to use. - :param AbstractProblem problem: The formulation of the problem. It must - inherit from at least - :class:`~pina.problem.spatial_problem.SpatialProblem` to compute - the gradient of the loss. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default `None`. - :param torch.optim.LRScheduler scheduler: Learning rate scheduler; - default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. + Initialization of the :class:`GradientPINN` class. + + :param AbstractProblem problem: The problem to be solved. + It must inherit from at least + :class:`~pina.problem.spatial_problem.SpatialProblem` to compute the + gradient of the loss. + :param torch.nn.Module model: The neural network model to be used. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. + :raises ValueError: If the problem is not a SpatialProblem. """ super().__init__( model=model, @@ -102,14 +106,12 @@ def __init__( def loss_phys(self, samples, equation): """ - Computes the physics loss for the GPINN solver based on given - samples and equation. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ # classical PINN loss diff --git a/pina/solver/physic_informed_solver/pinn.py b/pina/solver/physic_informed_solver/pinn.py index d3c2af6f3..6d92d9c36 100644 --- a/pina/solver/physic_informed_solver/pinn.py +++ b/pina/solver/physic_informed_solver/pinn.py @@ -1,4 +1,4 @@ -"""Module for Physics Informed Neural Network.""" +"""Module for the Physics-Informed Neural Network solver.""" import torch @@ -9,14 +9,13 @@ class PINN(PINNInterface, SingleSolverInterface): r""" - Physics Informed Neural Network (PINN) solver class. - This class implements Physics Informed Neural - Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + Physics-Informed Neural Network (PINN) solver class. + This class implements Physics-Informed Neural Network solver, using a user + specified ``model`` to solve a specific ``problem``. + It can be used to solve both forward and inverse problems. - The Physics Informed Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Physics Informed Neural Network solver aims to find the solution + :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential problem: .. math:: @@ -26,16 +25,15 @@ class PINN(PINNInterface, SingleSolverInterface): \mathbf{x}\in\partial\Omega \end{cases} - minimizing the loss function + minimizing the loss function: .. math:: \mathcal{L}_{\rm{problem}} = \frac{1}{N}\sum_{i=1}^N \mathcal{L}(\mathcal{A}[\mathbf{u}](\mathbf{x}_i)) + \frac{1}{N}\sum_{i=1}^N - \mathcal{L}(\mathcal{B}[\mathbf{u}](\mathbf{x}_i)) + \mathcal{L}(\mathcal{B}[\mathbf{u}](\mathbf{x}_i)), - where :math:`\mathcal{L}` is a specific loss function, - default Mean Square Error: + where :math:`\mathcal{L}` is a specific loss function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. @@ -44,7 +42,8 @@ class PINN(PINNInterface, SingleSolverInterface): **Original reference**: Karniadakis, G. E., Kevrekidis, I. G., Lu, L., Perdikaris, P., Wang, S., & Yang, L. (2021). - Physics-informed machine learning. Nature Reviews Physics, 3, 422-440. + *Physics-informed machine learning.* + Nature Reviews Physics, 3, 422-440. DOI: `10.1038 `_. """ @@ -58,16 +57,21 @@ def __init__( loss=None, ): """ - :param torch.nn.Module model: The neural network model to use. - :param AbstractProblem problem: The formulation of the problem. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default `None`. - :param torch.optim.LRScheduler scheduler: Learning rate scheduler; - default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. + Initialization of the :class:`PINN` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The neural network model to be used. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. """ super().__init__( model=model, @@ -80,14 +84,12 @@ def __init__( def loss_phys(self, samples, equation): """ - Computes the physics loss for the PINN solver based on given - samples and equation. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ residual = self.compute_residual(samples=samples, equation=equation) @@ -101,7 +103,7 @@ def configure_optimizers(self): Optimizer configuration for the PINN solver. :return: The optimizers and the schedulers - :rtype: tuple(list, list) + :rtype: tuple[list[Optimizer], list[Scheduler]] """ # If the problem is an InverseProblem, add the unknown parameters # to the parameters to be optimized. diff --git a/pina/solver/physic_informed_solver/pinn_interface.py b/pina/solver/physic_informed_solver/pinn_interface.py index f31e80c00..09e152feb 100644 --- a/pina/solver/physic_informed_solver/pinn_interface.py +++ b/pina/solver/physic_informed_solver/pinn_interface.py @@ -1,4 +1,4 @@ -"""Module for Physics Informed Neural Network Interface.""" +"""Module for the Physics-Informed Neural Network Interface.""" from abc import ABCMeta, abstractmethod import torch @@ -17,14 +17,13 @@ class PINNInterface(SolverInterface, metaclass=ABCMeta): """ - Base PINN solver class. This class implements the Solver Interface - for Physics Informed Neural Network solver. - - This class can be used to define PINNs with multiple ``optimizers``, - and/or ``models``. - By default it takes :class:`~pina.problem.abstract_problem.AbstractProblem`, - so the user can choose what type of problem the implemented solver, - inheriting from this class, is designed to solve. + Base class for Physics-Informed Neural Network (PINN) solvers, implementing + the :class:`~pina.solver.solver.SolverInterface` class. + + The `PINNInterface` class can be used to define PINNs that work with one or + multiple optimizers and/or models. By default, it is compatible with + problems defined by :class:`~pina.problem.abstract_problem.AbstractProblem`, + and users can choose the problem type the solver is meant to address. """ accepted_conditions_types = ( @@ -35,9 +34,14 @@ class PINNInterface(SolverInterface, metaclass=ABCMeta): def __init__(self, problem, loss=None, **kwargs): """ - :param AbstractProblem problem: A problem definition instance. - :param torch.nn.Module loss: The loss function to be minimized, - default `None`. + Initialization of the :class:`PINNInterface` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. + :param kwargs: Additional keyword arguments to be passed to the + :class:`~pina.solver.solver.SolverInterface` class. """ if loss is None: @@ -62,10 +66,32 @@ def __init__(self, problem, loss=None, **kwargs): self.__metric = None def optimization_cycle(self, batch): + """ + The optimization cycle for the PINN solver. + + This method allows to call `_run_optimization_cycle` with the physics + loss as argument, thus distinguishing the training step from the + validation and test steps. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict + containing the condition name and the associated scalar loss. + :rtype: dict + """ return self._run_optimization_cycle(batch, self.loss_phys) @torch.set_grad_enabled(True) def validation_step(self, batch): + """ + The validation step for the PINN solver. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The loss of the validation step. + :rtype: torch.Tensor + """ losses = self._run_optimization_cycle(batch, self._residual_loss) loss = self.weighting.aggregate(losses).as_subclass(torch.Tensor) self.store_log("val_loss", loss, self.get_batch_size(batch)) @@ -73,6 +99,14 @@ def validation_step(self, batch): @torch.set_grad_enabled(True) def test_step(self, batch): + """ + The test step for the PINN solver. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The loss of the test step. + :rtype: torch.Tensor + """ losses = self._run_optimization_cycle(batch, self._residual_loss) loss = self.weighting.aggregate(losses).as_subclass(torch.Tensor) self.store_log("test_loss", loss, self.get_batch_size(batch)) @@ -80,14 +114,14 @@ def test_step(self, batch): def loss_data(self, input_pts, output_pts): """ - The data loss for the PINN solver. It computes the loss between - the network output against the true solution. This function - should not be override if not intentionally. - - :param LabelTensor input_pts: The input to the neural networks. - :param LabelTensor output_pts: The true solution to compare the - network solution. - :return: The residual loss averaged on the input coordinates + Compute the data loss for the PINN solver by evaluating the loss + between the network's output and the true solution. This method should + not be overridden, if not intentionally. + + :param LabelTensor input_pts: The input points to the neural network. + :param LabelTensor output_pts: The true solution to compare with the + network's output. + :return: The supervised loss, averaged over the number of observations. :rtype: torch.Tensor """ return self._loss(self.forward(input_pts), output_pts) @@ -95,28 +129,23 @@ def loss_data(self, input_pts, output_pts): @abstractmethod def loss_phys(self, samples, equation): """ - Computes the physics loss for the physics informed solver based on given - samples and equation. This method must be override by all inherited - classes and it is the core to define a new physics informed solver. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. This method must be overridden in + subclasses. It distinguishes different types of PINN solvers. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ def compute_residual(self, samples, equation): """ - Compute the residual for Physics Informed learning. This function - returns the :obj:`~pina.equation.equation.Equation` specified in the - :obj:`~pina.condition.Condition` evaluated at the ``samples`` points. + Compute the residuals of the equation. - :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The residual of the neural network solution. + :param LabelTensor samples: The samples to evaluate the loss. + :param EquationInterface equation: The governing equation. + :return: The residual of the solution of the model. :rtype: LabelTensor """ try: @@ -129,10 +158,30 @@ def compute_residual(self, samples, equation): return residual def _residual_loss(self, samples, equation): + """ + Compute the residual loss. + + :param LabelTensor samples: The samples to evaluate the loss. + :param EquationInterface equation: The governing equation. + :return: The residual loss. + :rtype: torch.Tensor + """ residuals = self.compute_residual(samples, equation) return self.loss(residuals, torch.zeros_like(residuals)) def _run_optimization_cycle(self, batch, loss_residuals): + """ + Compute, given a batch, the loss for each condition and return a + dictionary with the condition name as key and the loss as value. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :param function loss_residuals: The loss function to be minimized. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict + containing the condition name and the associated scalar loss. + :rtype: dict + """ condition_loss = {} for condition_name, points in batch: self.__metric = condition_name @@ -158,8 +207,7 @@ def _run_optimization_cycle(self, batch, loss_residuals): def _clamp_inverse_problem_params(self): """ - Clamps the parameters of the inverse problem - solver to the specified ranges. + Clamps the parameters of the inverse problem solver to specified ranges. """ for v in self._params: self._params[v].data.clamp_( @@ -170,7 +218,10 @@ def _clamp_inverse_problem_params(self): @property def loss(self): """ - Loss used for training. + The loss used for training. + + :return: The loss function used for training. + :rtype: torch.nn.Module """ return self._loss @@ -178,5 +229,8 @@ def loss(self): def current_condition_name(self): """ The current condition name. + + :return: The current condition name. + :rtype: str """ return self.__metric diff --git a/pina/solver/physic_informed_solver/rba_pinn.py b/pina/solver/physic_informed_solver/rba_pinn.py index 3f189e97a..feeb5c817 100644 --- a/pina/solver/physic_informed_solver/rba_pinn.py +++ b/pina/solver/physic_informed_solver/rba_pinn.py @@ -1,4 +1,4 @@ -"""Module for Residual-Based Attention PINN.""" +"""Module for the Residual-Based Attention PINN solver.""" from copy import deepcopy import torch @@ -9,14 +9,14 @@ class RBAPINN(PINN): r""" - Residual-based Attention PINN (RBAPINN) solver class. - This class implements Residual-based Attention Physics Informed Neural - Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + Residual-based Attention Physics-Informed Neural Network (RBAPINN) solver + class. This class implements the Residual-based Attention Physics-Informed + Neural Network solver, using a user specified ``model`` to solve a specific + ``problem``. It can be used to solve both forward and inverse problems. - The Residual-based Attention Physics Informed Neural Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Residual-based Attention Physics-Informed Neural Network solver aims to + find the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a + differential problem: .. math:: @@ -26,7 +26,7 @@ class RBAPINN(PINN): \mathbf{x}\in\partial\Omega \end{cases} - minimizing the loss function + minimizing the loss function: .. math:: @@ -38,32 +38,32 @@ class RBAPINN(PINN): \left( \mathcal{B}[\mathbf{u}](\mathbf{x}) \right), - denoting the weights as + denoting the weights as: :math:`\lambda_{\Omega}^1, \dots, \lambda_{\Omega}^{N_\Omega}` and :math:`\lambda_{\partial \Omega}^1, \dots, \lambda_{\Omega}^{N_\partial \Omega}` for :math:`\Omega` and :math:`\partial \Omega`, respectively. - Residual-based Attention Physics Informed Neural Network computes - the weights by updating them at every epoch as follows + Residual-based Attention Physics-Informed Neural Network updates the weights + of the residuals at every epoch as follows: .. math:: \lambda_i^{k+1} \leftarrow \gamma\lambda_i^{k} + \eta\frac{\lvert r_i\rvert}{\max_j \lvert r_j\rvert}, - where :math:`r_i` denotes the residual at point :math:`i`, - :math:`\gamma` denotes the decay rate, and :math:`\eta` is - the learning rate for the weights' update. + where :math:`r_i` denotes the residual at point :math:`i`, :math:`\gamma` + denotes the decay rate, and :math:`\eta` is the learning rate for the + weights' update. .. seealso:: **Original reference**: Sokratis J. Anagnostopoulos, Juan D. Toscano, Nikolaos Stergiopulos, and George E. Karniadakis. - "Residual-based attention and connection to information - bottleneck theory in PINNs". + *Residual-based attention and connection to information + bottleneck theory in PINNs.* Computer Methods in Applied Mechanics and Engineering 421 (2024): 116805 - DOI: `10.1016/ - j.cma.2024.116805 `_. + DOI: `10.1016/j.cma.2024.116805 + `_. """ def __init__( @@ -78,20 +78,26 @@ def __init__( gamma=0.999, ): """ - :param torch.nn.Module model: The neural network model to use. - :param AbstractProblem problem: The formulation of the problem. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default `None`. - :param torch.optim.LRScheduler scheduler: Learning rate scheduler; - default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. + Initialization of the :class:`RBAPINN` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The neural network model to be used. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. :param float | int eta: The learning rate for the weights of the - residual; default 0.001. + residuals. Default is ``0.001``. :param float gamma: The decay parameter in the update of the weights - of the residual. Must be between 0 and 1; default 0.999. + of the residuals. Must be between ``0`` and ``1``. + Default is ``0.999``. """ super().__init__( model=model, @@ -122,6 +128,11 @@ def __init__( # for now RBAPINN is implemented only for batch_size = None def on_train_start(self): + """ + Hook method called at the beginning of training. + + :raises NotImplementedError: If the batch size is not ``None``. + """ if self.trainer.batch_size is not None: raise NotImplementedError( "RBAPINN only works with full batch " @@ -132,12 +143,12 @@ def on_train_start(self): def _vect_to_scalar(self, loss_value): """ - Elaboration of the pointwise loss. + Computation of the scalar loss. - :param LabelTensor loss_value: the matrix of pointwise loss. - - :return: the scalar loss. - :rtype LabelTensor + :param LabelTensor loss_value: the tensor of pointwise losses. + :raises RuntimeError: If the loss reduction is not ``mean`` or ``sum``. + :return: The computed scalar loss. + :rtype: LabelTensor """ if self.loss.reduction == "mean": ret = torch.mean(loss_value) @@ -152,14 +163,12 @@ def _vect_to_scalar(self, loss_value): def loss_phys(self, samples, equation): """ - Computes the physics loss for the residual-based attention PINN - solver based on given samples and equation. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. :rtype: LabelTensor """ residual = self.compute_residual(samples=samples, equation=equation) diff --git a/pina/solver/physic_informed_solver/self_adaptive_pinn.py b/pina/solver/physic_informed_solver/self_adaptive_pinn.py index 2a0208e9c..a6310d515 100644 --- a/pina/solver/physic_informed_solver/self_adaptive_pinn.py +++ b/pina/solver/physic_informed_solver/self_adaptive_pinn.py @@ -1,4 +1,4 @@ -"""Module for Self-Adaptive PINN.""" +"""Module for the Self-Adaptive PINN solver.""" from copy import deepcopy import torch @@ -11,13 +11,15 @@ class Weights(torch.nn.Module): """ - This class aims to implements the mask model for the - self-adaptive weights of the Self-Adaptive PINN solver. + Implementation of the mask model for the self-adaptive weights of the + :class:`SelfAdaptivePINN` solver. """ def __init__(self, func): """ - :param torch.nn.Module func: the mask module of SAPINN. + Initialization of the :class:`Weights` class. + + :param torch.nn.Module func: the mask model. """ super().__init__() check_consistency(func, torch.nn.Module) @@ -27,7 +29,6 @@ def __init__(self, func): def forward(self): """ Forward pass implementation for the mask module. - It returns the function on the weights evaluation. :return: evaluation of self adaptive weights through the mask. :rtype: torch.Tensor @@ -37,14 +38,14 @@ def forward(self): class SelfAdaptivePINN(PINNInterface, MultiSolverInterface): r""" - Self Adaptive Physics Informed Neural Network (SelfAdaptivePINN) - solver class. This class implements Self-Adaptive Physics Informed Neural + Self-Adaptive Physics-Informed Neural Network (SelfAdaptivePINN) solver + class. This class implements the Self-Adaptive Physics-Informed Neural Network solver, using a user specified ``model`` to solve a specific - ``problem``. It can be used for solving both forward and inverse problems. + ``problem``. It can be used to solve both forward and inverse problems. - The Self Adapive Physics Informed Neural Network aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Self-Adapive Physics-Informed Neural Network solver aims to find the + solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential + problem: .. math:: @@ -54,9 +55,10 @@ class SelfAdaptivePINN(PINNInterface, MultiSolverInterface): \mathbf{x}\in\partial\Omega \end{cases} - integrating the pointwise loss evaluation through a mask :math:`m` and - self adaptive weights that permit to focus the loss function on - specific training samples. + integrating pointwise loss evaluation using a mask :math:m and self-adaptive + weights, which allow the model to focus on regions of the domain where the + residual is higher. + The loss function to solve the problem is .. math:: @@ -69,34 +71,33 @@ class SelfAdaptivePINN(PINNInterface, MultiSolverInterface): \left( \mathcal{B}[\mathbf{u}](\mathbf{x}) \right), - denoting the self adaptive weights as :math:`\lambda_{\Omega}^1, \dots, \lambda_{\Omega}^{N_\Omega}` and :math:`\lambda_{\partial \Omega}^1, \dots, \lambda_{\Omega}^{N_\partial \Omega}` for :math:`\Omega` and :math:`\partial \Omega`, respectively. - Self Adaptive Physics Informed Neural Network identifies the solution - and appropriate self adaptive weights by solving the following problem + The Self-Adaptive Physics-Informed Neural Network solver identifies the + solution and appropriate self adaptive weights by solving the following + optimization problem: .. math:: \min_{w} \max_{\lambda_{\Omega}^k, \lambda_{\partial \Omega}^s} \mathcal{L} , - where :math:`w` denotes the network parameters, and - :math:`\mathcal{L}` is a specific loss - function, default Mean Square Error: + where :math:`w` denotes the network parameters, and :math:`\mathcal{L}` is a + specific loss function, , typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. .. seealso:: **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. - "Self-adaptive physics-informed neural networks." + *Self-adaptive physics-informed neural networks.* Journal of Computational Physics 474 (2023): 111722. - DOI: `10.1016/ - j.jcp.2022.111722 `_. + DOI: `10.1016/j.jcp.2022.111722 + `_. """ def __init__( @@ -112,23 +113,32 @@ def __init__( loss=None, ): """ - :param AbstractProblem problem: The formulation of the problem. - :param torch.nn.Module model: The neural network model to use for - the model. - :param torch.nn.Module weight_function: The neural network model - related to the Self-Adaptive PINN mask; default `torch.nn.Sigmoid()` - :param torch.optim.Optimizer optimizer_model: The neural network - optimizer to use for the model network; default `None`. - :param torch.optim.Optimizer optimizer_weights: The neural network - optimizer to use for mask model; default `None`. - :param torch.optim.LRScheduler scheduler_model: Learning rate scheduler - for the model; default `None`. - :param torch.optim.LRScheduler scheduler_weights: Learning rate - scheduler for the mask model; default `None`. - :param WeightingInterface weighting: The weighting schema to use; - default `None`. - :param torch.nn.Module loss: The loss function to be minimized; - default `None`. + Initialization of the :class:`SelfAdaptivePINN` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The model to be used. + :param torch.nn.Module weight_function: The Self-Adaptive mask model. + Default is ``torch.nn.Sigmoid()``. + :param Optimizer optimizer_model: The optimizer of the ``model``. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Optimizer optimizer_weights: The optimizer of the + ``weight_function``. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler_model: Learning rate scheduler for the + ``model``. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param Scheduler scheduler_weights: Learning rate scheduler for the + ``weight_function``. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. """ # check consistency weitghs_function check_consistency(weight_function, torch.nn.Module) @@ -155,16 +165,11 @@ def __init__( self._vectorial_loss.reduction = "none" def forward(self, x): - r""" - Forward pass implementation for the PINN - solver. It returns the function - evaluation :math:`\mathbf{u}(\mathbf{x})` at the control points - :math:`\mathbf{x}`. - - :param LabelTensor x: Input tensor for the SAPINN solver. It expects - a tensor :math:`N \\times D`, where :math:`N` the number of points - in the mesh, :math:`D` the dimension of the problem, - :return: PINN solution. + """ + Forward pass. + + :param LabelTensor x: Input tensor. + :return: The output of the neural network. :rtype: LabelTensor """ return self.model(x) @@ -173,9 +178,9 @@ def training_step(self, batch): """ Solver training step, overridden to perform manual optimization. - :param batch: The batch element in the dataloader. - :type batch: tuple - :return: The sum of the loss functions. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The aggregated loss. :rtype: LabelTensor """ # Weights optimization @@ -194,10 +199,10 @@ def training_step(self, batch): def configure_optimizers(self): """ - Optimizer configuration for the SelfAdaptive PINN solver. + Optimizer configuration. :return: The optimizers and the schedulers - :rtype: tuple(list, list) + :rtype: tuple[list[Optimizer], list[Scheduler]] """ # If the problem is an InverseProblem, add the unknown parameters # to the parameters to be optimized @@ -221,16 +226,14 @@ def configure_optimizers(self): def on_train_batch_end(self, outputs, batch, batch_idx): """ - This method is called at the end of each training batch, and ovverides - the PytorchLightining implementation for logging the checkpoints. + This method is called at the end of each training batch and overrides + the PyTorch Lightning implementation to log checkpoints. - :param torch.Tensor outputs: The output from the model for the - current batch. - :param tuple batch: The current batch of data. + :param torch.Tensor outputs: The ``model``'s output for the current + batch. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. :param int batch_idx: The index of the current batch. - :return: Whatever is returned by the parent - method ``on_train_batch_end``. - :rtype: Any """ # increase by one the counter of optimization to save loggers ( @@ -241,12 +244,10 @@ def on_train_batch_end(self, outputs, batch, batch_idx): def on_train_start(self): """ - This method is called at the start of the training for setting - the self adaptive weights as parameters of the mask model. + This method is called at the start of the training process to set the + self-adaptive weights as parameters of the mask model. - :return: Whatever is returned by the parent - method ``on_train_start``. - :rtype: Any + :raises NotImplementedError: If the batch size is not ``None``. """ if self.trainer.batch_size is not None: raise NotImplementedError( @@ -270,9 +271,9 @@ def on_train_start(self): def on_load_checkpoint(self, checkpoint): """ - Override the Pytorch Lightning ``on_load_checkpoint`` to handle - checkpoints for Self-Adaptive Weights. This method should not be - overridden if not intentionally. + Override of the Pytorch Lightning ``on_load_checkpoint`` method to + handle checkpoints for Self-Adaptive Weights. This method should not be + overridden, if not intentionally. :param dict checkpoint: Pytorch Lightning checkpoint dict. """ @@ -289,14 +290,13 @@ def on_load_checkpoint(self, checkpoint): def loss_phys(self, samples, equation): """ - Computation of the physical loss for SelfAdaptive PINN solver. + Computes the physics loss for the physics-informed solver based on the + provided samples and equation. - :param LabelTensor samples: Input samples to evaluate the physics loss. - :param EquationInterface equation: the governing equation representing - the physics. - - :return: tuple with weighted and not weighted scalar loss - :rtype: List[LabelTensor, LabelTensor] + :param LabelTensor samples: The samples to evaluate the physics loss. + :param EquationInterface equation: The governing equation. + :return: The computed physics loss. + :rtype: LabelTensor """ residual = self.compute_residual(samples, equation) weights = self.weights_dict[self.current_condition_name].forward() @@ -307,13 +307,12 @@ def loss_phys(self, samples, equation): def _vect_to_scalar(self, loss_value): """ - Elaboration of the pointwise loss through the mask model and the - self adaptive weights + Computation of the scalar loss. - :param LabelTensor loss_value: the matrix of pointwise loss - - :return: the scalar loss - :rtype LabelTensor + :param LabelTensor loss_value: the tensor of pointwise losses. + :raises RuntimeError: If the loss reduction is not ``mean`` or ``sum``. + :return: The computed scalar loss. + :rtype: LabelTensor """ if self.loss.reduction == "mean": ret = torch.mean(loss_value) @@ -329,63 +328,59 @@ def _vect_to_scalar(self, loss_value): @property def model(self): """ - Return the mask models associate to the application of - the mask to the self adaptive weights for each loss that - compones the global loss of the problem. + The model. - :return: The ModuleDict for mask models. - :rtype: torch.nn.ModuleDict + :return: The model. + :rtype: torch.nn.Module """ return self.models[0] @property def weights_dict(self): """ - Return the mask models associate to the application of - the mask to the self adaptive weights for each loss that - compones the global loss of the problem. + The self-adaptive weights. - :return: The ModuleDict for mask models. - :rtype: torch.nn.ModuleDict + :return: The self-adaptive weights. + :rtype: torch.nn.Module """ return self.models[1] @property def scheduler_model(self): """ - Returns the scheduler associated with the neural network model. + The scheduler associated to the model. - :return: The scheduler for the neural network model. - :rtype: torch.optim.lr_scheduler._LRScheduler + :return: The scheduler for the model. + :rtype: Scheduler """ return self.schedulers[0] @property def scheduler_weights(self): """ - Returns the scheduler associated with the mask model (if applicable). + The scheduler associated to the mask model. :return: The scheduler for the mask model. - :rtype: torch.optim.lr_scheduler._LRScheduler + :rtype: Scheduler """ return self.schedulers[1] @property def optimizer_model(self): """ - Returns the optimizer associated with the neural network model. + Returns the optimizer associated to the model. - :return: The optimizer for the neural network model. - :rtype: torch.optim.Optimizer + :return: The optimizer for the model. + :rtype: Optimizer """ return self.optimizers[0] @property def optimizer_weights(self): """ - Returns the optimizer associated with the mask model (if applicable). + The optimizer associated to the mask model. :return: The optimizer for the mask model. - :rtype: torch.optim.Optimizer + :rtype: Optimizer """ return self.optimizers[1] diff --git a/pina/solver/reduced_order_model.py b/pina/solver/reduced_order_model.py index c80556b31..949cb0111 100644 --- a/pina/solver/reduced_order_model.py +++ b/pina/solver/reduced_order_model.py @@ -1,19 +1,17 @@ -"""Module for ReducedOrderModelSolver""" +"""Module for the Reduced Order Model solver""" import torch - from .supervised import SupervisedSolver class ReducedOrderModelSolver(SupervisedSolver): r""" - ReducedOrderModelSolver solver class. This class implements a - Reduced Order Model solver, using user specified ``reduction_network`` and + Reduced Order Model solver class. This class implements the Reduced Order + Model solver, using user specified ``reduction_network`` and ``interpolation_network`` to solve a specific ``problem``. - The Reduced Order Model approach aims to find - the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` - of the differential problem: + The Reduced Order Model solver aims to find the solution + :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` of a differential problem: .. math:: @@ -23,13 +21,13 @@ class ReducedOrderModelSolver(SupervisedSolver): \mathbf{x}\in\partial\Omega \end{cases} - This is done by using two neural networks. The ``reduction_network``, which - contains an encoder :math:`\mathcal{E}_{\rm{net}}`, a decoder - :math:`\mathcal{D}_{\rm{net}}`; and an ``interpolation_network`` + This is done by means of two neural networks: the ``reduction_network``, + which defines an encoder :math:`\mathcal{E}_{\rm{net}}`, and a decoder + :math:`\mathcal{D}_{\rm{net}}`; and the ``interpolation_network`` :math:`\mathcal{I}_{\rm{net}}`. The input is assumed to be discretised in the spatial dimensions. - The following loss function is minimized during training + The following loss function is minimized during training: .. math:: \mathcal{L}_{\rm{problem}} = \frac{1}{N}\sum_{i=1}^N @@ -39,49 +37,46 @@ class ReducedOrderModelSolver(SupervisedSolver): \mathcal{D}_{\rm{net}}[\mathcal{E}_{\rm{net}}[\mathbf{u}(\mu_i)]] - \mathbf{u}(\mu_i)) - where :math:`\mathcal{L}` is a specific loss function, default - Mean Square Error: + where :math:`\mathcal{L}` is a specific loss function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. - .. seealso:: **Original reference**: Hesthaven, Jan S., and Stefano Ubbiali. - "Non-intrusive reduced order modeling of nonlinear problems - using neural networks." Journal of Computational - Physics 363 (2018): 55-78. + *Non-intrusive reduced order modeling of nonlinear problems using + neural networks.* + Journal of Computational Physics 363 (2018): 55-78. DOI `10.1016/j.jcp.2018.02.037 `_. .. note:: - The specified ``reduction_network`` must contain two methods, - namely ``encode`` for input encoding and ``decode`` for decoding the - former result. The ``interpolation_network`` network ``forward`` output - represents the interpolation of the latent space obtain with + The specified ``reduction_network`` must contain two methods, namely + ``encode`` for input encoding, and ``decode`` for decoding the former + result. The ``interpolation_network`` network ``forward`` output + represents the interpolation of the latent space obtained with ``reduction_network.encode``. .. note:: This solver uses the end-to-end training strategy, i.e. the ``reduction_network`` and ``interpolation_network`` are trained - simultaneously. For reference on this trainig strategy look at: - Pichi, Federico, Beatriz Moya, and Jan S. Hesthaven. - "A graph convolutional autoencoder approach to model order reduction - for parametrized PDEs." Journal of - Computational Physics 501 (2024): 112762. - DOI - `10.1016/j.jcp.2024.112762 `_. + simultaneously. For reference on this trainig strategy look at the + following: + + ..seealso:: + **Original reference**: Pichi, Federico, Beatriz Moya, and Jan S. + Hesthaven. + *A graph convolutional autoencoder approach to model order reduction + for parametrized PDEs.* + Journal of Computational Physics 501 (2024): 112762. + DOI `10.1016/j.jcp.2024.112762 + `_. .. warning:: This solver works only for data-driven model. Hence in the ``problem`` definition the codition must only contain ``input`` (e.g. coefficient parameters, time parameters), and ``target``. - - .. warning:: - This solver does not currently support the possibility to pass - ``extra_feature``. """ def __init__( @@ -96,22 +91,29 @@ def __init__( use_lt=True, ): """ + Initialization of the :class:`ReducedOrderModelSolver` class. + :param AbstractProblem problem: The formualation of the problem. :param torch.nn.Module reduction_network: The reduction network used - for reducing the input space. It must contain two methods, - namely ``encode`` for input encoding and ``decode`` for decoding the + for reducing the input space. It must contain two methods, namely + ``encode`` for input encoding, and ``decode`` for decoding the former result. :param torch.nn.Module interpolation_network: The interpolation network - for interpolating the control parameters to latent space obtain by + for interpolating the control parameters to latent space obtained by the ``reduction_network`` encoding. - :param torch.nn.Module loss: The loss function used as minimizer, - default :class:`torch.nn.MSELoss`. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default is :class:`torch.optim.Adam`. - :param torch.optim.LRScheduler scheduler: Learning - rate scheduler. - :param WeightingInterface weighting: The loss weighting to use. - :param bool use_lt: Using LabelTensors as input during training. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param bool use_lt: If ``True``, the solver uses LabelTensors as input. + Default is ``True``. """ model = torch.nn.ModuleDict( { @@ -146,14 +148,15 @@ def __init__( def forward(self, x): """ - Forward pass implementation for the solver. It finds the encoder - representation by calling ``interpolation_network.forward`` on the - input, and maps this representation to output space by calling - ``reduction_network.decode``. + Forward pass implementation. + It computes the encoder representation by calling the forward method + of the ``interpolation_network`` on the input, and maps it to output + space by calling the decode methode of the ``reduction_network``. - :param torch.Tensor x: Input tensor. + :param x: Input tensor. + :type x: torch.Tensor | LabelTensor :return: Solver solution. - :rtype: torch.Tensor + :rtype: torch.Tensor | LabelTensor """ reduction_network = self.model["reduction_network"] interpolation_network = self.model["interpolation_network"] @@ -161,15 +164,14 @@ def forward(self, x): def loss_data(self, input_pts, output_pts): """ - The data loss for the ReducedOrderModelSolver solver. - It computes the loss between - the network output against the true solution. This function - should not be override if not intentionally. - - :param LabelTensor input_tensor: The input to the neural networks. - :param LabelTensor output_tensor: The true solution to compare the - network solution. - :return: The residual loss averaged on the input coordinates + Compute the data loss by evaluating the loss between the network's + output and the true solution. This method should not be overridden, if + not intentionally. + + :param LabelTensor input_pts: The input points to the neural network. + :param LabelTensor output_pts: The true solution to compare with the + network's output. + :return: The supervised loss, averaged over the number of observations. :rtype: torch.Tensor """ # extract networks diff --git a/pina/solver/solver.py b/pina/solver/solver.py index f671a7ebc..2a173b33d 100644 --- a/pina/solver/solver.py +++ b/pina/solver/solver.py @@ -14,17 +14,19 @@ class SolverInterface(lightning.pytorch.LightningModule, metaclass=ABCMeta): """ - SolverInterface base class. This class is a wrapper of LightningModule. + Abstract base class for PINA solvers. All specific solvers should inherit + from this interface. This class is a wrapper of + :class:`~lightning.pytorch.LightningModule`. """ def __init__(self, problem, weighting, use_lt): """ - :param problem: A problem definition instance. - :type problem: AbstractProblem - :param weighting: The loss weighting to use. - :type weighting: WeightingInterface - :param use_lt: Using LabelTensors as input during training. - :type use_lt: bool + Initialization of the :class:`SolverInterface` class. + + :param AbstractProblem problem: The problem to be solved. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param bool use_lt: If ``True``, the solver uses LabelTensors as input. """ super().__init__() @@ -59,22 +61,24 @@ def __init__(self, problem, weighting, use_lt): self._pina_schedulers = None def _check_solver_consistency(self, problem): + """ + Check the consistency of the solver with the problem formulation. + + :param AbstractProblem problem: The problem to be solved. + """ for condition in problem.conditions.values(): check_consistency(condition, self.accepted_conditions_types) def _optimization_cycle(self, batch): """ - Perform a private optimization cycle by computing the loss for each - condition in the given batch. The loss are later aggregated using the - specific weighting schema. + Aggregate the loss for each condition in the batch. - :param batch: A batch of data, where each element is a tuple containing - a condition name and a dictionary of points. - :type batch: list of tuples (str, dict) - :return: The computed loss for the all conditions in the batch, - cast to a subclass of `torch.Tensor`. It should return a dict + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict containing the condition name and the associated scalar loss. - :rtype: dict(torch.Tensor) + :rtype: dict """ losses = self.optimization_cycle(batch) for name, value in losses.items(): @@ -88,9 +92,9 @@ def training_step(self, batch): """ Solver training step. - :param batch: The batch element in the dataloader. - :type batch: tuple - :return: The sum of the loss functions. + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The loss of the training step. :rtype: LabelTensor """ loss = self._optimization_cycle(batch=batch) @@ -101,8 +105,8 @@ def validation_step(self, batch): """ Solver validation step. - :param batch: The batch element in the dataloader. - :type batch: tuple + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. """ loss = self._optimization_cycle(batch=batch) self.store_log("val_loss", loss, self.get_batch_size(batch)) @@ -111,15 +115,19 @@ def test_step(self, batch): """ Solver test step. - :param batch: The batch element in the dataloader. - :type batch: tuple + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. """ loss = self._optimization_cycle(batch=batch) self.store_log("test_loss", loss, self.get_batch_size(batch)) def store_log(self, name, value, batch_size): """ - TODO + Store the log of the solver. + + :param str name: The name of the log. + :param torch.Tensor value: The value of the log. + :param int batch_size: The size of the batch. """ self.log( @@ -132,49 +140,65 @@ def store_log(self, name, value, batch_size): @abstractmethod def forward(self, *args, **kwargs): """ - TODO + Abstract method for the forward pass implementation. + + :param args: The input tensor. + :type args: torch.Tensor | LabelTensor + :param dict kwargs: Additional keyword arguments. """ @abstractmethod def optimization_cycle(self, batch): """ - Perform an optimization cycle by computing the loss for each condition - in the given batch. + The optimization cycle for the solvers. - :param batch: A batch of data, where each element is a tuple containing - a condition name and a dictionary of points. - :type batch: list of tuples (str, dict) - :return: The computed loss for the all conditions in the batch, - cast to a subclass of `torch.Tensor`. It should return a dict + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict containing the condition name and the associated scalar loss. - :rtype: dict(torch.Tensor) + :rtype: dict """ @property def problem(self): """ - The problem formulation. + The problem instance. + + :return: The problem instance. + :rtype: :class:`~pina.problem.abstract_problem.AbstractProblem` """ return self._pina_problem @property def use_lt(self): """ - Using LabelTensor in training. + Using LabelTensors as input during training. + + :return: The use_lt attribute. + :rtype: bool """ return self._use_lt @property def weighting(self): """ - The weighting mechanism. + The weighting schema. + + :return: The weighting schema. + :rtype: :class:`~pina.loss.weighting_interface.WeightingInterface` """ return self._pina_weighting @staticmethod def get_batch_size(batch): """ - TODO + Get the batch size. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The size of the batch. + :rtype: int """ batch_size = 0 @@ -185,23 +209,29 @@ def get_batch_size(batch): @staticmethod def default_torch_optimizer(): """ - TODO - """ + Set the default optimizer to :class:`torch.optim.Adam`. + :return: The default optimizer. + :rtype: Optimizer + """ return TorchOptimizer(torch.optim.Adam, lr=0.001) @staticmethod def default_torch_scheduler(): """ - TODO + Set the default scheduler to + :class:`torch.optim.lr_scheduler.ConstantLR`. + + :return: The default scheduler. + :rtype: Scheduler """ return TorchScheduler(torch.optim.lr_scheduler.ConstantLR) def on_train_start(self): """ - Hook that is called before training begins. - Used to compile the model if the trainer is set to compile. + This method is called at the start of the training process to compile + the model if the :class:`~pina.trainer.Trainer` ``compile`` is ``True``. """ super().on_train_start() if self.trainer.compile: @@ -209,8 +239,8 @@ def on_train_start(self): def on_test_start(self): """ - Hook that is called before training begins. - Used to compile the model if the trainer is set to compile. + This method is called at the start of the test process to compile + the model if the :class:`~pina.trainer.Trainer` ``compile`` is ``True``. """ super().on_train_start() if self.trainer.compile and not self._check_already_compiled(): @@ -218,7 +248,10 @@ def on_test_start(self): def _check_already_compiled(self): """ - TODO + Check if the model is already compiled. + + :return: ``True`` if the model is already compiled, ``False`` otherwise. + :rtype: bool """ models = self._pina_models @@ -234,7 +267,12 @@ def _check_already_compiled(self): @staticmethod def _perform_compilation(model): """ - TODO + Perform the compilation of the model. + + :param torch.nn.Module model: The model to compile. + :raises Exception: If the compilation fails. + :return: The compiled model. + :rtype: torch.nn.Module """ model_device = next(model.parameters()).device @@ -249,7 +287,9 @@ def _perform_compilation(model): class SingleSolverInterface(SolverInterface, metaclass=ABCMeta): - """TODO""" + """ + Base class for PINA solvers using a single :class:`torch.nn.Module`. + """ def __init__( self, @@ -261,14 +301,19 @@ def __init__( use_lt=True, ): """ - :param problem: A problem definition instance. - :type problem: AbstractProblem - :param model: A torch nn.Module instances. - :type model: torch.nn.Module - :param Optimizer optimizers: A neural network optimizers to use. - :param Scheduler optimizers: A neural network scheduler to use. - :param WeightingInterface weighting: The loss weighting to use. - :param bool use_lt: Using LabelTensors as input during training. + Initialization of the :class:`SingleSolverInterface` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The neural network model to be used. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is + used. Default is ``None``. + :param Scheduler scheduler: The scheduler to be used. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param bool use_lt: If ``True``, the solver uses LabelTensors as input. """ if optimizer is None: optimizer = self.default_torch_optimizer() @@ -292,11 +337,12 @@ def __init__( def forward(self, x): """ - Forward pass implementation for the solver. + Forward pass implementation. - :param torch.Tensor x: Input tensor. + :param x: Input tensor. + :type x: torch.Tensor | LabelTensor :return: Solver solution. - :rtype: torch.Tensor + :rtype: torch.Tensor | LabelTensor """ x = self.model(x) return x @@ -305,52 +351,69 @@ def configure_optimizers(self): """ Optimizer configuration for the solver. - :return: The optimizers and the schedulers - :rtype: tuple(list, list) + :return: The optimizer and the scheduler + :rtype: tuple[list[Optimizer], list[Scheduler]] """ self.optimizer.hook(self.model.parameters()) self.scheduler.hook(self.optimizer) return ([self.optimizer.instance], [self.scheduler.instance]) def _compile_model(self): + """ + Compile the model. + """ if isinstance(self._pina_models[0], torch.nn.ModuleDict): self._compile_module_dict() else: self._compile_single_model() def _compile_module_dict(self): + """ + Compile the model if it is a :class:`torch.nn.ModuleDict`. + """ for name, model in self._pina_models[0].items(): self._pina_models[0][name] = self._perform_compilation(model) def _compile_single_model(self): + """ + Compile the model if it is a single :class:`torch.nn.Module`. + """ self._pina_models[0] = self._perform_compilation(self._pina_models[0]) @property def model(self): """ - Model for training. + The model used for training. + + :return: The model used for training. + :rtype: torch.nn.Module """ return self._pina_models[0] @property def scheduler(self): """ - Scheduler for training. + The scheduler used for training. + + :return: The scheduler used for training. + :rtype: Scheduler """ return self._pina_schedulers[0] @property def optimizer(self): """ - Optimizer for training. + The optimizer used for training. + + :return: The optimizer used for training. + :rtype: Optimizer """ return self._pina_optimizers[0] class MultiSolverInterface(SolverInterface, metaclass=ABCMeta): """ - Multiple Solver base class. This class inherits is a wrapper of - SolverInterface class + Base class for PINA solvers using multiple :class:`torch.nn.Module`. """ def __init__( @@ -363,16 +426,22 @@ def __init__( use_lt=True, ): """ - :param problem: A problem definition instance. - :type problem: AbstractProblem - :param models: Multiple torch nn.Module instances. + Initialization of the :class:`MultiSolverInterface` class. + + :param AbstractProblem problem: The problem to be solved. + :param models: The neural network models to be used. :type model: list[torch.nn.Module] | tuple[torch.nn.Module] - :param list(Optimizer) optimizers: A list of neural network - optimizers to use. - :param list(Scheduler) optimizers: A list of neural network - schedulers to use. - :param WeightingInterface weighting: The loss weighting to use. - :param bool use_lt: Using LabelTensors as input during training. + :param list[Optimizer] optimizers: The optimizers to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used for all + models. Default is ``None``. + :param list[Scheduler] schedulers: The schedulers to be used. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used for all the models. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param bool use_lt: If ``True``, the solver uses LabelTensors as input. + :raises ValueError: If the models are not a list or tuple with length + greater than one. """ if not isinstance(models, (list, tuple)) or len(models) < 2: raise ValueError( @@ -418,10 +487,11 @@ def __init__( self._pina_schedulers = schedulers def configure_optimizers(self): - """Optimizer configuration for the solver. + """ + Optimizer configuration for the solver. - :return: The optimizers and the schedulers - :rtype: tuple(list, list) + :return: The optimizer and the scheduler + :rtype: tuple[list[Optimizer], list[Scheduler]] """ for optimizer, scheduler, model in zip( self.optimizers, self.schedulers, self.models @@ -435,6 +505,9 @@ def configure_optimizers(self): ) def _compile_model(self): + """ + Compile the model. + """ for i, model in enumerate(self._pina_models): if not isinstance(model, torch.nn.ModuleDict): self._pina_models[i] = self._perform_compilation(model) @@ -442,17 +515,29 @@ def _compile_model(self): @property def models(self): """ - The torch model.""" + The models used for training. + + :return: The models used for training. + :rtype: torch.nn.ModuleList + """ return self._pina_models @property def optimizers(self): """ - The torch model.""" + The optimizers used for training. + + :return: The optimizers used for training. + :rtype: list[Optimizer] + """ return self._pina_optimizers @property def schedulers(self): """ - The torch model.""" + The schedulers used for training. + + :return: The schedulers used for training. + :rtype: list[Scheduler] + """ return self._pina_schedulers diff --git a/pina/solver/supervised.py b/pina/solver/supervised.py index 2bfa85890..9a5a5f4f8 100644 --- a/pina/solver/supervised.py +++ b/pina/solver/supervised.py @@ -1,4 +1,4 @@ -"""Module for SupervisedSolver""" +"""Module for the Supervised solver.""" import torch from torch.nn.modules.loss import _Loss @@ -10,31 +10,28 @@ class SupervisedSolver(SingleSolverInterface): r""" - SupervisedSolver solver class. This class implements a SupervisedSolver, + Supervised Solver solver class. This class implements a Supervised Solver, using a user specified ``model`` to solve a specific ``problem``. - The Supervised Solver class aims to find - a map between the input :math:`\mathbf{s}:\Omega\rightarrow\mathbb{R}^m` - and the output :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m`. The input - can be discretised in space (as in :obj:`~pina.solver.rom.ROMe2eSolver`), - or not (e.g. when training Neural Operators). + The Supervised Solver class aims to find a map between the input + :math:`\mathbf{s}:\Omega\rightarrow\mathbb{R}^m` and the output + :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m`. Given a model :math:`\mathcal{M}`, the following loss function is minimized during training: .. math:: \mathcal{L}_{\rm{problem}} = \frac{1}{N}\sum_{i=1}^N - \mathcal{L}(\mathbf{u}_i - \mathcal{M}(\mathbf{v}_i)) + \mathcal{L}(\mathbf{u}_i - \mathcal{M}(\mathbf{v}_i)), - where :math:`\mathcal{L}` is a specific loss function, - default Mean Square Error: + where :math:`\mathcal{L}` is a specific loss function, typically the MSE: .. math:: \mathcal{L}(v) = \| v \|^2_2. - In this context :math:`\mathbf{u}_i` and :math:`\mathbf{v}_i` means that - we are seeking to approximate multiple (discretised) functions given - multiple (discretised) input functions. + In this context, :math:`\mathbf{u}_i` and :math:`\mathbf{v}_i` indicates + the will to approximate multiple (discretised) functions given multiple + (discretised) input functions. """ accepted_conditions_types = InputTargetCondition @@ -50,16 +47,23 @@ def __init__( use_lt=True, ): """ - :param AbstractProblem problem: The formualation of the problem. - :param torch.nn.Module model: The neural network model to use. - :param torch.nn.Module loss: The loss function used as minimizer, - default :class:`torch.nn.MSELoss`. - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default is :class:`torch.optim.Adam`. - :param torch.optim.LRScheduler scheduler: Learning - rate scheduler. - :param WeightingInterface weighting: The loss weighting to use. - :param bool use_lt: Using LabelTensors as input during training. + Initialization of the :class:`SupervisedSolver` class. + + :param AbstractProblem problem: The problem to be solved. + :param torch.nn.Module model: The neural network model to be used. + :param torch.nn.Module loss: The loss function to be minimized. + If `None`, the :class:`torch.nn.MSELoss` loss is used. + Default is `None`. + :param Optimizer optimizer: The optimizer to be used. + If `None`, the :class:`torch.optim.Adam` optimizer is used. + Default is ``None``. + :param Scheduler scheduler: Learning rate scheduler. + If `None`, the :class:`torch.optim.lr_scheduler.ConstantLR` + scheduler is used. Default is ``None``. + :param WeightingInterface weighting: The weighting schema to be used. + If `None`, no weighting schema is used. Default is ``None``. + :param bool use_lt: If ``True``, the solver uses LabelTensors as input. + Default is ``True``. """ if loss is None: loss = torch.nn.MSELoss() @@ -81,16 +85,14 @@ def __init__( def optimization_cycle(self, batch): """ - Perform an optimization cycle by computing the loss for each condition - in the given batch. - - :param batch: A batch of data, where each element is a tuple containing - a condition name and a dictionary of points. - :type batch: list of tuples (str, dict) - :return: The computed loss for the all conditions in the batch, - cast to a subclass of `torch.Tensor`. It should return a dict + The optimization cycle for the solvers. + + :param list[tuple[str, dict]] batch: A batch of data. Each element is a + tuple containing a condition name and a dictionary of points. + :return: The losses computed for all conditions in the batch, casted + to a subclass of :class:`torch.Tensor`. It should return a dict containing the condition name and the associated scalar loss. - :rtype: dict(torch.Tensor) + :rtype: dict """ condition_loss = {} for condition_name, points in batch: @@ -105,16 +107,16 @@ def optimization_cycle(self, batch): def loss_data(self, input_pts, output_pts): """ - The data loss for the Supervised solver. It computes the loss between - the network output against the true solution. This function - should not be override if not intentionally. + Compute the data loss for the Supervised solver by evaluating the loss + between the network's output and the true solution. This method should + not be overridden, if not intentionally. - :param input_pts: The input to the neural networks. + :param input_pts: The input points to the neural network. :type input_pts: LabelTensor | torch.Tensor - :param output_pts: The true solution to compare the - network solution. + :param output_pts: The true solution to compare with the network's + output. :type output_pts: LabelTensor | torch.Tensor - :return: The residual loss. + :return: The supervised loss, averaged over the number of observations. :rtype: torch.Tensor """ return self._loss(self.forward(input_pts), output_pts) @@ -122,6 +124,9 @@ def loss_data(self, input_pts, output_pts): @property def loss(self): """ - Loss for training. + The loss function to be minimized. + + :return: The loss function to be minimized. + :rtype: torch.nn.Module """ return self._loss diff --git a/pina/solvers/__init__.py b/pina/solvers/__init__.py index aaa44b9b0..366b1b7b9 100644 --- a/pina/solvers/__init__.py +++ b/pina/solvers/__init__.py @@ -1,6 +1,4 @@ -""" -Old module for solvers. Deprecated in 0.2.0 . -""" +"""Old module for solvers. Deprecated in 0.2.0 .""" import warnings diff --git a/pina/solvers/pinns/__init__.py b/pina/solvers/pinns/__init__.py index c9012bbc2..362cd628a 100644 --- a/pina/solvers/pinns/__init__.py +++ b/pina/solvers/pinns/__init__.py @@ -1,6 +1,4 @@ -""" -Old module for the PINNs solver. Deprecated in 0.2.0. -""" +"""Old module for the PINNs solver. Deprecated in 0.2.0.""" import warnings diff --git a/pina/trainer.py b/pina/trainer.py index 3fb73bf07..a29152cfc 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -1,4 +1,4 @@ -"""Trainer module.""" +"""Module for the Trainer.""" import sys import torch @@ -10,8 +10,12 @@ class Trainer(lightning.pytorch.Trainer): """ - PINA custom Trainer class which allows to customize standard Lightning - Trainer class for PINNs training. + PINA custom Trainer class to extend the standard Lightning functionality. + + This class enables specific features or behaviors required by the PINA + framework. It modifies the standard + :class:`lightning.pytorch.Trainer ` + class to better support the training process in PINA. """ def __init__( @@ -29,42 +33,35 @@ def __init__( **kwargs, ): """ - Initialize the Trainer class for by calling Lightning costructor and - adding many other functionalities. - - :param solver: A pina:class:`SolverInterface` solver for the - differential problem. - :type solver: SolverInterface - :param batch_size: How many samples per batch to load. - If ``batch_size=None`` all - samples are loaded and data are not batched, defaults to None. - :type batch_size: int | None - :param train_size: Percentage of elements in the train dataset. - :type train_size: float - :param test_size: Percentage of elements in the test dataset. - :type test_size: float - :param val_size: Percentage of elements in the val dataset. - :type val_size: float - :param compile: if True model is compiled before training, - default False. For Windows users compilation is always disabled. - :type compile: bool - :param automatic_batching: if True automatic PyTorch batching is - performed. Please avoid using automatic batching when batch_size is - large, default False. - :type automatic_batching: bool - :param num_workers: Number of worker threads for data loading. - Default 0 (serial loading). - :type num_workers: int - :param pin_memory: Whether to use pinned memory for faster data - transfer to GPU. Default False. - :type pin_memory: bool - :param shuffle: Whether to shuffle the data for training. Default True. - :type pin_memory: bool - - :Keyword Arguments: - The additional keyword arguments specify the training setup - and can be choosen from the `pytorch-lightning - Trainer API `_ + Initialization of the :class:`Trainer` class. + + :param SolverInterface solver: A + :class:`~pina.solver.solver.SolverInterface` solver used to solve a + :class:`~pina.problem.abstract_problem.AbstractProblem`. + :param int batch_size: The number of samples per batch to load. + If ``None``, all samples are loaded and data is not batched. + Default is ``None``. + :param float train_size: The percentage of elements to include in the + training dataset. Default is ``1.0``. + :param float test_size: The percentage of elements to include in the + test dataset. Default is ``0.0``. + :param float val_size: The percentage of elements to include in the + validation dataset. Default is ``0.0``. + :param bool compile: If ``True``, the model is compiled before training. + Default is ``False``. For Windows users, it is always disabled. + :param bool automatic_batching: If ``True``, automatic PyTorch batching + is performed. Avoid using automatic batching when ``batch_size`` is + large. Default is ``False``. + :param int num_workers: The number of worker threads for data loading. + Default is ``0`` (serial loading). + :param bool pin_memory: Whether to use pinned memory for faster data + transfer to GPU. Default is ``False``. + :param bool shuffle: Whether to shuffle the data during training. + Default is ``True``. + :param dict kwargs: Additional keyword arguments that specify the + training setup. These can be selected from the `pytorch-lightning + Trainer API + `_. """ # check consistency for init types self._check_input_consistency( @@ -134,6 +131,11 @@ def __init__( } def _move_to_device(self): + """ + Moves the ``unknown_parameters`` of an instance of + :class:`~pina.problem.abstract_problem.AbstractProblem` to the + :class:`Trainer` device. + """ device = self._accelerator_connector._parallel_devices[0] # move parameters to device pb = self.solver.problem @@ -155,9 +157,33 @@ def _create_datamodule( shuffle, ): """ - This method is used here because is resampling is needed - during training, there is no need to define to touch the - trainer dataloader, just call the method. + This method is designed to handle the creation of a data module when + resampling is needed during training. Instead of manually defining and + modifying the trainer's dataloaders, this method is called to + automatically configure the data module. + + :param float train_size: The percentage of elements to include in the + training dataset. + :param float test_size: The percentage of elements to include in the + test dataset. + :param float val_size: The percentage of elements to include in the + validation dataset. + :param int batch_size: The number of samples per batch to load. + :param bool automatic_batching: Whether to perform automatic batching + with PyTorch. If ``True``, automatic PyTorch batching + is performed, which consists of extracting one element at a time + from the dataset and collating them into a batch. This is useful + when the dataset is too large to fit into memory. On the other hand, + if ``False``, the items are retrieved from the dataset all at once + avoind the overhead of collating them into a batch and reducing the + __getitem__ calls to the dataset. This is useful when the dataset + fits into memory. Avoid using automatic batching when ``batch_size`` + is large. Default is ``False``. + :param bool pin_memory: Whether to use pinned memory for faster data + transfer to GPU. + :param int num_workers: The number of worker threads for data loading. + :param bool shuffle: Whether to shuffle the data during training. + :raises RuntimeError: If not all conditions are sampled. """ if not self.solver.problem.are_all_domains_discretised: error_message = "\n".join( @@ -188,25 +214,41 @@ def _create_datamodule( def train(self, **kwargs): """ - Train the solver method. + Manage the training process of the solver. + + :param dict kwargs: Additional keyword arguments. See `pytorch-lightning + Trainer API `_ + for details. """ return super().fit(self.solver, datamodule=self.data_module, **kwargs) def test(self, **kwargs): """ - Test the solver method. + Manage the test process of the solver. + + :param dict kwargs: Additional keyword arguments. See `pytorch-lightning + Trainer API `_ + for details. """ return super().test(self.solver, datamodule=self.data_module, **kwargs) @property def solver(self): """ - Returning trainer solver. + Get the solver. + + :return: The solver. + :rtype: SolverInterface """ return self._solver @solver.setter def solver(self, solver): + """ + Set the solver. + + :param SolverInterface solver: The solver to set. + """ self._solver = solver @staticmethod @@ -214,7 +256,18 @@ def _check_input_consistency( solver, train_size, test_size, val_size, automatic_batching, compile ): """ - Check the consistency of the input parameters." + Verifies the consistency of the parameters for the solver configuration. + + :param SolverInterface solver: The solver. + :param float train_size: The percentage of elements to include in the + training dataset. + :param float test_size: The percentage of elements to include in the + test dataset. + :param float val_size: The percentage of elements to include in the + validation dataset. + :param bool automatic_batching: Whether to perform automatic batching + with PyTorch. + :param bool compile: If ``True``, the model is compiled before training. """ check_consistency(solver, SolverInterface) @@ -231,8 +284,14 @@ def _check_consistency_and_set_defaults( pin_memory, num_workers, shuffle, batch_size ): """ - Check the consistency of the input parameters and set the default - values. + Checks the consistency of input parameters and sets default values + for missing or invalid parameters. + + :param bool pin_memory: Whether to use pinned memory for faster data + transfer to GPU. + :param int num_workers: The number of worker threads for data loading. + :param bool shuffle: Whether to shuffle the data during training. + :param int batch_size: The number of samples per batch to load. """ if pin_memory is not None: check_consistency(pin_memory, bool) diff --git a/pina/utils.py b/pina/utils.py index 529c98b67..56b329bd9 100644 --- a/pina/utils.py +++ b/pina/utils.py @@ -1,4 +1,4 @@ -"""Utils module.""" +"""Module for utility functions.""" import types from functools import reduce @@ -12,14 +12,15 @@ def custom_warning_format( message, category, filename, lineno, file=None, line=None ): """ - Depewarning custom format. + Custom warning formatting function. :param str message: The warning message. - :param class category: The warning category. - :param str filename: The filename where the warning was raised. - :param int lineno: The line number where the warning was raised. - :param str file: The file object where the warning was raised. - :param inr line: The line where the warning was raised. + :param Warning category: The warning category. + :param str filename: The filename where the warning is raised. + :param int lineno: The line number where the warning is raised. + :param str file: The file object where the warning is raised. + Default is None. + :param int line: The line where the warning is raised. :return: The formatted warning message. :rtype: str """ @@ -27,20 +28,20 @@ def custom_warning_format( def check_consistency(object_, object_instance, subclass=False): - """Helper function to check object inheritance consistency. - Given a specific ``'object'`` we check if the object is - instance of a specific ``'object_instance'``, or in case - ``'subclass=True'`` we check if the object is subclass - if the ``'object_instance'``. - - :param (iterable or class object) object: The object to check the - inheritance - :param Object object_instance: The parent class from where the object - is expected to inherit - :param str object_name: The name of the object - :param bool subclass: Check if is a subclass and not instance - :raises ValueError: If the object does not inherit from the - specified class + """ + Check if an object maintains inheritance consistency. + + This function checks whether a given object is an instance of a specified + class or, if ``subclass=True``, whether it is a subclass of the specified + class. + + :param object: The object to check. + :type object: Iterable | Object + :param Object object_instance: The expected parent class. + :param bool subclass: If True, checks whether ``object_`` is a subclass + of ``object_instance`` instead of an instance. Default is ``False``. + :raises ValueError: If ``object_`` does not inherit from ``object_instance`` + as expected. """ if not isinstance(object_, (list, set, tuple)): object_ = [object_] @@ -59,18 +60,28 @@ def check_consistency(object_, object_instance, subclass=False): def labelize_forward(forward, input_variables, output_variables): """ - Wrapper decorator to allow users to enable or disable the use of - LabelTensors during the forward pass. - - :param forward: The torch.nn.Module forward function. - :type forward: Callable - :param input_variables: The problem input variables. - :type input_variables: list[str] | tuple[str] - :param output_variables: The problem output variables. - :type output_variables: list[str] | tuple[str] + Decorator to enable or disable the use of + :class:`~pina.label_tensor.LabelTensor` during the forward pass. + + :param Callable forward: The forward function of a :class:`torch.nn.Module`. + :param list[str] input_variables: The names of the input variables of a + :class:`~pina.problem.abstract_problem.AbstractProblem`. + :param list[str] output_variables: The names of the output variables of a + :class:`~pina.problem.abstract_problem.AbstractProblem`. + :return: The decorated forward function. + :rtype: Callable """ def wrapper(x): + """ + Decorated forward function. + + :param LabelTensor x: The labelized input of the forward pass of an + instance of :class:`torch.nn.Module`. + :return: The labelized output of the forward pass of an instance of + :class:`torch.nn.Module`. + :rtype: LabelTensor + """ x = x.extract(input_variables) output = forward(x) # keep it like this, directly using LabelTensor(...) raises errors @@ -82,15 +93,33 @@ def wrapper(x): return wrapper -def merge_tensors(tensors): # name to be changed - """TODO""" +def merge_tensors(tensors): + """ + Merge a list of :class:`~pina.label_tensor.LabelTensor` instances into a + single :class:`~pina.label_tensor.LabelTensor` tensor, by applying + iteratively the cartesian product. + + :param list[LabelTensor] tensors: The list of tensors to merge. + :raises ValueError: If the list of tensors is empty. + :return: The merged tensor. + :rtype: LabelTensor + """ if tensors: return reduce(merge_two_tensors, tensors[1:], tensors[0]) raise ValueError("Expected at least one tensor") def merge_two_tensors(tensor1, tensor2): - """TODO""" + """ + Merge two :class:`~pina.label_tensor.LabelTensor` instances into a single + :class:`~pina.label_tensor.LabelTensor` tensor, by applying the cartesian + product. + + :param LabelTensor tensor1: The first tensor to merge. + :param LabelTensor tensor2: The second tensor to merge. + :return: The merged tensor. + :rtype: LabelTensor + """ n1 = tensor1.shape[0] n2 = tensor2.shape[0] @@ -102,12 +131,14 @@ def merge_two_tensors(tensor1, tensor2): def torch_lhs(n, dim): - """Latin Hypercube Sampling torch routine. - Sampling in range $[0, 1)^d$. + """ + The Latin Hypercube Sampling torch routine, sampling in :math:`[0, 1)`$. - :param int n: number of samples - :param int dim: dimensions of latin hypercube - :return: samples + :param int n: The number of points to sample. + :param int dim: The number of dimensions of the sampling space. + :raises TypeError: If `n` or `dim` are not integers. + :raises ValueError: If `dim` is less than 1. + :return: The sampled points. :rtype: torch.tensor """ @@ -137,10 +168,10 @@ def torch_lhs(n, dim): def is_function(f): """ - Checks whether the given object `f` is a function or lambda. + Check if the given object is a function or a lambda. - :param object f: The object to be checked. - :return: `True` if `f` is a function, `False` otherwise. + :param Object f: The object to be checked. + :return: ``True`` if ``f`` is a function, ``False`` otherwise. :rtype: bool """ return isinstance(f, (types.FunctionType, types.LambdaType)) @@ -148,11 +179,11 @@ def is_function(f): def chebyshev_roots(n): """ - Return the roots of *n* Chebyshev polynomials (between [-1, 1]). + Compute the roots of the Chebyshev polynomial of degree ``n``. - :param int n: number of roots - :return: roots - :rtype: torch.tensor + :param int n: The number of roots to return. + :return: The roots of the Chebyshev polynomials. + :rtype: torch.Tensor """ pi = torch.acos(torch.zeros(1)).item() * 2 k = torch.arange(n) diff --git a/tutorials/tutorial1/tutorial.ipynb b/tutorials/tutorial1/tutorial.ipynb index 4a0d205c2..f92dc4d6c 100644 --- a/tutorials/tutorial1/tutorial.ipynb +++ b/tutorials/tutorial1/tutorial.ipynb @@ -80,32 +80,34 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "2373a925", "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import warnings\n", "\n", "from pina.problem import SpatialProblem, TimeDependentProblem\n", "from pina.domain import CartesianDomain\n", "\n", - "warnings.filterwarnings('ignore')\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", "\n", "class TimeSpaceODE(SpatialProblem, TimeDependentProblem):\n", - " \n", - " output_variables = ['u']\n", - " spatial_domain = CartesianDomain({'x': [0, 1]})\n", - " temporal_domain = CartesianDomain({'t': [0, 1]})\n", + "\n", + " output_variables = [\"u\"]\n", + " spatial_domain = CartesianDomain({\"x\": [0, 1]})\n", + " temporal_domain = CartesianDomain({\"t\": [0, 1]})\n", "\n", " # other stuff ..." ] @@ -152,6 +154,7 @@ "from pina.domain import CartesianDomain\n", "from pina.equation import Equation, FixedValue\n", "\n", + "\n", "# defining the ode equation\n", "def ode_equation(input_, output_):\n", "\n", @@ -164,6 +167,7 @@ " # calculate the residual and return it\n", " return u_x - u\n", "\n", + "\n", "class SimpleODE(SpatialProblem):\n", "\n", " output_variables = [\"u\"]\n", diff --git a/tutorials/tutorial1/tutorial.py b/tutorials/tutorial1/tutorial.py new file mode 100644 index 000000000..b6cb93c8e --- /dev/null +++ b/tutorials/tutorial1/tutorial.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Physics Informed Neural Networks on PINA +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial1/tutorial.ipynb) +# + +# In this tutorial, we will demonstrate a typical use case of **PINA** on a toy problem, following the standard API procedure. +# +#

+# PINA API +#

+# +# Specifically, the tutorial aims to introduce the following topics: +# +# * Explaining how to build **PINA** Problems, +# * Showing how to generate data for `PINN` training +# +# These are the two main steps needed **before** starting the modelling optimization (choose model and solver, and train). We will show each step in detail, and at the end, we will solve a simple Ordinary Differential Equation (ODE) problem using the `PINN` solver. + +# ## Build a PINA problem + +# Problem definition in the **PINA** framework is done by building a python `class`, which inherits from one or more problem classes (`SpatialProblem`, `TimeDependentProblem`, `ParametricProblem`, ...) depending on the nature of the problem. Below is an example: +# ### Simple Ordinary Differential Equation +# Consider the following: +# +# $$ +# \begin{equation} +# \begin{cases} +# \frac{d}{dx}u(x) &= u(x) \quad x\in(0,1)\\ +# u(x=0) &= 1 \\ +# \end{cases} +# \end{equation} +# $$ +# +# with the analytical solution $u(x) = e^x$. In this case, our ODE depends only on the spatial variable $x\in(0,1)$ , meaning that our `Problem` class is going to be inherited from the `SpatialProblem` class: +# +# ```python +# from pina.problem import SpatialProblem +# from pina.domain import CartesianProblem +# +# class SimpleODE(SpatialProblem): +# +# output_variables = ['u'] +# spatial_domain = CartesianProblem({'x': [0, 1]}) +# +# # other stuff ... +# ``` +# +# Notice that we define `output_variables` as a list of symbols, indicating the output variables of our equation (in this case only $u$), this is done because in **PINA** the `torch.Tensor`s are labelled, allowing the user maximal flexibility for the manipulation of the tensor. The `spatial_domain` variable indicates where the sample points are going to be sampled in the domain, in this case $x\in[0,1]$. +# +# What if our equation is also time-dependent? In this case, our `class` will inherit from both `SpatialProblem` and `TimeDependentProblem`: +# + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import warnings + +from pina.problem import SpatialProblem, TimeDependentProblem +from pina.domain import CartesianDomain + +warnings.filterwarnings("ignore") + + +class TimeSpaceODE(SpatialProblem, TimeDependentProblem): + + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 1]}) + temporal_domain = CartesianDomain({"t": [0, 1]}) + + # other stuff ... + + +# where we have included the `temporal_domain` variable, indicating the time domain wanted for the solution. +# +# In summary, using **PINA**, we can initialize a problem with a class which inherits from different base classes: `SpatialProblem`, `TimeDependentProblem`, `ParametricProblem`, and so on depending on the type of problem we are considering. Here are some examples (more on the official documentation): +# * ``SpatialProblem`` $\rightarrow$ a differential equation with spatial variable(s) ``spatial_domain`` +# * ``TimeDependentProblem`` $\rightarrow$ a time-dependent differential equation with temporal variable(s) ``temporal_domain`` +# * ``ParametricProblem`` $\rightarrow$ a parametrized differential equation with parametric variable(s) ``parameter_domain`` +# * ``AbstractProblem`` $\rightarrow$ any **PINA** problem inherits from here + +# ### Write the problem class +# +# Once the `Problem` class is initialized, we need to represent the differential equation in **PINA**. In order to do this, we need to load the **PINA** operators from `pina.operator` module. Again, we'll consider Equation (1) and represent it in **PINA**: + +# In[ ]: + + +import torch +import matplotlib.pyplot as plt + +from pina.problem import SpatialProblem +from pina.operator import grad +from pina import Condition +from pina.domain import CartesianDomain +from pina.equation import Equation, FixedValue + + +# defining the ode equation +def ode_equation(input_, output_): + + # computing the derivative + u_x = grad(output_, input_, components=["u"], d=["x"]) + + # extracting the u input variable + u = output_.extract(["u"]) + + # calculate the residual and return it + return u_x - u + + +class SimpleODE(SpatialProblem): + + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 1]}) + + domains = { + "x0": CartesianDomain({"x": 0.0}), + "D": CartesianDomain({"x": [0, 1]}), + } + + # conditions to hold + conditions = { + "bound_cond": Condition(domain="x0", equation=FixedValue(1.0)), + "phys_cond": Condition(domain="D", equation=Equation(ode_equation)), + } + + # defining the true solution + def solution(self, pts): + return torch.exp(pts.extract(["x"])) + + +problem = SimpleODE() + + +# After we define the `Problem` class, we need to write different class methods, where each method is a function returning a residual. These functions are the ones minimized during PINN optimization, given the initial conditions. For example, in the domain $[0,1]$, the ODE equation (`ode_equation`) must be satisfied. We represent this by returning the difference between subtracting the variable `u` from its gradient (the residual), which we hope to minimize to 0. This is done for all conditions. Notice that we do not pass directly a `python` function, but an `Equation` object, which is initialized with the `python` function. This is done so that all the computations and internal checks are done inside **PINA**. +# +# Once we have defined the function, we need to tell the neural network where these methods are to be applied. To do so, we use the `Condition` class. In the `Condition` class, we pass the location points and the equation we want minimized on those points (other possibilities are allowed, see the documentation for reference). +# +# Finally, it's possible to define a `solution` function, which can be useful if we want to plot the results and see how the real solution compares to the expected (true) solution. Notice that the `solution` function is a method of the `PINN` class, but it is not mandatory for problem definition. +# + +# ## Generate data +# +# Data for training can come in form of direct numerical simulation results, or points in the domains. In case we perform unsupervised learning, we just need the collocation points for training, i.e. points where we want to evaluate the neural network. Sampling point in **PINA** is very easy, here we show three examples using the `.discretise_domain` method of the `AbstractProblem` class. + +# In[ ]: + + +# sampling 20 points in [0, 1] through discretization in all locations +problem.discretise_domain(n=20, mode="grid", domains="all") + +# sampling 20 points in (0, 1) through latin hypercube sampling in D, and 1 point in x0 +problem.discretise_domain(n=20, mode="latin", domains=["D"]) +problem.discretise_domain(n=1, mode="random", domains=["x0"]) + +# sampling 20 points in (0, 1) randomly +problem.discretise_domain(n=20, mode="random") + + +# We are going to use latin hypercube points for sampling. We need to sample in all the conditions domains. In our case we sample in `D` and `x0`. + +# In[ ]: + + +# sampling for training +problem.discretise_domain(1, "random", domains=["x0"]) +problem.discretise_domain(20, "lh", domains=["D"]) + + +# The points are saved in a python `dict`, and can be accessed by calling the attribute `input_pts` of the problem + +# In[ ]: + + +print("Input points:", problem.discretised_domains) +print("Input points labels:", problem.discretised_domains["D"].labels) + + +# To visualize the sampled points we can use `matplotlib.pyplot`: + +# In[ ]: + + +for location in problem.input_pts: + coords = ( + problem.input_pts[location].extract(problem.spatial_variables).flatten() + ) + plt.scatter(coords, torch.zeros_like(coords), s=10, label=location) +plt.legend() + + +# ## Perform a small training + +# Once we have defined the problem and generated the data we can start the modelling. Here we will choose a `FeedForward` neural network available in `pina.model`, and we will train using the `PINN` solver from `pina.solver`. We highlight that this training is fairly simple, for more advanced stuff consider the tutorials in the ***Physics Informed Neural Networks*** section of ***Tutorials***. For training we use the `Trainer` class from `pina.trainer`. Here we show a very short training and some method for plotting the results. Notice that by default all relevant metrics (e.g. MSE error during training) are going to be tracked using a `lightning` logger, by default `CSVLogger`. If you want to track the metric by yourself without a logger, use `pina.callback.MetricTracker`. + +# In[ ]: + + +from pina import Trainer +from pina.solver import PINN +from pina.model import FeedForward +from lightning.pytorch.loggers import TensorBoardLogger +from pina.optim import TorchOptimizer + + +# build the model +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) + +# create the PINN object +pinn = PINN(problem, model, TorchOptimizer(torch.optim.Adam, lr=0.005)) + +# create the trainer +trainer = Trainer( + solver=pinn, + max_epochs=1500, + logger=TensorBoardLogger("tutorial_logs"), + accelerator="cpu", + train_size=1.0, + test_size=0.0, + val_size=0.0, + enable_model_summary=False, +) # we train on CPU and avoid model summary at beginning of training (optional) + +# train +trainer.train() + + +# After the training we can inspect trainer logged metrics (by default **PINA** logs mean square error residual loss). The logged metrics can be accessed online using one of the `Lightning` loggers. The final loss can be accessed by `trainer.logged_metrics` + +# In[27]: + + +# inspecting final loss +trainer.logged_metrics + + +# By using `matplotlib` we can also do some qualitative plots of the solution. + +# In[ ]: + + +pts = pinn.problem.spatial_domain.sample(256, "grid", variables="x") +predicted_output = pinn.forward(pts).extract("u").tensor.detach() +true_output = pinn.problem.solution(pts).detach() +fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 8)) +ax.plot(pts.extract(["x"]), predicted_output, label="Neural Network solution") +ax.plot(pts.extract(["x"]), true_output, label="True solution") +plt.legend() + + +# The solution is overlapped with the actual one, and they are barely indistinguishable. We can also take a look at the loss using `TensorBoard`: + +# In[ ]: + + +print("\nTo load TensorBoard run load_ext tensorboard on your terminal") +print( + "To visualize the loss you can run tensorboard --logdir 'tutorial_logs' on your terminal\n" +) +# # uncomment for running tensorboard +# %load_ext tensorboard +# %tensorboard --logdir=tutorial_logs + + +# As we can see the loss has not reached a minimum, suggesting that we could train for longer! Alternatively, we can also take look at the loss using callbacks. Here we use `MetricTracker` from `pina.callback`: + +# In[ ]: + + +from pina.callback import MetricTracker + +# create the model +newmodel = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) + +# create the PINN object +newpinn = PINN( + problem, newmodel, optimizer=TorchOptimizer(torch.optim.Adam, lr=0.005) +) + +# create the trainer +newtrainer = Trainer( + solver=newpinn, + max_epochs=1500, + logger=True, # enable parameter logging + callbacks=[MetricTracker()], + accelerator="cpu", + train_size=1.0, + test_size=0.0, + val_size=0.0, + enable_model_summary=False, +) # we train on CPU and avoid model summary at beginning of training (optional) + +# train +newtrainer.train() + +# plot loss +trainer_metrics = newtrainer.callbacks[0].metrics +loss = trainer_metrics["train_loss"] +epochs = range(len(loss)) +plt.plot(epochs, loss.cpu()) +# plotting +plt.xlabel("epoch") +plt.ylabel("loss") +plt.yscale("log") + + +# ## What's next? +# +# Congratulations on completing the introductory tutorial of **PINA**! There are several directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the finaly accuracy +# +# 2. Train the network using other types of models (see `pina.model`) +# +# 3. GPU training and speed benchmarking +# +# 4. Many more... diff --git a/tutorials/tutorial10/tutorial.ipynb b/tutorials/tutorial10/tutorial.ipynb index 8f9baf797..fa0642d5e 100644 --- a/tutorials/tutorial10/tutorial.ipynb +++ b/tutorials/tutorial10/tutorial.ipynb @@ -19,22 +19,23 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", - " # get the data\n", - " !mkdir \"data\"\n", - " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS.mat\" -O \"data/Data_KS.mat\"\n", - " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS2.mat\" -O \"data/Data_KS2.mat\"\n", + " !pip install \"pina-mathlab\"\n", + " # get the data\n", + " !mkdir \"data\"\n", + " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS.mat\" -O \"data/Data_KS.mat\"\n", + " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS2.mat\" -O \"data/Data_KS2.mat\"\n", "\n", "import torch\n", "import matplotlib.pyplot as plt\n", @@ -46,7 +47,7 @@ "from pina.solver import SupervisedSolver\n", "from pina.problem.zoo import SupervisedProblem\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -106,17 +107,24 @@ ], "source": [ "# load data\n", - "data=io.loadmat(\"data/Data_KS.mat\")\n", + "data = io.loadmat(\"data/Data_KS.mat\")\n", "\n", "# converting to label tensor\n", - "initial_cond_train = LabelTensor(torch.tensor(data['initial_cond_train'], dtype=torch.float), ['t','x','u0'])\n", - "initial_cond_test = LabelTensor(torch.tensor(data['initial_cond_test'], dtype=torch.float), ['t','x','u0'])\n", - "sol_train = LabelTensor(torch.tensor(data['sol_train'], dtype=torch.float), ['u'])\n", - "sol_test = LabelTensor(torch.tensor(data['sol_test'], dtype=torch.float), ['u'])\n", - "\n", - "print('Data Loaded')\n", - "print(f' shape initial condition: {initial_cond_train.shape}')\n", - "print(f' shape solution: {sol_train.shape}')" + "initial_cond_train = LabelTensor(\n", + " torch.tensor(data[\"initial_cond_train\"], dtype=torch.float),\n", + " [\"t\", \"x\", \"u0\"],\n", + ")\n", + "initial_cond_test = LabelTensor(\n", + " torch.tensor(data[\"initial_cond_test\"], dtype=torch.float), [\"t\", \"x\", \"u0\"]\n", + ")\n", + "sol_train = LabelTensor(\n", + " torch.tensor(data[\"sol_train\"], dtype=torch.float), [\"u\"]\n", + ")\n", + "sol_test = LabelTensor(torch.tensor(data[\"sol_test\"], dtype=torch.float), [\"u\"])\n", + "\n", + "print(\"Data Loaded\")\n", + "print(f\" shape initial condition: {initial_cond_train.shape}\")\n", + "print(f\" shape solution: {sol_train.shape}\")" ] }, { @@ -151,35 +159,58 @@ "# helper function\n", "def plot_trajectory(coords, real, no_sol=None):\n", " # find the x-t shapes\n", - " dim_x = len(torch.unique(coords.extract('x')))\n", - " dim_t = len(torch.unique(coords.extract('t')))\n", + " dim_x = len(torch.unique(coords.extract(\"x\")))\n", + " dim_t = len(torch.unique(coords.extract(\"t\")))\n", " # if we don't have the Neural Operator solution we simply plot the real one\n", " if no_sol is None:\n", " fig, axs = plt.subplots(1, 1, figsize=(15, 5), sharex=True, sharey=True)\n", - " c = axs.imshow(real.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto')\n", - " axs.set_title('Real solution')\n", + " c = axs.imshow(\n", + " real.reshape(dim_t, dim_x).T.detach(),\n", + " extent=[0, 50, 0, 64],\n", + " cmap=\"PuOr_r\",\n", + " aspect=\"auto\",\n", + " )\n", + " axs.set_title(\"Real solution\")\n", " fig.colorbar(c, ax=axs)\n", - " axs.set_xlabel('t')\n", - " axs.set_ylabel('x')\n", + " axs.set_xlabel(\"t\")\n", + " axs.set_ylabel(\"x\")\n", " # otherwise we plot the real one, the Neural Operator one, and their difference\n", " else:\n", " fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharex=True, sharey=True)\n", - " axs[0].imshow(real.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto')\n", - " axs[0].set_title('Real solution')\n", - " axs[1].imshow(no_sol.reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto')\n", - " axs[1].set_title('NO solution')\n", - " c = axs[2].imshow((real - no_sol).abs().reshape(dim_t, dim_x).T.detach(),extent=[0, 50, 0, 64], cmap='PuOr_r', aspect='auto')\n", - " axs[2].set_title('Absolute difference')\n", + " axs[0].imshow(\n", + " real.reshape(dim_t, dim_x).T.detach(),\n", + " extent=[0, 50, 0, 64],\n", + " cmap=\"PuOr_r\",\n", + " aspect=\"auto\",\n", + " )\n", + " axs[0].set_title(\"Real solution\")\n", + " axs[1].imshow(\n", + " no_sol.reshape(dim_t, dim_x).T.detach(),\n", + " extent=[0, 50, 0, 64],\n", + " cmap=\"PuOr_r\",\n", + " aspect=\"auto\",\n", + " )\n", + " axs[1].set_title(\"NO solution\")\n", + " c = axs[2].imshow(\n", + " (real - no_sol).abs().reshape(dim_t, dim_x).T.detach(),\n", + " extent=[0, 50, 0, 64],\n", + " cmap=\"PuOr_r\",\n", + " aspect=\"auto\",\n", + " )\n", + " axs[2].set_title(\"Absolute difference\")\n", " fig.colorbar(c, ax=axs.ravel().tolist())\n", " for ax in axs:\n", - " ax.set_xlabel('t')\n", - " ax.set_ylabel('x')\n", + " ax.set_xlabel(\"t\")\n", + " ax.set_ylabel(\"x\")\n", " plt.show()\n", "\n", + "\n", "# a sample trajectory (we use the sample 5, feel free to change)\n", "sample_number = 20\n", - "plot_trajectory(coords=initial_cond_train[sample_number].extract(['x', 't']),\n", - " real=sol_train[sample_number].extract('u'))\n" + "plot_trajectory(\n", + " coords=initial_cond_train[sample_number].extract([\"x\", \"t\"]),\n", + " real=sol_train[sample_number].extract(\"u\"),\n", + ")" ] }, { @@ -300,7 +331,12 @@ ], "source": [ "# initialize problem\n", - "problem = SupervisedProblem(initial_cond_train, sol_train, input_variables=initial_cond_train.labels, output_variables=sol_train.labels)\n", + "problem = SupervisedProblem(\n", + " initial_cond_train,\n", + " sol_train,\n", + " input_variables=initial_cond_train.labels,\n", + " output_variables=sol_train.labels,\n", + ")\n", "# initialize solver\n", "solver = SupervisedSolver(problem=problem, model=model)\n", "# train, only CPU and avoid model summary at beginning of training (optional)\n", @@ -343,9 +379,11 @@ "source": [ "sample_number = 2\n", "no_sol = solver(initial_cond_test)\n", - "plot_trajectory(coords=initial_cond_test[sample_number].extract(['x', 't']),\n", - " real=sol_test[sample_number].extract('u'),\n", - " no_sol=no_sol[5])" + "plot_trajectory(\n", + " coords=initial_cond_test[sample_number].extract([\"x\", \"t\"]),\n", + " real=sol_test[sample_number].extract(\"u\"),\n", + " no_sol=no_sol[5],\n", + ")" ] }, { @@ -373,15 +411,19 @@ "source": [ "from pina.loss import PowerLoss\n", "\n", - "error_metric = PowerLoss(p=2) # we use the MSE loss\n", + "error_metric = PowerLoss(p=2) # we use the MSE loss\n", "\n", "with torch.no_grad():\n", " no_sol_train = solver(initial_cond_train)\n", - " err_train = error_metric(sol_train.extract('u'), no_sol_train).mean() # we average the error over trajectories\n", + " err_train = error_metric(\n", + " sol_train.extract(\"u\"), no_sol_train\n", + " ).mean() # we average the error over trajectories\n", " no_sol_test = solver(initial_cond_test)\n", - " err_test = error_metric(sol_test.extract('u'),no_sol_test).mean() # we average the error over trajectories\n", - " print(f'Training error: {float(err_train):.3f}')\n", - " print(f'Testing error: {float(err_test):.3f}')" + " err_test = error_metric(\n", + " sol_test.extract(\"u\"), no_sol_test\n", + " ).mean() # we average the error over trajectories\n", + " print(f\"Training error: {float(err_train):.3f}\")\n", + " print(f\"Testing error: {float(err_test):.3f}\")" ] }, { diff --git a/tutorials/tutorial10/tutorial.py b/tutorials/tutorial10/tutorial.py new file mode 100644 index 000000000..f5f57db70 --- /dev/null +++ b/tutorials/tutorial10/tutorial.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Averaging Neural Operator for solving Kuramoto Sivashinsky equation +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial10/tutorial.ipynb) +# +# In this tutorial we will build a Neural Operator using the +# `AveragingNeuralOperator` model and the `SupervisedSolver`. At the end of the +# tutorial you will be able to train a Neural Operator for learning +# the operator of time dependent PDEs. +# +# +# First of all, some useful imports. Note we use `scipy` for i/o operations. +# + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + # get the data + get_ipython().system('mkdir "data"') + get_ipython().system( + 'wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS.mat" -O "data/Data_KS.mat"' + ) + get_ipython().system( + 'wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial10/data/Data_KS2.mat" -O "data/Data_KS2.mat"' + ) + +import torch +import matplotlib.pyplot as plt +import warnings + +from scipy import io +from pina import Condition, Trainer, LabelTensor +from pina.model import AveragingNeuralOperator +from pina.solver import SupervisedSolver +from pina.problem.zoo import SupervisedProblem + +warnings.filterwarnings("ignore") + + +# ## Data Generation +# +# We will focus on solving a specific PDE, the **Kuramoto Sivashinsky** (KS) equation. +# The KS PDE is a fourth-order nonlinear PDE with the following form: +# +# $$ +# \frac{\partial u}{\partial t}(x,t) = -u(x,t)\frac{\partial u}{\partial x}(x,t)- \frac{\partial^{4}u}{\partial x^{4}}(x,t) - \frac{\partial^{2}u}{\partial x^{2}}(x,t). +# $$ +# +# In the above $x\in \Omega=[0, 64]$ represents a spatial location, $t\in\mathbb{T}=[0,50]$ the time and $u(x, t)$ is the value of the function $u:\Omega \times\mathbb{T}\in\mathbb{R}$. We indicate with $\mathbb{U}$ a suitable space for $u$, i.e. we have that the solution $u\in\mathbb{U}$. +# +# +# We impose Dirichlet boundary conditions on the derivative of $u$ on the border of the domain $\partial \Omega$ +# $$ +# \frac{\partial u}{\partial x}(x,t)=0 \quad \forall (x,t)\in \partial \Omega\times\mathbb{T}. +# $$ +# +# Initial conditions are sampled from a distribution over truncated Fourier series with random coefficients +# $\{A_k, \ell_k, \phi_k\}_k$ as +# $$ +# u(x,0) = \sum_{k=1}^N A_k \sin(2 \pi \ell_k x / L + \phi_k) \ , +# $$ +# +# where $A_k \in [-0.4, -0.3]$, $\ell_k = 2$, $\phi_k = 2\pi \quad \forall k=1,\dots,N$. +# +# +# We have already generated some data for differenti initial conditions, and our objective will +# be to build a Neural Operator that, given $u(x, t)$ will output $u(x, t+\delta)$, where +# $\delta$ is a fixed time step. We will come back on the Neural Operator architecture, for now +# we first need to import the data. +# +# **Note:** +# *The numerical integration is obtained by using pseudospectral method for spatial derivative discratization and +# implicit Runge Kutta 5 for temporal dynamics.* +# + +# In[2]: + + +# load data +data = io.loadmat("data/Data_KS.mat") + +# converting to label tensor +initial_cond_train = LabelTensor( + torch.tensor(data["initial_cond_train"], dtype=torch.float), + ["t", "x", "u0"], +) +initial_cond_test = LabelTensor( + torch.tensor(data["initial_cond_test"], dtype=torch.float), ["t", "x", "u0"] +) +sol_train = LabelTensor( + torch.tensor(data["sol_train"], dtype=torch.float), ["u"] +) +sol_test = LabelTensor(torch.tensor(data["sol_test"], dtype=torch.float), ["u"]) + +print("Data Loaded") +print(f" shape initial condition: {initial_cond_train.shape}") +print(f" shape solution: {sol_train.shape}") + + +# The data are saved in the form `B \times N \times D`, where `B` is the batch_size +# (basically how many initial conditions we sample), `N` the number of points in the mesh +# (which is the product of the discretization in `x` timese the one in `t`), and +# `D` the dimension of the problem (in this case we have three variables `[u, t, x]`). +# +# We are now going to plot some trajectories! + +# In[3]: + + +# helper function +def plot_trajectory(coords, real, no_sol=None): + # find the x-t shapes + dim_x = len(torch.unique(coords.extract("x"))) + dim_t = len(torch.unique(coords.extract("t"))) + # if we don't have the Neural Operator solution we simply plot the real one + if no_sol is None: + fig, axs = plt.subplots(1, 1, figsize=(15, 5), sharex=True, sharey=True) + c = axs.imshow( + real.reshape(dim_t, dim_x).T.detach(), + extent=[0, 50, 0, 64], + cmap="PuOr_r", + aspect="auto", + ) + axs.set_title("Real solution") + fig.colorbar(c, ax=axs) + axs.set_xlabel("t") + axs.set_ylabel("x") + # otherwise we plot the real one, the Neural Operator one, and their difference + else: + fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharex=True, sharey=True) + axs[0].imshow( + real.reshape(dim_t, dim_x).T.detach(), + extent=[0, 50, 0, 64], + cmap="PuOr_r", + aspect="auto", + ) + axs[0].set_title("Real solution") + axs[1].imshow( + no_sol.reshape(dim_t, dim_x).T.detach(), + extent=[0, 50, 0, 64], + cmap="PuOr_r", + aspect="auto", + ) + axs[1].set_title("NO solution") + c = axs[2].imshow( + (real - no_sol).abs().reshape(dim_t, dim_x).T.detach(), + extent=[0, 50, 0, 64], + cmap="PuOr_r", + aspect="auto", + ) + axs[2].set_title("Absolute difference") + fig.colorbar(c, ax=axs.ravel().tolist()) + for ax in axs: + ax.set_xlabel("t") + ax.set_ylabel("x") + plt.show() + + +# a sample trajectory (we use the sample 5, feel free to change) +sample_number = 20 +plot_trajectory( + coords=initial_cond_train[sample_number].extract(["x", "t"]), + real=sol_train[sample_number].extract("u"), +) + + +# As we can see, as the time progresses the solution becomes chaotic, which makes +# it really hard to learn! We will now focus on building a Neural Operator using the +# `SupervisedSolver` class to tackle the problem. +# +# ## Averaging Neural Operator +# +# We will build a neural operator $\texttt{NO}$ which takes the solution at time $t=0$ for any $x\in\Omega$, +# the time $(t)$ at which we want to compute the solution, and gives back the solution to the KS equation $u(x, t)$, mathematically: +# $$ +# \texttt{NO}_\theta : \mathbb{U} \rightarrow \mathbb{U}, +# $$ +# such that +# $$ +# \texttt{NO}_\theta[u(t=0)](x, t) \rightarrow u(x, t). +# $$ +# +# There are many ways on approximating the following operator, e.g. by 2D [FNO](https://mathlab.github.io/PINA/_rst/models/fno.html) (for regular meshes), +# a [DeepOnet](https://mathlab.github.io/PINA/_rst/models/deeponet.html), [Continuous Convolutional Neural Operator](https://mathlab.github.io/PINA/_rst/layers/convolution.html), +# [MIONet](https://mathlab.github.io/PINA/_rst/models/mionet.html). +# In this tutorial we will use the *Averaging Neural Operator* presented in [*The Nonlocal Neural Operator: Universal Approximation*](https://arxiv.org/abs/2304.13221) +# which is a [Kernel Neural Operator](https://mathlab.github.io/PINA/_rst/models/base_no.html) with integral kernel: +# +# $$ +# K(v) = \sigma\left(Wv(x) + b + \frac{1}{|\Omega|}\int_\Omega v(y)dy\right) +# $$ +# +# where: +# +# * $v(x)\in\mathbb{R}^{\rm{emb}}$ is the update for a function $v$ with $\mathbb{R}^{\rm{emb}}$ the embedding (hidden) size +# * $\sigma$ is a non-linear activation +# * $W\in\mathbb{R}^{\rm{emb}\times\rm{emb}}$ is a tunable matrix. +# * $b\in\mathbb{R}^{\rm{emb}}$ is a tunable bias. +# +# If PINA many Kernel Neural Operators are already implemented, and the modular componets of the [Kernel Neural Operator](https://mathlab.github.io/PINA/_rst/models/base_no.html) class permits to create new ones by composing base kernel layers. +# +# **Note:*** We will use the already built class* `AveragingNeuralOperator`, *as constructive excercise try to use the* [KernelNeuralOperator](https://mathlab.github.io/PINA/_rst/models/base_no.html) *class for building a kernel neural operator from scratch. You might employ the different layers that we have in pina, e.g.* [FeedForward](https://mathlab.github.io/PINA/_rst/models/fnn.html), *and* [AveragingNeuralOperator](https://mathlab.github.io/PINA/_rst/layers/avno_layer.html) *layers*. + +# In[4]: + + +class SIREN(torch.nn.Module): + def forward(self, x): + return torch.sin(x) + + +embedding_dimesion = 40 # hyperparameter embedding dimension +input_dimension = 3 # ['u', 'x', 't'] +number_of_coordinates = 2 # ['x', 't'] +lifting_net = torch.nn.Linear(input_dimension, embedding_dimesion) +projecting_net = torch.nn.Linear(embedding_dimesion + number_of_coordinates, 1) +model = AveragingNeuralOperator( + lifting_net=lifting_net, + projecting_net=projecting_net, + coordinates_indices=["x", "t"], + field_indices=["u0"], + n_layers=4, + func=SIREN, +) + + +# Super easy! Notice that we use the `SIREN` activation function, more on [Implicit Neural Representations with Periodic Activation Functions](https://arxiv.org/abs/2006.09661). +# +# ## Solving the KS problem +# +# We will now focus on solving the KS equation using the `SupervisedSolver` class +# and the `AveragingNeuralOperator` model. As done in the [FNO tutorial](https://github.com/mathLab/PINA/blob/master/tutorials/tutorial5/tutorial.ipynb) we now create the Neural Operator problem class with `SupervisedProblem`. + +# In[5]: + + +# initialize problem +problem = SupervisedProblem( + initial_cond_train, + sol_train, + input_variables=initial_cond_train.labels, + output_variables=sol_train.labels, +) +# initialize solver +solver = SupervisedSolver(problem=problem, model=model) +# train, only CPU and avoid model summary at beginning of training (optional) +trainer = Trainer( + solver=solver, + max_epochs=40, + accelerator="cpu", + enable_model_summary=False, + batch_size=5, # we train on CPU and avoid model summary at beginning of training (optional) + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# We can now see some plots for the solutions + +# In[6]: + + +sample_number = 2 +no_sol = solver(initial_cond_test) +plot_trajectory( + coords=initial_cond_test[sample_number].extract(["x", "t"]), + real=sol_test[sample_number].extract("u"), + no_sol=no_sol[5], +) + + +# As we can see we can obtain nice result considering the small training time and the difficulty of the problem! +# Let's take a look at the training and testing error: + +# In[7]: + + +from pina.loss import PowerLoss + +error_metric = PowerLoss(p=2) # we use the MSE loss + +with torch.no_grad(): + no_sol_train = solver(initial_cond_train) + err_train = error_metric( + sol_train.extract("u"), no_sol_train + ).mean() # we average the error over trajectories + no_sol_test = solver(initial_cond_test) + err_test = error_metric( + sol_test.extract("u"), no_sol_test + ).mean() # we average the error over trajectories + print(f"Training error: {float(err_train):.3f}") + print(f"Testing error: {float(err_test):.3f}") + + +# As we can see the error is pretty small, which agrees with what we can see from the previous plots. + +# ## What's next? +# +# Now you know how to solve a time dependent neural operator problem in **PINA**! There are multiple directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the final accuracy +# +# 2. We left a more challenging dataset [Data_KS2.mat](dat/Data_KS2.mat) where $A_k \in [-0.5, 0.5]$, $\ell_k \in [1, 2, 3]$, $\phi_k \in [0, 2\pi]$ for longer training +# +# 3. Compare the performance between the different neural operators (you can even try to implement your favourite one!) diff --git a/tutorials/tutorial11/tutorial.ipynb b/tutorials/tutorial11/tutorial.ipynb index 3506ca7ec..b9acb6d0c 100644 --- a/tutorials/tutorial11/tutorial.ipynb +++ b/tutorials/tutorial11/tutorial.ipynb @@ -19,17 +19,18 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import torch\n", "import warnings\n", @@ -42,7 +43,7 @@ "from pina.domain import CartesianDomain\n", "from pina.equation import Equation, FixedValue\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -70,6 +71,7 @@ " # calculate the residual and return it\n", " return u_x - u\n", "\n", + "\n", "class SimpleODE(SpatialProblem):\n", "\n", " output_variables = [\"u\"]\n", @@ -101,7 +103,7 @@ " layers=[10, 10],\n", " func=torch.nn.Tanh,\n", " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", + " input_dimensions=len(problem.input_variables),\n", ")\n", "\n", "# create the PINN object\n", @@ -437,20 +439,22 @@ ], "source": [ "model = FeedForward(\n", - " layers=[10, 10],\n", - " func=torch.nn.Tanh,\n", - " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", - " )\n", + " layers=[10, 10],\n", + " func=torch.nn.Tanh,\n", + " output_dimensions=len(problem.output_variables),\n", + " input_dimensions=len(problem.input_variables),\n", + ")\n", "pinn = PINN(problem, model)\n", - "trainer = Trainer(solver=pinn,\n", - " accelerator='cpu',\n", - " logger=True,\n", - " callbacks=[NaiveMetricTracker()], # adding a callbacks\n", - " enable_model_summary=False,\n", - " train_size=1.0,\n", - " val_size=0.0,\n", - " test_size=0.0)\n", + "trainer = Trainer(\n", + " solver=pinn,\n", + " accelerator=\"cpu\",\n", + " logger=True,\n", + " callbacks=[NaiveMetricTracker()], # adding a callbacks\n", + " enable_model_summary=False,\n", + " train_size=1.0,\n", + " val_size=0.0,\n", + " test_size=0.0,\n", + ")\n", "trainer.train()" ] }, @@ -486,7 +490,7 @@ } ], "source": [ - "trainer.callbacks[0].saved_metrics[:3] # only the first three epochs" + "trainer.callbacks[0].saved_metrics[:3] # only the first three epochs" ] }, { @@ -500,7 +504,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -533,6 +537,7 @@ " accelerator=\"cpu\",\n", " max_epochs=-1,\n", " enable_model_summary=False,\n", + " enable_progress_bar=False,\n", " val_size=0.2,\n", " train_size=0.8,\n", " test_size=0.0,\n", @@ -615,19 +620,21 @@ "seed_everything(42, workers=True)\n", "\n", "model = FeedForward(\n", - " layers=[10, 10],\n", - " func=torch.nn.Tanh,\n", - " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", - " )\n", + " layers=[10, 10],\n", + " func=torch.nn.Tanh,\n", + " output_dimensions=len(problem.output_variables),\n", + " input_dimensions=len(problem.input_variables),\n", + ")\n", "\n", "pinn = PINN(problem, model)\n", - "trainer = Trainer(solver=pinn,\n", - " accelerator='cpu',\n", - " deterministic=True, # setting deterministic=True ensure reproducibility when a seed is imposed\n", - " max_epochs = 2000,\n", - " enable_model_summary=False,\n", - " callbacks=[Timer()]) # adding a callbacks\n", + "trainer = Trainer(\n", + " solver=pinn,\n", + " accelerator=\"cpu\",\n", + " deterministic=True, # setting deterministic=True ensure reproducibility when a seed is imposed\n", + " max_epochs=2000,\n", + " enable_model_summary=False,\n", + " callbacks=[Timer()],\n", + ") # adding a callbacks\n", "trainer.train()\n", "print(f'Total training time {trainer.callbacks[0].time_elapsed(\"train\"):.5f} s')" ] @@ -698,19 +705,20 @@ "seed_everything(42, workers=True)\n", "\n", "model = FeedForward(\n", - " layers=[10, 10],\n", - " func=torch.nn.Tanh,\n", - " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", - " )\n", + " layers=[10, 10],\n", + " func=torch.nn.Tanh,\n", + " output_dimensions=len(problem.output_variables),\n", + " input_dimensions=len(problem.input_variables),\n", + ")\n", "pinn = PINN(problem, model)\n", - "trainer = Trainer(solver=pinn,\n", - " accelerator='cpu',\n", - " deterministic=True,\n", - " max_epochs = 2000,\n", - " enable_model_summary=False,\n", - " callbacks=[Timer(),\n", - " StochasticWeightAveraging(swa_lrs=0.005)]) # adding StochasticWeightAveraging callbacks\n", + "trainer = Trainer(\n", + " solver=pinn,\n", + " accelerator=\"cpu\",\n", + " deterministic=True,\n", + " max_epochs=2000,\n", + " enable_model_summary=False,\n", + " callbacks=[Timer(), StochasticWeightAveraging(swa_lrs=0.005)],\n", + ") # adding StochasticWeightAveraging callbacks\n", "trainer.train()\n", "print(f'Total training time {trainer.callbacks[0].time_elapsed(\"train\"):.5f} s')" ] @@ -783,19 +791,20 @@ "seed_everything(42, workers=True)\n", "\n", "model = FeedForward(\n", - " layers=[10, 10],\n", - " func=torch.nn.Tanh,\n", - " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", - " )\n", + " layers=[10, 10],\n", + " func=torch.nn.Tanh,\n", + " output_dimensions=len(problem.output_variables),\n", + " input_dimensions=len(problem.input_variables),\n", + ")\n", "pinn = PINN(problem, model)\n", - "trainer = Trainer(solver=pinn,\n", - " accelerator='cpu',\n", - " max_epochs = 2000,\n", - " enable_model_summary=False,\n", - " gradient_clip_val=0.1, # clipping the gradient\n", - " callbacks=[Timer(),\n", - " StochasticWeightAveraging(swa_lrs=0.005)])\n", + "trainer = Trainer(\n", + " solver=pinn,\n", + " accelerator=\"cpu\",\n", + " max_epochs=2000,\n", + " enable_model_summary=False,\n", + " gradient_clip_val=0.1, # clipping the gradient\n", + " callbacks=[Timer(), StochasticWeightAveraging(swa_lrs=0.005)],\n", + ")\n", "trainer.train()\n", "print(f'Total training time {trainer.callbacks[0].time_elapsed(\"train\"):.5f} s')" ] diff --git a/tutorials/tutorial11/tutorial.py b/tutorials/tutorial11/tutorial.py new file mode 100644 index 000000000..df36aa18a --- /dev/null +++ b/tutorials/tutorial11/tutorial.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: PINA and PyTorch Lightning, training tips and visualizations +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial11/tutorial.ipynb) +# +# In this tutorial, we will delve deeper into the functionality of the `Trainer` class, which serves as the cornerstone for training **PINA** [Solvers](https://mathlab.github.io/PINA/_rst/_code.html#solvers). +# +# The `Trainer` class offers a plethora of features aimed at improving model accuracy, reducing training time and memory usage, facilitating logging visualization, and more thanks to the amazing job done by the PyTorch Lightning team! +# +# Our leading example will revolve around solving the `SimpleODE` problem, as outlined in the [*Introduction to PINA for Physics Informed Neural Networks training*](https://github.com/mathLab/PINA/blob/master/tutorials/tutorial1/tutorial.ipynb). If you haven't already explored it, we highly recommend doing so before diving into this tutorial. +# +# Let's start by importing useful modules, define the `SimpleODE` problem and the `PINN` solver. + +# In[ ]: + + +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import warnings + +from pina import Condition, Trainer +from pina.solver import PINN +from pina.model import FeedForward +from pina.problem import SpatialProblem +from pina.operator import grad +from pina.domain import CartesianDomain +from pina.equation import Equation, FixedValue + +warnings.filterwarnings("ignore") + + +# Define problem and solver. + +# In[2]: + + +# defining the ode equation +def ode_equation(input_, output_): + + # computing the derivative + u_x = grad(output_, input_, components=["u"], d=["x"]) + + # extracting the u input variable + u = output_.extract(["u"]) + + # calculate the residual and return it + return u_x - u + + +class SimpleODE(SpatialProblem): + + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 1]}) + + domains = { + "x0": CartesianDomain({"x": 0.0}), + "D": CartesianDomain({"x": [0, 1]}), + } + + # conditions to hold + conditions = { + "bound_cond": Condition(domain="x0", equation=FixedValue(1.0)), + "phys_cond": Condition(domain="D", equation=Equation(ode_equation)), + } + + # defining the true solution + def solution(self, pts): + return torch.exp(pts.extract(["x"])) + + +# sampling for training +problem = SimpleODE() +problem.discretise_domain(1, "random", domains=["x0"]) +problem.discretise_domain(20, "lh", domains=["D"]) + +# build the model +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) + +# create the PINN object +pinn = PINN(problem, model) + + +# Till now we just followed the extact step of the previous tutorials. The `Trainer` object +# can be initialized by simiply passing the `PINN` solver + +# In[3]: + + +trainer = Trainer(solver=pinn) + + +# ## Trainer Accelerator +# +# When creating the trainer, **by defualt** the `Trainer` will choose the most performing `accelerator` for training which is available in your system, ranked as follow: +# 1. [TPU](https://cloud.google.com/tpu/docs/intro-to-tpu) +# 2. [IPU](https://www.graphcore.ai/products/ipu) +# 3. [HPU](https://habana.ai/) +# 4. [GPU](https://www.intel.com/content/www/us/en/products/docs/processors/what-is-a-gpu.html#:~:text=What%20does%20GPU%20stand%20for,video%20editing%2C%20and%20gaming%20applications) or [MPS](https://developer.apple.com/metal/pytorch/) +# 5. CPU + +# For setting manually the `accelerator` run: +# +# * `accelerator = {'gpu', 'cpu', 'hpu', 'mps', 'cpu', 'ipu'}` sets the accelerator to a specific one + +# In[4]: + + +trainer = Trainer(solver=pinn, accelerator="cpu") + + +# as you can see, even if in the used system `GPU` is available, it is not used since we set `accelerator='cpu'`. + +# ## Trainer Logging +# +# In **PINA** you can log metrics in different ways. The simplest approach is to use the `MetricTraker` class from `pina.callbacks` as seen in the [*Introduction to PINA for Physics Informed Neural Networks training*](https://github.com/mathLab/PINA/blob/master/tutorials/tutorial1/tutorial.ipynb) tutorial. +# +# However, expecially when we need to train multiple times to get an average of the loss across multiple runs, `pytorch_lightning.loggers` might be useful. Here we will use `TensorBoardLogger` (more on [logging](https://lightning.ai/docs/pytorch/stable/extensions/logging.html) here), but you can choose the one you prefer (or make your own one). +# +# We will now import `TensorBoardLogger`, do three runs of training and then visualize the results. Notice we set `enable_model_summary=False` to avoid model summary specifications (e.g. number of parameters), set it to true if needed. +# + +# In[5]: + + +from lightning.pytorch.loggers import TensorBoardLogger + +# three run of training, by default it trains for 1000 epochs +# we reinitialize the model each time otherwise the same parameters will be optimized +for _ in range(3): + model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), + ) + pinn = PINN(problem, model) + trainer = Trainer( + solver=pinn, + accelerator="cpu", + logger=TensorBoardLogger(save_dir="training_log"), + enable_model_summary=False, + train_size=1.0, + val_size=0.0, + test_size=0.0, + ) + trainer.train() + + +# We can now visualize the logs by simply running `tensorboard --logdir=training_log/` on terminal, you should obtain a webpage as the one shown below: + +#

+# \"Logging +#

+ +# as you can see, by default, **PINA** logs the losses which are shown in the progress bar, as well as the number of epochs. You can always insert more loggings by either defining a **callback** ([more on callbacks](https://lightning.ai/docs/pytorch/stable/extensions/callbacks.html)), or inheriting the solver and modify the programs with different **hooks** ([more on hooks](https://lightning.ai/docs/pytorch/stable/common/lightning_module.html#hooks)). + +# ## Trainer Callbacks + +# Whenever we need to access certain steps of the training for logging, do static modifications (i.e. not changing the `Solver`) or updating `Problem` hyperparameters (static variables), we can use `Callabacks`. Notice that `Callbacks` allow you to add arbitrary self-contained programs to your training. At specific points during the flow of execution (hooks), the Callback interface allows you to design programs that encapsulate a full set of functionality. It de-couples functionality that does not need to be in **PINA** `Solver`s. +# Lightning has a callback system to execute them when needed. Callbacks should capture NON-ESSENTIAL logic that is NOT required for your lightning module to run. +# +# The following are best practices when using/designing callbacks. +# +# * Callbacks should be isolated in their functionality. +# * Your callback should not rely on the behavior of other callbacks in order to work properly. +# * Do not manually call methods from the callback. +# * Directly calling methods (eg. on_validation_end) is strongly discouraged. +# * Whenever possible, your callbacks should not depend on the order in which they are executed. +# +# We will try now to implement a naive version of `MetricTraker` to show how callbacks work. Notice that this is a very easy application of callbacks, fortunately in **PINA** we already provide more advanced callbacks in `pina.callbacks`. +# +# + +# In[6]: + + +from lightning.pytorch.callbacks import Callback +from lightning.pytorch.callbacks import EarlyStopping +import torch + + +# define a simple callback +class NaiveMetricTracker(Callback): + def __init__(self): + self.saved_metrics = [] + + def on_train_epoch_end( + self, trainer, __ + ): # function called at the end of each epoch + self.saved_metrics.append( + {key: value for key, value in trainer.logged_metrics.items()} + ) + + +# Let's see the results when applyed to the `SimpleODE` problem. You can define callbacks when initializing the `Trainer` by the `callbacks` argument, which expects a list of callbacks. + +# In[7]: + + +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) +pinn = PINN(problem, model) +trainer = Trainer( + solver=pinn, + accelerator="cpu", + logger=True, + callbacks=[NaiveMetricTracker()], # adding a callbacks + enable_model_summary=False, + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# We can easily access the data by calling `trainer.callbacks[0].saved_metrics` (notice the zero representing the first callback in the list given at initialization). + +# In[8]: + + +trainer.callbacks[0].saved_metrics[:3] # only the first three epochs + + +# PyTorch Lightning also has some built in `Callbacks` which can be used in **PINA**, [here an extensive list](https://lightning.ai/docs/pytorch/stable/extensions/callbacks.html#built-in-callbacks). +# +# We can for example try the `EarlyStopping` routine, which automatically stops the training when a specific metric converged (here the `train_loss`). In order to let the training keep going forever set `max_epochs=-1`. + +# In[ ]: + + +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) +pinn = PINN(problem, model) +trainer = Trainer( + solver=pinn, + accelerator="cpu", + max_epochs=-1, + enable_model_summary=False, + enable_progress_bar=False, + val_size=0.2, + train_size=0.8, + test_size=0.0, + callbacks=[EarlyStopping("val_loss")], +) # adding a callbacks +trainer.train() + + +# As we can see the model automatically stop when the logging metric stopped improving! + +# ## Trainer Tips to Boost Accuracy, Save Memory and Speed Up Training +# +# Untill now we have seen how to choose the right `accelerator`, how to log and visualize the results, and how to interface with the program in order to add specific parts of code at specific points by `callbacks`. +# Now, we well focus on how boost your training by saving memory and speeding it up, while mantaining the same or even better degree of accuracy! +# +# +# There are several built in methods developed in PyTorch Lightning which can be applied straight forward in **PINA**, here we report some: +# +# * [Stochastic Weight Averaging](https://pytorch.org/blog/pytorch-1.6-now-includes-stochastic-weight-averaging/) to boost accuracy +# * [Gradient Clippling](https://deepgram.com/ai-glossary/gradient-clipping) to reduce computational time (and improve accuracy) +# * [Gradient Accumulation](https://lightning.ai/docs/pytorch/stable/common/optimization.html#id3) to save memory consumption +# * [Mixed Precision Training](https://lightning.ai/docs/pytorch/stable/common/optimization.html#id3) to save memory consumption +# +# We will just demonstrate how to use the first two, and see the results compared to a standard training. +# We use the [`Timer`](https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.Timer.html#lightning.pytorch.callbacks.Timer) callback from `pytorch_lightning.callbacks` to take the times. Let's start by training a simple model without any optimization (train for 2000 epochs). + +# In[10]: + + +from lightning.pytorch.callbacks import Timer +from lightning.pytorch import seed_everything + +# setting the seed for reproducibility +seed_everything(42, workers=True) + +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) + +pinn = PINN(problem, model) +trainer = Trainer( + solver=pinn, + accelerator="cpu", + deterministic=True, # setting deterministic=True ensure reproducibility when a seed is imposed + max_epochs=2000, + enable_model_summary=False, + callbacks=[Timer()], +) # adding a callbacks +trainer.train() +print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') + + +# Now we do the same but with StochasticWeightAveraging + +# In[11]: + + +from lightning.pytorch.callbacks import StochasticWeightAveraging + +# setting the seed for reproducibility +seed_everything(42, workers=True) + +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) +pinn = PINN(problem, model) +trainer = Trainer( + solver=pinn, + accelerator="cpu", + deterministic=True, + max_epochs=2000, + enable_model_summary=False, + callbacks=[Timer(), StochasticWeightAveraging(swa_lrs=0.005)], +) # adding StochasticWeightAveraging callbacks +trainer.train() +print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') + + +# As you can see, the training time does not change at all! Notice that around epoch `1600` +# the scheduler is switched from the defalut one `ConstantLR` to the Stochastic Weight Average Learning Rate (`SWALR`). +# This is because by default `StochasticWeightAveraging` will be activated after `int(swa_epoch_start * max_epochs)` with `swa_epoch_start=0.7` by default. Finally, the final `mean_loss` is lower when `StochasticWeightAveraging` is used. +# +# We will now now do the same but clippling the gradient to be relatively small. + +# In[12]: + + +# setting the seed for reproducibility +seed_everything(42, workers=True) + +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) +pinn = PINN(problem, model) +trainer = Trainer( + solver=pinn, + accelerator="cpu", + max_epochs=2000, + enable_model_summary=False, + gradient_clip_val=0.1, # clipping the gradient + callbacks=[Timer(), StochasticWeightAveraging(swa_lrs=0.005)], +) +trainer.train() +print(f'Total training time {trainer.callbacks[0].time_elapsed("train"):.5f} s') + + +# As we can see we by applying gradient clipping we were able to even obtain lower error! +# +# ## What's next? +# +# Now you know how to use efficiently the `Trainer` class **PINA**! There are multiple directions you can go now: +# +# 1. Explore training times on different devices (e.g.) `TPU` +# +# 2. Try to reduce memory cost by mixed precision training and gradient accumulation (especially useful when training Neural Operators) +# +# 3. Benchmark `Trainer` speed for different precisions. diff --git a/tutorials/tutorial12/tutorial.ipynb b/tutorials/tutorial12/tutorial.ipynb index 77538395a..0223da5ae 100644 --- a/tutorials/tutorial12/tutorial.ipynb +++ b/tutorials/tutorial12/tutorial.ipynb @@ -53,16 +53,17 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import torch\n", "\n", - "#useful imports\n", + "# useful imports\n", "from pina import Condition\n", "from pina.problem import SpatialProblem, TimeDependentProblem\n", "from pina.equation import Equation, FixedValue\n", @@ -166,22 +167,23 @@ "outputs": [], "source": [ "class Burgers1DEquation(Equation):\n", - " \n", - " def __init__(self, nu = 0.):\n", + "\n", + " def __init__(self, nu=0.0):\n", " \"\"\"\n", " Burgers1D class. This class can be\n", " used to enforce the solution u to solve the viscous Burgers 1D Equation.\n", - " \n", + "\n", " :param torch.float32 nu: the viscosity coefficient. Default value is set to 0.\n", " \"\"\"\n", - " self.nu = nu \n", - " \n", + " self.nu = nu\n", + "\n", " def equation(input_, output_):\n", - " return grad(output_, input_, d='t') +\\\n", - " output_*grad(output_, input_, d='x') -\\\n", - " self.nu*laplacian(output_, input_, d='x')\n", + " return (\n", + " grad(output_, input_, d=\"t\")\n", + " + output_ * grad(output_, input_, d=\"x\")\n", + " - self.nu * laplacian(output_, input_, d=\"x\")\n", + " )\n", "\n", - " \n", " super().__init__(equation)" ] }, diff --git a/tutorials/tutorial12/tutorial.py b/tutorials/tutorial12/tutorial.py new file mode 100644 index 000000000..300744081 --- /dev/null +++ b/tutorials/tutorial12/tutorial.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: The `Equation` Class +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial12/tutorial.ipynb) + +# In this tutorial, we will show how to use the `Equation` Class in PINA. Specifically, we will see how use the Class and its inherited classes to enforce residuals minimization in PINNs. + +# # Example: The Burgers 1D equation + +# We will start implementing the viscous Burgers 1D problem Class, described as follows: +# +# +# $$ +# \begin{equation} +# \begin{cases} +# \frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} &= \nu \frac{\partial^2 u}{ \partial x^2}, \quad x\in(0,1), \quad t>0\\ +# u(x,0) &= -\sin (\pi x)\\ +# u(x,t) &= 0 \quad x = \pm 1\\ +# \end{cases} +# \end{equation} +# $$ +# +# where we set $ \nu = \frac{0.01}{\pi}$. +# +# In the class that models this problem we will see in action the `Equation` class and one of its inherited classes, the `FixedValue` class. + +# In[1]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch + +# useful imports +from pina import Condition +from pina.problem import SpatialProblem, TimeDependentProblem +from pina.equation import Equation, FixedValue +from pina.domain import CartesianDomain +from pina.operator import grad, laplacian + + +# In[2]: + + +# define the burger equation +def burger_equation(input_, output_): + du = grad(output_, input_) + ddu = grad(du, input_, components=["dudx"]) + return ( + du.extract(["dudt"]) + + output_.extract(["u"]) * du.extract(["dudx"]) + - (0.01 / torch.pi) * ddu.extract(["ddudxdx"]) + ) + + +# define initial condition +def initial_condition(input_, output_): + u_expected = -torch.sin(torch.pi * input_.extract(["x"])) + return output_.extract(["u"]) - u_expected + + +class Burgers1D(TimeDependentProblem, SpatialProblem): + + # assign output/ spatial and temporal variables + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [-1, 1]}) + temporal_domain = CartesianDomain({"t": [0, 1]}) + + domains = { + "bound_cond1": CartesianDomain({"x": -1, "t": [0, 1]}), + "bound_cond2": CartesianDomain({"x": 1, "t": [0, 1]}), + "time_cond": CartesianDomain({"x": [-1, 1], "t": 0}), + "phys_cond": CartesianDomain({"x": [-1, 1], "t": [0, 1]}), + } + # problem condition statement + conditions = { + "bound_cond1": Condition( + domain="bound_cond1", equation=FixedValue(0.0) + ), + "bound_cond2": Condition( + domain="bound_cond2", equation=FixedValue(0.0) + ), + "time_cond": Condition( + domain="time_cond", equation=Equation(initial_condition) + ), + "phys_cond": Condition( + domain="phys_cond", equation=Equation(burger_equation) + ), + } + + +# +# The `Equation` class takes as input a function (in this case it happens twice, with `initial_condition` and `burger_equation`) which computes a residual of an equation, such as a PDE. In a problem class such as the one above, the `Equation` class with such a given input is passed as a parameter in the specified `Condition`. +# +# The `FixedValue` class takes as input a value of same dimensions of the output functions; this class can be used to enforce a fixed value for a specific condition, e.g. Dirichlet boundary conditions, as it happens for instance in our example. +# +# Once the equations are set as above in the problem conditions, the PINN solver will aim to minimize the residuals described in each equation in the training phase. + +# Available classes of equations include also: +# - `FixedGradient` and `FixedFlux`: they work analogously to `FixedValue` class, where we can require a constant value to be enforced, respectively, on the gradient of the solution or the divergence of the solution; +# - `Laplace`: it can be used to enforce the laplacian of the solution to be zero; +# - `SystemEquation`: we can enforce multiple conditions on the same subdomain through this class, passing a list of residual equations defined in the problem. +# + +# # Defining a new Equation class + +# `Equation` classes can be also inherited to define a new class. As example, we can see how to rewrite the above problem introducing a new class `Burgers1D`; during the class call, we can pass the viscosity parameter $\nu$: + +# In[3]: + + +class Burgers1DEquation(Equation): + + def __init__(self, nu=0.0): + """ + Burgers1D class. This class can be + used to enforce the solution u to solve the viscous Burgers 1D Equation. + + :param torch.float32 nu: the viscosity coefficient. Default value is set to 0. + """ + self.nu = nu + + def equation(input_, output_): + return ( + grad(output_, input_, d="t") + + output_ * grad(output_, input_, d="x") + - self.nu * laplacian(output_, input_, d="x") + ) + + super().__init__(equation) + + +# Now we can just pass the above class as input for the last condition, setting $\nu= \frac{0.01}{\pi}$: + +# In[4]: + + +class Burgers1D(TimeDependentProblem, SpatialProblem): + + # define initial condition + def initial_condition(input_, output_): + u_expected = -torch.sin(torch.pi * input_.extract(["x"])) + return output_.extract(["u"]) - u_expected + + # assign output/ spatial and temporal variables + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [-1, 1]}) + temporal_domain = CartesianDomain({"t": [0, 1]}) + + domains = { + "bound_cond1": CartesianDomain({"x": -1, "t": [0, 1]}), + "bound_cond2": CartesianDomain({"x": 1, "t": [0, 1]}), + "time_cond": CartesianDomain({"x": [-1, 1], "t": 0}), + "phys_cond": CartesianDomain({"x": [-1, 1], "t": [0, 1]}), + } + # problem condition statement + conditions = { + "bound_cond1": Condition( + domain="bound_cond1", equation=FixedValue(0.0) + ), + "bound_cond2": Condition( + domain="bound_cond2", equation=FixedValue(0.0) + ), + "time_cond": Condition( + domain="time_cond", equation=Equation(initial_condition) + ), + "phys_cond": Condition( + domain="phys_cond", equation=Burgers1DEquation(nu=0.01 / torch.pi) + ), + } + + +# # What's next? + +# Congratulations on completing the `Equation` class tutorial of **PINA**! As we have seen, you can build new classes that inherit `Equation` to store more complex equations, as the Burgers 1D equation, only requiring to pass the characteristic coefficients of the problem. +# From now on, you can: +# - define additional complex equation classes (e.g. `SchrodingerEquation`, `NavierStokeEquation`..) +# - define more `FixedOperator` (e.g. `FixedCurl`) diff --git a/tutorials/tutorial13/tutorial.ipynb b/tutorials/tutorial13/tutorial.ipynb index 5bd059d56..765ca479f 100644 --- a/tutorials/tutorial13/tutorial.ipynb +++ b/tutorials/tutorial13/tutorial.ipynb @@ -19,18 +19,19 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import torch\n", "import matplotlib.pyplot as plt\n", @@ -46,7 +47,7 @@ "from pina.model import FeedForward\n", "from pina.model.block import FourierFeatureEmbedding\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -312,9 +313,13 @@ "l2_loss = LpLoss(p=2, relative=False)\n", "\n", "# sample new test points\n", - "pts = pts = problem.spatial_domain.sample(100, 'grid')\n", - "print(f'Relative l2 error PINN {l2_loss(pinn(pts), problem.solution(pts)).item():.2%}')\n", - "print(f'Relative l2 error SAPINN {l2_loss(sapinn(pts), problem.solution(pts)).item():.2%}')" + "pts = pts = problem.spatial_domain.sample(100, \"grid\")\n", + "print(\n", + " f\"Relative l2 error PINN {l2_loss(pinn(pts), problem.solution(pts)).item():.2%}\"\n", + ")\n", + "print(\n", + " f\"Relative l2 error SAPINN {l2_loss(sapinn(pts), problem.solution(pts)).item():.2%}\"\n", + ")" ] }, { @@ -462,12 +467,14 @@ } ], "source": [ - "#plot solution obtained\n", - "plot_solution(multiscale_pinn, 'Multiscale PINN solution')\n", + "# plot solution obtained\n", + "plot_solution(multiscale_pinn, \"Multiscale PINN solution\")\n", "\n", "# sample new test points\n", - "pts = pts = problem.spatial_domain.sample(100, 'grid')\n", - "print(f'Relative l2 error PINN with MultiscaleFourierNet: {l2_loss(multiscale_pinn(pts), problem.solution(pts)).item():.2%}')" + "pts = pts = problem.spatial_domain.sample(100, \"grid\")\n", + "print(\n", + " f\"Relative l2 error PINN with MultiscaleFourierNet: {l2_loss(multiscale_pinn(pts), problem.solution(pts)).item():.2%}\"\n", + ")" ] }, { diff --git a/tutorials/tutorial13/tutorial.py b/tutorials/tutorial13/tutorial.py new file mode 100644 index 000000000..257e79537 --- /dev/null +++ b/tutorials/tutorial13/tutorial.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Multiscale PDE learning with Fourier Feature Network +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial13/tutorial.ipynb) +# +# This tutorial presents how to solve with Physics-Informed Neural Networks (PINNs) +# a PDE characterized by multiscale behaviour, as +# presented in [*On the eigenvector bias of Fourier feature networks: From regression to solving +# multi-scale PDEs with physics-informed neural networks*]( +# https://doi.org/10.1016/j.cma.2021.113938). +# +# First of all, some useful imports. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import matplotlib.pyplot as plt +import warnings + +from pina import Condition, Trainer +from pina.problem import SpatialProblem +from pina.operator import laplacian +from pina.solver import PINN, SelfAdaptivePINN as SAPINN +from pina.loss import LpLoss +from pina.domain import CartesianDomain +from pina.equation import Equation, FixedValue +from pina.model import FeedForward +from pina.model.block import FourierFeatureEmbedding + +warnings.filterwarnings("ignore") + + +# ## Multiscale Problem +# +# We begin by presenting the problem which also can be found in Section 2 of [*On the eigenvector bias of Fourier feature networks: From regression to solving +# multi-scale PDEs with physics-informed neural networks*]( +# https://doi.org/10.1016/j.cma.2021.113938). The one-dimensional Poisson problem we aim to solve is mathematically written as: +# +# \begin{equation} +# \begin{cases} +# \Delta u (x) + f(x) = 0 \quad x \in [0,1], \\ +# u(x) = 0 \quad x \in \partial[0,1], \\ +# \end{cases} +# \end{equation} +# +# We impose the solution as $u(x) = \sin(2\pi x) + 0.1 \sin(50\pi x)$ and obtain the force term $f(x) = (2\pi)^2 \sin(2\pi x) + 0.1 (50 \pi)^2 \sin(50\pi x)$. +# Though this example is simple and pedagogical, it is worth noting that +# the solution exhibits low frequency in the macro-scale and high frequency in the micro-scale, which resembles many +# practical scenarios. +# +# +# In **PINA** this problem is written, as always, as a class [see here for a tutorial on the Problem class](https://mathlab.github.io/PINA/_rst/tutorials/tutorial1/tutorial.html). Below you can find the `Poisson` problem which is mathmatically described above. + +# In[2]: + + +class Poisson(SpatialProblem): + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 1]}) + + def poisson_equation(input_, output_): + x = input_.extract("x") + u_xx = laplacian(output_, input_, components=["u"], d=["x"]) + f = ((2 * torch.pi) ** 2) * torch.sin(2 * torch.pi * x) + 0.1 * ( + (50 * torch.pi) ** 2 + ) * torch.sin(50 * torch.pi * x) + return u_xx + f + + domains = { + "bound_cond0": CartesianDomain({"x": 0.0}), + "bound_cond1": CartesianDomain({"x": 1.0}), + "phys_cond": spatial_domain, + } + # here we write the problem conditions + conditions = { + "bound_cond0": Condition( + domain="bound_cond0", equation=FixedValue(0.0) + ), + "bound_cond1": Condition( + domain="bound_cond1", equation=FixedValue(0.0) + ), + "phys_cond": Condition( + domain="phys_cond", equation=Equation(poisson_equation) + ), + } + + def solution(self, x): + return torch.sin(2 * torch.pi * x) + 0.1 * torch.sin(50 * torch.pi * x) + + +problem = Poisson() + +# let's discretise the domain +problem.discretise_domain(128, "grid", domains=["phys_cond"]) +problem.discretise_domain(1, "grid", domains=["bound_cond0", "bound_cond1"]) + + +# A standard PINN approach would be to fit this model using a Feed Forward (fully connected) Neural Network. For a conventional fully-connected neural network is easy to +# approximate a function $u$, given sufficient data inside the computational domain. However solving high-frequency or multi-scale problems presents great challenges to PINNs especially when the number of data cannot capture the different scales. +# +# Below we run a simulation using the `PINN` solver and the self adaptive `SAPINN` solver, using a [`FeedForward`](https://mathlab.github.io/PINA/_modules/pina/model/feed_forward.html#FeedForward) model. + +# In[3]: + + +# training with PINN and visualize results +pinn = PINN( + problem=problem, + model=FeedForward( + input_dimensions=1, output_dimensions=1, layers=[100, 100, 100] + ), +) + +trainer = Trainer( + pinn, + max_epochs=1500, + accelerator="cpu", + enable_model_summary=False, + val_size=0.0, + train_size=1.0, + test_size=0.0, +) +trainer.train() + +# training with PINN and visualize results +sapinn = SAPINN( + problem=problem, + model=FeedForward( + input_dimensions=1, output_dimensions=1, layers=[100, 100, 100] + ), +) +trainer_sapinn = Trainer( + sapinn, + max_epochs=1500, + accelerator="cpu", + enable_model_summary=False, + val_size=0.0, + train_size=1.0, + test_size=0.0, +) +trainer_sapinn.train() + + +# In[4]: + + +# define the function to plot the solution obtained using matplotlib +def plot_solution(pinn_to_use, title): + pts = pinn_to_use.problem.spatial_domain.sample(256, "grid", variables="x") + predicted_output = pinn_to_use.forward(pts).extract("u").tensor.detach() + true_output = pinn_to_use.problem.solution(pts).detach() + plt.plot( + pts.extract(["x"]), predicted_output, label="Neural Network solution" + ) + plt.plot(pts.extract(["x"]), true_output, label="True solution") + plt.title(title) + plt.legend() + + +# plot the solution of the two PINNs +plot_solution(pinn, "PINN solution") +plt.figure() +plot_solution(sapinn, "Self Adaptive PINN solution") + + +# We can clearly see that the solution has not been learned by the two different solvers. Indeed the big problem is not in the optimization strategy (i.e. the solver), but in the model used to solve the problem. A simple `FeedForward` network can hardly handle multiscales if not enough collocation points are used! +# +# We can also compute the $l_2$ relative error for the `PINN` and `SAPINN` solutions: + +# In[5]: + + +# l2 loss from PINA losses +l2_loss = LpLoss(p=2, relative=False) + +# sample new test points +pts = pts = problem.spatial_domain.sample(100, "grid") +print( + f"Relative l2 error PINN {l2_loss(pinn(pts), problem.solution(pts)).item():.2%}" +) +print( + f"Relative l2 error SAPINN {l2_loss(sapinn(pts), problem.solution(pts)).item():.2%}" +) + + +# Which is indeed very high! + +# ## Fourier Feature Embedding in PINA + +# Fourier Feature Embedding is a way to transform the input features, to help the network in learning multiscale variations in the output. It was +# first introduced in [*On the eigenvector bias of Fourier feature networks: From regression to solving +# multi-scale PDEs with physics-informed neural networks*]( +# https://doi.org/10.1016/j.cma.2021.113938) showing great results for multiscale problems. The basic idea is to map the input $\mathbf{x}$ into an embedding $\tilde{\mathbf{x}}$ where: +# +# $$ \tilde{\mathbf{x}} =\left[\cos\left( \mathbf{B} \mathbf{x} \right), \sin\left( \mathbf{B} \mathbf{x} \right)\right] $$ +# +# and $\mathbf{B}_{ij} \sim \mathcal{N}(0, \sigma^2)$. This simple operation allow the network to learn on multiple scales! +# +# In PINA we already have implemented the feature as a `layer` called [`FourierFeatureEmbedding`](https://mathlab.github.io/PINA/_rst/layers/fourier_embedding.html). Below we will build the *Multi-scale Fourier Feature Architecture*. In this architecture multiple Fourier feature embeddings (initialized with different $\sigma$) +# are applied to input coordinates and then passed through the same fully-connected neural network, before the outputs are finally concatenated with a linear layer. + +# In[6]: + + +class MultiscaleFourierNet(torch.nn.Module): + def __init__(self): + super().__init__() + self.embedding1 = FourierFeatureEmbedding( + input_dimension=1, output_dimension=100, sigma=1 + ) + self.embedding2 = FourierFeatureEmbedding( + input_dimension=1, output_dimension=100, sigma=10 + ) + self.layers = FeedForward( + input_dimensions=100, output_dimensions=100, layers=[100] + ) + self.final_layer = torch.nn.Linear(2 * 100, 1) + + def forward(self, x): + e1 = self.layers(self.embedding1(x)) + e2 = self.layers(self.embedding2(x)) + return self.final_layer(torch.cat([e1, e2], dim=-1)) + + +# We will train the `MultiscaleFourierNet` with the `PINN` solver (and feel free to try also with our PINN variants (`SAPINN`, `GPINN`, `CompetitivePINN`, ...). + +# In[7]: + + +multiscale_pinn = PINN(problem=problem, model=MultiscaleFourierNet()) +trainer = Trainer( + multiscale_pinn, + max_epochs=1500, + accelerator="cpu", + enable_model_summary=False, + val_size=0.0, + train_size=1.0, + test_size=0.0, +) +trainer.train() + + +# Let us now plot the solution and compute the relative $l_2$ again! + +# In[8]: + + +# plot solution obtained +plot_solution(multiscale_pinn, "Multiscale PINN solution") + +# sample new test points +pts = pts = problem.spatial_domain.sample(100, "grid") +print( + f"Relative l2 error PINN with MultiscaleFourierNet: {l2_loss(multiscale_pinn(pts), problem.solution(pts)).item():.2%}" +) + + +# It is pretty clear that the network has learned the correct solution, with also a very low error. Obviously a longer training and a more expressive neural network could improve the results! +# +# ## What's next? +# +# Congratulations on completing the one dimensional Poisson tutorial of **PINA** using `FourierFeatureEmbedding`! There are multiple directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the finaly accuracy +# +# 2. Understand the role of `sigma` in `FourierFeatureEmbedding` (see original paper for a nice reference) +# +# 3. Code the *Spatio-temporal multi-scale Fourier feature architecture* for a more complex time dependent PDE (section 3 of the original reference) +# +# 4. Many more... diff --git a/tutorials/tutorial14/tutorial.ipynb b/tutorials/tutorial14/tutorial.ipynb index 312e725d7..da1c02013 100644 --- a/tutorials/tutorial14/tutorial.ipynb +++ b/tutorials/tutorial14/tutorial.ipynb @@ -27,18 +27,19 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "%matplotlib inline\n", "\n", @@ -50,7 +51,7 @@ "from pina.model.block import PODBlock, RBFBlock\n", "from pina import LabelTensor\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -70,6 +71,7 @@ "source": [ "import smithers\n", "from smithers.dataset import LidCavity\n", + "\n", "dataset = LidCavity()" ] }, @@ -108,13 +110,13 @@ ], "source": [ "fig, axs = plt.subplots(1, 3, figsize=(14, 3))\n", - "for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots['mag(v)'][:3]):\n", + "for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots[\"mag(v)\"][:3]):\n", " ax.tricontourf(dataset.triang, u, levels=16)\n", - " ax.set_title(f'$u$ field for $\\mu$ = {par[0]:.4f}')\n", + " ax.set_title(f\"$u$ field for $\\mu$ = {par[0]:.4f}\")\n", "fig, axs = plt.subplots(1, 3, figsize=(14, 3))\n", - "for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots['p'][:3]):\n", + "for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots[\"p\"][:3]):\n", " ax.tricontourf(dataset.triang, u, levels=16)\n", - " ax.set_title(f'$p$ field for $\\mu$ = {par[0]:.4f}')" + " ax.set_title(f\"$p$ field for $\\mu$ = {par[0]:.4f}\")" ] }, { @@ -130,15 +132,16 @@ "metadata": {}, "outputs": [], "source": [ - "'''velocity magnitude data, 5041 for each snapshot'''\n", - "u=torch.tensor(dataset.snapshots['mag(v)']).float() \n", - "u = LabelTensor(u, labels=[f's{i}' for i in range(u.shape[1])])\n", - "'''pressure data, 5041 for each snapshot'''\n", - "p=torch.tensor(dataset.snapshots['p']).float()\n", - "p = LabelTensor(p, labels=[f's{i}' for i in range(p.shape[1])])\n", - "'''mu corresponding to each snapshot'''\n", - "mu=torch.tensor(dataset.params).float()\n", - "mu = LabelTensor(mu, labels=['mu'])\n" + "\"\"\"velocity magnitude data, 5041 for each snapshot\"\"\"\n", + "\n", + "u = torch.tensor(dataset.snapshots[\"mag(v)\"]).float()\n", + "u = LabelTensor(u, labels=[f\"s{i}\" for i in range(u.shape[1])])\n", + "\"\"\"pressure data, 5041 for each snapshot\"\"\"\n", + "p = torch.tensor(dataset.snapshots[\"p\"]).float()\n", + "p = LabelTensor(p, labels=[f\"s{i}\" for i in range(p.shape[1])])\n", + "\"\"\"mu corresponding to each snapshot\"\"\"\n", + "mu = torch.tensor(dataset.params).float()\n", + "mu = LabelTensor(mu, labels=[\"mu\"])" ] }, { @@ -154,15 +157,16 @@ "metadata": {}, "outputs": [], "source": [ - "'''number of snapshots'''\n", + "\"\"\"number of snapshots\"\"\"\n", + "\n", "n = u.shape[0]\n", - "'''training over total snapshots ratio and number of training snapshots'''\n", - "ratio = 0.9 \n", - "n_train = int(n*ratio)\n", - "'''split u and p data'''\n", - "u_train, u_test = u[:n_train], u[n_train:] #for mag(v)\n", - "p_train, p_test = p[:n_train], p[n_train:] #for p\n", - "'''split snapshots'''\n", + "\"\"\"training over total snapshots ratio and number of training snapshots\"\"\"\n", + "ratio = 0.9\n", + "n_train = int(n * ratio)\n", + "\"\"\"split u and p data\"\"\"\n", + "u_train, u_test = u[:n_train], u[n_train:] # for mag(v)\n", + "p_train, p_test = p[:n_train], p[n_train:] # for p\n", + "\"\"\"split snapshots\"\"\"\n", "mu_train, mu_test = mu[:n_train], mu[n_train:]" ] }, @@ -183,8 +187,9 @@ " \"\"\"\n", " Proper orthogonal decomposition with Radial Basis Function interpolation model.\n", " \"\"\"\n", + "\n", " def __init__(self, pod_rank, rbf_kernel):\n", - " \n", + "\n", " super().__init__()\n", " self.pod = PODBlock(pod_rank)\n", " self.rbf = RBFBlock(kernel=rbf_kernel)" @@ -207,8 +212,9 @@ " \"\"\"\n", " Proper orthogonal decomposition with Radial Basis Function interpolation model.\n", " \"\"\"\n", + "\n", " def __init__(self, pod_rank, rbf_kernel):\n", - " \n", + "\n", " super().__init__()\n", " self.pod = PODBlock(pod_rank)\n", " self.rbf = RBFBlock(kernel=rbf_kernel)\n", @@ -223,6 +229,7 @@ " \"\"\"\n", " coefficients = self.rbf(x)\n", " return self.pod.expand(coefficients)\n", + "\n", " def fit(self, p, x):\n", " \"\"\"\n", " Call the :meth:`pina.model.layers.PODBlock.fit` method of the\n", @@ -231,8 +238,7 @@ " :attr:`pina.model.layers.RBFBlock` attribute to fit the interpolation.\n", " \"\"\"\n", " self.pod.fit(x)\n", - " self.rbf.fit(p, self.pod.reduce(x))\n", - " " + " self.rbf.fit(p, self.pod.reduce(x))" ] }, { @@ -248,15 +254,16 @@ "metadata": {}, "outputs": [], "source": [ - "'''create the model'''\n", - "pod_rbfu = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline')\n", + "\"\"\"create the model\"\"\"\n", "\n", - "'''fit the model to velocity training data'''\n", + "pod_rbfu = PODRBF(pod_rank=20, rbf_kernel=\"thin_plate_spline\")\n", + "\n", + "\"\"\"fit the model to velocity training data\"\"\"\n", "pod_rbfu.fit(mu_train, u_train)\n", "\n", - "'''predict the parameter using the fitted model'''\n", + "\"\"\"predict the parameter using the fitted model\"\"\"\n", "u_train_rbf = pod_rbfu(mu_train)\n", - "u_test_rbf = pod_rbfu(mu_test)\n" + "u_test_rbf = pod_rbfu(mu_test)" ] }, { @@ -282,12 +289,12 @@ } ], "source": [ - "relative_u_error_train = torch.norm(u_train_rbf - u_train)/torch.norm(u_train)\n", - "relative_u_error_test = torch.norm(u_test_rbf - u_test)/torch.norm(u_test)\n", + "relative_u_error_train = torch.norm(u_train_rbf - u_train) / torch.norm(u_train)\n", + "relative_u_error_test = torch.norm(u_test_rbf - u_test) / torch.norm(u_test)\n", "\n", - "print('Error summary for POD-RBF model:')\n", - "print(f' Train: {relative_u_error_train.item():e}')\n", - "print(f' Test: {relative_u_error_test.item():e}')" + "print(\"Error summary for POD-RBF model:\")\n", + "print(f\" Train: {relative_u_error_train.item():e}\")\n", + "print(f\" Test: {relative_u_error_test.item():e}\")" ] }, { @@ -323,23 +330,32 @@ "fig, axs = plt.subplots(3, 4, figsize=(14, 10))\n", "\n", "relative_u_error_rbf = np.abs(u_test[idx] - u_idx_rbf.detach())\n", - "relative_u_error_rbf = np.where(u_test[idx] < 1e-7, 1e-7, relative_u_error_rbf/u_test[idx])\n", - " \n", - "for i, (idx_, rbf_, rbf_err_) in enumerate(\n", - " zip(idx, u_idx_rbf, relative_u_error_rbf)):\n", - " axs[0, i].set_title('Prediction for ' f'$\\mu$ = {mu_test[idx_].item():.4f}')\n", - " axs[1, i].set_title('True snapshot for ' f'$\\mu$ = {mu_test[idx_].item():.4f}')\n", - " axs[2, i].set_title('Error for ' f'$\\mu$ = {mu_test[idx_].item():.4f}')\n", + "relative_u_error_rbf = np.where(\n", + " u_test[idx] < 1e-7, 1e-7, relative_u_error_rbf / u_test[idx]\n", + ")\n", "\n", - " cm = axs[0, i].tricontourf(dataset.triang, rbf_.detach()) # POD-RBF prediction\n", + "for i, (idx_, rbf_, rbf_err_) in enumerate(\n", + " zip(idx, u_idx_rbf, relative_u_error_rbf)\n", + "):\n", + " axs[0, i].set_title(\"Prediction for \" f\"$\\mu$ = {mu_test[idx_].item():.4f}\")\n", + " axs[1, i].set_title(\n", + " \"True snapshot for \" f\"$\\mu$ = {mu_test[idx_].item():.4f}\"\n", + " )\n", + " axs[2, i].set_title(\"Error for \" f\"$\\mu$ = {mu_test[idx_].item():.4f}\")\n", + "\n", + " cm = axs[0, i].tricontourf(\n", + " dataset.triang, rbf_.detach()\n", + " ) # POD-RBF prediction\n", " plt.colorbar(cm, ax=axs[0, i])\n", - " \n", - " cm = axs[1, i].tricontourf(dataset.triang, u_test[idx_].flatten()) # Truth\n", + "\n", + " cm = axs[1, i].tricontourf(dataset.triang, u_test[idx_].flatten()) # Truth\n", " plt.colorbar(cm, ax=axs[1, i])\n", "\n", - " cm = axs[2, i].tripcolor(dataset.triang, rbf_err_, norm=matplotlib.colors.LogNorm()) # Error for POD-RBF\n", + " cm = axs[2, i].tripcolor(\n", + " dataset.triang, rbf_err_, norm=matplotlib.colors.LogNorm()\n", + " ) # Error for POD-RBF\n", " plt.colorbar(cm, ax=axs[2, i])\n", - " \n", + "\n", "plt.show()" ] }, @@ -366,22 +382,23 @@ } ], "source": [ - "'''create the model'''\n", - "pod_rbfp = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline')\n", + "\"\"\"create the model\"\"\"\n", + "\n", + "pod_rbfp = PODRBF(pod_rank=20, rbf_kernel=\"thin_plate_spline\")\n", "\n", - "'''fit the model to pressure training data'''\n", + "\"\"\"fit the model to pressure training data\"\"\"\n", "pod_rbfp.fit(mu_train, p_train)\n", "\n", - "'''predict the parameter using the fitted model'''\n", + "\"\"\"predict the parameter using the fitted model\"\"\"\n", "p_train_rbf = pod_rbfp(mu_train)\n", "p_test_rbf = pod_rbfp(mu_test)\n", "\n", - "relative_p_error_train = torch.norm(p_train_rbf - p_train)/torch.norm(p_train)\n", - "relative_p_error_test = torch.norm(p_test_rbf - p_test)/torch.norm(p_test)\n", + "relative_p_error_train = torch.norm(p_train_rbf - p_train) / torch.norm(p_train)\n", + "relative_p_error_test = torch.norm(p_test_rbf - p_test) / torch.norm(p_test)\n", "\n", - "print('Error summary for POD-RBF model:')\n", - "print(f' Train: {relative_p_error_train.item():e}')\n", - "print(f' Test: {relative_p_error_test.item():e}')" + "print(\"Error summary for POD-RBF model:\")\n", + "print(f\" Train: {relative_p_error_train.item():e}\")\n", + "print(f\" Test: {relative_p_error_test.item():e}\")" ] }, { @@ -409,10 +426,12 @@ ], "source": [ "fig, axs = plt.subplots(2, 3, figsize=(14, 6))\n", - "for ax, par, u in zip(axs.ravel(), dataset.params[66:72], dataset.snapshots['p'][66:72]):\n", + "for ax, par, u in zip(\n", + " axs.ravel(), dataset.params[66:72], dataset.snapshots[\"p\"][66:72]\n", + "):\n", " cm = ax.tricontourf(dataset.triang, u, levels=16)\n", " plt.colorbar(cm, ax=ax)\n", - " ax.set_title(f'$p$ field for $\\mu$ = {par[0]:.4f}')\n", + " ax.set_title(f\"$p$ field for $\\mu$ = {par[0]:.4f}\")\n", "plt.tight_layout()\n", "plt.show()" ] @@ -442,11 +461,13 @@ ], "source": [ "fig, axs = plt.subplots(2, 3, figsize=(14, 6))\n", - "for ax, par, u in zip(axs.ravel(), dataset.params[98:104], dataset.snapshots['p'][98:104]):\n", + "for ax, par, u in zip(\n", + " axs.ravel(), dataset.params[98:104], dataset.snapshots[\"p\"][98:104]\n", + "):\n", " cm = ax.tricontourf(dataset.triang, u, levels=16)\n", " plt.colorbar(cm, ax=ax)\n", - " ax.set_title(f'$p$ field for $\\mu$ = {par[0]:.4f}')\n", - "plt.tight_layout() \n", + " ax.set_title(f\"$p$ field for $\\mu$ = {par[0]:.4f}\")\n", + "plt.tight_layout()\n", "plt.show()" ] }, @@ -473,37 +494,42 @@ } ], "source": [ - "'''excluding problematic snapshots'''\n", + "\"\"\"excluding problematic snapshots\"\"\"\n", + "\n", "data = list(range(300))\n", "data_to_consider = data[:67] + data[71:100] + data[102:]\n", - "'''proceed as before'''\n", - "newp=torch.tensor(dataset.snapshots['p'][data_to_consider]).float()\n", - "newp = LabelTensor(newp, labels=[f's{i}' for i in range(newp.shape[1])])\n", + "\"\"\"proceed as before\"\"\"\n", + "newp = torch.tensor(dataset.snapshots[\"p\"][data_to_consider]).float()\n", + "newp = LabelTensor(newp, labels=[f\"s{i}\" for i in range(newp.shape[1])])\n", "\n", - "newmu=torch.tensor(dataset.params[data_to_consider]).float()\n", - "newmu = LabelTensor(newmu, labels=['mu'])\n", + "newmu = torch.tensor(dataset.params[data_to_consider]).float()\n", + "newmu = LabelTensor(newmu, labels=[\"mu\"])\n", "\n", "newn = newp.shape[0]\n", - "ratio = 0.9 \n", - "new_train = int(newn*ratio)\n", + "ratio = 0.9\n", + "new_train = int(newn * ratio)\n", "\n", - "new_p_train, new_p_test = newp[:new_train], newp[new_train:] \n", + "new_p_train, new_p_test = newp[:new_train], newp[new_train:]\n", "\n", "new_mu_train, new_mu_test = newmu[:new_train], newmu[new_train:]\n", "\n", - "new_pod_rbfp = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline')\n", + "new_pod_rbfp = PODRBF(pod_rank=20, rbf_kernel=\"thin_plate_spline\")\n", "\n", "new_pod_rbfp.fit(new_mu_train, new_p_train)\n", "\n", "new_p_train_rbf = new_pod_rbfp(new_mu_train)\n", "new_p_test_rbf = new_pod_rbfp(new_mu_test)\n", "\n", - "new_relative_p_error_train = torch.norm(new_p_train_rbf - new_p_train)/torch.norm(new_p_train)\n", - "new_relative_p_error_test = torch.norm(new_p_test_rbf - new_p_test)/torch.norm(new_p_test)\n", + "new_relative_p_error_train = torch.norm(\n", + " new_p_train_rbf - new_p_train\n", + ") / torch.norm(new_p_train)\n", + "new_relative_p_error_test = torch.norm(\n", + " new_p_test_rbf - new_p_test\n", + ") / torch.norm(new_p_test)\n", "\n", - "print('Error summary for POD-RBF model:')\n", - "print(f' Train: {new_relative_p_error_train.item():e}')\n", - "print(f' Test: {new_relative_p_error_test.item():e}')" + "print(\"Error summary for POD-RBF model:\")\n", + "print(f\" Train: {new_relative_p_error_train.item():e}\")\n", + "print(f\" Test: {new_relative_p_error_test.item():e}\")" ] }, { diff --git a/tutorials/tutorial14/tutorial.py b/tutorials/tutorial14/tutorial.py index 97c0bccff..ed423b4b5 100644 --- a/tutorials/tutorial14/tutorial.py +++ b/tutorials/tutorial14/tutorial.py @@ -2,28 +2,29 @@ # coding: utf-8 # # Tutorial: Predicting Lid-driven cavity problem parameters with POD-RBF -# +# # [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial14/tutorial.ipynb) -# In this tutorial we will show how to use the **PINA** library to predict the distributions of velocity and pressure the Lid-driven Cavity problem, a benchmark in Computational Fluid Dynamics. The problem consists of a square cavity with a lid on top moving with tangential velocity (by convention to the right), with the addition of no-slip conditions on the walls of the cavity and null static pressure on the lower left angle. -# +# In this tutorial we will show how to use the **PINA** library to predict the distributions of velocity and pressure the Lid-driven Cavity problem, a benchmark in Computational Fluid Dynamics. The problem consists of a square cavity with a lid on top moving with tangential velocity (by convention to the right), with the addition of no-slip conditions on the walls of the cavity and null static pressure on the lower left angle. +# # Our goal is to predict the distributions of velocity and pressure of the fluid inside the cavity as the Reynolds number of the inlet fluid varies. To do so we're using a Reduced Order Model (ROM) based on Proper Orthogonal Decomposition (POD). The parametric solution manifold is approximated here with Radial Basis Function (RBF) Interpolation, a common mesh-free interpolation method that doesn't require trainers or solvers as the found radial basis functions are used to interpolate new points. # Let's start with the necessary imports. We're particularly interested in the `PODBlock` and `RBFBlock` classes which will allow us to define the POD-RBF model. -# In[1]: +# In[ ]: ## routine needed to run the notebook on Google Colab try: - import google.colab - IN_COLAB = True + import google.colab + + IN_COLAB = True except: - IN_COLAB = False + IN_COLAB = False if IN_COLAB: - get_ipython().system('pip install "pina-mathlab"') + get_ipython().system('pip install "pina-mathlab"') -get_ipython().run_line_magic('matplotlib', 'inline') +get_ipython().run_line_magic("matplotlib", "inline") import matplotlib.pyplot as plt import torch @@ -33,11 +34,11 @@ from pina.model.block import PODBlock, RBFBlock from pina import LabelTensor -warnings.filterwarnings('ignore') +warnings.filterwarnings("ignore") # In this tutorial we're gonna use the `LidCavity` class from the [Smithers](https://github.com/mathLab/Smithers) library, which contains a set of parametric solutions of the Lid-driven cavity problem in a square domain. The dataset consists of 300 snapshots of the parameter fields, which in this case are the magnitude of velocity and the pressure, and the corresponding parameter values $u$ and $p$. Each snapshot corresponds to a different value of the tangential velocity $\mu$ of the lid, which has been sampled uniformly between 0.01 m/s and 1 m/s. -# +# # Let's start by importing the dataset: # In[2]: @@ -45,6 +46,7 @@ import smithers from smithers.dataset import LidCavity + dataset = LidCavity() @@ -54,13 +56,13 @@ fig, axs = plt.subplots(1, 3, figsize=(14, 3)) -for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots['mag(v)'][:3]): +for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots["mag(v)"][:3]): ax.tricontourf(dataset.triang, u, levels=16) - ax.set_title(f'$u$ field for $\mu$ = {par[0]:.4f}') + ax.set_title(f"$u$ field for $\mu$ = {par[0]:.4f}") fig, axs = plt.subplots(1, 3, figsize=(14, 3)) -for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots['p'][:3]): +for ax, par, u in zip(axs, dataset.params[:3], dataset.snapshots["p"][:3]): ax.tricontourf(dataset.triang, u, levels=16) - ax.set_title(f'$p$ field for $\mu$ = {par[0]:.4f}') + ax.set_title(f"$p$ field for $\mu$ = {par[0]:.4f}") # To train the model we only need the snapshots for the two parameters. In order to be able to work with the snapshots in **PINA** we first need to assure they're in a compatible format, hence why we start by casting them into `LabelTensor` objects: @@ -68,15 +70,16 @@ # In[4]: -'''velocity magnitude data, 5041 for each snapshot''' -u=torch.tensor(dataset.snapshots['mag(v)']).float() -u = LabelTensor(u, labels=[f's{i}' for i in range(u.shape[1])]) -'''pressure data, 5041 for each snapshot''' -p=torch.tensor(dataset.snapshots['p']).float() -p = LabelTensor(p, labels=[f's{i}' for i in range(p.shape[1])]) -'''mu corresponding to each snapshot''' -mu=torch.tensor(dataset.params).float() -mu = LabelTensor(mu, labels=['mu']) +"""velocity magnitude data, 5041 for each snapshot""" + +u = torch.tensor(dataset.snapshots["mag(v)"]).float() +u = LabelTensor(u, labels=[f"s{i}" for i in range(u.shape[1])]) +"""pressure data, 5041 for each snapshot""" +p = torch.tensor(dataset.snapshots["p"]).float() +p = LabelTensor(p, labels=[f"s{i}" for i in range(p.shape[1])]) +"""mu corresponding to each snapshot""" +mu = torch.tensor(dataset.params).float() +mu = LabelTensor(mu, labels=["mu"]) # The goal of our training is to be able to predict the solution for new test parameters. The first thing we need to do is validate the accuracy of the model, and in order to do so we split the 300 snapshots in training and testing dataset. In the example we set the training `ratio` to 0.9, which means that 90% of the total snapshots is used for training and the remaining 10% for testing. @@ -84,15 +87,16 @@ # In[5]: -'''number of snapshots''' +"""number of snapshots""" + n = u.shape[0] -'''training over total snapshots ratio and number of training snapshots''' -ratio = 0.9 -n_train = int(n*ratio) -'''split u and p data''' -u_train, u_test = u[:n_train], u[n_train:] #for mag(v) -p_train, p_test = p[:n_train], p[n_train:] #for p -'''split snapshots''' +"""training over total snapshots ratio and number of training snapshots""" +ratio = 0.9 +n_train = int(n * ratio) +"""split u and p data""" +u_train, u_test = u[:n_train], u[n_train:] # for mag(v) +p_train, p_test = p[:n_train], p[n_train:] # for p +"""split snapshots""" mu_train, mu_test = mu[:n_train], mu[n_train:] @@ -105,8 +109,9 @@ class PODRBF(torch.nn.Module): """ Proper orthogonal decomposition with Radial Basis Function interpolation model. """ + def __init__(self, pod_rank, rbf_kernel): - + super().__init__() self.pod = PODBlock(pod_rank) self.rbf = RBFBlock(kernel=rbf_kernel) @@ -121,8 +126,9 @@ class PODRBF(torch.nn.Module): """ Proper orthogonal decomposition with Radial Basis Function interpolation model. """ + def __init__(self, pod_rank, rbf_kernel): - + super().__init__() self.pod = PODBlock(pod_rank) self.rbf = RBFBlock(kernel=rbf_kernel) @@ -137,6 +143,7 @@ def forward(self, x): """ coefficients = self.rbf(x) return self.pod.expand(coefficients) + def fit(self, p, x): """ Call the :meth:`pina.model.layers.PODBlock.fit` method of the @@ -146,7 +153,6 @@ def fit(self, p, x): """ self.pod.fit(x) self.rbf.fit(p, self.pod.reduce(x)) - # Now that we've built our class, we can fit the model and ask it to predict the parameters for the remaining snapshots. We remember that we don't need to train the model, as it doesn't involve any learnable parameter. The only things we have to set are the rank of the decomposition and the radial basis function (here we use thin plate). Here we focus on predicting the magnitude of velocity: @@ -154,13 +160,14 @@ def fit(self, p, x): # In[8]: -'''create the model''' -pod_rbfu = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline') +"""create the model""" -'''fit the model to velocity training data''' +pod_rbfu = PODRBF(pod_rank=20, rbf_kernel="thin_plate_spline") + +"""fit the model to velocity training data""" pod_rbfu.fit(mu_train, u_train) -'''predict the parameter using the fitted model''' +"""predict the parameter using the fitted model""" u_train_rbf = pod_rbfu(mu_train) u_test_rbf = pod_rbfu(mu_test) @@ -170,12 +177,12 @@ def fit(self, p, x): # In[9]: -relative_u_error_train = torch.norm(u_train_rbf - u_train)/torch.norm(u_train) -relative_u_error_test = torch.norm(u_test_rbf - u_test)/torch.norm(u_test) +relative_u_error_train = torch.norm(u_train_rbf - u_train) / torch.norm(u_train) +relative_u_error_test = torch.norm(u_test_rbf - u_test) / torch.norm(u_test) -print('Error summary for POD-RBF model:') -print(f' Train: {relative_u_error_train.item():e}') -print(f' Test: {relative_u_error_test.item():e}') +print("Error summary for POD-RBF model:") +print(f" Train: {relative_u_error_train.item():e}") +print(f" Test: {relative_u_error_test.item():e}") # The results are promising! Now let's visualise them, comparing four random predicted snapshots to the true ones: @@ -192,23 +199,32 @@ def fit(self, p, x): fig, axs = plt.subplots(3, 4, figsize=(14, 10)) relative_u_error_rbf = np.abs(u_test[idx] - u_idx_rbf.detach()) -relative_u_error_rbf = np.where(u_test[idx] < 1e-7, 1e-7, relative_u_error_rbf/u_test[idx]) - -for i, (idx_, rbf_, rbf_err_) in enumerate( - zip(idx, u_idx_rbf, relative_u_error_rbf)): - axs[0, i].set_title('Prediction for ' f'$\mu$ = {mu_test[idx_].item():.4f}') - axs[1, i].set_title('True snapshot for ' f'$\mu$ = {mu_test[idx_].item():.4f}') - axs[2, i].set_title('Error for ' f'$\mu$ = {mu_test[idx_].item():.4f}') +relative_u_error_rbf = np.where( + u_test[idx] < 1e-7, 1e-7, relative_u_error_rbf / u_test[idx] +) - cm = axs[0, i].tricontourf(dataset.triang, rbf_.detach()) # POD-RBF prediction +for i, (idx_, rbf_, rbf_err_) in enumerate( + zip(idx, u_idx_rbf, relative_u_error_rbf) +): + axs[0, i].set_title("Prediction for " f"$\mu$ = {mu_test[idx_].item():.4f}") + axs[1, i].set_title( + "True snapshot for " f"$\mu$ = {mu_test[idx_].item():.4f}" + ) + axs[2, i].set_title("Error for " f"$\mu$ = {mu_test[idx_].item():.4f}") + + cm = axs[0, i].tricontourf( + dataset.triang, rbf_.detach() + ) # POD-RBF prediction plt.colorbar(cm, ax=axs[0, i]) - - cm = axs[1, i].tricontourf(dataset.triang, u_test[idx_].flatten()) # Truth + + cm = axs[1, i].tricontourf(dataset.triang, u_test[idx_].flatten()) # Truth plt.colorbar(cm, ax=axs[1, i]) - cm = axs[2, i].tripcolor(dataset.triang, rbf_err_, norm=matplotlib.colors.LogNorm()) # Error for POD-RBF + cm = axs[2, i].tripcolor( + dataset.triang, rbf_err_, norm=matplotlib.colors.LogNorm() + ) # Error for POD-RBF plt.colorbar(cm, ax=axs[2, i]) - + plt.show() @@ -217,34 +233,37 @@ def fit(self, p, x): # In[11]: -'''create the model''' -pod_rbfp = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline') +"""create the model""" + +pod_rbfp = PODRBF(pod_rank=20, rbf_kernel="thin_plate_spline") -'''fit the model to pressure training data''' +"""fit the model to pressure training data""" pod_rbfp.fit(mu_train, p_train) -'''predict the parameter using the fitted model''' +"""predict the parameter using the fitted model""" p_train_rbf = pod_rbfp(mu_train) p_test_rbf = pod_rbfp(mu_test) -relative_p_error_train = torch.norm(p_train_rbf - p_train)/torch.norm(p_train) -relative_p_error_test = torch.norm(p_test_rbf - p_test)/torch.norm(p_test) +relative_p_error_train = torch.norm(p_train_rbf - p_train) / torch.norm(p_train) +relative_p_error_test = torch.norm(p_test_rbf - p_test) / torch.norm(p_test) -print('Error summary for POD-RBF model:') -print(f' Train: {relative_p_error_train.item():e}') -print(f' Test: {relative_p_error_test.item():e}') +print("Error summary for POD-RBF model:") +print(f" Train: {relative_p_error_train.item():e}") +print(f" Test: {relative_p_error_test.item():e}") -# Unfortunately here we obtain a very high relative test error, although this is likely due to the nature of the available data. Looking at the plots we can see that the pressure field is subject to high variations between subsequent snapshots, especially here: +# Unfortunately here we obtain a very high relative test error, although this is likely due to the nature of the available data. Looking at the plots we can see that the pressure field is subject to high variations between subsequent snapshots, especially here: # In[12]: fig, axs = plt.subplots(2, 3, figsize=(14, 6)) -for ax, par, u in zip(axs.ravel(), dataset.params[66:72], dataset.snapshots['p'][66:72]): +for ax, par, u in zip( + axs.ravel(), dataset.params[66:72], dataset.snapshots["p"][66:72] +): cm = ax.tricontourf(dataset.triang, u, levels=16) plt.colorbar(cm, ax=ax) - ax.set_title(f'$p$ field for $\mu$ = {par[0]:.4f}') + ax.set_title(f"$p$ field for $\mu$ = {par[0]:.4f}") plt.tight_layout() plt.show() @@ -255,11 +274,13 @@ def fit(self, p, x): fig, axs = plt.subplots(2, 3, figsize=(14, 6)) -for ax, par, u in zip(axs.ravel(), dataset.params[98:104], dataset.snapshots['p'][98:104]): +for ax, par, u in zip( + axs.ravel(), dataset.params[98:104], dataset.snapshots["p"][98:104] +): cm = ax.tricontourf(dataset.triang, u, levels=16) plt.colorbar(cm, ax=ax) - ax.set_title(f'$p$ field for $\mu$ = {par[0]:.4f}') -plt.tight_layout() + ax.set_title(f"$p$ field for $\mu$ = {par[0]:.4f}") +plt.tight_layout() plt.show() @@ -268,45 +289,50 @@ def fit(self, p, x): # In[14]: -'''excluding problematic snapshots''' +"""excluding problematic snapshots""" + data = list(range(300)) data_to_consider = data[:67] + data[71:100] + data[102:] -'''proceed as before''' -newp=torch.tensor(dataset.snapshots['p'][data_to_consider]).float() -newp = LabelTensor(newp, labels=[f's{i}' for i in range(newp.shape[1])]) +"""proceed as before""" +newp = torch.tensor(dataset.snapshots["p"][data_to_consider]).float() +newp = LabelTensor(newp, labels=[f"s{i}" for i in range(newp.shape[1])]) -newmu=torch.tensor(dataset.params[data_to_consider]).float() -newmu = LabelTensor(newmu, labels=['mu']) +newmu = torch.tensor(dataset.params[data_to_consider]).float() +newmu = LabelTensor(newmu, labels=["mu"]) newn = newp.shape[0] -ratio = 0.9 -new_train = int(newn*ratio) +ratio = 0.9 +new_train = int(newn * ratio) -new_p_train, new_p_test = newp[:new_train], newp[new_train:] +new_p_train, new_p_test = newp[:new_train], newp[new_train:] new_mu_train, new_mu_test = newmu[:new_train], newmu[new_train:] -new_pod_rbfp = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline') +new_pod_rbfp = PODRBF(pod_rank=20, rbf_kernel="thin_plate_spline") new_pod_rbfp.fit(new_mu_train, new_p_train) new_p_train_rbf = new_pod_rbfp(new_mu_train) new_p_test_rbf = new_pod_rbfp(new_mu_test) -new_relative_p_error_train = torch.norm(new_p_train_rbf - new_p_train)/torch.norm(new_p_train) -new_relative_p_error_test = torch.norm(new_p_test_rbf - new_p_test)/torch.norm(new_p_test) +new_relative_p_error_train = torch.norm( + new_p_train_rbf - new_p_train +) / torch.norm(new_p_train) +new_relative_p_error_test = torch.norm( + new_p_test_rbf - new_p_test +) / torch.norm(new_p_test) -print('Error summary for POD-RBF model:') -print(f' Train: {new_relative_p_error_train.item():e}') -print(f' Test: {new_relative_p_error_test.item():e}') +print("Error summary for POD-RBF model:") +print(f" Train: {new_relative_p_error_train.item():e}") +print(f" Test: {new_relative_p_error_test.item():e}") # ## What's next? -# +# # Congratulations on completing the **PINA** tutorial on building and using a custom POD class! Now you can try: -# +# # 1. Varying the inputs of the model (for a list of the supported RB functions look at the `rbf_layer.py` file in `pina.layers`) -# +# # 2. Changing the POD model, for example using Artificial Neural Networks. For a more in depth overview of POD-NN and a comparison with the POD-RBF model already shown, look at [Tutorial: Reduced order model (POD-RBF or POD-NN) for parametric problems](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial9/tutorial.ipynb) -# +# # 3. Building your own classes or adapt the one shown to other datasets/problems diff --git a/tutorials/tutorial2/tutorial.ipynb b/tutorials/tutorial2/tutorial.ipynb index 6a1a86282..d0d891c77 100644 --- a/tutorials/tutorial2/tutorial.ipynb +++ b/tutorials/tutorial2/tutorial.ipynb @@ -23,12 +23,13 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import torch\n", "import matplotlib.pyplot as plt\n", @@ -39,7 +40,7 @@ "from pina.solver import PINN\n", "from torch.nn import Softplus\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -186,8 +187,7 @@ " train_size=0.8, # set train size\n", " val_size=0.0, # set validation size\n", " test_size=0.2, # set testing size\n", - " shuffle=True, # shuffle the data\n", - " \n", + " shuffle=True, # shuffle the data\n", ")\n", "\n", "# train\n", @@ -348,7 +348,7 @@ "\n", " def forward(self, pts):\n", " x, y = pts.extract([\"x\"]), pts.extract([\"y\"])\n", - " f = 2 *torch.pi**2 * torch.sin(x * torch.pi) * torch.sin(y * torch.pi)\n", + " f = 2 * torch.pi**2 * torch.sin(x * torch.pi) * torch.sin(y * torch.pi)\n", " return LabelTensor(f, [\"feat\"])\n", "\n", "\n", @@ -487,28 +487,36 @@ "source": [ "class SinSinAB(torch.nn.Module):\n", " \"\"\" \"\"\"\n", + "\n", " def __init__(self):\n", " super().__init__()\n", " self.alpha = torch.nn.Parameter(torch.tensor([1.0]))\n", " self.beta = torch.nn.Parameter(torch.tensor([1.0]))\n", "\n", " def forward(self, x):\n", - " t = (\n", - " self.beta*torch.sin(self.alpha*x.extract(['x'])*torch.pi)*\n", - " torch.sin(self.alpha*x.extract(['y'])*torch.pi)\n", + " t = (\n", + " self.beta\n", + " * torch.sin(self.alpha * x.extract([\"x\"]) * torch.pi)\n", + " * torch.sin(self.alpha * x.extract([\"y\"]) * torch.pi)\n", " )\n", - " return LabelTensor(t, ['b*sin(a*x)sin(a*y)'])\n", + " return LabelTensor(t, [\"b*sin(a*x)sin(a*y)\"])\n", "\n", "\n", "# make model + solver + trainer\n", "model_learn = FeedForwardWithExtraFeatures(\n", - " input_dimensions=len(problem.input_variables) + 1, #we add one as also we consider the extra feature dimension\n", + " input_dimensions=len(problem.input_variables)\n", + " + 1, # we add one as also we consider the extra feature dimension\n", " output_dimensions=len(problem.output_variables),\n", " func=Softplus,\n", " layers=[10, 10],\n", - " extra_features=SinSinAB())\n", + " extra_features=SinSinAB(),\n", + ")\n", "\n", - "pinn_learn = PINN(problem, model_learn, optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006,weight_decay=1e-8))\n", + "pinn_learn = PINN(\n", + " problem,\n", + " model_learn,\n", + " optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006, weight_decay=1e-8),\n", + ")\n", "trainer_learn = Trainer(\n", " solver=pinn_learn, # setting the solver, i.e. PINN\n", " max_epochs=1000, # setting max epochs in training\n", @@ -649,14 +657,14 @@ ], "source": [ "# test error base pinn\n", - "print('PINN')\n", + "print(\"PINN\")\n", "trainer_base.test()\n", "# test error extra features pinn\n", "print(\"PINN with extra features\")\n", "trainer_feat.test()\n", "# test error learnable extra features pinn\n", "print(\"PINN with learnable extra features\")\n", - "_=trainer_learn.test()" + "_ = trainer_learn.test()" ] }, { diff --git a/tutorials/tutorial2/tutorial.py b/tutorials/tutorial2/tutorial.py new file mode 100644 index 000000000..622783aaa --- /dev/null +++ b/tutorials/tutorial2/tutorial.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Two dimensional Poisson problem using Extra Features Learning +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial2/tutorial.ipynb) +# +# This tutorial presents how to solve with Physics-Informed Neural Networks (PINNs) a 2D Poisson problem with Dirichlet boundary conditions. We will train with standard PINN's training, and with extrafeatures. For more insights on extrafeature learning please read [*An extended physics informed neural network for preliminary analysis of parametric optimal control problems*](https://www.sciencedirect.com/science/article/abs/pii/S0898122123002018). +# +# First of all, some useful imports. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import matplotlib.pyplot as plt +import warnings + +from pina import LabelTensor, Trainer +from pina.model import FeedForward +from pina.solver import PINN +from torch.nn import Softplus + +warnings.filterwarnings("ignore") + + +# ## The problem definition + +# The two-dimensional Poisson problem is mathematically written as: +# \begin{equation} +# \begin{cases} +# \Delta u = 2\pi^2\sin{(\pi x)} \sin{(\pi y)} \text{ in } D, \\ +# u = 0 \text{ on } \Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4, +# \end{cases} +# \end{equation} +# where $D$ is a square domain $[0,1]^2$, and $\Gamma_i$, with $i=1,...,4$, are the boundaries of the square. +# +# The Poisson problem is written in **PINA** code as a class. The equations are written as *conditions* that should be satisfied in the corresponding domains. The *solution* +# is the exact solution which will be compared with the predicted one. If interested in how to write problems see [this tutorial](https://mathlab.github.io/PINA/_rst/tutorials/tutorial1/tutorial.html). +# +# We will directly import the problem from `pina.problem.zoo`, which contains a vast list of PINN problems and more. + +# In[2]: + + +from pina.problem.zoo import Poisson2DSquareProblem as Poisson + +# initialize the problem +problem = Poisson() + +# print the conditions +print( + f"The problem is made of {len(problem.conditions.keys())} conditions: \n" + f"They are: {list(problem.conditions.keys())}" +) + +# let's discretise the domain +problem.discretise_domain(30, "grid", domains=["D"]) +problem.discretise_domain( + 100, + "grid", + domains=["g1", "g2", "g3", "g4"], +) + + +# ## Solving the problem with standard PINNs + +# After the problem, the feed-forward neural network is defined, through the class `FeedForward`. This neural network takes as input the coordinates (in this case $x$ and $y$) and provides the unkwown field of the Poisson problem. The residual of the equations are evaluated at several sampling points and the loss minimized by the neural network is the sum of the residuals. +# +# In this tutorial, the neural network is composed by two hidden layers of 10 neurons each, and it is trained for 1000 epochs with a learning rate of 0.006 and $l_2$ weight regularization set to $10^{-8}$. These parameters can be modified as desired. We set the `train_size` to 0.8 and `test_size` to 0.2, this mean that the discretised points will be divided in a 80%-20% fashion, where 80% will be used for training and the remaining 20% for testing. + +# In[3]: + + +# make model + solver + trainer +from pina.optim import TorchOptimizer + +model = FeedForward( + layers=[10, 10], + func=Softplus, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) +pinn = PINN( + problem, + model, + optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006, weight_decay=1e-8), +) +trainer_base = Trainer( + solver=pinn, # setting the solver, i.e. PINN + max_epochs=1000, # setting max epochs in training + accelerator="cpu", # we train on cpu, also other are available + enable_model_summary=False, # model summary statistics not printed + train_size=0.8, # set train size + val_size=0.0, # set validation size + test_size=0.2, # set testing size + shuffle=True, # shuffle the data +) + +# train +trainer_base.train() + + +# Now we plot the results using `matplotlib`. +# The solution predicted by the neural network is plotted on the left, the exact one is represented at the center and on the right the error between the exact and the predicted solutions is showed. + +# In[4]: + + +@torch.no_grad() +def plot_solution(solver): + # get the problem + problem = solver.problem + # get spatial points + spatial_samples = problem.spatial_domain.sample(30, "grid") + # compute pinn solution, true solution and absolute difference + data = { + "PINN solution": solver(spatial_samples), + "True solution": problem.solution(spatial_samples), + "Absolute Difference": torch.abs( + solver(spatial_samples) - problem.solution(spatial_samples) + ), + } + # plot the solution + for idx, (title, field) in enumerate(data.items()): + plt.subplot(1, 3, idx + 1) + plt.title(title) + plt.tricontourf( # convert to torch tensor + flatten + spatial_samples.extract("x").tensor.flatten(), + spatial_samples.extract("y").tensor.flatten(), + field.tensor.flatten(), + ) + plt.colorbar(), plt.tight_layout() + + +# Here the solution: + +# In[5]: + + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn) + + +# As you can see the solution is not very accurate, in what follows we will use **Extra Feature** as introduced in [*An extended physics informed neural network for preliminary analysis of parametric optimal control problems*](https://www.sciencedirect.com/science/article/abs/pii/S0898122123002018) to boost the training accuracy. Of course, even extra training will benefit, this tutorial is just to show that convergence using Extra Features is usally faster. + +# ## Solving the problem with extra-features PINNs + +# Now, the same problem is solved in a different way. +# A new neural network is now defined, with an additional input variable, named extra-feature, which coincides with the forcing term in the Laplace equation. +# The set of input variables to the neural network is: +# +# \begin{equation} +# [x, y, k(x, y)], \text{ with } k(x, y)= 2\pi^2\sin{(\pi x)}\sin{(\pi y)}, +# \end{equation} +# +# where $x$ and $y$ are the spatial coordinates and $k(x, y)$ is the added feature which is equal to the forcing term. +# +# This feature is initialized in the class `SinSin`, which is a simple `torch.nn.Module`. After declaring such feature, we can just adjust the `FeedForward` class by creating a subclass `FeedForwardWithExtraFeatures` with an adjusted forward method and the additional attribute `extra_features`. +# +# Finally, we perform the same training as before: the problem is `Poisson`, the network is composed by the same number of neurons and optimizer parameters are equal to previous test, the only change is the new extra feature. + +# In[6]: + + +class SinSin(torch.nn.Module): + """Feature: sin(x)*sin(y)""" + + def __init__(self): + super().__init__() + + def forward(self, pts): + x, y = pts.extract(["x"]), pts.extract(["y"]) + f = 2 * torch.pi**2 * torch.sin(x * torch.pi) * torch.sin(y * torch.pi) + return LabelTensor(f, ["feat"]) + + +class FeedForwardWithExtraFeatures(FeedForward): + def __init__(self, *args, extra_features, **kwargs): + super().__init__(*args, **kwargs) + self.extra_features = extra_features + + def forward(self, x): + extra_feature = self.extra_features(x) # we append extra features + x = x.append(extra_feature) + return super().forward(x) + + +model_feat = FeedForwardWithExtraFeatures( + input_dimensions=len(problem.input_variables) + 1, + output_dimensions=len(problem.output_variables), + func=Softplus, + layers=[10, 10], + extra_features=SinSin(), +) + +pinn_feat = PINN( + problem, + model_feat, + optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006, weight_decay=1e-8), +) +trainer_feat = Trainer( + solver=pinn_feat, # setting the solver, i.e. PINN + max_epochs=1000, # setting max epochs in training + accelerator="cpu", # we train on cpu, also other are available + enable_model_summary=False, # model summary statistics not printed + train_size=0.8, # set train size + val_size=0.0, # set validation size + test_size=0.2, # set testing size + shuffle=True, # shuffle the data +) + +trainer_feat.train() + + +# The predicted and exact solutions and the error between them are represented below. +# We can easily note that now our network, having almost the same condition as before, is able to reach additional order of magnitudes in accuracy. + +# In[7]: + + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn_feat) + + +# ## Solving the problem with learnable extra-features PINNs + +# We can still do better! +# +# Another way to exploit the extra features is the addition of learnable parameter inside them. +# In this way, the added parameters are learned during the training phase of the neural network. In this case, we use: +# +# \begin{equation} +# k(x, \mathbf{y}) = \beta \sin{(\alpha x)} \sin{(\alpha y)}, +# \end{equation} +# +# where $\alpha$ and $\beta$ are the abovementioned parameters. +# Their implementation is quite trivial: by using the class `torch.nn.Parameter` we cam define all the learnable parameters we need, and they are managed by `autograd` module! + +# In[8]: + + +class SinSinAB(torch.nn.Module): + """ """ + + def __init__(self): + super().__init__() + self.alpha = torch.nn.Parameter(torch.tensor([1.0])) + self.beta = torch.nn.Parameter(torch.tensor([1.0])) + + def forward(self, x): + t = ( + self.beta + * torch.sin(self.alpha * x.extract(["x"]) * torch.pi) + * torch.sin(self.alpha * x.extract(["y"]) * torch.pi) + ) + return LabelTensor(t, ["b*sin(a*x)sin(a*y)"]) + + +# make model + solver + trainer +model_learn = FeedForwardWithExtraFeatures( + input_dimensions=len(problem.input_variables) + + 1, # we add one as also we consider the extra feature dimension + output_dimensions=len(problem.output_variables), + func=Softplus, + layers=[10, 10], + extra_features=SinSinAB(), +) + +pinn_learn = PINN( + problem, + model_learn, + optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006, weight_decay=1e-8), +) +trainer_learn = Trainer( + solver=pinn_learn, # setting the solver, i.e. PINN + max_epochs=1000, # setting max epochs in training + accelerator="cpu", # we train on cpu, also other are available + enable_model_summary=False, # model summary statistics not printed + train_size=0.8, # set train size + val_size=0.0, # set validation size + test_size=0.2, # set testing size + shuffle=True, # shuffle the data +) +# train +trainer_learn.train() + + +# Umh, the final loss is not appreciabily better than previous model (with static extra features), despite the usage of learnable parameters. This is mainly due to the over-parametrization of the network: there are many parameter to optimize during the training, and the model in unable to understand automatically that only the parameters of the extra feature (and not the weights/bias of the FFN) should be tuned in order to fit our problem. A longer training can be helpful, but in this case the faster way to reach machine precision for solving the Poisson problem is removing all the hidden layers in the `FeedForward`, keeping only the $\alpha$ and $\beta$ parameters of the extra feature. + +# In[9]: + + +# make model + solver + trainer +model_learn = FeedForwardWithExtraFeatures( + layers=[], + func=Softplus, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables) + 1, + extra_features=SinSinAB(), +) +pinn_learn = PINN( + problem, + model_learn, + optimizer=TorchOptimizer(torch.optim.Adam, lr=0.006, weight_decay=1e-8), +) +trainer_learn = Trainer( + solver=pinn_learn, # setting the solver, i.e. PINN + max_epochs=1000, # setting max epochs in training + accelerator="cpu", # we train on cpu, also other are available + enable_model_summary=False, # model summary statistics not printed + train_size=0.8, # set train size + val_size=0.0, # set validation size + test_size=0.2, # set testing size + shuffle=True, # shuffle the data +) +# train +trainer_learn.train() + + +# In such a way, the model is able to reach a very high accuracy! +# Of course, this is a toy problem for understanding the usage of extra features: similar precision could be obtained if the extra features are very similar to the true solution. The analyzed Poisson problem shows a forcing term very close to the solution, resulting in a perfect problem to address with such an approach. + +# We conclude here by showing the test error for the analysed methodologies: the standard PINN, PINN with extra features, and PINN with learnable extra features. + +# In[12]: + + +# test error base pinn +print("PINN") +trainer_base.test() +# test error extra features pinn +print("PINN with extra features") +trainer_feat.test() +# test error learnable extra features pinn +print("PINN with learnable extra features") +_ = trainer_learn.test() + + +# ## What's next? +# +# Congratulations on completing the two dimensional Poisson tutorial of **PINA**! There are multiple directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the finaly accuracy +# +# 2. Propose new types of extrafeatures and see how they affect the learning +# +# 3. Exploit extrafeature training in more complex problems +# +# 4. Many more... diff --git a/tutorials/tutorial3/tutorial.ipynb b/tutorials/tutorial3/tutorial.ipynb index 5ffa55b9c..3ef328ea7 100644 --- a/tutorials/tutorial3/tutorial.ipynb +++ b/tutorials/tutorial3/tutorial.ipynb @@ -16,20 +16,21 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "d93daba0", "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", - " \n", + " !pip install \"pina-mathlab\"\n", + "\n", "import torch\n", "import matplotlib.pyplot as plt\n", "import warnings\n", @@ -42,7 +43,7 @@ "from pina.equation import Equation, FixedValue\n", "from pina.callback import MetricTracker\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -261,7 +262,7 @@ " train_size=1.0,\n", " val_size=0.0,\n", " test_size=0.0,\n", - " callbacks=[MetricTracker(['train_loss', 'initial_loss','D_loss'])],\n", + " callbacks=[MetricTracker([\"train_loss\", \"initial_loss\", \"D_loss\"])],\n", ")\n", "trainer.train()" ] @@ -343,10 +344,10 @@ " \"True solution\": problem.solution(points),\n", " \"Absolute Difference\": torch.abs(\n", " solver(points) - problem.solution(points)\n", - " )\n", + " ),\n", " }\n", " # plot the solution\n", - " plt.suptitle(f'Solution for time {time.item()}')\n", + " plt.suptitle(f\"Solution for time {time.item()}\")\n", " for idx, (title, field) in enumerate(data.items()):\n", " plt.subplot(1, 3, idx + 1)\n", " plt.title(title)\n", diff --git a/tutorials/tutorial3/tutorial.py b/tutorials/tutorial3/tutorial.py new file mode 100644 index 000000000..97ad5ed69 --- /dev/null +++ b/tutorials/tutorial3/tutorial.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Two dimensional Wave problem with hard constraint +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial3/tutorial.ipynb) +# +# In this tutorial we present how to solve the wave equation using hard constraint PINNs. For doing so we will build a costum `torch` model and pass it to the `PINN` solver. +# +# First of all, some useful imports. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import matplotlib.pyplot as plt +import warnings + +from pina import Condition, LabelTensor, Trainer +from pina.problem import SpatialProblem, TimeDependentProblem +from pina.operator import laplacian, grad +from pina.domain import CartesianDomain +from pina.solver import PINN +from pina.equation import Equation, FixedValue +from pina.callback import MetricTracker + +warnings.filterwarnings("ignore") + + +# ## The problem definition + +# The problem is written in the following form: +# +# \begin{equation} +# \begin{cases} +# \Delta u(x,y,t) = \frac{\partial^2}{\partial t^2} u(x,y,t) \quad \text{in } D, \\\\ +# u(x, y, t=0) = \sin(\pi x)\sin(\pi y), \\\\ +# u(x, y, t) = 0 \quad \text{on } \Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4, +# \end{cases} +# \end{equation} +# +# where $D$ is a squared domain $[0,1]^2$, and $\Gamma_i$, with $i=1,...,4$, are the boundaries of the square, and the velocity in the standard wave equation is fixed to one. + +# Now, the wave problem is written in PINA code as a class, inheriting from `SpatialProblem` and `TimeDependentProblem` since we deal with spatial, and time dependent variables. The equations are written as `conditions` that should be satisfied in the corresponding domains. `solution` is the exact solution which will be compared with the predicted one. + +# In[2]: + + +def wave_equation(input_, output_): + u_t = grad(output_, input_, components=["u"], d=["t"]) + u_tt = grad(u_t, input_, components=["dudt"], d=["t"]) + nabla_u = laplacian(output_, input_, components=["u"], d=["x", "y"]) + return nabla_u - u_tt + + +def initial_condition(input_, output_): + u_expected = torch.sin(torch.pi * input_.extract(["x"])) * torch.sin( + torch.pi * input_.extract(["y"]) + ) + return output_.extract(["u"]) - u_expected + + +class Wave(TimeDependentProblem, SpatialProblem): + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 1], "y": [0, 1]}) + temporal_domain = CartesianDomain({"t": [0, 1]}) + domains = { + "g1": CartesianDomain({"x": 1, "y": [0, 1], "t": [0, 1]}), + "g2": CartesianDomain({"x": 0, "y": [0, 1], "t": [0, 1]}), + "g3": CartesianDomain({"x": [0, 1], "y": 0, "t": [0, 1]}), + "g4": CartesianDomain({"x": [0, 1], "y": 1, "t": [0, 1]}), + "initial": CartesianDomain({"x": [0, 1], "y": [0, 1], "t": 0}), + "D": CartesianDomain({"x": [0, 1], "y": [0, 1], "t": [0, 1]}), + } + conditions = { + "g1": Condition(domain="g1", equation=FixedValue(0.0)), + "g2": Condition(domain="g2", equation=FixedValue(0.0)), + "g3": Condition(domain="g3", equation=FixedValue(0.0)), + "g4": Condition(domain="g4", equation=FixedValue(0.0)), + "initial": Condition( + domain="initial", equation=Equation(initial_condition) + ), + "D": Condition(domain="D", equation=Equation(wave_equation)), + } + + def solution(self, pts): + f = ( + torch.sin(torch.pi * pts.extract(["x"])) + * torch.sin(torch.pi * pts.extract(["y"])) + * torch.cos( + torch.sqrt(torch.tensor(2.0)) * torch.pi * pts.extract(["t"]) + ) + ) + return LabelTensor(f, self.output_variables) + + +# define problem +problem = Wave() + + +# ## Hard Constraint Model + +# After the problem, a **torch** model is needed to solve the PINN. Usually, many models are already implemented in **PINA**, but the user has the possibility to build his/her own model in `torch`. The hard constraint we impose is on the boundary of the spatial domain. Specifically, our solution is written as: +# +# $$ u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t), $$ +# +# where $NN$ is the neural net output. This neural network takes as input the coordinates (in this case $x$, $y$ and $t$) and provides the unknown field $u$. By construction, it is zero on the boundaries. The residuals of the equations are evaluated at several sampling points (which the user can manipulate using the method `discretise_domain`) and the loss minimized by the neural network is the sum of the residuals. + +# In[3]: + + +class HardMLP(torch.nn.Module): + + def __init__(self, input_dim, output_dim): + super().__init__() + + self.layers = torch.nn.Sequential( + torch.nn.Linear(input_dim, 40), + torch.nn.ReLU(), + torch.nn.Linear(40, 40), + torch.nn.ReLU(), + torch.nn.Linear(40, output_dim), + ) + + # here in the foward we implement the hard constraints + def forward(self, x): + hard = ( + x.extract(["x"]) + * (1 - x.extract(["x"])) + * x.extract(["y"]) + * (1 - x.extract(["y"])) + ) + return hard * self.layers(x) + + +# ## Train and Inference + +# In this tutorial, the neural network is trained for 1000 epochs with a learning rate of 0.001 (default in `PINN`). As always, we will log using `Tensorboard`. + +# In[4]: + + +# generate the data +problem.discretise_domain(1000, "random", domains="all") + +# define model +model = HardMLP(len(problem.input_variables), len(problem.output_variables)) + +# crete the solver +pinn = PINN(problem=problem, model=model) + +# create trainer and train +trainer = Trainer( + solver=pinn, + max_epochs=1000, + accelerator="cpu", + enable_model_summary=False, + train_size=1.0, + val_size=0.0, + test_size=0.0, + callbacks=[MetricTracker(["train_loss", "initial_loss", "D_loss"])], +) +trainer.train() + + +# Let's now plot the losses inside `MetricTracker` to see how they vary during training. + +# In[5]: + + +trainer_metrics = trainer.callbacks[0].metrics +for metric, loss in trainer_metrics.items(): + plt.plot(range(len(loss)), loss, label=metric) +# plotting +plt.xlabel("epoch") +plt.ylabel("loss") +plt.yscale("log") +plt.legend() + + +# Notice that the loss on the boundaries of the spatial domain is exactly zero, as expected! After the training is completed one can now plot some results using the `matplotlib`. We plot the predicted output on the left side, the true solution at the center and the difference on the right side using the `plot_solution` function. + +# In[6]: + + +@torch.no_grad() +def plot_solution(solver, time): + # get the problem + problem = solver.problem + # get spatial points + spatial_samples = problem.spatial_domain.sample(30, "grid") + # get temporal value + time = LabelTensor(torch.tensor([[time]]), "t") + # cross data + points = spatial_samples.append(time, mode="cross") + # compute pinn solution, true solution and absolute difference + data = { + "PINN solution": solver(points), + "True solution": problem.solution(points), + "Absolute Difference": torch.abs( + solver(points) - problem.solution(points) + ), + } + # plot the solution + plt.suptitle(f"Solution for time {time.item()}") + for idx, (title, field) in enumerate(data.items()): + plt.subplot(1, 3, idx + 1) + plt.title(title) + plt.tricontourf( # convert to torch tensor + flatten + points.extract("x").tensor.flatten(), + points.extract("y").tensor.flatten(), + field.tensor.flatten(), + ) + plt.colorbar(), plt.tight_layout() + + +# Let's take a look at the results at different times, for example `0.0`, `0.5` and `1.0`: + +# In[7]: + + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=0) + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=0.5) + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=1) + + +# The results are not so great, and we can clearly see that as time progresses the solution gets worse.... Can we do better? +# +# A valid option is to impose the initial condition as hard constraint as well. Specifically, our solution is written as: +# +# $$ u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t)\cdot t + \cos(\sqrt{2}\pi t)\sin(\pi x)\sin(\pi y), $$ +# +# Let us build the network first + +# In[8]: + + +class HardMLPtime(torch.nn.Module): + + def __init__(self, input_dim, output_dim): + super().__init__() + + self.layers = torch.nn.Sequential( + torch.nn.Linear(input_dim, 40), + torch.nn.ReLU(), + torch.nn.Linear(40, 40), + torch.nn.ReLU(), + torch.nn.Linear(40, output_dim), + ) + + # here in the foward we implement the hard constraints + def forward(self, x): + hard_space = ( + x.extract(["x"]) + * (1 - x.extract(["x"])) + * x.extract(["y"]) + * (1 - x.extract(["y"])) + ) + hard_t = ( + torch.sin(torch.pi * x.extract(["x"])) + * torch.sin(torch.pi * x.extract(["y"])) + * torch.cos( + torch.sqrt(torch.tensor(2.0)) * torch.pi * x.extract(["t"]) + ) + ) + return hard_space * self.layers(x) * x.extract(["t"]) + hard_t + + +# Now let's train with the same configuration as the previous test + +# In[9]: + + +# define model +model = HardMLPtime(len(problem.input_variables), len(problem.output_variables)) + +# crete the solver +pinn = PINN(problem=problem, model=model) + +# create trainer and train +trainer = Trainer( + solver=pinn, + max_epochs=1000, + accelerator="cpu", + enable_model_summary=False, + train_size=1.0, + val_size=0.0, + test_size=0.0, + callbacks=[MetricTracker(["train_loss", "initial_loss", "D_loss"])], +) +trainer.train() + + +# We can clearly see that the loss is way lower now. Let's plot the results + +# In[10]: + + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=0) + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=0.5) + +plt.figure(figsize=(12, 6)) +plot_solution(solver=pinn, time=1) + + +# We can see now that the results are way better! This is due to the fact that previously the network was not learning correctly the initial conditon, leading to a poor solution when time evolved. By imposing the initial condition the network is able to correctly solve the problem. + +# ## What's next? +# +# Congratulations on completing the two dimensional Wave tutorial of **PINA**! There are multiple directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the finaly accuracy +# +# 2. Propose new types of hard constraints in time, e.g. $$ u_{\rm{pinn}} = xy(1-x)(1-y)\cdot NN(x, y, t)(1-\exp(-t)) + \cos(\sqrt{2}\pi t)sin(\pi x)\sin(\pi y), $$ +# +# 3. Exploit extrafeature training for model 1 and 2 +# +# 4. Many more... diff --git a/tutorials/tutorial4/tutorial.ipynb b/tutorials/tutorial4/tutorial.ipynb index 9f3353801..f1df1b224 100644 --- a/tutorials/tutorial4/tutorial.ipynb +++ b/tutorials/tutorial4/tutorial.ipynb @@ -35,26 +35,27 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", - "import torch \n", - "import matplotlib.pyplot as plt \n", - "import torchvision # for MNIST dataset\n", + "import torch\n", + "import matplotlib.pyplot as plt\n", + "import torchvision # for MNIST dataset\n", "import warnings\n", "\n", "from pina import Trainer\n", "from pina.problem.zoo import SupervisedProblem\n", "from pina.solver import SupervisedSolver\n", "from pina.trainer import Trainer\n", - "from pina.model.block import ContinuousConvBlock \n", - "from pina.model import FeedForward # for building AE and MNIST classification\n", + "from pina.model.block import ContinuousConvBlock\n", + "from pina.model import FeedForward # for building AE and MNIST classification\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -517,7 +518,7 @@ "source": [ "# setting the problem\n", "problem = SupervisedProblem(\n", - " input_=train_data.train_data.unsqueeze(1), # adding channel dimension\n", + " input_=train_data.train_data.unsqueeze(1), # adding channel dimension\n", " output_=train_data.train_labels,\n", ")\n", "\n", @@ -568,7 +569,7 @@ "source": [ "correct = 0\n", "total = 0\n", - "trainer.data_module.setup('test')\n", + "trainer.data_module.setup(\"test\")\n", "with torch.no_grad():\n", " for data in trainer.data_module.test_dataloader():\n", " test_data = data[\"data\"]\n", @@ -580,9 +581,7 @@ " total += labels.size(0)\n", " correct += (predicted == labels).sum().item()\n", "\n", - "print(\n", - " f\"Accuracy of the network on the test images: {(correct / total):.3%}\"\n", - ")" + "print(f\"Accuracy of the network on the test images: {(correct / total):.3%}\")" ] }, { diff --git a/tutorials/tutorial4/tutorial.py b/tutorials/tutorial4/tutorial.py new file mode 100644 index 000000000..d4db53c89 --- /dev/null +++ b/tutorials/tutorial4/tutorial.py @@ -0,0 +1,676 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Unstructured convolutional autoencoder via continuous convolution +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial4/tutorial.ipynb) + +# In this tutorial, we will show how to use the Continuous Convolutional Filter, and how to build common Deep Learning architectures with it. The implementation of the filter follows the original work [*A Continuous Convolutional Trainable Filter for Modelling Unstructured Data*](https://arxiv.org/abs/2210.13416). + +# First of all we import the modules needed for the tutorial: + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import matplotlib.pyplot as plt +import torchvision # for MNIST dataset +import warnings + +from pina import Trainer +from pina.problem.zoo import SupervisedProblem +from pina.solver import SupervisedSolver +from pina.trainer import Trainer +from pina.model.block import ContinuousConvBlock +from pina.model import FeedForward # for building AE and MNIST classification + +warnings.filterwarnings("ignore") + + +# The tutorial is structured as follow: +# * [Continuous filter background](#continuous-filter-background): understand how the convolutional filter works and how to use it. +# * [Building a MNIST Classifier](#building-a-mnist-classifier): show how to build a simple classifier using the MNIST dataset and how to combine a continuous convolutional layer with a feedforward neural network. +# * [Building a Continuous Convolutional Autoencoder](#building-a-continuous-convolutional-autoencoder): show how to use the continuous filter to work with unstructured data for autoencoding and up-sampling. + +# ## Continuous filter background + +# As reported by the authors in the original paper: in contrast to discrete convolution, continuous convolution is mathematically defined as: +# +# $$ +# \mathcal{I}_{\rm{out}}(\mathbf{x}) = \int_{\mathcal{X}} \mathcal{I}(\mathbf{x} + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{\tau}) d\mathbf{\tau}, +# $$ +# where $\mathcal{K} : \mathcal{X} \rightarrow \mathbb{R}$ is the *continuous filter* function, and $\mathcal{I} : \Omega \subset \mathbb{R}^N \rightarrow \mathbb{R}$ is the input function. The continuous filter function is approximated using a FeedForward Neural Network, thus trainable during the training phase. The way in which the integral is approximated can be different, currently on **PINA** we approximate it using a simple sum, as suggested by the authors. Thus, given $\{\mathbf{x}_i\}_{i=1}^{n}$ points in $\mathbb{R}^N$ of the input function mapped on the $\mathcal{X}$ filter domain, we approximate the above equation as: +# $$ +# \mathcal{I}_{\rm{out}}(\mathbf{\tilde{x}}_i) = \sum_{{\mathbf{x}_i}\in\mathcal{X}} \mathcal{I}(\mathbf{x}_i + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{x}_i), +# $$ +# where $\mathbf{\tau} \in \mathcal{S}$, with $\mathcal{S}$ the set of available strides, corresponds to the current stride position of the filter, and $\mathbf{\tilde{x}}_i$ points are obtained by taking the centroid of the filter position mapped on the $\Omega$ domain. + +# We will now try to pratically see how to work with the filter. From the above definition we see that what is needed is: +# 1. A domain and a function defined on that domain (the input) +# 2. A stride, corresponding to the positions where the filter needs to be $\rightarrow$ `stride` variable in `ContinuousConv` +# 3. The filter rectangular domain $\rightarrow$ `filter_dim` variable in `ContinuousConv` + +# ### Input function +# +# The input function for the continuous filter is defined as a tensor of shape: $$[B \times N_{in} \times N \times D]$$ where $B$ is the batch_size, $N_{in}$ is the number of input fields, $N$ the number of points in the mesh, $D$ the dimension of the problem. In particular: +# * $D$ is the number of spatial variables + 1. The last column must contain the field value. For example for 2D problems $D=3$ and the tensor will be something like `[first coordinate, second coordinate, field value]` +# * $N_{in}$ represents the number of vectorial function presented. For example a vectorial function $f = [f_1, f_2]$ will have $N_{in}=2$ +# +# Let's see an example to clear the ideas. We will be verbose to explain in details the input form. We wish to create the function: +# $$ +# f(x, y) = [\sin(\pi x) \sin(\pi y), -\sin(\pi x) \sin(\pi y)] \quad (x,y)\in[0,1]\times[0,1] +# $$ +# +# using a batch size equal to 1. + +# In[2]: + + +# batch size fixed to 1 +batch_size = 1 + +# points in the mesh fixed to 200 +N = 200 + +# vectorial 2 dimensional function, number_input_fields=2 +number_input_fields = 2 + +# 2 dimensional spatial variables, D = 2 + 1 = 3 +D = 3 + +# create the function f domain as random 2d points in [0, 1] +domain = torch.rand(size=(batch_size, number_input_fields, N, D - 1)) +print(f"Domain has shape: {domain.shape}") + +# create the functions +pi = torch.acos(torch.tensor([-1.0])) # pi value +f1 = torch.sin(pi * domain[:, 0, :, 0]) * torch.sin(pi * domain[:, 0, :, 1]) +f2 = -torch.sin(pi * domain[:, 1, :, 0]) * torch.sin(pi * domain[:, 1, :, 1]) + +# stacking the input domain and field values +data = torch.empty(size=(batch_size, number_input_fields, N, D)) +data[..., :-1] = domain # copy the domain +data[:, 0, :, -1] = f1 # copy first field value +data[:, 1, :, -1] = f1 # copy second field value +print(f"Filter input data has shape: {data.shape}") + + +# ### Stride +# +# The stride is passed as a dictionary `stride` which tells the filter where to go. Here is an example for the $[0,1]\times[0,5]$ domain: +# +# ```python +# # stride definition +# stride = {"domain": [1, 5], +# "start": [0, 0], +# "jump": [0.1, 0.3], +# "direction": [1, 1], +# } +# ``` +# This tells the filter: +# 1. `domain`: square domain (the only implemented) $[0,1]\times[0,5]$. The minimum value is always zero, while the maximum is specified by the user +# 2. `start`: start position of the filter, coordinate $(0, 0)$ +# 3. `jump`: the jumps of the centroid of the filter to the next position $(0.1, 0.3)$ +# 4. `direction`: the directions of the jump, with `1 = right`, `0 = no jump`, `-1 = left` with respect to the current position +# +# **Note** +# +# We are planning to release the possibility to directly pass a list of possible strides! + +# ### Filter definition +# +# Having defined all the previous blocks, we are now able to construct the continuous filter. +# +# Suppose we would like to get an output with only one field, and let us fix the filter dimension to be $[0.1, 0.1]$. + +# In[3]: + + +# filter dim +filter_dim = [0.1, 0.1] + +# stride +stride = { + "domain": [1, 1], + "start": [0, 0], + "jump": [0.08, 0.08], + "direction": [1, 1], +} + +# creating the filter +cConv = ContinuousConvBlock( + input_numb_field=number_input_fields, + output_numb_field=1, + filter_dim=filter_dim, + stride=stride, +) + + +# That's it! In just one line of code we have created the continuous convolutional filter. By default the `pina.model.FeedForward` neural network is intitialised, more on the [documentation](https://mathlab.github.io/PINA/_rst/fnn.html). In case the mesh doesn't change during training we can set the `optimize` flag equals to `True`, to exploit optimizations for finding the points to convolve. + +# In[4]: + + +# creating the filter + optimization +cConv = ContinuousConvBlock( + input_numb_field=number_input_fields, + output_numb_field=1, + filter_dim=filter_dim, + stride=stride, + optimize=True, +) + + +# Let's try to do a forward pass: + +# In[5]: + + +print(f"Filter input data has shape: {data.shape}") + +# input to the filter +output = cConv(data) + +print(f"Filter output data has shape: {output.shape}") + + +# If we don't want to use the default `FeedForward` neural network, we can pass a specified torch model in the `model` keyword as follow: +# + +# In[6]: + + +class SimpleKernel(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.model = torch.nn.Sequential( + torch.nn.Linear(2, 20), + torch.nn.ReLU(), + torch.nn.Linear(20, 20), + torch.nn.ReLU(), + torch.nn.Linear(20, 1), + ) + + def forward(self, x): + return self.model(x) + + +cConv = ContinuousConvBlock( + input_numb_field=number_input_fields, + output_numb_field=1, + filter_dim=filter_dim, + stride=stride, + optimize=True, + model=SimpleKernel, +) + + +# Notice that we pass the class and not an already built object! + +# ## Building a MNIST Classifier +# +# Let's see how we can build a MNIST classifier using a continuous convolutional filter. We will use the MNIST dataset from PyTorch. In order to keep small training times we use only 6000 samples for training and 1000 samples for testing. + +# In[7]: + + +from torch.utils.data import DataLoader, SubsetRandomSampler + +numb_training = 6000 # get just 6000 images for training +numb_testing = 1000 # get just 1000 images for training +seed = 111 # for reproducibility +batch_size = 8 # setting batch size + +# setting the seed +torch.manual_seed(seed) + +# downloading the dataset +train_data = torchvision.datasets.MNIST( + "./data/", + download=True, + train=False, + transform=torchvision.transforms.Compose( + [ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize((0.1307,), (0.3081,)), + ] + ), +) + + +# Let's now build a simple classifier. The MNIST dataset is composed by vectors of shape `[batch, 1, 28, 28]`, but we can image them as one field functions where the pixels $ij$ are the coordinate $x=i, y=j$ in a $[0, 27]\times[0,27]$ domain, and the pixels values are the field values. We just need a function to transform the regular tensor in a tensor compatible for the continuous filter: + +# In[8]: + + +def transform_input(x): + batch_size = x.shape[0] + dim_grid = tuple(x.shape[:-3:-1]) + + # creating the n dimensional mesh grid for a single channel image + values_mesh = [torch.arange(0, dim).float() for dim in dim_grid] + mesh = torch.meshgrid(values_mesh) + coordinates_mesh = [m.reshape(-1, 1).to(x.device) for m in mesh] + coordinates = ( + torch.cat(coordinates_mesh, dim=1) + .unsqueeze(0) + .repeat((batch_size, 1, 1)) + .unsqueeze(1) + ) + + return torch.cat((coordinates, x.flatten(2).unsqueeze(-1)), dim=-1) + + +# We can now build a simple classifier! We will use just one convolutional filter followed by a feedforward neural network + +# In[9]: + + +# setting the seed +torch.manual_seed(seed) + + +class ContinuousClassifier(torch.nn.Module): + def __init__(self): + super().__init__() + + # number of classes for classification + numb_class = 10 + + # convolutional block + self.convolution = ContinuousConvBlock( + input_numb_field=1, + output_numb_field=4, + stride={ + "domain": [27, 27], + "start": [0, 0], + "jumps": [4, 4], + "direction": [1, 1.0], + }, + filter_dim=[4, 4], + optimize=True, + ) + # feedforward net + self.nn = FeedForward( + input_dimensions=196, + output_dimensions=numb_class, + layers=[120, 64], + func=torch.nn.ReLU, + ) + + def forward(self, x): + # transform input + convolution + x = transform_input(x) + x = self.convolution(x) + # feed forward classification + return self.nn(x[..., -1].flatten(1)) + + +# We now aim to solve the classification problem. For this we will use the `SupervisedSolver` and the `SupervisedProblem`. The input of the supervised problems are the images, while the output the corresponding class. + +# In[10]: + + +# setting the problem +problem = SupervisedProblem( + input_=train_data.train_data.unsqueeze(1), # adding channel dimension + output_=train_data.train_labels, +) + +# setting the solver +solver = SupervisedSolver( + problem=problem, + model=ContinuousClassifier(), + loss=torch.nn.CrossEntropyLoss(), + use_lt=False, +) + +# setting the trainer +trainer = Trainer( + solver=solver, + max_epochs=1, + accelerator="cpu", + enable_model_summary=False, + train_size=0.7, + val_size=0.1, + test_size=0.2, + batch_size=64, +) +trainer.train() + + +# Let's see the performance on the test set! + +# In[11]: + + +correct = 0 +total = 0 +trainer.data_module.setup("test") +with torch.no_grad(): + for data in trainer.data_module.test_dataloader(): + test_data = data["data"] + images, labels = test_data["input"], test_data["target"] + # calculate outputs by running images through the network + outputs = solver(images) + # the class with the highest energy is what we choose as prediction + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + +print(f"Accuracy of the network on the test images: {(correct / total):.3%}") + + +# As we can see we have very good performance for having trained only for 1 epoch! Nevertheless, we are still using structured data... Let's see how we can build an autoencoder for unstructured data now. + +# ## Building a Continuous Convolutional Autoencoder +# +# Just as toy problem, we will now build an autoencoder for the following function $f(x,y)=\sin(\pi x)\sin(\pi y)$ on the unit circle domain centered in $(0.5, 0.5)$. We will also see the ability to up-sample (once trained) the results without retraining. Let's first create the input and visualize it, we will use firstly a mesh of $100$ points. + +# In[12]: + + +# create inputs +def circle_grid(N=100): + """Generate points withing a unit 2D circle centered in (0.5, 0.5) + + :param N: number of points + :type N: float + :return: [x, y] array of points + :rtype: torch.tensor + """ + + PI = torch.acos(torch.zeros(1)).item() * 2 + R = 0.5 + centerX = 0.5 + centerY = 0.5 + + r = R * torch.sqrt(torch.rand(N)) + theta = torch.rand(N) * 2 * PI + + x = centerX + r * torch.cos(theta) + y = centerY + r * torch.sin(theta) + + return torch.stack([x, y]).T + + +# create the grid +grid = circle_grid(500) + +# create input +input_data = torch.empty(size=(1, 1, grid.shape[0], 3)) +input_data[0, 0, :, :-1] = grid +input_data[0, 0, :, -1] = torch.sin(pi * grid[:, 0]) * torch.sin( + pi * grid[:, 1] +) + +# visualize data +plt.title("Training sample with 500 points") +plt.scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1]) +plt.colorbar() +plt.show() + + +# Let's now build a simple autoencoder using the continuous convolutional filter. The data is clearly unstructured and a simple convolutional filter might not work without projecting or interpolating first. Let's first build and `Encoder` and `Decoder` class, and then a `Autoencoder` class that contains both. + +# In[13]: + + +class Encoder(torch.nn.Module): + def __init__(self, hidden_dimension): + super().__init__() + + # convolutional block + self.convolution = ContinuousConvBlock( + input_numb_field=1, + output_numb_field=2, + stride={ + "domain": [1, 1], + "start": [0, 0], + "jumps": [0.05, 0.05], + "direction": [1, 1.0], + }, + filter_dim=[0.15, 0.15], + optimize=True, + ) + # feedforward net + self.nn = FeedForward( + input_dimensions=400, + output_dimensions=hidden_dimension, + layers=[240, 120], + ) + + def forward(self, x): + # convolution + x = self.convolution(x) + # feed forward pass + return self.nn(x[..., -1]) + + +class Decoder(torch.nn.Module): + def __init__(self, hidden_dimension): + super().__init__() + + # convolutional block + self.convolution = ContinuousConvBlock( + input_numb_field=2, + output_numb_field=1, + stride={ + "domain": [1, 1], + "start": [0, 0], + "jumps": [0.05, 0.05], + "direction": [1, 1.0], + }, + filter_dim=[0.15, 0.15], + optimize=True, + ) + # feedforward net + self.nn = FeedForward( + input_dimensions=hidden_dimension, + output_dimensions=400, + layers=[120, 240], + ) + + def forward(self, weights, grid): + # feed forward pass + x = self.nn(weights) + # transpose convolution + return torch.sigmoid(self.convolution.transpose(x, grid)) + + +# Very good! Notice that in the `Decoder` class in the `forward` pass we have used the `.transpose()` method of the `ContinuousConvolution` class. This method accepts the `weights` for upsampling and the `grid` on where to upsample. Let's now build the autoencoder! We set the hidden dimension in the `hidden_dimension` variable. We apply the sigmoid on the output since the field value is between $[0, 1]$. + +# In[14]: + + +class Autoencoder(torch.nn.Module): + def __init__(self, hidden_dimension=10): + super().__init__() + + self.encoder = Encoder(hidden_dimension) + self.decoder = Decoder(hidden_dimension) + + def forward(self, x): + # saving grid for later upsampling + grid = x.clone().detach() + # encoder + weights = self.encoder(x) + # decoder + out = self.decoder(weights, grid) + return out + + +# Let's now train the autoencoder, minimizing the mean square error loss and optimizing using Adam. We use the `SupervisedSolver` as solver, and the problem is a simple problem created by inheriting from `AbstractProblem`. It takes approximately two minutes to train on CPU. + +# In[15]: + + +# define the problem +problem = SupervisedProblem(input_data, input_data) + + +# define the solver +solver = SupervisedSolver( + problem=problem, + model=Autoencoder(), + loss=torch.nn.MSELoss(), + use_lt=False, +) + +# train +trainer = Trainer( + solver, + max_epochs=100, + accelerator="cpu", + enable_model_summary=False, # we train on CPU and avoid model summary at beginning of training (optional) + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# Let's visualize the two solutions side by side! + +# In[16]: + + +solver.eval() + +# get output and detach from computational graph for plotting +output = solver(input_data).detach() + +# visualize data +fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) +pic1 = axes[0].scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1]) +axes[0].set_title("Real") +fig.colorbar(pic1) +plt.subplot(1, 2, 2) +pic2 = axes[1].scatter(grid[:, 0], grid[:, 1], c=output[0, 0, :, -1]) +axes[1].set_title("Autoencoder") +fig.colorbar(pic2) +plt.tight_layout() +plt.show() + + +# As we can see, the two solutions are really similar! We can compute the $l_2$ error quite easily as well: + +# In[17]: + + +def l2_error(input_, target): + return torch.linalg.norm(input_ - target, ord=2) / torch.linalg.norm( + input_, ord=2 + ) + + +print(f"l2 error: {l2_error(input_data[0, 0, :, -1], output[0, 0, :, -1]):.2%}") + + +# More or less $4\%$ in $l_2$ error, which is really low considering the fact that we use just **one** convolutional layer and a simple feedforward to decrease the dimension. Let's see now some peculiarity of the filter. + +# ### Filter for upsampling +# +# Suppose we have already the hidden representation and we want to upsample on a differen grid with more points. Let's see how to do it: + +# In[18]: + + +# setting the seed +torch.manual_seed(seed) + +grid2 = circle_grid(1500) # triple number of points +input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3)) +input_data2[0, 0, :, :-1] = grid2 +input_data2[0, 0, :, -1] = torch.sin(pi * grid2[:, 0]) * torch.sin( + pi * grid2[:, 1] +) + +# get the hidden representation from original input +latent = solver.model.encoder(input_data) + +# upsample on the second input_data2 +output = solver.model.decoder(latent, input_data2).detach() + +# show the picture +fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) +pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1]) +axes[0].set_title("Real") +fig.colorbar(pic1) +plt.subplot(1, 2, 2) +pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1]) +axes[1].set_title("Up-sampling") +fig.colorbar(pic2) +plt.tight_layout() +plt.show() + + +# As we can see we have a very good approximation of the original function, even thought some noise is present. Let's calculate the error now: + +# In[19]: + + +print( + f"l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}" +) + + +# ### Autoencoding at different resolutions +# In the previous example we already had the hidden representation (of the original input) and we used it to upsample. Sometimes however we could have a finer mesh solution and we would simply want to encode it. This can be done without retraining! This procedure can be useful in case we have many points in the mesh and just a smaller part of them are needed for training. Let's see the results of this: + +# In[20]: + + +# setting the seed +torch.manual_seed(seed) + +grid2 = circle_grid(3500) # very fine mesh +input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3)) +input_data2[0, 0, :, :-1] = grid2 +input_data2[0, 0, :, -1] = torch.sin(pi * grid2[:, 0]) * torch.sin( + pi * grid2[:, 1] +) + +# get the hidden representation from finer mesh input +latent = solver.model.encoder(input_data2) + +# upsample on the second input_data2 +output = solver.model.decoder(latent, input_data2).detach() + +# show the picture +fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3)) +pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1]) +axes[0].set_title("Real") +fig.colorbar(pic1) +plt.subplot(1, 2, 2) +pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1]) +axes[1].set_title("Autoencoder not re-trained") +fig.colorbar(pic2) +plt.tight_layout() +plt.show() + +# calculate l2 error +print( + f"l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}" +) + + +# ## What's next? +# +# We have shown the basic usage of a convolutional filter. There are additional extensions possible: +# +# 1. Train using Physics Informed strategies +# +# 2. Use the filter to build an unstructured convolutional autoencoder for reduced order modelling +# +# 3. Many more... diff --git a/tutorials/tutorial5/tutorial.ipynb b/tutorials/tutorial5/tutorial.ipynb index 78d59afed..688046c91 100644 --- a/tutorials/tutorial5/tutorial.ipynb +++ b/tutorials/tutorial5/tutorial.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5f2744dc", "metadata": { "ExecuteTime": { @@ -33,15 +33,16 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", - " !pip install scipy\n", - " # get the data\n", - " !wget https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial5/Data_Darcy.mat\n", + " !pip install \"pina-mathlab\"\n", + " !pip install scipy\n", + " # get the data\n", + " !wget https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial5/Data_Darcy.mat\n", "\n", "import torch\n", "import matplotlib.pyplot as plt\n", @@ -54,7 +55,7 @@ "from pina.solver import SupervisedSolver\n", "from pina.problem.zoo import SupervisedProblem\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -129,10 +130,10 @@ ], "source": [ "plt.subplot(1, 2, 1)\n", - "plt.title('permeability')\n", + "plt.title(\"permeability\")\n", "plt.imshow(k_train[0])\n", "plt.subplot(1, 2, 2)\n", - "plt.title('field solution')\n", + "plt.title(\"field solution\")\n", "plt.imshow(u_train[0])\n", "plt.show()" ] @@ -278,12 +279,10 @@ " )\n", " * 100\n", ")\n", - "print(f'Final error training {err:.2f}%')\n", + "print(f\"Final error training {err:.2f}%\")\n", "\n", "err = (\n", - " float(\n", - " metric_err(u_test.unsqueeze(-1), model(k_test.unsqueeze(-1))).mean()\n", - " )\n", + " float(metric_err(u_test.unsqueeze(-1), model(k_test.unsqueeze(-1))).mean())\n", " * 100\n", ")\n", "print(f\"Final error testing {err:.2f}%\")" diff --git a/tutorials/tutorial5/tutorial.py b/tutorials/tutorial5/tutorial.py new file mode 100644 index 000000000..7a835c757 --- /dev/null +++ b/tutorials/tutorial5/tutorial.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Two dimensional Darcy flow using the Fourier Neural Operator +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial5/tutorial.ipynb) +# + +# In this tutorial we are going to solve the Darcy flow problem in two dimensions, presented in [*Fourier Neural Operator for +# Parametric Partial Differential Equation*](https://openreview.net/pdf?id=c8P9NQVtmnO). First of all we import the modules needed for the tutorial. Importing `scipy` is needed for input-output operations. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + get_ipython().system("pip install scipy") + # get the data + get_ipython().system( + "wget https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial5/Data_Darcy.mat" + ) + +import torch +import matplotlib.pyplot as plt +import warnings + +# !pip install scipy # install scipy +from scipy import io +from pina.model import FNO, FeedForward # let's import some models +from pina import Condition, Trainer +from pina.solver import SupervisedSolver +from pina.problem.zoo import SupervisedProblem + +warnings.filterwarnings("ignore") + + +# ## Data Generation +# +# We will focus on solving a specific PDE, the **Darcy Flow** equation. The Darcy PDE is a second-order elliptic PDE with the following form: +# +# $$ +# -\nabla\cdot(k(x, y)\nabla u(x, y)) = f(x) \quad (x, y) \in D. +# $$ +# +# Specifically, $u$ is the flow pressure, $k$ is the permeability field and $f$ is the forcing function. The Darcy flow can parameterize a variety of systems including flow through porous media, elastic materials and heat conduction. Here you will define the domain as a 2D unit square Dirichlet boundary conditions. The dataset is taken from the authors original reference. +# + +# In[2]: + + +# download the dataset +data = io.loadmat("Data_Darcy.mat") + +# extract data (we use only 100 data for train) +k_train = torch.tensor(data["k_train"], dtype=torch.float) +u_train = torch.tensor(data["u_train"], dtype=torch.float) +k_test = torch.tensor(data["k_test"], dtype=torch.float) +u_test = torch.tensor(data["u_test"], dtype=torch.float) +x = torch.tensor(data["x"], dtype=torch.float)[0] +y = torch.tensor(data["y"], dtype=torch.float)[0] + + +# Let's visualize some data + +# In[3]: + + +plt.subplot(1, 2, 1) +plt.title("permeability") +plt.imshow(k_train[0]) +plt.subplot(1, 2, 2) +plt.title("field solution") +plt.imshow(u_train[0]) +plt.show() + + +# We now create the Neural Operators problem class. Learning Neural Operators is similar as learning in a supervised manner, therefore we will use `SupervisedProblem`. + +# In[4]: + + +# make problem +problem = SupervisedProblem( + input_=k_train.unsqueeze(-1), output_=u_train.unsqueeze(-1) +) + + +# ## Solving the problem with a FeedForward Neural Network +# +# We will first solve the problem using a Feedforward neural network. We will use the `SupervisedSolver` for solving the problem, since we are training using supervised learning. + +# In[5]: + + +# make model +model = FeedForward(input_dimensions=1, output_dimensions=1) + + +# make solver +solver = SupervisedSolver(problem=problem, model=model, use_lt=False) + +# make the trainer and train +trainer = Trainer( + solver=solver, + max_epochs=10, + accelerator="cpu", + enable_model_summary=False, + batch_size=10, + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# The final loss is pretty high... We can calculate the error by importing `LpLoss`. + +# In[6]: + + +from pina.loss import LpLoss + +# make the metric +metric_err = LpLoss(relative=False) + +model = solver.model +err = ( + float( + metric_err(u_train.unsqueeze(-1), model(k_train.unsqueeze(-1))).mean() + ) + * 100 +) +print(f"Final error training {err:.2f}%") + +err = ( + float(metric_err(u_test.unsqueeze(-1), model(k_test.unsqueeze(-1))).mean()) + * 100 +) +print(f"Final error testing {err:.2f}%") + + +# ## Solving the problem with a Fourier Neural Operator (FNO) +# +# We will now move to solve the problem using a FNO. Since we are learning operator this approach is better suited, as we shall see. + +# In[7]: + + +# make model +lifting_net = torch.nn.Linear(1, 24) +projecting_net = torch.nn.Linear(24, 1) +model = FNO( + lifting_net=lifting_net, + projecting_net=projecting_net, + n_modes=8, + dimensions=2, + inner_size=24, + padding=8, +) + + +# make solver +solver = SupervisedSolver(problem=problem, model=model, use_lt=False) + +# make the trainer and train +trainer = Trainer( + solver=solver, + max_epochs=10, + accelerator="cpu", + enable_model_summary=False, + batch_size=10, + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# We can clearly see that the final loss is lower. Let's see in testing.. Notice that the number of parameters is way higher than a `FeedForward` network. We suggest to use GPU or TPU for a speed up in training, when many data samples are used. + +# In[8]: + + +model = solver.model +err = ( + float( + metric_err(u_train.unsqueeze(-1), model(k_train.unsqueeze(-1))).mean() + ) + * 100 +) +print(f"Final error training {err:.2f}%") + +err = ( + float(metric_err(u_test.unsqueeze(-1), model(k_test.unsqueeze(-1))).mean()) + * 100 +) +print(f"Final error testing {err:.2f}%") + + +# As we can see the loss is way lower! + +# ## What's next? +# +# We have made a very simple example on how to use the `FNO` for learning neural operator. Currently in **PINA** we implement 1D/2D/3D cases. We suggest to extend the tutorial using more complex problems and train for longer, to see the full potential of neural operators. diff --git a/tutorials/tutorial6/tutorial.ipynb b/tutorials/tutorial6/tutorial.ipynb index 563586178..522a9087f 100644 --- a/tutorials/tutorial6/tutorial.ipynb +++ b/tutorials/tutorial6/tutorial.ipynb @@ -26,21 +26,30 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import matplotlib.pyplot as plt\n", "\n", - "from pina.domain import EllipsoidDomain, Difference, CartesianDomain, Union, SimplexDomain, DomainInterface\n", + "from pina.domain import (\n", + " EllipsoidDomain,\n", + " Difference,\n", + " CartesianDomain,\n", + " Union,\n", + " SimplexDomain,\n", + " DomainInterface,\n", + ")\n", "from pina.label_tensor import LabelTensor\n", "\n", + "\n", "def plot_scatter(ax, pts, title):\n", " ax.title.set_text(title)\n", - " ax.scatter(pts.extract('x'), pts.extract('y'), color='blue', alpha=0.5)" + " ax.scatter(pts.extract(\"x\"), pts.extract(\"y\"), color=\"blue\", alpha=0.5)" ] }, { diff --git a/tutorials/tutorial6/tutorial.py b/tutorials/tutorial6/tutorial.py new file mode 100644 index 000000000..b35518434 --- /dev/null +++ b/tutorials/tutorial6/tutorial.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Building custom geometries with PINA `DomainInterface` class +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial6/tutorial.ipynb) +# +# In this tutorial we will show how to use geometries in PINA. Specifically, the tutorial will include how to create geometries and how to visualize them. The topics covered are: +# +# * Creating CartesianDomains and EllipsoidDomains +# * Getting the Union and Difference of Geometries +# * Sampling points in the domain (and visualize them) +# +# We import the relevant modules first. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import matplotlib.pyplot as plt + +from pina.domain import ( + EllipsoidDomain, + Difference, + CartesianDomain, + Union, + SimplexDomain, + DomainInterface, +) +from pina.label_tensor import LabelTensor + + +def plot_scatter(ax, pts, title): + ax.title.set_text(title) + ax.scatter(pts.extract("x"), pts.extract("y"), color="blue", alpha=0.5) + + +# ## Built-in Geometries + +# We will create one cartesian and two ellipsoids. For the sake of simplicity, we show here the 2-dimensional case, but the extension to 3D (and higher) cases is trivial. The geometries allow also the generation of samples belonging to the boundary. So, we will create one ellipsoid with the border and one without. + +# In[ ]: + + +cartesian = CartesianDomain({"x": [0, 2], "y": [0, 2]}) +ellipsoid_no_border = EllipsoidDomain({"x": [1, 3], "y": [1, 3]}) +ellipsoid_border = EllipsoidDomain( + {"x": [2, 4], "y": [2, 4]}, sample_surface=True +) + + +# The `{'x': [0, 2], 'y': [0, 2]}` are the bounds of the `CartesianDomain` being created. +# +# To visualize these shapes, we need to sample points on them. We will use the `sample` method of the `CartesianDomain` and `EllipsoidDomain` classes. This method takes a `n` argument which is the number of points to sample. It also takes different modes to sample, such as `'random'`. + +# In[ ]: + + +cartesian_samples = cartesian.sample(n=1000, mode="random") +ellipsoid_no_border_samples = ellipsoid_no_border.sample(n=1000, mode="random") +ellipsoid_border_samples = ellipsoid_border.sample(n=1000, mode="random") + + +# We can see the samples of each geometry to see what we are working with. + +# In[4]: + + +print(f"Cartesian Samples: {cartesian_samples}") +print(f"Ellipsoid No Border Samples: {ellipsoid_no_border_samples}") +print(f"Ellipsoid Border Samples: {ellipsoid_border_samples}") + + +# Notice how these are all `LabelTensor` objects. You can read more about these in the [documentation](https://mathlab.github.io/PINA/_rst/label_tensor.html). At a very high level, they are tensors where each element in a tensor has a label that we can access by doing `.labels`. We can also access the values of the tensor by doing `.extract(['x'])`. +# +# We are now ready to visualize the samples using matplotlib. + +# In[ ]: + + +fig, axs = plt.subplots(1, 3, figsize=(16, 4)) +pts_list = [ + cartesian_samples, + ellipsoid_no_border_samples, + ellipsoid_border_samples, +] +title_list = ["Cartesian Domain", "Ellipsoid Domain", "Ellipsoid Border Domain"] +for ax, pts, title in zip(axs, pts_list, title_list): + plot_scatter(ax, pts, title) + + +# We have now created, sampled, and visualized our first geometries! We can see that the `EllipsoidDomain` with the border has a border around it. We can also see that the `EllipsoidDomain` without the border is just the ellipse. We can also see that the `CartesianDomain` is just a square. + +# ### Simplex Domain +# +# Among the built-in shapes, we quickly show here the usage of `SimplexDomain`, which can be used for polygonal domains! + +# In[ ]: + + +import torch + +spatial_domain = SimplexDomain( + [ + LabelTensor(torch.tensor([[0, 0]]), labels=["x", "y"]), + LabelTensor(torch.tensor([[1, 1]]), labels=["x", "y"]), + LabelTensor(torch.tensor([[0, 2]]), labels=["x", "y"]), + ] +) + +spatial_domain2 = SimplexDomain( + [ + LabelTensor(torch.tensor([[0.0, -2.0]]), labels=["x", "y"]), + LabelTensor(torch.tensor([[-0.5, -0.5]]), labels=["x", "y"]), + LabelTensor(torch.tensor([[-2.0, 0.0]]), labels=["x", "y"]), + ] +) + +pts = spatial_domain2.sample(100) +fig, axs = plt.subplots(1, 2, figsize=(16, 6)) +for domain, ax in zip([spatial_domain, spatial_domain2], axs): + pts = domain.sample(1000) + plot_scatter(ax, pts, "Simplex Domain") + + +# ## Boolean Operations + +# To create complex shapes we can use the boolean operations, for example to merge two default geometries. We need to simply use the `Union` class: it takes a list of geometries and returns the union of them. +# +# Let's create three unions. Firstly, it will be a union of `cartesian` and `ellipsoid_no_border`. Next, it will be a union of `ellipse_no_border` and `ellipse_border`. Lastly, it will be a union of all three geometries. + +# In[7]: + + +cart_ellipse_nb_union = Union([cartesian, ellipsoid_no_border]) +cart_ellipse_b_union = Union([cartesian, ellipsoid_border]) +three_domain_union = Union([cartesian, ellipsoid_no_border, ellipsoid_border]) + + +# We can of course sample points over the new geometries, by using the `sample` method as before. We highlight that the available sample strategy here is only *random*. + +# In[ ]: + + +c_e_nb_u_points = cart_ellipse_nb_union.sample(n=2000, mode="random") +c_e_b_u_points = cart_ellipse_b_union.sample(n=2000, mode="random") +three_domain_union_points = three_domain_union.sample(n=3000, mode="random") + + +# We can plot the samples of each of the unions to see what we are working with. + +# In[ ]: + + +fig, axs = plt.subplots(1, 3, figsize=(16, 4)) +pts_list = [c_e_nb_u_points, c_e_b_u_points, three_domain_union_points] +title_list = [ + "Cartesian with Ellipsoid No Border Union", + "Cartesian with Ellipsoid Border Union", + "Three Domain Union", +] +for ax, pts, title in zip(axs, pts_list, title_list): + plot_scatter(ax, pts, title) + + +# Now, we will find the differences of the geometries. We will find the difference of `cartesian` and `ellipsoid_no_border`. + +# In[ ]: + + +cart_ellipse_nb_difference = Difference([cartesian, ellipsoid_no_border]) +c_e_nb_d_points = cart_ellipse_nb_difference.sample(n=2000, mode="random") + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +plot_scatter(ax, c_e_nb_d_points, "Difference") + + +# ## Create Custom DomainInterface + +# We will take a look on how to create our own geometry. The one we will try to make is a heart defined by the function $$(x^2+y^2-1)^3-x^2y^3 \le 0$$ + +# Let's start by importing what we will need to create our own geometry based on this equation. + +# In[11]: + + +import torch +from pina import LabelTensor + + +# Next, we will create the `Heart(DomainInterface)` class and initialize it. + +# In[ ]: + + +class Heart(DomainInterface): + """Implementation of the Heart Domain.""" + + def __init__(self, sample_border=False): + super().__init__() + + +# Because the `DomainInterface` class we are inheriting from requires both a `sample` method and `is_inside` method, we will create them and just add in "pass" for the moment. We also observe that the methods `sample_modes` and `variables` of the `DomainInterface` class are initialized as `abstractmethod`, so we need to redefine them both in the subclass `Heart` . + +# In[ ]: + + +class Heart(DomainInterface): + """Implementation of the Heart Domain.""" + + def __init__(self, sample_border=False): + super().__init__() + + def is_inside(self): + pass + + def sample(self): + pass + + @property + def sample_modes(self): + pass + + @property + def variables(self): + pass + + +# Now we have the skeleton for our `Heart` class. Also the `sample` method is where most of the work is done so let's fill it out. + +# In[ ]: + + +class Heart(DomainInterface): + """Implementation of the Heart Domain.""" + + def __init__(self, sample_border=False): + super().__init__() + + def is_inside(self): + pass + + def sample(self, n): + sampled_points = [] + + while len(sampled_points) < n: + x = torch.rand(1) * 3.0 - 1.5 + y = torch.rand(1) * 3.0 - 1.5 + if ((x**2 + y**2 - 1) ** 3 - (x**2) * (y**3)) <= 0: + sampled_points.append([x.item(), y.item()]) + + return LabelTensor(torch.tensor(sampled_points), labels=["x", "y"]) + + @property + def sample_modes(self): + pass + + @property + def variables(self): + pass + + +# To create the Heart geometry we simply run: + +# In[15]: + + +heart = Heart() + + +# To sample from the Heart geometry we simply run: + +# In[ ]: + + +pts_heart = heart.sample(1500) + +fig, ax = plt.subplots() +plot_scatter(ax, pts_heart, "Heart Domain") + + +# ## What's next? +# +# We have made a very simple tutorial on how to build custom geometries and use domain operation to compose base geometries. Now you can play around with different geometries and build your own! diff --git a/tutorials/tutorial7/tutorial.ipynb b/tutorials/tutorial7/tutorial.ipynb index 573f7954e..ad74cfe06 100644 --- a/tutorials/tutorial7/tutorial.ipynb +++ b/tutorials/tutorial7/tutorial.ipynb @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "00d1027d-13f2-4619-9ff7-a740568f13ff", "metadata": {}, "outputs": [ @@ -75,17 +75,18 @@ "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", - " # get the data\n", - " !mkdir \"data\"\n", - " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pinn_solution_0.5_0.5\" -O \"data/pinn_solution_0.5_0.5\"\n", - " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pts_0.5_0.5\" -O \"data/pts_0.5_0.5\"\n", - " \n", + " !pip install \"pina-mathlab\"\n", + " # get the data\n", + " !mkdir \"data\"\n", + " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pinn_solution_0.5_0.5\" -O \"data/pinn_solution_0.5_0.5\"\n", + " !wget \"https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pts_0.5_0.5\" -O \"data/pts_0.5_0.5\"\n", + "\n", "import matplotlib.pyplot as plt\n", "import torch\n", "import warnings\n", @@ -101,7 +102,7 @@ "from lightning.pytorch import seed_everything\n", "from lightning.pytorch.callbacks import Callback\n", "\n", - "warnings.filterwarnings('ignore')\n", + "warnings.filterwarnings(\"ignore\")\n", "seed_everything(883)" ] }, @@ -152,11 +153,11 @@ } ], "source": [ - "points = data_input.extract(['x', 'y']).detach().numpy()\n", + "points = data_input.extract([\"x\", \"y\"]).detach().numpy()\n", "truth = data_output.detach().numpy()\n", "\n", "plt.scatter(points[:, 0], points[:, 1], c=truth, s=8)\n", - "plt.axis('equal')\n", + "plt.axis(\"equal\")\n", "plt.colorbar()\n", "plt.show()" ] @@ -255,8 +256,8 @@ " layers=[20, 20, 20],\n", " func=torch.nn.Softplus,\n", " output_dimensions=len(problem.output_variables),\n", - " input_dimensions=len(problem.input_variables)\n", - " )" + " input_dimensions=len(problem.input_variables),\n", + ")" ] }, { diff --git a/tutorials/tutorial7/tutorial.py b/tutorials/tutorial7/tutorial.py new file mode 100644 index 000000000..69d51d729 --- /dev/null +++ b/tutorials/tutorial7/tutorial.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Resolution of an inverse problem +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial7/tutorial.ipynb) + +# ### Introduction to the inverse problem + +# This tutorial shows how to solve an inverse Poisson problem with Physics-Informed Neural Networks. The problem definition is that of a Poisson problem with homogeneous boundary conditions and it reads: +# \begin{equation} +# \begin{cases} +# \Delta u = e^{-2(x-\mu_1)^2-2(y-\mu_2)^2} \text{ in } \Omega\, ,\\ +# u = 0 \text{ on }\partial \Omega,\\ +# u(\mu_1, \mu_2) = \text{ data} +# \end{cases} +# \end{equation} +# where $\Omega$ is a square domain $[-2, 2] \times [-2, 2]$, and $\partial \Omega=\Gamma_1 \cup \Gamma_2 \cup \Gamma_3 \cup \Gamma_4$ is the union of the boundaries of the domain. +# +# This kind of problem, namely the "inverse problem", has two main goals: +# - find the solution $u$ that satisfies the Poisson equation; +# - find the unknown parameters ($\mu_1$, $\mu_2$) that better fit some given data (third equation in the system above). +# +# In order to achieve both goals we will need to define an `InverseProblem` in PINA. + +# Let's start with useful imports. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + # get the data + get_ipython().system('mkdir "data"') + get_ipython().system( + 'wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pinn_solution_0.5_0.5" -O "data/pinn_solution_0.5_0.5"' + ) + get_ipython().system( + 'wget "https://github.com/mathLab/PINA/raw/refs/heads/master/tutorials/tutorial7/data/pts_0.5_0.5" -O "data/pts_0.5_0.5"' + ) + +import matplotlib.pyplot as plt +import torch +import warnings + +from pina import Condition, Trainer +from pina.problem import SpatialProblem, InverseProblem +from pina.operator import laplacian +from pina.model import FeedForward +from pina.equation import Equation, FixedValue +from pina.solver import PINN +from pina.domain import CartesianDomain +from pina.optim import TorchOptimizer +from lightning.pytorch import seed_everything +from lightning.pytorch.callbacks import Callback + +warnings.filterwarnings("ignore") +seed_everything(883) + + +# Then, we import the pre-saved data, for ($\mu_1$, $\mu_2$)=($0.5$, $0.5$). These two values are the optimal parameters that we want to find through the neural network training. In particular, we import the `input` points (the spatial coordinates), and the `target` points (the corresponding $u$ values evaluated at the `input`). + +# In[21]: + + +data_output = torch.load( + "data/pinn_solution_0.5_0.5", weights_only=False +).detach() +data_input = torch.load("data/pts_0.5_0.5", weights_only=False) + + +# Moreover, let's plot also the data points and the reference solution: this is the expected output of the neural network. + +# In[22]: + + +points = data_input.extract(["x", "y"]).detach().numpy() +truth = data_output.detach().numpy() + +plt.scatter(points[:, 0], points[:, 1], c=truth, s=8) +plt.axis("equal") +plt.colorbar() +plt.show() + + +# ### Inverse problem definition in PINA + +# Then, we initialize the Poisson problem, that is inherited from the `SpatialProblem` and from the `InverseProblem` classes. We here have to define all the variables, and the domain where our unknown parameters ($\mu_1$, $\mu_2$) belong. Notice that the Laplace equation takes as inputs also the unknown variables, that will be treated as parameters that the neural network optimizes during the training process. + +# In[23]: + + +def laplace_equation(input_, output_, params_): + """ + Implementation of the laplace equation. + + :param LabelTensor input_: Input data of the problem. + :param LabelTensor output_: Output data of the problem. + :param dict params_: Parameters of the problem. + :return: The residual of the laplace equation. + :rtype: LabelTensor + """ + force_term = torch.exp( + -2 * (input_.extract(["x"]) - params_["mu1"]) ** 2 + - 2 * (input_.extract(["y"]) - params_["mu2"]) ** 2 + ) + delta_u = laplacian(output_, input_, components=["u"], d=["x", "y"]) + return delta_u - force_term + + +class Poisson(SpatialProblem, InverseProblem): + r""" + Implementation of the inverse 2-dimensional Poisson problem in the square + domain :math:`[0, 1] \times [0, 1]`, + with unknown parameter domain :math:`[-1, 1] \times [-1, 1]`. + """ + + output_variables = ["u"] + x_min, x_max = -2, 2 + y_min, y_max = -2, 2 + spatial_domain = CartesianDomain({"x": [x_min, x_max], "y": [y_min, y_max]}) + unknown_parameter_domain = CartesianDomain({"mu1": [-1, 1], "mu2": [-1, 1]}) + + domains = { + "g1": CartesianDomain({"x": [x_min, x_max], "y": y_max}), + "g2": CartesianDomain({"x": [x_min, x_max], "y": y_min}), + "g3": CartesianDomain({"x": x_max, "y": [y_min, y_max]}), + "g4": CartesianDomain({"x": x_min, "y": [y_min, y_max]}), + "D": CartesianDomain({"x": [x_min, x_max], "y": [y_min, y_max]}), + } + + conditions = { + "g1": Condition(domain="g1", equation=FixedValue(0.0)), + "g2": Condition(domain="g2", equation=FixedValue(0.0)), + "g3": Condition(domain="g3", equation=FixedValue(0.0)), + "g4": Condition(domain="g4", equation=FixedValue(0.0)), + "D": Condition(domain="D", equation=Equation(laplace_equation)), + "data": Condition(input=data_input, target=data_output), + } + + +problem = Poisson() + + +# Then, we define the neural network model we want to use. Here we used a model which imposes hard constrains on the boundary conditions, as also done in the Wave tutorial! + +# In[24]: + + +model = FeedForward( + layers=[20, 20, 20], + func=torch.nn.Softplus, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables), +) + + +# After that, we discretize the spatial domain. + +# In[25]: + + +problem.discretise_domain(20, "grid", domains=["D"]) +problem.discretise_domain( + 1000, + "random", + domains=["g1", "g2", "g3", "g4"], +) + + +# Here, we define a simple callback for the trainer. We use this callback to save the parameters predicted by the neural network during the training. The parameters are saved every 100 epochs as `torch` tensors in a specified directory (`tmp_dir` in our case). +# The goal is to read the saved parameters after training and plot their trend across the epochs. + +# In[26]: + + +# temporary directory for saving logs of training +tmp_dir = "tmp_poisson_inverse" + + +class SaveParameters(Callback): + """ + Callback to save the parameters of the model every 100 epochs. + """ + + def on_train_epoch_end(self, trainer, __): + if trainer.current_epoch % 100 == 99: + torch.save( + trainer.solver.problem.unknown_parameters, + "{}/parameters_epoch{}".format(tmp_dir, trainer.current_epoch), + ) + + +# Then, we define the `PINN` object and train the solver using the `Trainer`. + +# In[27]: + + +max_epochs = 1500 +pinn = PINN( + problem, model, optimizer=TorchOptimizer(torch.optim.Adam, lr=0.005) +) +# define the trainer for the solver +trainer = Trainer( + solver=pinn, + accelerator="cpu", + max_epochs=max_epochs, + default_root_dir=tmp_dir, + enable_model_summary=False, + callbacks=[SaveParameters()], + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# One can now see how the parameters vary during the training by reading the saved solution and plotting them. The plot shows that the parameters stabilize to their true value before reaching the epoch $1000$! + +# In[28]: + + +epochs_saved = range(99, max_epochs, 100) +parameters = torch.empty((int(max_epochs / 100), 2)) +for i, epoch in enumerate(epochs_saved): + params_torch = torch.load( + "{}/parameters_epoch{}".format(tmp_dir, epoch), weights_only=False + ) + for e, var in enumerate(pinn.problem.unknown_variables): + parameters[i, e] = params_torch[var].data + +# Plot parameters +plt.close() +plt.plot(epochs_saved, parameters[:, 0], label="mu1", marker="o") +plt.plot(epochs_saved, parameters[:, 1], label="mu2", marker="s") +plt.ylim(-1, 1) +plt.grid() +plt.legend() +plt.xlabel("Epoch") +plt.ylabel("Parameter value") +plt.show() + + +# ## What's next? +# +# We have shown the basic usage PINNs in inverse problem modelling, further extensions include: +# +# 1. Train using different Physics Informed strategies +# +# 2. Try on more complex problems +# +# 3. Many more... diff --git a/tutorials/tutorial8/tutorial.ipynb b/tutorials/tutorial8/tutorial.ipynb index acc263d6c..796b0937e 100644 --- a/tutorials/tutorial8/tutorial.ipynb +++ b/tutorials/tutorial8/tutorial.ipynb @@ -31,19 +31,20 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "id": "00d1027d-13f2-4619-9ff7-a740568f13ff", "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "%matplotlib inline\n", "\n", @@ -60,7 +61,7 @@ "from pina.problem.zoo import SupervisedProblem\n", "from pina.model.block import PODBlock, RBFBlock\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -93,12 +94,13 @@ ], "source": [ "from smithers.dataset import NavierStokesDataset\n", + "\n", "dataset = NavierStokesDataset()\n", "\n", "fig, axs = plt.subplots(1, 4, figsize=(14, 3))\n", - "for ax, p, u in zip(axs, dataset.params[:4], dataset.snapshots['mag(v)'][:4]):\n", + "for ax, p, u in zip(axs, dataset.params[:4], dataset.snapshots[\"mag(v)\"][:4]):\n", " ax.tricontourf(dataset.triang, u, levels=16)\n", - " ax.set_title(f'$\\mu$ = {p[0]:.2f}')" + " ax.set_title(f\"$\\mu$ = {p[0]:.2f}\")" ] }, { @@ -118,7 +120,7 @@ "metadata": {}, "outputs": [], "source": [ - "u = torch.tensor(dataset.snapshots['mag(v)']).float()\n", + "u = torch.tensor(dataset.snapshots[\"mag(v)\"]).float()\n", "p = torch.tensor(dataset.params).float()\n", "problem = SupervisedProblem(input_=p, output_=u)" ] @@ -158,20 +160,18 @@ " \"\"\"\n", " Proper orthogonal decomposition with neural network model.\n", " \"\"\"\n", + "\n", " def __init__(self, pod_rank, layers, func):\n", - " \"\"\"\n", - " \n", - " \"\"\"\n", + " \"\"\" \"\"\"\n", " super().__init__()\n", - " \n", + "\n", " self.pod = PODBlock(pod_rank)\n", " self.nn = FeedForward(\n", " input_dimensions=1,\n", " output_dimensions=pod_rank,\n", " layers=layers,\n", - " func=func\n", + " func=func,\n", " )\n", - " \n", "\n", " def forward(self, x):\n", " \"\"\"\n", @@ -210,10 +210,11 @@ "source": [ "pod_nn = PODNN(pod_rank=20, layers=[10, 10, 10], func=torch.nn.Tanh)\n", "pod_nn_stokes = SupervisedSolver(\n", - " problem=problem, \n", - " model=pod_nn, \n", + " problem=problem,\n", + " model=pod_nn,\n", " optimizer=TorchOptimizer(torch.optim.Adam, lr=0.0001),\n", - " use_lt=False)" + " use_lt=False,\n", + ")" ] }, { @@ -278,14 +279,17 @@ " solver=pod_nn_stokes,\n", " max_epochs=1000,\n", " batch_size=None,\n", - " accelerator='cpu',\n", + " accelerator=\"cpu\",\n", " train_size=0.9,\n", " val_size=0.0,\n", - " test_size=0.1)\n", + " test_size=0.1,\n", + ")\n", "\n", "# fit the pod basis\n", - "trainer.data_module.setup(\"fit\") # set up the dataset\n", - "x_train = trainer.data_module.train_dataset.conditions_dict[\"data\"][\"target\"] # extract data for training\n", + "trainer.data_module.setup(\"fit\") # set up the dataset\n", + "x_train = trainer.data_module.train_dataset.conditions_dict[\"data\"][\n", + " \"target\"\n", + "] # extract data for training\n", "pod_nn.fit_pod(x=x_train)\n", "\n", "# now train\n", @@ -328,12 +332,12 @@ "u_test_nn = pod_nn_stokes(p_test)\n", "u_train_nn = pod_nn_stokes(p_train)\n", "\n", - "relative_error_train = torch.norm(u_train_nn - u_train)/torch.norm(u_train)\n", - "relative_error_test = torch.norm(u_test_nn - u_test)/torch.norm(u_test)\n", + "relative_error_train = torch.norm(u_train_nn - u_train) / torch.norm(u_train)\n", + "relative_error_test = torch.norm(u_test_nn - u_test) / torch.norm(u_test)\n", "\n", - "print('Error summary for POD-NN model:')\n", - "print(f' Train: {relative_error_train.item():e}')\n", - "print(f' Test: {relative_error_test.item():e}')" + "print(\"Error summary for POD-NN model:\")\n", + "print(f\" Train: {relative_error_train.item():e}\")\n", + "print(f\" Test: {relative_error_test.item():e}\")" ] }, { @@ -365,14 +369,11 @@ " \"\"\"\n", "\n", " def __init__(self, pod_rank, rbf_kernel):\n", - " \"\"\"\n", - " \n", - " \"\"\"\n", + " \"\"\" \"\"\"\n", " super().__init__()\n", - " \n", + "\n", " self.pod = PODBlock(pod_rank)\n", " self.rbf = RBFBlock(kernel=rbf_kernel)\n", - " \n", "\n", " def forward(self, x):\n", " \"\"\"\n", @@ -412,7 +413,7 @@ "metadata": {}, "outputs": [], "source": [ - "pod_rbf = PODRBF(pod_rank=20, rbf_kernel='thin_plate_spline')\n", + "pod_rbf = PODRBF(pod_rank=20, rbf_kernel=\"thin_plate_spline\")\n", "pod_rbf.fit(p_train, u_train)" ] }, @@ -444,12 +445,12 @@ "u_test_rbf = pod_rbf(p_test)\n", "u_train_rbf = pod_rbf(p_train)\n", "\n", - "relative_error_train = torch.norm(u_train_rbf - u_train)/torch.norm(u_train)\n", - "relative_error_test = torch.norm(u_test_rbf - u_test)/torch.norm(u_test)\n", + "relative_error_train = torch.norm(u_train_rbf - u_train) / torch.norm(u_train)\n", + "relative_error_test = torch.norm(u_test_rbf - u_test) / torch.norm(u_test)\n", "\n", - "print('Error summary for POD-RBF model:')\n", - "print(f' Train: {relative_error_train.item():e}')\n", - "print(f' Test: {relative_error_test.item():e}')" + "print(\"Error summary for POD-RBF model:\")\n", + "print(f\" Train: {relative_error_train.item():e}\")\n", + "print(f\" Test: {relative_error_test.item():e}\")" ] }, { diff --git a/tutorials/tutorial8/tutorial.py b/tutorials/tutorial8/tutorial.py new file mode 100644 index 000000000..4f3f5bfcc --- /dev/null +++ b/tutorials/tutorial8/tutorial.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: Reduced order models (POD-NN and POD-RBF) for parametric problems +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial9/tutorial.ipynb) + +# The tutorial aims to show how to employ the **PINA** library in order to apply a reduced order modeling technique [1]. Such methodologies have several similarities with machine learning approaches, since the main goal consists in predicting the solution of differential equations (typically parametric PDEs) in a real-time fashion. +# +# In particular we are going to use the Proper Orthogonal Decomposition with either Radial Basis Function Interpolation (POD-RBF) or Neural Network (POD-NN) [2]. Here we basically perform a dimensional reduction using the POD approach, approximating the parametric solution manifold (at the reduced space) using a regression technique (NN) and comparing it to an RBF interpolation. In this example, we use a simple multilayer perceptron, but the plenty of different architectures can be plugged as well. + +# Let's start with the necessary imports. +# It's important to note the minimum PINA version to run this tutorial is the `0.1`. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +get_ipython().run_line_magic("matplotlib", "inline") + +import matplotlib +import matplotlib.pyplot as plt +import torch +import numpy as np +import warnings + +from pina import Trainer +from pina.model import FeedForward +from pina.solver import SupervisedSolver +from pina.optim import TorchOptimizer +from pina.problem.zoo import SupervisedProblem +from pina.model.block import PODBlock, RBFBlock + +warnings.filterwarnings("ignore") + + +# We exploit the [Smithers](https://github.com/mathLab/Smithers) library to collect the parametric snapshots. In particular, we use the `NavierStokesDataset` class that contains a set of parametric solutions of the Navier-Stokes equations in a 2D L-shape domain. The parameter is the inflow velocity. +# The dataset is composed by 500 snapshots of the velocity (along $x$, $y$, and the magnitude) and pressure fields, and the corresponding parameter values. +# +# To visually check the snapshots, let's plot also the data points and the reference solution: this is the expected output of our model. + +# In[83]: + + +from smithers.dataset import NavierStokesDataset + +dataset = NavierStokesDataset() + +fig, axs = plt.subplots(1, 4, figsize=(14, 3)) +for ax, p, u in zip(axs, dataset.params[:4], dataset.snapshots["mag(v)"][:4]): + ax.tricontourf(dataset.triang, u, levels=16) + ax.set_title(f"$\mu$ = {p[0]:.2f}") + + +# The *snapshots* - aka the numerical solutions computed for several parameters - and the corresponding parameters are the only data we need to train the model, in order to predict the solution for any new test parameter. To properly validate the accuracy, we will split the 500 snapshots into the training dataset (90% of the original data) and the testing one (the reamining 10%) inside the `Trainer`. +# +# It is now time to define the problem! + +# In[84]: + + +u = torch.tensor(dataset.snapshots["mag(v)"]).float() +p = torch.tensor(dataset.params).float() +problem = SupervisedProblem(input_=p, output_=u) + + +# We can then build a `POD-NN` model (using an MLP architecture as approximation) and compare it with a `POD-RBF` model (using a Radial Basis Function interpolation as approximation). + +# ## POD-NN reduced order model + +# Let's build the `PODNN` class + +# In[85]: + + +class PODNN(torch.nn.Module): + """ + Proper orthogonal decomposition with neural network model. + """ + + def __init__(self, pod_rank, layers, func): + """ """ + super().__init__() + + self.pod = PODBlock(pod_rank) + self.nn = FeedForward( + input_dimensions=1, + output_dimensions=pod_rank, + layers=layers, + func=func, + ) + + def forward(self, x): + """ + Defines the computation performed at every call. + + :param x: The tensor to apply the forward pass. + :type x: torch.Tensor + :return: the output computed by the model. + :rtype: torch.Tensor + """ + coefficents = self.nn(x) + return self.pod.expand(coefficents) + + def fit_pod(self, x): + """ + Just call the :meth:`pina.model.layers.PODBlock.fit` method of the + :attr:`pina.model.layers.PODBlock` attribute. + """ + self.pod.fit(x) + + +# We highlight that the POD modes are directly computed by means of the singular value decomposition (computed over the input data), and not trained using the backpropagation approach. Only the weights of the MLP are actually trained during the optimization loop. + +# In[86]: + + +pod_nn = PODNN(pod_rank=20, layers=[10, 10, 10], func=torch.nn.Tanh) +pod_nn_stokes = SupervisedSolver( + problem=problem, + model=pod_nn, + optimizer=TorchOptimizer(torch.optim.Adam, lr=0.0001), + use_lt=False, +) + + +# Before starting we need to fit the POD basis on the training dataset, this can be easily done in PINA as well: + +# In[87]: + + +trainer = Trainer( + solver=pod_nn_stokes, + max_epochs=1000, + batch_size=None, + accelerator="cpu", + train_size=0.9, + val_size=0.0, + test_size=0.1, +) + +# fit the pod basis +trainer.data_module.setup("fit") # set up the dataset +x_train = trainer.data_module.train_dataset.conditions_dict["data"][ + "target" +] # extract data for training +pod_nn.fit_pod(x=x_train) + +# now train +trainer.train() + + +# Done! Now that the computational expensive part is over, we can load in future the model to infer new parameters (simply loading the checkpoint file automatically created by `Lightning`) or test its performances. We measure the relative error for the training and test datasets, printing the mean one. + +# In[ ]: + + +# extract train and test data +trainer.data_module.setup("test") # set up the dataset +p_train = trainer.data_module.train_dataset.conditions_dict["data"]["input"] +u_train = trainer.data_module.train_dataset.conditions_dict["data"]["target"] +p_test = trainer.data_module.test_dataset.conditions_dict["data"]["input"] +u_test = trainer.data_module.test_dataset.conditions_dict["data"]["target"] + +# compute statistics +u_test_nn = pod_nn_stokes(p_test) +u_train_nn = pod_nn_stokes(p_train) + +relative_error_train = torch.norm(u_train_nn - u_train) / torch.norm(u_train) +relative_error_test = torch.norm(u_test_nn - u_test) / torch.norm(u_test) + +print("Error summary for POD-NN model:") +print(f" Train: {relative_error_train.item():e}") +print(f" Test: {relative_error_test.item():e}") + + +# ## POD-RBF reduced order model + +# Then, we define the model we want to use, with the POD (`PODBlock`) and the RBF (`RBFBlock`) objects. + +# In[89]: + + +class PODRBF(torch.nn.Module): + """ + Proper orthogonal decomposition with Radial Basis Function interpolation model. + """ + + def __init__(self, pod_rank, rbf_kernel): + """ """ + super().__init__() + + self.pod = PODBlock(pod_rank) + self.rbf = RBFBlock(kernel=rbf_kernel) + + def forward(self, x): + """ + Defines the computation performed at every call. + + :param x: The tensor to apply the forward pass. + :type x: torch.Tensor + :return: the output computed by the model. + :rtype: torch.Tensor + """ + coefficents = self.rbf(x) + return self.pod.expand(coefficents) + + def fit(self, p, x): + """ + Call the :meth:`pina.model.layers.PODBlock.fit` method of the + :attr:`pina.model.layers.PODBlock` attribute to perform the POD, + and the :meth:`pina.model.layers.RBFBlock.fit` method of the + :attr:`pina.model.layers.RBFBlock` attribute to fit the interpolation. + """ + self.pod.fit(x) + self.rbf.fit(p, self.pod.reduce(x)) + + +# We can then fit the model and ask it to predict the required field for unseen values of the parameters. Note that this model does not need a `Trainer` since it does not include any neural network or learnable parameters. + +# In[90]: + + +pod_rbf = PODRBF(pod_rank=20, rbf_kernel="thin_plate_spline") +pod_rbf.fit(p_train, u_train) + + +# Compute errors + +# In[91]: + + +u_test_rbf = pod_rbf(p_test) +u_train_rbf = pod_rbf(p_train) + +relative_error_train = torch.norm(u_train_rbf - u_train) / torch.norm(u_train) +relative_error_test = torch.norm(u_test_rbf - u_test) / torch.norm(u_test) + +print("Error summary for POD-RBF model:") +print(f" Train: {relative_error_train.item():e}") +print(f" Test: {relative_error_test.item():e}") + + +# ## POD-RBF vs POD-NN + +# We can of course also plot the solutions predicted by the `PODRBF` and by the `PODNN` model, comparing them to the original ones. We can note here, in the `PODNN` model and for low velocities, some differences, but improvements can be accomplished thanks to longer training. + +# In[92]: + + +idx = torch.randint(0, len(u_test), (4,)) +u_idx_rbf = pod_rbf(p_test[idx]) +u_idx_nn = pod_nn_stokes(p_test[idx]) + + +fig, axs = plt.subplots(4, 5, figsize=(14, 9)) + +relative_error_rbf = np.abs(u_test[idx] - u_idx_rbf.detach()) +relative_error_rbf = np.where( + u_test[idx] < 1e-7, 1e-7, relative_error_rbf / u_test[idx] +) + +relative_error_nn = np.abs(u_test[idx] - u_idx_nn.detach()) +relative_error_nn = np.where( + u_test[idx] < 1e-7, 1e-7, relative_error_nn / u_test[idx] +) + +for i, (idx_, rbf_, nn_, rbf_err_, nn_err_) in enumerate( + zip(idx, u_idx_rbf, u_idx_nn, relative_error_rbf, relative_error_nn) +): + + axs[0, 0].set_title(f"Real Snapshots") + axs[0, 1].set_title(f"POD-RBF") + axs[0, 2].set_title(f"POD-NN") + axs[0, 3].set_title(f"Error POD-RBF") + axs[0, 4].set_title(f"Error POD-NN") + + cm = axs[i, 0].tricontourf( + dataset.triang, rbf_.detach() + ) # POD-RBF prediction + plt.colorbar(cm, ax=axs[i, 0]) + + cm = axs[i, 1].tricontourf( + dataset.triang, nn_.detach() + ) # POD-NN prediction + plt.colorbar(cm, ax=axs[i, 1]) + + cm = axs[i, 2].tricontourf(dataset.triang, u_test[idx_].flatten()) # Truth + plt.colorbar(cm, ax=axs[i, 2]) + + cm = axs[i, 3].tripcolor( + dataset.triang, rbf_err_, norm=matplotlib.colors.LogNorm() + ) # Error for POD-RBF + plt.colorbar(cm, ax=axs[i, 3]) + + cm = axs[i, 4].tripcolor( + dataset.triang, nn_err_, norm=matplotlib.colors.LogNorm() + ) # Error for POD-NN + plt.colorbar(cm, ax=axs[i, 4]) + +plt.show() + + +# #### References +# 1. Rozza G., Stabile G., Ballarin F. (2022). Advanced Reduced Order Methods and Applications in Computational Fluid Dynamics, Society for Industrial and Applied Mathematics. +# 2. Hesthaven, J. S., & Ubbiali, S. (2018). Non-intrusive reduced order modeling of nonlinear problems using neural networks. Journal of Computational Physics, 363, 55-78. diff --git a/tutorials/tutorial9/tutorial.ipynb b/tutorials/tutorial9/tutorial.ipynb index 3f19c6c0e..daf81ec59 100644 --- a/tutorials/tutorial9/tutorial.ipynb +++ b/tutorials/tutorial9/tutorial.ipynb @@ -20,18 +20,19 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## routine needed to run the notebook on Google Colab\n", "try:\n", - " import google.colab\n", - " IN_COLAB = True\n", + " import google.colab\n", + "\n", + " IN_COLAB = True\n", "except:\n", - " IN_COLAB = False\n", + " IN_COLAB = False\n", "if IN_COLAB:\n", - " !pip install \"pina-mathlab\"\n", + " !pip install \"pina-mathlab\"\n", "\n", "import torch\n", "import matplotlib.pyplot as plt\n", @@ -47,7 +48,7 @@ "from pina.equation import Equation\n", "from pina.callback import MetricTracker\n", "\n", - "warnings.filterwarnings('ignore')" + "warnings.filterwarnings(\"ignore\")" ] }, { @@ -111,6 +112,7 @@ " def solution(self, pts):\n", " return torch.sin(torch.pi * pts) * torch.cos(3.0 * torch.pi * pts)\n", "\n", + "\n", "problem = Helmholtz()\n", "\n", "# let's discretise the domain\n", @@ -234,7 +236,7 @@ " pinn,\n", " max_epochs=5000,\n", " accelerator=\"cpu\",\n", - " enable_model_summary=False, \n", + " enable_model_summary=False,\n", " callbacks=[MetricTracker()],\n", " train_size=1.0,\n", " val_size=0.0,\n", @@ -266,9 +268,9 @@ " range(len(trainer_metrics[\"train_loss\"])), trainer_metrics[\"train_loss\"]\n", ")\n", "# plotting\n", - "plt.xlabel('epoch')\n", - "plt.ylabel('loss')\n", - "plt.yscale('log') " + "plt.xlabel(\"epoch\")\n", + "plt.ylabel(\"loss\")\n", + "plt.yscale(\"log\")" ] }, { @@ -305,11 +307,11 @@ } ], "source": [ - "pts = pinn.problem.spatial_domain.sample(256, 'grid', variables='x')\n", - "predicted_output = pinn.forward(pts).extract('u').tensor.detach()\n", + "pts = pinn.problem.spatial_domain.sample(256, \"grid\", variables=\"x\")\n", + "predicted_output = pinn.forward(pts).extract(\"u\").tensor.detach()\n", "true_output = pinn.problem.solution(pts)\n", - "plt.plot(pts.extract(['x']), predicted_output, label='Neural Network solution')\n", - "plt.plot(pts.extract(['x']), true_output, label='True solution')\n", + "plt.plot(pts.extract([\"x\"]), predicted_output, label=\"Neural Network solution\")\n", + "plt.plot(pts.extract([\"x\"]), true_output, label=\"True solution\")\n", "plt.legend()" ] }, @@ -340,21 +342,21 @@ "# plotting solution\n", "with torch.no_grad():\n", " # Notice here we put [-4, 4]!!!\n", - " new_domain = CartesianDomain({'x' : [0, 4]})\n", - " x = new_domain.sample(1000, mode='grid')\n", + " new_domain = CartesianDomain({\"x\": [0, 4]})\n", + " x = new_domain.sample(1000, mode=\"grid\")\n", " fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", " # Plot 1\n", - " axes[0].plot(x, problem.solution(x), label=r'$u(x)$', color='blue')\n", - " axes[0].set_title(r'True solution $u(x)$')\n", + " axes[0].plot(x, problem.solution(x), label=r\"$u(x)$\", color=\"blue\")\n", + " axes[0].set_title(r\"True solution $u(x)$\")\n", " axes[0].legend(loc=\"upper right\")\n", " # Plot 2\n", - " axes[1].plot(x, pinn(x), label=r'$u_{\\theta}(x)$', color='green')\n", - " axes[1].set_title(r'PINN solution $u_{\\theta}(x)$')\n", + " axes[1].plot(x, pinn(x), label=r\"$u_{\\theta}(x)$\", color=\"green\")\n", + " axes[1].set_title(r\"PINN solution $u_{\\theta}(x)$\")\n", " axes[1].legend(loc=\"upper right\")\n", " # Plot 3\n", " diff = torch.abs(problem.solution(x) - pinn(x))\n", - " axes[2].plot(x, diff, label=r'$|u(x) - u_{\\theta}(x)|$', color='red')\n", - " axes[2].set_title(r'Absolute difference $|u(x) - u_{\\theta}(x)|$')\n", + " axes[2].plot(x, diff, label=r\"$|u(x) - u_{\\theta}(x)|$\", color=\"red\")\n", + " axes[2].set_title(r\"Absolute difference $|u(x) - u_{\\theta}(x)|$\")\n", " axes[2].legend(loc=\"upper right\")\n", " # Adjust layout\n", " plt.tight_layout()\n", diff --git a/tutorials/tutorial9/tutorial.py b/tutorials/tutorial9/tutorial.py new file mode 100644 index 000000000..ae03c1892 --- /dev/null +++ b/tutorials/tutorial9/tutorial.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # Tutorial: One dimensional Helmholtz equation using Periodic Boundary Conditions +# +# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mathLab/PINA/blob/master/tutorials/tutorial9/tutorial.ipynb) +# +# This tutorial presents how to solve with Physics-Informed Neural Networks (PINNs) +# a one dimensional Helmholtz equation with periodic boundary conditions (PBC). +# We will train with standard PINN's training by augmenting the input with +# periodic expansion as presented in [*An expert’s guide to training +# physics-informed neural networks*]( +# https://arxiv.org/abs/2308.08468). +# +# First of all, some useful imports. + +# In[ ]: + + +## routine needed to run the notebook on Google Colab +try: + import google.colab + + IN_COLAB = True +except: + IN_COLAB = False +if IN_COLAB: + get_ipython().system('pip install "pina-mathlab"') + +import torch +import matplotlib.pyplot as plt +import warnings + +from pina import Condition, Trainer +from pina.problem import SpatialProblem +from pina.operator import laplacian +from pina.model import FeedForward +from pina.model.block import PeriodicBoundaryEmbedding # The PBC module +from pina.solver import PINN +from pina.domain import CartesianDomain +from pina.equation import Equation +from pina.callback import MetricTracker + +warnings.filterwarnings("ignore") + + +# ## The problem definition +# +# The one-dimensional Helmholtz problem is mathematically written as: +# $$ +# \begin{cases} +# \frac{d^2}{dx^2}u(x) - \lambda u(x) -f(x) &= 0 \quad x\in(0,2)\\ +# u^{(m)}(x=0) - u^{(m)}(x=2) &= 0 \quad m\in[0, 1, \cdots]\\ +# \end{cases} +# $$ +# In this case we are asking the solution to be $C^{\infty}$ periodic with +# period $2$, on the infinite domain $x\in(-\infty, \infty)$. Notice that the +# classical PINN would need infinite conditions to evaluate the PBC loss function, +# one for each derivative, which is of course infeasible... +# A possible solution, diverging from the original PINN formulation, +# is to use *coordinates augmentation*. In coordinates augmentation you seek for +# a coordinates transformation $v$ such that $x\rightarrow v(x)$ such that +# the periodicity condition $ u^{(m)}(x=0) - u^{(m)}(x=2) = 0 \quad m\in[0, 1, \cdots] $ is +# satisfied. +# +# For demonstration purposes, the problem specifics are $\lambda=-10\pi^2$, +# and $f(x)=-6\pi^2\sin(3\pi x)\cos(\pi x)$ which give a solution that can be +# computed analytically $u(x) = \sin(\pi x)\cos(3\pi x)$. + +# In[15]: + + +def helmholtz_equation(input_, output_): + x = input_.extract("x") + u_xx = laplacian(output_, input_, components=["u"], d=["x"]) + f = ( + -6.0 + * torch.pi**2 + * torch.sin(3 * torch.pi * x) + * torch.cos(torch.pi * x) + ) + lambda_ = -10.0 * torch.pi**2 + return u_xx - lambda_ * output_ - f + + +class Helmholtz(SpatialProblem): + output_variables = ["u"] + spatial_domain = CartesianDomain({"x": [0, 2]}) + + # here we write the problem conditions + conditions = { + "phys_cond": Condition( + domain=spatial_domain, equation=Equation(helmholtz_equation) + ), + } + + def solution(self, pts): + return torch.sin(torch.pi * pts) * torch.cos(3.0 * torch.pi * pts) + + +problem = Helmholtz() + +# let's discretise the domain +problem.discretise_domain(200, "grid", domains=["phys_cond"]) + + +# As usual, the Helmholtz problem is written in **PINA** code as a class. +# The equations are written as `conditions` that should be satisfied in the +# corresponding domains. The `solution` +# is the exact solution which will be compared with the predicted one. We used +# Latin Hypercube Sampling for choosing the collocation points. + +# ## Solving the problem with a Periodic Network + +# Any $\mathcal{C}^{\infty}$ periodic function +# $u : \mathbb{R} \rightarrow \mathbb{R}$ with period +# $L\in\mathbb{N}$ can be constructed by composition of an +# arbitrary smooth function $f : \mathbb{R}^n \rightarrow \mathbb{R}$ and a +# given smooth periodic function $v : \mathbb{R} \rightarrow \mathbb{R}^n$ with +# period $L$, that is $u(x) = f(v(x))$. The formulation is generalizable for +# arbitrary dimension, see [*A method for representing periodic functions and +# enforcing exactly periodic boundary conditions with +# deep neural networks*](https://arxiv.org/pdf/2007.07442). +# +# In our case, we rewrite +# $v(x) = \left[1, \cos\left(\frac{2\pi}{L} x\right), +# \sin\left(\frac{2\pi}{L} x\right)\right]$, i.e +# the coordinates augmentation, and $f(\cdot) = NN_{\theta}(\cdot)$ i.e. a neural +# network. The resulting neural network obtained by composing $f$ with $v$ gives +# the PINN approximate solution, that is +# $u(x) \approx u_{\theta}(x)=NN_{\theta}(v(x))$. +# +# In **PINA** this translates in using the `PeriodicBoundaryEmbedding` layer for $v$, and any +# `pina.model` for $NN_{\theta}$. Let's see it in action! +# + +# In[16]: + + +# we encapsulate all modules in a torch.nn.Sequential container +model = torch.nn.Sequential( + PeriodicBoundaryEmbedding(input_dimension=1, periods=2), + FeedForward( + input_dimensions=3, # output of PeriodicBoundaryEmbedding = 3 * input_dimension + output_dimensions=1, + layers=[10, 10], + ), +) + + +# As simple as that! Notice that in higher dimension you can specify different periods +# for all dimensions using a dictionary, e.g. `periods={'x':2, 'y':3, ...}` +# would indicate a periodicity of $2$ in $x$, $3$ in $y$, and so on... +# +# We will now solve the problem as usually with the `PINN` and `Trainer` class, then we will look at the losses using the `MetricTracker` callback from `pina.callback`. + +# In[17]: + + +pinn = PINN( + problem=problem, + model=model, +) +trainer = Trainer( + pinn, + max_epochs=5000, + accelerator="cpu", + enable_model_summary=False, + callbacks=[MetricTracker()], + train_size=1.0, + val_size=0.0, + test_size=0.0, +) +trainer.train() + + +# In[18]: + + +# plot loss +trainer_metrics = trainer.callbacks[0].metrics +plt.plot( + range(len(trainer_metrics["train_loss"])), trainer_metrics["train_loss"] +) +# plotting +plt.xlabel("epoch") +plt.ylabel("loss") +plt.yscale("log") + + +# We are going to plot the solution now! + +# In[19]: + + +pts = pinn.problem.spatial_domain.sample(256, "grid", variables="x") +predicted_output = pinn.forward(pts).extract("u").tensor.detach() +true_output = pinn.problem.solution(pts) +plt.plot(pts.extract(["x"]), predicted_output, label="Neural Network solution") +plt.plot(pts.extract(["x"]), true_output, label="True solution") +plt.legend() + + +# Great, they overlap perfectly! This seems a good result, considering the simple neural network used to some this (complex) problem. We will now test the neural network on the domain $[-4, 4]$ without retraining. In principle the periodicity should be present since the $v$ function ensures the periodicity in $(-\infty, \infty)$. + +# In[20]: + + +# plotting solution +with torch.no_grad(): + # Notice here we put [-4, 4]!!! + new_domain = CartesianDomain({"x": [0, 4]}) + x = new_domain.sample(1000, mode="grid") + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + # Plot 1 + axes[0].plot(x, problem.solution(x), label=r"$u(x)$", color="blue") + axes[0].set_title(r"True solution $u(x)$") + axes[0].legend(loc="upper right") + # Plot 2 + axes[1].plot(x, pinn(x), label=r"$u_{\theta}(x)$", color="green") + axes[1].set_title(r"PINN solution $u_{\theta}(x)$") + axes[1].legend(loc="upper right") + # Plot 3 + diff = torch.abs(problem.solution(x) - pinn(x)) + axes[2].plot(x, diff, label=r"$|u(x) - u_{\theta}(x)|$", color="red") + axes[2].set_title(r"Absolute difference $|u(x) - u_{\theta}(x)|$") + axes[2].legend(loc="upper right") + # Adjust layout + plt.tight_layout() + # Show the plots + plt.show() + + +# It is pretty clear that the network is periodic, with also the error following a periodic pattern. Obviously a longer training and a more expressive neural network could improve the results! +# +# ## What's next? +# +# Congratulations on completing the one dimensional Helmholtz tutorial of **PINA**! There are multiple directions you can go now: +# +# 1. Train the network for longer or with different layer sizes and assert the finaly accuracy +# +# 2. Apply the `PeriodicBoundaryEmbedding` layer for a time-dependent problem (see reference in the documentation) +# +# 3. Exploit extrafeature training ? +# +# 4. Many more...