diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index 77cbe2a8..00000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,35 +0,0 @@ -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Unit tests - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - test: - name: ${{ matrix.os }} / Python ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, macos-latest ] - python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Upgrade pip - run: | - python -m pip install --upgrade pip - python -m pip uninstall -y sympde - - name: Install SymPDE - run: | - python -m pip install . - - name: Test with pytest - run: | - cd - pytest -n auto --dist loadgroup --pyargs sympde -ra diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..41fd797b --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,81 @@ +name: Documentation + +on: + push: + branches: [ master ] + paths: + - 'README.rst' + - 'doc/**' + - 'sympde/**.py' + - 'pyproject.toml' + - '.github/workflows/documentation.yml' + + pull_request: + branches: [ master ] + types: + - opened + - reopened + - synchronize + - ready_for_review + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build_docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: 'pip' + cache-dependency-path: | + pyproject.toml + + - name: Upgrade pip, setuptools, and wheel + run: | + pip install --upgrade pip setuptools wheel + + - name: Install project and documentation dependencies + run: | + pip install .[docs] + pip freeze + + - name: Run documentation examples + run: | + for example in doc/examples/*.py; do + python "$example" + done + + - name: Make the Sphinx documentation + run: >- + python -m sphinx -W --keep-going + -b html doc doc/_build/html + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'doc/_build/html' + + deploy_docs: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: build_docs + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..6a3305b3 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,129 @@ +# This workflow installs SymPDE and runs its tests with supported Python versions. +# It follows the testing layout used by Psydac while omitting Psydac-specific +# MPI, HDF5, PETSc, and example jobs. + +name: Unit tests + +on: + push: + branches: [ master ] + paths: + - 'sympde/**' + - 'pyproject.toml' + - '.github/workflows/testing.yml' + + pull_request: + branches: [ master ] + types: + - opened + - reopened + - synchronize + - ready_for_review + + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04, macos-14 ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + isMerge: + - ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + exclude: + - { isMerge: false, python-version: '3.9', os: macos-14 } + - { isMerge: false, python-version: '3.10', os: macos-14 } + - { isMerge: false, python-version: '3.11', os: ubuntu-24.04 } + - { isMerge: false, python-version: '3.12', os: macos-14 } + - { isMerge: false, python-version: '3.13', os: ubuntu-24.04 } + - { isMerge: false, python-version: '3.14', os: macos-14 } + + name: ${{ matrix.os }} / Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + pyproject.toml + + - name: Upgrade pip, setuptools, and wheel + run: | + pip install --upgrade pip setuptools wheel + + - name: Install project + run: | + pip install .[test] + pip freeze + + - name: Initialize test directory + run: | + mkdir scratch + + - name: Run coverage tests on macOS + if: matrix.os == 'macos-14' + working-directory: ./scratch + run: >- + pytest -n auto --dist loadgroup + --cov sympde --cov-report term-missing --cov-report xml + --pyargs sympde -ra + + - name: Run tests on Ubuntu + if: matrix.os == 'ubuntu-24.04' + working-directory: ./scratch + run: >- + pytest -n auto --dist loadgroup + --pyargs sympde -ra + + - name: Upload coverage report + if: matrix.os == 'macos-14' + uses: actions/upload-artifact@v4 + with: + name: coverage-report-python-${{ matrix.python-version }} + path: scratch/coverage.xml + + - name: Print detailed coverage results on macOS + if: matrix.os == 'macos-14' + working-directory: ./scratch + run: | + coverage report --ignore-errors --show-missing --sort=cover + + minimum-sympy: + name: Ubuntu / Python 3.11 / SymPy 1.12 + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: | + pyproject.toml + + - name: Upgrade pip, setuptools, and wheel + run: | + pip install --upgrade pip setuptools wheel + + - name: Install project with minimum SymPy + run: | + pip install .[test] 'sympy == 1.12' + pip freeze + + - name: Initialize test directory + run: | + mkdir scratch + + - name: Run tests + working-directory: ./scratch + run: >- + pytest -n auto --dist loadgroup + --pyargs sympde -ra diff --git a/README.rst b/README.rst index 1d4f0496..24205b65 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ SymPDE ====== -|CI status| |binder| |docs| +|CI status| |docs| **SymPDE** is a symbolic calculus library for partial differential equations and variational forms. It can be used to have similar capabilities as the fenics_ project, by extending and writing your own *printing* functions. @@ -66,7 +66,30 @@ To check out a specific branch/tag/commit named ````, just use ``git checko In order to make changes to the library, and see these changes when the package is imported, SymPDE should be installed in **editable** mode:: - python3 -m pip install --editable . + python3 -m pip install --editable ".[test]" + +Public API +^^^^^^^^^^ + +User-facing objects can be imported from the flat, explicit API without +knowing which internal module defines them:: + + from sympde.api import Cube, Mapping, Constant + from sympde.api import ScalarFunctionSpace, VectorFunctionSpace, elements_of + from sympde.api import BilinearForm, LinearForm, Norm + +Running the tests +^^^^^^^^^^^^^^^^^ + +The complete test suite can be run from any directory with:: + + python3 -m pytest -n auto --dist loadgroup --pyargs sympde -ra + +The documentation dependencies are installed separately and the HTML pages +are built with warnings treated as errors:: + + python3 -m pip install --editable ".[docs]" + python3 -m sphinx -W --keep-going -b html doc doc/_build/html For developers @@ -74,7 +97,7 @@ For developers Because many important features of SymPDE are only tested in Psydac, new PRs should also be tested against the test suite of Psydac. This can be done by opening a PR in Psydac, where the only change consists of installing the corresponding branch of SymPDE. -To achieve this, one just needs to modify the line corresponding to ``sympde`` in the ``pyproject.yaml`` file. +To achieve this, one just needs to modify the line corresponding to ``sympde`` in the ``pyproject.toml`` file. For instance, to test a new SymPDE branch called ``my_feature``, one should write @@ -94,14 +117,10 @@ Do not forget the comma at the end of the line, as this is an item in a list. Also, pay attention to the words ``head`` and ``tags`` in the path: the former is used for Git branches, the latter is used for Git tags (which may or may not correspond to GitHub releases). -.. |CI status| image:: https://github.com/pyccel/sympde/actions/workflows/continuous-integration.yml/badge.svg?branch=master&event=push +.. |CI status| image:: https://github.com/pyccel/sympde/actions/workflows/testing.yml/badge.svg?branch=master&event=push :alt: CI status - :target: https://github.com/pyccel/sympde/actions/workflows/continuous-integration.yml + :target: https://github.com/pyccel/sympde/actions/workflows/testing.yml -.. |docs| image:: https://readthedocs.org/projects/sympde/badge/?version=latest +.. |docs| image:: https://github.com/pyccel/sympde/actions/workflows/documentation.yml/badge.svg :alt: Documentation Status - :target: http://sympde.readthedocs.io/en/latest/?badge=latest - -.. |binder| image:: https://mybinder.org/badge_logo.svg - :alt: Run notebooks in Binder - :target: https://mybinder.org/v2/gh/pyccel/sympde/master + :target: https://github.com/pyccel/sympde/actions/workflows/documentation.yml diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 1f4b09f4..00000000 --- a/TODO.rst +++ /dev/null @@ -1,27 +0,0 @@ -- add BasicDerivable class for all sympde objects that can be differentiate with respect to the coordinates - -- add a coordinates system - -- remove Unknown and VectorUnknown - -Linearity -********* - -- must check that BilinearForm and LinearForm are really linear, by calling them and then applying atomize - -- this verification must be done at the __new__ as long as the flag check_linearity is True. The later is an argument of the __new__ method for BilinearForm and LinearForm - - -Weak formulation -**************** - -- add the concept of weak formulation - -- a weak formulation does not need to be linear/bilinear. - -- add linearize function that returns either LinearForm or BilinearForm - -Kron -**** - -the kronecker features must be upgraded as well as the tensorize function diff --git a/bin/run_tests.sh b/bin/run_tests.sh deleted file mode 100755 index da2ed3e0..00000000 --- a/bin/run_tests.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -python3 -m pytest sympde/core/tests/test_geometry.py -python3 -m pytest sympde/core/tests/test_derivatives.py -python3 -m pytest sympde/core/tests/test_space.py -python3 -m pytest sympde/core/tests/test_mapping.py -python3 -m pytest sympde/core/tests/test_model_1d.py -python3 -m pytest sympde/core/tests/test_model_2d.py - -#python3 -m pytest sympde/core/tests/test_expr_1d.py -python3 -m pytest sympde/core/tests/test_expr_2d.py -#python3 -m pytest sympde/core/tests/test_expr_3d.py - -python3 -m pytest sympde/printing/tests/test_latex.py - -python3 -m pytest sympde/gallery/tests/test_poisson.py -python3 -m pytest sympde/gallery/tests/test_stokes.py -python3 -m pytest sympde/gallery/tests/test_wave.py diff --git a/doc/advanced.rst b/doc/advanced.rst index 1f4c96a5..7f4c086d 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -1,39 +1,34 @@ Advanced expression manipulation ******************************** -In this section, we shall present different different features for manipulating expressions such us finding the Kronecker representation for bilinear forms. +Tensor-product representation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Tensorization -^^^^^^^^^^^^^ +``TensorExpr`` lowers an integrated form and expresses it through one- +dimensional symbolic factors. This is useful to inspect the Kronecker +structure consumed by tensor-product discretizations. -Assume we have the following bilinear form for Laplace +.. literalinclude:: examples/tensorization.py + :language: python -.. code-block:: python +The example is an executable documentation source and is run by the +documentation workflow before Sphinx builds the page. - from sympde import grad, dot - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - from sympde.core import tensorize +Logical expressions +^^^^^^^^^^^^^^^^^^^ - V = FunctionSpace('V', ldim=2) - U = FunctionSpace('U', ldim=2) +``LogicalExpr`` pulls expressions on a mapped physical domain back to its +logical domain. The analytical-mapping example demonstrates symbolic and +numerical use of this transformation: - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') +.. literalinclude:: examples/analytical_mappings.py + :language: python - a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) - print(tensorize(a)) +Nonlinear forms +^^^^^^^^^^^^^^^ -the result is then +``linearize`` differentiates a nonlinear residual with respect to a field and +returns its symbolic Jacobian form: -.. code-block:: python - - >>> Mass(v1,u1)xMass(v0,u0) + Mass(v1,u1)xStiffness(v0,u0) + Stiffness(v1,u1)xMass(v0,u0) - -which is what we expect, when splitting the integrals with respect to the two directions. -Printing the corresponding latex code gives - -.. math:: - - {\int_{0}^{1} u_{1} v_{1} dy}\otimes {\int_{0}^{1} u_{0} v_{0} dx} + {\int_{0}^{1} u_{1} v_{1} dy}\otimes {\int_{0}^{1} u_{0}^\prime v_{0}^\prime dx} + {\int_{0}^{1} u_{1}^\prime v_{1}^\prime dy}\otimes {\int_{0}^{1} u_{0} v_{0} dx} +.. literalinclude:: examples/nonlinear_poisson.py + :language: python diff --git a/doc/api.rst b/doc/api.rst new file mode 100644 index 00000000..8352f4a0 --- /dev/null +++ b/doc/api.rst @@ -0,0 +1,19 @@ +Public API +========== + +The :mod:`sympde.api` package provides the supported flat namespace for +user-facing objects. For example: + +.. code-block:: python + + from sympde.api import Cube, Mapping, Constant + from sympde.api import ScalarFunctionSpace, elements_of + from sympde.api import BilinearForm, LinearForm, Norm + +The complete namespace is listed below. These objects are also documented and +indexed at their defining modules, so this page does not create duplicate +cross-reference targets. + +.. automodule:: sympde.api + :members: + :no-index: diff --git a/doc/conf.py b/doc/conf.py index e8531e46..6e82ec7a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config +"""Sphinx configuration for the SymPDE documentation.""" + +from pathlib import Path + +from sphinx.ext.apidoc import main as sphinx_apidoc + +from sympde.version import __version__ # -- Path setup -------------------------------------------------------------- @@ -19,21 +19,21 @@ # -- Project information ----------------------------------------------------- -project = 'sympde' -copyright = '2018, A. Ratnani, S. Hadjout' -author = 'A. Ratnani, S. Hadjout' +project = 'SymPDE' +copyright = '2018-2026, SymPDE developers' +author = 'SymPDE developers' # The short X.Y version -version = '' +version = __version__.split('-dev', maxsplit=1)[0] # The full version, including alpha/beta/rc tags -release = '' +release = version # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # -# needs_sphinx = '1.0' +needs_sphinx = '7.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -42,7 +42,7 @@ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'sphinx.ext.imgmath', + 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinxcontrib.bibtex', ] @@ -54,7 +54,7 @@ # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = {'.rst': 'restructuredtext'} # The master toctree document. master_doc = 'index' @@ -64,7 +64,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -143,26 +143,13 @@ latex_additional_files = ['latex_macros.sty'] latex_elements = { - 'printmodindex': '', 'printindex': '', 'preamble' : r'\usepackage{amsmath} \usepackage{amssymb} \usepackage{latex_macros}', - 'docclass':'report', } ##################################################### # add LaTeX macros -f = open('latex_macros.sty', 'r') - -try: - imgmath_latex_preamble # check whether this is already defined -except NameError: - imgmath_latex_preamble = "" - -for macro in f: - # used when building html version - imgmath_latex_preamble += macro + '\n' - # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples @@ -192,11 +179,22 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True +bibtex_bibfiles = ['math/refs_feec.bib'] -# -- APIDOC ---------------------------------------------- -import subprocess -cmd = 'rm -rf source; sphinx-apidoc --force --maxdepth=3 -o source/ ../sympde' -subprocess.call(cmd, shell=True) -# create _static directory -subprocess.call('mkdir -p _static', shell=True) +# -- APIDOC ---------------------------------------------- +doc_dir = Path(__file__).resolve().parent +source_dir = doc_dir / 'source' +static_dir = doc_dir / '_static' +source_dir.mkdir(exist_ok=True) +static_dir.mkdir(exist_ok=True) +apidoc_args = [ + '--force', + '--remove-old', + '--maxdepth', '3', + '--output-dir', str(source_dir), + str(doc_dir.parent / 'sympde'), +] +apidoc_args.extend(str(path) for path in (doc_dir.parent / 'sympde').glob('*/tests')) +apidoc_args.append(str(doc_dir.parent / 'sympde' / 'api')) +sphinx_apidoc(apidoc_args) diff --git a/doc/examples/analytical_mappings.py b/doc/examples/analytical_mappings.py new file mode 100644 index 00000000..6a83e206 --- /dev/null +++ b/doc/examples/analytical_mappings.py @@ -0,0 +1,43 @@ +"""Work with built-in and user-defined analytical mappings.""" + +import numpy as np +from sympy import pi, sin, symbols + +from sympde.api import CollelaMapping2D, Constant, Domain, LogicalExpr, Mapping + + +# Inspect a built-in mapping symbolically. +mapping = CollelaMapping2D('M', dim=2) +mapped_domain = mapping(Domain('Omega', dim=2)) +x1, x2 = symbols('x1, x2', real=True) +eps, k1, k2 = [Constant(name) for name in ('eps', 'k1', 'k2')] + +expected_x = 2.0 * ( + x1 + eps * sin(2.0 * pi * k1 * x1) * sin(2.0 * pi * k2 * x2) +) - 1.0 +assert LogicalExpr(mapping[0], mapped_domain) == expected_x + + +# Fix its parameters and evaluate it numerically. +numerical_mapping = CollelaMapping2D( + 'M_numeric', + dim=2, + eps=0.1, + k1=1.0, + k2=1.0, +).get_callable_mapping() +np.testing.assert_allclose(numerical_mapping(0.25, 0.5), (-0.5, 0.0)) + + +# Define a custom mapping by specifying its coordinate expressions. +class ComplexSquareMapping(Mapping): + """Map ``(x1, x2)`` through the square of a complex number.""" + + _expressions = { + 'x': 'A * (x1**2 - x2**2)', + 'y': '2 * A * x1 * x2', + } + + +custom_mapping = ComplexSquareMapping('F', dim=2, A=1.0).get_callable_mapping() +np.testing.assert_allclose(custom_mapping(0.5, 0.25), (0.1875, 0.25)) diff --git a/doc/examples/differential_calculus.py b/doc/examples/differential_calculus.py new file mode 100644 index 00000000..a7e54cd9 --- /dev/null +++ b/doc/examples/differential_calculus.py @@ -0,0 +1,40 @@ +"""Verify representative symbolic vector-calculus identities.""" + +from sympy import expand + +from sympde.api import Constant, Domain, ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import curl, div, elements_of, grad, laplace + + +domain = Domain('Omega', dim=3) +scalar_space = ScalarFunctionSpace('V', domain) +vector_space = VectorFunctionSpace('W', domain) + +alpha, beta = [Constant(name) for name in ('alpha', 'beta')] +f, g, h = elements_of(scalar_space, names='f, g, h') +field_f, field_g, field_h = elements_of(vector_space, names='F, G, H') + +# Gradient linearity and product rules. +assert grad(f + g) == grad(f) + grad(g) +assert grad(alpha * f + beta * g) == alpha * grad(f) + beta * grad(g) +assert grad(f * g) == f * grad(g) + g * grad(f) +assert grad(f / g) == -f * grad(g) / g**2 + grad(f) / g +assert expand(grad(f * g * h)) == ( + f * g * grad(h) + f * h * grad(g) + g * h * grad(f) +) + +# Linearity of curl, Laplace, and divergence operators. +assert curl(field_f + field_g) == curl(field_f) + curl(field_g) +assert curl(alpha * field_f + beta * field_g) == ( + alpha * curl(field_f) + beta * curl(field_g) +) +assert laplace(f + g) == laplace(f) + laplace(g) +assert laplace(alpha * f + beta * g) == alpha * laplace(f) + beta * laplace(g) +assert div(field_f + field_g) == div(field_f) + div(field_g) +assert div(alpha * field_f + beta * field_g) == ( + alpha * div(field_f) + beta * div(field_g) +) + +# Exact-sequence identities. +assert curl(grad(h)) == 0 +assert div(curl(field_h)) == 0 diff --git a/doc/examples/exterior_calculus.py b/doc/examples/exterior_calculus.py new file mode 100644 index 00000000..8e2e1579 --- /dev/null +++ b/doc/examples/exterior_calculus.py @@ -0,0 +1,48 @@ +"""Apply exterior-calculus operators to symbolic differential forms.""" + +from sympy import Symbol + +from sympde.api import Constant, DifferentialForm, Domain, VectorFunctionSpace +from sympde.api import d, delta, element_of, hodge, ip, jp, latex, ld, wedge + + +dimension = Symbol('n') +alpha = Constant('alpha') + +u_0, v_0 = [ + DifferentialForm(name, index=0, dim=dimension) + for name in ('u_0', 'v_0') +] +u_1, v_1 = [ + DifferentialForm(name, index=1, dim=dimension) + for name in ('u_1', 'v_1') +] +u_2 = DifferentialForm('u_2', index=2, dim=dimension) +u_n = DifferentialForm('u_n', index=dimension, dim=dimension) + +# The exterior derivative is linear, nilpotent, and zero on top forms. +assert d(d(u_0)) == 0 +assert d(u_0 + v_0) == d(u_0) + d(v_0) +assert d(alpha * u_0 + v_0) == alpha * d(u_0) + d(v_0) +assert d(u_n) == 0 + +# Its adjoint is linear and zero on zero-forms. +assert delta(u_0) == 0 +assert delta(u_1 + v_1) == delta(u_1) + delta(v_1) +assert delta(alpha * u_1 + v_1) == alpha * delta(u_1) + delta(v_1) + +domain = Domain('Omega', dim=3) +vector_space = VectorFunctionSpace('W', domain) +beta = element_of(vector_space, name='beta') + +expressions = ( + d(u_1) + u_2, + wedge(d(u_1), u_2), + hodge(u_1), + ip(beta, u_1), + jp(beta, u_1), + ld(beta, u_1), +) + +for expression in expressions: + print(latex(expression)) diff --git a/doc/examples/index.rst b/doc/examples/index.rst new file mode 100644 index 00000000..cf8999c4 --- /dev/null +++ b/doc/examples/index.rst @@ -0,0 +1,56 @@ +Examples +======== + +These examples are executable Python scripts. The documentation workflow runs +all of them before building the HTML pages, ensuring that their imports and +demonstrated APIs remain current. + +Variational forms +----------------- + +This example constructs a scalar Poisson problem and a Nitsche-type form on a +two-patch domain. + +.. literalinclude:: variational_forms.py + :language: python + :linenos: + +Differential calculus +--------------------- + +This example demonstrates the currently supported symbolic vector-calculus +identities. + +.. literalinclude:: differential_calculus.py + :language: python + :linenos: + +Analytical mappings +------------------- + +This example evaluates a built-in analytical mapping and defines a custom +mapping from coordinate expressions. + +.. literalinclude:: analytical_mappings.py + :language: python + :linenos: + +Exterior calculus +----------------- + +This example demonstrates differential forms, exterior derivatives, adjoint +operators, products, contractions, and Lie derivatives. + +.. literalinclude:: exterior_calculus.py + :language: python + :linenos: + +Nonlinear linearization +----------------------- + +This example constructs a nonlinear Poisson residual and derives its Jacobian +form symbolically. + +.. literalinclude:: nonlinear_poisson.py + :language: python + :linenos: diff --git a/doc/examples/nonlinear_poisson.py b/doc/examples/nonlinear_poisson.py new file mode 100644 index 00000000..7a6575bc --- /dev/null +++ b/doc/examples/nonlinear_poisson.py @@ -0,0 +1,30 @@ +"""Linearize the residual of a nonlinear Poisson problem.""" + +from sympy import exp + +from sympde.api import BilinearForm, Domain, LinearForm, ScalarFunctionSpace +from sympde.api import dot, elements_of, grad, integral, linearize + + +domain = Domain('Omega', dim=2) +space = ScalarFunctionSpace('V', domain) +field, increment, test = elements_of(space, names='u, delta_u, v') + + +def integrate(expression): + """Integrate an expression over the problem domain.""" + + return integral(domain, expression) + + +residual = LinearForm( + test, + integrate(dot(grad(test), grad(field)) - 4.0 * exp(-field) * test), +) + +jacobian = linearize(residual, field, trials=increment) +assert isinstance(jacobian, BilinearForm) +assert jacobian(increment, test) == integrate( + dot(grad(test), grad(increment)) + + 4.0 * exp(-field) * increment * test +) diff --git a/doc/examples/quickstart.py b/doc/examples/quickstart.py new file mode 100644 index 00000000..528ac64f --- /dev/null +++ b/doc/examples/quickstart.py @@ -0,0 +1,50 @@ +"""Build a scalar Poisson problem with the current public API.""" + +from sympy import cos + +from sympde.api import BilinearForm, LinearForm, ScalarFunctionSpace, Square +from sympde.api import TerminalExpr, div, dot, elements_of, find, grad +from sympde.api import integral, latex + + +# [domains-start] +domain = Square('Omega') +space = ScalarFunctionSpace('V', domain) +trial, test = elements_of(space, names='u, v') + +strong_expression = -div(grad(trial)) + trial +assert strong_expression.has(trial) +# [domains-end] + + +# [forms-start] +bilinear_form = BilinearForm( + (trial, test), + integral(domain, dot(grad(trial), grad(test)) + trial * test), +) + +x, y = domain.coordinates +linear_form = LinearForm(test, integral(domain, cos(x + y) * test)) + +equation = find( + trial, + forall=test, + lhs=bilinear_form(trial, test), + rhs=linear_form(test), +) +# [forms-end] + + +# [lowering-start] +coordinate_expression = TerminalExpr( + dot(grad(trial), grad(test)), + domain, +) +assert coordinate_expression.has(trial, test) +# [lowering-end] + + +# [printing-start] +latex_equation = latex(equation) +print(latex_equation) +# [printing-end] diff --git a/doc/examples/tensorization.py b/doc/examples/tensorization.py new file mode 100644 index 00000000..e7ee963b --- /dev/null +++ b/doc/examples/tensorization.py @@ -0,0 +1,21 @@ +"""Expose the tensor-product structure of a scalar bilinear form.""" + +from sympde.api import BilinearForm, Constant, Domain, ScalarFunctionSpace +from sympde.api import TensorExpr, dot, elements_of, grad, integral + + +domain = Domain('Omega', dim=2) +space = ScalarFunctionSpace('V', domain) +trial, test = elements_of(space, names='u, v') +coefficient = Constant('mu', real=True) + +form = BilinearForm( + (trial, test), + integral( + domain, + coefficient * trial * test + dot(grad(trial), grad(test)), + ), +) + +tensor_expression = TensorExpr(form, domain=domain) +print(tensor_expression) diff --git a/doc/examples/variational_forms.py b/doc/examples/variational_forms.py new file mode 100644 index 00000000..b2f2b938 --- /dev/null +++ b/doc/examples/variational_forms.py @@ -0,0 +1,59 @@ +"""Construct scalar variational forms on single- and multi-patch domains.""" + +from sympy import cos + +from sympde.api import BilinearForm, Constant, Domain, LinearForm, ScalarFunctionSpace +from sympde.api import Square, Dn, dot, elements_of, find, grad, integral +from sympde.api import jump, latex, minus, plus + + +# A Poisson problem on a single square. +domain = Square('Omega') +space = ScalarFunctionSpace('V', domain) +trial, test = elements_of(space, names='u, v') + +bilinear_form = BilinearForm( + (trial, test), + integral(domain, dot(grad(trial), grad(test))), +) + +x, y = domain.coordinates +linear_form = LinearForm(test, integral(domain, cos(x + y) * test)) +equation = find( + trial, + forall=test, + lhs=bilinear_form(trial, test), + rhs=linear_form(test), +) + +print(latex(equation)) + + +# A Nitsche-type interface contribution on two joined patches. +patch_a = Square('A') +patch_b = Square('B') +domain = Domain.join( + [patch_a, patch_b], + [((0, 0, 1), (1, 0, -1), 1)], + 'Omega', +) + +space = ScalarFunctionSpace('V', domain) +trial, test = elements_of(space, names='u, v') +interface = domain.interfaces +kappa = Constant('kappa') + +interface_integrand = ( + -jump(trial) * jump(Dn(test)) + + kappa * jump(trial) * jump(test) + + plus(Dn(trial)) * minus(test) + + minus(Dn(trial)) * plus(test) +) + +multi_patch_form = BilinearForm( + (trial, test), + integral(domain, dot(grad(trial), grad(test))) + + integral(interface, interface_integrand), +) + +print(latex(multi_patch_form)) diff --git a/doc/include/graph.py b/doc/include/graph.py deleted file mode 100644 index 84e1a5c9..00000000 --- a/doc/include/graph.py +++ /dev/null @@ -1,234 +0,0 @@ -# coding: utf-8 - -import os - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin, exp -from sympy import srepr -from sympy.physics.quantum import TensorProduct -from sympy.printing.dot import dotprint - -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, cross, rot, curl, div -from sympde.calculus import laplace, hessian, bracket, convect -from sympde.topology import (dx, dy, dz) -from sympde.topology import FunctionSpace, VectorFunctionSpace -from sympde.topology import Field, VectorField -from sympde.topology import ProductSpace -from sympde.topology import TestFunction -from sympde.topology import VectorTestFunction -from sympde.topology import Unknown -from sympde.topology import InteriorDomain, Union -from sympde.topology import Boundary, NormalVector, TangentVector -from sympde.topology import Domain -from sympde.topology import Trace, trace_0, trace_1 -from sympde.topology import Mapping -from sympde.topology import Square -from sympde.topology import ElementDomain -from sympde.topology import Area - -from sympde.expr.expr import LinearExpr, BilinearExpr -from sympde.expr.expr import LinearForm, BilinearForm -from sympde.expr.expr import DomainIntegral, BoundaryIntegral -from sympde.expr.expr import Functional, Norm -from sympde.expr.expr import linearize -from sympde.expr.evaluation import TerminalExpr - - -#============================================================================== -def mkdir_p(dir): - if os.path.isdir(dir): - return - os.makedirs(dir) - - -#============================================================================== -def export(expr, fname): - """saves the graph as a png/svg file. the extension is eitheir png or svg""" - - name, ext = os.path.splitext(fname) - ext = ext[1:] - - graph = dotprint(expr) - f = open('{name}.dot'.format(name=name), 'w') - f.write(graph) - f.close() - - cmd = "dot -T{ext} {name}.dot -o{name}.{ext}".format(name=name, ext=ext) - os.system(cmd) - -#============================================================================== -def test_linear_form_2d_1(): - - domain = Domain('Omega', dim=2) - B1 = Boundary(r'\Gamma_1', domain) - - x,y = domain.coordinates - - kappa = Constant('kappa', is_real=True) - mu = Constant('mu' , is_real=True) - eps = Constant('eps', real=True) - - V = FunctionSpace('V', domain) - - u,u1,u2 = [TestFunction(V, name=i) for i in ['u', 'u1', 'u2']] - v,v1,v2 = [TestFunction(V, name=i) for i in ['v', 'v1', 'v2']] - - g = Tuple(x**2, y**2) - - # ... - d_forms = {} - - d_forms['l1'] = LinearForm(v, x*y*v) - d_forms['l2'] = LinearForm(v, v*trace_1(g, B1)) - d_forms['l3'] = LinearForm(v, v*trace_1(g, B1) + x*y*v) - - # nitsche - f = Function('f') - g = Function('g') - - l0 = LinearForm(v, f(x,y)*v) - l_B1 = LinearForm(v, g(x,y)*trace_0(v, B1)/eps - - kappa * g(x,y) * trace_1(grad(v), B1)) - - d_forms['l4'] = LinearForm(v, l0(v) + l_B1(v)) - # ... - - # ... - for name, expr in d_forms.items(): - export(expr, 'linform_2d_{}.png'.format(name)) - # ... - -#============================================================================== -def test_bilinear_form_2d_1(): - - domain = Domain('Omega', dim=2) - B1 = Boundary(r'\Gamma_1', domain) - - x,y = domain.coordinates - - kappa = Constant('kappa', is_real=True) - mu = Constant('mu' , is_real=True) - eps = Constant('eps', real=True) - - V = FunctionSpace('V', domain) - - u,u1,u2 = [TestFunction(V, name=i) for i in ['u', 'u1', 'u2']] - v,v1,v2 = [TestFunction(V, name=i) for i in ['v', 'v1', 'v2']] - - # ... - d_forms = {} - - d_forms['a1'] = BilinearForm((u,v), u*v) - d_forms['a2'] = BilinearForm((u,v), u*v + dot(grad(u), grad(v))) - d_forms['a3'] = BilinearForm((u,v), v*trace_1(grad(u), B1)) - - - # Poisson with Nitsch method - a0 = BilinearForm((u,v), dot(grad(u),grad(v))) - a_B1 = BilinearForm((u,v), - kappa * u*trace_1(grad(v), B1) - - v*trace_1(grad(u), B1) - + trace_0(u, B1) * trace_0(v, B1) / eps) - a = BilinearForm((u,v), a0(u,v) + a_B1(u,v)) - - d_forms['a4'] = a - # ... - - # ... calls - d_calls = {} - for name, a in d_forms.items(): - d_calls[name] = a(u1,v1) - # ... - - # ... export forms - for name, expr in d_forms.items(): - export(expr, 'biform_2d_{}.png'.format(name)) - # ... - - # ... export calls - for name, expr in d_calls.items(): - export(expr, 'biform_2d_call_{}.png'.format(name)) - # ... - - -#============================================================================== -# stokes -def test_bilinear_form_2d_3(): - - domain = Domain('Omega', dim=2) - - x,y = domain.coordinates - - V = VectorFunctionSpace('V', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(V, name='v') - u = VectorTestFunction(V, name='u') - p = TestFunction(W, name='p') - q = TestFunction(W, name='q') - - a = BilinearForm((u,v), inner(grad(v), grad(u))) - b = BilinearForm((v,p), div(v)*p) - A = BilinearForm(((u,p),(v,q)), a(v,u) - b(v,p) + b(u,q)) - - export(A, 'stokes_2d.png') - -#============================================================================== -def test_linearize_form_2d_3(): - """steady Euler equation.""" - domain = Domain('Omega', dim=2) - x,y = domain.coordinates - - U = VectorFunctionSpace('U', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(U, name='v') - phi = TestFunction(W, name='phi') - q = TestFunction(W, name='q') - - U_0 = VectorField(U, name='U_0') - Rho_0 = Field(W, name='Rho_0') - P_0 = Field(W, name='P_0') - - # ... - expr = div(Rho_0*U_0) * phi - l1 = LinearForm(phi, expr) - - expr = Rho_0*dot(convect(U_0, grad(U_0)), v) + dot(grad(P_0), v) - l2 = LinearForm(v, expr) - - expr = dot(U_0, grad(P_0)) * q + P_0 * div(U_0) * q - l3 = LinearForm(q, expr) - # ... - - a1 = linearize(l1, [Rho_0, U_0], trials=['d_rho', 'd_u']) - print(a1) - print('') - - a2 = linearize(l2, [Rho_0, U_0, P_0], trials=['d_rho', 'd_u', 'd_p']) - print(a2) - print('') - - a3 = linearize(l3, [P_0, U_0], trials=['d_p', 'd_u']) - print(a3) - print('') - - l = LinearForm((phi, v, q), l1(phi) + l2(v) + l3(q)) - a = linearize(l, [Rho_0, U_0, P_0], trials=['d_rho', 'd_u', 'd_p']) - print(a) - - export(a, 'steady_euler.png') - -############################################ -if __name__ == '__main__': - -# test_linear_form_2d_1() -# test_bilinear_form_2d_1() -# test_bilinear_form_2d_3() - - test_linearize_form_2d_3() diff --git a/doc/index.rst b/doc/index.rst index 4f56451d..6f19fd85 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,6 +12,7 @@ Welcome to sympde's documentation! quick advanced + examples/index Mathematical background ======================= @@ -28,6 +29,7 @@ API .. toctree:: :maxdepth: 1 + api source/modules Indices and tables diff --git a/doc/quick.rst b/doc/quick.rst index 3b288657..efb34650 100644 --- a/doc/quick.rst +++ b/doc/quick.rst @@ -1,422 +1,70 @@ Quick start *********** -**sympde** allows you to create symbolic expressions using differential/algebraic operators. It can be used to define strong or weak forms, when dealing with finite elements methods. +SymPDE builds symbolic differential operators, variational forms, and PDE +problems on explicitly declared domains and function spaces. User-facing +objects are available from :mod:`sympde.api`. -Strong forms -^^^^^^^^^^^^ +Domains and fields +^^^^^^^^^^^^^^^^^^ -Assume you want to create the symbolic expression that is equivalent to +A field belongs to a scalar or vector function space. The following complete +example creates two scalar fields on a square and forms a differential +expression from them: -.. math:: +.. literalinclude:: examples/quickstart.py + :language: python + :start-after: # [domains-start] + :end-before: # [domains-end] - \mathcal{L} u := - \nabla^2 u + u +Differential operators remain symbolic, while linearity and product rules are +applied automatically. SymPDE also supports ``curl``, ``laplace``, normal +derivatives, traces, and interface operators. -where :math:`u` denotes the *unknown* of 2 variables (2D). +Variational forms +^^^^^^^^^^^^^^^^^ -In **sympde**, you can declare :math:`u` using +Integrals identify the domain on which an expression is evaluated. A +``BilinearForm`` declares its trial and test fields, and a ``LinearForm`` +declares its test field: -.. code-block:: python +.. literalinclude:: examples/quickstart.py + :language: python + :start-after: # [forms-start] + :end-before: # [forms-end] - from sympde import Unknown +The :func:`sympde.api.find` helper combines those forms into a symbolic +equation. Boundary conditions can be supplied with ``EssentialBC`` through +the optional ``bc`` argument. - u = Unknown('u', ldim=2) +Expression lowering +^^^^^^^^^^^^^^^^^^^ -The expression associated to the operator :math:`\mathcal{L}` is then defined as +Generic vector-calculus operators can be lowered to coordinate derivatives +for a specific domain with ``TerminalExpr``: -.. code-block:: python +.. literalinclude:: examples/quickstart.py + :language: python + :start-after: # [lowering-start] + :end-before: # [lowering-end] - from sympde import grad, div +Mappings and multipatch domains +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - expr = - div(grad(u)) + u - -you can also use a *lambda expression* - -.. code-block:: python - - from sympde import grad, div - - L = lambda u: - div(grad(u)) + u - -As any sympy expression, you can take a look at the diagram of the expression tree: - -.. figure:: include/quick/graph1.png - :scale: 70 % - :alt: map to buried treasure - - Expression tree for the :math:`\mathcal{L} u := - \nabla^2 u + u` operator. - -.. note:: - - The above diagram was made using `Graphviz `_ and - the `dotprint `_ function. - -**sympde** allows to write more complicated expressions and it knows many differential calculus rules to perform the computation on its own. - -Let us consider the following mathematical expression - -.. math:: - - \partial_x \partial_y uv - -The symbolic associated expression can be achieved using - -.. code-block:: python - - from sympde import Unknown - from sympde import dx, dy - - u = Unknown('u', ldim=2) - v = Unknown('v', ldim=2) - - expr = dx(dy(u*v)) - -Since we are using the atomic operators **dx** and **dy**, the expression will be automatically evaluated, in opposition to the use of **grad** or **div**. Hence, result is - -.. code-block:: python - - >>> u*dx(dy(v)) + v*dx(dy(u)) + dx(u)*dy(v) + dx(v)*dy(u) - -and the associated expression tree is - -.. figure:: include/quick/graph2.png - :scale: 70 % - :alt: map to buried treasure - - Expression tree for :math:`\partial_x \partial_y uv`. - -When evaluated, **sympde** differential operators are linear operators. Which means that the following code: - -.. code-block:: python - - from sympde import Unknown - from sympde import dx, dy - - u = Unknown('u', ldim=2) - v = Unknown('v', ldim=2) - - expr = dy(2*u+3*v) - -will return: - -.. code-block:: python - - >>> 2*dy(u) + 3*dy(v) - -**sympde** introduces the notion of a *constant* through the class **Constant**. As expected, applying a differential operator on it will return **0**: - -.. code-block:: python - - from sympde import Unknown, Constant - from sympde import dx - - u = Unknown('u', ldim=1) - alpha = Constant('alpha') - - expr = dx(alpha*u) + dx(dx(2*u)) - -The result is: - -.. code-block:: python - - >>> alpha*dx(u) + 2*dx(dx(u)) - -You can also apply a differential operator on an analytical function, which is useful to compute solution/rhs of a partial differential equation: - -.. code-block:: python - - from sympde import Constant - from sympde import dx, dy - from sympy.abc import x, y - from sympy import cos, exp - - alpha = Constant('alpha') - - L = lambda u: -dx(dx(u)) - dy(dy(u)) + alpha * u - - expr = L(cos(y)*exp(-x**2)) - -The result is: - -.. code-block:: python - - >>> alpha*exp(-x**2)*cos(y) - 4*x**2*exp(-x**2)*cos(y) + 3*exp(-x**2)*cos(y) - -sympy undefined can also be used: - -.. code-block:: python - - from sympde import Constant - from sympde import dx, dy - from sympy.abc import x, y - from sympy import Function - - alpha = Constant('alpha') - f = Function('f') - - L = lambda u: -dx(dx(u)) - dy(dy(u)) + alpha * u - - expr = L(f(x,y)) - -which gives: - -.. code-block:: python - - >>> alpha*f(x, y) - Derivative(f(x, y), x, x) - Derivative(f(x, y), y, y) - -Weak forms -^^^^^^^^^^ - -Other useful notions for partial differential equations are *bilinear/linear* forms, which are needed when using variational methods such as finite elements. - -Variational forms come with two very important concepts: - -* **FunctionSpace**: mathematical function space. It can be a Sobolev space for example. - -* **TestFunction**: a member of **FunctionSpace** - -Bilinear form -_____________ - -Unlike fenics, **sympde** does not distinguish between test and trial functions; there is no type for trial functions. In fact, they are implicitly infered from the arguments of a bilinear form. The later is defined like a sympy **Lambda** object. This means that the user must provide: - -* a couple describing test and trial functions - -* a symbolic expression of the bilinear form - -The nice thing about this approach is that it allows calling the bilinear form with different arguments and then ensures more modularity and reuse of the abstract mathematical models. - -The following example shows how to define the weak formulation of the Laplace operator - -.. code-block:: python - - from sympde import grad, dot - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - - V = FunctionSpace('V', ldim=2) - U = FunctionSpace('U', ldim=2) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - - a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) - -Let's take a look at the expression tree: - -.. figure:: include/quick/graph_laplace.png - :scale: 70 % - :alt: map to buried treasure - - Expression tree for Laplace weak form. - -We notice that the BilinearForm arguments are presented as a *Tuple*. The reason is that you can also define a bilinear form that is associated to a system of expressions. - -As any sympde expression, a BilinearForm can be printed, calling - -.. code-block:: python - - print(a) - -would give - -.. code-block:: python - - >>> u*v + Dot(Grad(v), Grad(u)) - -The following example implements a 1D wave model: - -.. code-block:: python - - from sympde import dx - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - - V = FunctionSpace('V', ldim=1) - W = FunctionSpace('W', ldim=1) - - T = Constant('T', real=True, label='Tension applied to the string') - rho = Constant('rho', real=True, label='mass density') - dt = Constant('dt', real=True, label='time step') - - # trial functions - u = TestFunction(V, name='u') - f = TestFunction(W, name='f') - - # test functions - v = TestFunction(V, name='v') - w = TestFunction(W, name='w') - - mass = BilinearForm((v,u), v*u) - adv = BilinearForm((v,u), dx(v)*u) - - expr = rho*mass(v,u) + dt*adv(v, f) + dt*adv(w,u) + mass(w,f) - a = BilinearForm(((v,w), (u,f)), expr) - -.. todo:: add example using Mapping -.. todo:: add example using vector test functions -.. todo:: add example using Field - -Linear form -___________ - -Linear forms are more simple to create, but follow the same logic: - -.. code-block:: python - - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import LinearForm - from sympy import cos - - V = FunctionSpace('V', ldim=2) - - v = TestFunction(V, name='v') - - x,y = V.coordinates - - b = LinearForm(v, cos(x-y)*v) - -Notice that the space gives access to the coordinates, which can be used for callable functions, such as the **cos** in our example. - -Function form -_____________ - -A FunctionForm allows you to write expressions that can be integrated over the compputational domain. It can be defined as follows: - -.. code-block:: python - - from sympde import grad, div - from sympde import FunctionSpace - from sympde import Field - from sympde import FunctionForm - from sympy import cos, pi - - V = FunctionSpace('V', ldim=1) - F = Field('F', space=V) - - x = V.coordinates - - b = FunctionForm(div(grad(F-cos(2*pi*x)))) - -Evaluation -^^^^^^^^^^ - -The purpose of **sympde** is to declare objects that are needed to write an abstract mathematical model for problems involving partial differential equations. It does not provide any discretization. However, it provides you also with algorithms to manipulate the symbolic expressions. -For example, when using *generic* operators such as **grad** or **div**, the expression is not evaluated. For this reason, **sympde** provides the function **evaluate** that allows you to transform your expression into atomic operators such as **dx**, **dy**, **dz**. In the following example, we *evaluate* the Laplace operator: - -.. code-block:: python - - from sympde import grad, dot - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - from sympde import evaluate - - V = FunctionSpace('V', ldim=2) - U = FunctionSpace('U', ldim=2) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - - a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) - print(evaluate(a)) - -The result is the following expression: - -.. code-block:: python - - >>> u_x*v_x + u_y*v_y + u*v - -You can then use this expression inside your code generation module to define the weak form associated to the bilinear form. What **sympde** does is the following: - -* converts generic operators such as **grad** or **div** to their atomic expressions - -* converts an atomic expression like **dx(u)** to **u_x** which can be used directly inside your python code (kernel of finite elements for example) - -If you only want to convert the generic operators into atomic operators, then you may use the **atomize** function: - -.. code-block:: python - - from sympde import grad, dot - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - from sympde import atomize - - V = FunctionSpace('V', ldim=2) - U = FunctionSpace('U', ldim=2) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - - a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) - print(atomize(a.expr)) - -The result is then: - -.. code-block:: python - - >>> u*v + dx(u)*dx(v) + dy(u)*dy(v) - -Notice that **atomize** is a low level function and is called inside **evaluate**. For this reason, **atomize** only operates on the expression of **sympde** forms. +Mappings turn logical domains into physical domains. Multiple patches can be +joined by declaring their connected sides and orientation. See the executable +:doc:`examples/index` for analytical mappings, interface forms, exterior +calculus, and nonlinear linearization. Printing ^^^^^^^^ -Latex -_____ - -A symbolic expression can be printed in latex. This is done by calling the function **latex** on your expression or **sympde** forms. - -.. code-block:: python - - from sympde import grad, div - from sympde import Unknown - from sympde.printing import latex - - u = Unknown('u', ldim=2) - - print(latex(- div(grad(u)) + u)) - -which gives the following result: - -.. code-block:: python - - >>> u - \nabla \cdot \nabla{u} - -.. math:: - - u - \nabla \cdot \nabla{u} - -A **BilinearForm** can also be printed: - -.. code-block:: python - - from sympde import grad, dot - from sympde import FunctionSpace - from sympde import TestFunction - from sympde import BilinearForm - from sympde import atomize - from sympde.printing import latex - - V = FunctionSpace('V', ldim=2) - U = FunctionSpace('U', ldim=2) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - - a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) - print(latex(a)) - -which returns - -.. code-block:: python - - >>> \int_{0}^{1}\int_{0}^{1} u v + \nabla{v} \cdot \nabla{u} dxdy +The public ``latex`` function produces LaTeX for expressions and forms: -.. math:: +.. literalinclude:: examples/quickstart.py + :language: python + :start-after: # [printing-start] + :end-before: # [printing-end] - \int_{0}^{1}\int_{0}^{1} u v + \nabla{v} \cdot \nabla{u} dxdy +The documentation workflow executes ``quickstart.py`` before building these +pages, so its imports and demonstrated API are checked continuously. diff --git a/doc/utils/advanced.py b/doc/utils/advanced.py deleted file mode 100644 index 0a747847..00000000 --- a/doc/utils/advanced.py +++ /dev/null @@ -1,38 +0,0 @@ -# coding: utf-8 -from sympy.printing.dot import dotprint -import os - -def dotexport(expr, fname): - txt = str(dotprint(expr)) - - name = os.path.splitext(fname)[0] - - f = open('{}.dot'.format(name), 'w') - f.write(txt) - f.close() - - cmd = 'dot {name}.dot -Tpng -o {name}.png'.format(name=name) - os.system(cmd) - -# ... -from sympde import grad, dot -from sympde import FunctionSpace -from sympde import TestFunction -from sympde import BilinearForm -from sympde.core import tensorize -from sympde.printing.latex import latex - -V = FunctionSpace('V', ldim=2) -U = FunctionSpace('U', ldim=2) - -v = TestFunction(V, name='v') -u = TestFunction(U, name='u') - -a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) -print('> a = ', a) -print('> tensorize(a) = ', tensorize(a)) - -print(latex(tensorize(a))) - -#dotexport(a, 'graph_laplace.png') -# ... diff --git a/doc/utils/quick.py b/doc/utils/quick.py deleted file mode 100644 index d940ad1e..00000000 --- a/doc/utils/quick.py +++ /dev/null @@ -1,228 +0,0 @@ -# coding: utf-8 -from sympy.printing.dot import dotprint -import os - -def dotexport(expr, fname): - txt = str(dotprint(expr)) - - name = os.path.splitext(fname)[0] - - f = open('{}.dot'.format(name), 'w') - f.write(txt) - f.close() - - cmd = 'dot {name}.dot -Tpng -o {name}.png'.format(name=name) - os.system(cmd) - - -# ... -#from sympde import Unknown -#from sympde import grad, div -# -#u = Unknown('u', ldim=2) -#expr = - div(grad(u)) + u -#dotexport(expr, 'graph1.png') -# ... - -# ... -#from sympde import Unknown -#from sympde import dx, dy -# -#u = Unknown('u', ldim=2) -#v = Unknown('v', ldim=2) -# -#expr = dx(dy(u*v)) -#dotexport(expr, 'graph2.png') -# ... - -# ... -#from sympde import Unknown -#from sympde import dx, dy -# -#u = Unknown('u', ldim=2) -#v = Unknown('v', ldim=2) -# -#expr = dy(2*u+3*v) -#print(expr) -# ... - -# ... -#from sympde import Unknown, Constant -#from sympde import dx -# -#u = Unknown('u', ldim=1) -#alpha = Constant('alpha') -# -#expr = dx(alpha*u) + dx(dx(2*u)) -#print(expr) -# ... - -# ... -#from sympde import Constant -#from sympde import dx, dy -#from sympy.abc import x, y -#from sympy import cos, exp -# -#alpha = Constant('alpha') -# -#L = lambda u: -dx(dx(u)) - dy(dy(u)) + alpha * u -# -#expr = L(cos(y)*exp(-x**2)) -#print(expr) -# ... - -# ... -#from sympde import Constant -#from sympde import dx, dy -#from sympy.abc import x, y -#from sympy import Function -# -#alpha = Constant('alpha') -#f = Function('f') -# -#L = lambda u: -dx(dx(u)) - dy(dy(u)) + alpha * u -# -#expr = L(f(x,y)) -#print(expr) -# ... - -# ... -#from sympde import grad, dot -#from sympde import FunctionSpace -#from sympde import TestFunction -#from sympde import BilinearForm -# -#V = FunctionSpace('V', ldim=2) -#U = FunctionSpace('U', ldim=2) -# -#v = TestFunction(V, name='v') -#u = TestFunction(U, name='u') -# -#a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) -# -#dotexport(a, 'graph_laplace.png') -# ... - -# ... -#from sympde import dx -#from sympde import FunctionSpace -#from sympde import TestFunction -#from sympde import BilinearForm -#from sympde import Constant -# -#V = FunctionSpace('V', ldim=1) -#W = FunctionSpace('W', ldim=1) -# -#T = Constant('T', real=True, label='Tension applied to the string') -#rho = Constant('rho', real=True, label='mass density') -#dt = Constant('dt', real=True, label='time step') -# -## trial functions -#u = TestFunction(V, name='u') -#f = TestFunction(W, name='f') -# -## test functions -#v = TestFunction(V, name='v') -#w = TestFunction(W, name='w') -# -#mass = BilinearForm((v,u), v*u) -#adv = BilinearForm((v,u), dx(v)*u) -# -#expr = rho*mass(v,u) + dt*adv(v, f) + dt*adv(w,u) + mass(w,f) -#a = BilinearForm(((v,w), (u,f)), expr) -# -#print(a) -##dotexport(a, 'graph_wave.png') -# ... - -# ... -#from sympde import FunctionSpace -#from sympde import TestFunction -#from sympde import LinearForm -#from sympy import cos -# -#V = FunctionSpace('V', ldim=2) -# -#v = TestFunction(V, name='v') -# -#x,y = V.coordinates -# -#b = LinearForm(v, cos(x-y)*v) -# ... - -# ... -#from sympde import grad, div -#from sympde import FunctionSpace -#from sympde import Field -#from sympde import FunctionForm -#from sympy import cos, pi -# -#V = FunctionSpace('V', ldim=1) -#F = Field('F', space=V) -# -#x = V.coordinates -# -#b = FunctionForm(div(grad(F-cos(2*pi*x)))) -# ... - -# ... -#from sympde import grad, dot -#from sympde import FunctionSpace -#from sympde import TestFunction -#from sympde import BilinearForm -#from sympde import evaluate -# -#V = FunctionSpace('V', ldim=2) -#U = FunctionSpace('U', ldim=2) -# -#v = TestFunction(V, name='v') -#u = TestFunction(U, name='u') -# -#a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) -#print(evaluate(a)) -# ... - -# ... -#from sympde import grad, dot -#from sympde import FunctionSpace -#from sympde import TestFunction -#from sympde import BilinearForm -#from sympde import atomize -# -#V = FunctionSpace('V', ldim=2) -#U = FunctionSpace('U', ldim=2) -# -#v = TestFunction(V, name='v') -#u = TestFunction(U, name='u') -# -#a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) -#print(atomize(a.expr)) -# ... - -# ... -#from sympde import grad, div -#from sympde import Unknown -#from sympde.printing import latex -# -#u = Unknown('u', ldim=2) -# -#print(latex(- div(grad(u)) + u)) -# ... - -# ... -from sympde import grad, dot -from sympde import FunctionSpace -from sympde import TestFunction -from sympde import BilinearForm -from sympde import atomize -from sympde.printing import latex - -V = FunctionSpace('V', ldim=2) -U = FunctionSpace('U', ldim=2) - -v = TestFunction(V, name='v') -u = TestFunction(U, name='u') - -a = BilinearForm((v,u), dot(grad(v), grad(u)) + v*u) -print(latex(a)) -# ... diff --git a/notebooks/Analytical_Mapping.ipynb b/notebooks/Analytical_Mapping.ipynb deleted file mode 100644 index 59719637..00000000 --- a/notebooks/Analytical_Mapping.ipynb +++ /dev/null @@ -1,291 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Analytical mapping" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "metadata": {}, - "outputs": [], - "source": [ - "from sympy.core.containers import Tuple\n", - "from sympy import Matrix\n", - "from sympy.tensor import IndexedBase\n", - "from sympy import symbols, simplify, Symbol\n", - "from sympy import expand\n", - "from sympy import cos, sin, sqrt, pi\n", - "from sympy import lambdify\n", - "\n", - "from numpy import linspace, meshgrid\n", - "from matplotlib import pyplot as plt\n", - "\n", - "from sympde.topology import Mapping, DetJacobian\n", - "from sympde.topology import Domain\n", - "\n", - "from sympde.core import Constant\n", - "from sympde.calculus import grad, dot, inner, cross, rot, curl, div\n", - "from sympde.calculus import laplace, hessian, bracket, convect\n", - "from sympde.topology import (dx, dy, dz)\n", - "from sympde.topology import (dx1, dx2, dx3)\n", - "from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace\n", - "from sympde.topology import ScalarTestFunction\n", - "from sympde.topology import VectorTestFunction\n", - "from sympde.topology import LogicalExpr\n", - "from sympde.topology import SymbolicExpr\n", - "from sympde.topology import element_of\n", - "from sympde.topology import IdentityMapping\n", - "from sympde.topology import PolarMapping\n", - "from sympde.topology import TargetMapping\n", - "from sympde.topology import CzarnyMapping\n", - "from sympde.topology import CollelaMapping\n", - "from sympde.topology import TorusMapping\n", - "from sympde.topology import TwistedTargetMapping" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Collela mapping" - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "metadata": {}, - "outputs": [], - "source": [ - "DIM = 2\n", - "M = CollelaMapping('M', DIM)" - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "metadata": {}, - "outputs": [], - "source": [ - "x1, x2 = symbols('x1, x2')\n", - "\n", - "constants = ['eps', 'k1', 'k2']\n", - "eps, k1, k2 = [Constant(i) for i in constants]" - ] - }, - { - "cell_type": "code", - "execution_count": 94, - "metadata": {}, - "outputs": [], - "source": [ - "x = LogicalExpr(M, M[0])\n", - "y = LogicalExpr(M, M[1])" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "metadata": {}, - "outputs": [], - "source": [ - "fx = lambdify((x1,x2,eps,k1,k2), x, 'numpy')\n", - "fy = lambdify((x1,x2,eps,k1,k2), y, 'numpy')" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "metadata": {}, - "outputs": [], - "source": [ - "eps = 0.1 ; k1 = 1. ; k2 = 1." - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "metadata": {}, - "outputs": [], - "source": [ - "T1 = linspace(0., 1., 50)\n", - "T2 = linspace(0., 1., 50)" - ] - }, - { - "cell_type": "code", - "execution_count": 93, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "ax = plt.axes()\n", - "ax.set_aspect('equal') \n", - "\n", - "# plot columns\n", - "for t1 in T1:\n", - " X = fx(t1, T2, eps, k1, k2)\n", - " Y = fy(t1, T2, eps, k1, k2)\n", - " plt.plot(X, Y, '-b',linewidth=0.7)\n", - "\n", - "# plot lines\n", - "for t2 in T2:\n", - " X = fx(T1, t2, eps, k1, k2)\n", - " Y = fy(T1, t2, eps, k1, k2)\n", - " plt.plot(X, Y, '-b', linewidth=0.7) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## User mapping" - ] - }, - { - "cell_type": "code", - "execution_count": 129, - "metadata": {}, - "outputs": [], - "source": [ - "class CustomMapping(Mapping):\n", - " _expressions = {'x': 'A*(x1**2-x2**2)',\n", - " 'y': '2*A*x1*x2'}" - ] - }, - { - "cell_type": "code", - "execution_count": 130, - "metadata": {}, - "outputs": [], - "source": [ - "DIM = 2\n", - "M = CustomMapping('M', DIM)" - ] - }, - { - "cell_type": "code", - "execution_count": 131, - "metadata": {}, - "outputs": [], - "source": [ - "x1, x2 = symbols('x1, x2')\n", - "A = Constant('A')" - ] - }, - { - "cell_type": "code", - "execution_count": 132, - "metadata": {}, - "outputs": [], - "source": [ - "fx = lambdify((x1,x2,A), LogicalExpr(M, M[0]), 'numpy')\n", - "fy = lambdify((x1,x2,A), LogicalExpr(M, M[1]), 'numpy')" - ] - }, - { - "cell_type": "code", - "execution_count": 133, - "metadata": {}, - "outputs": [], - "source": [ - "A = 1." - ] - }, - { - "cell_type": "code", - "execution_count": 134, - "metadata": {}, - "outputs": [], - "source": [ - "T1 = linspace(-1., 1., 50)\n", - "T2 = linspace(-1., 1., 50)" - ] - }, - { - "cell_type": "code", - "execution_count": 135, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "ax = plt.axes()\n", - "ax.set_aspect('equal') \n", - "\n", - "# plot columns\n", - "for t1 in T1:\n", - " X = fx(t1, T2, A)\n", - " Y = fy(t1, T2, A)\n", - " plt.plot(X, Y, '-b',linewidth=0.7)\n", - "\n", - "# plot lines\n", - "for t2 in T2:\n", - " X = fx(T1, t2, A)\n", - " Y = fy(T1, t2, A)\n", - " plt.plot(X, Y, '-b', linewidth=0.7) " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/Exterior_Calculus.ipynb b/notebooks/Exterior_Calculus.ipynb deleted file mode 100644 index b7de5187..00000000 --- a/notebooks/Exterior_Calculus.ipynb +++ /dev/null @@ -1,547 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Exterior Calculus" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import Math\n", - "\n", - "from sympy import symbols\n", - "from sympy import Tuple\n", - "from sympy import Matrix\n", - "from sympy import srepr\n", - "from sympy import Symbol\n", - "\n", - "from sympde import Constant\n", - "from sympde.exterior import d, wedge, ip, jp, delta, hodge\n", - "from sympde.exterior import DifferentialForm\n", - "from sympde.printing.latex import latex" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "x, y, z = symbols('x y z')\n", - "a = Constant('a')\n", - "n = Symbol('n')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "u_0, v_0 = [DifferentialForm(i, index=0, dim=n) for i in ['u_0', 'v_0']]\n", - "u_1, v_1 = [DifferentialForm(i, index=1, dim=n) for i in ['u_1', 'v_1']]\n", - "u_2, v_2 = [DifferentialForm(i, index=2, dim=n) for i in ['u_2', 'v_2']]\n", - "u_3, v_3 = [DifferentialForm(i, index=3, dim=n) for i in ['u_3', 'v_3']]" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "u_n, v_n = [DifferentialForm(i, index=n, dim=n) for i in ['u_n', 'v_n']]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Operators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exterior derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**\n", - "\n", - "* $ \\mathrm{d} \\circ \\mathrm{d} ~ u_i = 0$\n", - "* $\\mathrm{d}~ (\\alpha u_i + \\beta v_i) = \\alpha \\mathrm{d}~ u_i + \\beta \\mathrm{d}~ v_i \\quad$ [*linearity*]\n", - "* $\\mathrm{d}~ u_n = 0$" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "assert(d(d(u_0)) == 0)\n", - "assert(d(u_0+v_0) == d(u_0) + d(v_0))\n", - "assert(d(2*u_0) == 2*d(u_0))\n", - "assert(d(a*u_0+v_0) == a*d(u_0) + d(v_0))\n", - "assert(d(u_n) == 0)\n", - "assert(not(d(u_0) == 0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adjoint of the exterior derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**\n", - "\n", - "* $ \\delta \\circ \\delta ~ u_i = 0$\n", - "* $\\delta~ (\\alpha u_i + \\beta v_i) = \\alpha \\delta~ u_i + \\beta \\delta~ v_i \\quad$ [*linearity*]\n", - "* $\\delta~ u_0 = 0$" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "assert(delta(delta(u_3)) == 0)\n", - "assert(delta(u_0) == 0)\n", - "assert(delta(u_1+v_1) == delta(u_1) + delta(v_1))\n", - "assert(delta(2*u_1) == 2*delta(u_1))\n", - "assert(delta(a*u_1+v_1) == a*delta(u_1) + delta(v_1))\n", - "assert(not(delta(u_n) == 0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exterior product" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Euclidean Hodge" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Interior product/contraction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**\n" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "from sympde.topology import Domain\n", - "from sympde.topology import VectorFunctionSpace\n", - "from sympde.topology import element_of\n", - "\n", - "domain = Domain('Omega', dim=3)\n", - "\n", - "V = VectorFunctionSpace('V', domain)\n", - "beta = element_of(V, r'\\beta')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adjoint of the contraction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Lie derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adjoint of Lie derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Trace" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**properties**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Latex printing" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathrm{d} u_{1} + u_{2}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expr = d(u_1) + u_2\n", - "Math(latex(expr))" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathrm{d} u_{1} \\wedge u_{2}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expr = wedge(d(u_1), u_2)\n", - "Math(latex(expr))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathrm{d} \\star\\mathrm{d} u_{3}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expr = d(delta(u_3))\n", - "Math(latex(expr))" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\star\\mathrm{d} \\mathrm{d} u_{2} + \\mathrm{d} \\star\\mathrm{d} u_{2}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expr = d(delta(u_2)) + delta(d(u_2))\n", - "Math(latex(expr))" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathbf{\\beta} \\lrcorner u_{1}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(ip(beta,u_1)))" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# css style\n", - "from IPython.core.display import HTML\n", - "def css_styling():\n", - " styles = open(\"styles/custom.html\", \"r\").read()\n", - " return HTML(styles)\n", - "css_styling()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/Introduction.ipynb b/notebooks/Introduction.ipynb deleted file mode 100644 index 7639697f..00000000 --- a/notebooks/Introduction.ipynb +++ /dev/null @@ -1,642 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# some imports\n", - "from IPython.display import Math\n", - "from sympy import sin, cos, pi\n", - "from sympy.abc import x,y\n", - "\n", - "from sympde.core import Constant\n", - "from sympde.calculus import grad, dot, inner, cross, rot, curl, div\n", - "from sympde.calculus import laplace, hessian, bracket, D, conv\n", - "from sympde.calculus import jump, avg, minus, plus, Dn\n", - "\n", - "from sympde.topology import (dx, dy, dz)\n", - "from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace\n", - "from sympde.topology import ScalarField, VectorField\n", - "from sympde.topology import ProductSpace\n", - "from sympde.topology import Domain, Square\n", - "from sympde.topology import element_of, elements_of\n", - "from sympde.expr import BilinearForm, LinearForm, integral, find\n", - "from sympde.expr import TerminalExpr\n", - "from sympde.printing.latex import latex" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "DIM = 2\n", - "domain = Domain('Omega', dim=DIM)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "domain = Square('Omega')" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "V = ScalarFunctionSpace('V', domain)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "v = element_of(V, name='v')\n", - "u = element_of(V, name='u')" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "a = BilinearForm((v,u), integral(domain, dot(grad(v), grad(u))))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\left( v, u \\right) \\mapsto \\int_{\\Omega} \\nabla{u} \\cdot \\nabla{v} ~d\\mathbf{x}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(a))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "b = LinearForm(v, integral(domain, cos(x+y)*v))" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle v \\mapsto \\int_{\\Omega} v \\cos{\\left (x + y \\right )} ~d\\mathbf{x}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(b))" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle [\\![ u ]\\!]$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(jump(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\{ u \\}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(avg(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle u_{-}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(minus(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\nabla u \\cdot \\mathbf{n}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(Dn(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle [\\![ \\nabla u \\cdot \\mathbf{n} ]\\!]$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(jump(Dn(u))))" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\nabla{u_{-}} \\cdot \\mathbf{n}_{-}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(minus(Dn(u))))" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\nabla{u_{+}} \\cdot \\mathbf{n}_{+}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(plus(Dn(u))))" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "a = BilinearForm((v,u), integral(domain, dot(grad(v), grad(u)) + jump(u)*avg(v) - avg(u)*jump(v)))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\left( v, u \\right) \\mapsto \\int_{\\Omega} - \\{ u \\} [\\![ v ]\\!] ~d\\mathbf{x} + \\int_{\\Omega} \\{ v \\} [\\![ u ]\\!] ~d\\mathbf{x} + \\int_{\\Omega} \\nabla{u} \\cdot \\nabla{v} ~d\\mathbf{x}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(a))" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\nabla^2u$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(laplace(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathrm{hess}( u )$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(hessian(u)))" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle u \\ast v$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(conv(u, v)))" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "W = VectorFunctionSpace('W', domain)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "w = element_of(W, name='w')" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mathrm{D}\\left( \\mathbf{w} \\right)$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(D(w)))" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "l = LinearForm(v, integral(domain.boundary, cos(x+y)*v))" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle v \\mapsto \\int_{\\Gamma_1} v \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_2} v \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_3} v \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_4} v \\cos{\\left (x + y \\right )} ~ds$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(l))" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "equation = find(u, forall=v, lhs=a(u,v), rhs=l(v))" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\mbox{find} ~u \\in V ~\\mbox{such that}\\\\\\int_{\\Omega} - \\{ v \\} [\\![ u ]\\!] ~d\\mathbf{x} + \\int_{\\Omega} \\{ u \\} [\\![ v ]\\!] ~d\\mathbf{x} + \\int_{\\Omega} \\nabla{u} \\cdot \\nabla{v} ~d\\mathbf{x} = \\int_{\\Gamma_1} \\left ( v\\right ) \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_2} \\left ( v\\right ) \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_3} \\left ( v\\right ) \\cos{\\left (x + y \\right )} ~ds + \\int_{\\Gamma_4} \\left ( v\\right ) \\cos{\\left (x + y \\right )} ~ds,\\quad \\forall~ v \\in V$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(equation))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Multiple domains" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [], - "source": [ - "A = Square(r'A')\n", - "B = Square(r'B')\n", - "\n", - "domain = A.join(B, name = 'Omega',\n", - " bnd_minus = A.get_boundary(axis=0, ext=1),\n", - " bnd_plus = B.get_boundary(axis=0, ext=-1))\n", - "\n", - "I = domain.interfaces" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "V = ScalarFunctionSpace('V', domain, kind=None)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "u, v = elements_of(V, names='u, v')" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [], - "source": [ - "# Nitsch\n", - "kappa = Constant('kappa')\n", - "expr_I = ( - jump(u) * jump(Dn(v))\n", - " + kappa * jump(u) * jump(v)\n", - " + plus(Dn(u)) * minus(v)\n", - " + minus(Dn(u)) * plus(v) )\n", - "a = BilinearForm((u,v), integral(domain, dot(grad(u),grad(v)))\n", - " + integral(I, expr_I))" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\left( u, v \\right) \\mapsto \\int_{\\Omega} \\nabla{u} \\cdot \\nabla{v} ~d\\mathbf{x} + \\int_{A|B} - [\\![ u ]\\!] [\\![ \\nabla v \\cdot \\mathbf{n} ]\\!] ~ds + \\int_{A|B} \\left(\\nabla{u_{-}} \\cdot \\mathbf{n}_{-}\\right) v_{+} ~ds + \\int_{A|B} \\left(\\nabla{u_{+}} \\cdot \\mathbf{n}_{+}\\right) v_{-} ~ds + \\int_{A|B} \\kappa [\\![ u ]\\!] [\\![ v ]\\!] ~ds$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(a))" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [], - "source": [ - "expr = TerminalExpr(a)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\textbf{target}~A|B\\\\- \\kappa u_{-} v_{+} + \\left(\\nabla{v_{+}} \\cdot \\mathbf{n}_{+}\\right) u_{-}\\\\\\textbf{target}~A|B\\\\- \\kappa v_{-} u_{+} + \\left(\\nabla{v_{-}} \\cdot \\mathbf{n}_{-}\\right) u_{+}\\\\\\textbf{target}~\\Gamma_2\\\\\\kappa u v - u \\left(\\nabla{v} \\cdot \\mathbf{n}_{-}\\right)\\\\\\textbf{target}~\\Gamma_1\\\\\\kappa u v - u \\left(\\nabla{v} \\cdot \\mathbf{n}_{+}\\right)\\\\\\textbf{target}~A\\\\\\partial_x u_{A} \\partial_x v_{A} + \\partial_y u_{A} \\partial_y v_{A}\\\\\\textbf{target}~B\\\\\\partial_x u_{B} \\partial_x v_{B} + \\partial_y u_{B} \\partial_y v_{B}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "code = r'\\\\'.join(latex(e) for e in expr)\n", - "Math(code)\n", - "#print(code)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/differential_culculus.ipynb b/notebooks/differential_culculus.ipynb deleted file mode 100644 index 9b3a7ace..00000000 --- a/notebooks/differential_culculus.ipynb +++ /dev/null @@ -1,208 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Differential calculus " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from sympy import Symbol\n", - "from sympy.core.containers import Tuple\n", - "from sympy import symbols\n", - "from sympy import IndexedBase\n", - "from sympy import Matrix\n", - "from sympy import Function\n", - "from sympy import pi, cos, sin\n", - "from sympy import srepr\n", - "from sympy import expand\n", - "from sympy import Function\n", - "from sympy import Integer, Float, Rational\n", - "from sympy.physics.quantum import TensorProduct\n", - "\n", - "from sympde.core import Constant\n", - "from sympde.calculus import grad, dot, inner, outer, cross, rot, curl, div\n", - "from sympde.calculus import laplace, hessian, bracket, convect, D, conv\n", - "from sympde.calculus import ArgumentTypeError\n", - "from sympde.calculus import jump, avg, Dn, minus, plus\n", - "from sympde.topology import Domain\n", - "from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace\n", - "from sympde.topology import ProductSpace\n", - "from sympde.topology import H1Space, HcurlSpace, HdivSpace, L2Space, UndefinedSpace\n", - "from sympde.topology import TestFunction, ScalarTestFunction, VectorTestFunction\n", - "from sympde.topology import ScalarField, VectorField\n", - "from sympde.topology import element_of, elements_of" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "domain = Domain('Omega', dim=3)\n", - "\n", - "V = ScalarFunctionSpace('V', domain)\n", - "W = VectorFunctionSpace('W', domain)\n", - "\n", - "alpha, beta, gamma = [Constant(i) for i in ['alpha','beta','gamma']]\n", - "\n", - "f,g,h = [element_of(V, name=i) for i in ['f','g','h']]\n", - "F,G,H = [element_of(W, i) for i in ['F','G','H']]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Gradient properties" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$ \\nabla (f+g) = \\nabla f + \\nabla g $\n", - "\n", - "$ \\nabla (\\alpha f) = \\alpha \\nabla f$\n", - "\n", - "$ \\nabla (\\alpha f+ \\beta g) = \\alpha \\nabla f + \\beta \\nabla g $\n", - "\n", - "$ \\nabla (fg) = f\\nabla g + g\\nabla f $\n", - "\n", - "$ \\nabla (\\frac{f}{g}) = -\\frac{f}{g^2} \\nabla g + \\frac{1}{g} \\nabla f$" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "assert( grad(f+g) == grad(f) + grad(g) )\n", - "assert( grad(alpha*f) == alpha*grad(f) )\n", - "assert( grad(alpha*f + beta*g) == alpha*grad(f) + beta*grad(g) )\n", - "\n", - "assert( grad(f*g) == f*grad(g) + g*grad(f) )\n", - "assert( grad(f/g) == -f*grad(g)/g**2 + grad(f)/g )\n", - "\n", - "assert( expand(grad(f*g*h)) == f*g*grad(h) + f*h*grad(g) + g*h*grad(f) )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Curl properties" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "assert( curl(F+G) == curl(F) + curl(G) )\n", - "assert( curl(alpha*F) == alpha*curl(F) )\n", - "assert( curl(alpha*F + beta*G) == alpha*curl(F) + beta*curl(G) )\n", - "\n", - "assert( curl(cross(F,G)) == F*div(G) - G*div(F) - convect(F, G) + convect(G, F) )\n", - "assert( curl(f*F) == f*curl(F) + cross(grad(f), F) )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Laplace properties" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "assert( laplace(f+g) == laplace(f) + laplace(g) )\n", - "assert( laplace(alpha*f) == alpha*laplace(f) )\n", - "assert( laplace(alpha*f + beta*g) == alpha*laplace(f) + beta*laplace(g) )\n", - "assert( laplace(f*g) == f*laplace(g) + g*laplace(f) + 2*dot(grad(f), grad(g)) )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Divergence properties" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "assert( div(F+G) == div(F) + div(G) )\n", - "assert( div(alpha*F) == alpha*div(F) )\n", - "assert( div(alpha*F + beta*G) == alpha*div(F) + beta*div(G) )\n", - "\n", - "assert( div(cross(F,G)) == -dot(F, curl(G)) + dot(G, curl(F)) )\n", - "assert( div(f*F) == f*div(F) + dot(F, grad(f)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Other properties" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "assert( curl(grad(f)) == 0 )\n", - "assert( div(curl(F)) == 0 )\n", - "assert( div(cross(grad(f), grad(g))) == 0 )\n", - "assert( curl(curl(F)) == grad(div(F)) - laplace(F))\n", - "assert( curl(f*grad(g)) == cross(grad(f), grad(g)) )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/nonlinear_examples.ipynb b/notebooks/nonlinear_examples.ipynb deleted file mode 100644 index a831f5c1..00000000 --- a/notebooks/nonlinear_examples.ipynb +++ /dev/null @@ -1,145 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import Math\n", - "\n", - "from sympy import Symbol\n", - "from sympy.core.containers import Tuple\n", - "from sympy import pi, cos, sin, exp\n", - "from sympy import expand\n", - "\n", - "from sympde.core import Constant\n", - "from sympde.calculus import grad, dot, inner, outer, cross, rot, curl, div\n", - "from sympde.calculus import laplace, hessian, bracket, convect, D, conv\n", - "from sympde.calculus import ArgumentTypeError\n", - "from sympde.calculus import jump, avg, Dn, minus, plus\n", - "from sympde.topology import Domain, Boundary\n", - "from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace\n", - "from sympde.topology import ProductSpace\n", - "from sympde.topology import H1Space, HcurlSpace, HdivSpace, L2Space, UndefinedSpace\n", - "from sympde.topology import TestFunction, ScalarTestFunction, VectorTestFunction\n", - "from sympde.topology import ScalarField, VectorField\n", - "from sympde.topology import element_of, elements_of\n", - "from sympde.expr import LinearForm\n", - "from sympde.expr import integral\n", - "from sympde.expr import linearize\n", - "from sympde.printing.latex import latex" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Nonlinear problems" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Nonlinear poisson" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us consider the weak formulation $l(v)=0$, where\n", - "\n", - "\\begin{align}\n", - " l(v) = \\int_{\\Omega} \\nabla u \\cdot \\nabla v + e^{-u}v ~d\\Omega \n", - " \\label{eq:nonlinear-example-wf}\n", - "\\end{align}\n", - "\n", - "The linearization of such a linear form means that we replace $u$ by an expansion $u + \\epsilon \\delta u$ then we compute the Taylor expansion with respect to $\\epsilon$.\n", - "The linearisation leads to the construction of a bilinear form that has arguments $\\left( \\delta u, v \\right)$, as shown in the follozing Python code:" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "domain = Domain('Omega', dim=2)\n", - "Gamma_N = Boundary('Gamma_N', domain.interior)\n", - "x,y = domain.coordinates\n", - "\n", - "V = ScalarFunctionSpace('V', domain)\n", - "\n", - "u, du,v = elements_of(V, names='u, delta_u, v')\n", - "\n", - "int_0 = lambda expr: integral(domain, expr)\n", - "\n", - "expr = int_0(dot(grad(v), grad(u)) -4.*exp(-u)*v) \n", - "l = LinearForm(v, expr )" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "# linearising l around u, using du\n", - "a = linearize(l, u, trials=du)" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\displaystyle \\left( \\delta_{u}, v \\right) \\mapsto \\int_{\\Omega} 4.0 \\delta_{u} v e^{- u} ~d\\mathbf{x} + \\int_{\\Omega} \\nabla{\\delta_{u}} \\cdot \\nabla{v} ~d\\mathbf{x}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Math(latex(a))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/styles/custom.html b/notebooks/styles/custom.html deleted file mode 100644 index 0da4d105..00000000 --- a/notebooks/styles/custom.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - diff --git a/old/form.py b/old/form.py deleted file mode 100644 index f2ac7cd4..00000000 --- a/old/form.py +++ /dev/null @@ -1,2210 +0,0 @@ -# coding: utf-8 - -# TODO - transpose of BilinearForm -# - add unknown status if only one space is given to the BilinearForm -# => can not be evaluated except if it is called on a couple test/trial -# - check that a BilinearForm is bilinear (using Constant) -# - check that a LinearForm is linear -# - add is_symmetric property for BilinearForm - -from itertools import groupby - -from sympy.core import Basic -from sympy.core import Symbol -from sympy.core import Function -from sympy.simplify.simplify import simplify -from sympy import collect -from sympy.series.order import Order -from sympy.core import Expr, Add, Mul, Pow -from sympy import S -from sympy.core.containers import Tuple -from sympy import Indexed, IndexedBase, Matrix, ImmutableDenseMatrix -from sympy import expand -from sympy import Integer, Float -from sympy.core.expr import AtomicExpr -from sympy.physics.quantum import TensorProduct -from sympy.series.series import series - -from sympde.core.basic import _coeffs_registery -from sympde.core.basic import CalculusFunction -from sympde.core.basic import Constant -from sympde.core.algebra import (Dot_1d, - Dot_2d, Inner_2d, Cross_2d, - Dot_3d, Inner_3d, Cross_3d) -from sympde.core.utils import random_string - -from sympde.calculus import Dot, Inner, Cross -from sympde.calculus import Grad, Rot, Curl, Div -from sympde.calculus import Bracket -from sympde.calculus import Laplace -from sympde.calculus.core import _generic_ops - -from sympde.topology import BasicDomain, Domain, MappedDomain, Union, Interval -from sympde.topology import BoundaryVector, NormalVector, TangentVector, Boundary -from sympde.topology.derivatives import _partial_derivatives -from sympde.topology.derivatives import partial_derivative_as_symbol -from sympde.topology.derivatives import sort_partial_derivatives -from sympde.topology.derivatives import get_atom_derivatives -from sympde.topology.derivatives import dx, dy, dz -from sympde.topology.derivatives import (Grad_1d, Div_1d, - Grad_2d, Curl_2d, Rot_2d, Div_2d, - Grad_3d, Curl_3d, Div_3d) -from sympde.topology.derivatives import Bracket_2d -from sympde.topology.derivatives import Laplace_1d, Laplace_2d, Laplace_3d -from sympde.topology.derivatives import Hessian_1d, Hessian_2d, Hessian_3d -from sympde.topology.space import FunctionSpace -from sympde.topology.space import ProductSpace -from sympde.topology.space import TestFunction -from sympde.topology.space import VectorTestFunction -from sympde.topology.space import IndexedTestTrial -from sympde.topology.space import Unknown, VectorUnknown -from sympde.topology.space import Trace -from sympde.topology.space import Field, VectorField, IndexedVectorField -from sympde.topology.measure import CanonicalMeasure -from sympde.topology.measure import CartesianMeasure -from sympde.topology.measure import Measure - -from .errors import UnconsistentError -from .errors import UnconsistentLinearExpressionError - - -#============================================================================== -def _initialize_measure(measure, coordinates): - if not( measure is None ): - return measure - - if not( coordinates is None ): - return Measure(coordinates) - - else: - raise NotImplementedError('') - -#============================================================================== -def _initialize_boundary(expr): - - traces = expr.atoms(Trace) - boundaries = [trace.boundary for trace in traces] - boundaries = list(set(boundaries)) # remove redanduncy - - boundary = None - if len(boundaries) == 0: - boundary = None - - elif len(boundaries) == 1: - boundary = boundaries[0] - - elif (len(boundaries) > 1): - if not is_sum_of_form_calls(expr): - msg = '> BilinearForm can not be defined on different boundaries' - raise UnconsistentError(msg) - - boundary = Union(*boundaries) - - # ... - if isinstance(boundary, Boundary): - # ... - if isinstance(expr, Add) and not is_sum_of_form_calls(expr): - args = expr.args - args = [a for a in args if not a.atoms(Trace)] - if args: - msg = '> Only boundary terms, using traces, are allowed' - raise UnconsistentError(msg) - # ... - # ... - - return boundary - - -#============================================================================== -class BasicForm(Expr): - _name = None - _boundary = None - - # TODO use .atoms - @property - def fields(self): - ls = [a for a in self.expr.free_symbols if isinstance(a, (Field, VectorField))] - # no redanduncy - return sorted(list(set(ls))) - - # TODO use .atoms - @property - def constants(self): - ls = [a for a in self.expr.free_symbols if isinstance(a, Constant)] - # no redanduncy - return list(set(ls)) - - @property - def domain(self): - return self._domain - - @property - def boundary(self): - return self._boundary - - @property - def measure(self): - return self._measure - - @property - def mapping(self): - return self._mapping - - @property - def name(self): - return self._name - - -#============================================================================== -# TODO we should check that the only free symbols are fields, constants or coordinates -class Integral(BasicForm): - """ - - Examples - - """ - _ldim = None - _coordinates = None - def __new__(cls, expr, domain, measure=None, name=None): - # ... treat union of domains - # TODO improve - unions = expr.atoms(Union) - if unions: - if not( len(unions) == 1 ): - raise NotImplementedError('only one union is available for the moment') - - domains = [] - for i in list(unions): - domains += list(i._args) - domains = set(domains) - if len(domains) > 1: - forms = [] - for domain in domains: - i = list(unions)[0] - _expr = expr.replace(i, domain) - form = Integral(_expr, domain, measure=measure, - name=None) - - forms.append(form) - - expr = Add(*forms) - # ... - - - if not isinstance(domain, BasicDomain): - raise TypeError('> Expecting a BasicDomain object for domain') - - # ... check that there are no test functions in the expression - ls = [a for a in expr.free_symbols if isinstance(a, (TestFunction, VectorTestFunction))] - if not(len(ls) == 0): - raise TypeError('Cannot use test functions in Integral') - # ... - - # ... - coordinates = domain.coordinates - ldim = domain.dim - # ... - - # compute dim from fields if available - ls = list(expr.atoms((Field, VectorField))) - if ls: - F = ls[0] - space = F.space - - else: - tag = random_string( 3 ) - space_name = 'space_{}'.format(tag) - space = FunctionSpace(space_name, domain) - # TODO vector case - - # check if we are using a mapping - mapping = None - if isinstance( domain, MappedDomain ): - mapping = domain.mapping - - measure = _initialize_measure(measure, coordinates) - - # get boundary terms - boundary = _initialize_boundary(expr) - - obj = Basic.__new__(cls, expr) - obj._ldim = ldim - obj._coordinates = coordinates - obj._boundary = boundary - obj._domain = domain - obj._measure = measure - obj._mapping = mapping - obj._space = space - obj._name = name - - return obj - - @property - def expr(self): - return self._args[0] - - @property - def ldim(self): - return self._ldim - - @property - def space(self): - return self._space - - @property - def coordinates(self): - return self._coordinates - - def _sympystr(self, printer): - sstr = printer.doprint - expr = self.expr - return sstr(expr) - - # TODO how to implement this? - def __call__(self, *args): - raise NotImplementedError('') - - -#============================================================================== -class LinearForm(BasicForm): - """ - - Examples - - """ - def __new__(cls, arguments, expr, measure=None, name=None, check=False): - # ... treat union of domains - # TODO improve - unions = expr.atoms(Union) - if unions: - if not( len(unions) == 1 ): - raise NotImplementedError('only one union is available for the moment') - - domains = [] - for i in list(unions): - domains += list(i._args) - domains = set(domains) - if len(domains) > 1: - forms = [] - for domain in domains: - i = list(unions)[0] - _expr = expr.replace(i, domain) - _name = None - if not( name is None ): - _name = '{name}_{domain}'.format(name=name, domain=domain.name) - - form = LinearForm(arguments, _expr, measure=measure, - name=_name) - - forms.append(form(arguments)) - - expr = Add(*forms) - # ... - - # ... - calls = list(expr.atoms(FormCall)) - if check and not calls: - if not is_linear_form(expr, arguments): - msg = '> Expression is not linear' - raise UnconsistentLinearExpressionError(msg) - # ... - - args = _sanitize_form_arguments(arguments, expr, is_linear=True) - obj = Basic.__new__(cls, args, expr) - - # TODO must check that all domains are the same - domain = obj.test_spaces[0].domain - - # check if we are using a mapping - mapping = None - if isinstance( domain, MappedDomain ): - mapping = domain.mapping - - measure = _initialize_measure(measure, obj.coordinates) - - # get boundary terms - boundary = _initialize_boundary(expr) - - obj._domain = domain - obj._boundary = boundary - obj._measure = measure - obj._mapping = mapping - obj._name = name - - return obj - - @property - def variables(self): - return self._args[0] - - @property - def expr(self): - return self._args[1] - - @property - def test_functions(self): - return self.variables - - @property - def ldim(self): - return self.test_spaces[0].ldim - - @property - def coordinates(self): - return self.test_spaces[0].coordinates - - @property - def test_spaces(self): - return [u.space for u in self.test_functions] - - def _sympystr(self, printer): - sstr = printer.doprint - expr = self.expr - return sstr(expr) - - def __call__(self, *args): - args = Tuple(*args) - - if isinstance(self.expr, FormCall): - call = self.expr - return FormCall(call.expr, args) - - return FormCall(self, args) - - def _eval_nseries(self, x, n, logx): - return self.expr._eval_nseries(x, n, logx) - -#============================================================================== -class BilinearForm(BasicForm): - """ - - Examples - - """ - - def __new__(cls, arguments, expr, measure=None, name=None, check=False): - # ... treat union of domains - # TODO improve - unions = expr.atoms(Union) - if unions: - if not( len(unions) == 1 ): - raise NotImplementedError('only one union is available for the moment') - - domains = [] - for i in list(unions): - domains += list(i._args) - domains = set(domains) - if len(domains) > 1: - forms = [] - for domain in domains: - i = list(unions)[0] - _expr = expr.replace(i, domain) - form = BilinearForm(arguments, _expr, measure=measure, - name=None) - - forms.append(form(*arguments)) - - expr = Add(*forms) - # ... - - # ... - if not isinstance(arguments, (tuple, list, Tuple)): - raise TypeError('(test, trial) must be a tuple, list or Tuple') - - if not(len(arguments) == 2): - raise ValueError('Expecting a couple (test, trial)') - # ... - - # ... - calls = list(expr.atoms(FormCall)) - if check and not calls: - if not is_bilinear_form(expr, arguments): - msg = '> Expression is not bilinear' - raise UnconsistentLinearExpressionError(msg) - # ... - - args = _sanitize_form_arguments(arguments, expr, is_bilinear=True) - obj = Basic.__new__(cls, args, expr) - - # TODO must check that all domains are the same - domain = obj.test_spaces[0].domain - - # check if we are using a mapping - mapping = None - if isinstance( domain, MappedDomain ): - mapping = domain.mapping - - measure = _initialize_measure(measure, obj.coordinates) - - # get boundary terms - boundary = _initialize_boundary(expr) - - obj._domain = domain - obj._boundary = boundary - obj._measure = measure - obj._mapping = mapping - obj._name = name - - return obj - - @property - def variables(self): - return self._args[0] - - @property - def expr(self): - return self._args[1] - - @property - def test_functions(self): - return self.variables[0] - - @property - def trial_functions(self): - return self.variables[1] - - @property - def ldim(self): - return self.test_spaces[0].ldim - - @property - def coordinates(self): - return self.test_spaces[0].coordinates - - @property - def test_spaces(self): - return [u.space for u in self.test_functions] - - @property - def trial_spaces(self): - return [u.space for u in self.trial_functions] - - def _sympystr(self, printer): - sstr = printer.doprint - expr = self.expr - return sstr(expr) - - def __call__(self, *args): - if not(len(args) == 2): - raise ValueError('Expecting a couple (test, trial)') - - # ... - expr = self - if isinstance(self.expr, FormCall): - expr = self.expr.expr - # ... - - are_dummy = lambda xs: [isinstance(i, (TestFunction, VectorTestFunction)) - for i in xs] - - # ... - test_functions = args[0] - if not isinstance(test_functions, (tuple, list, Tuple)): - test_functions = [test_functions] - test_functions = Tuple(*test_functions) - # ... - - # ... - trial_functions = args[1] - if not isinstance(trial_functions, (tuple, list, Tuple)): - trial_functions = [trial_functions] - trial_functions = Tuple(*trial_functions) - # ... - - # ... - if not all(are_dummy(test_functions)) and not all(are_dummy(trial_functions)): - # This should return an Integral - raise NotImplementedError('') - - else: - free_variables = None - target = None - if not any(are_dummy(test_functions)): - assert(all(are_dummy(trial_functions))) - - args = trial_functions - free_variables = test_functions - target = 'test' - - elif not any(are_dummy(trial_functions)): - assert(all(are_dummy(test_functions))) - - args = test_functions - free_variables = trial_functions - target = 'trial' - - if not( free_variables is None ): - if target == 'test': - target = expr.variables[0] - - elif target == 'trial': - target = expr.variables[1] - - expr = expr.expr - - for old, new in zip(target, free_variables): - expr = expr.subs(old, new) - - expr = LinearForm(args, expr, check=False) - # ... - - return FormCall(expr, args) - - -#============================================================================== -class Norm(Integral): - def __new__(cls, expr, domain, kind='l2', measure=None, - name=None): - # ... - tests = expr.atoms((TestFunction, VectorTestFunction)) - if tests: - msg = '> Expecting an Expression without test functions' - raise UnconsistentArgumentsError(msg) - - if not isinstance(expr, (Expr, Matrix, ImmutableDenseMatrix)): - msg = '> Expecting Expr, Matrix, ImmutableDenseMatrix' - raise UnconsistentArgumentsError(msg) - - # ... - - # ... - if not(kind in ['l2', 'h1']): - raise ValueError('> Only L2, H1 norms are available') - # ... - - # ... - if name is None: - name = random_string( 3 ) - - name = '{kind}norm_{name}'.format(kind=kind, name=name) - # ... - - # ... - is_vector = isinstance(expr, (Matrix, Tuple, list, tuple)) - if is_vector: - expr = Matrix(expr) - # ... - - # ... - exponent = None - if kind == 'l2': - exponent = 2 - - if not is_vector: - expr = expr*expr - - else: - if not( expr.shape[1] == 1 ): - raise ValueError('Wrong expression for Matrix. must be a row') - - v = Tuple(*expr[:,0]) - expr = Dot(v, v) - - elif kind == 'h1': - exponent = 2 - - if not is_vector: - expr = Dot(Grad(expr), Grad(expr)) - - else: - if not( expr.shape[1] == 1 ): - raise ValueError('Wrong expression for Matrix. must be a row') - - v = Tuple(*expr[:,0]) - expr = Inner(Grad(v), Grad(v)) - # ... - - obj = Integral.__new__(cls, expr, domain, measure=name, name=name) - - obj._exponent = exponent - - return obj - - @property - def exponent(self): - return self._exponent - - -#============================================================================== -class BilinearAtomicForm(BilinearForm, AtomicExpr): - """ - - Examples - - """ - - def _sympystr(self, printer): - sstr = printer.doprint - name = sstr(self.name) - - test = [sstr(i) for i in self.test_functions] - test = ','.join(i for i in test) - - trial = [sstr(i) for i in self.trial_functions] - trial = ','.join(i for i in trial) - - return '{name}({test},{trial})'.format(name=name, trial=trial, test=test) - -#============================================================================== -class Mass(BilinearAtomicForm): - """ - - Examples - - """ - def __new__(cls, test, trial): - - test_trial = [test, trial] - expr = test * trial - - return BilinearForm.__new__(cls, test_trial, expr, name='Mass') - -#============================================================================== -class Stiffness(BilinearAtomicForm): - """ - - Examples - - """ - def __new__(cls, test, trial): - - test_trial = [test, trial] - - coordl = test.space.coordinates.name - coordr = trial.space.coordinates.name - if not(coordl == coordr): - raise ValueError('> Incompatible coordinates') - - ops = {'x': dx, 'y': dy, 'z': dz} - d = ops[coordl] - - expr = d(test) * d(trial) - - return BilinearForm.__new__(cls, test_trial, expr, name='Stiffness') - -#============================================================================== -class Advection(BilinearAtomicForm): - """ - - Examples - - """ - def __new__(cls, test, trial): - - test_trial = [test, trial] - - coordl = test.space.coordinates.name - coordr = trial.space.coordinates.name - if not(coordl == coordr): - raise ValueError('> Incompatible coordinates') - - ops = {'x': dx, 'y': dy, 'z': dz} - d = ops[coordl] - - expr = test * d(trial) - - return BilinearForm.__new__(cls, test_trial, expr, name='Advection') - -#============================================================================== -class AdvectionT(BilinearAtomicForm): - """ - - Examples - - """ - def __new__(cls, test, trial): - - test_trial = [test, trial] - - coordl = test.space.coordinates.name - coordr = trial.space.coordinates.name - if not(coordl == coordr): - raise ValueError('> Incompatible coordinates') - - ops = {'x': dx, 'y': dy, 'z': dz} - d = ops[coordl] - - expr = d(test) * trial - - return BilinearForm.__new__(cls, test_trial, expr, name='AdvectionT') - -#============================================================================== -class Bilaplacian(BilinearAtomicForm): - """ - - Examples - - """ - def __new__(cls, test, trial): - - test_trial = [test, trial] - - coordl = test.space.coordinates.name - coordr = trial.space.coordinates.name - if not(coordl == coordr): - raise ValueError('> Incompatible coordinates') - - ops = {'x': dx, 'y': dy, 'z': dz} - d = ops[coordl] - - expr = d(d(test)) * d(d(trial)) - - return BilinearForm.__new__(cls, test_trial, expr, name='Bilaplacian') - - -#============================================================================== -class Kron(BilinearAtomicForm): - - """.""" - - def __new__(cls, ls): - - ln = len(ls) - dim = len(ls[0]) - obj = Basic.__new__(cls, ls) - if dim >= 3: - raise NotImplementedError('TODO') - - args1 = ['A%s'%i for i in range(ln)] - args2 = ['B%s'%i for i in range(ln)] - try: - import importlib - package = importlib.import_module("sympde.codegen.templates.kron") - except: - raise ImportError('could not import kron_dot') - name = 'kron_dot_2d' - template = getattr(package,name) - body = [template['body'].format(MAT1=args1[i],MAT2=args2[i]) for i in range(ln)] - body = '\n'.join(i for i in body) - args = ','.join(arg for arg in args1+args2) - function = template['function'].format(___MAT_ARGS___=args,__BODY__=body) - args_types = ','.join('double[:,:]' for i in range(2*ln)) - header = template['header'].format(__ARGS_TYPES__=args_types) - dot = compile(function,'','single') - dic = {} - eval(dot,dic) - _dot = dic[name] - setattr(obj, '_dot',_dot) - return obj - - @property - def args(self): - return self._args[0] - - def dot(self, x): - space = x.space - args = list(zip(*self.args)) - args1 = args[0] - args2 = args[1] - args1 = [arg._data for arg in args1] - args2 = [arg._data for arg in args2] - #args1 = [arg._data.T for arg in args1] - #args2 = [arg._data.T for arg in args2] - starts = space.starts - ends = space.ends - pads = space.pads - - from spl.linalg.stencil import StencilVector - Y = StencilVector(space) - X_tmp = StencilVector(space) - #self._dot(starts,ends,pads,x._data.T,Y._data.T,X_tmp._data.T,*args1,*args2) - args = list(args1) + list(args2) - self._dot(starts,ends,pads,x._data,Y._data,X_tmp._data,*args) - return Y - - - def __str__(self): - return 'Kron' - - def _sympystr(self, printer): - return 'Kron' - - -#============================================================================== -class FormCall(AtomicExpr): - - is_commutative = False - - def __new__(cls, expr, args): - - # ... - if not isinstance(args, (list, tuple, Tuple)): - args = [args] - # ... - - # ... - name = expr.name - if isinstance( expr, BilinearForm ): - expr = subs_form(expr, args) - - if not isinstance(expr, BilinearForm): - expr = BilinearForm(args, expr, name=name, check=False) - - elif isinstance( expr, LinearForm ): - expr = subs_form(expr, args) - - if not isinstance(expr, LinearForm): - expr = LinearForm(args, expr, name=name, check=False) - - else: - raise TypeError('> Expecting BilinearForm, LinearForm') - # ... - - # ... - args = Tuple(*args) - obj = Basic.__new__(cls, expr, args) - # ... - - return obj - - @property - def expr(self): - return self._args[0] - - @property - def arguments(self): - return self._args[1] - - -def is_mul_of_form_call(expr): - if not isinstance(expr, Mul): - return False - - are_calls = [isinstance(i, FormCall) for i in expr.args] - any_are_calls = any(are_calls) - if not any_are_calls: - return False - - if (isinstance(any_are_calls, (list, tuple, Tuple)) and - (len(any_are_calls) > 1)): - raise TypeError('> Cannot multiply calls of Bilinear/Linear forms') - - return True - -def is_sum_of_form_calls(expr): - if isinstance(expr, FormCall): return True - if not isinstance(expr, Add): return False - - are_valid = [isinstance(i, FormCall) or is_mul_of_form_call(i) for i in expr.args] - - all_are_valid = all(are_valid) - if any(are_valid) and not(all_are_valid): - raise TypeError('> Invalid expression') - - return all_are_valid - - -def _sanitize_form_arguments(arguments, expr, is_bilinear=False, is_linear=False): - - is_linear = is_linear or (len(expr.atoms(LinearForm)) > 0) - is_bilinear = is_bilinear or (len(expr.atoms(BilinearForm)) > 0) - - # ... - if is_bilinear or is_linear: - - if is_bilinear: - test_functions = arguments[0] - - elif is_linear: - test_functions = arguments - - if isinstance(test_functions, (TestFunction, VectorTestFunction)): - test_functions = [test_functions] - - elif isinstance(test_functions, (tuple, list, Tuple)): - are_valid = [isinstance(i, (TestFunction, VectorTestFunction)) for i in test_functions] - if not all(are_valid): - raise TypeError('> Wrong arguments for test functions') - - else: - msg = 'Wrong type for test function(s). given {}'.format(type(test_functions)) - raise TypeError(msg) - - test_functions = Tuple(*test_functions) - # ... - - # ... - if is_bilinear: - - trial_functions = arguments[1] - if isinstance(trial_functions, (TestFunction, VectorTestFunction)): - trial_functions = [trial_functions] - - elif isinstance(trial_functions, (tuple, list, Tuple)): - are_valid = [isinstance(i, (TestFunction, VectorTestFunction)) for i in trial_functions] - if not all(are_valid): - raise TypeError('> Wrong arguments for trial functions') - - else: - msg = 'Wrong type for trial function(s). given {}'.format(type(trial_functions)) - raise TypeError(msg) - - trial_functions = Tuple(*trial_functions) - # ... - - if is_bilinear: - args = [test_functions, trial_functions] - args = Tuple(*args) - - else: - args = Tuple(*test_functions) - - return args - - -# ... -def atomize(expr, dim=None): - """ - """ - if not isinstance(expr, (Expr, - _partial_derivatives, _generic_ops, - TestFunction, VectorTestFunction, Indexed, - Field, Constant, Symbol, Function, - VectorField, - BoundaryVector, Trace, - Integer, Float, Matrix, ImmutableDenseMatrix, - list, tuple, Tuple)): - msg = ('> Wrong input type.') - - raise TypeError(msg, ', given ', expr, type(expr)) - - # ... replace a FormCall by its expression - calls = expr.atoms(FormCall) - for call in calls: - expr = expr.subs(call, call.expr) - # ... - -# print('> expr [atomize] = ', expr, type(expr)) - - # ... compute dim if None - if dim is None: - ls = [i for i in expr.free_symbols if isinstance(i, (TestFunction, - VectorTestFunction, - Field, - VectorField))] - -# ls = expr.atoms((TestFunction, VectorTestFunction, Field)) -# ls = list(ls) - if ls: - atom = ls[0] - if atom.space is None: - raise ValueError('Expecting atom to be associated to a space') - - dim = atom.space.ldim - # ... - - if isinstance(expr, (list, tuple, Tuple)): - args = [atomize(i, dim=dim) for i in expr] - return Tuple(*args) - - elif isinstance(expr, Add): - args = [atomize(i, dim=dim) for i in expr.args] - return Add(*args) - - elif isinstance(expr, Mul): - coeffs = [i for i in expr.args if isinstance(i, _coeffs_registery)] - vectors = [i for i in expr.args if not(i in coeffs)] - - i = S.One - if coeffs: - i = Mul(*coeffs) - - j = S.One - if vectors: - args = [atomize(i, dim=dim) for i in vectors] - j = Mul(*args) - - return Mul(i, j) - - elif isinstance(expr, Pow): - - b = atomize(expr.base, dim=dim) - e = expr.exp - - return Pow(b, e) - - elif isinstance(expr, BasicForm): - - return atomize(expr.expr, dim=dim) - - elif isinstance(expr, Trace): - # TODO treate different spaces - if expr.order == 0: - return atomize(expr.expr, dim=dim) - - elif expr.order == 1: - # TODO must be passed as key word to atomize - normal_vector_name = 'n' - n = NormalVector(normal_vector_name) - M = atomize(expr.expr, dim=dim) - if dim == 1: - return M - else: - if isinstance(M, (Add, Mul)): - ls = M.atoms(Tuple) - for i in ls: - M = M.subs(i, Matrix(i)) - M = simplify(M) - - e = 0 - for i in range(0, dim): - e += M[i] * n[i] - return e - - else: - raise ValueError('> Only traces of order 0 and 1 are available') - - elif isinstance(expr, _generic_ops): - # if i = Dot(...) then type(i) is Grad - op = type(expr) - new = eval('{0}_{1}d'.format(op, dim)) - - args = [atomize(i, dim=dim) for i in expr.args] - return new(*args) - - elif isinstance(expr, Matrix): - n,m = expr.shape - lines = [] - for i in range(0, n): - line = [] - for j in range(0, m): - line.append(atomize(expr[i,j], dim=dim)) - lines.append(line) - return Matrix(lines) - - return expr -# ... - -# ... -def _evaluate_core(a, verbose=False, variables=None, M=None): - - # ... - if not isinstance(a, (BasicForm, Add, Mul)): - msg = 'Expecting a BasicForm, Add or Mul. Given {}'.format(type(a)) - raise TypeError(msg) - # ... - - # ... - variables = [] - if isinstance(a, (BilinearForm, LinearForm)): - variables = a.variables - - elif isinstance(a, FormCall): - variables = a.variables - # ... - - # ... - def _get_size_and_starts(ls): - n = 0 - d_indices = {} - for x in ls: - d_indices[x] = n - if isinstance(x, TestFunction): - n += 1 - - elif isinstance(x, VectorTestFunction): - for j in range(0, x.shape[0]): - d_indices[x[j]] = n + j - - n += x.shape[0] - - return n, d_indices - # ... - - # ... - tests = [] - trials = [] - # ... - - # ... - if isinstance(a, BilinearForm): - tests = list(a.test_functions) - n_rows, test_indices = _get_size_and_starts(a.test_functions) - - trials = list(a.trial_functions) - n_cols, trial_indices = _get_size_and_starts(a.trial_functions) - - lines = [] - for i in range(0, n_rows): - line = [] - for j in range(0, n_cols): - line.append(0) - lines.append(line) - - M = Matrix(lines) - - elif isinstance(a, LinearForm): - tests = list(a.test_functions) - n_rows, test_indices = _get_size_and_starts(a.test_functions) - - lines = [0 for i in range(0, n_rows)] - M = Matrix(lines) - # ... - - # ... - if isinstance(a, Add): - args = [_evaluate_core(i, verbose=verbose, variables=variables, M=M) - for i in a.args] - - return Add(*args) - - elif isinstance(a, Mul): - # a coeff can be a symbol, otherwise the expression c1 * a - # raises an error - coeffs = [i for i in a.args if isinstance(i, _coeffs_registery) or isinstance(i, Symbol)] - vectors = [i for i in a.args if not(i in coeffs)] - - i = S.One - if coeffs: - i = Mul(*coeffs) - - j = S.One - if vectors: - args = [_evaluate_core(i, verbose=verbose, variables=variables, M=M) - for i in vectors] - j = Mul(*args) - - return Mul(i, j) - # ... - - dim = a.ldim - expr = a.expr - - # convert generic operators to atomic ones - expr = atomize(expr, dim=dim) - - # we need to expand the expression so that we have a sum of product - expr = expand(expr) - - if verbose: - print('> atomized >>> {0}'.format(expr)) - - # ... - def __evaluate_core_LinearForm(expr, M): - # ... - def treat_form(arg, M): - atoms = list(arg.atoms(TestFunction)) - atoms += list(arg.atoms(VectorTestFunction)) - atoms += list(arg.atoms(IndexedTestTrial)) - - for atom in atoms: - if atom in test_indices: - i_row = test_indices[atom] - - else: - raise ValueError('> Could not find {}'.format(atom)) - - M[i_row] += arg - return M - # ... - - # ... - if isinstance(expr, Add): - args = expr.args - for arg in args: - M = treat_form(arg, M) - - elif isinstance(expr, Mul): - M = treat_form(expr, M) - - else: - raise TypeError('> wrong type, given {}'.format(type(expr))) - # ... - - return M - # ... - - # ... - def __evaluate_core_BilinearForm(expr, M): - - # ... - def treat_form(arg, M): - atoms = list(arg.atoms(TestFunction)) - atoms += list(arg.atoms(VectorTestFunction)) - atoms += list(arg.atoms(IndexedTestTrial)) - - for atom in atoms: - if atom in test_indices: - i_row = test_indices[atom] - - elif atom in trial_indices: - i_col = trial_indices[atom] - - else: - raise ValueError('> Could not find {}'.format(atom)) - - M[i_row, i_col] += arg - return M - # ... - - # ... - if isinstance(expr, Add): - args = expr.args - for arg in args: - if isinstance(arg, Mul): - M = treat_form(arg, M) - - elif isinstance(expr, Mul): - M = treat_form(expr, M) - - else: - raise TypeError('> wrong type, given {}'.format(type(expr))) - # ... - - return M - # ... - - # ... - if isinstance(a, BilinearForm): - M = __evaluate_core_BilinearForm(expr, M) - - # returning scalars when possibl - if (n_rows == 1) and (n_cols == 1): - return M[0, 0] - - elif isinstance(a, LinearForm): - M = __evaluate_core_LinearForm(expr, M) - - # returning scalars when possibl - if (n_rows == 1): - return M[0] - - elif isinstance(a, Integral): - return expr - # ... - - return M -# ... - -# ... -def _evaluate_bnd(a, bnd_calls, verbose=False): - if verbose: - print('> bnd calls = ', bnd_calls) - - a_expr = a - if isinstance(a, BasicForm) and is_sum_of_form_calls(a.expr): - # TODO treat Mul node - if isinstance(a.boundary, Union): - if isinstance(a.expr, Add): - newargs = [] - for i in a.expr.args: - - expr = i.expr - if isinstance(expr, BasicForm): - expr = expr.expr - - if is_sum_of_form_calls(expr): - newargs += expr.args - - else: - newargs.append(i) - - a_expr = Add(*newargs) - - elif isinstance(a.expr, Mul): - raise NotImplementedError('') - - else: - a_expr = a.expr - - else: - a_expr = a.expr - - if isinstance(a_expr, FormCall): - a_expr = a_expr.expr.expr - - # ... - boundaries = [] - groups = [] - keyfunc = lambda call: call.expr.boundary - for bnd, g in groupby(bnd_calls, keyfunc): - ls = list(g) - a_bnd = _extract_linear_combination(a_expr, ls) - groups.append(a_bnd) - boundaries.append(bnd) - - if verbose: - print('> groups = ', groups) - print('> boundaries = ', boundaries) - # ... - - - # ... - groups_M = [] - for bnd, ai in zip(boundaries, groups): - a_bnd = ai - for call in ai.atoms(FormCall): - a_bnd = a_bnd.subs(call, call.expr) - - M_bnd = _evaluate_core(a_bnd, verbose=verbose) - - groups_M.append(BoundaryExpression(bnd, M_bnd)) - - if verbose: - print('> groups_M = ', groups_M) - # ... - - return groups_M -# ... - -def _extract_linear_combination(expr, ls): - """returns a new expression for terms that are in ls only.""" - # something like a1 + a2 or a1 + alpha * a2 - if isinstance(expr, Add): - args = [] - for arg in expr.args: - # somthing like alpha*a4 - if isinstance(arg, Mul): - m_args = [i for i in arg.args if i in ls] - if m_args: - args += [arg] - - elif arg in ls: - args += [arg] - - expr = Add(*args) - return expr - -# TODO check that a is a Form, FormCall or linear combination of them -def evaluate(a, verbose=False): - # ... - _calls = a.atoms(FormCall) - calls = [] - for call in _calls: - mycalls = call.expr.atoms(FormCall) - if mycalls: - calls += list(mycalls) - else: - calls += [call] - - # remove redundancy - calls = list(set(calls)) - # ... - - bnd_calls = [] - if calls: - bnd_calls = [a for a in calls if a.expr.boundary] - calls = [a for a in calls if not(a in bnd_calls)] - - if verbose: - print('> calls = ', calls) - - expr_bnd = [] - if bnd_calls: - expr_bnd = _evaluate_bnd(a, bnd_calls, verbose=verbose) - - bnd_done = [i.target for i in expr_bnd] - - expr_domain = [] - if calls: - # TODO - must check that calls have the same domein - # - shall we need to add a groupby here too? - domain = calls[0].expr.domain - - a_expr = a - if (isinstance(a, BasicForm) and is_sum_of_form_calls(a.expr) and - bnd_calls): - a_expr = a.expr - - a = _extract_linear_combination(a_expr, calls) - if verbose: - print('> a = ', a) - - # ... replace a FormCall by its expression - for call in calls: - a = a.subs(call, call.expr) - # ... - - expr = _evaluate_core(a, verbose=verbose) - boundary = list(a.atoms(Boundary)) - if not boundary: - expr_domain = [DomainExpression(domain, expr)] - - else: - boundary = boundary[0] - expr_domain = [BoundaryExpression(boundary, expr)] - - elif isinstance(a, BasicForm): - boundary = list(a.atoms(Boundary)) - - if not boundary: - domain = a.domain - expr = _evaluate_core(a, verbose=verbose) - expr_domain = [DomainExpression(domain, expr)] - - # TODO nitsch case -# else: -# expr_domain = [] -# boundary = [i for i in boundary if not(i in bnd_done)] -# for bnd in boundary: -# expr_domain += [BoundaryExpression(bnd, expr)] - - return expr_bnd + expr_domain - - -#============================================================================== -class KernelExpression(Basic): - def __new__(cls, target, expr): - return Basic.__new__(cls, target, expr) - - @property - def target(self): - return self._args[0] - - @property - def expr(self): - return self._args[1] - -#============================================================================== -class DomainExpression(KernelExpression): - pass - -#============================================================================== -class BoundaryExpression(KernelExpression): - pass - - -#============================================================================== -# TODO - get dim from atoms -# - check coefficinets/functions -def _tensorize_core(expr, dim, tests, trials): - - if isinstance(expr, Add): - args = [_tensorize_core(i, dim, tests, trials) for i in expr.args] - return Add(*args) - - elif isinstance(expr, Mul): - coeffs = [i for i in expr.args if isinstance(i, _coeffs_registery)] - args = [i for i in expr.args if not(i in coeffs)] - - d_atoms = {} - _coordinates = ['x', 'y', 'z'] - _coordinates = [Symbol(i) for i in _coordinates] - test_trial = list(tests) + list(trials) - for a in test_trial: - d_atoms[a] = [] - - new = S.One - for i in range(0, dim): - coord = _coordinates[i] - Di = Interval(coordinate=coord) - Vi = FunctionSpace('V_{}'.format(i), domain=Di) - - ai = TestFunction(Vi, '{test}{i}'.format(test=a.name, i=i)) - d_atoms[a].append(ai) - - new *= ai - expr = expr.subs({a: new}) - - # make sure we have sum of products - expr = expand(expr) - - # ... - # TODO - improve this later - # - must distinguish between test/trial - assert(len(tests) == 1) - assert(len(trials) == 1) - - v = tests[0] - u = trials[0] - - ops = {'x': dx, 'y': dy, 'z': dz} - - for ui,vi in zip(d_atoms[u], d_atoms[v]): - coord = ui.space.coordinates.name - d = ops[coord] - - # ... Mass - old = vi*ui - new = Mass(vi,ui) - - expr = expr.subs({old: new}) - # ... - - # ... Stiffness - old = d(vi)*d(ui) - new = Stiffness(vi,ui) - - expr = expr.subs({old: new}) - # ... - - # ... Advection - old = vi*d(ui) - new = Advection(vi,ui) - - expr = expr.subs({old: new}) - # ... - - # ... Transpose of Advection - old = d(vi)*ui - new = AdvectionT(vi,ui) - - expr = expr.subs({old: new}) - # ... - - # ... Bilaplacian - old = d(d(vi))*d(d(ui)) - new = Bilaplacian(vi,ui) - - expr = expr.subs({old: new}) - # ... - - expr = subs_mul(expr) - # ... - - return expr - -#============================================================================== -def _tensorize_weights(expr): - - if isinstance(expr, Add): - args = [] - for term in expr.args: - #print('> ', term, type(term)) - arg = _tensorize_weights(term) - args.append(arg) - expr = Add(*args) - - elif isinstance(expr, Mul): - args = [] - for term in expr.args: -# print('>> ', term, type(term)) - arg = _tensorize_weights(term) - args.append(arg) - - tensor = [a for a in args if isinstance(a, TensorProduct)] - weights = [a for a in args if not( a in tensor )] - - if tensor: - - tensor = tensor[0] - forms = tensor.args - - coords = [a.coordinates for a in forms] - - # print(forms) - # print(coords) - # print(weights) - - # # ... - # d_args = {} - # for x in coords: - # d_args[x] = [] - # - # for x in coords: - # for a in weights: - # # TODO improve for functions => separability - # ls = a.atoms(Symbol) - # if x in ls: - # print('found ', x, ' in ', a) - # # ... - - expr = Mul(*args) - - elif isinstance(expr, TensorProduct): - args = [] - for term in expr.args: -# print('>>> ', term, type(term)) - arg = _tensorize_weights(term) -# if not( arg is S.One ): -# args.append(arg) - if isinstance(term, BilinearAtomicForm): - coords = term.domain.coordinates - #print(coords) - expr = TensorProduct(*args) - - return expr - -#============================================================================== -def tensorize(a): - - if not isinstance(a, BilinearForm): - raise TypeError('Expecting a BilinearForm') - - # ... - def _get_size_and_starts(ls): - n = 0 - d_indices = {} - for x in ls: - d_indices[x] = n - if isinstance(x, TestFunction): - n += 1 - - elif isinstance(x, VectorTestFunction): - for j in range(0, x.shape[0]): - d_indices[x[j]] = n + j - - n += x.shape[0] - - return n, d_indices - # ... - - if is_sum_of_form_calls(a.expr): - # ... - n_rows, test_indices = _get_size_and_starts(a.test_functions) - n_cols, trial_indices = _get_size_and_starts(a.trial_functions) - - lines = [] - for i in range(0, n_rows): - line = [] - for j in range(0, n_cols): - line.append(0) - lines.append(line) - - M = Matrix(lines) - # ... - - calls = a.atoms(FormCall) - for call in calls: - t = tensorize(call.expr) - - atoms = call.arguments - i_row = None - i_col = None - l_row = 1 - l_col = 1 - for atom in atoms: - if atom in test_indices: - i_row = test_indices[atom] - - if isinstance(atom, VectorTestFunction): - l_row = atom.shape[0] - - elif atom in trial_indices: - i_col = trial_indices[atom] - - if isinstance(atom, VectorTestFunction): - l_col = atom.shape[0] - - else: - raise ValueError('> Could not find {}'.format(atom)) - - if isinstance(t, (Matrix, ImmutableDenseMatrix)): - M_loc = M[i_row:i_row+l_row, i_col:i_col+l_col] - if M_loc.shape == t.shape: - M_loc += t - - # TODO must check if a trial/test is used as test/trial - elif M_loc.shape == t.shape[::-1]: - M_loc += t.transpose() - - else: - raise ValueError('Wrong sizes') - - M[i_row:i_row+l_row, i_col:i_col+l_col] += M_loc - - else: - raise NotImplementedError('TODO') - - return M - - dim = a.ldim - domain = a.domain - tests = a.test_functions - trials = a.trial_functions - - assert(len(a.test_spaces) == 1) - assert(len(a.trial_spaces) == 1) - assert(len(tests) == 1) - assert(len(trials) == 1) - - V = a.test_spaces[0] - U = a.trial_spaces[0] - - # the result of evaluate is a list of KernelExpression - kernels = evaluate(a) - kernels = [i.expr for i in kernels] - - expressions = [] - for kernel in kernels: - if isinstance(kernel, (Matrix, ImmutableDenseMatrix)): - - n_rows, n_cols = kernel.shape - - # ... subs indexed test/trial functions by a new symbol - tmp_tests = [] - tmp_trials = [] - for i_row in range(0, n_rows): - for i_col in range(0, n_cols): - e = kernel[i_row,i_col] - indexed = e.atoms(IndexedTestTrial) - for ui in indexed: - i = ui.indices - if len(i) > 1: - raise ValueError('Expecting one index') - i = i[0] - - space_name = 'VTmp{}'.format(i) - Vi = FunctionSpace(space_name, V.domain) - vi = TestFunction(Vi, '{test}{i}'.format(test=ui.base.name, i=i)) - e = e.subs({ui: vi}) - - if ui.base in tests: - tmp_tests.append(vi) - - elif ui.base in trials: - tmp_trials.append(vi) - - kernel[i_row,i_col] = e - # ... - - tmp_tests += [i for i in tests if not(i in tmp_tests)] - tmp_trials += [i for i in trials if not(i in tmp_trials)] - - # ... - lines = [] - for i_row in range(0, n_rows): - line = [] - for i_col in range(0, n_cols): - e = kernel[i_row,i_col] - - atoms = e.atoms(TestFunction) - _tests = [i for i in atoms if i in tmp_tests] - _trials = [i for i in atoms if i in tmp_trials] - - eij = _tensorize_core(e, dim, _tests, _trials) - - line.append(eij) - - lines.append(line) - - expr = Matrix(lines) - # ... - - else: - expr = _tensorize_core(kernel, dim, tests, trials) - - expressions.append(expr) - # ... - - # TODO - # looking for weighted atomic forms - # this should be done if a flag is True - # and used for LinearOperator Kron -# expr = _tensorize_weights(expr) - - return expr - -#============================================================================== -def subs_mul(expr): - """substitute Mul with TensorProduct""" - - if isinstance(expr,(Add, Mul)): - args = expr.args - args = [subs_mul(arg) for arg in args] - - if isinstance(expr, Mul): - args = expr.args - forms = [i for i in args if isinstance(i, BilinearAtomicForm)] - others = [i for i in args if not( i in forms )] - - t = TensorProduct(*forms) - m = Mul(*others) - return m*t - - elif isinstance(expr, Add): - - return Add(*args) - else: - - return expr - -#============================================================================== -# form here is a BilinearForm -def subs_form(form, newargs): -# print('>>> subs_form : ', form) - - if isinstance( form, BilinearForm ): - calls = form.expr.atoms(FormCall) - if calls: - return subs_form(form.expr, newargs) - - else: - return _subs_bilinear_form_core(form, newargs) - - elif isinstance( form, LinearForm ): - calls = form.expr.atoms(FormCall) - if calls: - return subs_form(form.expr, newargs) - - else: - return _subs_linear_form_core(form, newargs) - - elif isinstance( form, FormCall ): - return form - - elif isinstance( form, Add ): - args = [subs_form(a, newargs) for a in form.args] - return Add(*args) - - elif isinstance( form, Mul ): - coeffs = [i for i in form.args if isinstance(i, _coeffs_registery)] - vectors = [i for i in form.args if not(i in coeffs)] - - i = S.One - if coeffs: - i = Mul(*coeffs) - - j = S.One - if vectors: - args = [subs_form(a, newargs) for a in vectors] - j = Mul(*args) - - return Mul(i, j) - - elif isinstance( form, Pow ): - - b = subs_form(form.base, newargs) - e = form.exp - - return Pow(b, e) - - else: - return form - - -#============================================================================== -def _subs_bilinear_form_core(form, newargs): - # ... - test_trial = _sanitize_form_arguments(newargs, form, is_bilinear=True) - - if not isinstance(test_trial, (tuple, list, Tuple)): - raise TypeError('(test, trial) must be a tuple, list or Tuple') - - if not(len(test_trial) == 2): - raise ValueError('Expecting a couple (test, trial)') - # ... - - # ... - test_functions = test_trial[0] - if isinstance(test_functions, (TestFunction, VectorTestFunction)): - test_functions = [test_functions] - - elif isinstance(test_functions, (tuple, list, Tuple)): - test_functions = Tuple(*test_functions) - # ... - - # ... - trial_functions = test_trial[1] - if isinstance(trial_functions, (TestFunction, VectorTestFunction)): - trial_functions = [trial_functions] - - elif isinstance(trial_functions, (tuple, list, Tuple)): - trial_functions = Tuple(*trial_functions) - # ... - - # in order to avoid problems when swapping indices, we need to create - # temp symbols - - # ... - d_tmp = {} - for x in trial_functions: - name = random_string( 6 ) - if isinstance(x, TestFunction): - X = TestFunction(x.space, name=name) - - elif isinstance(x, VectorTestFunction): - X = VectorTestFunction(x.space, name=name) - - else: - raise TypeError('Only TestFunction and VectorTestFunction are available') - - d_tmp[X] = x - # ... - - expr = form.expr - - # ... replacing trial functions by tmp symbols - for k,v in zip(form.trial_functions, d_tmp): - expr = expr.subs(k,v) - # ... - - # ... replacing test functions - for k,v in zip(form.test_functions, test_functions): - expr = expr.subs(k,v) - # ... - - # ... replacing trial functions from tmp symbols - for k,v in d_tmp.items(): - expr = expr.subs(k,v) - # ... - - # ... - if len(test_functions) == 1: test_functions = test_functions[0] - if len(trial_functions) == 1: trial_functions = trial_functions[0] - - test_trial = (test_functions, trial_functions) - # ... - - return BilinearForm(test_trial, expr, name=form.name, check=False) - - -#============================================================================== -def _subs_linear_form_core(form, newargs): - - # ... - test_functions = _sanitize_form_arguments(newargs, form, is_linear=True) - # TODO is it ok to do this? - test_functions = test_functions[0] - - if isinstance(test_functions, (TestFunction, VectorTestFunction)): - test_functions = [test_functions] - - elif isinstance(test_functions, (tuple, list, Tuple)): - test_functions = list(*test_functions) - # ... - - expr = form.expr - - # ... replacing test functions - for k,v in zip(form.test_functions, test_functions): - expr = expr.subs(k,v) - # ... - - if len(test_functions) == 1: test_functions = test_functions[0] - - return LinearForm(test_functions, expr, name=form.name, check=False) - -#============================================================================== -def is_linear_expression(expr, args, debug=True): - """checks if an expression is linear with respect to the given arguments.""" - # ... - left_args = [] - right_args = [] - for arg in args: - tag = random_string( 4 ) - - if isinstance(arg, TestFunction): - left = TestFunction(arg.space, name='l_' + tag) - right = TestFunction(arg.space, name='r_' + tag) - - elif isinstance(arg, VectorTestFunction): - left = VectorTestFunction(arg.space, name='l_' + tag) - right = VectorTestFunction(arg.space, name='r_' + tag) - - elif isinstance(arg, Field): - left = Field('l_' + tag, space=arg.space) - right = Field('r_' + tag, space=arg.space) - - elif isinstance(arg, VectorField): - left = VectorField(arg.space, 'l_' + tag) - right = VectorField(arg.space, 'r_' + tag) - - else: - raise TypeError('') - - left_args += [left] - right_args += [right] - # ... - - # ... check addition - newexpr = expr - for arg, left, right in zip(args, left_args, right_args): - newarg = left + right - newexpr = newexpr.subs(arg, newarg) - - left_expr = expr - for arg, left in zip(args, left_args): - left_expr = left_expr.subs(arg, left) - - right_expr = expr - for arg, right in zip(args, right_args): - right_expr = right_expr.subs(arg, right) - - if not( expand(newexpr) == expand(left_expr) + expand(right_expr) ): - # TODO use a warning or exception? - if debug: - print('Failed to assert addition property') - -# print('===========') -# print(arg, left, right) -# -# print(expand(newexpr)) -# print(expand(left_expr)) -# print(expand(right_expr)) -# print(expand(newexpr) - expand(left_expr) - expand(right_expr)) -# import sys; sys.exit(0) - - - return False - # ... - - # ... check multiplication - tag = random_string( 4 ) - coeff = Constant('alpha_' + tag) - - newexpr = expr - for arg, left in zip(args, left_args): - newarg = coeff * left - newexpr = newexpr.subs(arg, newarg) - - left_expr = expr - for arg, left in zip(args, left_args): - left_expr = left_expr.subs(arg, left) - - left_expr = coeff * left_expr.subs(arg, left) - - if not( expand(newexpr) == expand(left_expr)): - # TODO use a warning or exception? - if debug: - print('Failed to assert multiplication property') - - return False - # ... - - return True - - -#============================================================================== -def is_bilinear_form(expr, args): - """checks if an expression is bilinear with respect to the given arguments.""" - # ... - test_trial = _sanitize_form_arguments(args, expr, is_bilinear=True) - - if not isinstance(test_trial, (tuple, list, Tuple)): - raise TypeError('(test, trial) must be a tuple, list or Tuple') - - if not(len(test_trial) == 2): - raise ValueError('Expecting a couple (test, trial)') - # ... - - # ... - test_functions = test_trial[0] - if isinstance(test_functions, (TestFunction, VectorTestFunction)): - test_functions = [test_functions] - - elif isinstance(test_functions, (tuple, list, Tuple)): - test_functions = Tuple(*test_functions) - # ... - - # ... - trial_functions = test_trial[1] - if isinstance(trial_functions, (TestFunction, VectorTestFunction)): - trial_functions = [trial_functions] - - elif isinstance(trial_functions, (tuple, list, Tuple)): - trial_functions = Tuple(*trial_functions) - # ... - - # ... - if not is_linear_expression(expr, test_functions): - msg = ' Expression is not linear w.r.t [{}]'.format(test_functions) - raise UnconsistentLinearExpressionError(msg) - # ... - - # ... - if not is_linear_expression(expr, trial_functions): - msg = ' Expression is not linear w.r.t [{}]'.format(trial_functions) - raise UnconsistentLinearExpressionError(msg) - # ... - - return True - -#============================================================================== -def is_linear_form(expr, args): - """checks if an expression is linear with respect to the given arguments.""" - # ... - test_functions = _sanitize_form_arguments(args, expr, is_linear=True) - # TODO is it ok to do this? - test_functions = test_functions[0] - - if isinstance(test_functions, (TestFunction, VectorTestFunction)): - test_functions = [test_functions] - - elif isinstance(test_functions, (tuple, list, Tuple)): - test_functions = list(*test_functions) - # ... - - # ... - if not is_linear_expression(expr, test_functions): - msg = ' Expression is not linear w.r.t [{}]'.format(test_functions) - raise UnconsistentLinearExpressionError(msg) - # ... - - return True - - -#============================================================================== -def linearize(form, fields, trials=None): - """linearize a LinearForm around the fields.""" - # ... - if not isinstance(form, LinearForm): - raise TypeError('> Expecting a LinearForm') - - if not isinstance(fields, (list, tuple, Tuple)): - fields = [fields] - - for f in fields: - if not isinstance(f, (Field, VectorField)): - raise TypeError('{} is not Field/VectorField'.format(f)) - - if not(trials is None): - if not isinstance(trials, (list, tuple, Tuple)): - trials = [trials] - - assert( all([isinstance(i, (str, TestFunction, VectorTestFunction)) for i in trials]) ) - assert( len(fields) == len(trials) ) - - newtrials = [] - for i in trials: - if isinstance(i, (TestFunction, VectorTestFunction)): - newtrials += [i.name] - - else: - newtrials += [i] - - trials = newtrials - # ... - - expr = form.expr - test_functions = form.test_functions - fields = Tuple(*fields) - - # ... replace a FormCall by its expression - calls = expr.atoms(FormCall) - for call in calls: - expr = expr.subs(call, call.expr) - # ... - - # ... - trial_functions = [] - newargs = [] - eps = Constant('eps_' + random_string( 4 )) - for i,x in enumerate(fields): - tag = random_string( 4 ) - - if trials is None: - name = x.name + '_' + tag - else: - name = trials[i] - - if isinstance(x, Field): - trial = TestFunction(x.space, name=name) - - elif isinstance(x, VectorField): - trial = VectorTestFunction(x.space, name=name) - - else: - raise TypeError('Only TestFunction and VectorTestFunction are available') - - newargs += [x + eps*trial] - trial_functions += [trial] - # ... - - # ... - newexpr = expr - for k,v in zip(fields, newargs): - newexpr = newexpr.subs(k,v) - # ... - - newexpr = expand(newexpr) - - e = newexpr.series(eps, 0, 2) - d = collect(e, eps, evaluate=False) - expr = d[eps] - -# print('> linearize = ', expr) -# import sys; sys.exit(0) - - test_trial = (test_functions, trial_functions) - return BilinearForm(test_trial, expr, check=True) diff --git a/old/test_calculus.py.old b/old/test_calculus.py.old deleted file mode 100644 index 23770185..00000000 --- a/old/test_calculus.py.old +++ /dev/null @@ -1,161 +0,0 @@ -# coding: utf-8 - -import numpy as np - -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import Symbol -from sympy import Lambda - -from sympde.core import (dx, dy, dz) -from sympde.core import LinearOperator -from sympde.core import ScalarField -from sympde.core import grad, dot, inner -from sympde.core import get_index_derivatives - - -# ... -def test_0(): - print('============ test_0 ==============') - - x,y, a = symbols('x y a') - - # ... - expr = x+y - print('> expr := {0}'.format(expr)) - - expr = LinearOperator(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... - - # ... - expr = 2*x+y - print('> expr := {0}'.format(expr)) - - expr = LinearOperator(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... - - # ... - expr = a*x+y - print('> expr := {0}'.format(expr)) - - expr = LinearOperator(expr) - print('> evaluated := {0}'.format(expr)) - # ... - - # ... - expr = 2*a*x+y - print('> expr := {0}'.format(expr)) - - expr = LinearOperator(expr) - print('> evaluated := {0}'.format(expr)) - # ... -# ... - -# ... -def test_1(): - print('============ test_1 ==============') - - u, v, a = symbols('u v a') - - # ... - expr = u+v - print('> expr := {0}'.format(expr)) - - expr = dx(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... - - # ... - expr = 2*u*v - print('> expr := {0}'.format(expr)) - - expr = dx(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... - - # ... dx should not operate on u^2, - # since we consider only linearized weak formulations - expr = u*u - print('> expr := {0}'.format(expr)) - - expr = dx(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... -# ... - -# ... -def test_2(): - print('============ test_2 ==============') - - u, v = symbols('u v') - F = ScalarField('F') - - # ... - expr = F*v*u - print('> expr := {0}'.format(expr)) - - expr = dx(expr) - print('> evaluated := {0}'.format(expr)) - print('') - # ... -# ... - -# ... -def test_3(): - print('============ test_3 ==============') - - u = symbols('u') - - # ... - expr = dx(u) - d = get_index_derivatives(expr) - assert(d['x'] == 1) - assert(d['y'] == 0) - assert(d['z'] == 0) - # ... - - # ... - expr = dx(dy(u)) - d = get_index_derivatives(expr) - assert(d['x'] == 1) - assert(d['y'] == 1) - assert(d['z'] == 0) - # ... - - # ... - expr = dx(dy(dx(u))) - d = get_index_derivatives(expr) - assert(d['x'] == 2) - assert(d['y'] == 1) - assert(d['z'] == 0) - # ... - - print('') -# ... - -# ... -def test_poisson(): - print('============ test_poisson ==============') - - u, v = symbols('u v') - - # ... - expr = inner(grad(v), grad(u)) - print('> expr := {0}'.format(expr)) - # ... -# ... - -# ..................................................... -if __name__ == '__main__': - test_0() - test_1() - test_2() - test_3() - test_poisson() diff --git a/old/test_expr_1d.py.old b/old/test_expr_1d.py.old deleted file mode 100644 index 9659e9bc..00000000 --- a/old/test_expr_1d.py.old +++ /dev/null @@ -1,429 +0,0 @@ -# coding: utf-8 - -# TODO split the asserts between algebraic and weak formulations ones -# TODO: - __call__ examples are not working anymore - -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import Symbol -from sympy import Function -from sympy import Matrix -from sympy import pi, cos, sin -from sympy import srepr -from sympy.physics.quantum import TensorProduct - -from sympde.core import dx, dy, dz -from sympde.core import Constant -from sympde.core import ScalarField -from sympde.core import grad, dot, cross, rot, curl, div -from sympde.core import FunctionSpace -from sympde.core import ProductSpace -from sympde.core import ScalarTestFunction -from sympde.core import VectorTestFunction -from sympde.core import BilinearForm, LinearForm, Integral -from sympde.core import atomize -from sympde.core import evaluate -from sympde.core import tensorize -from sympde.core import Mass, Stiffness, Advection, AdvectionT -from sympde.core import Unknown -from sympde.core import Domain - -DIM = 1 -domain = Domain('Omega', dim=DIM) - - -# ... -def test_atomize_1d_1(): - print('============ test_atomize_1d_1 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - w = ScalarTestFunction(V, name='w') - c = Constant('c') - F = ScalarField('F', space=V) - x = Symbol('x') - f = Function('f') - - # ... - assert(atomize(grad(v)) == dx(v)) - assert(atomize(grad(c*v)) == c*dx(v)) - assert(atomize(grad(F*v)) == F*dx(v) + v*dx(F)) - assert(atomize(f(x)*grad(v)) == dx(v)*f(x)) - - assert(atomize(dot(grad(v), grad(w))) == dx(v)*dx(w)) - # ... - - # ... - assert(atomize(grad(v*w)) == w*dx(v) + v*dx(w)) - assert(atomize(div(grad(v*w))) == 2*dx(v)*dx(w) + dx(dx(v))*w + dx(dx(w))*v) - # ... - -# expr = div(grad(v*w)) -# print('> input >>> {0}'.format(expr)) -# print('> atomized >>> {0}'.format(atomize(expr))) -# print(expr.is_commutative) -# ... - -# ... -def test_evaluate_1d_1(): - print('============ test_evaluate_1d_1 =============') - - V = FunctionSpace('V', domain) - U = FunctionSpace('U', domain) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(U, name='u') - c = Constant('c') - F = ScalarField('F', space=V) - - Ni, Ni_x = symbols('Ni Ni_x') - Nj, Nj_x = symbols('Nj Nj_x') - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + c*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + c*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + F*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + F*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(F*v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == F*Ni_x*Nj_x + Ni*Nj_x*dx(F)) - # ... -# ... - -# ... -def test_calls_1d_3(): - print('============ test_calls_1d_3 =============') - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - - v1 = ScalarTestFunction(V1, name='v1') - v2 = ScalarTestFunction(V2, name='v2') - u1 = ScalarTestFunction(U1, name='u1') - u2 = ScalarTestFunction(U2, name='u2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - x = V1.coordinates - - v1v2 = VectorTestFunction(V, name='v1v2') - u1u2 = VectorTestFunction(U, name='u1u2') - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - - expr = a1(v2, u2) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v2, u2) + a2(v2, u2) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v1, u2) + a2(v2, u1) - # ... - - # ... - l1 = LinearForm(v1, x*v1, name='l1') - - expr = l1(v2) - # ... - - # ... - l1 = LinearForm(v1, x*v1, name='l1') - l2 = LinearForm(v2, cos(x)*v2, name='l2') - - expr = l1(u1) + l2(u2) - # ... -# ... - -# ... -def test_evaluate_1d_3(): - print('============ test_evaluate_1d_3 =============') - - V1 = FunctionSpace('V1', domain) - U1 = FunctionSpace('U1', domain) - V2 = FunctionSpace('V2', domain) - U2 = FunctionSpace('U2', domain) - - v1 = ScalarTestFunction(V1, name='v1') - u1 = ScalarTestFunction(U1, name='u1') - v2 = ScalarTestFunction(V2, name='v2') - u2 = ScalarTestFunction(U2, name='u2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - v1v2 = VectorTestFunction(V, name='v1v2') - u1u2 = VectorTestFunction(U, name='u1u2') - - c = Constant('c') - - Ni, Ni_x = symbols('Ni Ni_x') - Nj, Nj_x = symbols('Nj Nj_x') - - basis = {v1v2: 'Ni', u1u2: 'Nj'} - - # ... - expr = v1*u1 + dx(v2)*dx(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, 0], [0, Ni_x*Nj_x]]) - assert(evaluate(a, basis=basis) == expected) - # ... - - # ... - expr = v1*u1 + dx(v2)*dx(u2) + dx(v2)*u1 + v1*dx(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, Ni_x*Nj], [Ni*Nj_x, Ni_x*Nj_x]]) - assert(evaluate(a, basis=basis) == expected) - # ... - -# expr = v1*u1 + dx(v2)*dx(u2) + dx(v2)*u1 + v1*dx(u2) -# expr = BilinearForm(((v1, v2), (u1, u2)), expr) -# print('> input >>> {0}'.format(expr)) -# print('> normal form >>> {0}'.format(evaluate(expr, basis=basis))) - -# ... - -# ... -def test_bilinear_form_1d_10(): - print('============ test_bilinear_form_1d_10 =============') - - U = FunctionSpace('U', domain) - V = FunctionSpace('V', domain) - - u = ScalarTestFunction(U, name='u') - v = ScalarTestFunction(V, name='v') - - u1 = ScalarTestFunction(U, name='u1') - v1 = ScalarTestFunction(V, name='v1') - - Ni, Ni_x = symbols('Ni Ni_x') - Nj, Nj_x = symbols('Nj Nj_x') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - a = BilinearForm((v,u), dot(grad(u), grad(v))) - b = BilinearForm((v,u), u*v) - adv = BilinearForm((v,u), dx(u)*v) - - # ... - expected = Ni*Nj + Ni_x*Nj_x - assert(evaluate(a + b, basis={v: 'Nj', u: 'Ni'}) == expected) - # ... - - # ... - expected = 2*Ni_x*Nj_x - assert(evaluate(2 * a, basis={v: 'Nj', u: 'Ni'}) == expected) - # ... - - # ... - expected = c1*Ni_x*Nj_x - assert(evaluate(c1*a, basis={v: 'Nj', u: 'Ni'}) == expected) - # ... - - # ... - expected = c2*Ni*Nj + c1*Ni_x*Nj_x - assert(evaluate(c1*a + c2*b, basis={v: 'Nj', u: 'Ni'}) == expected) - # ... - - # ... - expected = Ni_x*Nj_x*c1 + c2*(Ni*Nj + Ni_x*Nj) - assert(evaluate(c1*a + c2*(b + adv), basis={v: 'Nj', u: 'Ni'}) == expected) - # ... - -# expr = adv(v1, u1) -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr, basis={V: 'Nj', U: 'Ni'}) )) -# print('') -# ... - -# ... -def test_linear_form_1d_10(): - print('============ test_linear_form_1d_10 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - - x = V.coordinates - f = Function('f') - - Ni, Ni_x, Ni_xx = symbols('Ni Ni_x Ni_xx') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - # ... - expected = cos(2*pi*x)*Ni - assert(evaluate(LinearForm(v, cos(2*pi*x)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x)*Ni - assert(evaluate(LinearForm(v, f(x)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = cos(2*pi*x)*Ni_x - assert(evaluate(LinearForm(v, cos(2*pi*x)*dx(v)), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x)*Ni_xx - assert(evaluate(LinearForm(v, f(x)*dx(dx(v))), - basis={v: 'Ni'}) == expected) - # ... - -# expr = LinearForm(v, cos(2*pi*x)*dx(v)) -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr, basis={V: 'Ni'}) )) -# print('') -# ... - -# ... -def test_function_form_1d_10(): - print('============ test_function_form_1d_10 =============') - - V = FunctionSpace('V', domain) - - F = ScalarField('F', space=V) - - x = V.coordinates - f = Function('f') - - Ni, Ni_x, Ni_xx = symbols('Ni Ni_x Ni_xx') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - # ... - expected = -2*pi*sin(2*pi*x) - assert(evaluate(Integral(grad(cos(2*pi*x)), coordinates=[x])) == expected) - # ... - - # ... - expected = F-cos(2*pi*x) - assert(evaluate(Integral(F-cos(2*pi*x))) == expected) - # ... - - # ... - expected = (F-cos(2*pi*x))**2 - assert(evaluate(Integral((F-cos(2*pi*x))**2)) == expected) - # ... - - # ... - expected = dx(F) + 2*pi*sin(2*pi*x) - assert(evaluate(Integral(grad(F-cos(2*pi*x)))) == expected) - # ... - - # ... - expected = (dx(F) + 2*pi*sin(2*pi*x))**2 - assert(evaluate(Integral((grad(F-cos(2*pi*x)))**2)) == expected) - # ... - -# expr = Integral() -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr) )) -# print('') -# ... - -# ... -def test_tensorize_1d_1(): - print('============ test_tensorize_1d_1 =============') - - V = FunctionSpace('V', domain) - V_0 = FunctionSpace('V_0', domain, coordinates=['x']) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(V, name='u') - - v0 = ScalarTestFunction(V_0, name='v0') - u0 = ScalarTestFunction(V_0, name='u0') - - c = Constant('c') - - # ... - expected = Mass(v0, u0) - assert(tensorize(BilinearForm((v,u), u*v)) == expected) - # ... - - # ... - expected = Stiffness(v0, u0) - assert(tensorize(BilinearForm((v,u), dx(u)*dx(v))) == expected) - # ... - - # ... - expected = Advection(v0,u0) - assert(tensorize(BilinearForm((v,u), dx(u) * v)) == expected) - # ... - - # ... - expected = Advection(v0,u0) + AdvectionT(v0,u0) + TensorProduct(c ,Stiffness(v0,u0)) - assert(tensorize(BilinearForm((v,u), dx(v) * u + v * dx(u) + c * dx(v)*dx(u))) == expected) - # ... - -# expr = dx(v) * u + v * dx(u) + c * dx(v)*dx(u) -# expr = BilinearForm((v,u), expr) -# -# print('> input >>> {0}'.format(expr)) -# print('> tensorized >>> {0}'.format(tensorize(expr))) -# ... - -# ... -def test_unknown_1d_1(): - print('============ test_unknown_1d_1 =============') - - domain = Domain('Omega', dim=DIM) - - v = Unknown('v', domain) - c = Constant('c') - - # ... - assert(atomize(grad(v)) == dx(v)) - assert(atomize(grad(c*v)) == c*dx(v)) - # ... -# ... - -# ..................................................... -if __name__ == '__main__': - test_atomize_1d_1() - test_evaluate_1d_1() - - test_evaluate_1d_3() - - # TODO bug -# test_bilinear_form_1d_10() - test_linear_form_1d_10() - test_function_form_1d_10() - - test_tensorize_1d_1() - test_calls_1d_3() - - test_unknown_1d_1() diff --git a/old/test_expr_2d.py b/old/test_expr_2d.py deleted file mode 100644 index d11dad6c..00000000 --- a/old/test_expr_2d.py +++ /dev/null @@ -1,471 +0,0 @@ -# coding: utf-8 - -# TODO - split the asserts between algebraic and weak formulations ones -# - add assert for grad in vector case -# TODO: - __call__ examples are not working anymore - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin -from sympy import srepr -from sympy.physics.quantum import TensorProduct - -from sympde.core import dx, dy, dz -from sympde.core import Constant -from sympde.core import ScalarField -from sympde.core import grad, dot, inner, cross, rot, curl, div -from sympde.core import FunctionSpace -from sympde.core import ProductSpace -from sympde.core import ScalarTestFunction -from sympde.core import VectorTestFunction -from sympde.core import BilinearForm, LinearForm, Integral -from sympde.core import atomize -from sympde.core import evaluate -from sympde.core import tensorize -from sympde.core import Mass, Stiffness, Advection, AdvectionT -from sympde.core import Unknown -from sympde.core import FormCall -from sympde.core import Domain, Boundary, NormalVector, TangentVector -from sympde.core import Trace, trace_0, trace_1 - -DIM = 2 -domain = Domain('Omega', dim=DIM) - -# ... -def test_atomize_2d_1(): - print('============ test_atomize_2d_1 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - w = ScalarTestFunction(V, name='w') - c = Constant('c') - F = ScalarField('F', space=V) - - # ... - assert(atomize(grad(v)) == Tuple(dx(v), - dy(v))) - assert(atomize(grad(c*v)) == Tuple(c*dx(v), - c*dy(v))) - assert(atomize(grad(F*v)) == Tuple(F*dx(v) + v*dx(F), - F*dy(v) + v*dy(F))) - - assert(atomize(dot(grad(v), grad(w))) == dx(v)*dx(w) + dy(v)*dy(w)) - # ... - -# expr = grad(F*v) -# print('> input >>> {0}'.format(expr)) -# print('> atomized >>> {0}'.format(atomize(expr))) -# ... - -# ... -def test_evaluate_2d_1(): - print('============ test_evaluate_2d_1 =============') - - V = FunctionSpace('V', domain) - U = FunctionSpace('U', domain) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(U, name='u') - - x,y = V.coordinates - - c = Constant('c') - F = ScalarField('F', space=V) - f1 = Function('f1') - f2 = Function('f2') - - Ni, Ni_x, Ni_y = symbols('Ni Ni_x Ni_y') - Nj, Nj_x, Nj_y = symbols('Nj Nj_x Nj_y') - - bx, by = symbols('bx by') - b = Tuple(bx, by) - - f = Tuple(f1(x,y), f2(x,y)) - - a00 = Constant('a00') - a10 = Constant('a10') - a01 = Constant('a01') - a11 = Constant('a11') - A = Matrix([[a00, a01], [a10, a11]]) - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + c*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y + c*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + F*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y + F*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(F*v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == F*Ni_x*Nj_x + F*Ni_y*Nj_y + Ni*Nj_x*dx(F) + Ni*Nj_y*dy(F)) - # ... - -# ... - -# ... -def test_atomize_2d_2(): - print('============ test_atomize_2d_2 =============') - - V = FunctionSpace('V', domain, is_block=True, shape=2) - - v = VectorTestFunction(V, name='v') - - assert(atomize(rot(v)) == -dx(v[1]) + dy(v[0])) - assert(atomize(div(v)) == dx(v[0]) + dy(v[1])) - -# expr = div(v) -# print('> input >>> {0}'.format(expr)) -# print('> atomized >>> {0}'.format(atomize(expr))) -# ... - -# ... -def test_evaluate_2d_3(): - print('============ test_evaluate_2d_3 =============') - - V1 = FunctionSpace('V1', domain) - U1 = FunctionSpace('U1', domain) - V2 = FunctionSpace('V2', domain) - U2 = FunctionSpace('U2', domain) - - v1 = ScalarTestFunction(V1, name='v1') - u1 = ScalarTestFunction(U1, name='u1') - v2 = ScalarTestFunction(V2, name='v2') - u2 = ScalarTestFunction(U2, name='u2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - v1v2 = VectorTestFunction(V, name='v1v2') - u1u2 = VectorTestFunction(U, name='u1u2') - - c = Constant('c') - - Ni, Ni_x, Ni_y = symbols('Ni Ni_x Ni_y') - Nj, Nj_x, Nj_y = symbols('Nj Nj_x Nj_y') - - basis = {v1v2: 'Ni', u1u2: 'Nj'} - - # ... - expr = v1*u1 + dx(v2)*dx(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, 0], [0, Ni_x*Nj_x]]) - assert(evaluate(a, basis=basis) == expected) - # ... - - # ... - expr = v1*u1 + dy(v2)*u1 + v1*dx(u2) + dx(v2)*dx(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, Ni_y*Nj], [Ni*Nj_x, Ni_x*Nj_x]]) - assert(evaluate(a, basis=basis) == expected) - # ... - -# expr = v1*u1 + dy(v2)*u1 + v1*dx(u2) + dx(v2)*dx(u2) -# expr = BilinearForm(((v1, v2), (u1, u2)), expr) -# print('> input >>> {0}'.format(expr)) -# print('> normal form >>> {0}'.format(evaluate(expr, basis=basis))) -# ... - -# ... -#def test_bilinear_form_2d_10(): -# print('============ test_bilinear_form_2d_10 =============') -# -# U = FunctionSpace('U', domain) -# V = FunctionSpace('V', domain) -# -# u = ScalarTestFunction(U, name='u') -# v = ScalarTestFunction(V, name='v') -# -# u1 = ScalarTestFunction(U, name='u1') -# v1 = ScalarTestFunction(V, name='v1') -# -# Ni, Ni_x, Ni_y = symbols('Ni Ni_x Ni_y') -# Nj, Nj_x, Nj_y = symbols('Nj Nj_x Nj_y') -# -# c1 = Symbol('c1') -# c2 = Symbol('c2') -# -# a = BilinearForm((v,u), inner(grad(u), grad(v))) -# b = BilinearForm((v,u), u*v) -# adv = BilinearForm((v,u), dx(u)*v) -# -# # ... -# expected = Ni*Nj + Ni_x*Nj_x + Ni_y*Nj_y -# assert(evaluate(a + b, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = 2*Ni_x*Nj_x + 2*Ni_y*Nj_y -# assert(evaluate(2 * a, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = c1*(Ni_x*Nj_x + Ni_y*Nj_y) -# assert(evaluate(c1*a, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = Ni*Nj*c2 + c1*(Ni_x*Nj_x + Ni_y*Nj_y) -# assert(evaluate(c1*a + c2*b, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = c1*(Ni_x*Nj_x + Ni_y*Nj_y) + c2*(Ni*Nj + Ni_x*Nj) -# assert(evaluate(c1*a + c2*(b + adv), basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -## expr = c1*a + c2*(b + adv) -## print('> input >>> {0}'.format(expr)) -## print('> evaluated >>> {0}'.format(evaluate(expr, basis={v: 'Nj', u: 'Ni'}) )) -## print('') -# ... - -# ... -def test_linear_form_2d_10(): - print('============ test_linear_form_2d_10 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - - x,y = V.coordinates - f = Function('f') - g = Function('g') - - Ni, Ni_x, Ni_y = symbols('Ni Ni_x Ni_y') - Nj, Nj_x, Nj_y = symbols('Nj Nj_x Nj_y') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - bx, by = symbols('bx by') - b = Tuple(bx, by) - fg = Tuple(f(x,y), g(x,y)) - - a = LinearForm(v, cos(2*pi*x)*cos(4*pi*y)*v) - - # ... - expected = cos(2*pi*x)*cos(4*pi*y)*Ni - assert(evaluate(LinearForm(v, cos(2*pi*x)*cos(4*pi*y)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x,y)*Ni - assert(evaluate(LinearForm(v, f(x,y)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = bx*Ni_x + by*Ni_y + f(x,y)*Ni - assert(evaluate(LinearForm(v, dot(b, grad(v)) + f(x,y)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x,y)*Ni_x + g(x,y)*Ni_y - assert(evaluate(LinearForm(v, dot(fg, grad(v))), - basis={v: 'Ni'}) == expected) - # ... - -# expr = -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr, basis={v: 'Ni'}) )) -# print('') -# ... - -# ... -def test_function_form_2d_10(): - print('============ test_function_form_2d_10 =============') - - V = FunctionSpace('V', domain) - - F = ScalarField('F', space=V) - - x,y = V.coordinates - - f = Function('f') - g = Function('g') - - Ni, Ni_x, Ni_y = symbols('Ni Ni_x Ni_y') - Nj, Nj_x, Nj_y = symbols('Nj Nj_x Nj_y') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - bx, by = symbols('bx by') - b = Tuple(bx, by) - fg = Tuple(f(x,y), g(x,y)) - - # ... - expected = 4*pi**2*sin(2*pi*x)**2*cos(3*pi*y)**2 + 9*pi**2*sin(3*pi*y)**2*cos(2*pi*x)**2 - e = cos(2*pi*x)*cos(3*pi*y) - assert(evaluate(Integral(dot(grad(e), grad(e)), coordinates=[x,y])) == expected) - # ... - - # ... - expected = F - cos(2*pi*x)*cos(3*pi*y) - assert(evaluate(Integral(F-cos(2*pi*x)*cos(3*pi*y))) == expected) - # ... - - # ... - expected = (F - cos(2*pi*x)*cos(3*pi*y))**2 - assert(evaluate(Integral((F - cos(2*pi*x)*cos(3*pi*y))**2)) == expected) - # ... - - # ... - expected = (dx(F) + 2*pi*sin(2*pi*x)*cos(3*pi*y))**2 + (dy(F) + 3*pi*sin(3*pi*y)*cos(2*pi*x))**2 - e = F -cos(2*pi*x)*cos(3*pi*y) - assert(evaluate(Integral(dot(grad(e), grad(e)))) == expected) - # ... - -# e = F -cos(2*pi*x)*cos(3*pi*y) -# expr = Integral(dot(grad(e), grad(e))) -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr) )) -# print('') -# ... - -# ... -def test_tensorize_2d_1(): - print('============ test_tensorize_2d_1 =============') - - V = FunctionSpace('V', domain) - V_0 = FunctionSpace('V_0', domain, coordinates=['x']) - V_1 = FunctionSpace('V_1', domain, coordinates=['y']) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(V, name='u') - - v0 = ScalarTestFunction(V_0, name='v0') - u0 = ScalarTestFunction(V_0, name='u0') - - v1 = ScalarTestFunction(V_1, name='v1') - u1 = ScalarTestFunction(V_1, name='u1') - - c = Constant('c') - - bx = Constant('bx') - by = Constant('by') - b = Tuple(bx, by) - - # ... - expected = TensorProduct(Mass(v1, u1), Mass(v0, u0)) - assert(tensorize(BilinearForm((v,u), u*v)) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v1, u1), Stiffness(v0, u0)) - assert(tensorize(BilinearForm((v,u), dx(u)*dx(v))) == expected) - # ... - - # ... - expected = TensorProduct(Advection(v1, u1), Mass(v0, u0)) - assert(tensorize(BilinearForm((v,u), dy(u) * v)) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v1,u1), Advection(v0,u0)) - assert(tensorize(BilinearForm((v,u), dx(u) * v)) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v1,u1), Stiffness(v0,u0)) + TensorProduct(Stiffness(v1,u1), Mass(v0,u0)) - assert(tensorize(BilinearForm((v,u), dot(grad(v), grad(u)))) == expected) - # ... - - # ... - expected = (TensorProduct(Advection(v1,u1), Mass(v0,u0)) + - TensorProduct(Mass(v1,u1), Advection(v0,u0)) + - TensorProduct(Mass(v1,u1), Stiffness(v0,u0)) + - TensorProduct(Stiffness(v1,u1), Mass(v0,u0))) - assert(tensorize(BilinearForm((v,u), dot(grad(v), grad(u)) + dx(u)*v + dy(u)*v)) == expected) - # ... - - # ... - expected = (TensorProduct(bx, Mass(v1,u1), AdvectionT(v0,u0)) + - TensorProduct(by, AdvectionT(v1,u1), Mass(v0,u0))) - assert(tensorize(BilinearForm((v,u), dot(b, grad(v)) * u)) == expected) - # ... - - # ... - expected = (TensorProduct(bx**2,Mass(v1,u1),Stiffness(v0,u0)) + - TensorProduct(bx,by,Advection(v1,u1),AdvectionT(v0,u0)) + - TensorProduct(bx,by,AdvectionT(v1,u1),Advection(v0,u0)) + - TensorProduct(by**2,Stiffness(v1,u1),Mass(v0,u0))) - assert(tensorize(BilinearForm((v,u), dot(b, grad(v)) * dot(b, grad(u)))) == expected) - # ... - -# expr = dot(b, grad(v)) * u -# expr = BilinearForm((v,u), expr) -# -# print('> input >>> {0}'.format(expr)) -# print('> tensorized >>> {0}'.format(tensorize(expr))) -# ... - - -# ... -def test_tensorize_2d_2(): - print('============ test_tensorize_2d_2 =============') - - V = FunctionSpace('V', domain, is_block=True, shape=2) -# V_0 = FunctionSpace('V_0', domain, coordinates=['x']) -# V_1 = FunctionSpace('V_1', domain, coordinates=['y']) - - v = VectorTestFunction(V, name='v') - u = VectorTestFunction(V, name='u') - -# v0 = ScalarTestFunction(V_0, name='v0') -# u0 = ScalarTestFunction(V_0, name='u0') -# -# v1 = ScalarTestFunction(V_1, name='v1') -# u1 = ScalarTestFunction(V_1, name='u1') - - c = Constant('c') - - bx = Constant('bx') - by = Constant('by') - b = Tuple(bx, by) - -# # ... -# expected = Mass(v1, u1)*Mass(v0, u0) -# assert(tensorize(BilinearForm((v,u), div(v) * div(u))) == expected) -# # ... - -# expr = div(v) * div(u) + rot(v) * rot(u) -# expr = BilinearForm((v,u), expr) -# -# print('> input >>> {0}'.format(expr)) -# print('> tensorized >>> {0}'.format(tensorize(expr))) -# ... - -# ... -def test_unknown_2d_1(): - print('============ test_unknown_2d_1 =============') - - domain = Domain('Omega', dim=DIM) - - v = Unknown('v', domain) - c = Constant('c') - - # ... - assert(atomize(grad(v)) == Tuple(dx(v), - dy(v))) - assert(atomize(grad(c*v)) == Tuple(c*dx(v), - c*dy(v))) - # ... -# ... diff --git a/old/test_expr_3d.py.old b/old/test_expr_3d.py.old deleted file mode 100644 index 94af95c1..00000000 --- a/old/test_expr_3d.py.old +++ /dev/null @@ -1,589 +0,0 @@ -# coding: utf-8 - -# TODO - split the asserts between algebraic and weak formulations ones -# - add assert for grad in vector case -# TODO: - __call__ examples are not working anymore - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin -from sympy.physics.quantum import TensorProduct - -from sympde.core import dx, dy, dz -from sympde.core import Constant -from sympde.core import ScalarField -from sympde.core import grad, dot, inner, cross, rot, curl, div -from sympde.core import FunctionSpace -from sympde.core import ProductSpace -from sympde.core import ScalarTestFunction -from sympde.core import VectorTestFunction -from sympde.core import BilinearForm, LinearForm, Integral -from sympde.core import atomize -from sympde.core import evaluate -from sympde.core import tensorize -from sympde.core import Mass, Stiffness, Advection, AdvectionT -from sympde.core import Unknown -from sympde.core import Domain - -DIM = 3 -domain = Domain('Omega', dim=DIM) - - -# ... -def test_atomize_3d_1(): - print('============ test_atomize_3d_1 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - w = ScalarTestFunction(V, name='w') - c = Constant('c') - F = ScalarField('F', space=V) - - # ... - assert(atomize(grad(v)) == Tuple(dx(v), - dy(v), - dz(v))) - assert(atomize(grad(c*v)) == Tuple(c*dx(v), - c*dy(v), - c*dz(v))) - assert(atomize(grad(F*v)) == Tuple(F*dx(v) + v*dx(F), - F*dy(v) + v*dy(F), - F*dz(v) + v*dz(F))) - - assert(atomize(dot(grad(v), grad(w))) == dx(v)*dx(w) + dy(v)*dy(w) + dz(v)*dz(w)) - # ... - -# expr = grad(F*v) -# print('> input >>> {0}'.format(expr)) -# print('> atomized >>> {0}'.format(atomize(expr))) -# ... - -# ... -def test_evaluate_3d_1(): - print('============ test_evaluate_3d_1 =============') - - V = FunctionSpace('V', domain) - U = FunctionSpace('U', domain) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(U, name='u') - - x,y,z = V.coordinates - - c = Constant('c') - F = ScalarField('F', space=V) - f1 = Function('f1') - f2 = Function('f2') - f3 = Function('f3') - - Ni, Ni_x, Ni_y, Ni_z = symbols('Ni Ni_x Ni_y Ni_z') - Nj, Nj_x, Nj_y, Nj_z = symbols('Nj Nj_x Nj_y Nj_z') - - bx, by, bz = symbols('bx by bz') - b = Tuple(bx, by, bz) - - f = Tuple(f1(x,y,z), f2(x,y,z), f3(x,y,z)) - - a00 = Constant('a00') - a10 = Constant('a10') - a20 = Constant('a20') - a01 = Constant('a01') - a11 = Constant('a11') - a21 = Constant('a21') - a02 = Constant('a02') - a12 = Constant('a12') - a22 = Constant('a22') - A = Matrix([[a00, a01, a02], [a10, a11, a12], [a20, a21, a22]]) - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + c*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z + c*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(v), grad(u)) + F*v*u) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z + F*Ni*Nj) - # ... - - # ... - a = BilinearForm((v,u), dot(grad(F*v), grad(u))) - assert(evaluate(a, basis={v: 'Ni', u: 'Nj'}) == F*Ni_x*Nj_x + F*Ni_y*Nj_y + F*Ni_z*Nj_z + Ni*Nj_x*dx(F) + Ni*Nj_y*dy(F) + Ni*Nj_z*dz(F)) - # ... - -# ... - -# ... -def test_atomize_3d_2(): - print('============ test_atomize_3d_2 =============') - - V = FunctionSpace('V', domain, is_vector=True, shape=3) - - v = VectorTestFunction(V, name='v') - - assert(atomize(curl(v)) == Tuple( dy(v[2]) - dz(v[1]), - -dx(v[2]) + dz(v[0]), - dx(v[1]) - dy(v[0]))) - assert(atomize(div(v)) == dx(v[0]) + dy(v[1]) + dz(v[2])) - -# expr = curl(v) -# print('> input >>> {0}'.format(expr)) -# print('> atomized >>> {0}'.format(atomize(expr))) -# ... - -# ... -#def test_bilinear_form_3d_10(): -# print('============ test_bilinear_form_3d_10 =============') -# -# U = FunctionSpace('U', domain) -# V = FunctionSpace('V', domain) -# -# u = ScalarTestFunction(U, name='u') -# v = ScalarTestFunction(V, name='v') -# -# u1 = ScalarTestFunction(U, name='u1') -# v1 = ScalarTestFunction(V, name='v1') -# -# Ni, Ni_x, Ni_y, Ni_z = symbols('Ni Ni_x Ni_y Ni_z') -# Nj, Nj_x, Nj_y, Nj_z = symbols('Nj Nj_x Nj_y Nj_z') -# -# c1 = Symbol('c1') -# c2 = Symbol('c2') -# -# a = BilinearForm((v,u), inner(grad(u), grad(v))) -# b = BilinearForm((v,u), u*v) -# adv = BilinearForm((v,u), dx(u)*v) -# -# # ... -# expected = Ni*Nj + Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z -# assert(evaluate(a + b, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = Ni*Nj + Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z -# assert(evaluate(a + b, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = 2*Ni_x*Nj_x + 2*Ni_y*Nj_y + 2*Ni_z*Nj_z -# assert(evaluate(2 * a, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = c1*(Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z) -# assert(evaluate(c1*a, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = Ni*Nj*c2 + c1*(Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z) -# assert(evaluate(c1*a + c2*b, basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# expected = c1*(Ni_x*Nj_x + Ni_y*Nj_y + Ni_z*Nj_z) + c2*(Ni*Nj + Ni_x*Nj) -# assert(evaluate(c1*a + c2*(b + adv), basis={v: 'Nj', u: 'Ni'}) == expected) -# # ... -# -# # ... -# assert(evaluate(a(u1, v1), basis={v: 'Nj', u: 'Ni'}) == evaluate(a(v1, u1), basis={v: 'Nj', u: 'Ni'})) -# # ... -# -## # ... TODO debug -## expected = Ni_x*Nj -## assert(evaluate(adv(v1, u1), basis={v: 'Nj', u: 'Ni'}) == expected) -## -## expected = Nj_x*Ni -## assert(evaluate(adv(u1, v1), basis={v: 'Nj', u: 'Ni'}) == expected) -## # ... -# -## expr = c1*a + c2*(b + adv) -## print('> input >>> {0}'.format(expr)) -## print('> evaluated >>> {0}'.format(evaluate(expr, basis={v: 'Nj', u: 'Ni'}) )) -## print('') -# ... - -# ... -def test_linear_form_3d_10(): - print('============ test_linear_form_3d_10 =============') - - V = FunctionSpace('V', domain) - - v = ScalarTestFunction(V, name='v') - x,y,z = V.coordinates - - f = Function('f') - g = Function('g') - r = Function('r') - - Ni, Ni_x, Ni_y, Ni_z = symbols('Ni Ni_x Ni_y Ni_z') - Nj, Nj_x, Nj_y, Nj_z = symbols('Nj Nj_x Nj_y Nj_z') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - bx, by, bz = symbols('bx by bz') - b = Tuple(bx, by, bz) - fgr = Tuple(f(x,y,z), g(x,y,z), r(x,y,z)) - - a = LinearForm(v, cos(2*pi*x)*cos(4*pi*y)*cos(5*pi*z)*v) - - # ... - expected = cos(2*pi*x)*cos(4*pi*y)*cos(5*pi*z)*Ni - assert(evaluate(LinearForm(v, cos(2*pi*x)*cos(4*pi*y)*cos(5*pi*z)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x,y,z)*Ni - assert(evaluate(LinearForm(v, f(x,y,z)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = bx*Ni_x + by*Ni_y + bz*Ni_z + f(x,y,z)*Ni - assert(evaluate(LinearForm(v, dot(b, grad(v)) + f(x,y,z)*v), - basis={v: 'Ni'}) == expected) - # ... - - # ... - expected = f(x,y,z)*Ni_x + g(x,y,z)*Ni_y + r(x,y,z)*Ni_z - assert(evaluate(LinearForm(v, dot(fgr, grad(v))), - basis={v: 'Ni'}) == expected) - # ... - -# expr = c1*a + c2*(b + adv) -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr, basis={v: 'Ni'}) )) -# print('') -# ... - -# ... -def test_function_form_3d_10(): - print('============ test_function_form_3d_10 =============') - - V = FunctionSpace('V', domain) - - F = ScalarField('F', space=V) - - x,y,z = V.coordinates - - f = Function('f') - g = Function('g') - r = Function('r') - - Ni, Ni_x, Ni_y, Ni_z = symbols('Ni Ni_x Ni_y Ni_z') - Nj, Nj_x, Nj_y, Nj_z = symbols('Nj Nj_x Nj_y Nj_z') - - c1 = Symbol('c1') - c2 = Symbol('c2') - - bx, by, bz = symbols('bx by bz') - b = Tuple(bx, by, bz) - fgr = Tuple(f(x,y,z), g(x,y,z), r(x,y,z)) - - # ... - expected = cos(2*pi*x)*cos(3*pi*y)*cos(5*pi*z) - assert(evaluate(Integral(cos(2*pi*x)*cos(3*pi*y)*cos(5*pi*z), coordinates=[x,y,z])) == expected) - # ... - - # ... - expected = x**2 + y**2 + 1 - e = x*y + z - assert(evaluate(Integral(dot(grad(e), grad(e)), coordinates=[x,y,z])) == expected) - # ... - - # ... - expected = F - cos(2*pi*x)*cos(3*pi*y)*cos(5*pi*z) - assert(evaluate(Integral(F-cos(2*pi*x)*cos(3*pi*y)*cos(5*pi*z))) == expected) - # ... - - # ... - expected = (F - x*y - z)**2 - assert(evaluate(Integral((F - x*y - z)**2)) == expected) - # ... - - # ... - expected = dx(F)**2 + dy(F)**2 + dz(F)**2 - assert(evaluate(Integral(dot(grad(F), grad(F)))) == expected) - # ... - - # ... - expected = (-x + dy(F))**2 + (-y + dx(F))**2 + (dz(F) - 1)**2 - e = F - (x*y + z) - assert(evaluate(Integral(dot(grad(e), grad(e)), coordinates=[x,y,z])) == expected) - # ... - - # ... TODO debug. => infinite recursion!!! why? - # must be a problem with Mul treatements -# e = cos(2*pi*x)*cos(3*pi*y)*cos(5*pi*z) -# e = cos(2*pi*x)*cos(3*pi*y)*z -# e = x*y*z - # ... - -# e = F - (x*y + z) -# expr = Integral(dot(grad(e), grad(e)), coordinates=[x,y,z]) -# print('> input >>> {0}'.format(expr)) -# print('> evaluated >>> {0}'.format(evaluate(expr) )) -# print('') -# ... - -# ... -def test_calls_3d_3(): - print('============ test_calls_3d_3 =============') - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - W1 = FunctionSpace('W1', domain, is_block=True, shape=3) - W2 = FunctionSpace('W2', domain, is_block=True, shape=3) - T1 = FunctionSpace('T1', domain, is_block=True, shape=3) - T2 = FunctionSpace('T2', domain, is_block=True, shape=3) - - v1 = ScalarTestFunction(V1, name='v1') - v2 = ScalarTestFunction(V2, name='v2') - u1 = ScalarTestFunction(U1, name='u1') - u2 = ScalarTestFunction(U2, name='u2') - w1 = VectorTestFunction(W1, name='w1') - w2 = VectorTestFunction(W2, name='w2') - t1 = VectorTestFunction(T1, name='t1') - t2 = VectorTestFunction(T2, name='t2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - x,y,z = V1.coordinates - - v1v2 = VectorTestFunction(V, name='v1v2') - u1u2 = VectorTestFunction(U, name='u1u2') - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - - expr = a1(v2, u2) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v2, u2) + a2(v2, u2) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v1, u2) + a2(v2, u1) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - a3 = BilinearForm((w1, t1), dot(curl(w1), curl(t1)) + div(w1)*div(t1), name='a3') - a4 = BilinearForm((w1, u1), div(w1)*u1, name='a4') - - expr = a3(w2,t2) + a2(v2,u2) + a4(w2,u2) - # ... - - # ... - l1 = LinearForm(v1, x*y*z*v1, name='l1') - - expr = l1(v2) - # ... - - # ... - l1 = LinearForm(v1, x*y*z*v1, name='l1') - l2 = LinearForm(v2, cos(x+y+z)*v2, name='l2') - - expr = l1(u1) + l2(u2) - # ... -# ... - -# ... -def test_evaluate_3d_3(): - print('============ test_evaluate_3d_3 =============') - - V1 = FunctionSpace('V1', domain) - U1 = FunctionSpace('U1', domain) - V2 = FunctionSpace('V2', domain) - U2 = FunctionSpace('U2', domain) - - v1 = ScalarTestFunction(V1, name='v1') - u1 = ScalarTestFunction(U1, name='u1') - v2 = ScalarTestFunction(V2, name='v2') - u2 = ScalarTestFunction(U2, name='u2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - v1v2 = VectorTestFunction(V, name='v1v2') - u1u2 = VectorTestFunction(U, name='u1u2') - - c = Constant('c') - - Ni, Ni_x, Ni_y, Ni_z = symbols('Ni Ni_x Ni_y Ni_z') - Nj, Nj_x, Nj_y, Nj_z = symbols('Nj Nj_x Nj_y Nj_z') - - basis = {v1v2: 'Ni', u1u2: 'Nj'} - - # ... - expr = v1*u1 + dz(v2)*dz(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, 0], [0, Ni_z*Nj_z]]) - assert(evaluate(a, basis=basis) == expected) - # ... - - # ... - expr = v1*u1 + dx(v2)*dx(u1) + dy(v1)*dy(u2) + dz(v2)*dz(u2) - a = BilinearForm(((v1, v2), (u1, u2)), expr) - - expected = Matrix([[Ni*Nj, Ni_x*Nj_x], [Ni_y*Nj_y, Ni_z*Nj_z]]) - assert(evaluate(a, basis=basis) == expected) - # ... - -# expr = v1*u1 + dx(v2)*dx(u1) + dy(v1)*dy(u2) + dz(v2)*dz(u2) -# expr = BilinearForm(((v1, v2), (u1, u2)), expr) -# print('> input >>> {0}'.format(expr)) -# print('> normal form >>> {0}'.format(evaluate(expr, basis=basis))) -# ... - -# ... -def test_tensorize_3d_1(): - print('============ test_tensorize_3d_1 =============') - - V = FunctionSpace('V', domain) - V_0 = FunctionSpace('V_0', domain, coordinates=['x']) - V_1 = FunctionSpace('V_1', domain, coordinates=['y']) - V_2 = FunctionSpace('V_2', domain, coordinates=['z']) - - v = ScalarTestFunction(V, name='v') - u = ScalarTestFunction(V, name='u') - - v0 = ScalarTestFunction(V_0, name='v0') - u0 = ScalarTestFunction(V_0, name='u0') - - v1 = ScalarTestFunction(V_1, name='v1') - u1 = ScalarTestFunction(V_1, name='u1') - - v2 = ScalarTestFunction(V_2, name='v2') - u2 = ScalarTestFunction(V_2, name='u2') - - c = Constant('c') - - bx = Constant('bx') - by = Constant('by') - bz = Constant('bz') - b = Tuple(bx, by, bz) - - # ... - expected = TensorProduct(Mass(v2,u2),Mass(v1,u1),Mass(v0,u0)) - assert(tensorize(BilinearForm((v,u), u*v)) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v2,u2),Mass(v1,u1),Stiffness(v0,u0)) - assert(tensorize(BilinearForm((v,u), dx(u)*dx(v))) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v2,u2),Advection(v1,u1),Mass(v0,u0)) - assert(tensorize(BilinearForm((v,u), dy(u) * v)) == expected) - # ... - - # ... - expected = TensorProduct(Mass(v2,u2),Mass(v1,u1),Advection(v0,u0)) - assert(tensorize(BilinearForm((v,u), dx(u) * v)) == expected) - # ... - - # ... - expected = (TensorProduct(Mass(v2,u2),Mass(v1,u1),Stiffness(v0,u0)) + - TensorProduct(Mass(v2,u2),Stiffness(v1,u1),Mass(v0,u0)) + - TensorProduct(Stiffness(v2,u2),Mass(v1,u1),Mass(v0,u0))) - assert(tensorize(BilinearForm((v,u), dot(grad(v), grad(u)))) == expected) - # ... - - # ... - expected = (TensorProduct(Mass(v2,u2),Advection(v1,u1),Mass(v0,u0)) + - TensorProduct(Mass(v2,u2),Mass(v1,u1),Advection(v0,u0)) + - TensorProduct(Mass(v2,u2),Mass(v1,u1),Stiffness(v0,u0)) + - TensorProduct(Mass(v2,u2),Stiffness(v1,u1),Mass(v0,u0)) + - TensorProduct(Stiffness(v2,u2),Mass(v1,u1),Mass(v0,u0))) - assert(tensorize(BilinearForm((v,u), dot(grad(v), grad(u)) + dx(u)*v + dy(u)*v)) == expected) - # ... - - # ... - expected = (TensorProduct(bx,Mass(v2,u2),Mass(v1,u1),AdvectionT(v0,u0)) + - TensorProduct(by,Mass(v2,u2),AdvectionT(v1,u1),Mass(v0,u0)) + - TensorProduct(bz,AdvectionT(v2,u2),Mass(v1,u1),Mass(v0,u0))) - - assert(tensorize(BilinearForm((v,u), dot(b, grad(v)) * u)) == expected) - # ... - - # ... - expected = (TensorProduct(bx**2,Mass(v2,u2),Mass(v1,u1),Stiffness(v0,u0)) + - TensorProduct(bx,by,Mass(v2,u2),Advection(v1,u1),AdvectionT(v0,u0)) + - TensorProduct(bx,by,Mass(v2,u2),AdvectionT(v1,u1),Advection(v0,u0)) + - TensorProduct(bx,bz,Advection(v2,u2),Mass(v1,u1),AdvectionT(v0,u0)) + - TensorProduct(bx,bz,AdvectionT(v2,u2),Mass(v1,u1),Advection(v0,u0)) + - TensorProduct(by**2,Mass(v2,u2),Stiffness(v1,u1),Mass(v0,u0)) + - TensorProduct(by,bz,Advection(v2,u2),AdvectionT(v1,u1),Mass(v0,u0)) + - TensorProduct(by,bz,AdvectionT(v2,u2),Advection(v1,u1),Mass(v0,u0)) + - TensorProduct(bz**2,Stiffness(v2,u2),Mass(v1,u1),Mass(v0,u0))) - - assert(tensorize(BilinearForm((v,u), dot(b, grad(v)) * dot(b, grad(u)))) == expected) - # ... - -# expr = dot(b, grad(v)) * dot(b, grad(u)) -# expr = BilinearForm((v,u), expr) -# -# print('> input >>> {0}'.format(expr)) -# print('> tensorized >>> {0}'.format(tensorize(expr))) -# ... - -# ... -def test_unknown_3d_1(): - print('============ test_unknown_3d_1 =============') - - domain = Domain('Omega', dim=DIM) - - v = Unknown('v', domain) - c = Constant('c') - - # ... - assert(atomize(grad(v)) == Tuple(dx(v), - dy(v), - dz(v))) - assert(atomize(grad(c*v)) == Tuple(c*dx(v), - c*dy(v), - c*dz(v))) - # ... -# ... - -# ..................................................... -if __name__ == '__main__': - test_atomize_3d_1() - test_evaluate_3d_1() - - test_atomize_3d_2() - -# test_bilinear_form_3d_10() # TODO not working, since args are the same - test_linear_form_3d_10() - test_function_form_3d_10() - - test_evaluate_3d_3() - - test_tensorize_3d_1() - test_calls_3d_3() - - test_unknown_3d_1() diff --git a/old/test_form_2d.py b/old/test_form_2d.py deleted file mode 100644 index 2087c33c..00000000 --- a/old/test_form_2d.py +++ /dev/null @@ -1,1487 +0,0 @@ -# coding: utf-8 - -# TODO - split the asserts between algebraic and weak formulations ones -# - add assert for grad in vector case -# TODO: - __call__ examples are not working anymore - -import pytest - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin, exp -from sympy import srepr -from sympy.physics.quantum import TensorProduct - -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, cross, rot, curl, div -from sympde.calculus import laplace, hessian, bracket, convect -from sympde.topology import (dx, dy, dz) -from sympde.topology import FunctionSpace, VectorFunctionSpace -from sympde.topology import Field, VectorField -from sympde.topology import ProductSpace -from sympde.topology import TestFunction -from sympde.topology import VectorTestFunction -from sympde.topology import Unknown -from sympde.topology import InteriorDomain, Union -from sympde.topology import Boundary, NormalVector, TangentVector -from sympde.topology import Domain -from sympde.topology import Trace, trace_0, trace_1 -from sympde.topology import Mapping -from sympde.topology import Square -from sympde.topology import ElementDomain -from sympde.topology import Area - -from sympde.expr import BilinearForm, LinearForm, Integral -from sympde.expr import atomize -from sympde.expr import evaluate -from sympde.expr import Mass, Stiffness, Advection, AdvectionT -from sympde.expr import Projection -from sympde.expr import Norm -from sympde.expr import FormCall -from sympde.expr import is_linear_form, is_bilinear_form -from sympde.expr import linearize - -from sympde.expr.errors import UnconsistentError -from sympde.expr.errors import UnconsistentLinearExpressionError -from sympde.expr.errors import UnconsistentLhsError -from sympde.expr.errors import UnconsistentRhsError -from sympde.expr.errors import UnconsistentBCError - -DIM = 2 -VERBOSE = False -VERBOSE = True - -#============================================================================== -def test_boundary_2d_1(): - domain = Domain('Omega', dim=DIM) - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - W1 = VectorFunctionSpace('W1', domain) - W2 = VectorFunctionSpace('W2', domain) - T1 = VectorFunctionSpace('T1', domain) - T2 = VectorFunctionSpace('T2', domain) - - v1 = TestFunction(V1, name='v1') - v2 = TestFunction(V2, name='v2') - u1 = TestFunction(U1, name='u1') - u2 = TestFunction(U2, name='u2') - w1 = VectorTestFunction(W1, name='w1') - w2 = VectorTestFunction(W2, name='w2') - t1 = VectorTestFunction(T1, name='t1') - t2 = VectorTestFunction(T2, name='t2') - - x,y = V1.coordinates - - alpha = Constant('alpha') - - B1 = Boundary(r'\Gamma_1', domain) - B2 = Boundary(r'\Gamma_2', domain) - B3 = Boundary(r'\Gamma_3', domain) - - # ... - with pytest.raises(UnconsistentError): - expr = dot(grad(v1), grad(u1)) + v1*trace_0(u1, B1) - a = BilinearForm((v1,u1), expr, name='a') - # ... - - # ... - with pytest.raises(UnconsistentError): - expr = v1*trace_0(u1, B3) + v1*trace_1(grad(u1), B3) + u1*trace_0(v1, B2) - a1 = BilinearForm((v1, u1), expr, name='a1') - # ... - - # ... - expr = dot(grad(v1), grad(u1)) - a_0 = BilinearForm((v1,u1), expr, name='a_0') - - expr = v1*trace_0(u1, B1) - a_bnd = BilinearForm((v1, u1), expr, name='a_bnd') - - expr = a_0(v1,u1) + a_bnd(v1,u1) - a = BilinearForm((v1,u1), expr, name='a') - print(a) - print(evaluate(a, verbose=True)) - print('') -# import sys; sys.exit(0) - # ... - - - # ... - expr = v1*trace_0(u1, B1) + v1*trace_1(grad(u1), B1) - a1 = BilinearForm((v1, u1), expr, name='a1') - - expr = u1*trace_1(grad(v1), B2) - a2 = BilinearForm((v1, u1), expr, name='a2') - - expr = a1(v2, u2) + a2(v2, u2) - # as expected, we can define the form call, but we cannot create a Bilinear - # form out of it. - # TODO add assert on exception type -# a = BilinearForm((v2, u2), expr, name='a') - - print(expr) - print('') - # ... - - # ... - expr = v1*trace_0(u1, B1) - a0 = BilinearForm((v1, u1), expr, name='a0') - - expr = v1*trace_1(grad(u1), B1) - a1 = BilinearForm((v1, u1), expr, name='a1') - - expr = v1*trace_1(grad(u1), B2) - a2 = BilinearForm((v1, u1), expr, name='a2') - - expr = dot(grad(u1), grad(v1)) - a3 = BilinearForm((v1, u1), expr, name='a3') - - expr = u1*v1 - a4 = BilinearForm((v1, u1), expr, name='a4') - - # TODO Mul not treated yet -# expr = a0(v2, u2) + a1(v2, u2) + alpha * a2(v2, u2) + a3(v2, u2) + alpha*a4(v2, u2) -# a = BilinearForm((v2, u2), expr, name='a') -## print(expr) -# print(evaluate(expr, verbose=True)) -# print('') -# -# print(evaluate(a, verbose=True)) -# print('') - - -# expr = a(v2, u2) + a1(v2, u2) -# b = BilinearForm((v2, u2), expr, name='b') -# print(b) -# print(evaluate(b, verbose=True)) - # ... - - # ... - g = Tuple(x**2, y**2) - expr = v1*trace_1(g, B1) - l1 = LinearForm(v1, expr, name='l1') - print(l1) -# print(atomize(l1)) -# print(evaluate(l1)) - print('') - # ... - -#============================================================================== -def test_boundary_2d_2(): - Omega_1 = InteriorDomain('Omega_1', dim=2) - - B1 = Boundary('B1', Omega_1) - B2 = Boundary('B2', Omega_1) - B3 = Boundary('B3', Omega_1) - - domain = Domain('Omega', interiors=[Omega_1], - boundaries=[B1, B2, B3]) - - V = FunctionSpace('V', domain) - v = TestFunction(V, name='v') - u = TestFunction(V, name='u') - - x,y = V.coordinates - - alpha = Constant('alpha') - - # ... - print('==== l0 ====') - l0 = LinearForm(v, x*y*v, name='l0') - - print(evaluate(l0, verbose=VERBOSE)) - print('') - # ... - - # ... - print('==== l1 ====') - g = Tuple(x**2, y**2) - l1 = LinearForm(v, v*trace_1(g, domain.boundary)) - - print(evaluate(l1, verbose=VERBOSE)) - print('') - # ... - - # ... - print('==== l2 ====') - B_neumann = Union(B1, B2) - g = Tuple(x**2, y**2) - l2 = LinearForm(v, v*trace_1(g, B_neumann), name='l2') - - print(evaluate(l2, verbose=VERBOSE)) - print('') - # ... - - # ... - print('==== l3 ====') - l3 = LinearForm(v, l2(v)) - - assert(l3(v).__str__ == l2(v).__str__) - - print(evaluate(l3, verbose=VERBOSE)) - print('') - # ... - - # ... - print('==== l4 ====') - l4 = LinearForm(v, l0(v) + l2(v)) - - print(evaluate(l4, verbose=VERBOSE)) - print('') - # ... - -# # ... -# print('==== a1 ====') -# a1 = BilinearForm((v, u), v*trace_0(u, domain.boundary)) -# -# print(evaluate(a1, verbose=VERBOSE)) -# print('') -# # ... - - -#============================================================================== -def test_calls_2d(): - domain = Domain('Omega', dim=DIM) - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - W1 = VectorFunctionSpace('W1', domain) - W2 = VectorFunctionSpace('W2', domain) - T1 = VectorFunctionSpace('T1', domain) - T2 = VectorFunctionSpace('T2', domain) - - v1 = TestFunction(V1, name='v1') - v2 = TestFunction(V2, name='v2') - u1 = TestFunction(U1, name='u1') - u2 = TestFunction(U2, name='u2') - w1 = VectorTestFunction(W1, name='w1') - w2 = VectorTestFunction(W2, name='w2') - t1 = VectorTestFunction(T1, name='t1') - t2 = VectorTestFunction(T2, name='t2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - x,y = V1.coordinates - - alpha = Constant('alpha') - - F = Field('F', space=V1) - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - print(a1) - print(atomize(a1)) - print(evaluate(a1)) - print('') - - expr = a1(v2, u2) - a = BilinearForm((v2, u2), expr, name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - print('') - # ... - - # ... - a = BilinearForm((v1, u1), dot(grad(v1), grad(u1)), name='a') - - print(a) - print(atomize(a)) - print(evaluate(a)) - print('') - # ... - - # ... - a1 = BilinearForm((v1, u1), dot(grad(v1), grad(u1)), name='a1') - - expr = a1(v2, u2) - a = BilinearForm((v2, u2), expr, name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - print('') - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v2, u2) + a2(v2, u2) - a = BilinearForm((v2, u2), expr, name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - print('') - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v1, u2) - print('> before = ', expr) - expr = expr.subs(u2, u1) - print('> after = ', expr) - print('') - - expr = a1(v1, u2) + a1(v2, u2) - print('> before = ', expr) - expr = expr.subs(u2, u1) - print('> after = ', expr) - print('') - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - - expr = a1(v1, u2) + a2(v2, u1) - a = BilinearForm(((v1,v2),(u1,u2)), expr, name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - print('') - # ... - - # ... - a = BilinearForm((w1, t1), rot(w1)*rot(t1) + div(w1)*div(t1), name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, name='a1') - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), name='a2') - a3 = BilinearForm((w1, t1), rot(w1)*rot(t1) + div(w1)*div(t1), name='a3') - a4 = BilinearForm((w1, u1), div(w1)*u1, name='a4') - - expr = a3(w2,t2) + a2(v2,u2) + a4(w2,u2) - a = BilinearForm(((w2,v2),(t2,u2)), expr, name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - # ... - - # ... - a1 = BilinearForm((v1, u1), laplace(u1)*laplace(v1), name='a1') - print(a1) - print(atomize(a1)) - print(evaluate(a1)) - print('') - # ... - - # ... - a1 = BilinearForm((v1, u1), inner(hessian(u1),hessian(v1)), name='a1') - print('================================') - print(a1) - print(atomize(a1)) - print(evaluate(a1)) - print('') - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, name='11') - - expr = l1(v2) - l = LinearForm(v2, expr, name='1') - print(l) - print(atomize(l)) - print(evaluate(l)) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, name='l1') - l2 = LinearForm(v2, cos(x+y)*v2, name='l2') - - expr = l1(u1) + l2(u2) - l = LinearForm((u1,u2), expr, name='1') - print(l) - print(atomize(l)) - print(evaluate(l)) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, name='l1') - l2 = LinearForm(v2, cos(x+y)*v2, name='l2') - - expr = l1(u1) + alpha * l2(u2) - l = LinearForm((u1,u2), expr, name='1') - print(l) - print(atomize(l)) - print(evaluate(l)) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, name='l1') - l2 = LinearForm(w1, div(w1), name='l2') - - expr = l1(v2) + l2(w2) - l = LinearForm((v2,w2), expr, name='1') - print(l) - print(atomize(l)) - print(evaluate(l)) - # ... - - # ... - I1 = Integral(x*y, domain, name='I1') - - print(I1) - print(atomize(I1)) - print(evaluate(I1)) - # ... - - # ... - expr = F - cos(2*pi*x)*cos(3*pi*y) - expr = dot(grad(expr), grad(expr)) - I2 = Integral(expr, domain, name='I2') - - print(I2) - print(atomize(I2)) - print(evaluate(I2)) - # ... - - # ... - expr = F - cos(2*pi*x)*cos(3*pi*y) - expr = dot(grad(expr), grad(expr)) - I2 = Integral(expr, domain, name='I2') - - print(I2) - print(atomize(I2)) - print(evaluate(I2)) - # ... - - # ... stokes - V = VectorFunctionSpace('V', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(V, name='v') - u = VectorTestFunction(V, name='u') - p = TestFunction(W, name='p') - q = TestFunction(W, name='q') - - a = BilinearForm((v,u), inner(grad(v), grad(u)), name='a') - b = BilinearForm((v,p), div(v)*p, name='b') - A = BilinearForm(((v,q),(u,p)), a(v,u) - b(v,p) + b(u,q), name='A') - - print(A) - print(atomize(A)) - print(evaluate(A)) - # ... - -#============================================================================== -def test_projection_2d(): - domain = Domain('Omega', dim=DIM) - - V = FunctionSpace('V', domain) - x,y = domain.coordinates - - alpha = Constant('alpha') - - u = Projection(x**2+alpha*y, V, name='u') - - -#============================================================================== -def test_norm_2d(): - domain = Domain('Omega', dim=DIM) - - x,y = domain.coordinates - V = FunctionSpace('V', domain) - F = Field('F', V) - - # ... - expr = x*y - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - # ... - expr = sin(pi*x)*sin(pi*y) - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - # ... - expr = F-x*y - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - # ... - expr = F-sin(pi*x)*sin(pi*y) - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - # ... - expr = F-sin(0.5*pi*(1.-x))*sin(pi*y) - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - # ... - expr = F-cos(0.5*pi*x)*sin(pi*y) - l2_norm_u = Norm(expr, domain, kind='l2') - h1_norm_u = Norm(expr, domain, kind='h1') - - print('> l2 norm = ', evaluate(l2_norm_u)) - print('> h1 norm = ', evaluate(h1_norm_u)) - print('') - # ... - - -#============================================================================== -def test_vector_2d_1(): - domain = Domain('Omega', dim=DIM) - - W1 = VectorFunctionSpace('W1', domain) - T1 = VectorFunctionSpace('T1', domain) - - w1 = VectorTestFunction(W1, name='w1') - t1 = VectorTestFunction(T1, name='t1') - - x,y = W1.coordinates - - F = VectorField(W1, 'F') - -# # ... -# l1 = LinearForm(w1, dot(w1, F), name='l1') -# print(l1) -# print(atomize(l1)) -# print(evaluate(l1)) -# print('') -# # ... -# -# # ... -# l2 = LinearForm(w1, rot(w1)*rot(F) + div(w1)*div(F), name='l2') -# print(l2) -# print(atomize(l2)) -# print(evaluate(l2)) -# print('') -# # ... - - # ... - f = Tuple(sin(pi*x)*sin(pi*y), sin(pi*x)*sin(pi*y)) - error = Matrix([F[0]-f[0], F[1]-f[1]]) - l2_norm = Norm(error, domain, kind='l2') - print(l2_norm) - print(atomize(l2_norm)) - print(evaluate(l2_norm)) - print('') - # ... - - # ... - f = Tuple(sin(pi*x)*sin(pi*y), sin(pi*x)*sin(pi*y)) - error = Matrix([F[0]-f[0], F[1]-f[1]]) - h1_norm = Norm(error, domain, kind='h1') - print(h1_norm) - print(atomize(h1_norm)) - print(evaluate(h1_norm)) - print('') - # ... - -#============================================================================== -def test_expr_mapping_2d(): - - F = Mapping('F', DIM) - patch = Domain('Omega', dim=DIM) - domain = F(patch) - - V = FunctionSpace('V', domain) - v = TestFunction(V, name='v') - u = TestFunction(V, name='u') - - x,y = V.coordinates - - a = BilinearForm((v,u), dot(grad(v), grad(u))) - assert(a.mapping is F) - - l = LinearForm(v, x*y*v) - assert(l.mapping is F) - -#============================================================================== -def test_system_2d(): - - domain = Square() - - V = FunctionSpace('V', domain) - x,y = V.coordinates - - u,v,p,q = [TestFunction(V, name=i) for i in ['u','v','p','q']] - - a1,a2,b1,b2 = [Constant(i, real=True) for i in ['a1','a2','b1','b2']] - - # ... - a = BilinearForm((v,u), dot(grad(u), grad(v))) - m = BilinearForm((v,u), u*v) - - expr = a(p,u) + a1*m(p,u) + b1*m(p,v) + a(q,v) + a2*m(q,u) + b2*m(q,v) - b = BilinearForm(((p,q), (u,v)), expr) - - print(evaluate(b, verbose=True)) - # ... - - # ... - f1 = x*y - f2 = x+y - l1 = LinearForm(p, f1*p) - l2 = LinearForm(q, f2*q) - - expr = l1(p) + l2(q) - l = LinearForm((p,q), expr) - - print(evaluate(l, verbose=True)) - # ... - -#============================================================================== -def test_curldiv_2d(): - - domain = Square() - - W1 = VectorFunctionSpace('W1', domain) - T1 = VectorFunctionSpace('T1', domain) - - w1 = VectorTestFunction(W1, name='w1') - t1 = VectorTestFunction(T1, name='t1') - - mu = Constant('mu') - - # ... - a = BilinearForm((w1, t1), rot(w1)*rot(t1) + mu*div(w1)*div(t1), name='a') - print(a) - print(atomize(a)) - print(evaluate(a)) - # ... - - -#============================================================================== -def test_calls_2d_2(): - - domain = Square() - - V = FunctionSpace('V', domain) - x,y = V.coordinates - - u,v = [TestFunction(V, name=i) for i in ['u','v']] - Un = Field('Un', V) - - # ... - a = BilinearForm((v,u), dot(grad(u), grad(v))) - - expr = a(v, Un) - print(evaluate(expr, verbose=True)) - # ... - - # ... - l = LinearForm(v, a(v, Un)) - - print(evaluate(l, verbose=True)) - # ... - -#============================================================================== -def test_calls_2d_3(): - - domain = Square() - - V = FunctionSpace('V', domain) - - x,y = domain.coordinates - - pn = Field('pn', V) - wn = Field('wn', V) - - dp = TestFunction(V, name='dp') - dw = TestFunction(V, name='dw') - tau = TestFunction(V, name='tau') - sigma = TestFunction(V, name='sigma') - - Re = Constant('Re', real=True) - dt = Constant('dt', real=True) - alpha = Constant('alpha', real=True) - - l1 = LinearForm(tau, bracket(pn, wn)*tau - 1./Re * dot(grad(tau), grad(wn))) - - # ... - l = LinearForm((tau, sigma), dt*l1(tau)) - - print(evaluate(l, verbose=True)) - # ... - -#============================================================================== -def test_evaluation_2d_1(): - domain = Domain('Omega', dim=2) - B_neumann = Boundary(r'\Gamma_1', domain) - - V = FunctionSpace('V', domain) - W = VectorFunctionSpace('W', domain) - - p,q = [TestFunction(V, name=i) for i in ['p', 'q']] - u,v = [VectorTestFunction(W, name=i) for i in ['u', 'v']] - - alpha = Constant('alpha') - - x,y = V.coordinates - F = Field('F', space=V) - - a1 = BilinearForm((p, q), dot(grad(p), grad(q))) - m = BilinearForm((p, q), p*q) - a2 = BilinearForm((p, q), a1(p,q) + alpha*m(p,q)) - a3 = BilinearForm((u, v), rot(u)*rot(v) + alpha*div(u)*div(v)) - - a11 = BilinearForm((v,u), inner(grad(v), grad(u))) - a12 = BilinearForm((v,p), div(v)*p) - a4 = BilinearForm(((v,q),(u,p)), a11(v,u) - a12(v,p) + a12(u,q)) - - l0 = LinearForm(p, F*p) - l_neu = LinearForm(p, p*trace_1(grad(F), B_neumann)) - l = LinearForm(p, l0(p) + l_neu(p)) - - # ... - print(a1) - print(evaluate(a1)) - print('') - # ... - - # ... - print(a2) - print(evaluate(a2)) - print('') - # ... - - # ... - print(a3) - print(evaluate(a3)) - print('') - # ... - - # ... - print(a4) - print(evaluate(a4)) - print('') - # ... - - # ... - print(l) - print(evaluate(l)) - print('') - # ... - -#============================================================================== -def test_evaluation_2d_2(): - domain = Square() - x,y = domain.coordinates - - f0 = Tuple(2*pi**2*sin(pi*x)*sin(pi*y), - 2*pi**2*sin(pi*x)*sin(pi*y)) - - f1 = cos(pi*x)*cos(pi*y) - - W = VectorFunctionSpace('W', domain) - V = FunctionSpace('V', domain) - X = ProductSpace(W, V) - - # TODO improve: naming are not given the same way - F = VectorField(W, name='F') - G = Field('G', V) - - u,v = [VectorTestFunction(W, name=i) for i in ['u', 'v']] - p,q = [ TestFunction(V, name=i) for i in ['p', 'q']] - - a0 = BilinearForm((v,u), inner(grad(v), grad(u))) - a1 = BilinearForm((q,p), p*q) - a = BilinearForm(((v,q),(u,p)), a0(v,u) + a1(q,p)) - - l0 = LinearForm(v, dot(f0, v)) - l1 = LinearForm(q, f1*q) - l = LinearForm((v,q), l0(v) + l1(q)) - - # ... - print(a) - print(evaluate(a)) - print('') - # ... - - # ... - print(l) - print(evaluate(l)) - print('') - # ... - -#============================================================================== -def test_linearity_2d_1(): - domain = Square() - x,y = domain.coordinates - - alpha = Constant('alpha') - - f1 = x*y - f2 = x+y - f = Tuple(f1, f2) - - V = FunctionSpace('V', domain) - - # TODO improve: naming are not given the same way - G = Field('G', V) - - p,q = [TestFunction(V, name=i) for i in ['p', 'q']] - - ##################################### - # linear expressions - ##################################### - # ... - expr = p - assert(is_linear_form(expr, p)) - # ... - - # ... - expr = alpha*p - assert(is_linear_form(expr, p)) - # ... - - # ... - expr = dx(p) - assert(is_linear_form(expr, p)) - # ... - - # ... - expr = dot(grad(p), f) - assert(is_linear_form(expr, p)) - # ... - - # ... - expr = laplace(p) - assert(is_linear_form(expr, p)) - # ... - - # ... - expr = alpha*p + dot(grad(p), f) + dx(p) + laplace(p) - assert(is_linear_form(expr, p)) - # ... - ##################################### - - ##################################### - # nonlinear expressions - ##################################### - # ... - with pytest.raises(UnconsistentLinearExpressionError): - expr = p**2 - is_linear_form(expr, p) - # ... - - # ... - with pytest.raises(UnconsistentLinearExpressionError): - expr = dot(grad(p), grad(p)) - is_linear_form(expr, p) - # ... - ##################################### - -# expr = dot(grad(p), grad(p)) -# print(is_linear_form(expr, p)) - - -#============================================================================== -def test_bilinearity_2d_1(): - domain = Square() - x,y = domain.coordinates - - alpha = Constant('alpha') - beta = Constant('beta') - - f1 = x*y - f2 = x+y - f = Tuple(f1, f2) - - V = FunctionSpace('V', domain) - - # TODO improve: naming are not given the same way - G = Field('G', V) - - p,q = [TestFunction(V, name=i) for i in ['p', 'q']] - - ##################################### - # linear expressions - ##################################### - # ... - expr = p*q - assert(is_bilinear_form(expr, (p,q))) - # ... - - # ... - expr = dot(grad(p), grad(q)) - assert(is_bilinear_form(expr, (p,q))) - # ... - - # ... - expr = alpha*dot(grad(p), grad(q)) + beta*p*q + laplace(p)*laplace(q) - assert(is_bilinear_form(expr, (p,q))) - # ... - ##################################### - - ##################################### - # nonlinear expressions - ##################################### - # ... - with pytest.raises(UnconsistentLinearExpressionError): - expr = alpha*dot(grad(p**2), grad(q)) + beta*p*q - is_bilinear_form(expr, (p,q)) - # ... - ##################################### - -# expr = p*q -# print(is_bilinear_form(expr, (p,q))) - -#============================================================================== -def test_linearity_2d_2(): - domain = Domain('Omega', dim=DIM) - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - W1 = VectorFunctionSpace('W1', domain) - W2 = VectorFunctionSpace('W2', domain) - T1 = VectorFunctionSpace('T1', domain) - T2 = VectorFunctionSpace('T2', domain) - - v1 = TestFunction(V1, name='v1') - v2 = TestFunction(V2, name='v2') - u1 = TestFunction(U1, name='u1') - u2 = TestFunction(U2, name='u2') - w1 = VectorTestFunction(W1, name='w1') - w2 = VectorTestFunction(W2, name='w2') - t1 = VectorTestFunction(T1, name='t1') - t2 = VectorTestFunction(T2, name='t2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - x,y = V1.coordinates - - alpha = Constant('alpha') - - F = Field('F', space=V1) - - # ... - l1 = LinearForm(v1, x*y*v1, check=True) - - l = LinearForm(v2, l1(v2), check=True) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, check=True) - l2 = LinearForm(v2, cos(x+y)*v2, check=True) - - l = LinearForm((u1,u2), l1(u1) + l2(u2), check=True) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, check=True) - l2 = LinearForm(v2, cos(x+y)*v2, check=True) - - l = LinearForm((u1,u2), l1(u1) + alpha * l2(u2), check=True) - # ... - - # ... - l1 = LinearForm(v1, x*y*v1, check=True) - l2 = LinearForm(w1, div(w1), check=True) - - l = LinearForm((v2,w2), l1(v2) + l2(w2), check=True) - # ... - - ################################ - # non bilinear forms - ################################ - # ... - with pytest.raises(UnconsistentLinearExpressionError): - l = LinearForm(v1, x*y*v1**2, check=True) - # ... - - # ... - with pytest.raises(UnconsistentLinearExpressionError): - l = LinearForm(v1, x*y, check=True) - # ... - ################################ - -#============================================================================== -def test_bilinearity_2d_2(): - domain = Domain('Omega', dim=DIM) - - V1 = FunctionSpace('V1', domain) - V2 = FunctionSpace('V2', domain) - U1 = FunctionSpace('U1', domain) - U2 = FunctionSpace('U2', domain) - W1 = VectorFunctionSpace('W1', domain) - W2 = VectorFunctionSpace('W2', domain) - T1 = VectorFunctionSpace('T1', domain) - T2 = VectorFunctionSpace('T2', domain) - - v1 = TestFunction(V1, name='v1') - v2 = TestFunction(V2, name='v2') - u1 = TestFunction(U1, name='u1') - u2 = TestFunction(U2, name='u2') - w1 = VectorTestFunction(W1, name='w1') - w2 = VectorTestFunction(W2, name='w2') - t1 = VectorTestFunction(T1, name='t1') - t2 = VectorTestFunction(T2, name='t2') - - V = ProductSpace(V1, V2) - U = ProductSpace(U1, U2) - - x,y = V1.coordinates - - alpha = Constant('alpha') - - F = Field('F', space=V1) - - # ... - a1 = BilinearForm((v1, u1), u1*v1, check=True) - a = BilinearForm((v2, u2), a1(v2, u2), check=True) - # ... - - # ... - a = BilinearForm((v1, u1), dot(grad(v1), grad(u1)), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), dot(grad(v1), grad(u1)), check=True) - a = BilinearForm((v2, u2), a1(v2, u2), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1) - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), check=True) - a = BilinearForm((v2, u2), a1(v2, u2) + a2(v2, u2), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, check=True) - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, check=True) - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), check=True) - a = BilinearForm(((v1,v2),(u1,u2)), a1(v1, u2) + a2(v2, u1), check=True) - # ... - - # ... - a = BilinearForm((w1, t1), rot(w1)*rot(t1) + div(w1)*div(t1), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), u1*v1, check=True) - a2 = BilinearForm((v1, u1), dx(u1)*dx(v1), check=True) - a3 = BilinearForm((w1, t1), rot(w1)*rot(t1) + div(w1)*div(t1), check=True) - a4 = BilinearForm((w1, u1), div(w1)*u1, check=True) - - a = BilinearForm(((w2,v2),(t2,u2)), a3(w2,t2) + a2(v2,u2) + a4(w2,u2), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), laplace(u1)*laplace(v1), check=True) - # ... - - # ... - a1 = BilinearForm((v1, u1), inner(hessian(u1),hessian(v1)), check=True) - # ... - - # ... stokes - V = VectorFunctionSpace('V', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(V, name='v') - u = VectorTestFunction(V, name='u') - p = TestFunction(W, name='p') - q = TestFunction(W, name='q') - - a = BilinearForm((v,u), inner(grad(v), grad(u)), check=True) - b = BilinearForm((v,p), div(v)*p, check=True) - A = BilinearForm(((v,q),(u,p)), a(v,u) - b(v,p) + b(u,q), check=True) - # ... - - ################################ - # non bilinear forms - ################################ - # ... - with pytest.raises(UnconsistentLinearExpressionError): - a = BilinearForm((v1, u1), dot(grad(v1), grad(u1)) + v1, check=True) - # ... - - # ... - with pytest.raises(UnconsistentLinearExpressionError): - a = BilinearForm((v1, u1), v1**2*u1, check=True) - # ... - - # ... - with pytest.raises(UnconsistentLinearExpressionError): - a = BilinearForm((v1, u1), dot(grad(v1), grad(v1)), check=True) - # ... - ################################ - -#============================================================================== -#def test_nonlinear_2d_1(): -# -# domain = Square() -# -# V = FunctionSpace('V', domain) -# x,y = V.coordinates -# -# u,v = [TestFunction(V, name=i) for i in ['u','v']] -# Un = Field('Un', V) -# -# from sympy import diff as sympy_diff -# -# def diff(L, u): -# ls = [] -# expr = evaluate(L) -# for i in expr: -# e = sympy_diff(i.expr, u) -# ls.append(e) -# -# if len(ls) == 1: -# return ls[0] -# -# else: -# return ls -# -# # ... -# expr = Un**2 * dot(grad(Un), grad(v)) -# l = LinearForm(v, expr) -# -# a = diff(l, Un) -# print(a) -# # ... - -#============================================================================== -def test_linearize_2d_1(): - domain = Domain('Omega', dim=DIM) - x,y = domain.coordinates - - V1 = FunctionSpace('V1', domain) - W1 = VectorFunctionSpace('W1', domain) - - v1 = TestFunction(V1, name='v1') - w1 = VectorTestFunction(W1, name='w1') - - alpha = Constant('alpha') - - F = Field('F', space=V1) - G = VectorField(W1, 'G') - - # ... - l = LinearForm(v1, F**2*v1, check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(v1, dot(grad(F), grad(F))*v1, check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(v1, exp(-F)*v1, check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(v1, cos(F)*v1, check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(v1, cos(F**2)*v1, check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(v1, F**2*dot(grad(F), grad(v1)), check=True) - a = linearize(l, F, trials='u1') - print(a) - # ... - - # ... - l = LinearForm(w1, dot(rot(G), grad(G))*w1, check=True) - a = linearize(l, G, trials='u1') - print(a) - # ... - -#============================================================================== -def test_linearize_2d_2(): - domain = Domain('Omega', dim=DIM) - x,y = domain.coordinates - - V1 = FunctionSpace('V1', domain) - - v1 = TestFunction(V1, name='v1') - - alpha = Constant('alpha') - - F = Field('F', space=V1) - G = Field('G', space=V1) - - # ... - l1 = LinearForm(v1, F**2*v1, check=True) - l = LinearForm(v1, l1(v1)) - - a = linearize(l, F, trials='u1') - print(a) - - expected = linearize(l1, F, trials='u1') - assert( linearize(l, F, trials='u1') == expected ) - # ... - -#============================================================================== -def test_linearize_2d_3(): - """steady Euler equation.""" - domain = Domain('Omega', dim=DIM) - x,y = domain.coordinates - - U = VectorFunctionSpace('U', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(U, name='v') - phi = TestFunction(W, name='phi') - q = TestFunction(W, name='q') - - U_0 = VectorField(U, name='U_0') - Rho_0 = Field('Rho_0', W) - P_0 = Field('P_0', W) - - # ... - expr = div(Rho_0*U_0) * phi - l1 = LinearForm(phi, expr, check=True) - - expr = Rho_0*dot(convect(U_0, grad(U_0)), v) + dot(grad(P_0), v) - l2 = LinearForm(v, expr, check=True) - - expr = dot(U_0, grad(P_0)) * q + P_0 * div(U_0) * q - l3 = LinearForm(q, expr, check=True) - # ... - - a1 = linearize(l1, [Rho_0, U_0], trials=['d_rho', 'd_u']) - print(a1) - print('') - - a2 = linearize(l2, [Rho_0, U_0, P_0], trials=['d_rho', 'd_u', 'd_p']) - print(a2) - print('') - - a3 = linearize(l3, [P_0, U_0], trials=['d_p', 'd_u']) - print(a3) - print('') - - l = LinearForm((phi, v, q), l1(phi) + l2(v) + l3(q)) - a = linearize(l, [Rho_0, U_0, P_0], trials=['d_rho', 'd_u', 'd_p']) - print(a) - -#============================================================================== -def test_area_2d_1(): - - domain = Domain('Omega', dim=2) - x,y = domain.coordinates - - mu = Constant('mu' , is_real=True) - - e = ElementDomain(domain) - area = Area(e) - - V = FunctionSpace('V', domain) - - u,v = [TestFunction(V, name=i) for i in ['u', 'v']] - - # ... - a = BilinearForm((v,u), area * u * v) - # ... - -#============================================================================== -def test_stabilization_2d_1(): - - domain = Domain('Omega', dim=2) - x,y = domain.coordinates - - kappa = Constant('kappa', is_real=True) - mu = Constant('mu' , is_real=True) - - b1 = 1. - b2 = 0. - b = Tuple(b1, b2) - - # right hand side - f = x*y - - e = ElementDomain() - area = Area(e) - - V = FunctionSpace('V', domain) - - u,v = [TestFunction(V, name=i) for i in ['u', 'v']] - - # ... - expr = kappa * dot(grad(u), grad(v)) + dot(b, grad(u)) * v - a = BilinearForm((v,u), expr) - # ... - - # ... - expr = f * v - l = LinearForm(v, expr) - # ... - - # ... - expr = (- kappa * laplace(u) + dot(b, grad(u))) * dot(b, grad(v)) - s1 = BilinearForm((v,u), expr) - - expr = - f * dot(b, grad(v)) - l1 = LinearForm(v, expr) - # ... - - # ... - expr = (- kappa * laplace(u) + dot(b, grad(u))) * ( dot(b, grad(v)) - kappa * laplace(v)) - s2 = BilinearForm((v,u), expr) - - expr = - f * ( dot(b, grad(v)) - kappa * laplace(v)) - l2 = LinearForm(v, expr) - # ... - - # ... - expr = (- kappa * laplace(u) + dot(b, grad(u))) * ( dot(b, grad(v)) + kappa * laplace(v)) - s3 = BilinearForm((v,u), expr) - - expr = - f * ( dot(b, grad(v)) + kappa * laplace(v)) - l3 = LinearForm(v, expr) - # ... - - # ... - expr = a(v,u) + mu*area*s1(v,u) - a1 = BilinearForm((v,u), expr) - # ... - - # ... - expr = a(v,u) + mu*area*s2(v,u) - a2 = BilinearForm((v,u), expr) - # ... - - # ... - expr = a(v,u) + mu*area*s3(v,u) - a3 = BilinearForm((v,u), expr) - # ... - - print(a1) - print(evaluate(a1, verbose=True)) - print('') - - print(a2) - print(evaluate(a2, verbose=True)) - print('') - - print(a3) - print(evaluate(a3, verbose=True)) - print('') - -#============================================================================== -def test_user_function_2d_1(): - - domain = Domain('Omega', dim=2) - x,y = domain.coordinates - - kappa = Constant('kappa', is_real=True) - mu = Constant('mu' , is_real=True) - - # right hand side - f = Function('f') - - V = FunctionSpace('V', domain) - - u,v = [TestFunction(V, name=i) for i in ['u', 'v']] - - # ... - expr = dot(grad(u), grad(v)) + f(x,y) * u * v - a = BilinearForm((v,u), expr) - - print(a) - print(evaluate(a, verbose=True)) - print('') - # ... - - # ... - expr = f(x,y) * v - l = LinearForm(v, expr) - - print(l) - print(evaluate(l, verbose=True)) - print('') - # ... - -#============================================================================== -# CLEAN UP SYMPY NAMESPACE -#============================================================================== - -def teardown_module(): - from sympy import cache - cache.clear_cache() - -def teardown_function(): - from sympy import cache - cache.clear_cache() - -#test_user_function_2d_1() - -#test_area_2d_1() -#test_stabilization_2d_1() - -#test_linearize_2d_1() -#test_linearize_2d_2() -#test_linearize_2d_3() - -#test_linearity_2d_1() -#test_linearity_2d_2() -#test_bilinearity_2d_1() -#test_bilinearity_2d_2() - -#test_boundary_2d_1() -#test_boundary_2d_2() -#test_projection_2d() -#test_norm_2d() -#test_vector_2d_1() -#test_expr_mapping_2d() -#test_system_2d() -#test_curldiv_2d() -#test_calls_2d() -#test_calls_2d_2() -#test_calls_2d_3() -#test_evaluation_2d_1() -#test_evaluation_2d_2() diff --git a/old/test_form_3d.py b/old/test_form_3d.py deleted file mode 100644 index 575bd92b..00000000 --- a/old/test_form_3d.py +++ /dev/null @@ -1,110 +0,0 @@ -# coding: utf-8 - -# TODO - split the asserts between algebraic and weak formulations ones -# - add assert for grad in vector case -# TODO: - __call__ examples are not working anymore - -import pytest - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin -from sympy import srepr -from sympy.physics.quantum import TensorProduct - -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, cross, rot, curl, div -from sympde.calculus import laplace, hessian -from sympde.topology import (dx, dy, dz) -from sympde.topology import FunctionSpace, VectorFunctionSpace -from sympde.topology import Field, VectorField -from sympde.topology import ProductSpace -from sympde.topology import TestFunction -from sympde.topology import VectorTestFunction -from sympde.topology import Unknown -from sympde.topology import Domain, Boundary, NormalVector, TangentVector -from sympde.topology import Trace, trace_0, trace_1 - -from sympde.expr import BilinearForm, LinearForm, Integral -from sympde.expr import atomize -from sympde.expr import evaluate -from sympde.expr import tensorize -from sympde.expr import Mass, Stiffness, Advection, AdvectionT -from sympde.expr import Projection -from sympde.expr import Norm -from sympde.expr import FormCall - -from sympde.expr.errors import UnconsistentError -from sympde.expr.errors import UnconsistentLhsError -from sympde.expr.errors import UnconsistentRhsError -from sympde.expr.errors import UnconsistentBCError - - -DIM = 3 -domain = Domain('Omega', dim=DIM) - - -#============================================================================== -def test_tensorize_3d(): - - V = FunctionSpace('V', domain) - U = FunctionSpace('U', domain) - W1 = VectorFunctionSpace('W1', domain) - T1 = VectorFunctionSpace('T1', domain) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - w1 = VectorTestFunction(W1, name='w1') - t1 = VectorTestFunction(T1, name='t1') - - x,y,z = domain.coordinates - - alpha = Constant('alpha') - - # ... - expr = dot(grad(v), grad(u)) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... - expr = x*dx(v)*dx(u) + y*dy(v)*dy(u) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... - expr = sin(x)*dx(v)*dx(u) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... - expr = dot(curl(w1), curl(t1)) + div(w1)*div(t1) - a = BilinearForm((w1, t1), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - -#============================================================================== -# CLEAN UP SYMPY NAMESPACE -#============================================================================== - -def teardown_module(): - from sympy import cache - cache.clear_cache() - -def teardown_function(): - from sympy import cache - cache.clear_cache() diff --git a/old/test_tensorize_2d.py b/old/test_tensorize_2d.py deleted file mode 100644 index ab7cb485..00000000 --- a/old/test_tensorize_2d.py +++ /dev/null @@ -1,136 +0,0 @@ -# coding: utf-8 - -# TODO - tensorize is not working due to the last changes on calculus - -import pytest - -from sympy import Symbol -from sympy.core.containers import Tuple -from sympy import symbols -from sympy import IndexedBase -from sympy import Matrix -from sympy import Function -from sympy import pi, cos, sin -from sympy import srepr -from sympy.physics.quantum import TensorProduct - -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, cross, rot, curl, div -from sympde.calculus import laplace, hessian, bracket -from sympde.topology import (dx, dy, dz) -from sympde.topology import FunctionSpace, VectorFunctionSpace -from sympde.topology import Field, VectorField -from sympde.topology import ProductSpace -from sympde.topology import TestFunction -from sympde.topology import VectorTestFunction -from sympde.topology import Unknown -from sympde.topology import InteriorDomain, Union -from sympde.topology import Boundary, NormalVector, TangentVector -from sympde.topology import Domain -from sympde.topology import Trace, trace_0, trace_1 -from sympde.topology import Mapping -from sympde.topology import Square - -from sympde.expr import BilinearForm, LinearForm, Integral -from sympde.expr import atomize -from sympde.expr import evaluate -from sympde.expr import tensorize -from sympde.expr import Mass, Stiffness, Advection, AdvectionT -from sympde.expr import Projection -from sympde.expr import Norm -from sympde.expr import FormCall - -from sympde.expr.errors import UnconsistentError -from sympde.expr.errors import UnconsistentLhsError -from sympde.expr.errors import UnconsistentRhsError -from sympde.expr.errors import UnconsistentBCError - -DIM = 2 -VERBOSE = False -VERBOSE = True - -#============================================================================== -def test_tensorize_2d(): - domain = Domain('Omega', dim=DIM) - - V = FunctionSpace('V', domain) - U = FunctionSpace('U', domain) - W1 = VectorFunctionSpace('W1', domain) - T1 = VectorFunctionSpace('T1', domain) - - v = TestFunction(V, name='v') - u = TestFunction(U, name='u') - w1 = VectorTestFunction(W1, name='w1') - t1 = VectorTestFunction(T1, name='t1') - - x,y = domain.coordinates - - alpha = Constant('alpha') - - # ... - expr = dot(grad(v), grad(u)) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... - expr = x*dx(v)*dx(u) + y*dy(v)*dy(u) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... - expr = sin(x)*dx(v)*dx(u) - a = BilinearForm((v,u), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - - # ... -# expr = rot(w1)*rot(t1) + div(w1)*div(t1) - expr = rot(w1)*rot(t1) #+ div(w1)*div(t1) - a = BilinearForm((w1, t1), expr, name='a') - print(a) - print(tensorize(a)) - print('') - # ... - -#============================================================================== -def test_tensorize_2d_stokes(): - domain = Domain('Omega', dim=DIM) - - # ... abstract model - V = VectorFunctionSpace('V', domain) - W = FunctionSpace('W', domain) - - v = VectorTestFunction(V, name='v') - u = VectorTestFunction(V, name='u') - p = TestFunction(W, name='p') - q = TestFunction(W, name='q') - - a = BilinearForm((v,u), inner(grad(v), grad(u)), name='a') - b = BilinearForm((v,p), div(v)*p, name='b') - A = BilinearForm(((v,q),(u,p)), a(v,u) - b(v,p) + b(u,q), name='A') - # ... - - print(A) - print(tensorize(A)) - print('') - - -#============================================================================== -# CLEAN UP SYMPY NAMESPACE -#============================================================================== - -def teardown_module(): - from sympy import cache - cache.clear_cache() - -def teardown_function(): - from sympy import cache - cache.clear_cache() diff --git a/pyproject.toml b/pyproject.toml index e45220c4..79de87b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 64.0", "wheel"] +requires = ["setuptools >= 77.0", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,25 +8,43 @@ version = "0.20.0" description = "Symbolic calculus for partial differential equations (and variational forms)" readme = "README.rst" requires-python = ">= 3.9" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] authors = [{name = "Ahmed Ratnani", email = "ratnaniahmed@gmail.com"}] maintainers = [ {name = "Yaman Güçlü", email = "yaman.guclu@gmail.com"}, {name = "Said Hadjout"}, ] -keywords = ["math"] -classifiers = ["Programming Language :: Python :: 3"] +keywords = ["finite elements", "partial differential equations", "symbolic mathematics"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Mathematics", +] dependencies = [ - 'sympy >= 1.5, < 1.10', + 'sympy >= 1.12, < 1.15', 'h5py', - 'pytest', - 'pytest-xdist', 'pyyaml', 'numpy', 'matplotlib' ] +[project.optional-dependencies] +test = [ + "pytest >= 8", + "pytest-cov >= 5", + "pytest-xdist >= 3", +] +docs = [ + "sphinx >= 7", + "sphinxcontrib-bibtex >= 2.6", +] + [project.urls] +Documentation = "https://pyccel.github.io/sympde/" +Issues = "https://github.com/pyccel/sympde/issues" Repository = "https://github.com/pyccel/sympde" [tool.setuptools.packages.find] @@ -35,3 +53,27 @@ namespaces = false [tool.setuptools.package-data] "*" = ["README.rst"] + +[tool.pytest.ini_options] +minversion = "8.0" +addopts = ["--strict-markers"] +python_files = ["test_*.py"] +python_classes = [] +python_functions = ["test_*"] + +[tool.coverage.run] +branch = true +source = ["sympde"] +omit = ["*/tests/*"] + +[tool.coverage.report] +ignore_errors = true +exclude_also = [ + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if False:", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a1500aee..00000000 --- a/pytest.ini +++ /dev/null @@ -1,8 +0,0 @@ -# this file shows to pytest what to collect -# here we avoid collecting test classes (which are not used in SPL) -# this is to avoid getting the warning on TestFunction -# TODO can we exlude TestFunction from python_classes pattern? -[pytest] -python_files = test_*.py -python_classes = -python_functions = test_* diff --git a/sympde/__init__.py b/sympde/__init__.py index f7fc8041..e69de29b 100644 --- a/sympde/__init__.py +++ b/sympde/__init__.py @@ -1,7 +0,0 @@ -from .version import __version__ -from .old_sympy_utilities import with_metaclass -from .core import * -from .topology import * -from .exterior import * -from .printing import * -from .utilities import * diff --git a/sympde/api/__init__.py b/sympde/api/__init__.py new file mode 100644 index 00000000..515eac95 --- /dev/null +++ b/sympde/api/__init__.py @@ -0,0 +1,539 @@ +"""Public user-facing API for SymPDE. + +Objects are imported from the modules that define them instead of relying on +the wildcard re-exports in the package initializers. This module is the one +supported location for convenient, flat imports such as:: + + from sympde.api import BilinearForm, Square, elements_of, grad + +Internal SymPDE code should continue to import objects from their defining +modules to keep its dependencies explicit. +""" + +from sympde.version import __version__ + +from sympde.core.basic import Constant + +from sympde.calculus.core import ( + Average, + BasicOperator, + BasicOperatorAdd, + Bracket, + Convect, + Convolution, + Cross, + Curl, + D, + DiffOperator, + Div, + Dn, + Dot, + Grad, + Hessian, + Inner, + Jump, + Laplace, + MinusInterfaceOperator, + NormalDerivative, + Outer, + PlusInterfaceOperator, + Rot, + StrainTensor, + add_basicop, + avg, + bracket, + conv, + convect, + cross, + curl, + div, + dot, + grad, + has, + hessian, + inner, + is_constant, + is_scalar, + is_zero, + jump, + laplace, + minus, + outer, + plus, + rot, +) +from sympde.calculus.errors import ArgumentTypeError, CalculusError +from sympde.calculus.matrices import ( + Inverse, + MatrixElement, + MatrixSymbolicExpr, + SymbolicDeterminant, + SymbolicTrace, + Transpose, +) + +from sympde.topology.basic import ( + BasicDomain, + Boundary, + Connectivity, + CornerBoundary, + CornerInterface, + Edge, + Interface, + InteriorDomain, + Interval, + ProductDomain, + Union, +) +from sympde.topology.domain import ( + Area, + BoundaryVector, + Cube, + Domain, + ElementDomain, + Line, + MinusNormalVector, + NCube, + NCubeInterior, + NormalVector, + PeriodicDomain, + PlusNormalVector, + Square, + TangentVector, + split, +) +from sympde.topology.mapping import ( + BasicCallableMapping, + Contravariant, + Covariant, + InterfaceMapping, + InverseMapping, + Jacobian, + JacobianInverseSymbol, + JacobianSymbol, + LogicalExpr, + MappedDomain, + Mapping, + MappingApplication, + MultiPatchMapping, + PullBack as MappingPullBack, + SymbolicExpr, + SymbolicWeightedVolume, + get_logical_test_function, +) +from sympde.topology.space import ( + BasicFunctionSpace, + Derham, + IndexedVectorFunction, + ProductSpace, + Projection, + Projector, + ScalarFunction, + ScalarFunctionSpace, + TestFunction, + Trace, + VectorFunction, + VectorFunctionSpace, + element_of, + elements_of, + trace_0, + trace_1, +) +from sympde.topology.derivatives import ( + DifferentialOperator, + dx, + dx1, + dx2, + dx3, + dy, + dz, + find_partial_derivatives, + get_atom_derivatives, + get_atom_logical_derivatives, + get_index_derivatives, + get_index_derivatives_atom, + get_index_logical_derivatives, + get_index_logical_derivatives_atom, + get_max_logical_partial_derivatives, + get_max_partial_derivatives, + get_number_derivatives, + sort_partial_derivatives, +) +from sympde.topology.datatype import ( + H1Regularity, + H1RegularityType, + H1Space, + H1SpaceType, + HcurlRegularity, + HcurlRegularityType, + HcurlSpace, + HcurlSpaceType, + HdivRegularity, + HdivRegularityType, + HdivSpace, + HdivSpaceType, + L2Regularity, + L2RegularityType, + L2Space, + L2SpaceType, + RegularityType, + SpaceType, + UndefinedSpace, + UndefinedSpaceType, +) +from sympde.topology.analytical_mapping import ( + AffineMapping, + CollelaMapping2D, + CzarnyMapping, + IdentityMapping, + PolarMapping, + SphericalMapping, + TargetMapping, + TorusMapping, + TorusSurfaceMapping, + TwistedTargetMapping, + TwistedTargetSurfaceMapping, +) +from sympde.topology.callable_mapping import CallableMapping +from sympde.topology.measure import ( + BasicMeasure, + CanonicalMeasure, + CartesianMeasure, + Measure, +) + +from sympde.expr.basic import BasicExpr, BasicForm +from sympde.expr.equation import ( + Equation, + EssentialBC, + Mean, + NewtonIteration, + find, +) +from sympde.expr.errors import ( + UnconsistentArgumentsError, + UnconsistentBCError, + UnconsistentError, + UnconsistentLhsError, + UnconsistentLinearExpressionError, + UnconsistentRhsError, +) +from sympde.expr.evaluation import ( + BoundaryExpression, + DomainExpression, + InterfaceExpression, + KernelExpression, + TensorExpr, + TerminalExpr, +) +from sympde.expr.expr import ( + BilinearForm, + Functional, + IntAdd, + Integral, + LinearExpr, + LinearForm, + Norm, + SemiNorm, + integral, + is_linear_expression, + linearize, +) + +from sympde.exterior.calculus import ( + Adjoint, + AdjointExteriorDerivative, + AdjointInteriorProduct, + AdjointLieDerivative, + ExteriorDerivative, + ExteriorProduct, + Hodge, + InteriorProduct, + Ld, + LieDerivative, + PullBack, + d, + delta, + hodge, + ip, + jp, + ld, + wedge, +) +from sympde.exterior.compiler import ExteriorCalculusExpr, augmented_expression +from sympde.exterior.datatype import ( + FiveForm, + FiveFormType, + FormType, + FourForm, + FourFormType, + OneForm, + OneFormType, + SixForm, + SixFormType, + ThreeForm, + ThreeFormType, + TwoForm, + TwoFormType, + ZeroForm, + ZeroFormType, +) +from sympde.exterior.form import DifferentialForm +from sympde.exterior.inference import infere_type + +from sympde.printing.latex import LatexPrinter, latex +from sympde.utilities.utils import lambdify_sympde, plot_domain + +# ``PullBack`` historically referred to the exterior-calculus operator in the +# root namespace. Keep that spelling and provide explicit aliases for both +# otherwise-colliding pull-back concepts. +ExteriorPullBack = PullBack + +__all__ = ( + '__version__', + 'Constant', + # Calculus + 'Average', + 'BasicOperator', + 'BasicOperatorAdd', + 'Bracket', + 'Convect', + 'Convolution', + 'Cross', + 'Curl', + 'D', + 'DiffOperator', + 'Div', + 'Dn', + 'Dot', + 'Grad', + 'Hessian', + 'Inner', + 'Jump', + 'Laplace', + 'MinusInterfaceOperator', + 'NormalDerivative', + 'Outer', + 'PlusInterfaceOperator', + 'Rot', + 'StrainTensor', + 'add_basicop', + 'avg', + 'bracket', + 'conv', + 'convect', + 'cross', + 'curl', + 'div', + 'dot', + 'grad', + 'has', + 'hessian', + 'inner', + 'is_constant', + 'is_scalar', + 'is_zero', + 'jump', + 'laplace', + 'minus', + 'outer', + 'plus', + 'rot', + 'ArgumentTypeError', + 'CalculusError', + 'Inverse', + 'MatrixElement', + 'MatrixSymbolicExpr', + 'SymbolicDeterminant', + 'SymbolicTrace', + 'Transpose', + # Topology + 'BasicDomain', + 'Boundary', + 'Connectivity', + 'CornerBoundary', + 'CornerInterface', + 'Edge', + 'Interface', + 'InteriorDomain', + 'Interval', + 'ProductDomain', + 'Union', + 'Area', + 'BoundaryVector', + 'Cube', + 'Domain', + 'ElementDomain', + 'Line', + 'MinusNormalVector', + 'NCube', + 'NCubeInterior', + 'NormalVector', + 'PeriodicDomain', + 'PlusNormalVector', + 'Square', + 'TangentVector', + 'split', + 'BasicCallableMapping', + 'Contravariant', + 'Covariant', + 'InterfaceMapping', + 'InverseMapping', + 'Jacobian', + 'JacobianInverseSymbol', + 'JacobianSymbol', + 'LogicalExpr', + 'MappedDomain', + 'Mapping', + 'MappingApplication', + 'MappingPullBack', + 'MultiPatchMapping', + 'SymbolicExpr', + 'SymbolicWeightedVolume', + 'get_logical_test_function', + 'BasicFunctionSpace', + 'Derham', + 'IndexedVectorFunction', + 'ProductSpace', + 'Projection', + 'Projector', + 'ScalarFunction', + 'ScalarFunctionSpace', + 'TestFunction', + 'Trace', + 'VectorFunction', + 'VectorFunctionSpace', + 'element_of', + 'elements_of', + 'trace_0', + 'trace_1', + 'DifferentialOperator', + 'dx', + 'dx1', + 'dx2', + 'dx3', + 'dy', + 'dz', + 'find_partial_derivatives', + 'get_atom_derivatives', + 'get_atom_logical_derivatives', + 'get_index_derivatives', + 'get_index_derivatives_atom', + 'get_index_logical_derivatives', + 'get_index_logical_derivatives_atom', + 'get_max_logical_partial_derivatives', + 'get_max_partial_derivatives', + 'get_number_derivatives', + 'sort_partial_derivatives', + 'H1Regularity', + 'H1RegularityType', + 'H1Space', + 'H1SpaceType', + 'HcurlRegularity', + 'HcurlRegularityType', + 'HcurlSpace', + 'HcurlSpaceType', + 'HdivRegularity', + 'HdivRegularityType', + 'HdivSpace', + 'HdivSpaceType', + 'L2Regularity', + 'L2RegularityType', + 'L2Space', + 'L2SpaceType', + 'RegularityType', + 'SpaceType', + 'UndefinedSpace', + 'UndefinedSpaceType', + 'AffineMapping', + 'CollelaMapping2D', + 'CzarnyMapping', + 'IdentityMapping', + 'PolarMapping', + 'SphericalMapping', + 'TargetMapping', + 'TorusMapping', + 'TorusSurfaceMapping', + 'TwistedTargetMapping', + 'TwistedTargetSurfaceMapping', + 'CallableMapping', + 'BasicMeasure', + 'CanonicalMeasure', + 'CartesianMeasure', + 'Measure', + # Expressions + 'BasicExpr', + 'BasicForm', + 'Equation', + 'EssentialBC', + 'Mean', + 'NewtonIteration', + 'find', + 'UnconsistentArgumentsError', + 'UnconsistentBCError', + 'UnconsistentError', + 'UnconsistentLhsError', + 'UnconsistentLinearExpressionError', + 'UnconsistentRhsError', + 'BoundaryExpression', + 'DomainExpression', + 'InterfaceExpression', + 'KernelExpression', + 'TensorExpr', + 'TerminalExpr', + 'BilinearForm', + 'Functional', + 'IntAdd', + 'Integral', + 'LinearExpr', + 'LinearForm', + 'Norm', + 'SemiNorm', + 'integral', + 'is_linear_expression', + 'linearize', + # Exterior calculus + 'Adjoint', + 'AdjointExteriorDerivative', + 'AdjointInteriorProduct', + 'AdjointLieDerivative', + 'ExteriorDerivative', + 'ExteriorProduct', + 'ExteriorPullBack', + 'Hodge', + 'InteriorProduct', + 'Ld', + 'LieDerivative', + 'PullBack', + 'd', + 'delta', + 'hodge', + 'ip', + 'jp', + 'ld', + 'wedge', + 'ExteriorCalculusExpr', + 'augmented_expression', + 'FiveForm', + 'FiveFormType', + 'FormType', + 'FourForm', + 'FourFormType', + 'OneForm', + 'OneFormType', + 'SixForm', + 'SixFormType', + 'ThreeForm', + 'ThreeFormType', + 'TwoForm', + 'TwoFormType', + 'ZeroForm', + 'ZeroFormType', + 'DifferentialForm', + 'infere_type', + # Output and utilities + 'LatexPrinter', + 'latex', + 'lambdify_sympde', + 'plot_domain', +) diff --git a/sympde/calculus/__init__.py b/sympde/calculus/__init__.py index d468d7df..e69de29b 100644 --- a/sympde/calculus/__init__.py +++ b/sympde/calculus/__init__.py @@ -1,3 +0,0 @@ -from .core import * -from .errors import * -from .matrices import * diff --git a/sympde/calculus/core.py b/sympde/calculus/core.py index ac59ef41..7d40dab3 100644 --- a/sympde/calculus/core.py +++ b/sympde/calculus/core.py @@ -102,8 +102,8 @@ from sympy.core.containers import Tuple from sympy.core.singleton import S from sympy.core.decorators import call_highest_priority +from sympy.utilities.iterables import is_sequence -from sympde.old_sympy_utilities import is_sequence from sympde.core.basic import CalculusFunction from sympde.core.basic import _coeffs_registery from sympde.topology.space import ScalarFunction, VectorFunction @@ -1860,4 +1860,3 @@ def add_basicop(expr): Basic._constructor_postprocessor_mapping[BasicOperator] = { "Add": [add_basicop], } - diff --git a/sympde/calculus/matrices.py b/sympde/calculus/matrices.py index 16b827e1..c796e638 100644 --- a/sympde/calculus/matrices.py +++ b/sympde/calculus/matrices.py @@ -2,7 +2,7 @@ from sympy import Add, Mul, Pow from sympy import sympify from sympy.core.decorators import call_highest_priority -from sympde.core.basic import _coeffs_registery, Basic +from sympde.core.basic import _coeffs_registery, _new_basic, Basic class MatrixSymbolicExpr(Expr): is_commutative = False @@ -275,7 +275,7 @@ def _sympystr(self, printer): class MatrixElement(Expr): def __new__(cls, base, indices, **options): - return Expr.__new__(cls, base, indices) + return _new_basic(cls, base, indices) @property def base(self): @@ -293,4 +293,3 @@ def _sympystr(self, printer): "Mul": [lambda x: MatSymbolicMul(*x.args)], "Add": [lambda x: MatSymbolicAdd(*x.args)] } - diff --git a/sympde/calculus/tests/test_calculus.py b/sympde/calculus/tests/test_calculus.py index f7ea8fac..a3cb6ce5 100644 --- a/sympde/calculus/tests/test_calculus.py +++ b/sympde/calculus/tests/test_calculus.py @@ -6,15 +6,15 @@ from sympy import Integer, Float, Rational from sympy import expand -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, outer, cross, rot, curl, div -from sympde.calculus import laplace, hessian, bracket, convect, D, conv -from sympde.calculus import ArgumentTypeError -from sympde.calculus import jump, avg, Dn, minus, plus -from sympde.topology import Domain -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import ProductSpace -from sympde.topology import element_of, elements_of +from sympde.api import Constant +from sympde.api import grad, dot, inner, outer, cross, rot, curl, div +from sympde.api import laplace, hessian, bracket, convect, D, conv +from sympde.api import ArgumentTypeError +from sympde.api import jump, avg, Dn, minus, plus +from sympde.api import Domain +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import ProductSpace +from sympde.api import element_of, elements_of #============================================================================== @pytest.mark.parametrize('dim', [1, 2, 3]) diff --git a/sympde/core/__init__.py b/sympde/core/__init__.py index 4bc8bacf..e69de29b 100644 --- a/sympde/core/__init__.py +++ b/sympde/core/__init__.py @@ -1,3 +0,0 @@ -from .algebra import * -from .basic import * -from .utils import * diff --git a/sympde/core/algebra.py b/sympde/core/algebra.py index 7b626117..3cdae83c 100644 --- a/sympde/core/algebra.py +++ b/sympde/core/algebra.py @@ -1,40 +1,12 @@ # coding: utf-8 -import numpy as np -from itertools import groupby - -#from sympy.core.sympify import sympify -from sympy.simplify.simplify import simplify -from sympy import Symbol -from sympy import Lambda -from sympy import Function -from sympy import bspline_basis -from sympy import lambdify -from sympy import cos -from sympy import sin -from sympy import Rational -from sympy import diff -from sympy import Matrix, ImmutableDenseMatrix -from sympy import latex -from sympy import I as sympy_I -from sympy.core import Basic -from sympy.core.singleton import S -from sympy.simplify.simplify import nsimplify -from sympy.utilities.lambdify import implemented_function -from sympy.matrices.dense import MutableDenseMatrix -from sympy import Mul, Add -from sympy import postorder_traversal -from sympy import preorder_traversal -from sympy.core.expr import Expr +from sympy import Indexed, Matrix, ImmutableDenseMatrix +from sympy.core import Add, Basic, Mul from sympy.core.containers import Tuple -from sympy import Integer, Float -from sympy import Add, Mul -from sympy import simplify -from sympy import S -from sympy import Basic -from sympy import Indexed, IndexedBase - -from sympde.old_sympy_utilities import is_sequence +from sympy.core.singleton import S +from sympy.simplify.simplify import simplify +from sympy.utilities.iterables import is_sequence + from .basic import CalculusFunction from .basic import _coeffs_registery diff --git a/sympde/core/basic.py b/sympde/core/basic.py index 5f8fff14..3cd00023 100644 --- a/sympde/core/basic.py +++ b/sympde/core/basic.py @@ -3,10 +3,80 @@ from sympy import Function from sympy import Number from sympy import NumberSymbol +from sympy import Integer +from sympy import sympify from sympy.core import Basic from sympy.core import Symbol +from sympy.core.containers import Dict, Tuple +from sympy.core.symbol import Str from sympy.tensor import IndexedBase + +class _NoneArgument(Basic): + """SymPy-compatible representation of ``None`` in expression arguments.""" + + +_none = _NoneArgument() + + +def _sympify_argument(value): + """Convert structural metadata to objects accepted in ``Basic.args``.""" + if isinstance(value, Basic): + return value + if value is None: + return _none + if isinstance(value, str): + return Str(value) + if isinstance(value, dict): + return Dict(*((_sympify_argument(k), _sympify_argument(v)) + for k, v in value.items())) + if isinstance(value, (tuple, list)): + return Tuple(*(_sympify_argument(v) for v in value)) + return sympify(value) + + +def _new_basic(cls, *args, **options): + """Construct a ``Basic`` after normalizing all structural arguments.""" + args = tuple(_sympify_argument(arg) for arg in args) + return Basic.__new__(cls, *args, **options) + + +def _is_none_argument(value): + return value is _none + + +def _restore_optional_argument(value): + """Restore an optional value stored in a symbolic argument list.""" + return None if _is_none_argument(value) else value + + +def _restore_string_argument(value): + """Restore a Python string stored as a SymPy ``Str`` object.""" + return value.name if isinstance(value, Str) else value + + +def _restore_int_argument(value): + """Restore a Python integer stored as a SymPy ``Integer`` object.""" + value = _restore_optional_argument(value) + return int(value) if isinstance(value, Integer) else value + + +def _restore_container_argument(value): + """Restore Python containers used as non-expression metadata.""" + value = _restore_optional_argument(value) + if isinstance(value, Str): + return value.name + if isinstance(value, Dict): + return { + _restore_container_argument(k): _restore_container_argument(v) + for k, v in value.items() + } + if isinstance(value, Tuple): + return tuple(_restore_container_argument(v) for v in value) + if isinstance(value, Integer): + return int(value) + return value + #============================================================================== class Constant(Symbol): """ diff --git a/sympde/core/tests/test_api.py b/sympde/core/tests/test_api.py new file mode 100644 index 00000000..a6e9bd6f --- /dev/null +++ b/sympde/core/tests/test_api.py @@ -0,0 +1,40 @@ +from pathlib import Path + + +def test_non_api_package_initializers_are_empty(): + package_dir = Path(__file__).parents[2] + initializers = package_dir.rglob('__init__.py') + + for initializer in initializers: + if initializer.parent != package_dir / 'api': + assert initializer.read_text() == '' + + +def test_api_has_only_explicit_exports(): + import sympde.api as api + + assert len(api.__all__) == len(set(api.__all__)) + assert all(hasattr(api, name) for name in api.__all__) + assert {name for name in vars(api) if not name.startswith('_')} == { + name for name in api.__all__ if not name.startswith('_') + } + + +def test_api_objects_come_from_defining_modules(): + from sympde.api import BilinearForm, Constant, Cube, Mapping, elements_of + from sympde.api import ScalarFunctionSpace, VectorFunctionSpace + from sympde.core.basic import Constant as CoreConstant + from sympde.expr.expr import BilinearForm as ExprBilinearForm + from sympde.topology.domain import Cube as TopologyCube + from sympde.topology.mapping import Mapping as TopologyMapping + from sympde.topology.space import elements_of as topology_elements_of + from sympde.topology.space import ScalarFunctionSpace as TopologyScalarSpace + from sympde.topology.space import VectorFunctionSpace as TopologyVectorSpace + + assert BilinearForm is ExprBilinearForm + assert Constant is CoreConstant + assert Cube is TopologyCube + assert Mapping is TopologyMapping + assert elements_of is topology_elements_of + assert ScalarFunctionSpace is TopologyScalarSpace + assert VectorFunctionSpace is TopologyVectorSpace diff --git a/sympde/expr/__init__.py b/sympde/expr/__init__.py index a092e4cd..e69de29b 100644 --- a/sympde/expr/__init__.py +++ b/sympde/expr/__init__.py @@ -1,5 +0,0 @@ -from .basic import * -from .equation import * -from .errors import * -from .evaluation import * -from .expr import * diff --git a/sympde/expr/equation.py b/sympde/expr/equation.py index 5b4ed1aa..ce3e0ad9 100644 --- a/sympde/expr/equation.py +++ b/sympde/expr/equation.py @@ -5,19 +5,23 @@ from sympy.core import Expr from sympy.parsing.sympy_parser import parse_expr -from sympde.topology.basic import Boundary, Union -from sympde.topology.space import VectorFunctionSpace, ScalarFunctionSpace +from sympde.topology.basic import Union +from sympde.topology.domain import NormalVector +from sympde.topology.space import IndexedVectorFunction from sympde.topology.space import ScalarFunction -from sympde.topology.space import VectorFunction, IndexedVectorFunction -from sympde.topology import Boundary, NormalVector, TangentVector -from sympde.topology import Trace, trace_0, trace_1 -from sympde.calculus import grad, dot -from sympde.core.utils import random_string +from sympde.topology.space import Trace +from sympde.topology.space import VectorFunction, VectorFunctionSpace +from sympde.calculus.core import dot, grad +from sympde.core.basic import ( + _is_none_argument, + _new_basic, + _restore_optional_argument, +) from .expr import BilinearForm, LinearForm from .expr import linearize -from .errors import ( UnconsistentLhsError, UnconsistentRhsError, - UnconsistentArgumentsError, UnconsistentBCError ) +from .errors import (UnconsistentLhsError, UnconsistentRhsError, + UnconsistentArgumentsError) #============================================================================== @@ -191,6 +195,8 @@ def rhs(self): class Equation(Basic): def __new__(cls, lhs, rhs, trials, tests, bc=None, constraint=None): + bc = _restore_optional_argument(bc) + constraint = _restore_optional_argument(constraint) # ... if not isinstance(lhs, BilinearForm): raise UnconsistentLhsError('> lhs must be a bilinear') @@ -332,7 +338,7 @@ def __new__(cls, lhs, rhs, trials, tests, bc=None, constraint=None): tests = Tuple(*tests) # ... - return Basic.__new__(cls, lhs, rhs, trials, tests, bc, constraint) + return _new_basic(cls, lhs, rhs, trials, tests, bc, constraint) @property def lhs(self): @@ -352,11 +358,13 @@ def test_functions(self): @property def bc(self): - return self._args[4] + bc = self._args[4] + return None if _is_none_argument(bc) else bc @property def constraint(self): - return self._args[5] + constraint = self._args[5] + return None if _is_none_argument(constraint) else constraint #============================================================================== # TODO must subtitute expr by given args => call then create BasicForm diff --git a/sympde/expr/evaluation.py b/sympde/expr/evaluation.py index d3f0ace4..a325974c 100644 --- a/sympde/expr/evaluation.py +++ b/sympde/expr/evaluation.py @@ -8,13 +8,14 @@ from sympde.core.basic import _coeffs_registery from sympde.core.basic import CalculusFunction +from sympde.core.basic import _new_basic from sympde.core.algebra import (Dot_1d, Dot_2d, Inner_2d, Cross_2d, Dot_3d, Inner_3d, Cross_3d) #from sympde.core.utils import random_string -from sympde.calculus import jump, avg, minus, plus -from sympde.calculus import Jump, is_zero +from sympde.calculus.core import Jump, is_zero +from sympde.calculus.core import jump, minus, plus from sympde.calculus.core import _generic_ops, _diff_ops from sympde.calculus.core import MinusInterfaceOperator, PlusInterfaceOperator from sympde.calculus.matrices import SymbolicDeterminant, Inverse, Transpose @@ -502,8 +503,7 @@ class TerminalExpr(CalculusFunction): Parameters ---------- - expr : Expr | Matrix | ImmutableDenseMatrix (from sympy) - | LogicalExpr (from sympde.topology.mapping) + expr : Expr or Matrix or ImmutableDenseMatrix or LogicalExpr The mathematical expression. domain : BasicDomain (from sympde.topology.basic) @@ -974,15 +974,15 @@ def _tensorize_atomic_expr(expr, d_atoms): class Basic1dForm(AtomicExpr): def __new__(cls, name, axis, weight=S.One): - return Basic.__new__(cls, name, axis, weight) + return _new_basic(cls, name, axis, weight) @property def name(self): - return self._args[0] + return self._args[0].name @property def axis(self): - return self._args[1] + return int(self._args[1]) @property def weight(self): @@ -1004,38 +1004,48 @@ def _sympystr(self, printer): class Mass(Basic1dForm): - def __new__(cls, axis, weight=S.One): + def __new__(cls, axis, weight=S.One, *structural_args): + if structural_args: + return Basic1dForm.__new__(cls, axis, weight, *structural_args) # name = 'Mass' name = 'M' - return Basic.__new__(cls, name, axis, weight) + return Basic1dForm.__new__(cls, name, axis, weight) class Stiffness(Basic1dForm): - def __new__(cls, axis, weight=S.One): + def __new__(cls, axis, weight=S.One, *structural_args): + if structural_args: + return Basic1dForm.__new__(cls, axis, weight, *structural_args) # name = 'Stiffness' name = 'S' - return Basic.__new__(cls, name, axis, weight) + return Basic1dForm.__new__(cls, name, axis, weight) class Advection(Basic1dForm): - def __new__(cls, axis, weight=S.One): + def __new__(cls, axis, weight=S.One, *structural_args): + if structural_args: + return Basic1dForm.__new__(cls, axis, weight, *structural_args) # name = 'Advection' name = 'A' - return Basic.__new__(cls, name, axis, weight) + return Basic1dForm.__new__(cls, name, axis, weight) class AdvectionT(Basic1dForm): - def __new__(cls, axis, weight=S.One): + def __new__(cls, axis, weight=S.One, *structural_args): + if structural_args: + return Basic1dForm.__new__(cls, axis, weight, *structural_args) # name = 'AdvectionT' name = 'AT' - return Basic.__new__(cls, name, axis, weight) + return Basic1dForm.__new__(cls, name, axis, weight) class Bilaplacian(Basic1dForm): - def __new__(cls, axis, weight=S.One): + def __new__(cls, axis, weight=S.One, *structural_args): + if structural_args: + return Basic1dForm.__new__(cls, axis, weight, *structural_args) # name = 'Bilaplacian' name = 'B' - return Basic.__new__(cls, name, axis, weight) + return Basic1dForm.__new__(cls, name, axis, weight) Mass_0 = Mass(0) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 67557467..47ae9c71 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -9,16 +9,15 @@ from sympy.core import Expr, Add, Mul from sympy.core.numbers import Zero as sy_Zero from sympy.core.containers import Tuple +from sympy.utilities.iterables import is_sequence -from sympde.old_sympy_utilities import is_sequence from sympde.core.basic import CalculusFunction from sympde.core.basic import Constant from sympde.core.utils import random_string -from sympde.calculus import Dot, Inner, BasicOperator -from sympde.calculus import Grad, Hessian -from sympde.topology import BasicDomain, Union -from sympde.topology import NormalVector -from sympde.topology import Boundary, Interface, Domain, InteriorDomain +from sympde.calculus.core import BasicOperator, Dot, Grad, Hessian, Inner +from sympde.topology.basic import BasicDomain, Boundary, Interface +from sympde.topology.basic import InteriorDomain, Union +from sympde.topology.domain import Domain, NormalVector from sympde.topology.space import ScalarFunction from sympde.topology.space import VectorFunction from sympde.topology.space import Trace, trace_0, trace_1 @@ -402,7 +401,7 @@ def __call__(self, *tests, **kwargs): # Make sure that 'values' is always a list if len(tests) == 1: values = tests[0] - if not is_sequence(values, vector=isinstance(values, VectorFunction)): + if isinstance(values, VectorFunction) or not is_sequence(values): values = [values] else: values = tests @@ -513,8 +512,8 @@ def __call__(self, trials, tests, **kwargs): expr = self._update_free_variables(**kwargs) # If needed, convert positional arguments to lists - if not is_sequence(trials, vector=isinstance(trials, VectorFunction)): trials = [trials] - if not is_sequence(tests, vector=isinstance(tests, VectorFunction)): tests = [tests ] + if isinstance(trials, VectorFunction) or not is_sequence(trials): trials = [trials] + if isinstance(tests, VectorFunction) or not is_sequence(tests): tests = [tests ] # Concatenate input values into single list values = [*trials, *tests] @@ -855,7 +854,7 @@ def mul_integral(expr): def mul_add(expr): args_int = [a for a in expr.args if isinstance(a, IntAdd)] coeff = Mul(*[a for a in expr.args if not a in args_int]) - args_int[0] = [IntAdd(*[coeff*i for i in args_int[0].args])] + args_int[0] = IntAdd(*[coeff*i for i in args_int[0].args]) return Mul(*args_int) def add_int(expr): @@ -869,4 +868,3 @@ def add_int(expr): Basic._constructor_postprocessor_mapping[IntAdd] = { "Mul": [mul_add], } - diff --git a/sympde/expr/tests/test_equation_2d.py b/sympde/expr/tests/test_equation_2d.py index cb446f2f..bce39c38 100644 --- a/sympde/expr/tests/test_equation_2d.py +++ b/sympde/expr/tests/test_equation_2d.py @@ -6,18 +6,18 @@ from sympy import pi, cos, sin from sympy import ImmutableDenseMatrix as Matrix -from sympde.core import Constant -from sympde.calculus import grad, dot, inner -from sympde.calculus import laplace, bracket -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of -from sympde.topology import ProductSpace -from sympde.topology import Domain, Boundary, NormalVector -from sympde.topology import Square -from sympde.topology import ElementDomain -from sympde.topology import Area -from sympde.expr import BilinearForm, LinearForm, integral -from sympde.expr import Equation, EssentialBC +from sympde.api import Constant +from sympde.api import grad, dot, inner +from sympde.api import laplace, bracket +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of +from sympde.api import ProductSpace +from sympde.api import Domain, Boundary, NormalVector +from sympde.api import Square +from sympde.api import ElementDomain +from sympde.api import Area +from sympde.api import BilinearForm, LinearForm, integral +from sympde.api import Equation, EssentialBC from sympde.expr.errors import UnconsistentLhsError from sympde.expr.errors import UnconsistentRhsError from sympde.expr.errors import UnconsistentBCError @@ -109,6 +109,7 @@ def test_equation_2d_1(): a = BilinearForm((v,u), a1(v,u) + a_B1(v,u)) l = LinearForm(v, l1(v) + l2(v)) equation = Equation(a, l, tests=v, trials=u) + assert equation.func(*equation.args) == equation # ... # ... @@ -415,4 +416,3 @@ def teardown_module(): def teardown_function(): from sympy.core import cache cache.clear_cache() - diff --git a/sympde/expr/tests/test_essential_bc.py b/sympde/expr/tests/test_essential_bc.py index e5c04d5c..6bdc5395 100644 --- a/sympde/expr/tests/test_essential_bc.py +++ b/sympde/expr/tests/test_essential_bc.py @@ -1,11 +1,11 @@ # coding: utf-8 -from sympde.core import Constant -from sympde.calculus import grad, dot -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of -from sympde.topology import Domain, Boundary, NormalVector -from sympde.expr import EssentialBC +from sympde.api import Constant +from sympde.api import grad, dot +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of +from sympde.api import Domain, Boundary, NormalVector +from sympde.api import EssentialBC #============================================================================== def test_essential_bc_1(): diff --git a/sympde/expr/tests/test_expr.py b/sympde/expr/tests/test_expr.py index 77a96008..cca3884f 100644 --- a/sympde/expr/tests/test_expr.py +++ b/sympde/expr/tests/test_expr.py @@ -7,23 +7,23 @@ from sympy import pi, cos, sin, exp from sympy import ImmutableDenseMatrix as Matrix -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, rot, div -from sympde.calculus import laplace, bracket, convect -from sympde.calculus import jump, avg, Dn, minus, plus - -from sympde.topology import dx1, dx2, dx3 -from sympde.topology import dx, dy, dz -from sympde.topology import Mapping -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of, elements_of -from sympde.topology import InteriorDomain, Union -from sympde.topology import Boundary, NormalVector -from sympde.topology import Domain +from sympde.api import Constant +from sympde.api import grad, dot, inner, rot, div +from sympde.api import laplace, bracket, convect +from sympde.api import jump, avg, Dn, minus, plus + +from sympde.api import dx1, dx2, dx3 +from sympde.api import dx, dy, dz +from sympde.api import Mapping +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of, elements_of +from sympde.api import InteriorDomain, Union +from sympde.api import Boundary, NormalVector +from sympde.api import Domain #from sympde.topology import trace_1 # TODO [YG, 27.01.2021]: fix trace -from sympde.topology import Square -from sympde.topology import ElementDomain -from sympde.topology import Area +from sympde.api import Square +from sympde.api import ElementDomain +from sympde.api import Area from sympde.expr.expr import LinearExpr from sympde.expr.expr import LinearForm, BilinearForm diff --git a/sympde/expr/tests/test_find_2d.py b/sympde/expr/tests/test_find_2d.py index 9f176d7e..7b91313f 100644 --- a/sympde/expr/tests/test_find_2d.py +++ b/sympde/expr/tests/test_find_2d.py @@ -3,16 +3,16 @@ from sympy import cos #from sympy import exp -from sympde.core import Constant -from sympde.calculus import grad, dot +from sympde.api import Constant +from sympde.api import grad, dot #from sympde.topology import dx, dy -from sympde.topology import ScalarFunctionSpace -from sympde.topology import element_of -from sympde.topology import Domain, Boundary -from sympde.expr import BilinearForm, LinearForm, integral -from sympde.expr import TerminalExpr -from sympde.expr import find -from sympde.expr import EssentialBC +from sympde.api import ScalarFunctionSpace +from sympde.api import element_of +from sympde.api import Domain, Boundary +from sympde.api import BilinearForm, LinearForm, integral +from sympde.api import TerminalExpr +from sympde.api import find +from sympde.api import EssentialBC #from sympde.expr import NewtonIteration DIM = 2 diff --git a/sympde/expr/tests/test_newton_2d.py b/sympde/expr/tests/test_newton_2d.py index 365bd18c..aa419c0a 100644 --- a/sympde/expr/tests/test_newton_2d.py +++ b/sympde/expr/tests/test_newton_2d.py @@ -2,12 +2,12 @@ from sympy import exp -from sympde.calculus import grad, dot -from sympde.topology import ScalarFunctionSpace -from sympde.topology import element_of -from sympde.topology import Domain, Boundary -from sympde.expr import LinearForm, integral -from sympde.expr import EssentialBC, NewtonIteration +from sympde.api import grad, dot +from sympde.api import ScalarFunctionSpace +from sympde.api import element_of +from sympde.api import Domain, Boundary +from sympde.api import LinearForm, integral +from sympde.api import EssentialBC, NewtonIteration DIM = 2 domain = Domain('Omega', dim=DIM) diff --git a/sympde/expr/tests/test_tensor_2d.py b/sympde/expr/tests/test_tensor_2d.py index 0fc9e31d..724c96de 100644 --- a/sympde/expr/tests/test_tensor_2d.py +++ b/sympde/expr/tests/test_tensor_2d.py @@ -4,15 +4,15 @@ from sympy.core.containers import Tuple -from sympde.core import Constant -from sympde.calculus import grad, dot, curl, div +from sympde.api import Constant +from sympde.api import grad, dot, curl, div #from sympde.calculus import laplace #from sympde.topology import dx -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import elements_of -from sympde.topology import Boundary -from sympde.topology import Domain -from sympde.topology import Mapping +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import elements_of +from sympde.api import Boundary +from sympde.api import Domain +from sympde.api import Mapping from sympde.expr.expr import BilinearForm, integral from sympde.expr.evaluation import TensorExpr diff --git a/sympde/exterior/__init__.py b/sympde/exterior/__init__.py index c026ed14..e69de29b 100644 --- a/sympde/exterior/__init__.py +++ b/sympde/exterior/__init__.py @@ -1,6 +0,0 @@ -#from .basic import * -from .calculus import * -from .datatype import * -from .form import * -from .compiler import * -from .inference import * diff --git a/sympde/exterior/calculus.py b/sympde/exterior/calculus.py index c9b97059..d074c784 100644 --- a/sympde/exterior/calculus.py +++ b/sympde/exterior/calculus.py @@ -2,21 +2,14 @@ # TODO - use BasicOperator instead of LinearOperator -from numpy import unique - from sympy.core import Basic -from sympy.tensor import Indexed, IndexedBase -from sympy.core import Symbol -from sympy.core import Expr -from sympy.core.containers import Tuple -from sympy import Function -from sympy import Integer, Float -from sympy.core.singleton import Singleton +from sympy.tensor import Indexed from sympy.core import Add, Mul from sympy.core.singleton import S +from sympy.utilities.iterables import is_sequence from sympde.core.basic import _coeffs_registery -from sympde.core import LinearOperator +from sympde.core.algebra import LinearOperator from sympde.core.basic import CalculusFunction from .form import DifferentialForm diff --git a/sympde/exterior/compiler.py b/sympde/exterior/compiler.py index 0fefba08..02e28d1e 100644 --- a/sympde/exterior/compiler.py +++ b/sympde/exterior/compiler.py @@ -7,14 +7,15 @@ from sympy import S from sympy import Indexed from sympy import Tuple +from sympy.utilities.iterables import is_sequence from sympde.core.basic import _coeffs_registery from sympde.core.basic import CalculusFunction -from sympde.topology import H1SpaceType, HcurlSpaceType, HdivSpaceType -from sympde.topology import L2SpaceType, UndefinedSpaceType -from sympde.topology import ScalarFunction, VectorFunction -from sympde.calculus import Grad, Curl, Div -from sympde.calculus import Dot, Inner, Cross +from sympde.topology.datatype import H1SpaceType, HcurlSpaceType, HdivSpaceType +from sympde.topology.datatype import L2SpaceType, UndefinedSpaceType +from sympde.topology.space import ScalarFunction, VectorFunction +from sympde.calculus.core import Curl, Div, Grad +from sympde.calculus.core import Cross, Dot, Inner #from sympde.calculus import grad, dot, inner, cross, rot, curl, div from .form import DifferentialForm diff --git a/sympde/exterior/datatype.py b/sympde/exterior/datatype.py index cb96ba5e..72736879 100644 --- a/sympde/exterior/datatype.py +++ b/sympde/exterior/datatype.py @@ -1,25 +1,12 @@ # coding: utf-8 # TODO: - Unknown is not used here (mlhipy) remove it? -from numpy import unique - from sympy.core import Basic -from sympy.tensor import Indexed, IndexedBase from sympy.core import Symbol -from sympy.core import Expr -from sympy.core.containers import Tuple -from sympy import Function -from sympy import Integer, Float from sympy.core.singleton import Singleton -from sympy.core import Add, Mul -from sympy.core.singleton import S - -from sympde.old_sympy_utilities import with_metaclass -from sympde.core.basic import _coeffs_registery -from sympde.core import LinearOperator #============================================================================== -class FormType(with_metaclass(Singleton, Basic)): +class FormType(Basic, metaclass=Singleton): """Base class representing differential form types""" _index = None diff --git a/sympde/exterior/form.py b/sympde/exterior/form.py index dc4652e7..65b738c4 100644 --- a/sympde/exterior/form.py +++ b/sympde/exterior/form.py @@ -1,20 +1,7 @@ # coding: utf-8 -from numpy import unique - -from sympy.core import Basic -from sympy.tensor import Indexed, IndexedBase +from sympy.core import Integer from sympy.core import Symbol -from sympy.core import Expr -from sympy.core.containers import Tuple -from sympy import Function -from sympy import Integer, Float -from sympy.core.singleton import Singleton -from sympy.core import Add, Mul -from sympy.core.singleton import S - -from sympde.core.basic import _coeffs_registery -from sympde.core import LinearOperator from .datatype import get_index_form @@ -34,16 +21,18 @@ def __new__(cls, name, index, dim): index = get_index_form(index) - return Basic.__new__(cls, name, index, dim) - - @property - def name(self): - return self._args[0] + obj = Symbol.__xnew__(cls, name) + obj._index = index + obj._dim = Integer(dim) if isinstance(dim, int) else dim + return obj @property def index(self): - return self._args[1] + return self._index @property def dim(self): - return self._args[2] + return int(self._dim) if self._dim.is_Integer else self._dim + + def _hashable_content(self): + return Symbol._hashable_content(self) + (self.index, self._dim) diff --git a/sympde/exterior/inference.py b/sympde/exterior/inference.py index 92fa4a7d..f17ff44f 100644 --- a/sympde/exterior/inference.py +++ b/sympde/exterior/inference.py @@ -1,22 +1,7 @@ # coding: utf-8 -from numpy import unique - -from sympy.core import Basic -from sympy.tensor import Indexed, IndexedBase -from sympy.core import Symbol -from sympy.core import Expr -from sympy.core.containers import Tuple -from sympy import Function -from sympy import Integer, Float -from sympy.core.singleton import Singleton -from sympy.core import Add, Mul -from sympy.core.singleton import S - -from sympde.core.basic import _coeffs_registery -from sympde.core import LinearOperator -from sympde.core.basic import CalculusFunction +from sympy.core import Add from .form import DifferentialForm from .datatype import get_index_form diff --git a/sympde/exterior/tests/test_compiler.py b/sympde/exterior/tests/test_compiler.py index 6bbc9d47..1dbd0ee9 100644 --- a/sympde/exterior/tests/test_compiler.py +++ b/sympde/exterior/tests/test_compiler.py @@ -2,15 +2,15 @@ import pytest -from sympde.topology import Domain -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of -from sympde.calculus import grad, dot, inner, cross, curl, div +from sympde.api import Domain +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of +from sympde.api import grad, dot, inner, cross, curl, div #from sympde.exterior import d, wedge, ip, delta, jp -from sympde.exterior import ld -from sympde.exterior import DifferentialForm -from sympde.exterior import ExteriorCalculusExpr, augmented_expression +from sympde.api import ld +from sympde.api import DifferentialForm +from sympde.api import ExteriorCalculusExpr, augmented_expression from sympde.calculus.errors import ArgumentTypeError #============================================================================== diff --git a/sympde/exterior/tests/test_datatype.py b/sympde/exterior/tests/test_datatype.py index 9bea28d1..86860add 100644 --- a/sympde/exterior/tests/test_datatype.py +++ b/sympde/exterior/tests/test_datatype.py @@ -7,11 +7,11 @@ from sympy import srepr from sympy import Symbol -from sympde import Constant +from sympde.api import Constant -from sympde.exterior import d, wedge, ip, jp, delta, hodge -from sympde.exterior import ZeroForm, OneForm, TwoForm -from sympde.exterior import ThreeForm, FourForm, FiveForm, SixForm +from sympde.api import d, wedge, ip, jp, delta, hodge +from sympde.api import ZeroForm, OneForm, TwoForm +from sympde.api import ThreeForm, FourForm, FiveForm, SixForm #============================================================================== diff --git a/sympde/exterior/tests/test_exterior.py b/sympde/exterior/tests/test_exterior.py index d5778cb3..d37757f9 100644 --- a/sympde/exterior/tests/test_exterior.py +++ b/sympde/exterior/tests/test_exterior.py @@ -7,10 +7,21 @@ from sympy import srepr from sympy import Symbol -from sympde import Constant +from sympde.api import Constant -from sympde.exterior import d, wedge, ip, jp, delta, hodge -from sympde.exterior import DifferentialForm +from sympde.api import d, wedge, ip, jp, delta, hodge +from sympde.api import DifferentialForm + + +def test_differential_form_symbol_identity(): + u = DifferentialForm('u', index=1, dim=3) + + assert u == DifferentialForm('u', index=1, dim=3) + assert hash(u) == hash(DifferentialForm('u', index=1, dim=3)) + assert u != DifferentialForm('u', index=2, dim=3) + assert u != DifferentialForm('u', index=1, dim=2) + assert u.free_symbols == {u} + assert u.args == () diff --git a/sympde/exterior/tests/test_inference.py b/sympde/exterior/tests/test_inference.py index 850cdaaa..03553964 100644 --- a/sympde/exterior/tests/test_inference.py +++ b/sympde/exterior/tests/test_inference.py @@ -7,14 +7,14 @@ from sympy import srepr from sympy import Symbol -from sympde import Constant - -from sympde.exterior import d, wedge, ip, jp, delta, hodge -from sympde.exterior import DifferentialForm -from sympde.exterior import PullBack -from sympde.exterior import infere_type -from sympde.exterior import ZeroFormType, OneFormType, TwoFormType, ThreeFormType -from sympde.exterior import FourFormType, FiveFormType, SixFormType +from sympde.api import Constant + +from sympde.api import d, wedge, ip, jp, delta, hodge +from sympde.api import DifferentialForm +from sympde.api import PullBack +from sympde.api import infere_type +from sympde.api import ZeroFormType, OneFormType, TwoFormType, ThreeFormType +from sympde.api import FourFormType, FiveFormType, SixFormType #============================================================================== diff --git a/sympde/old_sympy_utilities.py b/sympde/old_sympy_utilities.py deleted file mode 100644 index afa12ef6..00000000 --- a/sympde/old_sympy_utilities.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Reimplementations of constructs introduced in later versions of Python than -we support. Also some functions that are needed SymPy-wide and are located -here for easy import. -""" - -import operator -from collections import defaultdict - -def with_metaclass(meta, *bases): - """ - Create a base class with a metaclass. - - For example, if you have the metaclass - - >>> class Meta(type): - ... pass - - Use this as the metaclass by doing - - >>> from sympde.old_sympy_utilities import with_metaclass - >>> class MyClass(with_metaclass(Meta, object)): - ... pass - - This is equivalent to the Python 2:: - - class MyClass(object): - __metaclass__ = Meta - - or Python 3:: - - class MyClass(object, metaclass=Meta): - pass - - That is, the first argument is the metaclass, and the remaining arguments - are the base classes. Note that if the base class is just ``object``, you - may omit it. - - >>> MyClass.__mro__ - (, <... 'object'>) - >>> type(MyClass) - - - """ - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - # Code copied from the 'six' library. - class metaclass(meta): - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, "NewBase", (), {}) - -class NotIterable: - """ - Use this as mixin when creating a class which is not supposed to - return true when iterable() is called on its instances because - calling list() on the instance, for example, would result in - an infinite loop. - """ - -def iterable(i, exclude=(str, dict, NotIterable), vector=False): - """ - Return a boolean indicating whether ``i`` is SymPy iterable. - True also indicates that the iterator is finite, e.g. you can - call list(...) on the instance. - - When SymPy is working with iterables, it is almost always assuming - that the iterable is not a string or a mapping, so those are excluded - by default. If you want a pure Python definition, make exclude=None. To - exclude multiple items, pass them as a tuple. - - You can also set the _iterable attribute to True or False on your class, - which will override the checks here, including the exclude test. - - As a rule of thumb, some SymPy functions use this to check if they should - recursively map over an object. If an object is technically iterable in - the Python sense but does not desire this behavior (e.g., because its - iteration is not finite, or because iteration might induce an unwanted - computation), it should disable it by setting the _iterable attribute to False. - - See also: is_sequence - - Examples - ======== - - >>> from sympy.utilities.iterables import iterable - >>> from sympy import Tuple - >>> things = [[1], (1,), set([1]), Tuple(1), (j for j in [1, 2]), {1:2}, '1', 1] - >>> for i in things: - ... print('%s %s' % (iterable(i), type(i))) - True <... 'list'> - True <... 'tuple'> - True <... 'set'> - True - True <... 'generator'> - False <... 'dict'> - False <... 'str'> - False <... 'int'> - - >>> iterable({}, exclude=None) - True - >>> iterable({}, exclude=str) - True - >>> iterable("no", exclude=str) - False - - """ - if hasattr(i, '_iterable'): - return i._iterable - try: - iter(i) - except TypeError: - return False - if exclude: - #TODO: check why the isinstance(i, exclude) return False - if vector: - return False - return not isinstance(i, exclude) - return True - -def is_sequence(i, include=None, *, vector=False): - """ - Return a boolean indicating whether ``i`` is a sequence in the SymPy - sense. If anything that fails the test below should be included as - being a sequence for your application, set 'include' to that object's - type; multiple types should be passed as a tuple of types. - - Note: although generators can generate a sequence, they often need special - handling to make sure their elements are captured before the generator is - exhausted, so these are not included by default in the definition of a - sequence. - - See also: iterable - - Examples - ======== - - >>> from sympy.utilities.iterables import is_sequence - >>> from types import GeneratorType - >>> is_sequence([]) - True - >>> is_sequence(set()) - False - >>> is_sequence('abc') - False - >>> is_sequence('abc', include=str) - True - >>> generator = (c for c in 'abc') - >>> is_sequence(generator) - False - >>> is_sequence(generator, include=(str, GeneratorType)) - True - - """ - return ( hasattr(i, '__getitem__') and - iterable(i, vector=vector) or - bool(include) and - isinstance(i, include)) diff --git a/sympde/printing/__init__.py b/sympde/printing/__init__.py index 94107209..e69de29b 100644 --- a/sympde/printing/__init__.py +++ b/sympde/printing/__init__.py @@ -1 +0,0 @@ -from .latex import * diff --git a/sympde/printing/latex.py b/sympde/printing/latex.py index 28c89770..7c803af0 100644 --- a/sympde/printing/latex.py +++ b/sympde/printing/latex.py @@ -2,16 +2,12 @@ # from sympy.core import Symbol -from sympy import Mul, Tuple +from sympy import Tuple from sympy.printing.latex import LatexPrinter as LatexPrinterSympy from sympy.printing.latex import translate -from sympy import Indexed, IndexedBase, Matrix, ImmutableDenseMatrix +from sympy import Indexed, IndexedBase -from sympde.topology import NormalVector, TangentVector -from sympde.topology import Line, Square, Cube, Domain -from sympde.topology.derivatives import sort_partial_derivatives -from sympde.topology.derivatives import get_index_derivatives -from sympde.topology.derivatives import get_atom_derivatives +from sympde.topology.domain import NormalVector from sympde.topology.space import ProductSpace from sympde.topology.space import ScalarFunction, VectorFunction @@ -202,6 +198,9 @@ def _print_Kron(self, expr): def _print_Nil(self, expr): return r'\ldots' + def _print_BasicDomain(self, expr): + return translate(expr.name) + def _print_Domain(self, expr): return translate(expr.name) diff --git a/sympde/printing/tests/test_latex.py b/sympde/printing/tests/test_latex.py index 1c8cef5c..8796853b 100644 --- a/sympde/printing/tests/test_latex.py +++ b/sympde/printing/tests/test_latex.py @@ -2,17 +2,30 @@ from sympy import sin, cos, pi -from sympde.calculus import grad, dot, inner, rot, div +from sympde.api import grad, dot, inner, rot, div #from sympde.topology import (dx, dy, dz) -from sympde.topology import Domain, Boundary -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of -from sympde.expr import BilinearForm, LinearForm, integral -from sympde.exterior import d, wedge, ip, jp, delta, hodge -from sympde.exterior import DifferentialForm +from sympde.api import Domain, Boundary, Square +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of +from sympde.api import BilinearForm, LinearForm, integral +from sympde.api import d, wedge, ip, jp, delta, hodge +from sympde.api import DifferentialForm from sympde.printing.latex import latex +def test_latex_domain_subclasses_use_their_names(): + patch_a = Square('A') + patch_b = Square('B') + domain = Domain.join( + [patch_a, patch_b], + [((0, 0, 1), (1, 0, -1), 1)], + 'Omega', + ) + + assert latex(patch_a.interior) == 'A' + assert latex(domain.interfaces) == 'A|B' + + #============================================================================== def test_latex_1d(): @@ -300,4 +313,3 @@ def teardown_module(): def teardown_function(): from sympy.core import cache cache.clear_cache() - diff --git a/sympde/topology/__init__.py b/sympde/topology/__init__.py index de44bd3c..e69de29b 100644 --- a/sympde/topology/__init__.py +++ b/sympde/topology/__init__.py @@ -1,9 +0,0 @@ -from .basic import * -from .derivatives import * -from .datatype import * -from .domain import * -from .mapping import * -from .measure import * -from .space import * -from .analytical_mapping import * -from .callable_mapping import * diff --git a/sympde/topology/basic.py b/sympde/topology/basic.py index 4eb371d6..ca97177b 100644 --- a/sympde/topology/basic.py +++ b/sympde/topology/basic.py @@ -4,9 +4,17 @@ from collections import abc -from sympy.core import Basic, Symbol, Expr +from sympy.core import Basic, Symbol from sympy.core.containers import Tuple -from sympy.tensor import IndexedBase + +from sympde.core.basic import ( + _is_none_argument, + _new_basic, + _restore_container_argument, + _restore_int_argument, + _restore_optional_argument, + _restore_string_argument, +) #============================================================================== class BasicDomain(Basic): @@ -52,8 +60,15 @@ class InteriorDomain(BasicDomain): Examples """ - def __new__(cls, name, dim=None, dtype=None, mapping=None, logical_domain=None): - target = None + def __new__(cls, name, dim=None, dtype=None, mapping=None, + logical_domain=None, target=None): + name = _restore_string_argument(name) + dim = _restore_int_argument(dim) + dtype = _restore_container_argument(dtype) + mapping = _restore_optional_argument(mapping) + logical_domain = _restore_optional_argument(logical_domain) + target = _restore_optional_argument(target) + if not isinstance(name, str): target = name name = name.name @@ -64,7 +79,8 @@ def __new__(cls, name, dim=None, dtype=None, mapping=None, logical_domain=None): assert mapping is None and logical_domain is None or \ mapping is not None and logical_domain is not None - obj = Basic.__new__(cls, name) + obj = _new_basic(cls, name, dim, dtype, mapping, + logical_domain, target) obj._dim = dim obj._target = target @@ -76,7 +92,7 @@ def __new__(cls, name, dim=None, dtype=None, mapping=None, logical_domain=None): @property def name(self): - return self.args[0] + return self.args[0].name @property def target(self): @@ -231,13 +247,17 @@ class Interval(InteriorDomain): _dim = 1 def __new__(cls, name=None, coordinate=None, bounds=None): + name = _restore_string_argument(name) + coordinate = _restore_optional_argument(coordinate) + bounds = _restore_container_argument(bounds) + if name is None: name = 'Interval' if bounds is None: bounds = (0, 1) - obj = Basic.__new__(cls, name) + obj = _new_basic(cls, name, coordinate, bounds) if coordinate: obj._coordinates = [coordinate] @@ -247,7 +267,7 @@ def __new__(cls, name=None, coordinate=None, bounds=None): @property def name(self): - return self.args[0] + return self.args[0].name @property def bounds(self): @@ -264,13 +284,20 @@ class Boundary(BasicDomain): """ def __new__(cls, name, domain, axis=None, ext=None, mapping=None, logical_domain=None): + name = _restore_string_argument(name) + axis = _restore_int_argument(axis) + ext = _restore_int_argument(ext) + mapping = _restore_optional_argument(mapping) + logical_domain = _restore_optional_argument(logical_domain) + if axis is not None: assert isinstance(axis, int) if ext is not None: assert isinstance(ext, int) - obj = Basic.__new__(cls, name, domain, axis, ext) + obj = _new_basic(cls, name, domain, axis, ext, + mapping, logical_domain) obj._mapping = mapping obj._logical_domain = logical_domain @@ -278,7 +305,7 @@ def __new__(cls, name, domain, axis=None, ext=None, mapping=None, logical_domain @property def name(self): - return self.args[0] + return self.args[0].name @property def domain(self): @@ -286,11 +313,13 @@ def domain(self): @property def axis(self): - return self.args[2] + axis = self.args[2] + return None if _is_none_argument(axis) else int(axis) @property def ext(self): - return self.args[3] + ext = self.args[3] + return None if _is_none_argument(ext) else int(ext) @property def mapping(self): @@ -470,7 +499,13 @@ class Interface(BasicDomain): and T. Dokken, E. Quak, V. Skytt. Requirements from Isogeometric Analysis for changes in product design ontologies, 2010. """ - def __new__(cls, name, bnd_minus, bnd_plus, *, mapping=None, logical_domain=None, ornt=None): + def __new__(cls, name, bnd_minus, bnd_plus, ornt=None, + mapping=None, logical_domain=None): + + name = _restore_string_argument(name) + ornt = _restore_container_argument(ornt) + mapping = _restore_optional_argument(mapping) + logical_domain = _restore_optional_argument(logical_domain) if not isinstance(name , str ): raise TypeError(name) if not isinstance(bnd_minus, Boundary): raise TypeError(bnd_minus) @@ -518,7 +553,8 @@ def __new__(cls, name, bnd_minus, bnd_plus, *, mapping=None, logical_domain=None # TODO [YG 10.02.2026]: relax this requirement ASAP assert bnd_minus.axis == bnd_plus.axis - obj = Basic.__new__(cls, name, bnd_minus, bnd_plus, ornt) + obj = _new_basic(cls, name, bnd_minus, bnd_plus, ornt, + mapping, logical_domain) obj._mapping = mapping obj._logical_domain = logical_domain return obj @@ -529,7 +565,7 @@ def dim(self): @property def name(self): - return self.args[0] + return self.args[0].name @property def minus(self): @@ -541,7 +577,12 @@ def plus(self): @property def ornt(self): - return self.args[3] + ornt = self.args[3] + if _is_none_argument(ornt): + return None + if isinstance(ornt, Tuple): + return tuple(int(o) for o in ornt) + return int(ornt) @property def axis(self): @@ -620,6 +661,7 @@ def __setitem__(self, key, value): self._data[key] = value # ========================================== + # abstract methods # ========================================== def __getitem__(self, key): @@ -639,5 +681,3 @@ def __lt__(self, other): return 0 # ========================================== - - diff --git a/sympde/topology/datatype.py b/sympde/topology/datatype.py index 6ba5d4b2..887c5616 100644 --- a/sympde/topology/datatype.py +++ b/sympde/topology/datatype.py @@ -5,18 +5,11 @@ # derivatives -from numpy import unique - from sympy.core import Basic -from sympy.tensor import Indexed, IndexedBase -from sympy.core import Symbol -from sympy.core import Expr -from sympy.core.containers import Tuple from sympy.core.singleton import Singleton -from sympde.old_sympy_utilities import with_metaclass #============================================================================== -class SpaceType(with_metaclass(Singleton, Basic)): +class SpaceType(Basic, metaclass=Singleton): """Base class representing function space types""" pass @@ -48,7 +41,7 @@ class UndefinedSpaceType(SpaceType): 'undefined': UndefinedSpace} #============================================================================== -class RegularityType(with_metaclass(Singleton, Basic)): +class RegularityType(Basic, metaclass=Singleton): """Base class representing the regularity of a space of functions""" _index = None @@ -108,4 +101,3 @@ class L2RegularityType(RegularityType): 'hcurl': HcurlRegularity, 'hdiv': HdivRegularity, 'l2': L2Regularity} - diff --git a/sympde/topology/derivatives.py b/sympde/topology/derivatives.py index 560024a6..87393bee 100644 --- a/sympde/topology/derivatives.py +++ b/sympde/topology/derivatives.py @@ -2,8 +2,6 @@ # TODO add action of diff operators on sympy known functions -from itertools import groupby - import numpy as np from sympy import Basic @@ -18,7 +16,7 @@ from sympy import log from sympy import preorder_traversal from sympy import cacheit -from sympde.old_sympy_utilities import is_sequence +from sympy.utilities.iterables import is_sequence from sympde.core.basic import CalculusFunction from sympde.core.basic import _coeffs_registery @@ -106,6 +104,10 @@ def eval(cls, expr): elif isinstance(expr, Expr): x = Symbol(cls.coordinate, real=True) if cls.logical: + # Imported lazily to avoid a module cycle: mapping depends + # on the differential-operator definitions in this module. + from .mapping import Jacobian, Mapping + M = expr.atoms(Mapping) if len(M)>0: M = list(M)[0] @@ -1287,6 +1289,3 @@ def get_max_logical_partial_derivatives(expr, F=None): for k,v in dd.items(): if v > d[k]: d[k] = v return d - -from .mapping import Mapping, Jacobian - diff --git a/sympde/topology/domain.py b/sympde/topology/domain.py index f7cefdea..53f0650c 100644 --- a/sympde/topology/domain.py +++ b/sympde/topology/domain.py @@ -1,13 +1,10 @@ # coding: utf-8 from __future__ import annotations -import numpy as np -import h5py -import yaml import os -from collections import abc, OrderedDict -from typing import Union as TypeUnion, Optional, List, Dict, Iterable, TYPE_CHECKING +from collections import OrderedDict +from typing import Union as TypeUnion, Optional, List, Iterable, TYPE_CHECKING # Union clashes with core.basic.Union from sympy import Integer @@ -15,11 +12,19 @@ from sympy.core import Basic, symbols from sympy.core.containers import Tuple from sympy.tensor import IndexedBase, Indexed -from sympy.core import Add, Mul, Pow +from sympy.core import Add from sympy.core.expr import AtomicExpr - -from sympde.old_sympy_utilities import is_sequence, with_metaclass -from sympde.core.basic import CalculusFunction +from sympy.utilities.iterables import is_sequence + +from sympde.core.basic import ( + CalculusFunction, + _is_none_argument, + _new_basic, + _restore_container_argument, + _restore_int_argument, + _restore_optional_argument, + _restore_string_argument, +) from .basic import BasicDomain, InteriorDomain, Boundary, Union, Connectivity from .basic import Interval, Interface, CornerBoundary, CornerInterface from .basic import ProductDomain @@ -41,7 +46,7 @@ class Domain(BasicDomain): name and connectivity need to be passed. """ - def __new__(cls, name : str, *, + def __new__(cls, name : str, *structural_args, interiors : TypeUnion[Iterable[InteriorDomain], InteriorDomain, None] = None, boundaries : TypeUnion[Iterable[Boundary], Boundary, None] = None, dim : Optional[int] = None, @@ -69,6 +74,27 @@ def __new__(cls, name : str, *, logical_domain : Domain or None, optional Logical domain that is mapped to the physical domain """ + if structural_args: + if len(structural_args) != 5: + raise TypeError( + 'A reconstructed Domain expects five structural arguments' + ) + if any(value is not None for value in ( + interiors, boundaries, dim, connectivity, + mapping, logical_domain)): + raise TypeError( + 'Structural and keyword Domain arguments cannot be mixed' + ) + interiors, boundaries, mapping, logical_domain, interfaces = structural_args + boundaries = _restore_optional_argument(boundaries) + mapping = _restore_optional_argument(mapping) + logical_domain = _restore_optional_argument(logical_domain) + interfaces = _restore_container_argument(interfaces) + connectivity = Connectivity({str(i.name): i for i in interfaces}) + + name = _restore_string_argument(name) + dim = _restore_int_argument(dim) + # ... if not isinstance(name, str): raise TypeError('> name must be a string') @@ -149,7 +175,8 @@ def __new__(cls, name : str, *, # ... boundaries = Union(*boundaries) - obj = Basic.__new__(cls, name, interiors, boundaries, mapping) + obj = _new_basic(cls, name, interiors, boundaries, mapping, + logical_domain, tuple(connectivity.values())) obj._connectivity = connectivity obj._corners = None obj._dtype = dtype @@ -159,7 +186,7 @@ def __new__(cls, name : str, *, @property def name(self) -> str: - return self.args[0] + return self.args[0].name @property def interior(self) -> TypeUnion[Union, InteriorDomain]: @@ -171,12 +198,14 @@ def interior(self) -> TypeUnion[Union, InteriorDomain]: def boundary(self) -> TypeUnion[Union, Boundary]: """Either a Union object containing the boundaries or just a boundary if there is only one""" - return self.args[2] + boundary = self.args[2] + return None if _is_none_argument(boundary) else boundary @property def mapping(self) -> Optional[Mapping]: """The mapping that maps the logical domain to the physical domain""" - return self.args[3] + mapping = self.args[3] + return None if _is_none_argument(mapping) else mapping @property def subdomains(self) -> tuple: @@ -337,6 +366,9 @@ def todict(self): return dict(sorted(d.items())) def export( self, filename ): + import h5py + import numpy as np + import yaml yml = self.todict() @@ -369,6 +401,9 @@ def from_file(cls, filename): Domain Multipatch domain. """ + import h5py + import yaml + # ... check extension of the file _, ext = os.path.splitext(filename) @@ -434,23 +469,19 @@ def from_file(cls, filename): @classmethod def join(cls, patches, connectivity, name): - """ - Create a multipatch domain by joining two or more patches in 2D or 3D. + """Create a multipatch domain by joining patches in 2D or 3D. Parameters ---------- - patches : list[Domain] - List of patches. - - connectivity : list - List of interfaces, identified by a tuple of 2 boundaries and an orientation - (bound_minus, bound_plus, ornt) where - - Each boundary is identified by a tuple of 3 integers: (patch, axis, ext) - with patches given as objects (or by their indices in the patches list) - and - - In 2D, ornt is an integer that can take the value of 1 or -1 - - In 3D, ornt is a tuple of 3 integers that can take the value of 1 or -1 - (see below for more details) + patches : sequence of Domain + Atomic patches in the joined domain. + + connectivity : sequence of tuple + Interface descriptions of the form ``(minus, plus, orientation)``. + Each side is ``(patch, axis, ext)``, where ``patch`` is a patch + object or its index in ``patches`` and ``ext`` is ``-1`` or ``1``. + A 2D orientation is ``-1`` or ``1``. A 3D orientation is a tuple + of three values, each equal to ``-1`` or ``1``. name : str Name of the domain. @@ -462,56 +493,19 @@ def join(cls, patches, connectivity, name): Notes ----- - The orientations are specified in the same manner as in GeoPDES, see e.g. - - and - T. Dokken, E. Quak, V. Skytt. Requirements from Isogeometric Analysis for changes in product design ontologies, 2010. - - Example - ------- - # list of patches (mapped domains) - Omega_0 = F0(A) - Omega_1 = F1(A) - Omega_2 = F2(A) - Omega_3 = F3(A) - - patches = [Omega_0, Omega_1, Omega_2, Omega_3] - - # integers representing the axes - axis_0 = 0 - axis_1 = 1 - axis_2 = 2 - - # integers representing the extremities: left (-1) or right (+1) - ext_0 = -1 - ext_1 = +1 - - # A connectivity list in 2D - connectivity = [((Omega_0, axis_0, ext_0), (Omega_1, axis_0, ext_1), 1), - ((Omega_1, axis_1, ext_0), (Omega_3, axis_1, ext_1), -1), - ((Omega_0, axis_1, ext_0), (Omega_2, axis_1, ext_1), 1), - ((Omega_2, axis_0, ext_0), (Omega_3, axis_0, ext_1), -1)] - - # alternative option (passing interface patches by their indices in the patches list): - connectivity = [((0, axis_0, ext_0), (1, axis_0, ext_1), 1), - ((1, axis_1, ext_0), (3, axis_1, ext_1), -1), - ((0, axis_1, ext_0), (2, axis_1, ext_1), 1), - ((2, axis_0, ext_0), (3, axis_0, ext_1), -1)] - - # A connectivity list in 3D - connectivity = [((Omega_0, axis_0, ext_1), (Omega_1, axis_0, ext_0), ( 1, 1, 1)), - ((Omega_0, axis_1, ext_1), (Omega_2, axis_1, ext_0), ( 1, -1, 1)), - ((Omega_1, axis_1, ext_1), (Omega_3, axis_1, ext_0), (-1, 1, -1)), - ((Omega_2, axis_0, ext_1), (Omega_3, axis_0, ext_0), (-1, 1, 1))] - - # alternative option (passing interface patches by their indices in the patches list): - connectivity = [((0, axis_0, ext_1), (1, axis_0, ext_0), ( 1, 1, 1)), - ((0, axis_1, ext_1), (2, axis_1, ext_0), ( 1, -1, 1)), - ((1, axis_1, ext_1), (3, axis_1, ext_0), (-1, 1, -1)), - ((2, axis_0, ext_1), (3, axis_0, ext_0), (-1, 1, 1))] - - # the multi-patch domain - Omega = Domain.join(patches=patches, connectivity=connectivity, name='Omega') + The orientation convention follows the `GeoPDEs multipatch geometry + specification `_. + + Examples + -------- + Join the right side of one square to the left side of another: + + >>> patch_a = Square('A') + >>> patch_b = Square('B') + >>> interfaces = [((0, 0, 1), (1, 0, -1), 1)] + >>> domain = Domain.join([patch_a, patch_b], interfaces, 'Omega') + >>> len(domain) + 2 """ assert isinstance(patches, (tuple, list)) assert isinstance(connectivity, (tuple, list)) @@ -812,8 +806,21 @@ class NCubeInterior(InteriorDomain): def __new__(cls, name, dim=None, dtype=None, min_coords=None, max_coords=None, mapping=None, logical_domain=None): - obj = InteriorDomain.__new__(cls, name, dim=dim, dtype=dtype, - mapping=mapping, logical_domain=logical_domain) + name = _restore_string_argument(name) + dim = _restore_int_argument(dim) + dtype = _restore_container_argument(dtype) + min_coords = _restore_container_argument(min_coords) + max_coords = _restore_container_argument(max_coords) + mapping = _restore_optional_argument(mapping) + logical_domain = _restore_optional_argument(logical_domain) + + obj = _new_basic(cls, name, dim, dtype, min_coords, max_coords, + mapping, logical_domain) + obj._dim = dim + obj._target = None + obj._dtype = dtype + obj._mapping = mapping + obj._logical_domain = logical_domain obj._min_coords = min_coords obj._max_coords = max_coords @@ -840,6 +847,14 @@ def min_coords(self): def max_coords(self): return self._max_coords + @property + def mapping(self): + return self._mapping + + @property + def logical_domain(self): + return self._logical_domain + @property def boundary(self): return self._boundary @@ -867,7 +882,17 @@ def get_boundary(self, axis=None, ext=None): # class NCube(Domain): - def __new__(cls, name, dim, min_coords, max_coords): + def __new__(cls, name, dim, min_coords, max_coords, *structural_args): + + if structural_args: + return Domain.__new__( + cls, name, dim, min_coords, max_coords, *structural_args + ) + + name = _restore_string_argument(name) + dim = _restore_int_argument(dim) + min_coords = _restore_container_argument(min_coords) + max_coords = _restore_container_argument(max_coords) assert isinstance(name, str) assert isinstance(dim, (int, Integer)) @@ -952,7 +977,9 @@ def max_coords(self): #============================================================================== class Line(NCube): - def __new__(cls, name='Line', bounds=(0, 1)): + def __new__(cls, name='Line', bounds=(0, 1), *structural_args): + if structural_args: + return Domain.__new__(cls, name, bounds, *structural_args) dim = 1 min_coords = (bounds[0],) max_coords = (bounds[1],) @@ -965,7 +992,12 @@ def bounds(self): #============================================================================== class Square(NCube): - def __new__(cls, name='Square', bounds1=(0, 1), bounds2=(0, 1)): + def __new__(cls, name='Square', bounds1=(0, 1), bounds2=(0, 1), + *structural_args): + if structural_args: + return Domain.__new__( + cls, name, bounds1, bounds2, *structural_args + ) dim = 2 min_coords = (bounds1[0], bounds2[0]) max_coords = (bounds1[1], bounds2[1]) @@ -982,7 +1014,12 @@ def bounds2(self): #============================================================================== class Cube(NCube): - def __new__(cls, name='Cube', bounds1=(0, 1), bounds2=(0, 1), bounds3=(0, 1)): + def __new__(cls, name='Cube', bounds1=(0, 1), bounds2=(0, 1), + bounds3=(0, 1), *structural_args): + if structural_args: + return Domain.__new__( + cls, name, bounds1, bounds2, bounds3, *structural_args + ) dim = 3 min_coords = (bounds1[0], bounds2[0], bounds3[0]) max_coords = (bounds1[1], bounds2[1], bounds3[1]) @@ -1017,7 +1054,7 @@ class TangentVector(BoundaryVector): pass #============================================================================== -class ElementDomain(with_metaclass(Singleton, Basic)): +class ElementDomain(Basic, metaclass=Singleton): pass #============================================================================== @@ -1141,5 +1178,3 @@ def split(domain, value): else: raise NotImplementedError('TODO') - - diff --git a/sympde/topology/mapping.py b/sympde/topology/mapping.py index c4844316..c7666475 100644 --- a/sympde/topology/mapping.py +++ b/sympde/topology/mapping.py @@ -9,21 +9,23 @@ from sympy.core import Symbol,Integer from sympy.core import Add, Mul, Pow from sympy.core.numbers import ImaginaryUnit -from sympy.core.containers import Tuple +from sympy.core.containers import Dict, Tuple from sympy import S from sympy import sqrt, symbols from sympy.core.exprtools import factor_terms from sympy.polys.polytools import parallel_poly_from_expr +from sympy.utilities.iterables import is_sequence -from sympde.core import Constant +from sympde.core.basic import Constant from sympde.core.basic import BasicMapping from sympde.core.basic import CalculusFunction from sympde.core.basic import _coeffs_registery +from sympde.core.basic import _new_basic from sympde.calculus.core import PlusInterfaceOperator, MinusInterfaceOperator from sympde.calculus.core import grad, div, curl, laplace #, hessian from sympde.calculus.core import dot, inner, outer, _diff_ops from sympde.calculus.core import has, DiffOperator -from sympde.calculus.matrices import MatrixSymbolicExpr, MatrixElement, SymbolicTrace, Inverse +from sympde.calculus.matrices import MatrixSymbolicExpr, MatrixElement, SymbolicTrace from sympde.calculus.matrices import SymbolicDeterminant, Transpose from .basic import BasicDomain, Union, InteriorDomain @@ -560,12 +562,17 @@ def _eval_simplify(self, **kwargs): class MultiPatchMapping(Mapping): def __new__(cls, dic): - assert isinstance( dic, dict) - return Basic.__new__(cls, dic) + if isinstance(dic, Dict): + dic = dict(dic.items()) + if not isinstance(dic, dict): + raise TypeError('Expecting a dictionary of patch mappings') + obj = _new_basic(cls, dic) + obj._mappings = dict(dic) + return obj @property def mappings(self): - return self.args[0] + return self._mappings @property def is_analytical(self): @@ -589,8 +596,8 @@ def _eval_subs(self, old, new): def _eval_simplify(self, **kwargs): return self - def __hash__(self): - return hash((*self.mappings.values(), *self.mappings.keys())) + def _hashable_content(self): + return self.args def _sympystr(self, printer): sstr = printer.doprint @@ -751,12 +758,12 @@ def eval(cls, F): """ this class methods computes the jacobian of a mapping - Parameters: + Parameters ---------- F: Mapping mapping object - Returns: + Returns ---------- expr : ImmutableDenseMatrix the jacobian matrix @@ -799,7 +806,7 @@ def eval(cls, F, v): """ This class methods computes the covariant transformation - Parameters: + Parameters ---------- F: Mapping mapping object @@ -807,7 +814,7 @@ def eval(cls, F, v): v: the basis function - Returns: + Returns ---------- expr : Tuple the covariant transformation @@ -848,7 +855,7 @@ def eval(cls, F, v): """ This class methods computes the contravariant transformation - Parameters: + Parameters ---------- F: Mapping mapping object @@ -856,7 +863,7 @@ def eval(cls, F, v): v: the basis function - Returns: + Returns ---------- expr : Tuple the contravariant transformation @@ -911,7 +918,7 @@ def eval(cls, expr, domain, **options): """.""" from sympde.expr.evaluation import TerminalExpr, DomainExpression - from sympde.expr.expr import BilinearForm, LinearForm, BasicForm, Norm + from sympde.expr.expr import BilinearForm, LinearForm, Norm from sympde.expr.expr import Integral types = (ScalarFunction, VectorFunction, DifferentialOperator, Trace, Integral) diff --git a/sympde/topology/space.py b/sympde/topology/space.py index cce1f556..6495bb6a 100644 --- a/sympde/topology/space.py +++ b/sympde/topology/space.py @@ -16,6 +16,7 @@ from sympde.core.utils import random_string from sympde.core.basic import CalculusFunction from sympde.core.basic import _coeffs_registery +from sympde.core.basic import _new_basic from .basic import BasicDomain, Union, Interval from .datatype import SpaceType, dtype_space_registry @@ -219,6 +220,10 @@ def __mul__(self, other): def __hash__(self): return hash((self.name, self.domain, self.shape, self.kind)) + + def _hashable_content(self): + return (self.name, self.domain, self.shape, self.kind) + #============================================================================== class ScalarFunctionSpace(BasicFunctionSpace): """ @@ -438,19 +443,14 @@ class ScalarFunction(Symbol): def __new__(cls, space, name): if not isinstance(space, ScalarFunctionSpace): raise ValueError('Expecting a ScalarFunctionSpace') - obj = Expr.__new__(cls) + obj = Symbol.__xnew__(cls, name, commutative=True) obj._space = space - obj._name = name return obj @property def space(self): return self._space - @property - def name(self): - return self._name - @property def ldim(self): return self.space.ldim @@ -469,8 +469,9 @@ def _sympystr(self, printer): sstr = printer.doprint return sstr(self.name) - def __hash__(self): - return hash((self.name, self.space)) + def _hashable_content(self): + return Symbol._hashable_content(self) + (self.space,) + #============================================================================== # this class is needed, otherwise sympy will convert VectorFunction to # IndexedBase @@ -547,19 +548,14 @@ class VectorFunction(Symbol, IndexedBase): def __new__(cls, space, name): if not isinstance(space, VectorFunctionSpace): raise ValueError('Expecting a VectorFunctionSpace') - obj = Expr.__new__(cls) + obj = Symbol.__xnew__(cls, name, commutative=False) obj._space = space - obj._name = name return obj @property def space(self): return self._space - @property - def name(self): - return self._name - @property def shape(self): # we return a list to make it compatible with IndexedBase sympy object @@ -595,8 +591,8 @@ def _sympystr(self, printer): sstr = printer.doprint return sstr(self.name) - def __hash__(self): - return hash((self.name, self.space)) + def _hashable_content(self): + return Symbol._hashable_content(self) + (self.space,) #============================================================================== # this is implemented as a function, it would be better to have it as a class @@ -646,7 +642,7 @@ def __new__(cls, expr, boundary, order=0, **options): if options.pop('evaluate',True): return cls.eval(expr, boundary, order) - obj = Basic.__new__(cls, expr, boundary, order) + obj = _new_basic(cls, expr, boundary, order) obj.is_commutative = expr.is_commutative return obj @@ -660,7 +656,7 @@ def boundary(self): @property def order(self): - return self._args[2] + return int(self._args[2]) @classmethod def eval(cls, expr, boundary, order): diff --git a/sympde/topology/tests/test_derivatives.py b/sympde/topology/tests/test_derivatives.py index 3694adfe..a79192c5 100644 --- a/sympde/topology/tests/test_derivatives.py +++ b/sympde/topology/tests/test_derivatives.py @@ -5,14 +5,14 @@ from sympy import Matrix from sympy import srepr -from sympde.core import Constant -from sympde.calculus import grad, dot, inner -from sympde.topology import Domain, element_of -from sympde.topology import get_index_derivatives_atom -from sympde.topology import get_max_partial_derivatives -from sympde.topology import ScalarFunctionSpace -from sympde.topology import (dx, dy, dz) -from sympde.topology import Mapping +from sympde.api import Constant +from sympde.api import grad, dot, inner +from sympde.api import Domain, element_of +from sympde.api import get_index_derivatives_atom +from sympde.api import get_max_partial_derivatives +from sympde.api import ScalarFunctionSpace +from sympde.api import (dx, dy, dz) +from sympde.api import Mapping def indices_as_str(a): diff --git a/sympde/topology/tests/test_field_2d.py b/sympde/topology/tests/test_field_2d.py index 91e72212..728f93a2 100644 --- a/sympde/topology/tests/test_field_2d.py +++ b/sympde/topology/tests/test_field_2d.py @@ -2,11 +2,11 @@ from sympy import Matrix -from sympde.calculus import grad, inner -from sympde.topology import Domain -from sympde.topology import dx -from sympde.topology import VectorFunctionSpace -from sympde.topology import element_of +from sympde.api import grad, inner +from sympde.api import Domain +from sympde.api import dx +from sympde.api import VectorFunctionSpace +from sympde.api import element_of #============================================================================== diff --git a/sympde/topology/tests/test_gallery.py b/sympde/topology/tests/test_gallery.py index 9aaee6e3..8622dffa 100644 --- a/sympde/topology/tests/test_gallery.py +++ b/sympde/topology/tests/test_gallery.py @@ -4,8 +4,8 @@ from sympy import Tuple from sympy import symbols -from sympde.topology import Interval, ProductDomain, InteriorDomain, Domain -from sympde.topology import Line, Square, Cube, NCubeInterior +from sympde.api import Interval, ProductDomain, InteriorDomain, Domain +from sympde.api import Line, Square, Cube, NCubeInterior x1, x2, x3 = symbols('x1, x2, x3', real=True) diff --git a/sympde/topology/tests/test_logical_expr.py b/sympde/topology/tests/test_logical_expr.py index b66d3d86..18943f6b 100644 --- a/sympde/topology/tests/test_logical_expr.py +++ b/sympde/topology/tests/test_logical_expr.py @@ -5,27 +5,28 @@ from sympy import expand from sympy import cos, sin, sqrt, pi -from sympde.core import Constant -from sympde.calculus import grad, dot, inner, rot, div -from sympde.calculus import laplace, bracket, convect -from sympde.calculus import jump, avg, Dn, minus, plus -from sympde.topology import Domain, Mapping, Square -from sympde.topology import dx, dy -from sympde.topology import dx1, dx2, dx3 -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace -from sympde.topology import element_of, elements_of -from sympde.topology import LogicalExpr -from sympde.topology import SymbolicExpr -from sympde.topology import IdentityMapping -from sympde.topology import PolarMapping -from sympde.topology import TargetMapping -from sympde.topology import CzarnyMapping -from sympde.topology import CollelaMapping2D -from sympde.topology import TorusMapping -from sympde.topology import TwistedTargetMapping - -from sympde.expr import BilinearForm, integral -from sympde.calculus import grad, div, curl, dot +from sympde.api import Constant +from sympde.api import grad, dot, inner, rot, div +from sympde.api import laplace, bracket, convect +from sympde.api import jump, avg, Dn, minus, plus +from sympde.api import Domain, Mapping, Square +from sympde.api import dx, dy +from sympde.api import dx1, dx2, dx3 +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import element_of, elements_of +from sympde.api import LogicalExpr +from sympde.api import get_logical_test_function +from sympde.api import SymbolicExpr +from sympde.api import IdentityMapping +from sympde.api import PolarMapping +from sympde.api import TargetMapping +from sympde.api import CzarnyMapping +from sympde.api import CollelaMapping2D +from sympde.api import TorusMapping +from sympde.api import TwistedTargetMapping + +from sympde.api import BilinearForm, integral +from sympde.api import grad, div, curl, dot from sympde.topology.mapping import Jacobian @@ -426,11 +427,10 @@ def test_logical_expr_3d_2(): mapped_domain = M(domain) - V = ScalarFunctionSpace('V' , domain, kind='h1') VM = ScalarFunctionSpace('VM', mapped_domain, kind='h1') - u,v = elements_of(V, names='u,v') um,vm = elements_of(VM, names='u,v') + u, v = [get_logical_test_function(field) for field in (um, vm)] J = M.jacobian @@ -448,11 +448,10 @@ def test_logical_expr_3d_3(): mapped_domain = M(domain) - V = VectorFunctionSpace('V' , domain, kind='hcurl') VM = VectorFunctionSpace('VM', mapped_domain, kind='hcurl') - u,v = elements_of(V, names='u,v') um,vm = elements_of(VM, names='u,v') + u, v = [get_logical_test_function(field) for field in (um, vm)] J = M.jacobian @@ -469,11 +468,10 @@ def test_logical_expr_3d_4(): mapped_domain = M(domain) - V = VectorFunctionSpace('V' , domain, kind='hdiv') VM = VectorFunctionSpace('VM', mapped_domain, kind='hdiv') - u,v = elements_of(V, names='u,v') um,vm = elements_of(VM, names='u,v') + u, v = [get_logical_test_function(field) for field in (um, vm)] J = M.jacobian @@ -490,12 +488,11 @@ def test_logical_expr_3d_5(): mapped_domain = M(domain) - V = VectorFunctionSpace('V' , domain, kind='hcurl') VM = VectorFunctionSpace('VM', mapped_domain, kind='hcurl') J = M.jacobian - u,v = elements_of(V, names='u,v') um,vm = elements_of(VM, names='u,v') + u, v = [get_logical_test_function(field) for field in (um, vm)] int_md = lambda expr: integral(mapped_domain , expr) int_ld = lambda expr: integral(domain , expr) @@ -617,10 +614,12 @@ def test_identity_mapping_2d_2(): V = ScalarFunctionSpace('V', domain, kind='h1') u = element_of(V, name='u') + logical_u = get_logical_test_function(u) # ... - assert(LogicalExpr(dx(u), domain) == dx1(u)) - assert(LogicalExpr(dy(u), domain) == dx2(u)) + assert logical_u.space.domain == domain.logical_domain + assert(LogicalExpr(dx(u), domain) == dx1(logical_u)) + assert(LogicalExpr(dy(u), domain) == dx2(logical_u)) # ... #============================================================================== diff --git a/sympde/topology/tests/test_mapping.py b/sympde/topology/tests/test_mapping.py index 8d7c6798..9c8e31b6 100644 --- a/sympde/topology/tests/test_mapping.py +++ b/sympde/topology/tests/test_mapping.py @@ -7,12 +7,31 @@ from sympy.tensor import IndexedBase from sympy import symbols, simplify -from sympde.topology import Mapping, MappedDomain, AffineMapping -from sympde.topology import dx, dy, dz -from sympde.topology import dx1, dx2, dx3 -from sympde.topology import Domain - -from sympde.topology.mapping import Jacobian, Covariant, Contravariant +from sympde.api import IdentityMapping, Mapping, MappedDomain, AffineMapping +from sympde.api import dx, dy, dz +from sympde.api import dx1, dx2, dx3 +from sympde.api import Domain, Square + +from sympde.topology.mapping import ( + Contravariant, + Covariant, + Jacobian, + MultiPatchMapping, +) + + +def test_multipatch_mapping_reconstruction(): + patch = Square('A') + mapping = IdentityMapping('F', dim=2) + replacement = IdentityMapping('G', dim=2) + multipatch_mapping = MultiPatchMapping({patch: mapping}) + + rebuilt = multipatch_mapping.func(*multipatch_mapping.args) + replaced = multipatch_mapping.xreplace({mapping: replacement}) + + assert rebuilt == multipatch_mapping + assert rebuilt.mappings == multipatch_mapping.mappings + assert replaced.mappings == {patch: replacement} # ... def test_mapping_1d(): print('============ test_mapping_1d ==============') @@ -157,4 +176,3 @@ def teardown_module(): def teardown_function(): from sympy.core import cache cache.clear_cache() - diff --git a/sympde/topology/tests/test_space.py b/sympde/topology/tests/test_space.py index 793afe55..2b33233d 100644 --- a/sympde/topology/tests/test_space.py +++ b/sympde/topology/tests/test_space.py @@ -1,13 +1,42 @@ # coding: utf-8 -from sympde.calculus import grad, div -from sympde.topology import Domain -from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace +from sympde.api import grad, div +from sympde.api import Domain +from sympde.api import ScalarFunctionSpace, VectorFunctionSpace #from sympde.topology import ProductSpace -from sympde.topology import element_of -from sympde.topology import H1Space, HcurlSpace, HdivSpace, L2Space, UndefinedSpace -from sympde.topology import ScalarFunction, VectorFunction -from sympde.topology import Projector +from sympde.api import element_of +from sympde.api import H1Space, HcurlSpace, HdivSpace, L2Space, UndefinedSpace +from sympde.api import ScalarFunction, VectorFunction +from sympde.api import Projector + + +def test_function_symbol_identity(): + domain = Domain('Omega', dim=2) + scalar_space = ScalarFunctionSpace('V', domain) + equivalent_scalar_space = ScalarFunctionSpace('V', domain) + other_scalar_space = ScalarFunctionSpace('W', domain) + vector_space = VectorFunctionSpace('X', domain) + other_vector_space = VectorFunctionSpace('Y', domain) + + u = ScalarFunction(scalar_space, 'u') + same_u = ScalarFunction(equivalent_scalar_space, 'u') + other_u = ScalarFunction(other_scalar_space, 'u') + v = VectorFunction(vector_space, 'v') + other_v = VectorFunction(other_vector_space, 'v') + + assert scalar_space == equivalent_scalar_space + assert hash(scalar_space) == hash(equivalent_scalar_space) + assert scalar_space != other_scalar_space + assert u == same_u + assert hash(u) == hash(same_u) + assert u != other_u + assert v != other_v + assert u + other_u != 2 * u + assert (u + other_u).free_symbols == {u, other_u} + assert u.is_commutative + assert not v.is_commutative + assert u.free_symbols == {u} + assert v.free_symbols == {v} #============================================================================== def test_space_1d_1(): diff --git a/sympde/topology/tests/test_symbolic_expr.py b/sympde/topology/tests/test_symbolic_expr.py index 43d1151c..93b0b69a 100644 --- a/sympde/topology/tests/test_symbolic_expr.py +++ b/sympde/topology/tests/test_symbolic_expr.py @@ -1,12 +1,12 @@ from sympy import Symbol from sympy import sympify -from sympde.topology import Domain, ScalarFunctionSpace, element_of -from sympde.topology import dx, dy, dz -from sympde.topology import dx1, dx2, dx3 -from sympde.topology import Mapping, Jacobian -from sympde.topology import LogicalExpr -from sympde.topology import SymbolicExpr +from sympde.api import Domain, ScalarFunctionSpace, element_of +from sympde.api import dx, dy, dz +from sympde.api import dx1, dx2, dx3 +from sympde.api import Mapping, Jacobian +from sympde.api import LogicalExpr +from sympde.api import SymbolicExpr #============================================================================== def test_derivatives_2d_without_mapping(): diff --git a/sympde/topology/tests/test_topology.py b/sympde/topology/tests/test_topology.py index 704affc0..26740834 100644 --- a/sympde/topology/tests/test_topology.py +++ b/sympde/topology/tests/test_topology.py @@ -1,16 +1,46 @@ import os -from sympde.topology import InteriorDomain, Union -from sympde.topology import Boundary -from sympde.topology import Domain, ElementDomain -from sympde.topology import Area, Mapping -from sympde.topology import Interface -from sympde.topology import Line, Square, Cube -from sympde.topology import IdentityMapping +from sympy import preorder_traversal +from sympy.core import Basic + +from sympde.api import InteriorDomain, Union +from sympde.api import Boundary +from sympde.api import Domain, ElementDomain +from sympde.api import Area, Mapping +from sympde.api import Interface +from sympde.api import Line, Square, Cube +from sympde.api import IdentityMapping base_dir = os.path.dirname(os.path.realpath(__file__)) topo_dir = os.path.join(base_dir, 'data') + +def test_structural_arguments_are_sympy_objects(): + domain = Domain('Omega', dim=2) + boundary = Boundary(r'\Gamma_1', domain) + interface = Interface('I', boundary, boundary, ornt=1) + square = Square('S', bounds1=(-1, 2), bounds2=(3, 4)) + + assert domain.name == 'Omega' + assert boundary.name == r'\Gamma_1' + assert boundary.axis is None + assert interface.ornt == 1 + + for expr in (domain, domain.interior, boundary, interface, square, + square.interior, square.boundary.args[0]): + assert all(isinstance(arg, Basic) for arg in preorder_traversal(expr)) + assert expr.func(*expr.args) == expr + + replacement = Domain('Replacement', dim=2) + replaced_boundary = boundary.xreplace({domain: replacement}) + assert replaced_boundary.domain == replacement + + replacement_boundary = Boundary(r'\Gamma_2', domain) + replaced_interface = interface.xreplace({boundary: replacement_boundary}) + assert replaced_interface.minus == replacement_boundary + assert replaced_interface.plus == replacement_boundary + + #============================================================================== def test_interior_domain(): D1 = InteriorDomain('D1', dim=2) diff --git a/sympde/utilities/tests/test_utilities.py b/sympde/utilities/tests/test_utilities.py index f6fbfe55..d2502570 100644 --- a/sympde/utilities/tests/test_utilities.py +++ b/sympde/utilities/tests/test_utilities.py @@ -4,6 +4,8 @@ from sympy import Matrix, symbols, Array from sympy import S from sympde.utilities.utils import lambdify_sympde +from sympde.utilities.utils import plot_domain +from sympde.api import Square @@ -53,6 +55,13 @@ def test_lambdify_sympde_1d(): assert np.array_equal(array_input_da, expected_a_da) +def test_plot_unmapped_2d_domain(monkeypatch, tmp_path): + """Plotting an unmapped square must create a 2D identity mapping.""" + monkeypatch.setenv('MPLBACKEND', 'Agg') + monkeypatch.setenv('MPLCONFIGDIR', str(tmp_path)) + plot_domain(Square('Omega'), draw=False) + + def test_lambdify_sympde_2d(): x,y = symbols("x, y") diff --git a/sympde/utilities/utils.py b/sympde/utilities/utils.py index 3496a1df..164957ae 100644 --- a/sympde/utilities/utils.py +++ b/sympde/utilities/utils.py @@ -2,11 +2,9 @@ import itertools as it from sympy import lambdify -from mpl_toolkits.mplot3d import * -import matplotlib.pyplot as plt - -from sympde.topology import IdentityMapping, InteriorDomain, MultiPatchMapping -from sympde.topology.analytical_mapping import TorusMapping +from sympde.topology.analytical_mapping import IdentityMapping +from sympde.topology.basic import InteriorDomain +from sympde.topology.mapping import MultiPatchMapping def lambdify_sympde(variables, expr): """ @@ -134,6 +132,8 @@ def plot_2d(domain, draw=True, isolines=False, refinement=40): refinement : int Number of straight line segments used to approximate each boundary edge. """ + import matplotlib.pyplot as plt + fig = plt.figure() ax = fig.add_subplot(111) @@ -168,6 +168,8 @@ def plot_3d(domain, draw=True, refinement=15): refinement : int Number of straight line segments used to approximate each boundary edge. """ + import matplotlib.pyplot as plt + mapping = domain.mapping fig = plt.figure() @@ -265,7 +267,7 @@ def plot_2d_single_patch(patch, mapping, ax, isolines=False, refinement=40): Number of straight line segments used to approximate each boundary edge. """ if mapping is None: - mapping = IdentityMapping('Id', dim=3) + mapping = IdentityMapping('Id', dim=2) refinement+=1 map_call = mapping.get_callable_mapping() @@ -291,7 +293,8 @@ def plot_2d_single_patch(patch, mapping, ax, isolines=False, refinement=40): ax.plot(X_11, Y_11, 'k') if __name__ == '__main__': - from sympde.topology import Square, PolarMapping + from sympde.topology.analytical_mapping import PolarMapping + from sympde.topology.domain import Square A = Square('A', bounds1=(0, 1), bounds2=(0, np.pi/2)) F = PolarMapping('F', c1=0, c2=0, rmin=0.5, rmax=1) Omega = F(A)