├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── README.rst ├── appveyor.yml ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── api │ ├── api.rst │ ├── approx.rst │ ├── assemble.rst │ ├── bspline.rst │ ├── geometry.rst │ ├── hierarchical.rst │ ├── operators.rst │ ├── solvers.rst │ ├── tensor.rst │ ├── vform.rst │ └── vis.rst │ ├── conf.py │ ├── guide │ ├── geometry.ipynb │ ├── guide.rst │ └── vforms.rst │ └── index.rst ├── notebooks ├── adaptive.ipynb ├── geometry.ipynb ├── multipatch.ipynb ├── solve-convdiff.ipynb ├── solve-navier-stokes.ipynb ├── solve-poisson.ipynb ├── solve-stokes.ipynb └── subspace-correction-mg.ipynb ├── pyiga ├── __init__.py ├── _hdiscr.py ├── approx.py ├── assemble.py ├── assemble_tools.py ├── assemble_tools_cy.pyx ├── assemblers.pyx ├── bspline.py ├── bspline_cy.pyx ├── codegen │ ├── __init__.py │ └── cython.py ├── compile.py ├── fast_assemble_cy.pxd ├── fast_assemble_cy.pyx ├── fastasm.cc ├── genericasm.pxi ├── geometry.py ├── hierarchical.py ├── kronecker.py ├── lowrank.py ├── lowrank_cy.pyx ├── mlmatrix.py ├── mlmatrix_cy.pyx ├── operators.py ├── quadrature.py ├── relaxation_cy.pyx ├── solvers.py ├── spline.py ├── stilde.py ├── tensor.py ├── utils.py ├── vform.py └── vis.py ├── pyproject.toml ├── run-notebooks.py ├── scripts ├── asm-codegen.py ├── clear-cache.py ├── download_appveyor.py ├── generate-assemblers.py ├── setversion.py └── str2asm.py ├── setup.py └── test ├── __init__.py ├── poisson_neu_d2_p3_n15_mass.mtx.gz ├── poisson_neu_d2_p3_n15_stiff.mtx.gz ├── poisson_neu_d3_p2_n10_mass.mtx.gz ├── poisson_neu_d3_p2_n10_stiff.mtx.gz ├── test_approx.py ├── test_assemble.py ├── test_bspline.py ├── test_codegen.py ├── test_geometry.py ├── test_hierarchical.py ├── test_kronecker.py ├── test_localmg.py ├── test_lowrank.py ├── test_mlmatrix.py ├── test_operators.py ├── test_solve.py ├── test_solvers.py ├── test_spline.py ├── test_stilde.py ├── test_tensor.py ├── test_utils.py ├── test_vform.py └── test_vis.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: GH build 5 | 6 | on: [push] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: ["3.10", "3.11", "3.12", "3.13"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install flake8 pytest pytest-cov coverage codecov matplotlib 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Compile and install 35 | run: | 36 | python -m pip install . -v 37 | - name: Test with pytest 38 | run: | 39 | cd test && pytest -v --cov=pyiga --cov-report=xml --import-mode=importlib 40 | - name: Upload coverage data 41 | run: | 42 | codecov 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | __pycache__ 3 | *.swp 4 | *_cy.c 5 | *_cy.cpp 6 | pyiga/assemblers.c 7 | *_cy.html 8 | /build/ 9 | /dist/ 10 | *.egg-info/ 11 | .cache 12 | /docs/build/ 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | jobs: 13 | pre_build: 14 | - python -m pip install . 15 | 16 | # Build documentation in the "docs/" directory with Sphinx 17 | sphinx: 18 | configuration: docs/source/conf.py 19 | 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | pyiga is free for academic use. 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. |ghbuild| image:: https://github.com/c-f-h/pyiga/actions/workflows/python-package.yml/badge.svg 3 | :target: https://github.com/c-f-h/pyiga/actions/workflows/python-package.yml 4 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/1enc32o4ts2w9w17/branch/master?svg=true 5 | :target: https://ci.appveyor.com/project/c-f-h/pyiga 6 | .. |codecov| image:: https://codecov.io/gh/c-f-h/pyiga/branch/master/graph/badge.svg 7 | :target: https://codecov.io/gh/c-f-h/pyiga 8 | 9 | pyiga |ghbuild| |appveyor| |codecov| 10 | ==================================== 11 | 12 | ``pyiga`` is a Python research toolbox for Isogeometric Analysis. Its current highlights are: 13 | 14 | * Automatic generation of efficient matrix assembling code from a high-level, FEniCS_-like description of the bilinear form. See the section "Assembling custom forms" in the `Documentation`_ as well as the convection-diffusion and Navier-Stokes examples below. 15 | * Adaptivity via HB- and THB-spline spaces and a local multigrid solver for adaptive IgA (`read the paper `_). See `adaptive.ipynb `_ for an example. 16 | * Fast assembling by a black-box low-rank assembling algorithm described in 17 | `this paper `_ 18 | (or `this technical report `_). 19 | * Extensive support for fast tensor approximation methods for tensor product IgA. 20 | 21 | To find out more, have a look at the `Documentation`_ and the examples below. 22 | 23 | Examples 24 | -------- 25 | 26 | The ``notebooks`` directory contains several examples of how to use ``pyiga``: 27 | 28 | * `geometry.ipynb `_: create and manipulate geometry functions 29 | * `solve-poisson.ipynb `_: solve a Poisson equation and plot the solution 30 | * `multipatch.ipynb `_: solve a Poisson equation in a multipatch domain 31 | * `solve-convdiff.ipynb `_: solve a convection-diffusion problem with random inclusions 32 | * `solve-stokes.ipynb `_: solve stationary Stokes flow and plot the velocity field 33 | * `solve-navier-stokes.ipynb `_: solve the instationary Navier-Stokes equations with a time-adaptive Rosenbrock integrator and produce an animation of the result 34 | * `adaptive.ipynb `_: an adaptive solve-estimate-mark-refine loop using a local multigrid solver. 35 | * `mantle-convection.ipynb `_: Rayleigh-Bénard convection 36 | 37 | 38 | Installation 39 | ------------ 40 | 41 | ``pyiga`` is compatible with Python 3.10 and higher on Linux and Windows. 42 | MacOS is currently untested but might work. 43 | 44 | Before installing, make sure that your environment can compile Python extension 45 | modules; on Linux, this should work out of the box with gcc, but on Windows you need 46 | Microsoft Visual Studio or the Microsoft Build Tools. 47 | Alternatively, installing on WSL (Windows Subsystem for Linux) works very well on Windows too. 48 | You can use either a standard Python distribution or Anaconda_. 49 | 50 | To install ``pyiga``, clone this repository and execute :: 51 | 52 | $ python -m pip install . 53 | 54 | in the main directory. The installation script should now compile the Cython 55 | extensions and then install the package. 56 | 57 | If you have Intel MKL installed on your machine, be sure to install the 58 | **pyMKL** package; if ``pyiga`` detects this package, it will use the 59 | MKL PARDISO sparse direct solver instead of the internal scipy solver 60 | (typically SuperLU). 61 | 62 | In order to run the included Jupyter notebooks, you might also want to install **jupyterlab**, 63 | **matplotlib**, and **sympy**. 64 | 65 | 66 | Updating 67 | ~~~~~~~~ 68 | 69 | If you have already installed the package and want to update to the latest 70 | version, assuming that you have cloned it from Github, you can simply move to 71 | the project directory and execute :: 72 | 73 | $ git pull 74 | $ python -m pip install . 75 | 76 | Running tests 77 | ------------- 78 | 79 | `pyiga` comes with a small test suite to test basic functionality. 80 | To run them, first install ``pip install pytest``, then move to 81 | the ``test`` subdirectory and execute :: 82 | 83 | $ python -m pytest -v --import-mode=importlib 84 | 85 | If the test runner fails to find the Cython extensions modules (``pyiga.bspline_cy`` etc.), 86 | try running ``python setup.py build_ext -i`` to build them in-place. 87 | 88 | Usage 89 | ----- 90 | 91 | After successful installation, you should be able to load the package. A simple example: 92 | 93 | .. code:: python 94 | 95 | from pyiga import bspline, geometry, assemble 96 | 97 | kv = bspline.make_knots(3, 0.0, 1.0, 50) # knot vector over (0,1) with degree 3 and 50 knot spans 98 | geo = geometry.quarter_annulus() # a NURBS representation of a quarter annulus 99 | K = assemble.stiffness((kv,kv), geo=geo) # assemble a stiffness matrix for the 2D tensor product 100 | # B-spline basis over the quarter annulus 101 | 102 | There is a relatively complete `Documentation`_. Beyond that, look at the code, 103 | the unit tests, and the `IPython notebooks`_ to learn more. 104 | 105 | 106 | .. _IPython notebooks: ./notebooks 107 | .. _Documentation: http://pyiga.readthedocs.io/latest/ 108 | .. _FEniCS: https://fenicsproject.org/ 109 | .. _Anaconda: https://www.anaconda.com/distribution/ 110 | 111 | FAQ 112 | --- 113 | 114 | During compilation, I get an error message involving ``numpy._build_utils``. 115 | ~~~~~ 116 | 117 | Try installing/upgrading setuptools: :: 118 | 119 | $ pip install --upgrade setuptools 120 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | 3 | environment: 4 | matrix: 5 | - PYTHON: "C:\\Python310-x64" 6 | PYTHON_VERSION: "3.10" 7 | PYTHON_ARCH: "64" 8 | - PYTHON: "C:\\Python311-x64" 9 | PYTHON_VERSION: "3.11" 10 | PYTHON_ARCH: "64" 11 | - PYTHON: "C:\\Python312-x64" 12 | PYTHON_VERSION: "3.12" 13 | PYTHON_ARCH: "64" 14 | - PYTHON: "C:\\Python313-x64" 15 | PYTHON_VERSION: "3.13" 16 | PYTHON_ARCH: "64" 17 | 18 | init: 19 | - "ECHO Python %PYTHON_VERSION% %PYTHON_ARCH% bit" 20 | 21 | install: 22 | - "%PYTHON%\\python.exe -m pip install --upgrade pip" 23 | - "%PYTHON%\\python.exe -m pip install pytest matplotlib" 24 | - "%PYTHON%\\python.exe -m pip install . -v" 25 | 26 | build: off 27 | 28 | test_script: 29 | - "cd test" 30 | - "%PYTHON%\\python.exe -m pytest -v --import-mode=importlib" 31 | 32 | after_test: 33 | # build the wheels 34 | #- "%PYTHON%\\python.exe -m build --wheel" 35 | 36 | artifacts: 37 | # bdist_wheel puts the built wheel in the dist directory 38 | #- path: dist\* 39 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pyiga 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=pyiga 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.1.3 2 | nbsphinx 3 | sphinx-rtd-theme 4 | ipykernel 5 | matplotlib 6 | -------------------------------------------------------------------------------- /docs/source/api/api.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | API Reference 3 | ============= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: API Reference 8 | 9 | bspline 10 | geometry 11 | approx 12 | assemble 13 | Hierarchical 14 | operators 15 | tensor 16 | solvers 17 | vis 18 | vform 19 | -------------------------------------------------------------------------------- /docs/source/api/approx.rst: -------------------------------------------------------------------------------- 1 | Approximation 2 | ============= 3 | 4 | .. automodule:: pyiga.approx 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/api/assemble.rst: -------------------------------------------------------------------------------- 1 | Assembling 2 | ================== 3 | 4 | .. automodule:: pyiga.assemble 5 | 6 | -------------------------------------------------------------------------------- /docs/source/api/bspline.rst: -------------------------------------------------------------------------------- 1 | B-splines 2 | ========= 3 | 4 | .. automodule:: pyiga.bspline 5 | :members: 6 | :inherited-members: 7 | 8 | -------------------------------------------------------------------------------- /docs/source/api/geometry.rst: -------------------------------------------------------------------------------- 1 | Geometry 2 | ======== 3 | 4 | .. automodule:: pyiga.geometry 5 | :members: 6 | :inherited-members: 7 | 8 | -------------------------------------------------------------------------------- /docs/source/api/hierarchical.rst: -------------------------------------------------------------------------------- 1 | Hierarchical Spline Spaces 2 | ========================== 3 | 4 | .. automodule:: pyiga.hierarchical 5 | 6 | Hierarchical spline spaces 7 | -------------------------- 8 | 9 | .. autoclass:: HSpace 10 | :members: 11 | 12 | .. autoclass:: HSplineFunc 13 | :members: 14 | 15 | .. autoclass:: TPMesh 16 | :members: 17 | 18 | Discretization 19 | -------------- 20 | 21 | .. autoclass:: HDiscretization 22 | :members: 23 | -------------------------------------------------------------------------------- /docs/source/api/operators.rst: -------------------------------------------------------------------------------- 1 | Operators 2 | ========= 3 | 4 | .. automodule:: pyiga.operators 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/api/solvers.rst: -------------------------------------------------------------------------------- 1 | Solvers 2 | ======= 3 | 4 | .. automodule:: pyiga.solvers 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/api/tensor.rst: -------------------------------------------------------------------------------- 1 | Tensors 2 | ======= 3 | 4 | .. automodule:: pyiga.tensor 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/api/vform.rst: -------------------------------------------------------------------------------- 1 | Variational forms 2 | ================= 3 | 4 | .. automodule:: pyiga.vform 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/api/vis.rst: -------------------------------------------------------------------------------- 1 | Visualization 2 | ============= 3 | 4 | .. automodule:: pyiga.vis 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyiga documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Apr 15 01:04:49 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | 20 | import os 21 | import sys 22 | #sys.path.insert(0, os.path.abspath('..')) # pyiga package should be installed 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx.ext.mathjax', 38 | 'sphinx.ext.intersphinx', 39 | 'nbsphinx' 40 | ] 41 | 42 | intersphinx_mapping = { 43 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 44 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 45 | } 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'pyiga' 61 | copyright = '2017-2025, Clemens Hofreither' 62 | author = 'Clemens Hofreither' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | import pyiga 70 | version = pyiga.__version__ 71 | # The full version, including alpha/beta/rc tags. 72 | release = version 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # 77 | # This is also used if you do content translation via gettext catalogs. 78 | # Usually you set "language" from the command line for these cases. 79 | language = 'en' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | # This patterns also effect to html_static_path and html_extra_path 84 | exclude_patterns = [] 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # If true, `todo` and `todoList` produce output, else they produce nothing. 90 | todo_include_todos = False 91 | 92 | 93 | # -- Options for Napoleon ------------------------------------------------- 94 | 95 | napoleon_use_rtype = False 96 | 97 | 98 | # -- Options for HTML output ---------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | # 103 | html_theme = 'alabaster' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | #html_static_path = ['_static'] 115 | 116 | html_show_sourcelink = False 117 | 118 | html_sidebars = { '**': 119 | ['globaltoc.html', 'relations.html', 'searchbox.html'], } 120 | 121 | 122 | # -- Options for HTMLHelp output ------------------------------------------ 123 | 124 | # Output file base name for HTML help builder. 125 | htmlhelp_basename = 'pyigadoc' 126 | 127 | 128 | # -- Options for LaTeX output --------------------------------------------- 129 | 130 | latex_elements = { 131 | # The paper size ('letterpaper' or 'a4paper'). 132 | # 'papersize': 'letterpaper', 133 | 134 | # The font size ('10pt', '11pt' or '12pt'). 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 'figure_align': 'htbp', 142 | } 143 | 144 | # Grouping the document tree into LaTeX files. List of tuples 145 | # (source start file, target name, title, 146 | # author, documentclass [howto, manual, or own class]). 147 | latex_documents = [ 148 | (master_doc, 'pyiga.tex', 'pyiga Documentation', 149 | 'Clemens Hofreither', 'manual'), 150 | ] 151 | 152 | 153 | # -- Options for manual page output --------------------------------------- 154 | 155 | # One entry per manual page. List of tuples 156 | # (source start file, name, description, authors, manual section). 157 | man_pages = [ 158 | (master_doc, 'pyiga', 'pyiga Documentation', 159 | [author], 1) 160 | ] 161 | 162 | 163 | # -- Options for Texinfo output ------------------------------------------- 164 | 165 | # Grouping the document tree into Texinfo files. List of tuples 166 | # (source start file, target name, title, author, 167 | # dir menu entry, description, category) 168 | texinfo_documents = [ 169 | (master_doc, 'pyiga', 'pyiga Documentation', 170 | author, 'pyiga', 'One line description of project.', 171 | 'Miscellaneous'), 172 | ] 173 | -------------------------------------------------------------------------------- /docs/source/guide/geometry.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%pylab inline\n", 10 | "from pyiga import bspline, geometry, vis" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "# Geometry manipulation in `pyiga`" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "We can define line segments or circular arcs using the builtin functions\n", 25 | "from the `geometry` module\n", 26 | "(see [its documentation](https://pyiga.readthedocs.io/en/latest/api/geometry.html)\n", 27 | "for details on all the functions used here).\n", 28 | "All kinds of geometries can be conveniently plotted using the `vis.plot_geo()` function." 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "f = geometry.circular_arc(pi/2)\n", 38 | "g = geometry.line_segment([0,0], [1,1])\n", 39 | "\n", 40 | "vis.plot_geo(f, color='red')\n", 41 | "vis.plot_geo(g, color='green')\n", 42 | "axis('equal');" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "Geometries can be translated, rotated or scaled:" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "vis.plot_geo(f.rotate_2d(pi/4).translate([0,-0.5]), color='red')\n", 59 | "vis.plot_geo(g.scale([1,1/3]).translate([-.5,.25]), color='green')\n", 60 | "axis('equal');" 61 | ] 62 | }, 63 | { 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "We can combine the univariate geometry functions $f(y)$ and $g(x)$ to create\n", 68 | "biviariate ones using, for instance, the outer sum\n", 69 | "\n", 70 | "$$\n", 71 | " (f \\oplus g)(x,y) = f(y) + g(x)\n", 72 | "$$\n", 73 | "\n", 74 | "or the outer product\n", 75 | "\n", 76 | "$$\n", 77 | " (f \\otimes g)(x,y) = f(y) * g(x).\n", 78 | "$$\n", 79 | "\n", 80 | "Here, both the addition and the product have to be understood in a componentwise fashion\n", 81 | "if $f$ and/or $g$ are vector-valued (as they are in our example)." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "vis.plot_geo(geometry.outer_sum(f, g))\n", 91 | "axis('equal');" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "vis.plot_geo(geometry.outer_product(f, g))\n", 101 | "axis('equal');" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "The last outer product has a singularity at the origin because multiplying with $g(0)=(0,0)$\n", 109 | "forces all points into $(0,0)$. We can translate it first to avoid this, creating a\n", 110 | "quarter annulus domain in the process:" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "vis.plot_geo(geometry.outer_product(f, g.translate([1,1])))\n", 120 | "axis('equal');" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "In this example, the second operand was a line segment from $(1,1)$ to $(2,2)$. Since numpy-style broadcasting\n", 128 | "works for all these operations, we can also simply define the second operand as a linear scalar\n", 129 | "function ranging from 1 to 2 to obtain the same effect:" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "vis.plot_geo(geometry.outer_product(f, geometry.line_segment(1, 2)))\n", 139 | "axis('equal');" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "We can also generate tensor product geometries; here each input function $f$ and $g$\n", 147 | "has to be scalar such that the resulting output function $F$ is 2D-vector-valued, namely,\n", 148 | "$$\n", 149 | " F(x,y) = (g(x), f(y)).\n", 150 | "$$\n", 151 | "\n", 152 | "However, you can also use this with higher-dimensional inputs, for instance to build a\n", 153 | "3D cylinder on top of a 2D domain." 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "G = geometry.tensor_product(geometry.line_segment(0,1),\n", 163 | " geometry.line_segment(0,5, intervals=3))\n", 164 | "vis.plot_geo(G)\n", 165 | "axis('scaled');" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "We can also define geometries through user-defined functions, where we have to specify the\n", 173 | "domain:" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "def f(x, y):\n", 183 | " r = 1 + x\n", 184 | " phi = (y - 0.5) * np.pi/2\n", 185 | " return (r * np.cos(phi), r * np.sin(phi))\n", 186 | "\n", 187 | "f_func = geometry.UserFunction(f, [[0,1],[0,1]])\n", 188 | "vis.plot_geo(f_func)\n", 189 | "axis('equal');" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "Finally, a bit of fun with translated and rotated outer products of circular arcs:" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "figsize(10,10)\n", 206 | "G1 = geometry.circular_arc(pi/3).translate((-1,0)).rotate_2d(-pi/6)\n", 207 | "G2 = G1.scale(-1).rotate_2d(pi/2)\n", 208 | "G1 = G1.translate((1,1))\n", 209 | "G2 = G2.translate((1,1))\n", 210 | "\n", 211 | "G = geometry.outer_product(G1, G2).translate((-1,-2)).rotate_2d(3*pi/4).translate((0,1))\n", 212 | "\n", 213 | "for i in range(8):\n", 214 | " vis.plot_geo(G.rotate_2d(i*pi/4))\n", 215 | "axis('equal');\n", 216 | "axis('off');" 217 | ] 218 | } 219 | ], 220 | "metadata": { 221 | "kernelspec": { 222 | "display_name": "Python 3", 223 | "language": "python", 224 | "name": "python3" 225 | }, 226 | "language_info": { 227 | "codemirror_mode": { 228 | "name": "ipython", 229 | "version": 3 230 | }, 231 | "file_extension": ".py", 232 | "mimetype": "text/x-python", 233 | "name": "python", 234 | "nbconvert_exporter": "python", 235 | "pygments_lexer": "ipython3", 236 | "version": "3.7.4" 237 | } 238 | }, 239 | "nbformat": 4, 240 | "nbformat_minor": 2 241 | } 242 | -------------------------------------------------------------------------------- /docs/source/guide/guide.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | User Guide 3 | ========== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: User Guide 8 | 9 | geometry 10 | vforms 11 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyiga documentation master file, created by 2 | sphinx-quickstart on Sat Apr 15 01:04:49 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyiga's documentation! 7 | ================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | 12 | guide/guide 13 | api/api 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /pyiga/__init__.py: -------------------------------------------------------------------------------- 1 | """pyiga 2 | 3 | A Python research toolbox for Isogeometric Analysis. 4 | """ 5 | 6 | __version__ = '0.1.0' 7 | 8 | _max_threads = None 9 | 10 | def get_max_threads(): 11 | global _max_threads 12 | if not _max_threads: 13 | import multiprocessing 14 | _max_threads = multiprocessing.cpu_count() 15 | return _max_threads 16 | 17 | def set_max_threads(num): 18 | global _max_threads 19 | _max_threads = num 20 | -------------------------------------------------------------------------------- /pyiga/_hdiscr.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse 3 | from . import assemble, compile, mlmatrix 4 | 5 | def _assemble_partial_rows(asm, row_indices): 6 | """Assemble a submatrix which contains only the given rows.""" 7 | kvs0, kvs1 = asm.kvs 8 | S = mlmatrix.MLStructure.from_kvs(kvs0, kvs1) 9 | I, J = S.nonzeros_for_rows(row_indices) # the nonzero indices in the given rows 10 | data = asm.multi_entries(np.column_stack((I,J))) 11 | return scipy.sparse.coo_matrix((data, (I,J)), shape=S.shape).tocsr() 12 | 13 | class HDiscretization: 14 | """Represents the discretization of a variational problem over a 15 | hierarchical spline space. 16 | 17 | Args: 18 | hspace (:class:`HSpace`): the HB- or THB-spline space in which to discretize 19 | vform (:class:`.VForm`): the variational form describing the problem 20 | asm_args (dict): a dictionary which provides named inputs for the assembler. Most 21 | problems will require at least a geometry map; this can be given in 22 | the form ``{'geo': geo}``, where ``geo`` is a geometry function 23 | defined using the :mod:`pyiga.geometry` module. Further inputs 24 | declared via the :meth:`.VForm.input` method must be included in 25 | this dict. 26 | 27 | The assemblers both for the matrix and any linear functionals will draw 28 | their input arguments from this dict. 29 | """ 30 | def __init__(self, hspace, vform, asm_args): 31 | self.hs = hspace 32 | self.truncate = hspace.truncate 33 | self.vf = vform 34 | self.asm_args = asm_args 35 | self.asm_class = None 36 | 37 | def _assemble_level(self, k, rows=None, bbox=None, symmetric=False): 38 | """Assemble a subset of the rows of the full tensor product stiffness 39 | matrix on a given level `k`.""" 40 | if rows is not None and len(rows) == 0: 41 | # work around a Cython bug with contiguity of 0-sized arrays: 42 | # https://github.com/cython/cython/issues/2093 43 | n = np.prod(self.hs.mesh(k).numdofs) 44 | return scipy.sparse.csr_matrix((n,n)) 45 | 46 | # get needed assembler arguments 47 | asm_args = { inp.name: self.asm_args[inp.name] 48 | for inp in self.vf.inputs} 49 | asm_args['bbox'] = bbox 50 | 51 | if not self.asm_class: 52 | self.asm_class = compile.compile_vform(self.vf, on_demand=True) 53 | asm = self.asm_class(self.hs.knotvectors(k), **asm_args) 54 | if rows is None: 55 | return assemble.assemble(asm, symmetric=symmetric) 56 | else: 57 | return _assemble_partial_rows(asm, rows) 58 | 59 | def assemble_matrix(self, symmetric=False): 60 | """Assemble the stiffness matrix for the hierarchical discretization and return it. 61 | 62 | Returns: 63 | a sparse matrix whose size corresponds to the 64 | :attr:`HSpace.numdofs` attribute of `hspace` 65 | """ 66 | if self.truncate: 67 | # compute HB version and transform it 68 | # HACK - overwrite truncate flag and replace it afterwards 69 | try: 70 | self.truncate = False 71 | A_hb = self.assemble_matrix(symmetric=symmetric) 72 | finally: 73 | self.truncate = True 74 | T = self.hs.thb_to_hb() 75 | return (T.T @ A_hb @ T).tocsr() 76 | else: 77 | hs = self.hs 78 | # compute dofs interacting with active dofs on each level 79 | neighbors = hs.cell_supp_indices(remove_dirichlet=False) 80 | # interactions on the same level are handled separately, so remove them 81 | for k in range(hs.numlevels): 82 | neighbors[k][k] = [] 83 | 84 | # Determine the rows of the matrix we need to assemble: 85 | # 1. dofs for interlevel contributions - all level k dofs which are required 86 | # to represent the coarse functions which interact with level k 87 | # 2. all active dofs on level k 88 | to_assemble, interlevel_ix = [], [] 89 | bboxes = [] 90 | for k in range(hs.numlevels): 91 | indices = set() 92 | for lv in range(max(0, k - hs.disparity), k): 93 | indices |= set(hs.hmesh.function_grandchildren(lv, neighbors[k][lv], k)) 94 | interlevel_ix.append(indices) 95 | to_assemble.append(indices | hs.actfun[k]) 96 | 97 | # compute a bounding box for the supports of all functions to be assembled 98 | bboxes.append(self._bbox_for_functions(k, to_assemble[-1])) 99 | 100 | # convert them to raveled form 101 | to_assemble = hs.ravel_indices(to_assemble) 102 | interlevel_ix = hs.ravel_indices(interlevel_ix) 103 | 104 | # compute neighbors as matrix indices 105 | neighbors = [hs.raveled_to_virtual_canonical_indices(lv, hs.ravel_indices(idx)) 106 | for lv, idx in enumerate(neighbors)] 107 | 108 | # new indices per level as local tensor product indices 109 | new_loc = hs.active_indices() 110 | # new indices per level as global matrix indices 111 | na = tuple(len(ii) for ii in new_loc) 112 | new = [np.arange(sum(na[:k]), sum(na[:k+1])) for k in range(hs.numlevels)] 113 | 114 | # assemble the matrix from the levelwise contributions 115 | coo_I, coo_J, values = [], [], [] # blockwise COO data 116 | 117 | def insert_block(B, rows, columns): 118 | # store the block B into the given rows/columns of the output matrix 119 | B = B.tocsr() # this does nothing if B is already CSR 120 | I, J = B.nonzero() 121 | coo_I.append(rows[I]) 122 | coo_J.append(columns[J]) 123 | values.append(B.data) 124 | 125 | for k in range(hs.numlevels): 126 | # compute the needed rows of the tensor product stiffness matrix 127 | A_k = self._assemble_level(k, rows=to_assemble[k], bbox=bboxes[k], symmetric=symmetric) 128 | 129 | # matrix which maps HB-coefficients to TP coefficients on level k 130 | I_hb_k = hs.represent_fine(lv=k, truncate=False, rows=to_assemble[k]) 131 | 132 | # compute the diagonal block consisting of interactions on the same level 133 | A_hb_new = A_k[new_loc[k]][:,new_loc[k]] 134 | 135 | # store the diagonal block 136 | insert_block(A_hb_new, new[k], new[k]) 137 | 138 | # compute the off-diagonal block(s) which describe interactions with coarser levels 139 | A_hb_interlevel = (I_hb_k[interlevel_ix[k]][:, neighbors[k]].T 140 | @ A_k[interlevel_ix[k]][:, new_loc[k]] 141 | @ I_hb_k[new_loc[k]][:, new[k]]) 142 | 143 | if symmetric: 144 | A_hb_interlevel2 = A_hb_interlevel.T 145 | else: 146 | A_hb_interlevel2 = (I_hb_k[new_loc[k]][:, new[k]].T 147 | @ A_k[new_loc[k]][:, interlevel_ix[k]] 148 | @ I_hb_k[interlevel_ix[k]][:, neighbors[k]]) 149 | 150 | # store the two blocks containing interactions with coarser levels 151 | insert_block(A_hb_interlevel, neighbors[k], new[k]) 152 | insert_block(A_hb_interlevel2, new[k], neighbors[k]) 153 | 154 | # convert the blockwise COO data into a CSR matrix 155 | coo_I = np.concatenate(coo_I) 156 | coo_J = np.concatenate(coo_J) 157 | values = np.concatenate(values) 158 | return scipy.sparse.csr_matrix((values, (coo_I, coo_J)), 159 | shape=(hs.numdofs, hs.numdofs)) 160 | 161 | def assemble_rhs(self, vf=None): 162 | """Assemble the right-hand side vector for the hierarchical discretization and return it. 163 | 164 | By default (if `vf=None`), a standard L2 inner product `` is used 165 | for computing the right-hand side, and the function `f` is taken from 166 | the key ``'f'`` of the ``asm_args`` dict. It is assumed to be given in 167 | physical coordinates. 168 | 169 | A different functional can be specified by passing a :class:`.VForm` 170 | with ``arity=1`` as the `vf` parameter. 171 | 172 | Returns: 173 | a vector whose length is equal to the :attr:`HSpace.numdofs` 174 | attribute of `hspace`, corresponding to the active basis functions 175 | in canonical order 176 | """ 177 | if vf is None: 178 | from .vform import L2functional_vf 179 | vf = L2functional_vf(dim=self.hs.dim, physical=True) 180 | return self.assemble_functional(vf) 181 | 182 | def assemble_functional(self, vf): 183 | """Assemble a linear functional described by the :class:`.VForm` `vf` over 184 | the hierarchical spline space. 185 | 186 | Returns: 187 | a vector whose length is equal to the :attr:`HSpace.numdofs` 188 | attribute of `hspace`, corresponding to the active basis functions 189 | in canonical order 190 | """ 191 | if not vf.arity == 1: 192 | raise ValueError('vf must be a linear functional (arity=1)') 193 | RhsAsm = compile.compile_vform(vf, on_demand=True) 194 | 195 | # get needed assembler arguments 196 | asm_args = { inp.name: self.asm_args[inp.name] 197 | for inp in vf.inputs} 198 | 199 | def asm_rhs_level(k, rows): 200 | if len(rows) == 0: 201 | return np.zeros(0) 202 | 203 | # determine bounding box for active functions 204 | bbox = self._bbox_for_functions(k, self.hs.actfun[k]) 205 | 206 | kvs = self.hs.knotvectors(k) 207 | asm_args['bbox'] = bbox 208 | asm = RhsAsm(kvs, **asm_args) 209 | return asm.multi_entries(rows) 210 | 211 | act = self.hs.active_indices() 212 | na = tuple(len(ii) for ii in act) 213 | rhs = np.zeros(self.hs.numdofs) 214 | i = 0 215 | # collect the contributions from the active functions per level 216 | for k, na_k in enumerate(na): 217 | rhs[i:i+na_k] = asm_rhs_level(k, act[k]) 218 | i += na_k 219 | 220 | # if using THBs, apply the transformation matrix 221 | if self.truncate: 222 | rhs = self.hs.thb_to_hb().T @ rhs 223 | return rhs 224 | 225 | def _bbox_for_functions(self, lv, funcs): 226 | """Compute a bounding box for the supports of the given functions on the given level.""" 227 | supp_cells = np.array(sorted(self.hs.mesh(lv).support(funcs))) 228 | if len(supp_cells) == 0: 229 | return tuple((0,0) for j in range(self.hs.dim)) 230 | else: 231 | return tuple( 232 | (supp_cells[:,j].min(), supp_cells[:,j].max() + 1) # upper limit is exclusive 233 | for j in range(supp_cells.shape[1])) 234 | -------------------------------------------------------------------------------- /pyiga/approx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Methods for approximating functions in spline spaces.""" 3 | from . import bspline 4 | from . import assemble 5 | from . import tensor 6 | from . import operators 7 | from . import utils 8 | from . import hierarchical 9 | 10 | import sys 11 | import numpy as np 12 | import scipy.sparse.linalg 13 | 14 | def interpolate(kvs, f, geo=None, nodes=None): 15 | """Perform interpolation in a spline space. 16 | 17 | Returns the coefficients for the interpolant of the function `f` in the 18 | tensor product B-spline basis `kvs`. 19 | 20 | By default, `f` is assumed to be defined in the parameter domain. If a 21 | geometry is passed in `geo`, interpolation is instead done in physical 22 | coordinates. 23 | 24 | `nodes` should be a tensor grid (i.e., a sequence of one-dimensional 25 | arrays) in the parameter domain specifying the interpolation nodes. If not 26 | specified, the Gréville abscissae are used. 27 | 28 | It is possible to pass an array of function values for `f` instead of a 29 | function; they should have the proper shape and correspond to the function 30 | values at the `nodes`. In this case, `geo` is ignored. 31 | """ 32 | if isinstance(kvs, bspline.KnotVector): 33 | kvs = (kvs,) 34 | if nodes is None: 35 | nodes = [kv.greville() for kv in kvs] 36 | 37 | # evaluate f at interpolation nodes? 38 | if isinstance(f, np.ndarray): 39 | # check that leading dimensions match the number of dofs 40 | if np.shape(f)[:len(kvs)] != tuple(kv.numdofs for kv in kvs): 41 | raise ValueError('array f has wrong shape') 42 | rhs = f 43 | else: 44 | if geo is not None: 45 | rhs = utils.grid_eval_transformed(f, nodes, geo) 46 | else: 47 | rhs = utils.grid_eval(f, nodes) 48 | 49 | Cinvs = [operators.make_solver(bspline.collocation(kvs[i], nodes[i])) 50 | for i in range(len(kvs))] 51 | return tensor.apply_tprod(Cinvs, rhs) 52 | 53 | def _project_L2_hspace(hs, f, f_physical=False, geo=None): 54 | from . import vform, geometry 55 | if geo is None: 56 | geo = geometry.identity(hs.knotvectors(0)) 57 | M = assemble.assemble(vform.mass_vf(hs.dim), hs, geo=geo) 58 | rhs = assemble.assemble(vform.L2functional_vf(hs.dim, physical=f_physical), 59 | hs, geo=geo, f=f) 60 | return operators.make_solver(M, spd=True).dot(rhs) 61 | 62 | def project_L2(kvs, f, f_physical=False, geo=None): 63 | """Perform :math:`L_2`-projection into a spline space. 64 | 65 | Returns the coefficients for the :math:`L_2`-projection of the function `f` 66 | into the tensor product B-spline basis `kvs`. Optionally, a geometry 67 | transform `geo` can be specified to compute the projection in a physical 68 | domain. 69 | 70 | By default, `f` is assumed to be defined in the parameter domain. If it is 71 | given in physical coordinates, pass `f_physical=True`. This requires `geo` 72 | to be specified. 73 | 74 | This function also supports projection into a hierarchical spline space by 75 | passing a :class:`.HSpace` object in place of `kvs`. 76 | """ 77 | if isinstance(kvs, hierarchical.HSpace): 78 | return _project_L2_hspace(kvs, f, f_physical, geo) 79 | elif isinstance(kvs, bspline.KnotVector): 80 | kvs = (kvs,) 81 | Minvs = [operators.make_solver(assemble.mass(kv), spd=True) for kv in kvs] 82 | rhs = assemble.inner_products(kvs, f, f_physical=f_physical, geo=geo) 83 | if geo is None: 84 | assert not f_physical, 'Cannot use physical coordinates without geometry' 85 | # in the parameter domain, we simply apply the Kronecker product of the M^{-1} 86 | return tensor.apply_tprod(Minvs, rhs) 87 | else: 88 | # in the physical domain, use the Kronecker product as a preconditioner 89 | M = assemble.mass(kvs, geo=geo) 90 | b = rhs.ravel() 91 | assert b.shape[0] == M.shape[1], 'L2 projection with geometry only implemented for scalar functions' 92 | x, info = scipy.sparse.linalg.cg(M, b, rtol=1e-12, atol=1e-12, 93 | maxiter=100, M=operators.KroneckerOperator(*Minvs)) 94 | if info: 95 | print('WARNING: L2 projection - CG did not converge:', info, file=sys.stderr) 96 | return x.reshape(rhs.shape) 97 | -------------------------------------------------------------------------------- /pyiga/assemble_tools.py: -------------------------------------------------------------------------------- 1 | from .assemble_tools_cy import * 2 | 3 | from . import bspline 4 | import numpy as np 5 | 6 | # returned array has axes (basis function, grid point, derivative) 7 | def compute_values_derivs(kv, grid, derivs): 8 | colloc = bspline.collocation_derivs(kv, grid, derivs=derivs) 9 | colloc = tuple(X.T.toarray() for X in colloc) 10 | # The assemblers expect the resulting array to be in C-order. Depending on 11 | # numpy version, stack() does not guarantee that, so enforce contiguity. 12 | return np.ascontiguousarray(np.stack(colloc, axis=-1)) 13 | -------------------------------------------------------------------------------- /pyiga/assemble_tools_cy.pyx: -------------------------------------------------------------------------------- 1 | # cython: profile=False 2 | # cython: linetrace=False 3 | # cython: binding=False 4 | 5 | cimport cython 6 | from cython.parallel import prange 7 | #from libcpp.vector cimport vector 8 | from cpython cimport pycapsule 9 | 10 | from libc.string cimport memset 11 | 12 | import numpy as np 13 | cimport numpy as np 14 | 15 | import pyiga 16 | from . import bspline 17 | from .mlmatrix import get_transpose_idx_for_bidx 18 | 19 | from concurrent.futures import ThreadPoolExecutor 20 | 21 | import itertools 22 | 23 | ################################################################################ 24 | # Public utility functions 25 | ################################################################################ 26 | 27 | @cython.boundscheck(False) 28 | @cython.wraparound(False) 29 | @cython.cdivision(True) 30 | cdef void from_seq1(size_t i, size_t[1] ndofs, size_t[1] out) noexcept nogil: 31 | out[0] = i 32 | 33 | @cython.boundscheck(False) 34 | @cython.wraparound(False) 35 | @cython.cdivision(True) 36 | cdef void from_seq2(size_t i, size_t[2] ndofs, size_t[2] out) noexcept nogil: 37 | out[1] = i % ndofs[1] 38 | i /= ndofs[1] 39 | out[0] = i 40 | 41 | @cython.boundscheck(False) 42 | @cython.wraparound(False) 43 | @cython.cdivision(True) 44 | cdef void from_seq3(size_t i, size_t[3] ndofs, size_t[3] out) noexcept nogil: 45 | out[2] = i % ndofs[2] 46 | i /= ndofs[2] 47 | out[1] = i % ndofs[1] 48 | i /= ndofs[1] 49 | out[0] = i 50 | 51 | #### these two functions were only used in some CUDA assembling experiments 52 | ## 53 | ## # returns an array where each row contains: 54 | ## # i0 i1 i2 j0 j1 j2 t0 t1 t2 55 | ## # where ix and jx are block indices of a matrix entry 56 | ## # and (t0,t1,t2) is a single tile index which is contained 57 | ## # in the joint support for the matrix entry 58 | ## def prepare_tile_indices3(ij_arr, meshsupp, numdofs): 59 | ## cdef size_t[3] ndofs 60 | ## ndofs[:] = numdofs 61 | ## cdef vector[unsigned] result 62 | ## cdef size_t I[3] 63 | ## cdef size_t J[3] 64 | ## cdef size_t[:, :] ij = ij_arr 65 | ## cdef size_t N = ij.shape[0], M = 0 66 | ## cdef IntInterval[3] intvs 67 | ## 68 | ## for k in range(N): 69 | ## from_seq3(ij[k,0], ndofs, I) 70 | ## from_seq3(ij[k,1], ndofs, J) 71 | ## 72 | ## for r in range(3): 73 | ## ii = I[r] 74 | ## jj = J[r] 75 | ## intvs[r] = intersect_intervals( 76 | ## make_intv(meshsupp[r][ii, 0], meshsupp[r][ii, 1]), 77 | ## make_intv(meshsupp[r][jj, 0], meshsupp[r][jj, 1])) 78 | ## 79 | ## for t0 in range(intvs[0].a, intvs[0].b): 80 | ## for t1 in range(intvs[1].a, intvs[1].b): 81 | ## for t2 in range(intvs[2].a, intvs[2].b): 82 | ## result.push_back(I[0]) 83 | ## result.push_back(I[1]) 84 | ## result.push_back(I[2]) 85 | ## result.push_back(J[0]) 86 | ## result.push_back(J[1]) 87 | ## result.push_back(J[2]) 88 | ## result.push_back(t0) 89 | ## result.push_back(t1) 90 | ## result.push_back(t2) 91 | ## M += 1 92 | ## return np.array( result.data(), order='C').reshape((M,9)) 93 | ## 94 | ## 95 | ## # Used to recombine the results of tile-wise assemblers, where a single matrix 96 | ## # entry is split up into is contributions per tile. This class sums these 97 | ## # contributions up again. 98 | ## cdef class MatrixEntryAccumulator: 99 | ## cdef ssize_t idx 100 | ## cdef int num_indices 101 | ## cdef double[::1] _result 102 | ## cdef unsigned[16] old_indices 103 | ## 104 | ## def __init__(self, int num_indices, size_t N): 105 | ## self.idx = -1 106 | ## assert num_indices <= 16 107 | ## self.num_indices = num_indices 108 | ## self._result = np.empty(N) 109 | ## for i in range(16): 110 | ## self.old_indices[i] = 0xffffffff 111 | ## 112 | ## @cython.boundscheck(False) 113 | ## @cython.wraparound(False) 114 | ## cdef bint index_changed(self, unsigned[:, :] indices, size_t i) noexcept nogil: 115 | ## cdef bint changed = False 116 | ## for k in range(self.num_indices): 117 | ## if indices[i, k] != self.old_indices[k]: 118 | ## changed = True 119 | ## break 120 | ## if changed: # update old_indices 121 | ## for k in range(self.num_indices): 122 | ## self.old_indices[k] = indices[i, k] 123 | ## return changed 124 | ## 125 | ## @cython.boundscheck(False) 126 | ## @cython.wraparound(False) 127 | ## cpdef process(self, unsigned[:, :] indices, double[::1] values): 128 | ## cdef size_t M = indices.shape[0] 129 | ## for i in range(M): 130 | ## if self.index_changed(indices, i): 131 | ## self.idx += 1 132 | ## self._result[self.idx] = values[i] 133 | ## else: 134 | ## self._result[self.idx] += values[i] 135 | ## 136 | ## @property 137 | ## def result(self): 138 | ## return self._result[:self.idx+1] 139 | 140 | 141 | ################################################################################ 142 | # Internal helper functions 143 | ################################################################################ 144 | 145 | cdef struct IntInterval: 146 | int a 147 | int b 148 | 149 | cdef IntInterval make_intv(int a, int b) noexcept nogil: 150 | cdef IntInterval intv 151 | intv.a = a 152 | intv.b = b 153 | return intv 154 | 155 | cdef IntInterval intersect_intervals(IntInterval intva, IntInterval intvb) noexcept nogil: 156 | return make_intv(max(intva.a, intvb.a), min(intva.b, intvb.b)) 157 | 158 | 159 | cdef int next_lexicographic1(size_t[1] cur, size_t start[1], size_t end[1]) noexcept nogil: 160 | cur[0] += 1 161 | if cur[0] == end[0]: 162 | return 0 163 | else: 164 | return 1 165 | 166 | cdef int next_lexicographic2(size_t[2] cur, size_t start[2], size_t end[2]) noexcept nogil: 167 | cdef size_t i 168 | for i in reversed(range(2)): 169 | cur[i] += 1 170 | if cur[i] == end[i]: 171 | if i == 0: 172 | return 0 173 | else: 174 | cur[i] = start[i] 175 | else: 176 | return 1 177 | 178 | cdef int next_lexicographic3(size_t[3] cur, size_t start[3], size_t end[3]) noexcept nogil: 179 | cdef size_t i 180 | for i in reversed(range(3)): 181 | cur[i] += 1 182 | if cur[i] == end[i]: 183 | if i == 0: 184 | return 0 185 | else: 186 | cur[i] = start[i] 187 | else: 188 | return 1 189 | 190 | @cython.boundscheck(False) 191 | @cython.wraparound(False) 192 | cdef IntInterval find_joint_support_functions(ssize_t[:,::1] meshsupp, long i) noexcept nogil: 193 | cdef long j, n, minj, maxj 194 | minj = j = i 195 | while j >= 0 and meshsupp[j,1] > meshsupp[i,0]: 196 | minj = j 197 | j -= 1 198 | 199 | maxj = i 200 | j = i + 1 201 | n = meshsupp.shape[0] 202 | while j < n and meshsupp[j,0] < meshsupp[i,1]: 203 | maxj = j 204 | j += 1 205 | return make_intv(minj, maxj+1) 206 | 207 | 208 | #### determinants and inverses 209 | 210 | def det_and_inv(X): 211 | """Return (np.linalg.det(X), np.linalg.inv(X)), but much 212 | faster for 2x2- and 3x3-matrices.""" 213 | d = X.shape[-1] 214 | if d == 2: 215 | det = np.empty(X.shape[:-2]) 216 | inv = det_and_inv_2x2(X, det) 217 | return det, inv 218 | elif d == 3: 219 | det = np.empty(X.shape[:-2]) 220 | inv = det_and_inv_3x3(X, det) 221 | return det, inv 222 | else: 223 | return np.linalg.det(X), np.linalg.inv(X) 224 | 225 | def determinants(X): 226 | """Compute the determinants of an ndarray of square matrices. 227 | 228 | This behaves mostly identically to np.linalg.det(), but is faster for 2x2 matrices.""" 229 | shape = X.shape 230 | d = shape[-1] 231 | assert shape[-2] == d, "Input matrices need to be square" 232 | if d == 2: 233 | # optimization for 2x2 matrices 234 | assert len(shape) == 4, "Only implemented for n x m x 2 x 2 arrays" 235 | return X[:,:,0,0] * X[:,:,1,1] - X[:,:,0,1] * X[:,:,1,0] 236 | elif d == 3: 237 | return determinants_3x3(X) 238 | else: 239 | return np.linalg.det(X) 240 | 241 | def inverses(X): 242 | if X.shape[-2:] == (2,2): 243 | return inverses_2x2(X) 244 | elif X.shape[-2:] == (3,3): 245 | return inverses_3x3(X) 246 | else: 247 | return np.linalg.inv(X) 248 | 249 | #### 2D determinants and inverses 250 | 251 | @cython.boundscheck(False) 252 | @cython.wraparound(False) 253 | @cython.cdivision(True) 254 | cdef double[:,:,:,::1] det_and_inv_2x2(double[:,:,:,::1] X, double[:,::1] det_out) noexcept: 255 | cdef long m,n, i,j 256 | cdef double det, a,b,c,d 257 | m,n = X.shape[0], X.shape[1] 258 | 259 | cdef double[:,:,:,::1] Y = np.empty_like(X) 260 | for i in prange(m, nogil=True, schedule='static'): 261 | for j in range(n): 262 | a,b,c,d = X[i,j, 0,0], X[i,j, 0,1], X[i,j, 1,0], X[i,j, 1,1] 263 | det = a*d - b*c 264 | det_out[i,j] = det 265 | Y[i,j, 0,0] = d / det 266 | Y[i,j, 0,1] = -b / det 267 | Y[i,j, 1,0] = -c / det 268 | Y[i,j, 1,1] = a / det 269 | return Y 270 | 271 | @cython.boundscheck(False) 272 | @cython.wraparound(False) 273 | @cython.cdivision(True) 274 | cdef double[:,:,:,::1] inverses_2x2(double[:,:,:,::1] X) noexcept: 275 | cdef size_t m,n, i,j 276 | cdef double det, a,b,c,d 277 | m,n = X.shape[0], X.shape[1] 278 | 279 | cdef double[:,:,:,::1] Y = np.empty_like(X) 280 | for i in range(m): 281 | for j in range(n): 282 | a,b,c,d = X[i,j, 0,0], X[i,j, 0,1], X[i,j, 1,0], X[i,j, 1,1] 283 | det = a*d - b*c 284 | Y[i,j, 0,0] = d / det 285 | Y[i,j, 0,1] = -b / det 286 | Y[i,j, 1,0] = -c / det 287 | Y[i,j, 1,1] = a / det 288 | return Y 289 | 290 | #### 3D determinants and inverses 291 | 292 | @cython.boundscheck(False) 293 | @cython.wraparound(False) 294 | @cython.cdivision(True) 295 | cdef double[:,:,:,:,::1] det_and_inv_3x3(double[:,:,:,:,::1] X, double[:,:,::1] det_out) noexcept: 296 | cdef long n0, n1, n2, i0, i1, i2 297 | cdef double det, invdet 298 | n0,n1,n2 = X.shape[0], X.shape[1], X.shape[2] 299 | cdef double x00,x01,x02,x10,x11,x12,x20,x21,x22 300 | 301 | cdef double[:,:,:,:,::1] Y = np.empty_like(X) 302 | 303 | for i0 in prange(n0, nogil=True, schedule='static'): 304 | for i1 in range(n1): 305 | for i2 in range(n2): 306 | x00,x01,x02 = X[i0, i1, i2, 0, 0], X[i0, i1, i2, 0, 1], X[i0, i1, i2, 0, 2] 307 | x10,x11,x12 = X[i0, i1, i2, 1, 0], X[i0, i1, i2, 1, 1], X[i0, i1, i2, 1, 2] 308 | x20,x21,x22 = X[i0, i1, i2, 2, 0], X[i0, i1, i2, 2, 1], X[i0, i1, i2, 2, 2] 309 | 310 | det = x00 * (x11 * x22 - x21 * x12) - \ 311 | x01 * (x10 * x22 - x12 * x20) + \ 312 | x02 * (x10 * x21 - x11 * x20) 313 | 314 | det_out[i0, i1, i2] = det 315 | 316 | invdet = 1.0 / det 317 | 318 | Y[i0, i1, i2, 0, 0] = (x11 * x22 - x21 * x12) * invdet 319 | Y[i0, i1, i2, 0, 1] = (x02 * x21 - x01 * x22) * invdet 320 | Y[i0, i1, i2, 0, 2] = (x01 * x12 - x02 * x11) * invdet 321 | Y[i0, i1, i2, 1, 0] = (x12 * x20 - x10 * x22) * invdet 322 | Y[i0, i1, i2, 1, 1] = (x00 * x22 - x02 * x20) * invdet 323 | Y[i0, i1, i2, 1, 2] = (x10 * x02 - x00 * x12) * invdet 324 | Y[i0, i1, i2, 2, 0] = (x10 * x21 - x20 * x11) * invdet 325 | Y[i0, i1, i2, 2, 1] = (x20 * x01 - x00 * x21) * invdet 326 | Y[i0, i1, i2, 2, 2] = (x00 * x11 - x10 * x01) * invdet 327 | 328 | return Y 329 | 330 | @cython.boundscheck(False) 331 | @cython.wraparound(False) 332 | cdef double[:,:,::1] determinants_3x3(double[:,:,:,:,::1] X) noexcept: 333 | cdef size_t n0, n1, n2, i0, i1, i2 334 | n0,n1,n2 = X.shape[0], X.shape[1], X.shape[2] 335 | 336 | cdef double[:,:,::1] Y = np.empty((n0,n1,n2)) 337 | cdef double[:,::1] x 338 | 339 | for i0 in range(n0): 340 | for i1 in range(n1): 341 | for i2 in range(n2): 342 | x = X[i0, i1, i2, :, :] 343 | 344 | Y[i0,i1,i2] = x[0, 0] * (x[1, 1] * x[2, 2] - x[2, 1] * x[1, 2]) - \ 345 | x[0, 1] * (x[1, 0] * x[2, 2] - x[1, 2] * x[2, 0]) + \ 346 | x[0, 2] * (x[1, 0] * x[2, 1] - x[1, 1] * x[2, 0]) 347 | return Y 348 | 349 | @cython.boundscheck(False) 350 | @cython.wraparound(False) 351 | @cython.cdivision(True) 352 | cdef double[:,:,:,:,::1] inverses_3x3(double[:,:,:,:,::1] X) noexcept: 353 | cdef size_t n0, n1, n2, i0, i1, i2 354 | cdef double det, invdet 355 | n0,n1,n2 = X.shape[0], X.shape[1], X.shape[2] 356 | 357 | cdef double[:,:,:,:,::1] Y = np.empty_like(X) 358 | cdef double[:,::1] x, y 359 | 360 | for i0 in range(n0): 361 | for i1 in range(n1): 362 | for i2 in range(n2): 363 | x = X[i0, i1, i2, :, :] 364 | y = Y[i0, i1, i2, :, :] 365 | 366 | det = x[0, 0] * (x[1, 1] * x[2, 2] - x[2, 1] * x[1, 2]) - \ 367 | x[0, 1] * (x[1, 0] * x[2, 2] - x[1, 2] * x[2, 0]) + \ 368 | x[0, 2] * (x[1, 0] * x[2, 1] - x[1, 1] * x[2, 0]) 369 | 370 | invdet = 1.0 / det 371 | 372 | y[0, 0] = (x[1, 1] * x[2, 2] - x[2, 1] * x[1, 2]) * invdet 373 | y[0, 1] = (x[0, 2] * x[2, 1] - x[0, 1] * x[2, 2]) * invdet 374 | y[0, 2] = (x[0, 1] * x[1, 2] - x[0, 2] * x[1, 1]) * invdet 375 | y[1, 0] = (x[1, 2] * x[2, 0] - x[1, 0] * x[2, 2]) * invdet 376 | y[1, 1] = (x[0, 0] * x[2, 2] - x[0, 2] * x[2, 0]) * invdet 377 | y[1, 2] = (x[1, 0] * x[0, 2] - x[0, 0] * x[1, 2]) * invdet 378 | y[2, 0] = (x[1, 0] * x[2, 1] - x[2, 0] * x[1, 1]) * invdet 379 | y[2, 1] = (x[2, 0] * x[0, 1] - x[0, 0] * x[2, 1]) * invdet 380 | y[2, 2] = (x[0, 0] * x[1, 1] - x[1, 0] * x[0, 1]) * invdet 381 | 382 | return Y 383 | 384 | 385 | #### Parallelization 386 | 387 | def chunk_tasks(tasks, num_chunks): 388 | """Generator that splits the list `tasks` into roughly `num_chunks` equally-sized parts.""" 389 | n = len(tasks) // num_chunks + 1 390 | for i in range(0, len(tasks), n): 391 | yield tasks[i:i+n] 392 | 393 | cdef object _threadpool = None 394 | 395 | cdef object get_thread_pool(): 396 | global _threadpool 397 | if _threadpool is None: 398 | _threadpool = ThreadPoolExecutor(pyiga.get_max_threads()) 399 | return _threadpool 400 | 401 | ################################################################################ 402 | # Assembler infrastructure (autogenerated) 403 | ################################################################################ 404 | 405 | include "genericasm.pxi" 406 | 407 | 408 | -------------------------------------------------------------------------------- /pyiga/bspline_cy.pyx: -------------------------------------------------------------------------------- 1 | # cython: profile=False 2 | # cython: linetrace=False 3 | # cython: binding=False 4 | 5 | cimport cython 6 | 7 | import numpy as np 8 | cimport numpy as np 9 | 10 | @cython.cdivision(True) 11 | @cython.boundscheck(False) 12 | @cython.wraparound(False) 13 | cpdef int pyx_findspan(double[::1] kv, int p, double u) noexcept nogil: 14 | cdef int n = kv.shape[0] 15 | 16 | if u >= kv[n - p - 1]: 17 | return n - p - 2 # last interval 18 | 19 | cdef int a = 0, b = n - 1, c 20 | 21 | while b - a > 1: 22 | c = a + (b - a) // 2 23 | if kv[c] > u: 24 | b = c 25 | else: 26 | a = c 27 | return a 28 | 29 | @cython.boundscheck(False) 30 | @cython.wraparound(False) 31 | cpdef object pyx_findspans(double[::1] kv, int p, double[::1] u): 32 | out = np.empty(u.shape[0], dtype=np.long) 33 | cdef long[::1] result = out 34 | cdef int i 35 | for i in range(u.shape[0]): 36 | result[i] = pyx_findspan(kv, p, u[i]) 37 | return out 38 | 39 | @cython.cdivision(True) 40 | @cython.boundscheck(False) 41 | @cython.wraparound(False) 42 | cdef double[:,:] bspline_active_deriv_single(object knotvec, double u, int numderiv, double[:,:] result=None) noexcept: 43 | """Evaluate all active B-spline basis functions and their derivatives 44 | up to `numderiv` at a single point `u`""" 45 | cdef double[::1] kv 46 | cdef int p, j, r, k, span, rk, pk, fac, j1, j2 47 | cdef double[:,::1] NDU 48 | cdef double saved, temp, d 49 | cdef double[64] left, right, a1buf, a2buf 50 | cdef double* a1 51 | cdef double* a2 52 | 53 | kv, p = knotvec.kv, knotvec.p 54 | assert p < 64, "Spline degree too high" # need to change constant array sizes above (p+1) 55 | 56 | NDU = np.empty((p+1, p+1), order='C') 57 | if result is None: 58 | result = np.empty((numderiv+1, p+1)) 59 | else: 60 | assert result.shape[0] is numderiv+1 and result.shape[1] is p+1 61 | 62 | span = pyx_findspan(kv, p, u) 63 | 64 | NDU[0,0] = 1.0 65 | 66 | for j in range(1, p+1): 67 | # Compute knot splits 68 | left[j-1] = u - kv[span+1-j] 69 | right[j-1] = kv[span+j] - u 70 | saved = 0.0 71 | 72 | for r in range(j): # For all but the last basis functions of degree j (ndu row) 73 | # Strictly lower triangular part: Knot differences of distance j 74 | NDU[j, r] = right[r] + left[j-r-1] 75 | temp = NDU[r, j-1] / NDU[j, r] 76 | # Upper triangular part: Basis functions of degree j 77 | NDU[r, j] = saved + right[r] * temp # r-th function value of degree j 78 | saved = left[j-r-1] * temp 79 | 80 | # Diagonal: j-th (last) function value of degree j 81 | NDU[j, j] = saved 82 | 83 | # copy function values into result array 84 | for j in range(p+1): 85 | result[0, j] = NDU[j, p] 86 | 87 | (a1,a2) = a1buf, a2buf 88 | 89 | for r in range(p+1): # loop over basis functions 90 | a1[0] = 1.0 91 | 92 | fac = p # fac = fac(p) / fac(p-k) 93 | 94 | # Compute the k-th derivative of the r-th basis function 95 | for k in range(1, numderiv+1): 96 | rk = r - k 97 | pk = p - k 98 | d = 0.0 99 | 100 | if r >= k: 101 | a2[0] = a1[0] / NDU[pk+1, rk] 102 | d = a2[0] * NDU[rk, pk] 103 | 104 | j1 = 1 if rk >= -1 else -rk 105 | j2 = k-1 if r-1 <= pk else p - r 106 | 107 | for j in range(j1, j2+1): 108 | a2[j] = (a1[j] - a1[j-1]) / NDU[pk+1, rk+j] 109 | d += a2[j] * NDU[rk+j, pk] 110 | 111 | if r <= pk: 112 | a2[k] = -a1[k-1] / NDU[pk+1, r] 113 | d += a2[k] * NDU[r, pk] 114 | 115 | result[k, r] = d * fac 116 | fac *= pk # update fac = fac(p) / fac(p-k) for next k 117 | 118 | # swap rows a1 and a2 119 | (a1,a2) = (a2,a1) 120 | 121 | return result 122 | 123 | 124 | @cython.boundscheck(False) 125 | @cython.wraparound(False) 126 | def active_deriv(object knotvec, u, int numderiv): 127 | """Evaluate all active B-spline basis functions and their derivatives 128 | up to `numderiv` at the points `u`. 129 | 130 | Returns an array with shape (numderiv+1, p+1) if `u` is scalar or 131 | an array with shape (numderiv+1, p+1, len(u)) otherwise. 132 | """ 133 | cdef double[:,:,:] result 134 | cdef double[:] u_arr 135 | cdef int i, n 136 | 137 | if np.isscalar(u): 138 | return bspline_active_deriv_single(knotvec, u, numderiv) 139 | else: 140 | u_arr = u 141 | n = u.shape[0] 142 | result = np.empty((numderiv+1, knotvec.p+1, n)) 143 | for i in range(n): 144 | bspline_active_deriv_single(knotvec, u_arr[i], numderiv, result=result[:,:,i]) 145 | return result 146 | 147 | -------------------------------------------------------------------------------- /pyiga/codegen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/pyiga/codegen/__init__.py -------------------------------------------------------------------------------- /pyiga/compile.py: -------------------------------------------------------------------------------- 1 | import pyiga 2 | from pyiga.codegen import cython as codegen 3 | 4 | import tempfile 5 | import importlib 6 | import sys 7 | import os.path 8 | import platformdirs 9 | import hashlib 10 | 11 | import numpy 12 | 13 | from setuptools import Extension 14 | 15 | import Cython 16 | import Cython.Compiler.Options 17 | from Cython.Build.Inline import _get_build_extension 18 | from Cython.Build.Dependencies import cythonize 19 | 20 | 21 | PYIGAPATH = os.path.normpath(os.path.join(os.path.split(pyiga.__file__)[0], '..')) 22 | MODDIR = os.path.join(platformdirs.user_cache_dir('pyiga'), 'modules') 23 | 24 | 25 | def _compile_cython_module_nocache(src, modname, verbose=False): 26 | modfile = os.path.join(MODDIR, modname + '.pyx') 27 | with open(modfile, 'w+') as f: 28 | f.write(src) 29 | Cython.Compiler.Options.cimport_from_pyx = True 30 | 31 | include_dirs = [ 32 | numpy.get_include() 33 | ] 34 | extra_compile_args = ['-O3', '-march=native', '-ffast-math', '-fopenmp', '-g1'] 35 | 36 | c_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] 37 | 38 | extension = Extension(name=modname, 39 | sources=[modfile], 40 | include_dirs=include_dirs, 41 | extra_compile_args=extra_compile_args, 42 | define_macros=c_macros) 43 | 44 | cython_include_dirs = [PYIGAPATH] 45 | build_extension = _get_build_extension() 46 | build_extension.extensions = cythonize([extension], 47 | include_path=cython_include_dirs, 48 | compiler_directives={'language_level': 3}, 49 | quiet=False) 50 | build_extension.build_temp = MODDIR 51 | build_extension.build_lib = MODDIR 52 | 53 | #distutils.log.set_verbosity(verbose) 54 | build_extension.run() 55 | return importlib.import_module(modname) 56 | 57 | 58 | def compile_cython_module(src, verbose=False): 59 | """Compile module from the given Cython source string and return the loaded 60 | module object. 61 | 62 | Performs caching.""" 63 | os.makedirs(MODDIR, exist_ok=True) 64 | if not MODDIR in sys.path: 65 | sys.path.append(MODDIR) 66 | 67 | # NB: builtin hash() is not deterministic across runs! use SHAKE instead 68 | modname = 'mod' + hashlib.shake_128(src.encode()).hexdigest(8) 69 | try: 70 | mod = importlib.import_module(modname) 71 | except ImportError: 72 | mod = _compile_cython_module_nocache(src, modname, verbose=verbose) 73 | return mod 74 | 75 | 76 | def generate(vf, classname='CustomAssembler', on_demand=False): 77 | """Generate Cython code for an assembler class which implements the vform `vf`.""" 78 | code = codegen.CodeGen() 79 | codegen.AsmGenerator(vf, classname, code, on_demand=on_demand).generate() 80 | return codegen.preamble() + '\n' + code.result() 81 | 82 | # There are two levels of caching for compiling vforms: (1) from a hash of the 83 | # vform expression to the assembler class, (2) from a hash of the Cython source 84 | # code to the compiled and loaded extension module. 85 | # 86 | # (2) is the most important one since compiling Cython -> C -> extension module 87 | # is very slow. This cache persists across processes since the modules are 88 | # stored on disk. 89 | # 90 | # (1) can still be useful since going from VForm to Cython source code, while 91 | # much faster than the compilation steps afterwards, can still add up if 92 | # compile_vform() is called repeatedly (like in an adaptive refinement loop). 93 | # This cache (which uses the following __vform_asm_cache dict) is much lighter- 94 | # weight and is only kept in-process. 95 | # 96 | # As an added benefit, this cache allows us to cache compilers for predefined 97 | # vforms which are contained in the assemblers module. 98 | # 99 | __vform_asm_cache = dict() 100 | 101 | def __asm_cache_args(on_demand): 102 | return (on_demand,) 103 | 104 | def __add_to_vform_asm_cache(vf, asm): 105 | cache_key = (vf.hash(), __asm_cache_args(False)) 106 | __vform_asm_cache[cache_key] = asm 107 | 108 | # add predefined assemblers to the cache 109 | from . import assemblers, vform 110 | for dim in (2, 3): 111 | nD = str(dim) + 'D' 112 | __add_to_vform_asm_cache(vform.mass_vf(dim), getattr(assemblers, 'MassAssembler'+nD)) 113 | __add_to_vform_asm_cache(vform.stiffness_vf(dim), getattr(assemblers, 'StiffnessAssembler'+nD)) 114 | __add_to_vform_asm_cache(vform.heat_st_vf(dim), getattr(assemblers, 'HeatAssembler_ST'+nD)) 115 | __add_to_vform_asm_cache(vform.wave_st_vf(dim), getattr(assemblers, 'WaveAssembler_ST'+nD)) 116 | __add_to_vform_asm_cache(vform.divdiv_vf(dim), getattr(assemblers, 'DivDivAssembler'+nD)) 117 | __add_to_vform_asm_cache(vform.L2functional_vf(dim), getattr(assemblers, 'L2FunctionalAssembler'+nD)) 118 | __add_to_vform_asm_cache(vform.L2functional_vf(dim, physical=True), getattr(assemblers, 'L2FunctionalAssemblerPhys'+nD)) 119 | 120 | def compile_vform(vf, verbose=False, on_demand=False): 121 | """Compile the vform `vf` into an assembler class.""" 122 | cache_key = (vf.hash(), __asm_cache_args(on_demand)) 123 | global __vform_asm_cache 124 | cached_asm = __vform_asm_cache.get(cache_key) 125 | if cached_asm: 126 | return cached_asm 127 | else: 128 | src = generate(vf, on_demand=on_demand) 129 | mod = compile_cython_module(src, verbose=verbose) 130 | asm = mod.CustomAssembler 131 | __vform_asm_cache[cache_key] = asm 132 | return asm 133 | 134 | def compile_vforms(vfs, verbose=False): 135 | """Compile a list of vforms into assembler classes. 136 | 137 | This may be faster than compiling each vform individually since they are 138 | all combined into one source file. 139 | """ 140 | vfs = tuple(vfs) 141 | names = tuple('CustomAssembler%d' % i for i in range(len(vfs))) 142 | 143 | code = codegen.CodeGen() 144 | for (name, vf) in zip(names, vfs): 145 | codegen.AsmGenerator(vf, name, code).generate() 146 | src = codegen.preamble() + '\n' + code.result() 147 | 148 | mod = compile_cython_module(src, verbose=verbose) 149 | return tuple(getattr(mod, name) for name in names) 150 | -------------------------------------------------------------------------------- /pyiga/fast_assemble_cy.pxd: -------------------------------------------------------------------------------- 1 | 2 | ctypedef double (*MatrixEntryFn)(size_t i, size_t j, void * data) noexcept 3 | 4 | cdef object fast_assemble_2d_wrapper(MatrixEntryFn entry_func, void * data, kvs, 5 | double tol, int maxiter, int skipcount, int tolcount, int verbose) 6 | 7 | cdef object fast_assemble_3d_wrapper(MatrixEntryFn entry_func, void * data, kvs, 8 | double tol, int maxiter, int skipcount, int tolcount, int verbose) 9 | -------------------------------------------------------------------------------- /pyiga/fast_assemble_cy.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libcpp.vector cimport vector 3 | from cython cimport view # avoid compiler crash 4 | from cpython cimport pycapsule 5 | 6 | import numpy as np 7 | cimport numpy as np 8 | 9 | import scipy.sparse 10 | 11 | # 12 | # Imports from fastasm.cc: 13 | # 14 | cdef extern void set_log_func(void (*logfunc)(const char * str, size_t)) noexcept 15 | 16 | cdef extern void fast_assemble_2d_cimpl "fast_assemble_2d"( 17 | MatrixEntryFn entryfunc, void * data, 18 | size_t n0, int bw0, 19 | size_t n1, int bw1, 20 | double tol, int maxiter, int skipcount, int tolcount, 21 | int verbose, 22 | vector[size_t]& entries_i, vector[size_t]& entries_j, vector[double]& entries) noexcept 23 | 24 | cdef extern void fast_assemble_3d_cimpl "fast_assemble_3d"( 25 | MatrixEntryFn entryfunc, void * data, 26 | size_t n0, int bw0, 27 | size_t n1, int bw1, 28 | size_t n2, int bw2, 29 | double tol, int maxiter, int skipcount, int tolcount, 30 | int verbose, 31 | vector[size_t]& entries_i, vector[size_t]& entries_j, vector[double]& entries) noexcept 32 | # 33 | # Imports end 34 | # 35 | 36 | 37 | # this is so that IPython notebooks can capture the output 38 | cdef void _stdout_log_func(const char * s, size_t nbytes) noexcept: 39 | import sys 40 | sys.stdout.write(s[:nbytes].decode('ascii')) 41 | 42 | # slightly higher-level wrapper for the C++ implementation 43 | cdef object fast_assemble_2d_wrapper(MatrixEntryFn entry_func, void * data, kvs, 44 | double tol, int maxiter, int skipcount, int tolcount, int verbose): 45 | cdef vector[size_t] entries_i 46 | cdef vector[size_t] entries_j 47 | cdef vector[double] entries 48 | 49 | set_log_func(_stdout_log_func) 50 | 51 | fast_assemble_2d_cimpl(entry_func, data, 52 | kvs[0].numdofs, kvs[0].p, 53 | kvs[1].numdofs, kvs[1].p, 54 | tol, maxiter, skipcount, tolcount, 55 | verbose, 56 | entries_i, entries_j, entries) 57 | 58 | cdef size_t ne = entries.size() 59 | cdef size_t N = kvs[0].numdofs * kvs[1].numdofs 60 | 61 | #cdef double[:] edata = 62 | (entries.data()) 63 | 64 | return scipy.sparse.coo_matrix( 65 | ( entries.data(), 66 | ( entries_i.data(), 67 | entries_j.data())), 68 | shape=(N,N)).tocsr() 69 | 70 | # slightly higher-level wrapper for the C++ implementation 71 | cdef object fast_assemble_3d_wrapper(MatrixEntryFn entry_func, void * data, kvs, 72 | double tol, int maxiter, int skipcount, int tolcount, int verbose): 73 | cdef vector[size_t] entries_i 74 | cdef vector[size_t] entries_j 75 | cdef vector[double] entries 76 | 77 | set_log_func(_stdout_log_func) 78 | 79 | fast_assemble_3d_cimpl(entry_func, data, 80 | kvs[0].numdofs, kvs[0].p, 81 | kvs[1].numdofs, kvs[1].p, 82 | kvs[2].numdofs, kvs[2].p, 83 | tol, maxiter, skipcount, tolcount, 84 | verbose, 85 | entries_i, entries_j, entries) 86 | 87 | cdef size_t ne = entries.size() 88 | cdef size_t N = kvs[0].numdofs * kvs[1].numdofs * kvs[2].numdofs 89 | 90 | return scipy.sparse.coo_matrix( 91 | ( entries.data(), 92 | ( entries_i.data(), 93 | entries_j.data())), 94 | shape=(N,N)).tocsr() 95 | 96 | 97 | ################################################################################ 98 | # Generic wrapper for arbitrary dimension 99 | ################################################################################ 100 | 101 | def fast_assemble(asm, kvs, tol=1e-10, maxiter=100, skipcount=3, tolcount=3, verbose=2): 102 | capsule = asm.entry_func_ptr() 103 | cdef MatrixEntryFn entryfunc = (pycapsule.PyCapsule_GetPointer(capsule, "entryfunc")) 104 | 105 | dim = len(kvs) 106 | if dim == 2: 107 | return fast_assemble_2d_wrapper(entryfunc, asm, kvs, 108 | tol, maxiter, skipcount, tolcount, verbose) 109 | elif dim == 3: 110 | return fast_assemble_3d_wrapper(entryfunc, asm, kvs, 111 | tol, maxiter, skipcount, tolcount, verbose) 112 | else: 113 | raise NotImplementedError('fast assemblers only implemented for 2D and 3D') 114 | -------------------------------------------------------------------------------- /pyiga/kronecker.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse.linalg 3 | 4 | from . import tensor 5 | 6 | def apply_kronecker(ops, x): 7 | """Apply the Kronecker product of a sequence of square matrices or linear operators.""" 8 | if all(isinstance(A, np.ndarray) for A in ops): 9 | return _apply_kronecker_dense(ops, x) 10 | else: 11 | ops = [scipy.sparse.linalg.aslinearoperator(B) for B in ops] 12 | return _apply_kronecker_linops(ops, x) 13 | 14 | 15 | def _apply_kronecker_linops(ops, x): 16 | """Apply the Kronecker product of a sequence of square linear operators.""" 17 | assert len(ops) >= 1, "Empty Kronecker product" 18 | 19 | if len(ops) == 1: 20 | return ops[0].dot(x) 21 | 22 | # assumption: all operators are square 23 | sz = np.prod([A.shape[0] for A in ops]) 24 | assert sz == x.shape[0], "Wrong size for input matrix" 25 | 26 | orig_shape = x.shape 27 | # make sure input x is 2D ndarray 28 | if (len(orig_shape) == 1): 29 | x = x.reshape((orig_shape[0], 1)) 30 | 31 | n = x.shape[1] # number of input vectors 32 | 33 | # algorithm relies on column-major matrices 34 | q0 = np.empty(x.shape, order='F') 35 | q0[:] = x 36 | q1 = np.empty(x.shape, order='F') 37 | 38 | for i in reversed(range(len(ops))): 39 | sz_i = ops[i].shape[1] 40 | r_i = sz // sz_i # result is always integer 41 | 42 | q0 = q0.reshape((sz_i, n * r_i), order='F') 43 | q1.resize((r_i, n * sz_i)) 44 | 45 | if n == 1: 46 | q1[:] = ops[i].dot(q0).T 47 | else: 48 | for k in range(n): 49 | # apply op to coefficients for k-th rhs vector 50 | temp = ops[i].dot(q0[:, k*r_i : (k+1)*r_i]) # sz_i x r_i 51 | q1[:, k*sz_i : (k+1)*sz_i] = temp.T 52 | 53 | q0, q1 = q1, q0 # swap input and output 54 | 55 | return q0.reshape(orig_shape, order='F') 56 | 57 | 58 | def _apply_kronecker_dense(ops, x): 59 | shape_in = tuple(op.shape[1] for op in ops) 60 | shape_out = (np.prod([op.shape[0] for op in ops]),) + x.shape[1:] 61 | assert x.ndim == 1 or x.ndim == 2, \ 62 | 'Only vectors or matrices allowed as right-hand sides' 63 | if x.ndim == 2 and x.shape[1] > 1: 64 | m = x.shape[1] 65 | shape_in = shape_in + (m,) 66 | X = x.reshape(shape_in) 67 | Y = tensor.apply_tprod(ops, X) 68 | return Y.reshape(shape_out) 69 | 70 | -------------------------------------------------------------------------------- /pyiga/lowrank.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.random 3 | 4 | from .lowrank_cy import * 5 | from . import tensor 6 | from . import utils 7 | 8 | ################################################################################ 9 | # Utility class for entrywise matrix/tensor generation 10 | ################################################################################ 11 | 12 | class TensorGenerator: 13 | def __init__(self, shape, entryfunc=None, multientryfunc=None): 14 | """A tensor which is given via a function which computes its entries. 15 | 16 | Arguments: 17 | shape (tuple): the shape of the tensor 18 | `entryfunc`: a function taking a multi-index `(i1, ..., id)` and 19 | producing the corresponding entry of the tensor 20 | `multientryfunc`: a function taking a sequence of multi-indices 21 | and producing an ndarray of the requested entries 22 | 23 | At least one of the two functions must be provided. 24 | """ 25 | self.shape = tuple(shape) 26 | self.ndim = len(self.shape) 27 | assert entryfunc is not None or multientryfunc is not None, \ 28 | "At least one of entryfunc and multientryfunc must be specified" 29 | if entryfunc is not None: 30 | self.entry = entryfunc 31 | if multientryfunc is not None: 32 | self.compute_entries = multientryfunc 33 | 34 | @staticmethod 35 | def from_array(X): 36 | return TensorGenerator(X.shape, lambda I: X[tuple(I)]) 37 | 38 | def __getitem__(self, I): 39 | I, shp, singl = tensor._normalize_indices(I, self.shape) 40 | I_arange = [np.arange(ik.start, ik.stop, ik.step) 41 | if isinstance(ik, range) else ik 42 | for ik in I] 43 | indices = utils.cartesian_product(I_arange) 44 | X = self.compute_entries(indices).reshape(shp) 45 | return np.squeeze(X, axis=singl) 46 | 47 | def entry(self, I): 48 | """Generate the entry at index I""" 49 | return self.compute_entries([I])[0] 50 | 51 | def compute_entries(self, indices): 52 | """Compute all entries given by the list of tuples `indices`.""" 53 | indices = list(indices) 54 | n = len(indices) 55 | result = np.empty(n) 56 | for i in range(n): 57 | result[i] = self.entry(indices[i]) 58 | return result 59 | 60 | def matrix_at(self, I, axes): 61 | """Return a TensorGenerator for the matrix slice of this tensor which 62 | passes through index I along the given two axes.""" 63 | assert len(axes) == 2 64 | assert len(I) == len(self.shape) 65 | I = list(I) 66 | def multientryfunc(indices): 67 | indices = list(indices) 68 | for k in range(len(indices)): 69 | I[axes[0]], I[axes[1]] = indices[k] 70 | indices[k] = tuple(I) 71 | return self.compute_entries(indices) 72 | 73 | return TensorGenerator((self.shape[axes[0]], 74 | self.shape[axes[1]]), 75 | multientryfunc=multientryfunc) 76 | 77 | def asarray(self): 78 | """Generate the full tensor as an np.ndarray""" 79 | I = utils.cartesian_product(tuple(np.arange(nk) for nk in self.shape)) 80 | return self.compute_entries(I).reshape(self.shape, order='C') 81 | 82 | 83 | ################################################################################ 84 | # Adaptive cross approximation (ACA) 85 | ################################################################################ 86 | 87 | def aca(A, tol=1e-10, maxiter=100, skipcount=3, tolcount=3, verbose=2, startval=None): 88 | """Adaptive Cross Approximation (ACA) algorithm with row pivoting""" 89 | if not isinstance(A, TensorGenerator): 90 | A = TensorGenerator.from_array(A) # assume it's an array 91 | assert A.ndim == 2 92 | if startval is not None: 93 | X = np.array(startval, order='C') 94 | assert X.shape == A.shape 95 | else: 96 | X = np.zeros(A.shape, order='C') 97 | i = A.shape[0] // 2 # starting row 98 | k = 0 99 | skipcount, max_skipcount = 0, skipcount 100 | tolcount, max_tolcount = 0, tolcount 101 | 102 | while True: 103 | E_row = X[i,:] - A[i,:] 104 | j0 = abs(E_row).argmax() 105 | e = abs(E_row[j0]) 106 | if e < 1e-15: 107 | if verbose >= 2: 108 | print('skipping', i) 109 | i = np.random.randint(A.shape[0]) 110 | skipcount += 1 111 | if skipcount >= max_skipcount: 112 | if verbose >= 1: 113 | print('maximum skip count reached; stopping (%d it.)' % k) 114 | break 115 | else: 116 | continue 117 | elif e < tol: 118 | tolcount += 1 119 | if tolcount >= max_tolcount: 120 | if verbose >= 1: 121 | print('desired tolerance reached', tolcount, 'times; stopping (%d it.)' % k) 122 | break 123 | else: # error is large 124 | skipcount = tolcount = 0 # reset the counters 125 | 126 | if verbose >= 2: 127 | print(i, '\t', j0, '\t', e) 128 | 129 | col = A[:,j0] - X[:,j0] 130 | rank_1_update(X, 1 / E_row[j0], col, E_row) 131 | 132 | col[i] = 0 # error is now 0 there 133 | i = abs(col).argmax() # choose next row to pivot on 134 | k += 1 135 | if k >= maxiter: 136 | if verbose >= 1: 137 | print('Maximum iteration count reached; aborting (%d it.)' % k) 138 | break 139 | return X 140 | 141 | def aca_lr(A, tol=1e-10, maxiter=100, verbose=2): 142 | """ACA which returns the crosses rather than the full matrix""" 143 | if not isinstance(A, TensorGenerator): 144 | A = TensorGenerator.from_array(A) # assume it's an array 145 | assert A.ndim == 2 146 | crosses = [] 147 | 148 | def X_row(i): 149 | return sum(c[i]*r for (c,r) in crosses) 150 | def X_col(j): 151 | return sum(c*r[j] for (c,r) in crosses) 152 | 153 | i = A.shape[0] // 2 # starting row 154 | k = 0 155 | skipcount, max_skipcount = 0, 3 156 | tolcount, max_tolcount = 0, 3 157 | 158 | while k < maxiter: 159 | err_i = X_row(i) - A[i,:] 160 | j0 = abs(err_i).argmax() 161 | e = abs(err_i[j0]) 162 | if e < 1e-15: 163 | if verbose >= 2: 164 | print('skipping', i) #, ' error=', abs(err_i[j0])) 165 | #i = (i + 1) % A.shape[0] 166 | i = np.random.randint(A.shape[0]) 167 | skipcount += 1 168 | if skipcount >= max_skipcount: 169 | if verbose >= 1: 170 | print('maximum skip count reached; stopping (%d it.)' % k) 171 | break 172 | else: 173 | continue 174 | elif e < tol: 175 | tolcount += 1 176 | if tolcount >= max_tolcount: 177 | if verbose >= 1: 178 | print('desired tolerance reached', tolcount, 'times; stopping (%d it.)' % k) 179 | break 180 | else: # error is large 181 | skipcount = tolcount = 0 # reset the counters 182 | 183 | if verbose >= 2: 184 | print(i, '\t', j0, '\t', e) 185 | c = (A[:,j0] - X_col(j0)) / err_i[j0] 186 | crosses.append((c, err_i)) 187 | i = abs(c).argmax() 188 | k += 1 189 | return crosses 190 | 191 | 192 | def aca_3d(A, tol=1e-10, maxiter=100, skipcount=3, tolcount=3, verbose=2, lr=False): 193 | if not isinstance(A, TensorGenerator): 194 | A = TensorGenerator.from_array(A) # assume it's an array 195 | assert A.ndim == 3 196 | 197 | X = np.zeros(A.shape) 198 | if lr: X_lr = tensor.TensorSum(tensor.CanonicalTensor.zeros(A.shape)) 199 | 200 | I = list(m//2 for m in A.shape) # starting index 201 | def randomize(): 202 | for j in range(len(A.shape)): 203 | I[j] = np.random.randint(A.shape[j]) 204 | k = 0 205 | skipcount, max_skipcount = 0, skipcount 206 | tolcount, max_tolcount = 0, tolcount 207 | 208 | while k < maxiter: 209 | E_col = A[:,I[1],I[2]] - X[:,I[1],I[2]] 210 | i0 = abs(E_col).argmax() 211 | e = abs(E_col[i0]) 212 | if e < 1e-15: 213 | if verbose >= 2: 214 | print('skipping', I) 215 | randomize() 216 | skipcount += 1 217 | if skipcount >= max_skipcount: 218 | if verbose >= 1: 219 | print('maximum skip count reached; stopping (%d outer it.)' % k) 220 | break 221 | else: 222 | continue 223 | elif e < tol: 224 | tolcount += 1 225 | if tolcount >= max_tolcount: 226 | if verbose >= 1: 227 | print('desired tolerance reached', tolcount, 'times; stopping (%d outer it.)' % k) 228 | break 229 | else: # error is large 230 | skipcount = tolcount = 0 # reset the counters 231 | 232 | I[0] = i0 233 | if verbose >= 2: 234 | print(I, '\t', e) 235 | 236 | A_mat = aca(A.matrix_at(I, axes=(1,2)), startval=X[i0,:,:], 237 | tol=tol, maxiter=maxiter, 238 | skipcount=max_skipcount, tolcount=max_tolcount, 239 | verbose=min(verbose, 1)) 240 | E_mat = A_mat - X[i0,:,:] 241 | 242 | # add the scaled tensor product E_col * E_mat 243 | aca3d_update(X, 1.0 / E_col[i0], E_col, E_mat) 244 | if lr: 245 | X_lr += tensor.TensorProd(E_col / E_col[i0], E_mat.copy()) 246 | 247 | E_mat[I[1:]] = 0 # error is now (close to) zero there 248 | I[1:] = np.unravel_index(abs(E_mat).argmax(), E_mat.shape) 249 | k += 1 250 | if k >= maxiter: 251 | if verbose >= 1: 252 | print('Maximum iteration count reached; aborting (%d outer it.)' % k) 253 | break 254 | if lr: 255 | return tensor.TensorSum(*X_lr.Xs[1:]) # skip the zero CanonicalTensor 256 | else: 257 | return X 258 | 259 | 260 | #from .lowrank_cy import * 261 | 262 | -------------------------------------------------------------------------------- /pyiga/lowrank_cy.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | @cython.boundscheck(False) 4 | @cython.wraparound(False) 5 | cpdef void rank_1_update(double[:,::1] X, double alpha, double[::1] u, double[::1] v) noexcept: 6 | """Perform the update `X += alpha * u * v^T`. 7 | 8 | This does the same thing as the BLAS function `dger`, but OpenBLAS 9 | tries to parallelize it, which hurts more than it helps. Instead of 10 | forcing OMP_NUM_THREADS=1, which slows down many other things, 11 | we write our own. 12 | """ 13 | cdef double au 14 | cdef size_t i, j 15 | for i in range(X.shape[0]): 16 | au = alpha * u[i] 17 | for j in range(X.shape[1]): 18 | X[i,j] += au * v[j] 19 | 20 | 21 | @cython.boundscheck(False) 22 | @cython.wraparound(False) 23 | cpdef void aca3d_update(double[:,:,::1] X, double alpha, double[::1] u, double[:,::1] V) noexcept: 24 | cdef double au 25 | cdef size_t i, j, k 26 | for i in range(X.shape[0]): 27 | au = alpha * u[i] 28 | for j in range(X.shape[1]): 29 | for k in range(X.shape[2]): 30 | X[i,j,k] += au * V[j,k] 31 | 32 | -------------------------------------------------------------------------------- /pyiga/mlmatrix_cy.pyx: -------------------------------------------------------------------------------- 1 | # cython: profile=False 2 | 3 | cimport cython 4 | from cython.parallel import prange 5 | 6 | import numpy as np 7 | cimport numpy as np 8 | 9 | import scipy.sparse 10 | 11 | ################################################################################ 12 | # Reindexing 13 | ################################################################################ 14 | 15 | @cython.cdivision(True) 16 | def reindex_from_reordered(size_t i, size_t j, size_t m1, size_t n1, size_t m2, size_t n2): 17 | """Convert (i,j) from an index into reorder(X, m1, n1) into the 18 | corresponding index into X (reordered to original). 19 | 20 | Arguments: 21 | i = row = block index (0...m1*n1) 22 | j = column = index within block (0...m2*n2) 23 | 24 | Returns: 25 | a pair of indices with ranges `(0...m1*m2, 0...n1*n2)` 26 | """ 27 | cdef size_t bi0, bi1, ii0, ii1 28 | bi0, bi1 = i // n1, i % n1 # range: m1, n1 29 | ii0, ii1 = j // n2, j % n2 # range: m2, n2 30 | return (bi0*m2 + ii0, bi1*n2 + ii1) 31 | 32 | 33 | @cython.cdivision(True) 34 | @cython.boundscheck(False) 35 | @cython.wraparound(False) 36 | cdef inline void from_seq2(np.int64_t i, np.int64_t[:] dims, size_t[2] out) noexcept: 37 | out[0] = i // dims[1] 38 | out[1] = i % dims[1] 39 | 40 | @cython.cdivision(True) 41 | @cython.boundscheck(False) 42 | @cython.wraparound(False) 43 | cdef object from_seq(np.int64_t i, np.int64_t[:] dims): 44 | """Convert sequential (lexicographic) index into multiindex. 45 | 46 | Same as np.unravel_index(i, dims) except for returning a list. 47 | """ 48 | cdef np.int64_t L, k 49 | L = len(dims) 50 | I = L * [0] 51 | for k in reversed(range(L)): 52 | mk = dims[k] 53 | I[k] = i % mk 54 | i //= mk 55 | return I 56 | 57 | @cython.cdivision(True) 58 | @cython.boundscheck(False) 59 | @cython.wraparound(False) 60 | cdef np.int64_t to_seq(np.int64_t* I, np.int64_t* dims, int n) noexcept nogil: 61 | """Convert multiindex into sequential (lexicographic) index. 62 | 63 | Same as np.ravel_multiindex(I, dims). 64 | """ 65 | cdef np.int64_t i, k 66 | i = 0 67 | for k in range(n): 68 | i *= dims[k] 69 | i += I[k] 70 | return i 71 | 72 | # this doesn't work currently 73 | #def reindex_to_multilevel(i, j, bs): 74 | # bs = np.array(bs, copy=False) # bs has shape L x 2 75 | # I, J = from_seq(i, bs[:,0]), from_seq(j, bs[:,1]) # multilevel indices 76 | # return tuple(to_seq((I[k],J[k]), bs[k,:]) for k in range(bs.shape[0])) # <<- to_seq() expects buffers 77 | 78 | def reindex_from_multilevel(M, np.int64_t[:,:] bs): 79 | """Convert a multiindex M with length L into sequential indices (i,j) 80 | of a multilevel matrix with L levels and block sizes bs. 81 | 82 | This is the multilevel version of reindex_from_reordered, which does the 83 | same for the two-level case. 84 | 85 | Arguments: 86 | M: the multiindex to convert 87 | bs: the block sizes; ndarray of shape Lx2. Each row gives the sizes 88 | (rows and columns) of the blocks on the corresponding matrix level. 89 | """ 90 | cdef size_t L = len(M), k 91 | cdef size_t[2] ij 92 | cdef size_t ii=0, jj=0 93 | 94 | for k in range(L): 95 | from_seq2(M[k], bs[k,:], ij) 96 | ii *= bs[k,0] 97 | ii += ij[0] 98 | jj *= bs[k,1] 99 | jj += ij[1] 100 | return (ii, jj) 101 | 102 | 103 | def get_transpose_idx_for_bidx(bidx): 104 | cdef dict jidict 105 | jidict = {} 106 | for (k,(i,j)) in enumerate(bidx): 107 | jidict[(j,i)] = k 108 | 109 | transpose_bidx = np.empty(len(bidx), dtype=np.uintp) 110 | 111 | for k in range(len(bidx)): 112 | transpose_bidx[k] = jidict[ tuple(bidx[k]) ] 113 | return transpose_bidx 114 | 115 | 116 | # computes the Cartesian product of indices and ravels them according to the size `dims` 117 | @cython.boundscheck(False) 118 | @cython.wraparound(False) 119 | cdef pyx_raveled_cartesian_product(arrays, np.int64_t[::1] dims): 120 | cdef int L = len(arrays) 121 | cdef np.int64_t[8] I # iteration index 122 | cdef np.int64_t[8] K # corresponding indices K[k] = arrays[k][I[k]] 123 | cdef np.int64_t[8] shp # size of Cartesian product 124 | cdef int i, k, N 125 | 126 | cdef (np.int64_t*)[8] arr_ptrs 127 | cdef np.int64_t[:] arr 128 | 129 | N = 1 130 | for k in range(L): 131 | # initialize shape 132 | shp[k] = arrays[k].shape[0] 133 | if shp[k] == 0: 134 | return np.zeros(0, dtype=int) 135 | N *= shp[k] 136 | # initialize pointer 137 | arr = arrays[k] 138 | arr_ptrs[k] = &arr[0] 139 | # initialize I and K 140 | I[k] = 0 141 | K[k] = arr_ptrs[k][I[k]] 142 | 143 | out_buf = np.empty(N, dtype=int) 144 | cdef np.int64_t[::1] out = out_buf 145 | 146 | with nogil: 147 | for i in range(N): 148 | # compute raveled index at current multi-index K 149 | out[i] = to_seq(&K[0], &dims[0], L) 150 | 151 | # increment multi-index I and update K 152 | for k in reversed(range(L)): 153 | I[k] += 1 154 | if I[k] < shp[k]: 155 | K[k] = arr_ptrs[k][I[k]] 156 | break 157 | else: 158 | I[k] = 0 159 | K[k] = arr_ptrs[k][I[k]] 160 | # go on to increment previous one 161 | return out_buf 162 | 163 | # helper function for nonzeros_for_rows 164 | @cython.boundscheck(False) 165 | @cython.wraparound(False) 166 | def pyx_rowwise_cartesian_product(lvia, np.int64_t[:, ::1] ix, np.int64_t[::1] block_sizes): 167 | cdef int N = ix.shape[0] 168 | cdef int L = ix.shape[1] 169 | assert L <= 8, 'pyx_raveled_cartesian_product only implemented for L <= 8' 170 | cdef int i 171 | 172 | Js = N * [None] 173 | for i in range(N): # loop over all row_indices 174 | # obtain the levelwise interactions for each index i_k = ix[i,k] 175 | ia_k = tuple(lvia[k][ix[i,k]] for k in range(L)) 176 | # compute global interactions by taking the Cartesian product 177 | Js[i] = pyx_raveled_cartesian_product(ia_k, block_sizes) 178 | return Js 179 | 180 | ################################################################################ 181 | # Inflation and matvec 182 | ################################################################################ 183 | 184 | ## 2D 185 | 186 | @cython.cdivision(True) 187 | @cython.boundscheck(False) 188 | @cython.wraparound(False) 189 | cpdef object ml_nonzero_2d(bidx, block_sizes, bint lower_tri=False): 190 | cdef unsigned[:,::1] bidx1, bidx2 191 | cdef int m2,n2 192 | cdef size_t i, j, Ni, Nj, N, idx, I, J 193 | cdef unsigned xi0, xi1, yi0, yi1 194 | 195 | bidx1, bidx2 = bidx 196 | m2, n2 = block_sizes[1] 197 | Ni, Nj = len(bidx1), len(bidx2) 198 | N = Ni * Nj 199 | results = np.empty((2, N), dtype=np.uintp) 200 | cdef size_t[:, ::1] IJ = results 201 | 202 | with nogil: 203 | idx = 0 204 | for i in range(Ni): 205 | xi0, xi1 = bidx1[i,0], bidx1[i,1] # range: m1, n1 206 | for j in range(Nj): 207 | yi0, yi1 = bidx2[j,0], bidx2[j,1] # range: m2, n2 208 | 209 | I = xi0 * m2 + yi0 # range: m1*m2*m3 210 | J = xi1 * n2 + yi1 # range: n1*n2*n3 211 | if not lower_tri or J <= I: 212 | IJ[0,idx] = I 213 | IJ[1,idx] = J 214 | idx += 1 215 | if idx < N: # TODO: what's the closed formula for N in the triangular case? 216 | return results[:, :idx] 217 | else: 218 | return results 219 | 220 | 221 | @cython.cdivision(True) 222 | @cython.boundscheck(False) 223 | @cython.wraparound(False) 224 | cpdef void ml_matvec_2d(double[:,::1] X, bidx, block_sizes, double[::1] x, double[::1] y) noexcept: 225 | cdef unsigned[:,::1] bidx1, bidx2 226 | bidx1,bidx2 = bidx 227 | 228 | cdef int m2, n2 229 | m2,n2 = block_sizes[1] 230 | 231 | cdef size_t i, j, M, N, I, J 232 | M, N = X.shape[0], X.shape[1] 233 | 234 | cdef unsigned bi0, bi1, ii0, ii1 235 | 236 | assert len(bidx1) == M 237 | assert len(bidx2) == N 238 | 239 | #for i in prange(M, schedule='static', nogil=True): 240 | # the += update is not thread safe! need atomics 241 | for i in range(M): 242 | bi0, bi1 = bidx1[i,0], bidx1[i,1] # range: m1, n1 243 | for j in range(N): 244 | ii0, ii1 = bidx2[j,0], bidx2[j,1] # range: m2, n2 245 | 246 | I = bi0*m2 + ii0 # range: m1*m2 247 | J = bi1*n2 + ii1 # range: n1*n2 248 | 249 | y[I] += X[i,j] * x[J] 250 | 251 | 252 | ## 3D 253 | 254 | @cython.cdivision(True) 255 | @cython.boundscheck(False) 256 | @cython.wraparound(False) 257 | cpdef object ml_nonzero_3d(bidx, block_sizes, bint lower_tri=False): 258 | cdef unsigned[:,::1] bidx1, bidx2, bidx3 259 | cdef int m2,n2, m3,n3 260 | cdef size_t i, j, k, Ni, Nj, Nk, N, idx, I, J 261 | cdef unsigned xi0, xi1, yi0, yi1, zi0, zi1 262 | 263 | bidx1, bidx2, bidx3 = bidx 264 | m2, n2 = block_sizes[1] 265 | m3, n3 = block_sizes[2] 266 | Ni, Nj, Nk = len(bidx1), len(bidx2), len(bidx3) 267 | N = Ni * Nj * Nk 268 | results = np.empty((2, N), dtype=np.uintp) 269 | cdef size_t[:, ::1] IJ = results 270 | 271 | with nogil: 272 | idx = 0 273 | for i in range(Ni): 274 | xi0, xi1 = bidx1[i,0], bidx1[i,1] # range: m1, n1 275 | for j in range(Nj): 276 | yi0, yi1 = bidx2[j,0], bidx2[j,1] # range: m2, n2 277 | for k in range(Nk): 278 | zi0, zi1 = bidx3[k,0], bidx3[k,1] # range: m3, n3 279 | 280 | I = (xi0 * m2 + yi0) * m3 + zi0 # range: m1*m2*m3 281 | J = (xi1 * n2 + yi1) * n3 + zi1 # range: n1*n2*n3 282 | if not lower_tri or J <= I: 283 | IJ[0,idx] = I 284 | IJ[1,idx] = J 285 | idx += 1 286 | if idx < N: # TODO: what's the closed formula for N in the triangular case? 287 | return results[:, :idx] 288 | else: 289 | return results 290 | 291 | 292 | @cython.cdivision(True) 293 | @cython.boundscheck(False) 294 | @cython.wraparound(False) 295 | cpdef void ml_matvec_3d(double[:,:,::1] X, bidx, block_sizes, double[::1] x, double[::1] y) noexcept: 296 | cdef unsigned[:,::1] bidx1, bidx2, bidx3 297 | bidx1,bidx2,bidx3 = bidx 298 | 299 | cdef int m2,n2, m3,n3 300 | m2,n2 = block_sizes[1] 301 | m3,n3 = block_sizes[2] 302 | 303 | cdef size_t i, j, k, Ni, Nj, Nk, I, J 304 | cdef unsigned xi0, xi1, yi0, yi1, zi0, zi1 305 | 306 | Ni,Nj,Nk = X.shape[:3] 307 | assert len(bidx1) == Ni 308 | assert len(bidx2) == Nj 309 | assert len(bidx3) == Nk 310 | 311 | #for i in prange(Ni, schedule='static', nogil=True): 312 | # the += update is not thread safe! need atomics 313 | for i in range(Ni): 314 | xi0, xi1 = bidx1[i,0], bidx1[i,1] # range: m1, n1 315 | 316 | for j in range(Nj): 317 | yi0, yi1 = bidx2[j,0], bidx2[j,1] # range: m2, n2 318 | 319 | for k in range(Nk): 320 | zi0, zi1 = bidx3[k,0], bidx3[k,1] # range: m3, n3 321 | 322 | I = (xi0 * m2 + yi0) * m3 + zi0 # range: m1*m2*m3 323 | J = (xi1 * n2 + yi1) * n3 + zi1 # range: n1*n2*n3 324 | 325 | y[I] += X[i,j,k] * x[J] 326 | 327 | 328 | # generic dimension 329 | 330 | @cython.cdivision(True) 331 | @cython.boundscheck(False) 332 | @cython.wraparound(False) 333 | cpdef object ml_nonzero_nd(bidx, block_sizes, bint lower_tri=False): 334 | cdef unsigned L = len(bidx) 335 | assert L <= 8, 'ml_nonzero_nd only implemented for L <= 8' 336 | 337 | cdef (unsigned*)[8] bidx_ptr 338 | cdef np.int64_t NN[8] # number of nonzeros per dimension 339 | cdef np.int64_t block_rows[8] # number of rows in dimension k 340 | cdef np.int64_t block_cols[8] # number of columns in dimension k 341 | cdef np.int64_t cur_idx[8] # current nonzero idx (0..NN[k]) 342 | cdef np.int64_t block_i[8] # row of current nonzero corresponding to cur_idx[k] 343 | cdef np.int64_t block_j[8] # column of current nonzero corresponding to cur_idx[k] 344 | 345 | cdef unsigned[:,::1] bidx_temp 346 | cdef size_t k, idx, N, I, J 347 | 348 | N = 1 349 | for i in range(L): 350 | bidx_temp = bidx[i] 351 | bidx_ptr[i] = &bidx_temp[0,0] 352 | NN[i] = len(bidx_temp) 353 | N *= NN[i] 354 | block_rows[i], block_cols[i] = block_sizes[i] 355 | 356 | # intialize cur_idx to 0 and set the corresponding block_i/block_j values 357 | cur_idx[i] = 0 358 | block_i[i], block_j[i] = bidx_ptr[i][0], bidx_ptr[0][1] 359 | 360 | results = np.empty((2, N), dtype=np.uintp) 361 | cdef size_t[:, ::1] IJ = results 362 | 363 | cdef bint done = (N == 0) 364 | 365 | with nogil: 366 | idx = 0 367 | 368 | while not done: 369 | # compute sequential index for current (I,J) multi-indices 370 | I = to_seq(&block_i[0], &block_rows[0], L) 371 | J = to_seq(&block_j[0], &block_cols[0], L) 372 | 373 | if not lower_tri or J <= I: 374 | IJ[0,idx] = I 375 | IJ[1,idx] = J 376 | idx += 1 377 | 378 | # increment multi-index cur_idx and update block_i, block_j 379 | for k in reversed(range(L)): 380 | cur_idx[k] += 1 381 | if cur_idx[k] < NN[k]: 382 | block_i[k], block_j[k] = bidx_ptr[k][2*cur_idx[k]+0], bidx_ptr[k][2*cur_idx[k]+1] 383 | break 384 | else: 385 | if k == 0: # reached the end of the first dimension? 386 | done = True 387 | break 388 | cur_idx[k] = 0 389 | block_i[k], block_j[k] = bidx_ptr[k][2*cur_idx[k]+0], bidx_ptr[k][2*cur_idx[k]+1] 390 | # go on to increment previous one 391 | 392 | if idx < N: # TODO: what's the closed formula for N in the triangular case? 393 | return results[:, :idx] 394 | else: 395 | return results 396 | 397 | -------------------------------------------------------------------------------- /pyiga/operators.py: -------------------------------------------------------------------------------- 1 | """Classes and functions for creating custom instances of :class:`scipy.sparse.linalg.LinearOperator`.""" 2 | import numpy as np 3 | import scipy.sparse.linalg 4 | from builtins import range # Python 2 compatibility 5 | 6 | from . import kronecker 7 | 8 | HAVE_MKL = True 9 | try: 10 | import pyMKL 11 | except: 12 | HAVE_MKL = False 13 | 14 | 15 | class NullOperator(scipy.sparse.linalg.LinearOperator): 16 | """Null operator of the given shape which always returns zeros. Used as placeholder.""" 17 | def __init__(self, shape, dtype=np.float64): 18 | scipy.sparse.linalg.LinearOperator.__init__(self, shape=shape, dtype=dtype) 19 | 20 | def _matvec(self, x): 21 | return np.zeros(self.shape[0], dtype=self.dtype) 22 | def _matmat(self, x): 23 | return np.zeros((self.shape[0], x.shape[1]), dtype=self.dtype) 24 | def _transpose(self): 25 | return NullOperator((self.shape[1], self.shape[0]), dtype=self.dtype) 26 | 27 | class IdentityOperator(scipy.sparse.linalg.LinearOperator): 28 | """Identity operator of size `n`.""" 29 | def __init__(self, n, dtype=np.float64): 30 | scipy.sparse.linalg.LinearOperator.__init__(self, shape=(n,n), dtype=dtype) 31 | def _matvec(self, x): 32 | return x 33 | def _matmat(self, x): 34 | return x 35 | def _transpose(self): 36 | return self 37 | 38 | class DiagonalOperator(scipy.sparse.linalg.LinearOperator): 39 | """A :class:`LinearOperator` which acts like a diagonal matrix with the given diagonal.""" 40 | def __init__(self, diag): 41 | diag = np.squeeze(diag) 42 | assert diag.ndim == 1, 'Diagonal must be a vector' 43 | N = diag.shape[0] 44 | self.diag = diag 45 | scipy.sparse.linalg.LinearOperator.__init__(self, shape=(N,N), dtype=diag.dtype) 46 | 47 | def _matvec(self, x): 48 | if x.ndim == 1: 49 | return self.diag * x 50 | else: 51 | return self.diag[:,None] * x 52 | 53 | def _matmat(self, x): 54 | return self._matvec(x) 55 | 56 | def _transpose(self): 57 | return self 58 | 59 | 60 | class KroneckerOperator(scipy.sparse.linalg.LinearOperator): 61 | """A :class:`LinearOperator` which efficiently implements the 62 | application of the Kronecker product of the given input operators. 63 | """ 64 | def __init__(self, *ops): 65 | self.ops = ops 66 | sz = np.prod([A.shape[1] for A in ops]) 67 | sz_out = np.prod([A.shape[0] for A in ops]) 68 | alldense = all(isinstance(A, np.ndarray) for A in ops) 69 | allsquare = all(A.shape[0] == A.shape[1] for A in ops) 70 | if alldense or not allsquare: 71 | self.applyfunc = kronecker._apply_kronecker_dense 72 | else: # use implementation for square LinearOperators; TODO: is this faster?? 73 | self.applyfunc = kronecker._apply_kronecker_linops 74 | scipy.sparse.linalg.LinearOperator.__init__(self, dtype=ops[0].dtype, shape=(sz_out,sz)) 75 | 76 | def _matvec(self, x): 77 | return self.applyfunc(self.ops, x) 78 | 79 | def _matmat(self, x): 80 | return self.applyfunc(self.ops, x) 81 | 82 | def _transpose(self): 83 | return KroneckerOperator(*(B.T for B in self.ops)) 84 | 85 | def _adjoint(self): 86 | return KroneckerOperator(*(B.H for B in self.ops)) 87 | 88 | 89 | class BaseBlockOperator(scipy.sparse.linalg.LinearOperator): 90 | def __init__(self, shape, ops, ran_out, ran_in): 91 | self.ops = ops 92 | self.ran_out = ran_out 93 | self.ran_in = ran_in 94 | scipy.sparse.linalg.LinearOperator.__init__(self, ops[0].dtype, shape) 95 | 96 | def _matvec(self, x): 97 | y = np.zeros(self.shape[0]) 98 | if x.ndim == 2: 99 | x = x[:,0] 100 | for i in range(len(self.ops)): 101 | y[self.ran_out[i]] += self.ops[i].dot(x[self.ran_in[i]]) 102 | return y 103 | 104 | def _matmat(self, x): 105 | y = np.zeros((self.shape[0], x.shape[1])) 106 | for i in range(len(self.ops)): 107 | y[self.ran_out[i]] += self.ops[i].dot(x[self.ran_in[i]]) 108 | return y 109 | 110 | def _transpose(self): 111 | shape_T = (self.shape[1], self.shape[0]) 112 | return BaseBlockOperator(shape_T, tuple(op.T for op in self.ops), 113 | self.ran_in, self.ran_out) 114 | 115 | def _adjoint(self): 116 | shape_T = (self.shape[1], self.shape[0]) 117 | return BaseBlockOperator(shape_T, tuple(op.H for op in self.ops), 118 | self.ran_in, self.ran_out) 119 | 120 | 121 | def _sizes_to_ranges(sizes): 122 | """Convert an iterable of sizes into a list of consecutive ranges of these sizes.""" 123 | sizes = list(sizes) 124 | runsizes = [0] + list(np.cumsum(sizes)) 125 | return [range(runsizes[k], runsizes[k+1]) for k in range(len(sizes))] 126 | 127 | 128 | def BlockDiagonalOperator(*ops): 129 | """Return a :class:`LinearOperator` with block diagonal structure, with the given 130 | operators on the diagonal. 131 | """ 132 | ranges_i = _sizes_to_ranges(op.shape[0] for op in ops) 133 | ranges_j = _sizes_to_ranges(op.shape[1] for op in ops) 134 | shape = (ranges_i[-1].stop, ranges_j[-1].stop) 135 | return BaseBlockOperator(shape, ops, ranges_i, ranges_j) 136 | 137 | 138 | def BlockOperator(ops): 139 | """Construct a block operator. 140 | 141 | Args: 142 | ops (list): a rectangular list of lists of operators or matrices. 143 | All operators in a given row should have the same height 144 | (output dimension). 145 | All operators in a given column should have the same width 146 | (input dimension). 147 | Empty blocks should use :class:`NullOperator` as a placeholder. 148 | 149 | Returns: 150 | LinearOperator: a block structured linear operator. Its height is the 151 | total height of one input column of operators, and its width is the 152 | total width of one input row. 153 | 154 | See also :func:`numpy.block`, which has analogous functionality for 155 | dense matrices. 156 | """ 157 | M, N = len(ops), len(ops[0]) 158 | ranges_i = _sizes_to_ranges(ops[i][0].shape[0] for i in range(M)) 159 | ranges_j = _sizes_to_ranges(ops[0][j].shape[1] for j in range(N)) 160 | shape = (ranges_i[-1].stop, ranges_j[-1].stop) 161 | 162 | ops_list, ranges_i_list, ranges_j_list = [], [], [] 163 | for i in range(M): 164 | assert len(ops[i]) == N, "All rows must have equal length" 165 | for j in range(N): 166 | op = ops[i][j] 167 | if op is None or isinstance(op, NullOperator): 168 | continue 169 | else: 170 | assert op.shape == (len(ranges_i[i]), len(ranges_j[j])), \ 171 | "Operator at position (%d,%d) has wrong shape" % (i,j) 172 | ops_list.append(op) 173 | ranges_i_list.append(ranges_i[i]) 174 | ranges_j_list.append(ranges_j[j]) 175 | if ops_list: 176 | return BaseBlockOperator(shape, ops_list, ranges_i_list, ranges_j_list) 177 | else: 178 | return NullOperator(shape) 179 | 180 | 181 | class SubspaceOperator(scipy.sparse.linalg.LinearOperator): 182 | r"""Implements an abstract additive subspace correction operator. 183 | 184 | Args: 185 | subspaces (seq): a list of `k` prolongation matrices 186 | :math:`P_j \in \mathbb R^{n \times n_j}` 187 | Bs (seq): a list of `k` square matrices or instances of :class:`LinearOperator` 188 | :math:`B_j \in \mathbb R^{n_j \times n_j}` 189 | 190 | Returns: 191 | LinearOperator: operator with shape :math:`n \times n` that implements the action 192 | 193 | .. math:: 194 | Lx = \sum_{j=1}^k P_j B_j P_j^T x 195 | """ 196 | def __init__(self, subspaces, Bs): 197 | subspaces, Bs = tuple(subspaces), tuple(Bs) 198 | assert len(subspaces) == len(Bs) 199 | assert len(Bs) > 0, "No operators given" 200 | n = subspaces[0].shape[0] 201 | self.subspaces = subspaces 202 | self.Bs = Bs 203 | self._is_transpose = False 204 | super().__init__(shape=(n,n), dtype=Bs[0].dtype) 205 | 206 | def _matvec(self, x): 207 | if x.ndim > 1: 208 | x = np.squeeze(x) 209 | y = np.zeros(len(x)) 210 | if self._is_transpose: 211 | for j in range(len(self.subspaces)): 212 | P_j = self.subspaces[j] 213 | y += P_j.dot(self.Bs[j].T.dot(P_j.T.dot(x))) 214 | else: 215 | for j in range(len(self.subspaces)): 216 | P_j = self.subspaces[j] 217 | y += P_j.dot(self.Bs[j].dot(P_j.T.dot(x))) 218 | return y 219 | 220 | def _transpose(self): 221 | Y = SubspaceOperator(self.subspaces, self.Bs) 222 | Y._is_transpose = not self._is_transpose 223 | # shape stays the same since we are square 224 | return Y 225 | 226 | 227 | class PardisoSolverWrapper(scipy.sparse.linalg.LinearOperator): 228 | """Wraps a PARDISO solver object and frees up the memory when deallocated.""" 229 | def __init__(self, shape, dtype, solver): 230 | self.solver = solver 231 | scipy.sparse.linalg.LinearOperator.__init__(self, shape=shape, dtype=dtype) 232 | def _matvec(self, x): 233 | return self.solver.solve(x) 234 | def _matmat(self, x): 235 | return self.solver.solve(x) 236 | def __del__(self): 237 | self.solver.clear() 238 | self.solver = None 239 | 240 | 241 | def make_solver(B, symmetric=False, spd=False): 242 | """Return a :class:`LinearOperator` that acts as a linear solver for the 243 | (dense or sparse) square matrix `B`. 244 | 245 | If `B` is symmetric, passing ``symmetric=True`` may try to take advantage of this. 246 | If `B` is symmetric and positive definite, pass ``spd=True``. 247 | """ 248 | if spd: 249 | symmetric = True 250 | 251 | if scipy.sparse.issparse(B): 252 | if HAVE_MKL: 253 | # use MKL Pardiso 254 | mtype = 11 # real, nonsymmetric 255 | if symmetric: 256 | mtype = 2 if spd else -2 257 | solver = pyMKL.pardisoSolver(B, mtype) 258 | solver.factor() 259 | return PardisoSolverWrapper(B.shape, B.dtype, solver) 260 | else: 261 | # use SuperLU (unless scipy uses UMFPACK?) -- really slow! 262 | spLU = scipy.sparse.linalg.splu(B.tocsc(), permc_spec='NATURAL') 263 | return scipy.sparse.linalg.LinearOperator(B.shape, dtype=B.dtype, 264 | matvec=spLU.solve, matmat=spLU.solve) 265 | else: 266 | if symmetric: 267 | chol = scipy.linalg.cho_factor(B, check_finite=False) 268 | solve = lambda x: scipy.linalg.cho_solve(chol, x, check_finite=False) 269 | return scipy.sparse.linalg.LinearOperator(B.shape, dtype=B.dtype, 270 | matvec=solve, matmat=solve) 271 | else: 272 | LU = scipy.linalg.lu_factor(B, check_finite=False) 273 | solve = lambda x: scipy.linalg.lu_solve(LU, x, check_finite=False) 274 | return scipy.sparse.linalg.LinearOperator(B.shape, dtype=B.dtype, 275 | matvec=solve, matmat=solve) 276 | 277 | 278 | def make_kronecker_solver(*Bs): #, symmetric=False): # kw arg doesn't work in Py2 279 | """Given several square matrices, return an operator which efficiently applies 280 | the inverse of their Kronecker product. 281 | """ 282 | Binvs = tuple(make_solver(B) for B in Bs) 283 | return KroneckerOperator(*Binvs) 284 | 285 | -------------------------------------------------------------------------------- /pyiga/quadrature.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def gauss_rule(deg, a, b): 4 | """Return nodes and weights for Gauss-Legendre rule of given degree in (a,b). 5 | 6 | a and b are arrays containing the start and end points of the intervals.""" 7 | m = 0.5*(a + b) # array of interval midpoints 8 | h = 0.5*(b - a) # array of halved interval lengths 9 | x,w = np.polynomial.legendre.leggauss(deg) 10 | nodes = (np.outer(h,x) + m[:, np.newaxis]) 11 | weights = np.outer(h,w) 12 | return (nodes.ravel(), weights.ravel()) 13 | 14 | def make_iterated_quadrature(intervals, nqp): 15 | return gauss_rule(nqp, intervals[:-1], intervals[1:]) 16 | 17 | def make_tensor_quadrature(meshes, nqp): 18 | gauss = tuple(make_iterated_quadrature(mesh, nqp) for mesh in meshes) 19 | grid = tuple(g[0] for g in gauss) 20 | weights = tuple(g[1] for g in gauss) 21 | return grid, weights 22 | 23 | def make_boundary_quadrature(meshes, nqp, bdspec): 24 | """Compute an iterated Gauss quadrature rule restricted to the given boundary.""" 25 | bdax, bdside = bdspec 26 | bdcoord = meshes[bdax][0 if bdside==0 else -1] 27 | gauss = [make_iterated_quadrature(mesh, nqp) for mesh in meshes] 28 | gauss[bdax] = (np.array([bdcoord]), np.ones((1,))) 29 | grid = tuple(g[0] for g in gauss) 30 | weights = tuple(g[1] for g in gauss) 31 | return grid, weights 32 | -------------------------------------------------------------------------------- /pyiga/relaxation_cy.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | @cython.boundscheck(False) 4 | @cython.wraparound(False) 5 | @cython.cdivision(True) 6 | def gauss_seidel(const int[:] row_ptr, const int[:] col_indices, const double[:] data, 7 | double[:] x, const double[:] b, 8 | int row_start, int row_stop, int row_step): 9 | 10 | cdef int i, j, jj, start, end 11 | cdef double rsum, diag 12 | 13 | i = row_start 14 | while i != row_stop: 15 | start = row_ptr[i] 16 | end = row_ptr[i+1] 17 | rsum = 0.0 18 | diag = 0.0 19 | 20 | for jj in range(start, end): 21 | j = col_indices[jj] 22 | if i == j: 23 | diag = data[jj] 24 | else: 25 | rsum += data[jj] * x[j] 26 | 27 | if diag != 0.0: 28 | x[i] = (b[i] - rsum) / diag 29 | 30 | i += row_step 31 | 32 | @cython.boundscheck(False) 33 | @cython.wraparound(False) 34 | @cython.cdivision(True) 35 | def gauss_seidel_indexed(int[:] row_ptr, int[:] col_indices, double[:] data, 36 | double[:] x, double[:] b, 37 | int[:] indices, bint reverse): 38 | 39 | cdef int idx, I0, I1, Is, i, j, jj, start, end 40 | cdef double rsum, diag 41 | 42 | if reverse: 43 | I0,I1,Is = indices.shape[0] - 1, -1, -1 44 | else: 45 | I0,I1,Is = 0, indices.shape[0], 1 46 | 47 | idx = I0 48 | while idx != I1: 49 | i = indices[idx] 50 | start = row_ptr[i] 51 | end = row_ptr[i+1] 52 | rsum = 0.0 53 | diag = 0.0 54 | 55 | for jj in range(start, end): 56 | j = col_indices[jj] 57 | if i == j: 58 | diag = data[jj] 59 | else: 60 | rsum += data[jj] * x[j] 61 | 62 | if diag != 0.0: 63 | x[i] = (b[i] - rsum) / diag 64 | 65 | idx += Is 66 | -------------------------------------------------------------------------------- /pyiga/spline.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from . import bspline 4 | 5 | class Spline: 6 | def __init__(self, kv, coeffs): 7 | """Create a spline function with the given knot vector and coefficients.""" 8 | coeffs = np.asarray(coeffs) 9 | assert coeffs.shape == (kv.numdofs,) 10 | self.kv = kv 11 | self.coeffs = coeffs 12 | 13 | def eval(self, x): 14 | """Evaluate the spline at all points of the vector x""" 15 | return bspline.ev(self.kv, self.coeffs, x) 16 | 17 | def deriv(self, x, deriv=1): 18 | """Evaluate a derivative of the spline at all points of the vector x""" 19 | return bspline.deriv(self.kv, self.coeffs, deriv, x) 20 | 21 | def derivative(self): 22 | """Return the derivative of this spline as a spline""" 23 | p = self.kv.p 24 | diffcoeffs = p / (self.kv.kv[p+1:-1] - self.kv.kv[1:-(p+1)]) * np.diff(self.coeffs) 25 | diffkv = bspline.KnotVector(self.kv.kv[1:-1], p-1) 26 | return Spline(diffkv, diffcoeffs) 27 | 28 | -------------------------------------------------------------------------------- /pyiga/stilde.py: -------------------------------------------------------------------------------- 1 | # 2 | # Compute basis for the subspace S-tilde 3 | # (splines with vanishing odd derivatives at the boundary) 4 | # 5 | # Algorithm is documented in 6 | # C. Hofreither and S. Takacs: 7 | # "Robust Multigrid for Isogeometric Analysis Based on Stable Splittings of 8 | # Spline Spaces" 9 | # http://www.numa.uni-linz.ac.at/publications/List/2016/2016-02-r1.pdf 10 | # 11 | 12 | import scipy.linalg 13 | import numpy as np 14 | 15 | from . import bspline 16 | 17 | def Stilde_basis_side(kv, side): 18 | p = kv.p 19 | u = kv.kv[0] if side==0 else kv.kv[-1] 20 | # rows correspond to derivatives 21 | # columns correspond to basis functions 22 | derivs = bspline.active_deriv(kv, u, p-1) #shape: (p,p+1) 23 | 24 | # remove p+1st basis function since it's always in the nullspace 25 | if side==0: 26 | derivs = derivs[:, :p] 27 | else: 28 | derivs = derivs[:, 1:] 29 | 30 | # normalize with h^(deriv) 31 | h = kv.meshsize_avg() 32 | derivs = (np.repeat(h, p) ** range(p))[:,np.newaxis] * derivs 33 | 34 | n_tilde = (p + 1) // 2 35 | # use only odd derivatives 36 | evenderivs = range(0,p,2) 37 | assert n_tilde == len(evenderivs) 38 | derivs[evenderivs, :] = 0 39 | 40 | U, S, Vt = scipy.linalg.svd(derivs) 41 | # return nullspace and its orthogonal complement 42 | return (Vt.T[:, -n_tilde:], Vt.T[:, :-n_tilde]) 43 | 44 | def Stilde_basis(kv): 45 | """Compute a basis for S-tilde and one for its orthogonal complement""" 46 | p = kv.p 47 | (b_L, b_compl_L) = Stilde_basis_side(kv, 0) 48 | (b_R, b_compl_R) = Stilde_basis_side(kv, 1) 49 | 50 | n = kv.numdofs 51 | n_L = b_L.shape[1] 52 | n_R = b_R.shape[1] 53 | n_I = n - 2*p 54 | n_c_L = b_compl_L.shape[1] 55 | n_c_R = b_compl_R.shape[1] 56 | 57 | P_tilde = np.zeros((n, n_L + n_I + n_R)) 58 | P_compl = np.zeros((n, n_c_L + n_c_R)) 59 | 60 | P_tilde[:p,:n_L] = b_L 61 | P_tilde[p:-p, n_L:-n_R] = np.eye(n_I) 62 | P_tilde[-p:,-n_R:] = b_R 63 | 64 | P_compl[:p, :n_c_L] = b_compl_L 65 | P_compl[-p:, -n_c_R:] = b_compl_R 66 | 67 | return (P_tilde, P_compl) 68 | 69 | -------------------------------------------------------------------------------- /pyiga/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse 3 | import scipy.sparse.linalg 4 | import itertools 5 | import functools 6 | import operator 7 | 8 | def _broadcast_to_grid(X, grid_shape): 9 | num_dims = len(grid_shape) 10 | # input might be a single scalar; make sure it's an array 11 | X = np.asanyarray(X) 12 | # if the field X is not scalar, we need include the extra dimensions 13 | target_shape = grid_shape + X.shape[num_dims:] 14 | if X.shape != target_shape: 15 | X = np.broadcast_to(X, target_shape) 16 | return X 17 | 18 | def _ensure_grid_shape(values, grid): 19 | """Convert tuples of arrays into higher-dimensional arrays and make sure 20 | the array conforms to the grid size.""" 21 | grid_shape = tuple(len(g) for g in grid) 22 | 23 | # if values is a tuple, interpret as vector-valued function 24 | if isinstance(values, tuple): 25 | values = np.stack(tuple(_broadcast_to_grid(v, grid_shape) for v in values), 26 | axis=-1) 27 | 28 | # If values came from a function which uses only some of its arguments, 29 | # values may have the wrong shape. In that case, broadcast it to the full 30 | # mesh extent. 31 | return _broadcast_to_grid(values, grid_shape) 32 | 33 | def grid_eval(f, grid): 34 | """Evaluate function `f` over the tensor grid `grid`.""" 35 | if hasattr(f, 'grid_eval'): 36 | return f.grid_eval(grid) 37 | else: 38 | mesh = list(np.meshgrid(*grid, sparse=True, indexing='ij')) 39 | mesh.reverse() # convert order ZYX into XYZ 40 | values = f(*mesh) 41 | return _ensure_grid_shape(values, grid) 42 | 43 | def grid_eval_transformed(f, grid, geo): 44 | """Transform the tensor grid `grid` by the geometry transform `geo` and 45 | evaluate `f` on the resulting grid. 46 | """ 47 | trf_grid = grid_eval(geo, grid) # array of size shape(grid) x dim 48 | # extract coordinate components 49 | X = tuple(trf_grid[..., i] for i in range(trf_grid.shape[-1])) 50 | # evaluate the function 51 | vals = f(*X) 52 | return _ensure_grid_shape(vals, grid) 53 | 54 | def read_sparse_matrix(fname): 55 | I,J,vals = np.loadtxt(fname, skiprows=1, unpack=True) 56 | I = I.astype(int) 57 | J = J.astype(int) 58 | I -= 1 59 | J -= 1 60 | return scipy.sparse.coo_matrix((vals, (I,J))).tocsr() 61 | 62 | def multi_kron_sparse(As, format='csr'): 63 | """Compute the (sparse) Kronecker product of a sequence of sparse matrices.""" 64 | if len(As) == 1: 65 | return As[0].asformat(format, copy=True) 66 | else: 67 | return scipy.sparse.kron(As[0], multi_kron_sparse(As[1:], format=format), format=format) 68 | 69 | def kron_partial(As, rows, restrict=False, format='csr'): 70 | """Compute a partial Kronecker product between the sparse matrices 71 | `As = (A_1, ..., A_k)`, filling only the given `rows` in the output matrix. 72 | 73 | If `restrict=True`, a smaller matrix is returned which contains only the 74 | given rows of the original matrix. Otherwise, a matrix with the same shape 75 | as the full Kronecker product is returned. 76 | """ 77 | from .mlmatrix import MLStructure 78 | # determine the I,J indices of the nonzeros in the given rows 79 | S = MLStructure.from_kronecker(As) 80 | out_shape = (len(rows), S.shape[1]) if restrict else S.shape 81 | 82 | if restrict: 83 | I, J, I_idx = S.nonzeros_for_rows(rows, renumber_rows=True) 84 | else: 85 | I, J = S.nonzeros_for_rows(rows) 86 | if len(I) == 0: # no nonzeros? return zero matrix 87 | return scipy.sparse.csr_matrix(out_shape) 88 | 89 | # block sizes for unraveling the row and column indices 90 | bs_I = tuple(S.bs[k][0] for k in range(S.L)) 91 | bs_J = tuple(S.bs[k][1] for k in range(S.L)) 92 | # unravel the indices to refer to the individual blocks A_k 93 | I_ix = np.unravel_index(I, bs_I) 94 | J_ix = np.unravel_index(J, bs_J) 95 | # compute the values of the individual factor matrices 96 | values = tuple(As[k][I_ix[k], J_ix[k]].A1 for k in range(S.L)) 97 | # compute the Kronecker product as the product of the factors 98 | entries = functools.reduce(operator.mul, values) 99 | if restrict: 100 | I = I_idx 101 | return scipy.sparse.coo_matrix((entries, (I,J)), shape=out_shape).asformat(format) 102 | 103 | def cartesian_product(arrays): 104 | """Compute the Cartesian product of any number of input arrays.""" 105 | L = len(arrays) 106 | shp = tuple(a.shape[0] for a in arrays) 107 | arr = np.empty(shp + (L,), dtype=arrays[0].dtype) 108 | for i in range(L): 109 | # broadcast the i-th array along all but the i-th axis 110 | ix = L * [np.newaxis] 111 | ix[i] = slice(shp[i]) 112 | arr[..., i] = arrays[i][tuple(ix)] 113 | arr.shape = (-1, L) 114 | return arr 115 | 116 | class CSRRowSlice: 117 | """A simple class which allows quickly applying a row slice of a CSR matrix 118 | to dense matrices. 119 | 120 | This is required since creating submatrices of scipy sparse matrices is a 121 | very heavyweight operation. 122 | """ 123 | def __init__(self, A, row_bounds): 124 | assert isinstance(A, scipy.sparse.csr_matrix) 125 | self.A = A 126 | assert 0 <= row_bounds[0] <= row_bounds[1] <= A.shape[0], 'invalid row bounds' 127 | self.shape = (row_bounds[1] - row_bounds[0], A.shape[1]) 128 | self.indptr = self.A.indptr[row_bounds[0]:row_bounds[1]] 129 | self.dtype = A.dtype 130 | 131 | def _matmat(self, other): 132 | # adapted from _mul_multivector in scipy's compressed.py 133 | M, N = self.shape 134 | 135 | y = other if other.ndim == 2 else other.reshape((-1, 1)) 136 | n_vecs = y.shape[1] # number of column vectors 137 | result = np.zeros((M, n_vecs), dtype=self.dtype) 138 | 139 | scipy.sparse._sparsetools.csr_matvecs(M, N, n_vecs, 140 | self.indptr, self.A.indices, self.A.data, 141 | y.ravel(), result.ravel()) 142 | 143 | if other.ndim == 1: 144 | result.shape = (result.shape[0],) 145 | return result 146 | 147 | __mul__ = _matmat 148 | dot = _matmat 149 | 150 | class CSRRowSubset: 151 | """A simple class which allows quickly applying a subset of the rows of a 152 | CSR matrix to a vector. 153 | 154 | This is required since creating submatrices of scipy sparse matrices is a 155 | very heavyweight operation. 156 | """ 157 | def __init__(self, A, rows): 158 | assert isinstance(A, scipy.sparse.csr_matrix) 159 | self.A = A 160 | self.rows = rows 161 | self.shape = (len(rows), A.shape[1]) 162 | self.dtype = A.dtype 163 | 164 | def _matvec(self, other): 165 | M, N = self.shape 166 | assert other.shape == (N,) 167 | result = np.zeros(M, dtype=self.dtype) 168 | 169 | indptr = self.A.indptr 170 | y = other.ravel() 171 | out = result.ravel() 172 | for i, r in enumerate(self.rows): 173 | scipy.sparse._sparsetools.csr_matvec(1, N, 174 | indptr[r:r+1], self.A.indices, self.A.data, 175 | y, out[i:i+1]) 176 | return result 177 | 178 | __mul__ = _matvec 179 | dot = _matvec 180 | 181 | 182 | class LazyArray: 183 | """An interface for lazily evaluating functions over a tensor product grid 184 | with array slicing notation. 185 | """ 186 | def __init__(self, f, grid, mode='eval'): 187 | self.f = f 188 | self.grid = grid 189 | self.mode = mode 190 | 191 | def __getitem__(self, I): 192 | assert len(I) == len(self.grid), "Wrong number of indices" 193 | localgrid = tuple(g[i] for (g,i) in zip(self.grid, I)) 194 | if self.mode == 'eval': 195 | return grid_eval(self.f, localgrid) 196 | elif self.mode == 'jac': 197 | return self.f.grid_jacobian(localgrid) 198 | else: 199 | raise ValueError('invalid mode: ' + str(self.mode)) 200 | 201 | class LazyCachingArray: 202 | """An interface for lazily evaluating functions over a tensor product grid 203 | with array slicing notation. Already computed values are cached tile-wise. 204 | 205 | .. warning:: 206 | 207 | Only works correctly if the output is requested in full consecutive tiles! 208 | """ 209 | def __init__(self, f, outshape, grid, tilesize, mode='eval'): 210 | self.f = f 211 | self.outshape = outshape 212 | self.grid = grid 213 | self.mode = mode 214 | self.ts = tilesize 215 | self.tiles = {} 216 | 217 | def get_tile(self, I): 218 | """I is a tile index as a d-tuple.""" 219 | T = self.tiles.get(I) 220 | if T is None: 221 | ts = self.ts 222 | localgrid = tuple(g[i*ts:(i+1)*ts] for (g,i) in zip(self.grid, I)) 223 | if self.mode == 'eval': 224 | T = grid_eval(self.f, localgrid) 225 | elif self.mode == 'jac': 226 | T = self.f.grid_jacobian(localgrid) 227 | else: 228 | raise ValueError('invalid mode: ' + str(self.mode)) 229 | self.tiles[I] = T 230 | return T 231 | 232 | def __getitem__(self, I): 233 | assert len(I) == len(self.grid), "Wrong number of indices" 234 | idx = tuple(tuple(range(sl.start, sl.stop)) for sl in I) 235 | N = tuple(len(gi) for gi in idx) # size of output 236 | output = np.empty(N + self.outshape) 237 | ts = self.ts 238 | tiles = tuple(range(gi[0]//ts, (gi[-1] + ts - 1) // ts) for gi in idx) 239 | J0 = tuple(gi[0] // ts for gi in idx) # index of first tile 240 | for J in itertools.product(*tiles): 241 | dest = tuple(slice((j-j0)*ts, (j-j0+1)*ts) for (j,j0) in zip(J,J0)) 242 | output[dest] = self.get_tile(J) 243 | return output 244 | 245 | class BijectiveIndex: 246 | """Maps a list of values to consecutive indices in the range `0, ..., len(values) - 1` 247 | and allows reverse lookup of the index. 248 | """ 249 | def __init__(self, values): 250 | self.values = values 251 | self._index = dict() 252 | for (i, v) in enumerate(self.values): 253 | self._index[v] = i 254 | 255 | def __len__(self): 256 | return len(self.values) 257 | 258 | def __getitem__(self, i): 259 | return self.values[i] 260 | 261 | def index(self, v): 262 | return self._index[v] 263 | 264 | 265 | def _noop(self, *args, **kwargs): pass 266 | class _DummyPbar: 267 | """No-op stand-in for tqdm.""" 268 | def __init__(self, *args, **kwags): 269 | if len(args) > 0: 270 | self.r = args[0] 271 | def __iter__(self): 272 | return iter(self.r) 273 | def __enter__(self): 274 | return self 275 | __exit__ = _noop 276 | update = _noop 277 | close = _noop 278 | set_postfix = _noop 279 | 280 | def progress_bar(enable=True): 281 | if enable: 282 | import sys 283 | if 'tqdm' not in sys.modules: 284 | # tqdm shows an ugly warning when we slightly overstep the end time in a 285 | # time integration routine. To avoid that we disable its warnings. 286 | import tqdm 287 | import warnings 288 | warnings.simplefilter('ignore', tqdm.TqdmWarning) 289 | else: 290 | import tqdm 291 | return tqdm.tqdm 292 | else: 293 | return _DummyPbar 294 | -------------------------------------------------------------------------------- /pyiga/vis.py: -------------------------------------------------------------------------------- 1 | """Visualization functions.""" 2 | import numpy as np 3 | import matplotlib 4 | import matplotlib.pyplot as plt 5 | from matplotlib import animation 6 | 7 | from . import utils 8 | 9 | def plot_field(field, geo=None, res=80, physical=False, **kwargs): 10 | """Plot a scalar field, optionally over a geometry.""" 11 | kwargs.setdefault('shading', 'gouraud') 12 | if np.isscalar(res): 13 | res = (res, res) 14 | if geo is not None: 15 | grd = tuple(np.linspace(s[0], s[1], r) for (s,r) in zip(geo.support, res)) 16 | XY = utils.grid_eval(geo, grd) 17 | if physical: 18 | C = utils.grid_eval_transformed(field, grd, geo) 19 | else: 20 | C = utils.grid_eval(field, grd) 21 | return plt.pcolormesh(XY[...,0], XY[...,1], C, **kwargs) 22 | else: 23 | # assumes that `field` is a BSplineFunc or equivalent 24 | grd = tuple(np.linspace(s[0], s[1], r) for (s,r) in zip(field.support, res)) 25 | C = utils.grid_eval(field, grd) 26 | return plt.pcolormesh(grd[1], grd[0], C, **kwargs) 27 | 28 | 29 | def plot_geo(geo, 30 | grid=10, gridx=None, gridy=None, 31 | res=50, 32 | linewidth=None, color='black'): 33 | """Plot a wireframe representation of a 2D geometry.""" 34 | if geo.sdim == 1 and geo.dim == 2: 35 | return plot_curve(geo, res=res, linewidth=linewidth, color=color) 36 | assert geo.dim == geo.sdim == 2, 'Can only plot 2D geometries' 37 | if gridx is None: gridx = grid 38 | if gridy is None: gridy = grid 39 | supp = geo.support 40 | 41 | # if gridx/gridy is not an array, build an array with given number of ticks 42 | if np.isscalar(gridx): 43 | gridx = np.linspace(supp[0][0], supp[0][1], gridx) 44 | if np.isscalar(gridy): 45 | gridy = np.linspace(supp[1][0], supp[1][1], gridy) 46 | 47 | meshx = np.linspace(supp[0][0], supp[0][1], res) 48 | meshy = np.linspace(supp[1][0], supp[1][1], res) 49 | 50 | def plotline(pts, capstyle='butt'): 51 | plt.plot(pts[:,0], pts[:,1], color=color, linewidth=linewidth, 52 | solid_joinstyle='round', solid_capstyle=capstyle) 53 | 54 | pts = utils.grid_eval(geo, (gridx, meshy)) 55 | plotline(pts[0,:,:], capstyle='round') 56 | for i in range(1, pts.shape[0]-1): 57 | plotline(pts[i,:,:]) 58 | plotline(pts[-1,:,:], capstyle='round') 59 | 60 | pts = utils.grid_eval(geo, (meshx, gridy)) 61 | plotline(pts[:,0,:], capstyle='round') 62 | for j in range(1, pts.shape[1]-1): 63 | plotline(pts[:,j,:]) 64 | plotline(pts[:,-1,:], capstyle='round') 65 | 66 | 67 | def plot_curve(geo, res=50, linewidth=None, color='black'): 68 | """Plot a 2D curve.""" 69 | assert geo.dim == 2 and geo.sdim == 1, 'Can only plot 2D curves' 70 | supp = geo.support 71 | mesh = np.linspace(supp[0][0], supp[0][1], res) 72 | pts = utils.grid_eval(geo, (mesh,)) 73 | plt.plot(pts[:,0], pts[:,1], color=color, linewidth=linewidth) 74 | 75 | 76 | def animate_field(fields, geo, vrange=None, res=(50,50), cmap=None, interval=50, progress=False): 77 | """Animate a sequence of scalar fields over a geometry.""" 78 | fields = list(fields) 79 | fig, ax = plt.subplots() 80 | ax.set_aspect('equal') 81 | 82 | if np.isscalar(res): 83 | res = (res, res) 84 | grd = tuple(np.linspace(s[0], s[1], r) for (s,r) in zip(geo.support, res)) 85 | XY = geo.grid_eval(grd) 86 | C = np.zeros(res) 87 | 88 | if vrange is None: 89 | # determine range of values from first field 90 | C = utils.grid_eval(fields[0], grd) 91 | vrange = (C.min(), C.max()) 92 | 93 | quadmesh = plt.pcolormesh(XY[...,0], XY[...,1], C, shading='gouraud', cmap=cmap, 94 | vmin=vrange[0], vmax=vrange[1], axes=ax) 95 | fig.colorbar(quadmesh, ax=ax) 96 | 97 | tqdm = utils.progress_bar(progress) 98 | pbar = tqdm(total=len(fields)) 99 | def anim_func(i): 100 | C = utils.grid_eval(fields[i], grd) 101 | quadmesh.set_array(C.ravel()) 102 | pbar.update() 103 | if i == len(fields) - 1: 104 | pbar.close() 105 | 106 | return animation.FuncAnimation(fig, anim_func, frames=len(fields), interval=interval) 107 | 108 | class HSpaceVis: 109 | def __init__(self, hspace): 110 | assert hspace.dim == 2, 'Only 2D visualization implemented' 111 | self.hspace = hspace 112 | 113 | @staticmethod 114 | def vis_rect(r): 115 | Y, X = r # note: last axis = x 116 | return matplotlib.patches.Rectangle((X[0], Y[0]), X[1]-X[0], Y[1]-Y[0]) 117 | 118 | def cell_to_rect(self, lv, c): 119 | return self.vis_rect(self.hspace.cell_extents(lv, c)) 120 | 121 | def setup_axes(self): 122 | ax = plt.gca() 123 | ax.set_aspect('equal') 124 | ax.set_xticks([]) 125 | ax.set_yticks([]) 126 | return ax 127 | 128 | def plot_level(self, lv, color_act='steelblue', color_deact='lavender'): 129 | ax = self.setup_axes() 130 | 131 | from matplotlib.collections import PatchCollection 132 | if color_act is not None: 133 | Ra = [self.cell_to_rect(lv, c) for c in self.hspace.active_cells(lv)] 134 | ax.add_collection(PatchCollection(Ra, facecolor=color_act, edgecolor='black')) 135 | if color_deact is not None: 136 | Rd = [self.cell_to_rect(lv, c) for c in self.hspace.deactivated_cells(lv)] 137 | ax.add_collection(PatchCollection(Rd, facecolor=color_deact, edgecolor='black')); 138 | 139 | def plot_level_cells(self, cells, lv, color_act='steelblue', color_deact='white'): 140 | ax = self.setup_axes() 141 | 142 | from matplotlib.collections import PatchCollection 143 | if color_act is not None: 144 | Ra = [self.cell_to_rect(lv, c) for c in self.hspace.active_cells(lv) if c in cells] 145 | ax.add_collection(PatchCollection(Ra, facecolor=color_act, edgecolor='black')) 146 | if color_deact is not None: 147 | Rd = [self.cell_to_rect(lv, c) for c in self.hspace.active_cells(lv) if c not in cells] 148 | ax.add_collection(PatchCollection(Rd, facecolor=color_deact, edgecolor='black')) 149 | 150 | def plot_active_cells(self, values, cmap=None, edgecolor=None): 151 | ax = self.setup_axes() 152 | 153 | from matplotlib.collections import PatchCollection 154 | act_cells = self.hspace.active_cells(flat=True) 155 | if not len(values) == len(act_cells): 156 | raise ValueError('invalid length of `values` array') 157 | R = [self.cell_to_rect(lv, c) for (lv, c) in act_cells] 158 | p = PatchCollection(R, cmap=cmap, edgecolor=edgecolor) 159 | p.set_array(values) 160 | ax.add_collection(p) 161 | return ax, p 162 | 163 | def vis_function(self, lv, jj): 164 | r = self.vis_rect(self.hspace.function_support(lv, jj)) 165 | r.set_fill(False) 166 | r.set_edgecolor('red') 167 | r.set_linewidth(3) 168 | return r 169 | 170 | def plot_hierarchical_mesh(hspace, levels='all', levelwise=False, color_act='steelblue', color_deact='lavender'): 171 | """Visualize the mesh of a 2D hierarchical spline space. 172 | 173 | Args: 174 | hspace (:class:`.HSpace`): the space to be plotted 175 | levels: either 'all' or a list of levels to plot 176 | levelwise (bool): if True, show each level (including active and deactivated 177 | basis functions) in a separate subplot 178 | color_act: the color to use for the active cells 179 | color_deact: the color to use for the deactivated cells (only shown if `levelwise` is True) 180 | """ 181 | V = HSpaceVis(hspace) 182 | if levels == 'all': 183 | levels = tuple(range(hspace.numlevels)) 184 | else: 185 | levels = tuple(levels) 186 | 187 | for j,lv in enumerate(levels): 188 | if levelwise: 189 | plt.subplot(1, len(levels), j+1) 190 | V.plot_level(lv, color_act=color_act, color_deact=color_deact if levelwise else None) 191 | 192 | def plot_hierarchical_cells(hspace, cells, color_act='steelblue', color_deact='white'): 193 | """Visualize cells of a 2D hierarchical spline space. 194 | 195 | Args: 196 | hspace (:class:`.HSpace`): the space to be plotted 197 | cells: dict of sets of selected active cells 198 | color_act: the color to use for the selected cells 199 | color_deact: the color to use for the remaining cells 200 | """ 201 | V = HSpaceVis(hspace) 202 | 203 | for lv in range(hspace.numlevels): 204 | V.plot_level_cells(cells.get(lv, {}), lv, color_act=color_act, color_deact=color_deact) 205 | 206 | def plot_active_cells(hspace, values, cmap=None, edgecolor=None): 207 | """Plot the mesh of active cells with colors chosen according to the given 208 | `values`.""" 209 | return HSpaceVis(hspace).plot_active_cells(values, cmap=cmap) 210 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | "numpy>=1.11", 5 | "Cython", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | 10 | -------------------------------------------------------------------------------- /run-notebooks.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from glob import glob 4 | import subprocess 5 | import tempfile 6 | 7 | import nbformat 8 | 9 | def run_notebook(path, timeout=60): 10 | """Execute a notebook via nbconvert and collect output. 11 | :returns (parsed nb object, execution errors) 12 | """ 13 | with tempfile.NamedTemporaryFile(suffix=".ipynb", mode='w+') as fout: 14 | args = ["jupyter", "nbconvert", "--to", "notebook", "--execute", 15 | "--ExecutePreprocessor.timeout=%d" % timeout, 16 | "--output", fout.name, path] 17 | subprocess.check_call(args) 18 | 19 | fout.seek(0) 20 | nb = nbformat.read(fout, nbformat.current_nbformat) 21 | 22 | errors = [output for cell in nb.cells if "outputs" in cell 23 | for output in cell["outputs"]\ 24 | if output.output_type == "error"] 25 | 26 | return nb, errors 27 | 28 | if __name__ == '__main__': 29 | # move to directory of this script 30 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 31 | nbfiles = glob('notebooks/*ipynb') 32 | 33 | count = count_err = 0 34 | 35 | for nbfile in nbfiles: 36 | print('========== Running', nbfile, '==========') 37 | nb, errors = run_notebook(nbfile, timeout=120) 38 | count += 1 39 | if errors: 40 | count_err += 1 41 | print('Errors', errors) 42 | else: 43 | print('No errors.') 44 | print('=====================================') 45 | print('Ran %d notebooks, %d had errors.' % (count, count_err)) 46 | sys.exit(1 if count_err else 0) 47 | -------------------------------------------------------------------------------- /scripts/asm-codegen.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | from sympy.tensor import * 3 | from sympy.tensor.array import Array 4 | 5 | use_cse = False 6 | 7 | B, J = symbols('B J', cls=IndexedBase) 8 | i0, i1, i2 = symbols('i0 i1 i2', cls=Idx) 9 | 10 | def make_vararr(name, dim): 11 | v = [[Symbol(base + name + str(i)) for i in range(dim)] for base in ['v', 'd']] 12 | return Array(v) 13 | 14 | def make_vararr2(name, indices): 15 | dim = len(indices) 16 | return Array([ 17 | [symbols('%s%s%s' % (deriv, name, i), cls=IndexedBase)[indices[i]] for i in range(dim)] for deriv in ('V', 'D') 18 | ]) 19 | 20 | def make_vararr3(name, indices): 21 | dim = len(indices) 22 | return Array([ 23 | [symbols('VD%s%s' % (name, i), cls=IndexedBase)[2*indices[i]+deriv] for i in range(dim)] for deriv in (0, 1) 24 | ]) 25 | 26 | def grad(U): 27 | d = U.shape[1] 28 | components = [] 29 | for j in range(d): 30 | terms = [U[1 if (d-i == j+1) else 0, i] for i in range(d)] 31 | components.append(Mul(*terms)) 32 | return Array(components) 33 | 34 | dim = 3 35 | 36 | I = (i0, i1, i2)[:dim] 37 | 38 | U = make_vararr3('u', I) 39 | V = make_vararr3('v', I) 40 | 41 | if dim == 2: 42 | BI = Matrix([[B[i0,i1, i,j] for j in range(dim)] for i in range(dim)]) 43 | elif dim == 3: 44 | BI = Matrix([[B[i0,i1,i2, i,j] for j in range(dim)] for i in range(dim)]) 45 | 46 | gradu = grad(U) 47 | gradv = grad(V) 48 | 49 | Bgu = Matrix(BI.dot(gradu)) 50 | #Btgu = Matrix(BI.T.dot(gradu)) 51 | #Btgv = Matrix(BI.T.dot(gradv)) 52 | 53 | result = Bgu.T.dot(gradv) 54 | 55 | if use_cse: 56 | def is_atomic(pair): 57 | return len(pair[1].args) <= 1 58 | 59 | def fix_subs(pair): 60 | x, y = pair 61 | if y.func == IndexedBase: 62 | x = IndexedBase(x.name) 63 | return x, y 64 | 65 | subs, expr = cse(result) 66 | 67 | atm = [fix_subs(pair) for pair in subs if is_atomic(pair)] 68 | nonatm = [pair for pair in subs if not is_atomic(pair)] 69 | 70 | for (x,y) in nonatm: 71 | print('%s = %s' % (x, y.subs(atm))) 72 | 73 | print(expr[0].subs(atm)) 74 | else: 75 | print(result) 76 | #print(factor(result)) 77 | 78 | -------------------------------------------------------------------------------- /scripts/clear-cache.py: -------------------------------------------------------------------------------- 1 | import platformdirs 2 | import os 3 | import shutil 4 | 5 | if __name__ == '__main__': 6 | MODDIR = os.path.join(platformdirs.user_cache_dir('pyiga'), 'modules') 7 | print('Removing everything under', MODDIR) 8 | shutil.rmtree(MODDIR) 9 | -------------------------------------------------------------------------------- /scripts/download_appveyor.py: -------------------------------------------------------------------------------- 1 | # Source: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py 2 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 3 | # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 4 | 5 | """Use the Appveyor API to download Windows artifacts.""" 6 | 7 | import os 8 | import os.path 9 | import sys 10 | import zipfile 11 | 12 | import requests 13 | 14 | 15 | def make_auth_headers(): 16 | """Make the authentication headers needed to use the Appveyor API.""" 17 | return {} 18 | #with open("ci/appveyor.token") as f: 19 | # token = f.read().strip() 20 | 21 | #headers = { 22 | # 'Authorization': 'Bearer {0}'.format(token), 23 | #} 24 | #return headers 25 | 26 | 27 | def make_url(url, **kwargs): 28 | """Build an Appveyor API url.""" 29 | return "https://ci.appveyor.com/api" + url.format(**kwargs) 30 | 31 | 32 | def get_project_build(account_project): 33 | """Get the details of the latest Appveyor build.""" 34 | url = make_url("/projects/{account_project}", account_project=account_project) 35 | response = requests.get(url, headers=make_auth_headers()) 36 | return response.json() 37 | 38 | 39 | def download_latest_artifacts(account_project): 40 | """Download all the artifacts from the latest build.""" 41 | build = get_project_build(account_project) 42 | jobs = build['build']['jobs'] 43 | print("Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) 44 | for job in jobs: 45 | name = job['name'].partition(':')[2].split(',')[0].strip() 46 | print(" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) 47 | 48 | url = make_url("/buildjobs/{jobid}/artifacts", jobid=job['jobId']) 49 | response = requests.get(url, headers=make_auth_headers()) 50 | artifacts = response.json() 51 | 52 | for artifact in artifacts: 53 | is_zip = artifact['type'] == "Zip" 54 | filename = artifact['fileName'] 55 | print(" {0}, {1} bytes".format(filename, artifact['size'])) 56 | 57 | url = make_url( 58 | "/buildjobs/{jobid}/artifacts/{filename}", 59 | jobid=job['jobId'], 60 | filename=filename 61 | ) 62 | download_url(url, filename, make_auth_headers()) 63 | 64 | if is_zip: 65 | unpack_zipfile(filename) 66 | os.remove(filename) 67 | 68 | 69 | def ensure_dirs(filename): 70 | """Make sure the directories exist for `filename`.""" 71 | dirname, _ = os.path.split(filename) 72 | if dirname and not os.path.exists(dirname): 73 | os.makedirs(dirname) 74 | 75 | 76 | def download_url(url, filename, headers): 77 | """Download a file from `url` to `filename`.""" 78 | ensure_dirs(filename) 79 | response = requests.get(url, headers=headers, stream=True) 80 | if response.status_code == 200: 81 | with open(filename, 'wb') as f: 82 | for chunk in response.iter_content(16*1024): 83 | f.write(chunk) 84 | 85 | 86 | def unpack_zipfile(filename): 87 | """Unpack a zipfile, using the names in the zip.""" 88 | with open(filename, 'rb') as fzip: 89 | z = zipfile.ZipFile(fzip) 90 | for name in z.namelist(): 91 | print(" extracting {0}".format(name)) 92 | ensure_dirs(name) 93 | z.extract(name) 94 | 95 | 96 | if __name__ == "__main__": 97 | download_latest_artifacts(sys.argv[1]) 98 | -------------------------------------------------------------------------------- /scripts/generate-assemblers.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from sys import argv 3 | from pyiga import vform 4 | from pyiga.codegen import cython as backend 5 | 6 | def generate(dim): 7 | code = backend.CodeGen() 8 | 9 | def gen(vf, classname): 10 | backend.AsmGenerator(vf, classname, code).generate() 11 | 12 | nD = str(dim) + 'D' 13 | gen(vform.mass_vf(dim), 'MassAssembler'+nD) 14 | gen(vform.stiffness_vf(dim), 'StiffnessAssembler'+nD) 15 | gen(vform.heat_st_vf(dim), 'HeatAssembler_ST'+nD) 16 | gen(vform.wave_st_vf(dim), 'WaveAssembler_ST'+nD) 17 | gen(vform.divdiv_vf(dim), 'DivDivAssembler'+nD) 18 | gen(vform.L2functional_vf(dim), 'L2FunctionalAssembler'+nD) 19 | gen(vform.L2functional_vf(dim, physical=True), 'L2FunctionalAssemblerPhys'+nD) 20 | 21 | return code.result() 22 | 23 | if __name__ == '__main__': 24 | if '--generic' in argv[1:]: 25 | path = os.path.join(os.path.dirname(__file__), "..", "pyiga", "genericasm.pxi") 26 | with open(path, 'w') as f: 27 | f.write('# file generated by generate-assemblers.py\n') 28 | f.write(backend.generate_generic(dim=1)) 29 | f.write(backend.generate_generic(dim=2)) 30 | f.write(backend.generate_generic(dim=3)) 31 | 32 | if not '--generic-only' in argv[1:]: 33 | path = os.path.join(os.path.dirname(__file__), "..", "pyiga", "assemblers.pyx") 34 | with open(path, 'w') as f: 35 | f.write(backend.preamble()) 36 | f.write(generate(dim=2)) 37 | f.write(generate(dim=3)) 38 | 39 | -------------------------------------------------------------------------------- /scripts/setversion.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def replace_line(line, marker, newstr): 4 | if line.lstrip().startswith(marker): 5 | return newstr 6 | else: 7 | return line 8 | 9 | def replace_in_file(fname, marker, newstr): 10 | with open(fname) as f: 11 | lines = list(f) 12 | lines = [replace_line(l, marker, newstr) for l in lines] 13 | with open(fname, 'w') as f: 14 | f.write(''.join(lines)) 15 | 16 | if __name__ == '__main__': 17 | version = sys.argv[1] 18 | replace_in_file('setup.py', 'version =', " version = '%s',\n" % version) 19 | replace_in_file('pyiga/__init__.py', '__version__ =', "__version__ = '%s'\n" % version) 20 | replace_in_file('docs/source/conf.py', 'version =', "version = '%s'\n" % version) 21 | -------------------------------------------------------------------------------- /scripts/str2asm.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import argparse 3 | from pyiga import vform 4 | from pyiga.codegen import cython as backend 5 | 6 | def parse_args(): 7 | parser = argparse.ArgumentParser(description='Convert a vform expression to code for an assembler class.') 8 | parser.add_argument('expr', type=str, help='the expression to be compiled') 9 | parser.add_argument('--dim', type=int, default=2, help='the space dimension') 10 | parser.add_argument('--name', type=str, default='CustomAsm', help='the name of the assembler class') 11 | parser.add_argument('-o', '--output', help='the file to write to; by default, write to stdout') 12 | parser.add_argument('--scalarinput', nargs='*', metavar='NAME', help='names of scalar input fields') 13 | parser.add_argument('--vectorinput', nargs='*', metavar='NAME', help='names of vector input fields') 14 | parser.add_argument('--ondemand', action='store_true', help='create an on demand assembler') 15 | parser.add_argument('--dumptree', action='store_true', help='write the expression tree to stdout') 16 | return parser.parse_args() 17 | 18 | if __name__ == '__main__': 19 | _args = parse_args() 20 | 21 | vf = vform.VForm(dim=_args.dim) 22 | u, v = vf.basisfuns() 23 | 24 | # create scalar input fields 25 | if _args.scalarinput: 26 | for funcname in _args.scalarinput: 27 | locals()[funcname] = vf.input(funcname) 28 | 29 | # create vector input fields 30 | if _args.vectorinput: 31 | for funcname in _args.vectorinput: 32 | locals()[funcname] = vf.input(funcname, shape=(_args.dim,)) 33 | 34 | e = eval(_args.expr, vars(vform), locals()) 35 | if _args.dumptree: 36 | vform.tree_print(e) 37 | vf.add(e) 38 | 39 | code = backend.CodeGen() 40 | backend.AsmGenerator(vf, _args.name, code, on_demand=_args.ondemand).generate() 41 | 42 | f = open(_args.output, 'w') if _args.output else None 43 | print(backend.preamble(), file=f) 44 | print(code.result(), file=f) 45 | if _args.output: 46 | f.close() 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.extension import Extension 3 | from Cython.Build import cythonize 4 | import numpy 5 | 6 | import Cython.Compiler.Options 7 | Cython.Compiler.Options.cimport_from_pyx = True 8 | 9 | USE_OPENMP = True 10 | 11 | c_args = ['-O3', '-march=native', '-ffast-math'] 12 | c_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] 13 | 14 | if USE_OPENMP: 15 | c_args_openmp = l_args_openmp = ['-fopenmp'] 16 | else: 17 | c_args_openmp = l_args_openmp = [] 18 | 19 | extensions = [ 20 | Extension("pyiga.bspline_cy", 21 | ["pyiga/bspline_cy.pyx"], 22 | include_dirs = [numpy.get_include()], 23 | extra_compile_args=c_args, 24 | define_macros=c_macros, 25 | ), 26 | Extension("pyiga.lowrank_cy", 27 | ["pyiga/lowrank_cy.pyx"], 28 | include_dirs = [numpy.get_include()], 29 | extra_compile_args=c_args, 30 | define_macros=c_macros, 31 | ), 32 | Extension("pyiga.mlmatrix_cy", 33 | ["pyiga/mlmatrix_cy.pyx"], 34 | include_dirs = [numpy.get_include()], 35 | extra_compile_args=c_args + c_args_openmp, 36 | extra_link_args=l_args_openmp, 37 | define_macros=c_macros, 38 | ), 39 | Extension("pyiga.assemble_tools_cy", 40 | ["pyiga/assemble_tools_cy.pyx"], 41 | include_dirs = [numpy.get_include()], 42 | extra_compile_args=c_args + c_args_openmp, 43 | extra_link_args=l_args_openmp, 44 | define_macros=c_macros, 45 | ), 46 | Extension("pyiga.assemblers", 47 | ["pyiga/assemblers.pyx"], 48 | include_dirs = [numpy.get_include()], 49 | extra_compile_args=c_args + c_args_openmp, 50 | extra_link_args=l_args_openmp, 51 | define_macros=c_macros, 52 | ), 53 | Extension("pyiga.fast_assemble_cy", 54 | ["pyiga/fastasm.cc", 55 | "pyiga/fast_assemble_cy.pyx"], 56 | include_dirs = [numpy.get_include()], 57 | language='c++', 58 | extra_compile_args=c_args, 59 | define_macros=c_macros, 60 | ), 61 | Extension("pyiga.relaxation_cy", 62 | ["pyiga/relaxation_cy.pyx"], 63 | extra_compile_args=c_args, 64 | define_macros=c_macros, 65 | ), 66 | ] 67 | 68 | 69 | setup( 70 | name = 'pyiga', 71 | version = '0.1.0', 72 | description = 'A Python research toolbox for Isogeometric Analysis', 73 | long_description = 'pyiga is a Python research toolbox for Isogeometric Analysis.\n\nPlease visit the project homepage on Github to learn more.', 74 | author = 'Clemens Hofreither', 75 | author_email = 'chofreither@ricam.oeaw.ac.at', 76 | url = 'https://github.com/c-f-h/pyiga', 77 | 78 | classifiers=[ 79 | 'Programming Language :: Python :: 3', 80 | 'Development Status :: 3 - Alpha', 81 | 'Intended Audience :: Science/Research', 82 | 'Topic :: Scientific/Engineering :: Mathematics', 83 | 'Topic :: Scientific/Engineering :: Physics', 84 | 'License :: Free For Educational Use', 85 | ], 86 | packages = ['pyiga', 'pyiga.codegen'], 87 | 88 | ext_modules = cythonize(extensions, compiler_directives={'language_level': 3, 'legacy_implicit_noexcept': True}), 89 | package_data = { 90 | 'pyiga': [ '*.pyx' , '*.pxd' , '*.pxi' ,], 91 | }, 92 | 93 | setup_requires = ['numpy', 'Cython'], 94 | install_requires = [ 95 | 'Cython', 96 | 'numpy>=1.11', 97 | 'scipy', 98 | 'platformdirs', 99 | 'networkx', 100 | 'jinja2', 101 | 'setuptools', 102 | ], 103 | ) 104 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/test/__init__.py -------------------------------------------------------------------------------- /test/poisson_neu_d2_p3_n15_mass.mtx.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/test/poisson_neu_d2_p3_n15_mass.mtx.gz -------------------------------------------------------------------------------- /test/poisson_neu_d2_p3_n15_stiff.mtx.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/test/poisson_neu_d2_p3_n15_stiff.mtx.gz -------------------------------------------------------------------------------- /test/poisson_neu_d3_p2_n10_mass.mtx.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/test/poisson_neu_d3_p2_n10_mass.mtx.gz -------------------------------------------------------------------------------- /test/poisson_neu_d3_p2_n10_stiff.mtx.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-f-h/pyiga/cbda254d121721a67bb8f6ba6f95c27355112348/test/poisson_neu_d3_p2_n10_stiff.mtx.gz -------------------------------------------------------------------------------- /test/test_approx.py: -------------------------------------------------------------------------------- 1 | from pyiga.approx import * 2 | from pyiga import bspline, geometry, utils 3 | import numpy as np 4 | 5 | def _test_approx(approx_fun, extra_dims): 6 | kvs = [bspline.make_knots(p, 0.0, 1.0, 8+p) for p in range(3,6)] 7 | N = [kv.numdofs for kv in kvs] 8 | coeffs = np.random.random_sample(N + extra_dims) 9 | func = geometry.BSplineFunc(kvs, coeffs) 10 | result = approx_fun(kvs, func) 11 | assert np.allclose(coeffs, result) 12 | 13 | # try also with direct function call 14 | def f(X, Y, Z): 15 | return func.grid_eval([np.squeeze(w) for w in (Z,Y,X)]) 16 | result = approx_fun(kvs, f) 17 | assert np.allclose(coeffs, result) 18 | 19 | 20 | def test_project_L2(): 21 | _test_approx(project_L2, []) # scalar-valued 22 | 23 | def test_project_L2_vector(): 24 | _test_approx(project_L2, [3]) # vector-valued 25 | 26 | def test_project_L2_matrix(): 27 | _test_approx(project_L2, [2,2]) # matrix-valued 28 | 29 | 30 | def test_project_L2_geo(): 31 | f = lambda x,y,z: np.cos(x)*np.sin(y)*np.exp(z) 32 | kvs = 3 * (bspline.make_knots(3, 0.0, 1.0, 10),) 33 | x1 = project_L2(kvs, f) 34 | x2 = project_L2(kvs, f, geo=geometry.unit_cube()) 35 | assert np.allclose(x1, x2) 36 | 37 | 38 | def test_interpolate(): 39 | _test_approx(interpolate, []) # scalar-valued 40 | 41 | def test_interpolate_vector(): 42 | _test_approx(interpolate, [3]) # vector-valued 43 | 44 | def test_interpolate_matrix(): 45 | _test_approx(interpolate, [2,2]) # matrix-valued 46 | 47 | 48 | def test_interpolate_physical(): 49 | f = lambda x,y,z: np.cos(x)*np.sin(y)*np.exp(z) 50 | kvs = 3 * (bspline.make_knots(3, 0.0, 1.0, 10),) 51 | x1 = interpolate(kvs, f) 52 | x2 = interpolate(kvs, f, geo=geometry.unit_cube()) 53 | assert np.allclose(x1, x2) 54 | 55 | def test_interpolate_array(): 56 | def f(x, y): return (x + y)**2 57 | kvs = 2 * (bspline.make_knots(2, 0.0, 1.0, 10),) 58 | nodes = tuple(kv.greville() for kv in kvs) 59 | fvals = utils.grid_eval(f, nodes) 60 | coeffs = interpolate(kvs, fvals, nodes=nodes) 61 | spl = bspline.BSplineFunc(kvs, coeffs) 62 | X = np.linspace(0.0, 1.0, 12) 63 | assert np.allclose(utils.grid_eval(f, (X, X)), spl.grid_eval((X, X))) 64 | 65 | def test_compare_intproj(): 66 | f = lambda x,y: np.cos(x)*np.exp(y) 67 | kvs = 2 * (bspline.make_knots(3, 0.0, 1.0, 50),) 68 | x1 = interpolate(kvs, f) 69 | x2 = project_L2(kvs, f) 70 | assert abs(x1-x2).max() < 1e-5 71 | 72 | geo = geometry.bspline_quarter_annulus() 73 | x1 = interpolate(kvs, f, geo=geo) 74 | x2 = project_L2(kvs, f, f_physical=True, geo=geo) 75 | assert abs(x1-x2).max() < 1e-5 76 | 77 | def test_exact_poly(): 78 | for p in range(1, 5): 79 | for mult in range(1, p+1): 80 | kv = bspline.make_knots(p, 0.0, 1.0, 5, mult=mult) 81 | f = lambda x: (x+1)**p 82 | u = project_L2(kv, f) 83 | x = np.linspace(0, 1, 25) 84 | assert np.allclose(f(x), bspline.ev(kv, u, x)) 85 | -------------------------------------------------------------------------------- /test/test_bspline.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyiga.bspline import * 4 | 5 | def test_eval(): 6 | # create random spline 7 | kv = make_knots(4, 0.0, 1.0, 25) 8 | n = kv.numdofs 9 | coeffs = np.random.rand(n) 10 | # evaluate it, B-spline by B-spline 11 | x = np.linspace(0.0, 1.0, 100) 12 | values = sum((coeffs[j] * single_ev(kv, j, x) for j in range(n))) 13 | # evaluate all at once and compare 14 | values2 = ev(kv, coeffs, x) 15 | assert np.linalg.norm(values - values2) < 1e-10 16 | assert np.allclose(values2[7], BSplineFunc(kv, coeffs)(x[7])) 17 | # evaluate through collocation matrix and compare 18 | values3 = collocation(kv, x).dot(coeffs) 19 | assert np.linalg.norm(values - values3) < 1e-10 20 | 21 | def test_greville(): 22 | kv = make_knots(3, 0.9, 1.0, 5) 23 | g = kv.greville() 24 | assert np.allclose(g, [0.9, 0.90666667, 0.92, 0.94, 0.96, 0.98, 0.99333333, 1.0]) 25 | assert all(g >= 0.9) and all(g <= 1.0) 26 | 27 | def test_interpolation(): 28 | kv = make_knots(3, 0.0, 1.0, 10) 29 | # create random spline 30 | coeffs = np.random.rand(kv.numdofs) 31 | def f(x): return ev(kv, coeffs, x) 32 | # interpolate it at Gréville points and check that result is the same 33 | result = interpolate(kv, f) 34 | assert np.allclose(coeffs, result) 35 | ## 36 | # test for p=0 37 | kv = make_knots(0, 0.0, 1.0, 10) 38 | coeffs = np.random.rand(kv.numdofs) 39 | def f(x): return ev(kv, coeffs, x) 40 | # interpolate it at Gréville points and check that result is the same 41 | result = interpolate(kv, f) 42 | assert np.allclose(coeffs, result) 43 | 44 | def test_eq(): 45 | # Generate example knot vectors 46 | kv_ref = make_knots(4, 0.0, 1.0, 25) 47 | kv1 = make_knots(4, 0.0, 1.0, 25) 48 | kv2 = make_knots(2, 0.0, 1.0, 25) 49 | kv3 = make_knots(4, 0.1, 1.0, 25) 50 | kv4 = make_knots(4, 0.0, 1.1, 25) 51 | kv5 = make_knots(4, 0.0, 1.0, 50) 52 | 53 | # Check equivalence 54 | assert kv_ref == kv1 55 | assert not kv_ref == kv2 56 | assert not kv_ref == kv3 57 | assert not kv_ref == kv4 58 | assert not kv_ref == kv5 59 | 60 | def test_L2_projection(): 61 | kv = make_knots(3, 0.0, 1.0, 10) 62 | def f(x): return np.sin(2*np.pi * x**2) 63 | x = np.linspace(0.0, 1.0, 100) 64 | coeffs = project_L2(kv, f) 65 | assert np.linalg.norm(f(x) - ev(kv, coeffs, x)) / np.sqrt(len(x)) < 1e-3 66 | 67 | def test_deriv(): 68 | # create linear spline 69 | kv = make_knots(4, 0.0, 1.0, 25) 70 | coeffs = interpolate(kv, lambda x: 1.0 + 2.5*x) 71 | # check that derivative is 2.5 72 | x = np.linspace(0.0, 1.0, 100) 73 | drv = deriv(kv, coeffs, 1, x) 74 | assert np.linalg.norm(drv - 2.5) < 1e-10 75 | 76 | # create random spline 77 | coeffs = np.random.rand(kv.numdofs) 78 | # compare derivatives by two methods 79 | derivs1 = deriv(kv, coeffs, 1, x) 80 | derivs2 = deriv(kv, coeffs, 2, x) 81 | allders = collocation_derivs(kv, x, derivs=2) 82 | assert np.linalg.norm(derivs1 - allders[1].dot(coeffs), np.inf) < 1e-10 83 | assert np.linalg.norm(derivs2 - allders[2].dot(coeffs), np.inf) < 1e-10 84 | 85 | def test_refine(): 86 | kv = make_knots(2, 0.0, 1.0, 4) 87 | kv2 = kv.refine([0.1]) 88 | assert kv2.p == kv.p and np.array_equal(kv2.kv, 89 | [0.0, 0.0, 0.0, 0.1, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0]) 90 | kv2 = kv.refine() 91 | assert kv2.p == kv.p and np.array_equal(kv2.kv, make_knots(2, 0.0, 1.0, 8).kv) 92 | 93 | def test_prolongation(): 94 | # create random spline 95 | kv = make_knots(3, 0.0, 1.0, 10) 96 | coeffs = np.random.rand(kv.numdofs) 97 | # compute a refined knot vector and prolongation matrix 98 | kv2 = kv.refine() 99 | P = prolongation(kv, kv2) 100 | coeffs2 = P.dot(coeffs) 101 | # check that they evaluate to the same function 102 | x = np.linspace(0.0, 1.0, 100) 103 | val1 = ev(kv, coeffs, x) 104 | val2 = ev(kv2, coeffs2, x) 105 | assert np.linalg.norm(val1 - val2) < 1e-10 106 | 107 | def test_knot_insertion(): 108 | kv = KnotVector(np.array([0.0, 0.0, 0.0, 0.0, 0.0, 109 | 0.05, 0.12, 0.33, 0.51, 0.51, 0.51, 0.74, 0.88, 0.91, 110 | 1.0, 1.0, 1.0, 1.0, 1.0]), 4) 111 | u = np.random.rand(kv.numdofs) 112 | x = np.linspace(0, 1, 100) 113 | for newknot in (0.01, 0.2, 0.33, 0.44, 0.6, 0.99): 114 | P = knot_insertion(kv, newknot) 115 | kv1 = kv.refine([newknot]) 116 | assert np.allclose(ev(kv, u, x), ev(kv1, P @ u, x)) 117 | 118 | def test_mesh_span_indices(): 119 | kv = make_knots(3, 0.0, 1.0, 4) 120 | assert np.array_equal(kv.mesh_span_indices(), [3, 4, 5, 6]) 121 | kv = make_knots(3, 0.0, 1.0, 4, mult=3) 122 | assert np.array_equal(kv.mesh_span_indices(), [3, 6, 9, 12]) 123 | 124 | def test_hessian(): 125 | from pyiga.approx import interpolate 126 | 127 | # 2D test 128 | kvs = 2 * (make_knots(3, 0.0, 1.0, 4),) 129 | grid = 2 * (np.linspace(0, 1, 7),) 130 | u = BSplineFunc(kvs, interpolate(kvs, lambda x,y: x**2 + 4*x*y + 3*y**2)) 131 | hess = u.grid_hessian(grid) 132 | assert np.allclose(hess, [2.0, 4.0, 6.0]) # (xx, xy, yy) 133 | 134 | # 3D test 135 | kvs = 3 * (make_knots(3, 0.0, 1.0, 4),) 136 | grid = 3 * (np.linspace(0, 1, 5),) 137 | u = BSplineFunc(kvs, interpolate(kvs, lambda x,y,z: x**2 + 3*x*z + 2*y*z)) 138 | hess = u.grid_hessian(grid) 139 | assert np.allclose(hess, [2.0, 0.0, 3.0, 0.0, 2.0, 0.0]) # (xx, xy, xz, yy, yz, zz) 140 | 141 | # 2D vector test 142 | kvs = 2 * (make_knots(3, 0.0, 1.0, 4),) 143 | grid = 2 * (np.linspace(0, 1, 7),) 144 | u = BSplineFunc(kvs, interpolate(kvs, lambda x,y: (x**2 + 4*x*y, 3*y**2))) 145 | hess = u.grid_hessian(grid) 146 | assert np.allclose(hess, [[2.0, 4.0, 0.0], [0.0, 0.0, 6.0]]) # (xx, xy, yy) 147 | 148 | # 3D vector test 149 | kvs = 3 * (make_knots(3, 0.0, 1.0, 4),) 150 | grid = 3 * (np.linspace(0, 1, 5),) 151 | u = BSplineFunc(kvs, interpolate(kvs, lambda x,y,z: (x**2, 3*x*z, 2*y*z))) 152 | hess = u.grid_hessian(grid) 153 | assert np.allclose(hess, # (xx, xy, xz, yy, yz, zz) 154 | [[2.0, 0.0, 0.0, 0.0, 0.0, 0.0], 155 | [0.0, 0.0, 3.0, 0.0, 0.0, 0.0], 156 | [0.0, 0.0, 0.0, 0.0, 2.0, 0.0]]) 157 | -------------------------------------------------------------------------------- /test/test_codegen.py: -------------------------------------------------------------------------------- 1 | from pyiga.codegen import cython as codegen 2 | from pyiga import vform 3 | import numpy as np 4 | 5 | def my_stiffness_vf(dim): 6 | # same as stiffness_vf(), but slower implementation 7 | from pyiga.vform import VForm, inner, grad, dx 8 | V = VForm(dim) 9 | u, v = V.basisfuns() 10 | V.add(inner(grad(u), grad(v)) * dx) 11 | return V 12 | 13 | def vector_laplace_vf(dim): 14 | from pyiga.vform import VForm, inner, grad, dx 15 | V = VForm(dim) 16 | u, v = V.basisfuns(components=(dim,dim)) 17 | V.add(inner(grad(u), grad(v)) * dx) 18 | return V 19 | 20 | def vector_L2functional_vf(dim): 21 | from pyiga.vform import VForm, inner, dx 22 | V = VForm(dim, arity=1) 23 | u = V.basisfuns(components=(dim,)) 24 | f = V.input('f', shape=(dim,)) 25 | V.add(inner(u, f) * dx) 26 | return V 27 | 28 | 29 | def test_codegen_poisson2d(): 30 | code = codegen.CodeGen() 31 | vf = vform.stiffness_vf(2) 32 | assert (not vf.vec) and vf.arity == 2 33 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 34 | code = codegen.preamble() + '\n' + code.result() 35 | 36 | def test_codegen_poisson3d(): 37 | code = codegen.CodeGen() 38 | vf = my_stiffness_vf(3) 39 | assert (not vf.vec) and vf.arity == 2 40 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 41 | code = codegen.preamble() + '\n' + code.result() 42 | 43 | def test_codegen_vectorlaplace2d(): 44 | code = codegen.CodeGen() 45 | vf = vector_laplace_vf(2) 46 | assert vf.vec == 2*2 and vf.arity == 2 47 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 48 | code = codegen.preamble() + '\n' + code.result() 49 | 50 | def test_codegen_functional(): 51 | code = codegen.CodeGen() 52 | vf = vform.L2functional_vf(3, updatable=True) 53 | assert (not vf.vec) and vf.arity == 1 54 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 55 | code = codegen.preamble() + '\n' + code.result() 56 | 57 | def test_codegen_vecfunctional(): 58 | code = codegen.CodeGen() 59 | vf = vector_L2functional_vf(3) 60 | assert vf.vec == 3 and vf.arity == 1 61 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 62 | code = codegen.preamble() + '\n' + code.result() 63 | 64 | def test_codegen_parameter(): 65 | from pyiga.vform import VForm, inner, grad, dx, norm 66 | code = codegen.CodeGen() 67 | dim = 2 68 | vf = VForm(dim, arity=1) 69 | u = vf.basisfuns() 70 | a = vf.parameter('a') 71 | b = vf.parameter('b', shape=(dim,)) 72 | vf.add(norm(a * b) * inner(grad(u), b / norm(a * b)) * dx) 73 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 74 | code = codegen.preamble() + '\n' + code.result() 75 | 76 | def test_codegen_wave_st2d(): 77 | code = codegen.CodeGen() 78 | vf = vform.wave_st_vf(2) 79 | assert (not vf.vec) and vf.arity == 2 and vf.spacetime 80 | codegen.AsmGenerator(vf, 'TestAsm', code).generate() 81 | code = codegen.preamble() + '\n' + code.result() 82 | -------------------------------------------------------------------------------- /test/test_kronecker.py: -------------------------------------------------------------------------------- 1 | from pyiga.kronecker import * 2 | from pyiga.tensor import apply_tprod 3 | from numpy.random import rand 4 | 5 | def _kron2d_test(X, Y, XY): 6 | n = X.shape[0] 7 | 8 | x = rand(n**2) 9 | y1 = apply_kronecker((X,Y), x) 10 | y2 = apply_tprod((X,Y), x.reshape(n,n)) 11 | assert abs(XY.dot(x) - y1).max() < 1e-10 12 | assert abs(XY.dot(x) - y2.ravel()).max() < 1e-10 13 | 14 | x = rand(n**2, 1) 15 | assert np.allclose(XY.dot(x), apply_kronecker((X,Y), x)) 16 | 17 | x = rand(n**2, 7) 18 | assert np.allclose(XY.dot(x), apply_kronecker((X,Y), x)) 19 | 20 | def test_kronecker_2d(): 21 | X = rand(8,8) 22 | Y = rand(8,8) 23 | XY = np.kron(X, Y) 24 | _kron2d_test(X, Y, XY) 25 | 26 | def test_kronecker_2d_sparse(): 27 | n = 50 28 | X = scipy.sparse.diags([rand(n-1), rand(n), rand(n-1)], offsets=(-1,0,1)) 29 | Y = scipy.sparse.diags([rand(n-1), rand(n), rand(n-1)], offsets=(-1,0,1)) 30 | XY = scipy.sparse.kron(X, Y) 31 | _kron2d_test(X, Y, XY) 32 | 33 | 34 | def _kron3d_test(X, Y, Z, XYZ): 35 | n = X.shape[0] 36 | x = rand(n**3) 37 | y1 = apply_kronecker((X,Y,Z), x) 38 | y2 = apply_tprod((X,Y,Z), x.reshape(n,n,n)) 39 | assert abs(XYZ.dot(x) - y1).max() < 1e-10 40 | assert abs(XYZ.dot(x) - y2.ravel()).max() < 1e-10 41 | 42 | x = rand(n**3, 1) 43 | assert np.allclose(XYZ.dot(x), apply_kronecker((X,Y,Z), x)) 44 | 45 | x = rand(n**3, 7) 46 | assert np.allclose(XYZ.dot(x), apply_kronecker((X,Y,Z), x)) 47 | 48 | def test_kronecker_3d(): 49 | X = rand(8,8) 50 | Y = rand(8,8) 51 | Z = rand(8,8) 52 | XYZ = np.kron(np.kron(X, Y), Z) 53 | _kron3d_test(X, Y, Z, XYZ) 54 | 55 | def test_kronecker_3d_sparse(): 56 | n = 25 57 | X = scipy.sparse.diags([rand(n-1), rand(n), rand(n-1)], offsets=(-1,0,1)) 58 | Y = scipy.sparse.diags([rand(n-1), rand(n), rand(n-1)], offsets=(-1,0,1)) 59 | Z = scipy.sparse.diags([rand(n-1), rand(n), rand(n-1)], offsets=(-1,0,1)) 60 | XYZ = scipy.sparse.kron(scipy.sparse.kron(X, Y), Z) 61 | _kron3d_test(X, Y, Z, XYZ) 62 | -------------------------------------------------------------------------------- /test/test_localmg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import numpy as np 5 | import scipy.linalg 6 | from pyiga import bspline, assemble, hierarchical, solvers, vform, geometry, utils 7 | 8 | from .test_hierarchical import create_example_hspace 9 | 10 | def run_local_multigrid(p, dim, n0, disparity, smoother, smooth_steps, strategy, tol): 11 | hs = create_example_hspace(p, dim, n0, disparity, num_levels=3) 12 | dir_dofs = hs.dirichlet_dofs() 13 | 14 | def rhs(*x): 15 | return 1.0 16 | 17 | params = {'geo': geometry.unit_square(), 'f': rhs} 18 | 19 | # assemble and solve the HB-spline problem 20 | hdiscr = hierarchical.HDiscretization(hs, vform.stiffness_vf(dim=2), params) 21 | A_hb = hdiscr.assemble_matrix() 22 | f_hb = hdiscr.assemble_rhs() 23 | P_hb = hs.virtual_hierarchy_prolongators() 24 | 25 | LS_hb = assemble.RestrictedLinearSystem(A_hb, f_hb, 26 | (dir_dofs, np.zeros_like(dir_dofs))) 27 | u_hb = scipy.sparse.linalg.spsolve(LS_hb.A, LS_hb.b) 28 | u_hb0 = LS_hb.complete(u_hb) 29 | 30 | # assemble and solve the THB-spline problem 31 | hs.truncate = True 32 | hdiscr = hierarchical.HDiscretization(hs, vform.stiffness_vf(dim=2), params) 33 | A_thb = hdiscr.assemble_matrix() 34 | f_thb = hdiscr.assemble_rhs() 35 | P_thb = hs.virtual_hierarchy_prolongators() 36 | 37 | LS_thb = assemble.RestrictedLinearSystem(A_thb, f_thb, 38 | (dir_dofs, np.zeros_like(dir_dofs))) 39 | u_thb = scipy.sparse.linalg.spsolve(LS_thb.A, LS_thb.b) 40 | u_thb0 = LS_thb.complete(u_thb) 41 | 42 | # iteration numbers of the local MG method in the (T)HB basis 43 | inds = hs.indices_to_smooth(strategy) 44 | iter_hb = num_iterations(solvers.local_mg_step(hs, A_hb, f_hb, P_hb, inds, smoother, smooth_steps), u_hb0, tol=tol) 45 | iter_thb = num_iterations(solvers.local_mg_step(hs, A_thb, f_thb, P_thb, inds, smoother, smooth_steps), u_thb0, tol=tol) 46 | 47 | winner = "HB" if (iter_hb <= iter_thb) else "THB" 48 | linestr = f'{strategy} ({smoother}) '.ljust(2*22) 49 | print(linestr + f'{iter_hb:5} {iter_thb:5} {winner:6}') 50 | return (iter_hb, iter_thb) 51 | 52 | def num_iterations(step, sol, tol=1e-8): 53 | x = np.zeros_like(sol) 54 | for iterations in range(1, 20000): 55 | x = step(x) 56 | 57 | if scipy.linalg.norm(x-sol) < tol: 58 | return iterations 59 | return np.inf 60 | 61 | ################################################################################ 62 | 63 | def test_localmg(): 64 | tol = 1e-8 65 | dim = 2 66 | n0 = 6 67 | 68 | print("---------------------------------------------------------") 69 | print("dim =", dim, "n0 =", n0) 70 | print("---------------------------------------------------------") 71 | 72 | # "exact", "gs", "symmetric_gs", "forward_gs", "backward_gs", "symmetric_gs" 73 | smoother, smooth_steps = "symmetric_gs", 1 74 | 75 | p = 3 76 | 77 | results = dict() 78 | for disparity in (np.inf, 1): 79 | results[disparity] = [] 80 | linestr = (f"[p = {p} disparity = {disparity}]").ljust(2*22) 81 | print(linestr + "{:8s} {:8s} {:8s}".format("HB", "THB", "Winner")) 82 | # available strategies: "new", "trunc", "func_supp", "cell_supp", "global" 83 | for strategy in ("new", "trunc", "func_supp", "cell_supp"): 84 | results[disparity].append( 85 | run_local_multigrid(p, dim, n0, disparity, smoother, smooth_steps, strategy, tol) 86 | ) 87 | 88 | assert np.array_equal(results[np.inf], 89 | [ ( 107, 118 ), 90 | ( 49, 19 ), 91 | ( 49, 15 ), 92 | ( 41, 15 ) ]) 93 | 94 | assert np.array_equal(results[1], 95 | [ ( 105, 104 ), 96 | ( 59, 23 ), 97 | ( 59, 23 ), 98 | ( 61, 22 ) ]) 99 | 100 | def test_solve_hmultigrid(): 101 | # test the built-in solve_hmultigrid function in pyiga.solvers 102 | hs = create_example_hspace(p=3, dim=2, n0=10, disparity=1, num_levels=3) 103 | 104 | for truncate in (False, True): # test HB- and THB-splines 105 | hs.truncate = truncate 106 | # assemble and solve the (T)HB-spline problem 107 | hdiscr = hierarchical.HDiscretization(hs, vform.stiffness_vf(dim=2), 108 | {'geo': geometry.unit_square(), 'f': lambda *x: 1.0}) 109 | A_hb = hdiscr.assemble_matrix() 110 | f_hb = hdiscr.assemble_rhs() 111 | 112 | # solve using a direct solver for comparison 113 | dir_dofs = hs.dirichlet_dofs() 114 | LS_hb = assemble.RestrictedLinearSystem(A_hb, f_hb, 115 | (dir_dofs, np.zeros_like(dir_dofs))) 116 | u_hb = scipy.sparse.linalg.spsolve(LS_hb.A, LS_hb.b) 117 | u_hb0 = LS_hb.complete(u_hb) 118 | 119 | # we use default parameters for smoother and strategy 120 | u_mg, iters = solvers.solve_hmultigrid(hs, A_hb, f_hb, tol=1e-8) 121 | assert np.allclose(u_hb0, u_mg) 122 | -------------------------------------------------------------------------------- /test/test_lowrank.py: -------------------------------------------------------------------------------- 1 | from pyiga.lowrank import * 2 | from pyiga import tensor 3 | from numpy.random import rand 4 | 5 | def test_tensorgenerator(): 6 | X = rand(3,4,5) 7 | tgen = TensorGenerator.from_array(X) 8 | assert np.allclose(X, tgen.asarray()) 9 | assert np.allclose(X[1,2,3], tgen.entry((1,2,3))) 10 | assert np.allclose(X[:,3,:], tgen.matrix_at((0,3,0), axes=(0,2)).asarray()) 11 | ## slicing notation 12 | assert np.array_equal(tgen[1,2,3], X[1,2,3]) 13 | assert np.array_equal(tgen[2,:,1], X[2,:,1]) 14 | assert np.array_equal(tgen[:,3,:], X[:,3,:]) 15 | assert np.array_equal(tgen[::-1], X[::-1]) 16 | assert np.array_equal(tgen[:, 3:0:-2, 2], X[:, 3:0:-2, 2]) 17 | assert np.array_equal(tgen[1:,2:,4:], X[1:,2:,4:]) 18 | assert np.array_equal(tgen[-1,-2,-3:], X[-1,-2,-3:]) 19 | i = [1,3] 20 | assert np.array_equal(tgen[1,i,2], X[1,i,2]) 21 | 22 | def test_aca(): 23 | np.random.seed(23875203) 24 | n,k = 50, 3 25 | X = np.zeros((n,n)) 26 | for i in range(k): 27 | X += np.outer(rand(n), rand(n)) 28 | X_aca = aca(X, tol=0, maxiter=k, verbose=0) 29 | assert np.allclose(X, X_aca) 30 | # compute approximation in low-rank form 31 | crosses = aca_lr(X, tol=0, maxiter=k, verbose=0) 32 | assert len(crosses) == 3 33 | T = tensor.CanonicalTensor.from_terms(crosses) 34 | assert np.allclose(X, T.asarray()) 35 | # check that approximation terminates correctly 36 | crosses = aca_lr(X, tol=0, verbose=0) 37 | assert len(crosses) <= 5 # due to rounding error, may require some more 38 | T = tensor.CanonicalTensor.from_terms(crosses) 39 | assert np.allclose(X, T.asarray()) 40 | 41 | def test_aca3d(): 42 | np.random.seed(23875203) 43 | n,k = 10, 3 44 | X = np.zeros((n,n,n)) 45 | for i in range(k): 46 | X += rand(n,1,1) * rand(1,n,1) * rand(1,1,n) 47 | X_aca = aca_3d(TensorGenerator.from_array(X), tol=0, maxiter=k, verbose=0) 48 | assert np.allclose(X, X_aca) 49 | # test automatic termination and low-rank tensor output 50 | X_aca_lr = aca_3d(TensorGenerator.from_array(X), tol=0, lr=True, verbose=0) 51 | assert np.allclose(X, X_aca_lr.asarray()) 52 | -------------------------------------------------------------------------------- /test/test_mlmatrix.py: -------------------------------------------------------------------------------- 1 | from pyiga.mlmatrix import * 2 | from numpy.random import rand 3 | 4 | from pyiga import utils 5 | 6 | def _random_banded(n, bw): 7 | return scipy.sparse.spdiags(rand(2*bw+1, n), np.arange(-bw,bw+1), n, n) 8 | 9 | def test_mlstructure(): 10 | bs, bw = (5,5), (2,2) 11 | S = MLStructure.multi_banded(bs, bw) 12 | A = _random_banded(bs[0], bw[0]).tocsr() 13 | A2 = scipy.sparse.kron(A, A) 14 | assert np.array_equal(S.nonzero(), A2.nonzero()) 15 | ## 16 | S = MLStructure.from_matrix(A) 17 | assert np.array_equal(S.nonzero(), A.nonzero()) 18 | ## 19 | S = MLStructure.from_kronecker((A, A)) 20 | assert np.array_equal(S.nonzero(), A2.nonzero()) 21 | ## 22 | B = scipy.sparse.random(8, 20, density=0.1) 23 | S = MLStructure.from_matrix(B) 24 | assert np.array_equal(S.transpose().nonzero(), B.T.nonzero()) 25 | # 26 | C = scipy.sparse.random(17, 9, density=0.1) 27 | A2 = scipy.sparse.kron(B, C) 28 | S = MLStructure.from_kronecker((B, C)) 29 | assert np.array_equal(S.nonzero(), A2.nonzero()) 30 | assert np.array_equal(S.transpose().nonzero(), A2.T.nonzero()) 31 | 32 | def test_nonzeros_for_rows(): 33 | A = np.array( 34 | [[0,2,0], 35 | [3,0,1], 36 | [0,7,0]]) 37 | B = np.array( 38 | [[2,9,0,0], 39 | [0,2,9,0], 40 | [0,0,2,9]]) 41 | X = np.kron(A, B) 42 | S = MLStructure.from_kronecker((A, B)) 43 | m, n = X.shape 44 | 45 | I, J = S.nonzeros_for_rows([4,5,6,7]) 46 | IX, JX = X[4:8, :].nonzero() 47 | assert np.array_equal(I, IX+4) 48 | assert np.array_equal(J, JX) 49 | 50 | I, J = S.nonzeros_for_columns([1,2,7]) 51 | for j in range(X.shape[1]): # zero the remaining columns 52 | if j not in (1,2,7): 53 | X[:,j] = 0 54 | IX, JX = X.nonzero() 55 | IJ = np.column_stack((I, J)) 56 | IJ_X = np.column_stack((IX, JX)) 57 | assert np.array_equal( 58 | np.unique(IJ, axis=0), # sorted lexocigraphically since order differs 59 | np.unique(IJ_X, axis=0)) 60 | 61 | def test_mlbanded_1d(): 62 | bs = (20,) 63 | bw = (3,) 64 | S = MLStructure.multi_banded(bs, bw) 65 | A = _random_banded(bs[0], bw[0]).toarray() 66 | X = MLMatrix(structure=S, matrix=A) 67 | A2 = X.asmatrix() 68 | assert np.allclose(A, A2.toarray()) 69 | x = rand(A.shape[1]) 70 | assert np.allclose(A.dot(x), X.dot(x)) 71 | 72 | def test_mlbanded_2d(): 73 | bs = (9, 12) 74 | bw = (2, 3) 75 | S = MLStructure.multi_banded(bs, bw) 76 | 77 | A, B = (_random_banded(n, p).toarray() for (n,p) in zip(bs, bw)) 78 | # rowwise vectorizations of A and B 79 | vecA, vecB = (X.ravel()[np.flatnonzero(X.ravel())] for X in (A,B)) 80 | # reordering of Kronecker product is outer product of vecs 81 | M = MLMatrix(structure=S, data=np.outer(vecA, vecB)) 82 | assert M.shape == (9*12, 9*12) 83 | assert M.nnz == vecA.size * vecB.size 84 | # test asmatrix() 85 | X = np.kron(A, B) 86 | assert np.allclose(X, M.asmatrix().toarray()) 87 | # test reorder() 88 | Y = np.kron(B, A) 89 | assert np.allclose(Y, M.reorder((1,0)).asmatrix().toarray()) 90 | # test matvec 91 | x = rand(M.shape[1]) 92 | assert np.allclose(X.dot(x), M.dot(x)) 93 | # test matrix constructor 94 | M2 = MLMatrix(structure=S, matrix=X) 95 | assert np.allclose(X, M2.asmatrix().toarray()) 96 | M2 = MLMatrix(structure=S, matrix=scipy.sparse.csr_matrix(X)) 97 | assert np.allclose(X, M2.asmatrix().toarray()) 98 | 99 | def test_mlbanded_3d(): 100 | bs = (8, 7, 6) 101 | bw = (3, 2, 2) 102 | S = MLStructure.multi_banded(bs, bw) 103 | S1 = MLStructure.multi_banded(bs[:2], bw[:2]) 104 | S2 = MLStructure.multi_banded(bs[2:], bw[2:]) 105 | S12 = S1.join(S2) 106 | assert S.bs == S12.bs 107 | assert S.slice(0,2).bs == S1.bs 108 | 109 | A, B, C = (_random_banded(n, p).toarray() for (n,p) in zip(bs, bw)) 110 | # rowwise vectorizations of A, B, C 111 | vecA, vecB, vecC = (X.ravel()[np.flatnonzero(X.ravel())] for X in (A,B,C)) 112 | # reordering of Kronecker product is outer product of vecs 113 | M = MLMatrix(structure=S, 114 | data=vecA[:,None,None]*vecB[None,:,None]*vecC[None,None,:]) 115 | assert M.shape == (8*7*6, 8*7*6) 116 | assert M.nnz == vecA.size * vecB.size * vecC.size 117 | # test asmatrix() 118 | X = np.kron(np.kron(A, B), C) 119 | assert np.allclose(X, M.asmatrix().toarray()) 120 | # test reorder() 121 | Y = np.kron(np.kron(C, A), B) 122 | assert np.allclose(Y, M.reorder((2,0,1)).asmatrix().toarray()) 123 | # test matvec 124 | x = rand(M.shape[1]) 125 | assert np.allclose(X.dot(x), M.dot(x)) 126 | # test matrix constructor 127 | M2 = MLMatrix(structure=S, matrix=X) 128 | assert np.allclose(X, M2.asmatrix().toarray()) 129 | M2 = MLMatrix(structure=S, matrix=scipy.sparse.csr_matrix(X)) 130 | assert np.allclose(X, M2.asmatrix().toarray()) 131 | 132 | def test_mlbanded_4d(): 133 | bs = (7, 6, 5, 4) 134 | bw = (3, 2, 2, 2) 135 | 136 | As = tuple(_random_banded(n, p) for (n,p) in zip(bs, bw)) 137 | A = utils.multi_kron_sparse(As) 138 | 139 | S = MLStructure.multi_banded(bs, bw) 140 | M = MLMatrix(structure=S, matrix=A) 141 | assert np.allclose(A.toarray(), M.asmatrix().toarray()) 142 | 143 | def test_tofrom_seq(): 144 | for i in range(3*4*5): 145 | assert to_seq(from_seq(i, (3,4,5)), (3,4,5)) == i 146 | 147 | def test_tofrom_multilevel(): 148 | bs = np.array(((3,3), (4,4), (5,5))) # block sizes for each level 149 | for i in range(3*3 + 4*4 + 5*5): 150 | for j in range(3*3 + 4*4 + 5*5): 151 | assert reindex_from_multilevel(reindex_to_multilevel(i, j, bs), bs) == (i,j) 152 | 153 | def test_banded_sparsity(): 154 | n = 10 155 | bw = 2 156 | 157 | X = np.zeros((n,n), dtype=int) 158 | for i in range(n): 159 | for j in range(n): 160 | if abs(i-j) <= bw: 161 | X[i,j] = 1 162 | assert np.array_equal(np.flatnonzero(X), 163 | compute_banded_sparsity(n, bw)) 164 | assert np.array_equal(np.transpose(np.nonzero(X)), 165 | compute_banded_sparsity_ij(n, bw)) 166 | 167 | def test_reorder(): 168 | n1, n2 = 6, 7 169 | A1 = _random_banded(n1, 3).toarray() 170 | A2 = _random_banded(n2, 4).toarray() 171 | A = np.kron(A1, A2) 172 | AR = reorder(A, n1, n1) # shape: (n1*n1) x (n2*n2) 173 | for i in range(n1*n1): 174 | for j in range(n2*n2): 175 | ii, jj = reindex_from_reordered(i, j, n1, n1, n2, n2) 176 | assert AR[i, j] == A[ii, jj] 177 | -------------------------------------------------------------------------------- /test/test_operators.py: -------------------------------------------------------------------------------- 1 | from pyiga.operators import * 2 | 3 | from numpy.random import rand 4 | import scipy.linalg 5 | 6 | def _test_oper(A, B): 7 | assert A.shape == B.shape 8 | n = A.shape[1] 9 | x = rand(n) 10 | assert np.allclose(A.dot(x), B.dot(x)) 11 | x = rand(n,1) 12 | assert np.allclose(A.dot(x), B.dot(x)) 13 | x = rand(n,3) 14 | assert np.allclose(A.dot(x), B.dot(x)) 15 | 16 | def test_null(): 17 | Z = np.zeros((7,3)) 18 | _test_oper(NullOperator(Z.shape), Z) 19 | _test_oper(NullOperator(Z.shape).T, Z.T) 20 | 21 | def test_identity(): 22 | I = np.eye(7) 23 | _test_oper(IdentityOperator(7), I) 24 | _test_oper(IdentityOperator(7).T, I) 25 | 26 | def test_diagonal(): 27 | diag = rand(10) 28 | diag_op = DiagonalOperator(diag) 29 | D = np.diag(diag) 30 | _test_oper(diag_op, D) 31 | _test_oper(diag_op.T, D) 32 | 33 | def test_blockdiag(): 34 | A = rand(2,3) 35 | B = rand(4,4) 36 | C = rand(3,1) 37 | X = scipy.linalg.block_diag(A, B, C) 38 | Y = BlockDiagonalOperator(A, B, C) 39 | _test_oper(X, Y) 40 | _test_oper(X.T, Y.T) 41 | 42 | def test_block(): 43 | A,B = rand(3,3), rand(3,4) 44 | C,D = rand(2,3), rand(2,4) 45 | blocks = [[A,B],[C,D]] 46 | X = BlockOperator(blocks) 47 | Y = np.block(blocks) 48 | _test_oper(X, Y) 49 | _test_oper(X.T, Y.T) 50 | 51 | def test_subspace(): 52 | I = np.eye(4) 53 | P1, P2 = I[:, :2], I[:, 2:] 54 | B1, B2 = rand(2,2),rand(2,2) 55 | X = SubspaceOperator((P1,P2), (B1,B2)) 56 | _test_oper(X, scipy.linalg.block_diag(B1, B2)) 57 | _test_oper(X.T, scipy.linalg.block_diag(B1.T, B2.T)) 58 | _test_oper(X.T.T, scipy.linalg.block_diag(B1, B2)) 59 | 60 | def test_solver(): 61 | A = rand(3,3) 62 | _test_oper(make_solver(A), np.linalg.inv(A)) 63 | B = A + A.T + 3*np.eye(3) 64 | _test_oper(make_solver(B, symmetric=True), np.linalg.inv(B)) 65 | _test_oper(make_solver(B, spd=True), np.linalg.inv(B)) 66 | A = scipy.sparse.csr_matrix(A) 67 | _test_oper(make_solver(A), np.linalg.inv(A.toarray())) 68 | B = scipy.sparse.csr_matrix(B) 69 | _test_oper(make_solver(B, symmetric=True), np.linalg.inv(B.toarray())) 70 | _test_oper(make_solver(B, spd=True), np.linalg.inv(B.toarray())) 71 | 72 | def test_kronecker(): 73 | A = rand(2,3) 74 | B = rand(4,5) 75 | X = KroneckerOperator(A, B) 76 | Y = np.kron(A, B) 77 | _test_oper(X, Y) 78 | _test_oper(X.T, Y.T) 79 | 80 | def test_kron_solver(): 81 | A = rand(3,3) 82 | B = rand(4,4) 83 | _test_oper(make_kronecker_solver(A, B), np.linalg.inv(np.kron(A, B))) 84 | A = scipy.sparse.csr_matrix(A) 85 | B = scipy.sparse.csr_matrix(B) 86 | _test_oper(make_kronecker_solver(A, B), np.linalg.inv(np.kron(A.toarray(), B.toarray()))) 87 | -------------------------------------------------------------------------------- /test/test_solve.py: -------------------------------------------------------------------------------- 1 | # integration tests for solving PDEs 2 | 3 | import numpy as np 4 | from pyiga import bspline, geometry, assemble, solvers, approx 5 | 6 | def test_poisson_2d(): 7 | kvs = 2 * (bspline.make_knots(3, 0.0, 1.0, 10),) 8 | geo = geometry.quarter_annulus() 9 | 10 | def g(x, y): # exact solution / boundary data 11 | return np.cos(x + y) + np.exp(y - x) 12 | def f(x, y): # right-hand side (-Laplace of g) 13 | return 2 * (np.cos(x + y) - np.exp(y - x)) 14 | 15 | # pure Dirichlet boundary conditions 16 | bcs = assemble.compute_dirichlet_bcs(kvs, geo, ('all', g)) 17 | 18 | # compute right-hand side from function f 19 | rhs = assemble.inner_products(kvs, f, f_physical=True, geo=geo).ravel() 20 | A = assemble.stiffness(kvs, geo=geo) 21 | LS = assemble.RestrictedLinearSystem(A, rhs, bcs) 22 | 23 | u_sol = solvers.make_solver(LS.A, spd=True).dot(LS.b) 24 | u = LS.complete(u_sol) 25 | u_ex = approx.project_L2(kvs, g, f_physical=True, geo=geo).ravel() 26 | 27 | rms_err = np.sqrt(np.mean((u - u_ex)**2)) 28 | assert rms_err < 5e-5 # error: about 4.83e-05 29 | -------------------------------------------------------------------------------- /test/test_solvers.py: -------------------------------------------------------------------------------- 1 | from pyiga.solvers import * 2 | from pyiga import bspline, assemble 3 | 4 | def test_fastdiag_solver(): 5 | kvs = [ 6 | bspline.make_knots(4, 0.0, 1.0, 3), 7 | bspline.make_knots(3, 0.0, 1.0, 4), 8 | bspline.make_knots(2, 0.0, 1.0, 5) 9 | ] 10 | # compute Dirichlet matrices 11 | KM = [(assemble.stiffness(kv)[1:-1, 1:-1].toarray(), assemble.mass(kv)[1:-1, 1:-1].toarray()) for kv in kvs] 12 | solver = fastdiag_solver(KM) 13 | 14 | def multikron(*Xs): 15 | return reduce(np.kron, Xs) 16 | A = ( multikron(KM[0][0], KM[1][1], KM[2][1]) + 17 | multikron(KM[0][1], KM[1][0], KM[2][1]) + 18 | multikron(KM[0][1], KM[1][1], KM[2][0]) ) 19 | f = np.random.rand(A.shape[0]) 20 | assert np.allclose(f, solver.dot(A.dot(f))) 21 | 22 | def test_gauss_seidel(): 23 | from numpy.random import rand 24 | A = abs(rand(10,10)) + np.eye(10) # try to make it not too badly conditioned 25 | b = rand(10) 26 | 27 | for sweep in ('forward', 'backward', 'symmetric'): 28 | x1 = rand(10) 29 | x2 = x1.copy() 30 | 31 | gauss_seidel(scipy.sparse.csr_matrix(A), x1, b, iterations=2, sweep=sweep) 32 | gauss_seidel(A, x2, b, iterations=2, sweep=sweep) 33 | assert abs(x1-x2).max() < 1e-12 34 | 35 | def test_gauss_seidel_indexed(): 36 | from numpy.random import rand 37 | A = abs(rand(10,10)) + np.eye(10) # try to make it not too badly conditioned 38 | b = rand(10) 39 | indices = [3, 6, 9] 40 | 41 | for sweep in ('forward', 'backward', 'symmetric'): 42 | x1 = rand(10) 43 | x2 = x1.copy() 44 | 45 | gauss_seidel(scipy.sparse.csr_matrix(A), x1, b, iterations=2, indices=indices, sweep=sweep) 46 | gauss_seidel(A, x2, b, iterations=2, indices=indices, sweep=sweep) 47 | assert abs(x1-x2).max() < 1e-12 48 | 49 | def test_twogrid(): 50 | kv_c = bspline.make_knots(3, 0.0, 1.0, 50) 51 | kv = kv_c.refine() 52 | P = bspline.prolongation(kv_c, kv) 53 | A = assemble.mass(kv) + assemble.stiffness(kv) 54 | f = bspline.load_vector(kv, lambda x: 1.0) 55 | S = SequentialSmoother((GaussSeidelSmoother(), OperatorSmoother(1e-6*np.eye(len(f))))) 56 | x = twogrid(A, f, P, S) 57 | assert np.linalg.norm(f - A.dot(x)) < 1e-6 58 | 59 | def test_newton(): 60 | def F(x): return np.array([np.sin(x[0]) - 1/2]) 61 | def J(x): return np.array([[np.cos(x[0])]]) 62 | x = newton(F, J, [0.0]) 63 | assert np.allclose(x, np.pi / 6) 64 | 65 | def test_ode(): 66 | # simple stiff ODE 67 | A = np.array([ 68 | [0.0, 1.0], 69 | [-1000.0, -1001.0] 70 | ]) 71 | M = np.eye(2) 72 | def F(x): return A.dot(x) 73 | def J(x): return A 74 | x0 = np.array([1.0, 0.0]) 75 | 76 | def exsol(t): return -1/999 * np.exp(-1000*t) + 1000/999 * np.exp(-t) 77 | 78 | t_end = 1.0 79 | sol_1 = exsol(t_end) 80 | 81 | # constant step Crank-Nicolson 82 | sols = crank_nicolson(M, F, J, x0, 1e-2, t_end) 83 | assert np.isclose(sols[1][-1][0], sol_1, rtol=1e-4) 84 | 85 | # constant step DIRK method 86 | sols = sdirk3(M, F, J, x0, 1e-2, t_end) 87 | assert np.isclose(sols[1][-1][0], sol_1, rtol=1e-4) 88 | 89 | # constant step Rosenbrock method 90 | sols = ros3p(M, F, J, x0, 1e-2, t_end, tol=None) 91 | assert np.isclose(sols[1][-1][0], sol_1, rtol=1e-4) 92 | 93 | # adaptive step DIRK method 94 | sols = esdirk34(M, F, J, x0, 1e-2, t_end, tol=1e-5) 95 | ts = sols[0] 96 | xs = sols[1] 97 | assert ts[-2] <= t_end <= ts[-1] 98 | from scipy.interpolate import interp1d 99 | x_end = interp1d(ts, xs, kind='cubic', axis=0)(t_end) 100 | #print(len(ts), 'steps, error =', abs(x_end[0] - sol_1)) 101 | assert np.isclose(x_end[0], sol_1, rtol=1e-4) 102 | -------------------------------------------------------------------------------- /test/test_spline.py: -------------------------------------------------------------------------------- 1 | from pyiga.spline import * 2 | 3 | def _random_kv(p, n): 4 | steps = np.random.rand(n) * 0.75 + 0.25 5 | knots = np.cumsum(steps) 6 | knots -= knots.min() 7 | knots /= knots.max() 8 | return bspline.KnotVector( 9 | np.concatenate((p * [knots[0]], knots, p * [knots[-1]])), p) 10 | 11 | def test_derivative(): 12 | kv = _random_kv(4, 20) 13 | s = Spline(kv, np.random.rand(kv.numdofs)) 14 | s1 = s.derivative() 15 | x = np.linspace(0.0, 1.0, 50) 16 | d1 = s.deriv(x, 1) 17 | d2 = s1.eval(x) 18 | assert abs(d1-d2).max() < 1e-10 19 | 20 | -------------------------------------------------------------------------------- /test/test_stilde.py: -------------------------------------------------------------------------------- 1 | from pyiga.stilde import * 2 | 3 | def test_Stilde_basis(): 4 | kv = bspline.make_knots(4, 0.0, 1.0, 10) 5 | P_tilde, P_compl = Stilde_basis(kv) 6 | n = kv.numdofs 7 | assert n == P_tilde.shape[0] 8 | assert n == P_compl.shape[0] 9 | assert n == P_tilde.shape[1] + P_compl.shape[1] 10 | assert P_tilde.shape[1] == 10 11 | assert abs(P_tilde.T.dot(P_compl)).max() < 1e-14 12 | -------------------------------------------------------------------------------- /test/test_tensor.py: -------------------------------------------------------------------------------- 1 | from pyiga.tensor import * 2 | 3 | import unittest 4 | from numpy.random import rand 5 | from scipy.sparse.linalg import aslinearoperator 6 | from scipy.sparse import kron as spkron 7 | 8 | def test_modek_tprod(): 9 | X = rand(3,3,3) 10 | A = rand(3,3) 11 | Y = np.zeros((3,3,3)) 12 | for i in range(3): 13 | for j in range(3): 14 | for k in range(3): 15 | Y[i,j,k] = np.dot(A[i,:], X[:,j,k]) 16 | assert np.allclose(Y, modek_tprod(A, 0, X)) 17 | # test modek_tprod with LinearOperator 18 | assert np.allclose(modek_tprod(A, 1, X), 19 | modek_tprod(aslinearoperator(A), 1, X)) 20 | 21 | def test_tuckerprod(): 22 | U = [rand(10,n) for n in range(3,6)] 23 | C = rand(3,4,5) 24 | for i in range(3): 25 | U2 = U[:] 26 | U2[i] = np.eye(i+3) 27 | X1 = apply_tprod(U2, C) 28 | U2[i] = None 29 | X2 = apply_tprod(U2, C) 30 | assert np.allclose(X1, X2) 31 | 32 | def _random_tucker(shape, R): 33 | Us = tuple(rand(n,R) for n in shape) 34 | d = len(Us) 35 | return TuckerTensor(Us, rand(*(d * (R,)))) 36 | 37 | def _test_tensor_arithmetic(X, Y): 38 | assert np.allclose((-X).asarray(), -(X.asarray())) 39 | assert np.allclose((X + Y).asarray(), X.asarray() + Y.asarray()) 40 | assert np.allclose((X - Y).asarray(), X.asarray() - Y.asarray()) 41 | 42 | def _test_tensor_slicing(X): 43 | A = X.asarray() 44 | assert np.allclose(X[1,2,3], A[1,2,3]) 45 | assert np.allclose(X[2,:,1].asarray(), A[2,:,1]) 46 | assert np.allclose(X[:,3,:].asarray(), A[:,3,:]) 47 | assert np.allclose(X[::-1].asarray(), A[::-1]) 48 | assert np.allclose(X[:, 3:0:-2, 2].asarray(), A[:, 3:0:-2, 2]) 49 | assert np.allclose(X[1:,2:,4:].asarray(), A[1:,2:,4:]) 50 | assert np.allclose(X[-1,-2,-3:].asarray(), A[-1,-2,-3:]) 51 | i = [1,3] 52 | assert np.allclose(X[1,i,2].asarray(), A[1,i,2]) 53 | 54 | def test_tucker(): 55 | X = rand(3,4,5) 56 | T = hosvd(X) 57 | assert np.allclose(X, T.asarray()) 58 | assert np.allclose(T.asarray(), T.orthogonalize().asarray()) 59 | assert np.allclose(np.linalg.norm(X), T.norm()) 60 | assert np.allclose(T.asarray(), T.copy().asarray()) 61 | # zeros 62 | Z = TuckerTensor.zeros((3,4,5)) 63 | assert fro_norm(Z.asarray()) == 0.0 64 | # ones 65 | Z = TuckerTensor.ones((3,4,5)) 66 | assert np.allclose(Z.asarray(), np.ones((3,4,5))) 67 | ### 68 | X = _random_tucker((3,4,5), 2) 69 | # orthogonalize 70 | XO = X.orthogonalize() 71 | assert np.allclose(X.asarray(), XO.asarray()) 72 | assert X.X.shape == XO.X.shape 73 | for k in range(XO.ndim): 74 | U = XO.Us[k] 75 | assert U.shape == X.Us[k].shape 76 | assert np.allclose(U.T.dot(U), np.eye(U.shape[1])) 77 | # add and sub 78 | _test_tensor_arithmetic(X, _random_tucker((3,4,5), 3)) 79 | # compression 80 | XX = (X + X).compress() 81 | assert XX.R == X.R and np.allclose(XX.asarray(), 2*X.asarray()) 82 | # als1 83 | x = als1(X) 84 | y = als1(X.asarray()) 85 | assert np.allclose(outer(*x), outer(*y), atol=1e-4) 86 | # conversion 87 | X = _random_canonical((3,4,5), 2) 88 | Y = TuckerTensor.from_tensor(X) 89 | assert np.allclose(X.asarray(), Y.asarray()) 90 | X = rand(3,4,5) 91 | Y = TuckerTensor.from_tensor(X) 92 | assert np.allclose(X, Y.asarray()) 93 | # squeeze 94 | A = _random_tucker((7,1,6,1), 3) 95 | A2 = A.squeeze() 96 | assert np.allclose(A.asarray()[:,0,:,0], A2.asarray()) 97 | A2 = A.squeeze(1) 98 | assert np.allclose(A.asarray()[:,0,:,:], A2.asarray()) 99 | with unittest.TestCase().assertRaises(ValueError): 100 | A.squeeze(2) # invalid axis - not length 1 101 | assert A.squeeze(axis=()) is A 102 | A = _random_tucker((1,1,1), 3) 103 | assert A.squeeze() == A.ravel()[0] 104 | # slicing 105 | _test_tensor_slicing(_random_tucker((4,5,6), 2)) 106 | 107 | def test_gta(): 108 | X = _random_tucker((3,4,5), 2) 109 | Y = gta(X, R=2) 110 | assert np.allclose(X.asarray(), Y.asarray()) 111 | Y = gta(X.asarray(), R=2) 112 | assert np.allclose(X.asarray(), Y.asarray()) 113 | 114 | def test_join_tucker(): 115 | A = _random_tucker((3,4,5), 2) 116 | B = _random_tucker((3,4,5), 3) 117 | Us, XA, XB = join_tucker_bases(A, B) 118 | assert np.allclose(A.asarray(), TuckerTensor(Us,XA).asarray()) 119 | assert np.allclose(B.asarray(), TuckerTensor(Us,XB).asarray()) 120 | 121 | def test_truncate(): 122 | """Check that a rank 1 tensor is exactly represented 123 | by the 1-truncation of the HOSVD.""" 124 | # rank 1 tensor 125 | X = outer(rand(3), rand(4), rand(5)) 126 | T = hosvd(X) 127 | assert find_truncation_rank(T.X, 1e-12) == (1,1,1) 128 | T1 = T.truncate(1) 129 | assert np.allclose(X, T1.asarray()) 130 | 131 | def test_truncate2(): 132 | """Check that Tucker truncation error is the Frobenius norm 133 | of the residual core tensor.""" 134 | X = rand(5,5,5) 135 | T = hosvd(X) 136 | k = 3 137 | Tk = T.truncate(k) 138 | E = X - Tk.asarray() 139 | Cdk = T.X 140 | Cdk[:k,:k,:k] = 0 141 | assert np.allclose(fro_norm(E), fro_norm(Cdk)) 142 | 143 | def test_outer(): 144 | x, y, z, = rand(3), rand(4), rand(5) 145 | X = outer(x, y, z) 146 | Y = x[:, None, None] * y[None, :, None] * z[None, None, :] 147 | assert np.allclose(X, Y) 148 | 149 | def test_pad(): 150 | X = _random_tucker((3,4,5), 2) 151 | Y = pad(X, [(2,2), None, (0,1)]) 152 | assert Y.shape == (7, 4, 6) 153 | YA = Y.asarray() 154 | assert np.allclose(YA[2:-2, :, :-1], X.asarray()) 155 | assert np.linalg.norm(YA[:2, :, :].ravel()) < 1e-10 156 | assert np.linalg.norm(YA[-2:, :, :].ravel()) < 1e-10 157 | assert np.linalg.norm(YA[:, :, -1:].ravel()) < 1e-10 158 | 159 | def _random_canonical(shape, R): 160 | Xs = tuple(rand(n,R) for n in shape) 161 | return CanonicalTensor(Xs) 162 | 163 | def test_canonical(): 164 | X,Y,Z = tuple(np.zeros((5,2)) for _ in range(3)) 165 | for i in range(2): 166 | X[i,i] = Y[i,i] = Z[i,i] = 2.0 167 | A = CanonicalTensor((X,Y,Z)) 168 | assert A.ndim == 3 169 | assert A.shape == (5,5,5) 170 | assert A.R == 2 171 | assert np.allclose(A.asarray(), A.copy().asarray()) 172 | B = A.asarray() 173 | assert B.shape == A.shape 174 | C = np.zeros((2,2,2)) 175 | np.fill_diagonal(C, 8.0) 176 | B[:2, :2, :2] -= C 177 | assert np.allclose(B, 0.0) 178 | # zeros 179 | Z = CanonicalTensor.zeros((3,4,5)) 180 | assert fro_norm(Z.asarray()) == 0.0 181 | # ones 182 | Z = CanonicalTensor.ones((3,4,5)) 183 | assert np.allclose(Z.asarray(), np.ones((3,4,5))) 184 | # norm 185 | A = _random_canonical((3,4,5), 2) 186 | assert np.allclose(A.norm(), np.linalg.norm(A.asarray())) 187 | # generation from terms 188 | B = CanonicalTensor.from_terms(A.terms()) 189 | assert A.shape == B.shape and A.R == B.R 190 | assert np.allclose(A.asarray(), B.asarray()) 191 | # conversion from Tucker 192 | T = _random_tucker((3,4,5), 2) 193 | B = CanonicalTensor.from_tensor(T) 194 | assert B.R == 2**3 195 | assert np.allclose(T.asarray(), B.asarray()) 196 | # als1 197 | x = als1(A) 198 | y = als1(A.asarray()) 199 | assert np.allclose(outer(*x), outer(*y), atol=1e-4) 200 | # add and sub 201 | _test_tensor_arithmetic(A, _random_canonical(A.shape, 3)) 202 | # squeeze 203 | A = _random_canonical((7,1,6,1), 3) 204 | A2 = A.squeeze() 205 | assert np.allclose(A.asarray()[:,0,:,0], A2.asarray()) 206 | A2 = A.squeeze(1) 207 | assert np.allclose(A.asarray()[:,0,:,:], A2.asarray()) 208 | with unittest.TestCase().assertRaises(ValueError): 209 | A.squeeze(2) # invalid axis - not length 1 210 | assert A.squeeze(axis=()) is A 211 | A = _random_canonical((1,1,1), 3) 212 | assert A.squeeze() == A.ravel()[0] 213 | # slicing 214 | _test_tensor_slicing(_random_canonical((4,5,6), 2)) 215 | 216 | def test_coercion(): 217 | C = _random_canonical((3,4,5), 2) 218 | T = _random_tucker((3,4,5), 2) 219 | A = rand(3,4,5) 220 | 221 | def _test_sum_diff(X, Y, typ): 222 | XY = X + Y 223 | assert isinstance(XY, typ) 224 | assert np.allclose(asarray(XY), asarray(X) + asarray(Y)) 225 | XY = X - Y 226 | assert isinstance(XY, typ) 227 | assert np.allclose(asarray(XY), asarray(X) - asarray(Y)) 228 | 229 | _test_sum_diff(C, T, TuckerTensor) 230 | _test_sum_diff(C, A, np.ndarray) 231 | _test_sum_diff(T, C, TuckerTensor) 232 | _test_sum_diff(T, A, np.ndarray) 233 | 234 | def test_grou(): 235 | X = _random_canonical((3,4,5), 1) 236 | Y = grou(X, R=2) 237 | assert np.allclose(X.asarray(), Y.asarray()) 238 | Y = grou(X.asarray(), R=2) 239 | assert np.allclose(X.asarray(), Y.asarray()) 240 | 241 | def test_tensorsum(): 242 | X = _random_canonical((3,4,5), R=2) 243 | A = CanonicalTensor(Z[:,0] for Z in X.Xs) 244 | B = CanonicalTensor(Z[:,1] for Z in X.Xs) 245 | AB = TensorSum(A, B) 246 | assert X.shape == AB.shape 247 | assert np.allclose(X.asarray(), AB.asarray()) 248 | U = (rand(3,3), rand(4,4), rand(5,5)) 249 | assert np.allclose( 250 | apply_tprod(U, X).asarray(), 251 | apply_tprod(U, AB).asarray()) 252 | _test_tensor_arithmetic(AB, _random_tucker(AB.shape, 2)) 253 | _test_tensor_slicing(AB) 254 | 255 | def test_tensorprod(): 256 | A = _random_tucker((2,3), 2) 257 | B = _random_canonical((4,2), 3) 258 | X = TensorProd(A, B) 259 | assert X.ndim == A.ndim + B.ndim 260 | assert X.shape == A.shape + B.shape 261 | assert np.allclose(X.asarray(), 262 | array_outer(A.asarray(), B.asarray())) 263 | Us = (rand(2,2), rand(3,3), rand(4,4), rand(2,2)) 264 | assert np.allclose(apply_tprod(Us, X).asarray(), 265 | array_outer(apply_tprod(Us[:2], A).asarray(), 266 | apply_tprod(Us[2:], B).asarray())) 267 | # arithmetic 268 | _test_tensor_arithmetic(X, _random_tucker(X.shape, 2)) 269 | # slicing 270 | _test_tensor_slicing(TensorProd(_random_tucker((3,4), 2), 271 | _random_canonical((5,), 3))) 272 | ## compare to CanonicalTensor 273 | x, y = rand(7), rand(8) 274 | X = TensorProd(x, y) 275 | Y = CanonicalTensor((x, y)) 276 | assert np.allclose(X.asarray(), Y.asarray()) 277 | 278 | 279 | def test_als1(): 280 | xs = rand(3), rand(4), rand(5) 281 | X = outer(*xs) 282 | ys = als1(X) 283 | from numpy.linalg import norm 284 | assert all(np.allclose(x / norm(x), y / norm(y)) 285 | for (x,y) in zip(xs, ys)) 286 | 287 | def test_als(): 288 | # asserts are disabled for now since they sometimes fail at random 289 | ### canonical 290 | A = _random_canonical((3,4,5), 2) 291 | B = als(A, R=2, maxiter=100) 292 | #assert np.allclose(A.asarray(), B.asarray(), atol=1e-4) 293 | ### full tensor 294 | C = als(A.asarray(), R=2, maxiter=100) 295 | #assert np.allclose(A.asarray(), C.asarray(), atol=1e-4) 296 | ### Tucker 297 | A = _random_tucker((3,4,5), 2) 298 | # diagonalize core tensor 299 | A.X[:] = 0.0 300 | A.X[0,0,0] = A.X[1,1,1] = 1.0 301 | B = als(A, R=2, maxiter=100) 302 | #assert np.allclose(A.asarray(), B.asarray(), atol=1e-4) 303 | 304 | def test_ls(): 305 | from pyiga import bspline, assemble 306 | kv = bspline.make_knots(3, 0.0, 1.0, 10) 307 | K = assemble.stiffness(kv)[1:-1, 1:-1] 308 | M = assemble.mass(kv)[1:-1, 1:-1] 309 | A = [(K,M,M), (M,K,M), (M,M,K)] 310 | n = K.shape[0] 311 | F = CanonicalTensor.ones((n,n,n)) 312 | # 313 | X = CanonicalTensor(als1_ls(A, F)) 314 | Y = CanonicalTensor(als1_ls(A, F, spd=True)) 315 | assert X.shape == F.shape 316 | assert Y.shape == F.shape 317 | assert fro_norm(X - Y) < 0.1 * fro_norm(X) 318 | # 319 | T1 = gta_ls(A, F, 5) 320 | T2 = gta_ls(A, F, 5, spd=True) 321 | assert T1.shape == F.shape 322 | assert T2.shape == F.shape 323 | assert fro_norm(T1 - T2) < 0.01 * fro_norm(T1) 324 | A_op = CanonicalOperator(A) 325 | assert fro_norm(A_op.apply(T2) - F) < 0.01 * fro_norm(F) # check relative residual 326 | 327 | def _random_banded(n, bw): 328 | return scipy.sparse.spdiags(rand(2*bw+1, n), np.arange(-bw,bw+1), n, n).tocsr() 329 | 330 | def test_canonical_op(): 331 | N = (3,4,5) 332 | I = CanonicalOperator.eye(N) 333 | assert I.shape[0] == I.shape[1] == N 334 | X = _random_tucker(N, 2) 335 | Y = I.apply(X) 336 | assert Y.R == (2,2,2) 337 | assert np.allclose(X.asarray(), Y.asarray()) 338 | # multiplication 339 | A = CanonicalOperator([tuple(_random_banded(n, 1) for n in N) for k in range(3)]) 340 | B = CanonicalOperator([tuple(_random_banded(n, 1) for n in N) for k in range(2)]) 341 | AB = A * B 342 | assert AB.R == 6 343 | assert scipy.sparse.linalg.norm(AB.asmatrix() - (A.asmatrix().dot(B.asmatrix()))) < 1e-6 344 | Y1 = A.apply(B.apply(X)) 345 | Y2 = AB.apply(X) 346 | assert np.allclose(Y1.asarray(), Y2.asarray()) 347 | assert np.allclose(((A @ B) @ X).asarray(), (A @ (B @ X)).asarray()) 348 | # arithmetic 349 | assert np.allclose(((A + B) @ X).asarray(), (A @ X + B @ X).asarray()) 350 | assert np.allclose(((A - B) @ X).asarray(), (A @ X - B @ X).asarray()) 351 | assert np.allclose(((-A) @ X).asarray(), -(A @ X).asarray()) 352 | # kron 353 | assert scipy.sparse.linalg.norm( 354 | (A.kron(B)).asmatrix() - spkron(A.asmatrix(), B.asmatrix())) < 1e-10 355 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | from pyiga.utils import * 2 | import numpy as np 3 | from numpy.random import rand 4 | 5 | def _random_banded(n, bw): 6 | return scipy.sparse.spdiags(rand(2*bw+1, n), np.arange(-bw,bw+1), n, n).tocsr() 7 | 8 | def test_lazy(): 9 | def f(x, y, z): 10 | return np.sin(x) * np.cos(y + np.exp(z)) 11 | grid = 3 * (np.linspace(0, 1, 8),) 12 | F = grid_eval(f, grid) 13 | LF1 = LazyArray(f, grid) 14 | LF2 = LazyCachingArray(f, (), grid, 2) 15 | assert np.allclose(F[2:4, 2:6, 6:8], LF1[2:4, 2:6, 6:8]) 16 | assert np.allclose(F[2:4, 2:6, 6:8], LF2[2:4, 2:6, 6:8]) 17 | # try again to test the caching behavior 18 | assert np.allclose(F[2:4, 2:6, 6:8], LF2[2:4, 2:6, 6:8]) 19 | 20 | ## test a vector-valued function 21 | def f(x, y, z): 22 | return np.stack([x*y*np.ones_like(z), x*np.ones_like(y)*z], axis=-1) 23 | F = grid_eval(f, grid) 24 | LF1 = LazyArray(f, grid) 25 | LF2 = LazyCachingArray(f, (2,), grid, 2) 26 | assert np.allclose(F[2:4, 2:6, 6:8], LF1[2:4, 2:6, 6:8]) 27 | assert np.allclose(F[2:4, 2:6, 6:8], LF2[2:4, 2:6, 6:8]) 28 | # try again to test the caching behavior 29 | assert np.allclose(F[2:4, 2:6, 6:8], LF2[2:4, 2:6, 6:8]) 30 | 31 | def test_BijectiveIndex(): 32 | I = BijectiveIndex([ (1,2), (3,4), (2,7) ]) 33 | assert len(I) == 3 34 | assert I[1] == (3,4) 35 | assert I.index((2,7)) == 2 36 | 37 | def test_kron_partial(): 38 | As = (_random_banded(5, 1), _random_banded(4, 2), _random_banded(6, 3)) 39 | X = multi_kron_sparse(As) 40 | X_partial = kron_partial(As, rows=list(range(17, 25))) 41 | assert np.allclose(X[17:25].toarray(), X_partial[17:25].toarray()) 42 | assert X_partial[:17].nnz == 0 43 | assert X_partial[25:(5*4*6)].nnz == 0 44 | # 45 | X_partial = kron_partial(As, rows=list(range(17, 25)), restrict=True) 46 | assert np.allclose(X[17:25].toarray(), X_partial.toarray()) 47 | # 48 | X_partial = kron_partial(As, rows=[]) 49 | assert X_partial.shape == X.shape 50 | assert X_partial.nnz == 0 51 | # 52 | X_partial = kron_partial(As, rows=[], restrict=True) 53 | assert X_partial.shape == (0, X.shape[1]) 54 | assert X_partial.nnz == 0 55 | 56 | def test_CSRRowSlice(): 57 | A = scipy.sparse.rand(100, 100, density=0.05, format='csr') 58 | x = rand(100) 59 | assert np.allclose((A @ x)[12:23], CSRRowSlice(A, (12, 23)).dot(x)) 60 | x = rand(100, 7) 61 | assert np.allclose((A @ x)[12:23], CSRRowSlice(A, (12, 23)).dot(x)) 62 | # 63 | rows = np.array([1, 3, 10, 11, 12, 65]) 64 | x = rand(100) 65 | assert np.allclose((A @ x)[rows], CSRRowSubset(A, rows).dot(x)) 66 | -------------------------------------------------------------------------------- /test/test_vform.py: -------------------------------------------------------------------------------- 1 | from pyiga.vform import * 2 | 3 | def test_arithmetic(): 4 | vf = VForm(2) 5 | u, v = vf.basisfuns() 6 | f, g = vf.input('f'), vf.input('g') 7 | assert (f + g).shape == () 8 | assert (f - g).shape == () 9 | assert (f * g).shape == () 10 | assert (f / g).shape == () 11 | assert (f + 2).shape == () 12 | assert (f - 2).shape == () 13 | assert (f * 2).shape == () 14 | assert (f / 2).shape == () 15 | assert (3 + g).shape == () 16 | assert (3 - g).shape == () 17 | assert (3 * g).shape == () 18 | assert (3 / g).shape == () 19 | assert (3 * grad(u)).shape == (2,) 20 | assert (grad(v) / 3).shape == (2,) 21 | exprs_equal(f**1, f) 22 | exprs_equal(f**2, f*f) 23 | exprs_equal(f**as_expr(3), f*f*f) 24 | exprs_equal(f**0, as_expr(1)) 25 | exprs_equal(f**-1, 1.0 / f) 26 | exprs_equal(f**-2, 1.0 / (f*f)) 27 | 28 | def test_asvector(): 29 | vf = VForm(2) 30 | G = as_vector([1,2,3]) 31 | assert G.shape == (3,) 32 | G = as_vector(vf.Geo) 33 | assert G.shape == (2,) 34 | G = as_vector(2 * vf.Geo) 35 | assert G.shape == (2,) 36 | 37 | def test_asmatrix(): 38 | vf = VForm(2) 39 | G = as_matrix([[1,2,3],[4,5,6]]) 40 | assert G.shape == (2,3) 41 | G = as_matrix(grad(vf.Geo, parametric=True)) 42 | assert G.shape == (2,2) 43 | G = as_matrix(2 * grad(vf.Geo, parametric=True)) 44 | assert G.shape == (2,2) 45 | 46 | def exprs_equal(expr1, expr2, simplify=False): 47 | if simplify: 48 | from pyiga.vform import _to_literal_vec_mat 49 | def simpl(expr): 50 | expr = transform_expr(expr, _to_literal_vec_mat) 51 | expr = transform_expr(expr, lambda e: e.fold_constants()) 52 | return expr 53 | expr1 = simpl(expr1) 54 | expr2 = simpl(expr2) 55 | 56 | h1, h2 = exprhash(expr1), exprhash(expr2) 57 | if h1 != h2: 58 | print('Expression 1:') 59 | tree_print(expr1) 60 | print('Expression 2:') 61 | tree_print(expr2) 62 | assert h1 == h2 63 | 64 | def test_dx(): 65 | vf = VForm(2) 66 | u, v = vf.basisfuns() 67 | exprs_equal(u.dx(0).dx(1), u.dx(1).dx(0)) 68 | exprs_equal(vf.Geo.dx(1, parametric=True)[0], vf.Geo[0].dx(1, parametric=True)) 69 | G = vf.let('G', vf.Geo) 70 | exprs_equal(G.dx(0, parametric=True)[1], G[1].dx(0, parametric=True)) 71 | 72 | def test_vectorexpr(): 73 | vf = VForm(3) 74 | u, v = vf.basisfuns(components=(3,3)) 75 | A = vf.input('A', shape=(3,3)) 76 | assert inner(u, v).shape == () 77 | assert cross(u, v).shape == (3,) 78 | assert outer(u, v).shape == (3, 3) 79 | assert A.dot(u).shape == (3,) 80 | x = (1, 2, 3) 81 | assert inner(x, v).shape == () 82 | assert cross(x, v).shape == (3,) 83 | assert outer(x, v).shape == (3, 3) 84 | assert A.dot(x).shape == (3,) 85 | 86 | def test_basisderivs(): 87 | # scalar basis functions 88 | vf = VForm(3, arity=1) 89 | u = vf.basisfuns() 90 | assert grad(u).shape == (3,) 91 | 92 | # vector basis functions 93 | vf = VForm(3, arity=1) 94 | u = vf.basisfuns(components=(3,)) 95 | assert grad(u).shape == (3,3) 96 | assert grad(u, dims=(1,2)).shape == (3,2) 97 | assert div(u).shape == () 98 | assert curl(u).shape == (3,) 99 | 100 | def test_input(): 101 | vf = VForm(3, arity=1) 102 | f = vf.input('f') 103 | g = vf.input('g', shape=(3,)) 104 | G = vf.input('G', shape=(3,3)) 105 | assert f.shape == () 106 | assert g.shape == (3,) 107 | assert G.shape == (3,3) 108 | # expressions with parametric derivatives 109 | assert grad(f, parametric=True).shape == (3,) 110 | assert grad(g, parametric=True).shape == (3,3) 111 | assert grad(f, dims=(1,2), parametric=True).shape == (2,) 112 | exprs_equal(grad(f, dims=(1,2), parametric=True)[0], Dx(f, 1, parametric=True)) 113 | assert grad(g, dims=(1,2), parametric=True).shape == (3,2) 114 | exprs_equal(grad(g, dims=(1,2), parametric=True)[1,0], Dx(g[1], 1, parametric=True)) 115 | exprs_equal(grad(g, parametric=True)[1, :], grad(g[1], parametric=True)) 116 | # expressions with physical derivatives 117 | assert grad(f).shape == (3,) 118 | assert grad(g).shape == (3,3) 119 | assert grad(f, dims=(1,2)).shape == (2,) 120 | exprs_equal(grad(f, dims=(1,2))[0], Dx(f, 1)) 121 | assert grad(g, dims=(1,2)).shape == (3,2) 122 | exprs_equal(grad(g, dims=(1,2))[1,0], Dx(g[1], 1)) 123 | exprs_equal(grad(g)[1, :], grad(g[1])) 124 | 125 | def test_input_physical(): 126 | vf = VForm(3, arity=1) 127 | f = vf.input('f', physical=True) 128 | g = vf.input('g', shape=(3,), physical=True) 129 | G = vf.input('G', shape=(3,3), physical=True) 130 | assert f.shape == () 131 | assert g.shape == (3,) 132 | assert G.shape == (3,3) 133 | # expressions with physical derivatives 134 | assert grad(f).shape == (3,) 135 | exprs_equal(grad(f)[1:], grad(f, dims=[1,2])) 136 | assert grad(g).shape == (3,3) 137 | assert grad(f, dims=(1,2)).shape == (2,) 138 | exprs_equal(grad(f, dims=(1,2))[0], Dx(f, 1)) 139 | assert grad(g, dims=(1,2)).shape == (3,2) 140 | exprs_equal(grad(g, dims=(1,2))[1,0], Dx(g[1], 1)) 141 | exprs_equal(grad(g)[1, :], grad(g[1])) 142 | 143 | def test_parameter(): 144 | vf = VForm(2, arity=1) 145 | u = vf.basisfuns() 146 | a = vf.parameter('a') 147 | assert a.shape == () 148 | B = vf.parameter('B', (2, 3)) 149 | assert B.shape == (2, 3) 150 | # 151 | assert Dx(a, 1).shape == () 152 | exprs_equal(Dx(B[1, 2] * u, 1), B[1, 2] * Dx(u, 1), simplify=True) 153 | 154 | def test_symderiv(): 155 | vf = VForm(3, arity=1) 156 | u = vf.basisfuns() 157 | f = vf.input('f') 158 | G = vf.input('G', shape=(3,)) 159 | exprs_equal(grad(2 * f, parametric=True), 2 * grad(f, parametric=True), simplify=True) 160 | exprs_equal(div(G - 3, parametric=True), div(G, parametric=True), simplify=True) 161 | exprs_equal((f * u).dx(0, parametric=True), f.dx(0, parametric=True)*u + f*u.dx(0, parametric=True)) 162 | exprs_equal((1 / f).dx(1, parametric=True), -f.dx(1, parametric=True) / (f*f), simplify=True) 163 | exprs_equal(curl(2 + grad(u)), curl(grad(u)), simplify=True) 164 | 165 | def test_hess(): 166 | vf = VForm(3, arity=1) 167 | f = vf.input('f') 168 | v = vf.basisfuns() 169 | Hp = hess(f, parametric=True) 170 | assert Hp.shape == (3,3) 171 | exprs_equal(Hp[0,1], Dx(Dx(f, 0, parametric=True), 1, parametric=True)) 172 | exprs_equal(Hp[2,0], Dx(Dx(f, 0, parametric=True), 2, parametric=True)) 173 | H = hess(f) 174 | assert H.shape == (3,3) 175 | exprs_equal(H[1,1], Dx(Dx(f, 1), 1)) 176 | exprs_equal(H[1,2], Dx(Dx(f, 2), 1)) 177 | # test finalize 178 | vf.add(inner(H, H) * v * dx) 179 | vf.finalize() 180 | 181 | def test_tostring(): 182 | vf = VForm(2) 183 | u, v = vf.basisfuns() 184 | f = vf.input('f') 185 | g = vf.input('g', shape=(2,)) 186 | B = vf.input('B', shape=(2,2)) 187 | assert str(f) == 'f_a' # implementation detail of current generators 188 | assert str(-f) == '-f_a' 189 | assert str(cos(f)) == 'cos(f_a)' 190 | assert str(g[1]) == 'g_a[1]' 191 | tmp = f + 1 192 | assert str(tmp) == '+(f_a, 1.0)' 193 | tmp1 = vf.let('tmp1', tmp) 194 | assert str(tmp1) == 'tmp1' 195 | vec = as_vector((2, 3)) 196 | assert str(vec) == '(2.0, 3.0)' 197 | mat = as_matrix(((1,2),(3,4))) 198 | assert str(mat) == '((1.0, 2.0),\n (3.0, 4.0))' 199 | assert str(u) == 'u' 200 | assert str(Dx(u, 1)) == 'u_01(phys)' 201 | assert str(B[1,0]) == 'B_a[1,0]' 202 | assert str(B.dot(g)) == 'dot(((B_a[0,0], B_a[0,1]),\n (B_a[1,0], B_a[1,1])), (g_a[0], g_a[1]))' 203 | assert str(B.dot(B)) == 'dot(((B_a[0,0], B_a[0,1]),\n (B_a[1,0], B_a[1,1])), ((B_a[0,0], B_a[0,1]),\n (B_a[1,0], B_a[1,1])))' 204 | 205 | tree_print(tmp) 206 | 207 | def test_surface(): 208 | vf = VForm(1, geo_dim=2) 209 | assert (vf.dim, vf.geo_dim) == (1, 2) 210 | assert vf.normal.shape == (2,) 211 | assert vf.SW.shape == () # check surface weight 212 | 213 | vf = VForm(2, geo_dim=3) 214 | assert (vf.dim, vf.geo_dim) == (2, 3) 215 | assert vf.normal.shape == (3,) 216 | assert vf.SW.shape == () # check surface weight 217 | 218 | def test_parse(): 219 | from pyiga import bspline, geometry 220 | kvs = 2 * (bspline.make_knots(2, 0.0, 1.0, 5),) 221 | 222 | vf = parse_vf('u * v * dx', kvs, bfuns=[('u', 1), ('v', 1)]) 223 | assert vf.hash() == mass_vf(2).hash() 224 | 225 | f = bspline.BSplineFunc(kvs, np.ones(bspline.numdofs(kvs))) 226 | vf = parse_vf('f * v * dx', kvs, {'f': f}) 227 | assert vf.hash() == L2functional_vf(2, physical=False).hash() 228 | 229 | f = lambda x, y: 1.0 230 | vf = parse_vf('f * v * dx', kvs, {'f': f}) 231 | assert vf.hash() == L2functional_vf(2, physical=True).hash() 232 | 233 | vf = parse_vf('div(u) * div(v) * dx', kvs, bfuns=[('u', 2), ('v', 2)]) 234 | assert vf.hash() == divdiv_vf(2).hash() 235 | 236 | # some other features 237 | vf = parse_vf('f * v * ds', kvs[:1], {'f': geometry.circular_arc(1.4)[0]}) 238 | 239 | vf = parse_vf('inner(f, v) * ds', kvs, bfuns=[('v',2)], args={'f': lambda x, y: (-y, x)}) 240 | 241 | def test_sym_index(): 242 | n = 4 243 | idx = [[ sym_index_to_seq(n, i, j) for j in range(n) ] 244 | for i in range(n) ] 245 | assert np.array_equal(idx, 246 | [[0, 1, 2, 3], 247 | [1, 4, 5, 6], 248 | [2, 5, 7, 8], 249 | [3, 6, 8, 9]]) 250 | -------------------------------------------------------------------------------- /test/test_vis.py: -------------------------------------------------------------------------------- 1 | from pyiga.vis import * 2 | 3 | from pyiga import bspline, geometry, approx, hierarchical 4 | import numpy as np 5 | 6 | # We don't really "test" the vis functions at the moment but just 7 | # run them to make sure they aren't dead code. 8 | 9 | def test_plot_field(): 10 | def f(x, y): return np.sin(x) * np.exp(y) 11 | geo = geometry.quarter_annulus() 12 | plot_field(f, physical=True, geo=geo, res=10) 13 | # 14 | kvs = 2 * (bspline.make_knots(2, 0.0, 1.0, 5),) 15 | u = bspline.BSplineFunc(kvs, approx.interpolate(kvs, f)) 16 | plot_field(u, res=10) 17 | plot_field(u, geo=geo, res=10) 18 | 19 | def test_plot_geo(): 20 | plot_geo(geometry.line_segment([0,1], [1,2])) 21 | plot_geo(geometry.quarter_annulus(), res=10) 22 | 23 | def test_animate_field(): 24 | kvs = 2 * (bspline.make_knots(2, 0.0, 1.0, 5),) 25 | fields = [ bspline.BSplineFunc(kvs, 26 | approx.interpolate(kvs, lambda x,y: np.sin(t+x) * np.exp(y))) 27 | for t in range(3) ] 28 | anim = animate_field(fields, geo=geometry.bspline_quarter_annulus(), res=10) 29 | anim.to_jshtml() 30 | 31 | from .test_hierarchical import create_example_hspace 32 | 33 | def test_plot_hierarchical_mesh(): 34 | hs = create_example_hspace(p=3, dim=2, n0=4, disparity=1, num_levels=3) 35 | plot_hierarchical_mesh(hs, levelwise=False) 36 | plot_hierarchical_mesh(hs, levelwise=True) 37 | 38 | def test_plot_hierarchical_cells(): 39 | hs = create_example_hspace(p=3, dim=2, n0=4, disparity=1, num_levels=3) 40 | cells = hs.compute_supports(hs.cell_supp_indices()[-1]) 41 | plot_hierarchical_cells(hs, cells) 42 | 43 | def test_plot_active_cells(): 44 | hs = create_example_hspace(p=3, dim=2, n0=4, disparity=1, num_levels=3) 45 | data = 7.0 * np.arange(hs.total_active_cells) 46 | plot_active_cells(hs, data) 47 | --------------------------------------------------------------------------------