diff --git a/.gitignore b/.gitignore index 0481020f..d0824f77 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ docs/_build/ _autosummaries/ docs/source/opinf/literature.md +# Sphinx built documentation +sphinx-docs/build + # Extra Python files *.py[cod] __pycache__/ diff --git a/Makefile b/Makefile index fcbc055a..7048ac47 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help clean_docs install dev format style test docs deploy_package deploy_docs +.PHONY: help clean_docs install dev format style test docs deploy_package deploy_docs sphinx_docs REMOVE = rm -rfv @@ -54,6 +54,9 @@ test: style docs: $(TOX) -e literature,docs +sphinx_docs: + $(TOX) -e sphinx-docs + all: test docs diff --git a/sphinx-docs/bib2md.py b/sphinx-docs/bib2md.py new file mode 100644 index 00000000..67a14a45 --- /dev/null +++ b/sphinx-docs/bib2md.py @@ -0,0 +1,374 @@ +# bib2md.py +"""Generate the markdown Literature page from bibtex. + +Instructions for updating the Literature page: + +1. Add BibTeX entries to `docs/literature.bib`. + - Keep the entries sorted by year, then by author last name. + - Authors should be listed with first-then-last names and separated with + "and": `authors = {{First1 Last1 and First2 Last2}},`. + Enclose multi-word names in braces, for example, + `authors = {{Vincent {{van Gogh}} and Balthasar {{van der Pol}}}},` + - Include a "doi" field if applicable. + - Add a "category" field to indicate which section the reference should be + listed under on this page. + +2. Add the Google Scholar IDs of each author who has one to the `scholarIDS` + dictionary in `docs/bib2md.py`. This is the unique part of a Google Scholar + profile page url: + `https://scholar.google.com/citations?user=&hl=en` + +Note that `docs/literature.bib` is generated automatically from this file and +is not tracked by git. +""" + +import re +import collections + +import bibtexparser +import bibtexparser.middlewares as bm + + +# Configuration =============================================================== + +# Categories to group the references by. +# These are the options for the `categories` field in literature.bib entries. +categories = { + "origin": "## Original Paper", + "survey": "## Surveys", + "nonlinear": "## Methodology\n### Lifting and Nonlinearity", + "reprojection": "### Re-projection", + "structure": "### Structure Preservation", + "parametric": "### Parametric Problems", + "statistical": "### Statistical Methods", + "domaindecomp": "### Domain Decomposition", + "manifolds": "### Nonlinear Manifolds", + "scalable": "### Scalability", + "application": "## Applications", + "thesis": "## Dissertations and Theses", +} + +# Extra text to place under the heading before continuing. +details = { + "nonlinear": """Operator Inference learns reduced-order models with +polynomial structure. The methods developed in the following papers focus on +dealing with non-polynomial nonlinearities through variable transformations +(lifting) and/or coupling Operator Inference methods with other approximation +strategies.""", + "reprojection": """In some cases, if the training data are chosen +judiciously, Operator Inference can recover traditional reduced-order models +defined by intrusive projection. The following papers develop and apply this +idea.""", + "structure": """The methods developed in these works augment Operator +Inference so that the resulting reduced-order models automatically inherit +certain properties from the full-order system, such as block structure, +symmetries, energy conservation, gradient structure, and more.""", + "parametric": """Many systems depend on independent parameters that +describe material properties or other physical characteristics of the +phenomenon being modeled. The following papers develop Operator Inference +approaches that are specifically designed for parametric problems.""", + "statistical": """These papers focus on problems with noisy or missing +data, stochastic systems, and methods for constructing probabilistic +reduced-order models with Operator Inference.""", + "domaindecomp": """The methods in the following papers focus on scalability +and accuracy improvements by decomposition spatial or latent space domains and +learning a coupled system of reduced-order models.""", + "manifolds": """Traditional model reduction methods approximate the +high-dimensional system state with a low-dimensional linear (or affine) +representation. The methods in these papers explore using nonlinear +low-dimensional representations in the context of Operator Inference.""", + "scalable": """These works focus on the computational challenge of applying +Operator Inference to large-scale problems.""", +} + +# Author citation IDs (https://scholar.google.com/citation?user=) +scholarIDS = { + "nicole_aretz": "Oje7mbAAAAAJ", + "anthony_ashley": "9KFAXLYAAAAJ", + "sean_babiniec": "xcSVh00AAAAJ", + "laura_balzano": "X6fRNfUAAAAJ", + "peter_benner": "6zcRrC4AAAAJ", + "patrick_blonigan": "lOmH5XcAAAAJ", + "anirban_chaudhuri": "oGL9YJIAAAAJ", + "pascal_denboef": "vFlzL7kAAAAJ", + "igor_duff": "OAkPFdkAAAAJ", + "melina_freitag": "iE4t4WcAAAAJ", + "ionut-gabriel_farcas": "Cts5ePIAAAAJ", + "rudy_geelen": "vBzKRMsAAAAJ", + "yuwei_geng": "lms4MbwAAAAJ", + "omar_ghattas": "A5vhsIYAAAAJ", + "leonidas_gkimisis": "0GzUUzMAAAAJ", + "marcos_gomes": "s6mocWAAAAAJ", + "pawan_goyal": "9rEfaRwAAAAJ", + "anthony_gruber": "CJVuqfoAAAAJ", + "mengwu_guo": "eON6MykAAAAJ", + "dirk_hartmann": "4XvBneEAAAAJ", + "jan_heiland": "wkHSeoYAAAAJ", + "cheng_huang": "lUXijaQAAAAJ", + "opal_issan": "eEIe19oAAAAJ", + "lili_ju": "JkKUWoAAAAAJ", + "bülent_karasözen": "R906kj0AAAAJ", + "parisa_khodabakhshi": "lYr_g-MAAAAJ", + "hyeonghun_kim": "sdR-LZ4AAAAJ", + "tomoki_koike": "HFoIGcMAAAAJ", + "boris_kramer": "yfmbPNoAAAAJ", + "diana_manvelyan": "V0k8Xb4AAAAJ", + "jan_nicolaus": "47DRMUwAAAAJ", + "joseph_maubach": "nBRKw6cAAAAJ", + "alexandre_marquez": "p9zb2Y0AAAAJ", + "shane_mcquarrie": "qQ6JDJ4AAAAJ", + "jonathan_murray": "NScAg7AAAAAJ", + "david_najera-flores": "HJ-Dfl8AAAAJ", + "alberto_nogueira": "66DEy5wAAAAJ", + "benjamin_peherstorfer": "C81WhlkAAAAJ", + "arthur_pires": "qIUw-GEAAAAJ", + "gleb_pogudin": "C5NP1o0AAAAJ", + "elizabeth_qian": "jnHI7wQAAAAJ", + "thomas_richter": "C8R6xtMAAAAJ", + "wil_schilders": "UGKPyqkAAAAJ", + "harsh_sharma": "Pb-tL5oAAAAJ", + "jasdeep_singh": "VcmXMxgAAAAJ", + "renee_swischuk": "L9D0LBsAAAAJ", + "john_tencer": "M6AwtC4AAAAJ", + "irina_tezaur": "Q3fx78kAAAAJ", + "marco_tezzele": "UPcyNXIAAAAJ", + "michael_todd": "jzY8TSkAAAAJ", + "michael_tolley": "0kOHVOkAAAAJ", + "wayne_uy": "hNN_KRQAAAAJ", + "nathan_vandewouw": "pcQCbN8AAAAJ", + "arjun_vijaywargiya": "_fcSwDYAAAAJ", + "zhu_wang": "jkmwEF0AAAAJ", + "yuxiao_wen": "uXJoQCAAAAAJ", + "karen_willcox": "axvGyXoAAAAJ", + "stephen_wright": "VFQRIOwAAAAJ", + "süleyman_yıldız": "UVPD79MAAAAJ", + "benjamin_zastrow": "ODLjrBAAAAAJ", +} + +# LaTeX special characters to convert for the markdown version. +specialChars = ( + (r"\~{a}", "ã"), + (r"\'{e}", "é"), + (r"\i{}", "ı"), + (r"\"{o}", "ö"), + (r"\"{u}", "ü"), +) + +# Text before the list of references begins. +HEADER = r"""# Literature + +This page lists scholarly publications that develop, extend, or apply +Operator Inference, categorized into topics and sorted by publication year, +then by the last name of the first author. Although some could be placed in +multiple categories, each publication is only listed once. + +:::{admonition} Share Your Work! +:class: hint + +Don't see your publication? +[**Click here**](https://forms.gle/BgZK4b4DfuaPsGFd7) +to submit a request to add entries to this page. +::: +""" + +# Text after the list of references. +FOOTER = r"""## BibTex File + +:::{{admonition}} Sorted alphabetically by author +:class: dropdown seealso + +```bibtex +{} +``` +::: + +:::{{admonition}} Sorted by year then alphabetically by author +:class: dropdown seealso + +```bibtex +{} +``` +::: +""" + + +# Helper functions ============================================================ +class TrimMiddleware(bm.BlockMiddleware): + """Trim out a few fields when writing the bibtex file.""" + + def transform_entry(self, entry, *args, **kwargs): + for field in "category", "url": + if field in entry: + entry.pop(field) + return entry + + +def clean_name(name): + r"""Handle special LaTex characters in names: + \~{a} -> ã + \'{e} -> é + \i{} -> ı + \"{o} -> ö + \"{u} -> ü + """ + for before, after in specialChars: + name = name.replace(before, after) + return name + + +def clean_title(title): + r"""Remove extra braces, etc. from paper titles, + e.g.,"{B}\'{e}nard" -> Bénard. + """ + return re.subn(r"\{(\w+?)\}", r"\1", clean_name(title))[0] + + +def linkedname(author): + """Get the string of the form "First, Last" with a link to their + Google Scholar page if possible. + """ + # Separate names, keeping {} groups together. + names = [ + match[1:-1] if match.startswith("{") and match.endswith("}") else match + for match in re.findall(r"\{[^}]*\}|\S+", author) + ] + + # Extract first and last names and initials. + firstname = clean_name(names[0]).lower() + if names[-1] == "Jr": + lastname = clean_name(names[-2]) + initials = " ".join([name[0] + "." for name in names[:-2]]) + key = f"{firstname}_{lastname.replace(' ', '').lower()}" + lastname = f"{lastname} Jr." + else: + lastname = clean_name(names[-1]) + initials = " ".join([name[0] + "." for name in names[:-1]]) + key = f"{firstname}_{lastname.replace(' ', '').lower()}" + + # Get the Google Scholar link if possible. + if key in scholarIDS: + gsID = scholarIDS[key] + gsURL = f"https://scholar.google.com/citations?user={gsID}" # &hl=en + return f"[{initials} {lastname}]({gsURL})" + + return f"{initials} {lastname}" + + +def entry2txt(bibraw): + """Convert the bibtexparser entry to a printable string.""" + txt = f"@{bibraw.entry_type}{{{bibraw.key},\n" + for field in bibraw.fields: + if (key := field.key) == "category": + continue + elif key == "url" and "doi" in bibraw: + continue + txt = f"{txt}  {key} = {{{bibraw[key]}}},\n" + txt = f"{txt} }}".replace("\\", "\\\\") + return txt.replace('\\\\"', '\\\\\\\\"') + + +# Main routine ================================================================ +def main(bibfile, mdfile): + """Convert a BibTex file to Markdown.""" + + library = bibtexparser.parse_file(bibfile) + sectiontxt = collections.defaultdict(list) + + for entry in sorted( + library.entries, + key=lambda x: x["year"], + reverse=False, + ): + # Parse authors. + authors = [] + for author in entry["author"].split(" and "): + author = author.strip() + if "," in author: + raise ValueError( + f"change {bibfile} to avoid ',' in author '{author}'" + ) + authors.append(linkedname(author)) + + if len(authors) == 0: + raise ValueError("empty author field") + if len(authors) == 1: + authortxt = f"{authors[0]}" + elif len(authors) == 2: + authortxt = f"{authors[0]} and {authors[1]}" + else: + authortxt = ", ".join(authors[:-1]) + f", and {authors[-1]}" + + # Parse paper title. + title = clean_title(entry["title"]) + if "url" in entry: + titletxt = f"* [**{title}**]({entry['url']})" + elif "doi" in entry: + titletxt = f"* [**{title}**](https://doi.org/{entry['doi']})" + else: + titletxt = f"* **{title}**" + + # Parse journal and year. + if "journal" in entry: + publication = entry["journal"] + elif "booktitle" in entry: + publication = entry["booktitle"] + elif entry["category"] == "thesis" and "school" in entry: + if entry.entry_type == "phdthesis": + publication = "PhD Thesis, " + entry["school"] + elif entry.entry_type == "mastersthesis": + publication = "Master's Thesis, " + entry["school"] + else: + raise ValueError("could not identify publication") + else: + raise ValueError("could not identify publication") + + citetxt = ( + f"{publication}, {entry['year']} " + f"
BibTeX
{entry2txt(entry)}"
+            f"
" + ) + + # Combine parsed data. + cat = "other" + if "category" in entry: + cat = entry["category"] + sectiontxt[cat].append(" \n ".join([titletxt, authortxt, citetxt])) + + formatter = bibtexparser.BibtexFormat() + formatter.indent = " " + formatter.trailing_comma = True + formatter.block_separator = "\n" + + with open(mdfile, "w") as outfile: + outfile.write(HEADER) + for cat in categories: + if cat not in sectiontxt: + continue + text = f"\n{categories[cat]}\n\n" + if cat in details: + text = f"{text}{details[cat]}\n\n" + outfile.write(text) + outfile.write("\n

\n".join(sectiontxt[cat]) + "\n") + + footer = FOOTER.format( + bibtexparser.write_string( # Sorted by 1st author last name. + bibtexparser.parse_file( + bibfile, + append_middleware=[bm.SortBlocksByTypeAndKeyMiddleware()], + ), + bibtex_format=formatter, + prepend_middleware=[TrimMiddleware()], + ), + bibtexparser.write_string( # Sorted by year, then author. + library, + bibtex_format=formatter, + prepend_middleware=[TrimMiddleware()], + ), + ) + + outfile.write(footer) + + +# ============================================================================= +if __name__ == "__main__": + main("source/literature.bib", "source/opinf/literature.md") diff --git a/sphinx-docs/source/_static/basis-projection.svg b/sphinx-docs/source/_static/basis-projection.svg new file mode 100644 index 00000000..9bdd7dfa --- /dev/null +++ b/sphinx-docs/source/_static/basis-projection.svg @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx-docs/source/_static/dimension-reduction.svg b/sphinx-docs/source/_static/dimension-reduction.svg new file mode 100644 index 00000000..5e267276 --- /dev/null +++ b/sphinx-docs/source/_static/dimension-reduction.svg @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx-docs/source/_static/favicon.svg b/sphinx-docs/source/_static/favicon.svg new file mode 100644 index 00000000..d85ef81c --- /dev/null +++ b/sphinx-docs/source/_static/favicon.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx-docs/source/_static/logo.svg b/sphinx-docs/source/_static/logo.svg new file mode 100644 index 00000000..7f21e175 --- /dev/null +++ b/sphinx-docs/source/_static/logo.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx-docs/source/_static/summary.svg b/sphinx-docs/source/_static/summary.svg new file mode 100644 index 00000000..9157d7dd --- /dev/null +++ b/sphinx-docs/source/_static/summary.svg @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx-docs/source/_templates/autosummary/class.rst b/sphinx-docs/source/_templates/autosummary/class.rst new file mode 100644 index 00000000..e564d23e --- /dev/null +++ b/sphinx-docs/source/_templates/autosummary/class.rst @@ -0,0 +1,45 @@ +{{ objname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ fullname }} + :no-members: + :no-inherited-members: + :no-special-members: + + {% block properties %} + {% if attributes %} + + .. raw:: html + +
+ Properties: +
+ + {% for item in all_attributes %} + {%- if not item.startswith('_') %} + .. autoattribute:: {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block methods %} + {% if methods %} + + .. raw:: html + +
+ Methods: +
+ + .. autosummary:: + :toctree: + :nosignatures: + {% for item in all_methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/sphinx-docs/source/_templates/autosummary/function.rst b/sphinx-docs/source/_templates/autosummary/function.rst new file mode 100644 index 00000000..d4941d5c --- /dev/null +++ b/sphinx-docs/source/_templates/autosummary/function.rst @@ -0,0 +1,8 @@ +:orphan: + +{{ name }}\(\) +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ fullname }} diff --git a/sphinx-docs/source/_templates/autosummary/method.rst b/sphinx-docs/source/_templates/autosummary/method.rst new file mode 100644 index 00000000..3ae78cf1 --- /dev/null +++ b/sphinx-docs/source/_templates/autosummary/method.rst @@ -0,0 +1,8 @@ +:orphan: + +{{ name }}\(\) +{{ underline }} + +.. currentmodule:: {{ module }} + +.. automethod:: {{ fullname }} diff --git a/sphinx-docs/source/_templates/autosummary/module.rst b/sphinx-docs/source/_templates/autosummary/module.rst new file mode 100644 index 00000000..166d2e75 --- /dev/null +++ b/sphinx-docs/source/_templates/autosummary/module.rst @@ -0,0 +1,56 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + + **Module Attributes** + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + + **Classes** + + .. autosummary:: + :toctree: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + + {% block functions %} + {% if functions %} + + **Functions** + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + + **Exceptions** + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/sphinx-docs/source/api/basis.md b/sphinx-docs/source/api/basis.md new file mode 100644 index 00000000..d239d080 --- /dev/null +++ b/sphinx-docs/source/api/basis.md @@ -0,0 +1,977 @@ +--- +kernelspec: + display_name: opinf + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.basis` + ++++ + +```{eval-rst} +.. automodule:: opinf.basis + +.. currentmodule:: opinf.basis + +**Classes** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + BasisTemplate + BasisMulti + LinearBasis + PODBasis + +**Functions** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + pod_basis + method_of_snapshots + cumulative_energy + residual_energy + svdval_decay +``` + ++++ + +:::{admonition} Overview +:class: note + +- `opinf.basis` classes define mappings between high-dimensional states and low-dimensional latent coordinates. +- The most common low-dimensional state approximation for Operator Inference is [Proper Orthogonal Decomposition](sec:api-basis-pod). +- [Multivariable data](sec:api-basis-multivar) can be treated jointly (monolithic) or variable by variable (multilithic). +::: + ++++ + +:::{admonition} Example Data +:class: tip + +Some examples on this page use data from a compressible flow problem for an ideal gas as described in {cite}`qian2020liftandlearn,guo2022bayesopinf`. + +:::{dropdown} State Variables + +The data consists of three variables recorded at $k = 200$ points in time: + +- Velocity $v$, +- Pressure $p$, and +- Specific volume (inverse density) $\xi = 1/\rho$. + +The spatial discretization results in $400$ degrees of freedom per variable, i.e., $n = 3 \times 400 = 1{,}200$. +::: + +You can [download the data here](https://github.com/Willcox-Research-Group/rom-operator-inference-Python3/raw/data/basis_example.npy) to repeat the demonstration. +::: + +::: +```{code-cell} ipython3 +import warnings +import numpy as np +import scipy.linalg as la +import matplotlib.pyplot as plt + +import opinf + +# Suppress some warning printouts. +warnings.showwarning = lambda msg, cat, *args: print(f"{cat.__name__}: {msg}") +opinf.utils.mpl_config() +``` +::: + +## Low-dimensional Approximations + ++++ + +Reduced-order models enact a computational speedup by approximating the $n$-dimensional system state with $r \ll n$ degrees of freedom and working in the reduced state space. +The {mod}`opinf.basis` module provides tools for constructing low-dimensional state approximations. + ++++ + +::::{admonition} Notation +:class: note + +On this page, +- $\q \in \RR^n$ denotes the high-dimensional state vector to be reduced (the _full state_), +- $\qhat\in\RR^r$ is the low-dimensional, compressed version of $\q$ (the _reduced state_), +- $\breve{\q}\in\RR^n$ is the high-dimensional, decompressed version of $\qhat$ (the _reconstructed state_), +- $\boldsymbol{\Gamma}:\RR^r\to\RR^n$ is the decompression function defining the approximation of $\q$ with $\qhat$ (sometimes called the _decoder_), and +- $\boldsymbol{\Gamma}^{*}:\RR^n\to\RR^r$ is the compression function corresponding to $\boldsymbol{\Gamma}$ (sometimes called the _encoder_). + +That is, $\qhat = \boldsymbol{\Gamma}^{*}(\q)$ and $\q \approx \breve{\q} = \boldsymbol{\Gamma}(\qhat)$. + +:::{image} ../../_static/dimension-reduction.svg +:align: center +:width: 60 % +::: + +We assume that the full state $\q$ has already been preprocessed, for instance with the tools described in {mod}`opinf.pre`. +:::: + ++++ + +For a given $\boldsymbol{\Gamma}$ and norm $\|\cdot\|$, the natural compression function $\boldsymbol{\Gamma}^{*}$ is the one that minimizes the approximation error induced by $\boldsymbol{\Gamma}$: + +$$ +\begin{aligned} + \boldsymbol{\Gamma}^{*}(\q) + = \underset{\qhat\in\RR^{r}}{\operatorname{argmin}}\left\| + \q - \boldsymbol{\boldsymbol{\Gamma}}(\qhat) + \right\|. +\end{aligned} +$$ + +::::{margin} +:::{admonition} Compression vs Projection +:class: warning + +In the literature, the term _project_ is sometimes used to refer to applying the compression function $\boldsymbol{\Gamma}^{*}$. +In this package, _project_ always refers to compression followed by decompression. +::: +:::: + +In this case, $P = \boldsymbol{\Gamma} \circ \boldsymbol{\Gamma}^{*}:\RR^n\to\RR^n$ satisfies $P(P(\q)) = P(\q)$ for all $\q\in\RR^{n}$ and $P$ is called a _projection_. +The error between $\breve{\q} = P(\q)$ and $\q$ is called the _projection error_ at $\q$, and $\breve{\q}$ is, by definition, the closest point to $\q$ in $\operatorname{range}(\boldsymbol{\Gamma})$. + +:::{dropdown} Proof +For any $\tilde{\q}\in\RR^r$, we have + +$$ +\begin{aligned} + \boldsymbol{\Gamma}^{*}(\boldsymbol{\Gamma}(\tilde{\q})) + = \underset{\qhat\in\RR^{r}}{\operatorname{argmin}}\left\| + \boldsymbol{\Gamma}(\tilde{\q}) - \boldsymbol{\boldsymbol{\Gamma}}(\qhat) + \right\|. +\end{aligned} +$$ + +By inspection, setting $\qhat = \tilde{\q}$ achieves the minimal value of zero. +Hence, $\boldsymbol{\Gamma}^{*}(\boldsymbol{\Gamma}(\tilde{\q})) = \tilde{\q}$, and we have + +$$ +\begin{aligned} + \boldsymbol{\Gamma}(\boldsymbol{\Gamma}^{*}(\boldsymbol{\Gamma}(\boldsymbol{\Gamma}^{*}(\q)))) + = \boldsymbol{\Gamma}(\boldsymbol{\Gamma}^{*}(\q)). +\end{aligned} +$$ +::: + ++++ + +:::{admonition} Basis API +:class: important + +`opinf.basis` classes are equipped with the following methods. +- [**`fit()`**](opinf.basis.BasisTemplate.fit) uses state data to learn appropriate decompression / compression functions. +- [**`compress()`**](opinf.basis.BasisTemplate.compress) applies the compression function $\boldsymbol{\Gamma}^{*}$. +- [**`decompress()`**](opinf.basis.BasisTemplate.decompress) applies the decompression function $\boldsymbol{\Gamma}$. +- [**`project()`**](opinf.basis.BasisTemplate.project) applies the composition $\boldsymbol{\Gamma}\circ\boldsymbol{\Gamma}^{*}$. +- [**`projection_error()`**](opinf.basis.BasisTemplate.projection_error) computes the relative or absolute error of the projection at a given state. +- [**`verify()`**](opinf.basis.BasisTemplate.verify) checks that composing `decompress()` with `compress()` defines a projection. +::: + ++++ + +## Linear Bases + ++++ + +A linear basis approximates approximates the full state $\q\in\RR^n$ as a linear combination of $r$ _basis vectors_ $\v_1,\ldots,\v_r\in\RR^n$. +Mathematically, this means the reconstructed state $\breve{\q}\in\RR^n$ is contained in the $r$-dimensional linear subspace $\operatorname{span}(\{\v_1,\ldots,\v_r\})\subset\RR^n$. +The reduced state vector $\qhat = [~\hat{q}_1~~\cdots~~\hat{q}_r~]\trp\in\RR^r$ consists of latent coordinate coefficients for each basis vector. + +Gathering the basis vectors into the _basis matrix_ $\Vr = [~\v_1~~\cdots~~\v_r~]\in\RR^{n\times r}$, a linear basis approximation is given by + +$$ +\begin{aligned} + \q + \approx \Vr\qhat + = \sum_{i=1}^{r}\hat{q}_i\v_i. +\end{aligned} +$$ + +If the basis vectors are orthonormal, i.e., $\Vr\trp\Vr = \I\in\RR^{r\times r}$, then the compression function is defined by the application of $\Vr\trp$. + +::::{grid} +:gutter: 3 +:margin: 2 2 0 0 + +:::{grid-item-card} +Compression +^^^ +$\boldsymbol{\Gamma}^{*} : \q \mapsto \Vr\trp\q$ +::: + +:::{grid-item-card} +Decompression +^^^ +$\boldsymbol{\Gamma} : \qhat \mapsto \Vr\qhat$ +::: + +:::{grid-item-card} +Projection +^^^ +$\boldsymbol{\Gamma}\circ\boldsymbol{\Gamma}^{*} : \q \mapsto \Vr\Vr\trp\q$. +::: +:::: + +% This is a test. + +:::{dropdown} Derivation of $~\boldsymbol{\Gamma}^{*}$ +As stated above, the optimal compression function is defined by + +$$ +\begin{aligned} + \boldsymbol{\Gamma}^{*}(\q) + = \underset{\qhat\in\RR^{r}}{\operatorname{argmin}}\left\| + \q - \boldsymbol{\boldsymbol{\Gamma}}(\qhat) + \right\|_2 + = \underset{\qhat\in\RR^{r}}{\operatorname{arg min}}\left\| + \q - \Vr\qhat + \right\|_2^2. +\end{aligned} +$$ + +This is a linear least-squares problem; the solution is given by the normal equations + +$$ +\begin{aligned} + \Vr\trp\Vr\qhat + = \Vr\trp\q, +\end{aligned} +$$ + +which simplifies to $\qhat = \Vr\trp\q$ since $\Vr\trp\Vr = \I$. + +In general, $\qhat = (\Vr\trp\Vr)^{-1}\Vr\q = \Vr^{\dagger}\q$, where $\Vr^{\dagger}$ is the [Moore--Penrose pseudoinverse](https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse) of $\Vr$. +::: + ++++ + +### Known Basis Matrix + ++++ + + +The {class}`LinearBasis` class creates a linear basis from a given, orthonormal basis matrix. + +Unlike the other classes in this module, {meth}`LinearBasis.compress` and {meth}`LinearBasis.decompress` can be used immediately after instantiation because the basis matrix is provided up front instead of learned from state data in {meth}`LinearBasis.fit`. +The basis matrix is accessible via the `entries` attribute or by indexing with `[:]`. + ++++ + +:::{admonition} Non-orthonormal Bases +:class: tip + +{class}`LinearBasis` raises a warning if initialized with a basis matrix $\Vr$ whose columns are not orthonormal. +To avoid this issue, a basis matrix can be orthonormalized (without changing the column span) through the [Gram--Schmidt process](https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process) via, e.g., `scpiy.linalg.qr()`. + +```python +>>> import scipy.linalg as la + +>>> Vr_orth = la.qr(Vr, mode="economic")[0] +``` +::: + ++++ + +The following code uses a {class}`LinearBasis` for a trigonometric basis representing the functions $\sin(2\pi x)$, $\sin(4\pi x)$, ..., $\sin(2r\pi x)$ evaluated over a one-dimensional spatial domain. + +```{code-cell} ipython3 +# Choose full and reduced state dimensions. +n = 500 +r = 8 + +# Define a spatial domain and evaluate the basis functions over that domain. +x = np.linspace(0, 1, n) +Vr = np.column_stack([np.sin(2 * k * np.pi * x) for k in range(1, r + 1)]) + +# Check to see if Vr has orthonormal columns. +np.round(Vr.T @ Vr, 4) +``` + +Because $\Vr\trp\Vr$ is diagonal but not the identify matrix, these basis vectors are orthogonal but do not have unit length. +Using this basis matrix to initialize a {class}`LinearBasis` results in a warning; additionally, calling {meth}`LinearBasis.verify` raises an error ot indicate that composing the decompression and compression functions does not define a projection. + +```{code-cell} ipython3 +trig_basis = opinf.basis.LinearBasis(Vr) + +try: + trig_basis.verify() +except Exception as ex: + print(f"{type(ex).__name__}: {ex}") +``` + +To normalize the basis matrix, we divide each column vector by its norm. + +```{code-cell} ipython3 +# Normalize the columns of the basis matrix. +Vr_orth = Vr / la.norm(Vr, axis=0) +trig_basis_orth = opinf.basis.LinearBasis(Vr_orth) + +# Print dimensions and check that the basis matrix is orthonormal. +print(trig_basis_orth) +trig_basis_orth.verify() + +# Plot the first few basis vectors as functions of x. +trig_basis_orth.plot1D(x, num_vectors=4) +plt.show() +``` + +The next code block checks how well the function $f(x) = x(1 - x)e^x$ can be approximated by this basis. + +```{code-cell} ipython3 +# Represent a new function in the span of the basis. +f = x * (1 - x) * np.exp(x) +f_projected = trig_basis_orth.project(f) + +# Compute the projection error for the new function. +error = trig_basis_orth.projection_error(f) +print(f"Relative projection error: {error:%}") + +# Plot the original function and its projection with this basis. +plt.plot(x, f, label="$f(x)$") +plt.plot(x, f_projected, label="$f(x)$ projected") +plt.legend(loc="upper left") +plt.show() +``` + +The projection is a poor approximation for the original function because $f(x)$ lies well outside of the span of the basis functions $\sin(2\pi x),\ldots,\sin(2r\pi x)$. +A better option is a Fourier basis consisting of cosines as well as sines. + +```{code-cell} ipython3 +# Construct basis vectors with cosines and sines. +Vr_raw = np.column_stack( + [np.cos(2 * k * np.pi * x) for k in range(r // 2)] + + [np.sin(2 * k * np.pi * x) for k in range(1, (r // 2) + 1)] +) + +# Orthonormalize the basis with the Gram-Schmidt process. +Vr_orth = la.qr(Vr_raw, mode="economic")[0] +fourier_basis = opinf.basis.LinearBasis(Vr_orth) + +# Print dimensions and check that the basis matrix is orthonormal. +print(fourier_basis) +fourier_basis.verify() + +# Calculate the projection of f(x) and its relative projection error. +f_projected = fourier_basis.project(f) +error = fourier_basis.projection_error(f) +print(f"Relative projection error: {error:%}") + +# Plot the original function and its projection with this new basis. +plt.plot(x, f, label="$f(x)$") +plt.plot(x, f_projected, label="$f(x)$ projected") +plt.legend(loc="upper left") +plt.show() +``` + +The approximation with the Fourier basis is much better than with the sine basis, even when they have same number of basis vectors. +In general, using more basis vectors improves the approximation power of the basis and decreases projection error. + ++++ + +(sec:api-basis-pod)= +### Proper Orthogonal Decomposition + ++++ + +There is no one basis that works well for every problem. +The {class}`PODBasis` class uses [Proper Orthogonal Decomposition](https://en.wikipedia.org/wiki/Proper_orthogonal_decomposition) (POD), a close relative of [Principal Component Analysis](https://en.wikipedia.org/wiki/Principal_component_analysis) (PCA), to construct a linear basis that is tailored to a provided set of state data. +This is the most common basis used in Operator Inference applications. + ++++ + +#### Definition + ++++ + +In finite dimensions, POD is computed by taking the [singular value decomposition](https://en.wikipedia.org/wiki/Singular_value_decomposition) (SVD) of a snapshot matrix $\Q = [~\q_0~~\cdots~~\q_{k-1}~]\in\RR^{n\times k}$. +If $\boldsymbol{\Phi\Sigma\Psi}\trp = \Q$ is the SVD, then the rank-$r$ POD basis is $\Vr = \boldsymbol{\Phi}_{:,:r}$, the matrix comprising the first $r$ left singular vectors. +Since the singular vectors are orthogonal, we always have $\Vr\trp\Vr = \I$. + +```{code-cell} ipython3 +# Load the example snapshot data. +snapshots = np.load("basis_example.npy") +snapshots.shape +``` + +```{code-cell} ipython3 +# Extract the pressure variable from the snapshot data. +velocity, pressure, sp_volume = np.split(snapshots, 3, axis=0) + +pressure.shape +``` + +```{code-cell} ipython3 +# Construct a POD basis with a specified number of basis vectors. +r = 6 +pressure_basis = opinf.basis.PODBasis(num_vectors=r, name="pressure") +pressure_basis.fit(pressure) +print(pressure_basis) + +# Plot the first few vectors. +pressure_basis.plot1D() +plt.show() +``` + +(sec:api-basis-dimselect)= +#### Selecting the Reduced Dimension + ++++ + + +By default, {meth}`PODBasis.fit` stores all computed basis vectors, and the reduced state dimension $r$ can be changed on the fly with {meth}`PODBasis.set_dimension`. +In this case, we can increase the reduced state dimension up to $k = 200$. +However, we often don't need (or want) all $k$ basis vectors---as $r$ increases so does the computational cost of solving the eventual reduced-order model, and at some point adding additional vectors does not significantly increase the approximation power of the basis. + +The singular values of the snapshot matrix $\Q$ can provide guidance for choosing an appropriate reduced state dimension. +There are four related criteria stemming from the singular values $\sigma_1 > \sigma_2 > \cdots > \sigma_k$. +{class}`PODBasis` has methods for visualizing each criteria, and {meth}`PODBasis.set_dimension` has arguments related to each. + +```{eval-rst} +.. currentmodule:: opinf.basis.PODBasis +``` + +| Criterion | Definition | {meth}`set_dimension` arg | Visualization | +| -------------------: | :--------- |:------------------------- | :------------ | +| Singular value decay | $\sigma_i \ge \xi$ for $i\le r$ | `svdval_threshold` $\xi$ | {meth}`plot_svdval_decay` | +| Cumulative energy | $\frac{\sum_{i=1}^r\sigma_i^2}{\sum_{j=1}^k\sigma_j^2} \ge \kappa$ | `cumulative_energy` $\kappa$ | {meth}`plot_cumulative_energy` | +| Residual energy | $\frac{\sum_{i=r+1}^k\sigma_i^2}{\sum_{j=1}^k\sigma_j^2} \le \epsilon$ | `residual_energy` $\epsilon$ | {meth}`plot_residual_energy` | +| Projection error | $\frac{\\|\Q - \Vr\Vr\trp\Q\\|_F}{\\|\Q\\|_F} \le \rho$ | `projection_error` $\rho$ | {meth}`plot_projection_error` | + +```{eval-rst} +.. currentmodule:: opinf.basis +``` + +These singular value-based selection criteria are all related. +In particular, the squared projection error of the snapshot matrix is equal to the residual energy: + +$$ +\begin{aligned} + \frac{\|\Q - \Vr\Vr\trp\Q\|_{F}^{2}}{\|\Q\|_{F}^{2}} + = \displaystyle\frac{\sum_{j = r + 1}^{\ell}\sigma_{j}^{2}}{\sum_{j=1}^{\ell}\sigma_{j}^{2}}. +\end{aligned} +$$ + +:::{dropdown} Proof +Let $\Q = \boldsymbol{\Phi}\boldsymbol{\Sigma}\boldsymbol{\Psi}\trp$ be the thin singular value decomposition of $\Q\in\RR^{n \times k}$, +meaning $\boldsymbol{\Phi}\in\RR^{n\times \ell}$, $\boldsymbol{\Sigma}\in\RR^{\ell \times \ell}$, and $\boldsymbol{\Psi}\in\RR^{k\times \ell}$, +with $\boldsymbol{\Phi}\trp\boldsymbol{\Phi} = \boldsymbol{\Psi}\trp\boldsymbol{\Psi} = \I$. +Splitting the decomposition into "first $r$ modes" and "remaining modes" gives + +$$ + \Q + = \left[\begin{array}{cc} + \boldsymbol{\Phi}_{r} & \boldsymbol{\Phi}_{\perp} + \end{array}\right] + \left[\begin{array}{cc} + \boldsymbol{\Sigma}_{r} & \\ + & \boldsymbol{\Sigma}_{\perp} + \end{array}\right] + \left[\begin{array}{c} + \boldsymbol{\Psi}_{r}\trp \\ + \boldsymbol{\Psi}_{\perp}\trp + \end{array}\right] + = \underbrace{\boldsymbol{\Phi}_{r}\boldsymbol{\Sigma}_{r}\boldsymbol{\Psi}_{r}\trp}_{\Q_{r}} + \underbrace{\boldsymbol{\Phi}_{\perp}\boldsymbol{\Sigma}_{\perp}\boldsymbol{\Psi}_{\perp}\trp}_{\Q_{\perp}}, +$$ + +where + +\begin{align*} + &\boldsymbol{\Phi}_{r}\in\RR^{n\times r}, + & + &\boldsymbol{\Phi}_{\perp}\in\RR^{n\times (\ell - r)}, + & + &\boldsymbol{\Phi}_{r}\trp\boldsymbol{\Phi}_{r} + = \boldsymbol{\Psi}_{r}\trp\boldsymbol{\Psi}_{r} + = \I, + \\ + &\boldsymbol{\Sigma}_{r}\in\RR^{r\times r}, + & + &\boldsymbol{\Sigma}_{\perp}\in\RR^{(\ell - r)\times (\ell - r)}, + & + &\boldsymbol{\Phi}_{\perp}\trp\boldsymbol{\Phi}_{\perp} + = \boldsymbol{\Psi}_{\perp}\trp\boldsymbol{\Psi}_{\perp} + = \I, + \\ + &\boldsymbol{\Psi}_{r}\in\RR^{k\times r}, + & + &\boldsymbol{\Psi}_{\perp}\in\RR^{k\times (\ell - r)}, + & + &\boldsymbol{\Phi}_{r}\trp\boldsymbol{\Phi}_{\perp} + = \boldsymbol{\Psi}_{r}\trp\boldsymbol{\Psi}_{\perp} + = \mathbf{0}. +\end{align*} + +We have defined $\Vr = \boldsymbol{\Phi}_{r}$. +Using $\Vr\trp\boldsymbol{\Phi}_{r} = \Vr\trp\Vr = \I$ +and $\Vr\trp\boldsymbol{\Phi}_{\perp} = \boldsymbol{\Phi}_{r}\boldsymbol{\Phi}_{\perp} = \mathbf{0}$, + +\begin{align*} + \Vr\Vr\trp\Q_{r} + &= \Vr\Vr\trp\boldsymbol{\Phi}_{r}\boldsymbol{\Sigma}_{r}\boldsymbol{\Psi}_{r}\trp + = \Vr\boldsymbol{\Sigma}_{r}\boldsymbol{\Psi}_{r}\trp + = \Q_{r}, + \\ + \Vr\Vr\trp\Q_{\perp} + &= \Vr\Vr\trp\boldsymbol{\Phi}_{\perp}\boldsymbol{\Sigma}_{\perp}\boldsymbol{\Psi}_{\perp}\trp + = \mathbf{0}. +\end{align*} + +That is, $\Vr\Vr\trp\Q = \Vr\Vr\trp(\Q_{r} + \Q_{\perp}) = \Q_{r}$. +It follows that + +$$ + \Q - \Vr\Vr\trp\Q + = \Q_{r} + \Q_{\perp} - \Q_{r} + = \Q_{\perp} + = \boldsymbol{\Phi}_{\perp}\boldsymbol{\Sigma}_{\perp}\boldsymbol{\Psi}_{\perp}\trp. +$$ + +Since $\boldsymbol{\Phi}_{\perp}$ and $\boldsymbol{\Psi}_{\perp}$ have orthonormal columns, + +$$ + \left\|\boldsymbol{\Phi}_{\perp}\boldsymbol{\Sigma}_{\perp}\boldsymbol{\Psi}_{\perp}\trp\right\|_{F}^{2} + = \left\|\boldsymbol{\Sigma}_{\perp}\right\|_{F}^{2} + = \sum_{j=r + 1}^{\ell}\sigma_{j}^{2}. +$$ + +Putting it all together, + +$$ + \frac{\|\Q - \Vr\Vr\trp\Q\|_{F}^{2}}{\|\Q\|_{F}^{2}} + = \frac{\|\boldsymbol{\Sigma}_{\perp}\|_{F}^{2}}{\|\boldsymbol{\Sigma}\|_{F}^{2}} + = \frac{\sum_{j=r + 1}^{\ell}\sigma_{j}^{2}}{\sum_{j=1}^{\ell}\sigma_{j}^{2}}. +$$ + +::: + +{meth}`PODBasis.plot_energy` visualizes several criteria together. + +```{code-cell} ipython3 +# Show the number of normalized singular values above a given threshold. +pressure_basis.plot_svdval_decay(threshold=1e-5) +plt.show() +``` + +```{code-cell} ipython3 +# Set the reduced state dimension based on a singular value threshold. +pressure_basis.set_dimension(svdval_threshold=1e-5) +print(pressure_basis) + +# Plot the singular value decay and the singular value energies. +pressure_basis.plot_energy(right=100) +plt.show() +``` + +The {func}`pod_basis` function computes a POD basis matrix and the corresponding singular values. +The functions {func}`svdval_decay`, {func}`cumulative_energy`, and {func}`residual_energy` visualize the dimension selection criteria. + ++++ + +#### Centered POD + ++++ + +{class}`PODBasis` uses a linear approximation $\q\approx\Vr\qhat$ with $\Vr$ derived from the SVD of a snapshot matrix $\Q = [~\q_0~~\cdots~~\q_{k-1}~]\in\RR^{n \times k}$. +It is often advantageous to first center the training data columnwise, in which case POD coincides with PCA. +This results in an _affine_ state approximation, + +$$ +\begin{aligned} + \q \approx \Vr\qhat + \bar{\q}, + \qquad\qquad + \bar{\q} = \frac{1}{k}\sum_{j=0}^{k-1}\q_j. +\end{aligned} +$$ + +To get this kind of approximation, center the snapshot matrix first with {class}`opinf.pre.ShiftScaleTransformer`, and remember to "uncenter" the results of `decompress()`. + +```{code-cell} ipython3 +shifter = opinf.pre.ShiftScaleTransformer(centering=True) +pressure_centered = shifter.fit_transform(pressure) + +# Train a centered POD basis and plot the first few basis vectors. +pressure_pca = opinf.basis.PODBasis(num_vectors=r).fit(pressure_centered) + +# To project the (uncentered) pressure, transform, project, then untransform. +pressure_projected = shifter.inverse_transform( + pressure_pca.project(shifter.transform(pressure)) +) + +# Compute the projection error. +la.norm(pressure - pressure_projected) / la.norm(pressure) +``` + +```{code-cell} ipython3 +# Plot the first few basis vectors. +pressure_pca.plot1D() +plt.show() +``` + +The principal basis vector of the previous, non-centered basis is almost constant when compared to the other basis vectors. +By contrast, the new, centered basis does not have any relatively constant basis vectors. + +```{code-cell} ipython3 +# Check the variance of the first basis vector in the non-centered case. +np.var(pressure_basis.entries[:, 0]) +``` + +```{code-cell} ipython3 +# Check the variance of each basis vector in the centered case. +np.var(pressure_pca.entries, axis=0).min() +``` + +Applying a centering can be interpreted more or less as removing the principal component of non-centered data. +In other words, [the principal component of non-centered data is often comparable to the mean of the data](https://en.wikipedia.org/wiki/Principal_component_analysis#Further_considerations). + ++++ + +(sec:api-basis-multivar)= +## Multivariable Data + ++++ + +For systems where the full state consists of several variables (pressure, velocity, temperature, etc.), there are two ways to approach a dimensionality reduction. +In this section we use $\q^{(i)}\in\RR^{n_i}$ to denote the $i$-th of $n_q$ state variables, $i = 0, \ldots, n_q - 1$. +For instance, $\q^{(0)}\in\RR^{100}$ could represent the velocity measured at $n_0 = 100$ spatial points and $\q^{(1)}\in\RR^{200}$ could be the pressure at $n_1 = 200$ spatial points. +In many application the state variables are all defined on the same spatial grid with $n_x$ nodes, in which case $n_0 = \cdots = n_{n_q-1} = n_x$. + ++++ + +### Monolithic Reduction + ++++ + +One option is to reduce all state variables jointly by considering the concatenated state + +$$ +\begin{aligned} + \q = \left[\begin{array}{c} + \q^{(0)} \\ \vdots \\ \q^{(n_q - 1)} + \end{array}\right] + \in \RR^{n}, +\end{aligned} +$$ + +where $n = \sum_{i=0}^{n_q - 1} n_i$. +A low-dimensional approximation $\q \approx \boldsymbol{\Gamma}(\qhat)$ and a corresponding compression function $\boldsymbol{\Gamma}^{*}$ can be used to define a dimensionality reduction without reference to the sub-dimensions $n_i$. +With this approach, it is often very important to [preprocess](opinf.pre) the state variables so that their entries are of similar magnitude. +For instance, the following code creates a single {class}`PODBasis` for velocity, pressure, and specific volume jointly _without_ and preprocessing. + +```{code-cell} ipython3 +# Define a basis using the raw snapshot data for all three variables. +joint_pod = opinf.basis.PODBasis( + residual_energy=1e-8, + name="velocity-pressure-specificvolume", +) +joint_pod.fit(snapshots) + +print(joint_pod) + +# Plot the basis vectors. +joint_pod.plot1D() +plt.show() +``` + +This basis is highly suspicious: the left third of the plot above shows the portion of the basis vectors corresponding to the velocity, the middle third is for the pressure, and the final third is for the specific volume. +The pressure basis vector entries have a much stronger magnitude than the entries for the other two variables because the pressure data is of much larger magnitude than the other variables. +Furthermore, the relative projection error for the snapshot set as a whole is low, but it varies significantly across the variables. + +```{code-cell} ipython3 +def projection_error_report(snapshots_projected): + """Compute the projection error for each state variable separately.""" + velocity_proj, pressure_proj, spvol_proj = np.split(snapshots_projected, 3) + + # Compute projection errors for the physical variables individually. + v_error = la.norm(velocity_proj - velocity) / la.norm(velocity) + p_error = la.norm(pressure_proj - pressure) / la.norm(pressure) + s_error = la.norm(spvol_proj - sp_volume) / la.norm(sp_volume) + joint_error = la.norm(snapshots_projected - snapshots) / la.norm(snapshots) + + # Report the errors. + print(f"Relative projection error of velocity:\t\t{v_error:.4%}") + print(f"Relative projection error of pressure:\t\t{p_error:.4%}") + print(f"Relative projection error of specific volume:\t{s_error:.4%}") + print(f"Relative projection error of joint variables:\t{joint_error:.4%}") +``` + +```{code-cell} ipython3 +projection_error_report(joint_pod.project(snapshots)) +``` + +To improve the basis, one option is to scale each variables to the range $[0, 1]$ using {class}`opinf.pre.TransformerMulti`. + +```{code-cell} ipython3 +# Initialize a different scaling for each physical variable. +euler_transformer = opinf.pre.TransformerMulti( + [ + opinf.pre.ShiftScaleTransformer( + scaling="minmax", + name="velocity", + verbose=True, + ), + opinf.pre.ShiftScaleTransformer( + scaling="minmax", + name="pressure", + verbose=True, + ), + opinf.pre.ShiftScaleTransformer( + scaling="minmax", + name="specific volume", + verbose=True, + ), + ] +) + +# Learn and apply the transformation. +snapshots_scaled = euler_transformer.fit_transform(snapshots) +``` + +```{code-cell} ipython3 +# Learn a new POD basis from the scaled data. +joint_pod.fit(snapshots_scaled) + +print(joint_pod) + +# Plot the basis vectors. +joint_pod.plot1D(num_vectors=7) +plt.show() +``` + +The transition zones between the three variables are still visible, but now the basis vector entries are all of the same order of magnitude. +The projection error across the variables is also much more consistent than before (remember to unscale the projection!). + +```{code-cell} ipython3 +projection_error_report( + euler_transformer.inverse_transform(joint_pod.project(snapshots_scaled)) +) +``` + +### Multilithic Reduction + ++++ + +Instead of reducing multiple variables jointly, it is sometimes advantageous to define different reductions for each variable separately. +Mathematically, we define approximations $\q^{(i)} \approx \boldsymbol{\Gamma}_{i}(\qhat^{(i)})$ for $\qhat^{(i)}\in\RR^{r_i}$ for each $i = 0, \ldots, n_q - 1$, each with corresponding compression functions $\boldsymbol{\Gamma}_{i}^{*}:\RR^{r_i}\to\RR^{n_i}$. +The state approximation for the joint state $\q$ is then + +$$ +\begin{aligned} + \left[\begin{array}{c} + \q^{(0)} \\ \vdots \\ \q^{(n_q - 1)} + \end{array}\right] + = \q + \approx + \boldsymbol{\Gamma}(\qhat) + = \left[\begin{array}{c} + \boldsymbol{\Gamma}_{0}(\qhat^{(0)}) + \\ \vdots \\ + \boldsymbol{\Gamma}_{n_q-1}(\qhat^{(n_q - 1)}) + \end{array}\right], + \qquad + \qhat + = \left[\begin{array}{c} + \qhat^{(0)} \\ \vdots \\ \qhat^{(n_q - 1)} + \end{array}\right] + \in \RR^{r}, +\end{aligned} +$$ + +where $r = \sum_{i=0}^{n_q - 1}r_i$. +This type of reduction can be helpful for structure preservation because the vector $\qhat^{(i)}$ is the latent representation for $\q^{(i)}$, as opposed to the monolithic case in which each of the entries of $\qhat$ affect every $\q^{(0)},\ldots,\q^{(n_q-1)}$. + +The {class}`BasisMulti` class joins individual bases to reduce multiple variables independently. +The bases need not have the same dimensions or even be of the same class. + +```{code-cell} ipython3 +euler_pod = opinf.basis.BasisMulti( + [ + opinf.basis.PODBasis(residual_energy=1e-8, name="velocity"), + opinf.basis.PODBasis(projection_error=1e-2, name="pressure"), + opinf.basis.PODBasis(num_vectors=10, name="specific volume"), + ], +) + +euler_pod.fit(snapshots) +print(euler_pod) +``` + +```{code-cell} ipython3 +projection_error_report(euler_pod.project(snapshots)) +``` + +Note that we are working with the non-scaled data again. +Because the variables are treated separately, we can easily adjust the accuracy of the approximation. + +```{code-cell} ipython3 +# Use more basis vectors for the pressure. +euler_pod[1].set_dimension(projection_error=1e-4) +print(euler_pod[1], end="\n\n") + +projection_error_report(euler_pod.project(snapshots)) +``` + +:::{admonition} Block-diagonal Linear Basis +:class: tip + +In multilithic reduction, if all of the state approximations are linear, then the overall state approximation is linear with a block-diagonal matrix $\Vr$. +Specifically, if $\q^{(i)} \approx \Vr^{(i)}\qhat^{(i)}$, then the approximation for the joint state is $\q \approx \Vr\qhat$ where + +$$ +\begin{aligned} + \Vr + = \operatorname{blockdiag}(\Vr^{(0)},\ldots,\Vr^{(n_q - 1)}) + = \left[\begin{array}{ccc} + \Vr^{(0)} & & \\ + & \ddots & \\ + & & \Vr^{(n_q - 1)} + \end{array}\right] + \in \RR^{n \times r}. +\end{aligned} +$$ +::: + ++++ + +## Custom Bases + ++++ + +New bases can be defined by inheriting from {class}`BasisTemplate`. +Once implemented, the [`verify()`](opinf.basis.BasisTemplate.verify) method may be used to test for consistency between the required methods. + +```{code-cell} ipython3 +class MyBasis(opinf.basis.BasisTemplate): + """Custom basis for dimension reduction.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, hyperparameters, name=None): + """Set any basis hyperparameters. + If there are no hyperparameters, __init__() may be omitted. + """ + super().__init__(name) + # Process/store 'hyperparameters' here. + + # Required methods -------------------------------------------------------- + def fit(self, states): + """Construct the basis.""" + # Set the state dimensions. + self.full_state_dimension = NotImplemented + self.reduced_state_dimension = NotImplemented + + # Learn the basis from `states` to enable compress() and decompress(). + raise NotImplementedError + + def compress(self, states): + """Map high-dimensional states to low-dimensional latent + coordinates. + """ + raise NotImplementedError + + def decompress(self, states_compressed, locs=None): + """Map low-dimensional latent coordinates to high-dimensional + states. + """ + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + # These may be deleted if not implemented. + def save(self, savefile, overwrite=False): + """Save the basis to an HDF5 file.""" + return NotImplemented + + @classmethod + def load(cls, loadfile): + """Load a basis from an HDF5 file.""" + return NotImplemented +``` + +See {class}`BasisTemplate` for details on the arguments for each method. + ++++ + +### Example: Canonical Projection + ++++ + +The following custom basis reduces the full state vector $\q$ by selecting a limited number of entries. +This strategy is useful for impementing the discrete empirical interpolation method (DEIM). + +```{code-cell} ipython3 +class CanonicalBasis(opinf.basis.BasisTemplate): + """Canonical projection basis.""" + + def __init__(self, num_elements, name=None): + """Set the number of locations to keep when the full state is + compressed. + """ + super().__init__(name) + self.reduced_state_dimension = num_elements + self.indices = None + + def fit(self, states): + """Randomly select locations to keep when the full state is + compressed. + """ + self.full_state_dimension = states.shape[0] + self.indices = np.sort( + np.random.choice( + self.full_state_dimension, + size=self.reduced_state_dimension, + replace=False, + ) + ) + return self + + def compress(self, states): + """Extract the selected elements from the full state.""" + if self.indices is None: + raise AttributeError("indices not set, call fit()") + return states[self.indices] + + def decompress(self, states_compressed, locs=None): + """Populate a full state with the extracted elements.""" + if self.indices is None: + raise AttributeError("indices not set, call fit()") + + shape = self.full_state_dimension + if states_compressed.ndim == 2: + shape = (self.full_state_dimension, states_compressed.shape[1]) + + states = np.zeros(shape, dtype=states_compressed.dtype) + states[self.indices] = states_compressed + return states if locs is None else states[locs] + + def save(self, savefile, overwrite=False): + """Save the basis to an HDF5 file.""" + with opinf.utils.hdf5_savehandle(savefile, overwrite) as hf: + hf.create_dataset("r", data=[self.reduced_state_dimension]) + if self.indices is not None: + hf.create_dataset("n", data=[self.full_state_dimension]) + hf.create_dataset("indices", data=self.indices) + + @classmethod + def load(cls, loadfile): + """Load a basis from an HDF5 file.""" + with opinf.utils.hdf5_loadhandle(loadfile) as hf: + basis = cls(hf["r"][0]) + if "n" in hf: + basis.full_state_dimension = hf["n"][0] + basis.indices = hf["indices"][:] + return basis +``` + +```{code-cell} ipython3 +canon = CanonicalBasis(4, name="pressure").fit(pressure) + +print(canon) +canon.verify() +``` \ No newline at end of file diff --git a/sphinx-docs/source/api/ddt.md b/sphinx-docs/source/api/ddt.md new file mode 100644 index 00000000..0d4bdbf9 --- /dev/null +++ b/sphinx-docs/source/api/ddt.md @@ -0,0 +1,349 @@ +--- +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.ddt` + ++++ + +```{eval-rst} +.. automodule:: opinf.ddt + +.. currentmodule:: opinf.ddt + +**Classes** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + DerivativeEstimatorTemplate + UniformFiniteDifferencer + NonuniformFiniteDifferencer + InterpDerivativeEstimator + +**Finite Difference Schemes for Uniformly Spaced Data** + +*Forward Differences* + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + fwd1 + fwd2 + fwd3 + fwd4 + fwd5 + fwd6 + +*Backward Differences* + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + bwd1 + bwd2 + bwd3 + bwd4 + bwd5 + bwd6 + +*Central Differences* + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ctr2 + ctr4 + ctr6 + +*Mixed Differences* + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ord2 + ord4 + ord6 + ddt_uniform + +**Finite Difference Schemes for Nonuniformly Spaced Data** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ddt_nonuniform + ddt +``` + ++++ + +:::{admonition} Overview +:class: note + +- Operator Inference for [continuous models](opinf.models.ContinuousModel) requires time derivatives of the state snapshots. +- `opinf.ddt` provides tools for estimating the time derivatives of the state snapshots. +- Finite difference approximations are available through {class}`UniformFiniteDifferencer` and {class}`NonuniformFiniteDifferencer`. +::: + +```{code-cell} ipython3 +import opinf +import numpy as np +import scipy.interpolate as spinterpolate + +opinf.utils.mpl_config() +``` + +## Time Derivative Estimation + ++++ + +To calibrate time-continuous models, Operator Inference requires the time derivative of the state snapshots. +For example, consider the LTI system + +$$ +\begin{aligned} + \ddt\qhat(t) + = \Ahat\qhat(t) + \Bhat\u(t). +\end{aligned} +$$ (eq:ddt:lti-reduced) + +Here, $\qhat(t)\in\RR^{r}$ is the time-dependent ([reduced-order](./basis.md)) state and $\u(t)\in\RR^{m}$ is the time-dependent input. +In order to learn $\Ahat$ and $\Bhat$, Operator Inference solves a regression problem of the form + +$$ +\begin{aligned} + \min_{\Ahat,\Bhat}\sum_{j=0}^{k-1}\left\| + \Ahat\qhat_j + \Bhat\u_j + - \dot{\qhat}_j + \right\|_2^2 +\end{aligned} +$$ + +[or similar](opinf.lstsq), where each triplet $(\qhat_j, \dot{\qhat}_j, \u_j)$ should correspond to the solution of {eq}`eq:ddt:lti-reduced` at some time $t_j$, $j = 0, \ldots, k - 1$. +In particular, we want + +$$ +\begin{aligned} + \dot{\qhat}_j + \approx \ddt\qhat(t)\big|_{t = t_j} + = \Ahat\qhat_j + \Bhat\u_j. +\end{aligned} +$$ + +This module provides tools for estimating the snapshot time derivatives $\dot{\qhat}_0,\ldots,\dot{\qhat}_{k-1}\in\RR^{r}$ from the reduced snapshots $\qhat_0,\ldots,\qhat_{k-1}\in\RR^{r}$. + ++++ + +:::{admonition} Preprocessing and Time Derivatives +:class: warning + +In some cases, a full-order model may provide snapshot time derivatives $\dot{\q}_0,\ldots,\dot{\q}_{k-1}\in\RR^{n}$ in addition to state snapshots $\q_0,\ldots,\q_{k-1}\in\RR^{n}$. +If any lifting or preprocessing steps are used on the state snapshots, be careful to use the appropriate transformation for snapshot time derivatives, which may be different than the transformation used on the snapshots themselves. + +For example, consider the affine state approximation $\q(t) \approx \Vr\qhat(t) + \bar{\q}$ with an orthonormal basis matrix $\Vr\in\RR^{n\times r}$ and a fixed vector $\bar{\q}\in\RR^{n}$. +In this case, + +$$ +\begin{aligned} + \ddt\q(t) + \approx \ddt\left[\Vr\qhat(t) + \bar{\q}\right] + = \Vr\ddt\left[\qhat(t)\right]. +\end{aligned} +$$ + +Hence, while the compressed state snapshots are given by $\qhat_j = \Vr\trp(\q_j - \bar{\q})$, the correct compressed snapshot time derivatives are $\dot{\qhat}_j = \Vr\trp\dot{\q}_j$ (without the $\bar{\q}$ shift). + +See {meth}`opinf.lift.LifterTemplate.lift_ddts()` and {meth}`opinf.pre.TransformerTemplate.transform_ddts()`. +::: + ++++ + +## Finite Difference Estimators + ++++ + +### Partial Estimation + ++++ + +Every finite difference scheme has limitations on where the derivative can be estimated. +For example, a [first-order backward scheme](opinf.ddt.bwd1) requires $\qhat(t_{j-1})$ and $\qhat(t_j)$ to estimate $\dot{\qhat}(t_j)$, hence the derivative cannot be estimated at $t = t_0$. + +The forward, backward, and central difference functions ({func}`fwd1`, {func}`bwd3`, {func}`ctr6`, etc.) take in a snapshot matrix $\Qhat\in\RR^{r\times k}$, a time step, and (optionally) the corresponding input matrix $\U\in\RR^{m\times k}$ and return a subset of the snapshots $\Qhat'\in\mathbb{R}^{r\times k'}$, the corresponding derivatives $\dot{\Qhat}\in\RR^{r\times k'}$, and (optionally) the corresponding inputs $\U'\in\RR^{m \times k'}$. + +```{code-cell} ipython3 +# Set a state dimension, input dimension, and number of snapshots. +r = 20 +m = 3 +k = 400 + +# Make test data. +t = np.linspace(0, 1, k) +Q = np.random.random((r, k)) +U = np.random.random((m, k)) + +# Extract the time step. +dt = t[1] - t[0] +``` + +```{code-cell} ipython3 +Qnew, Qdot = opinf.ddt.bwd2(states=Q, dt=dt) +print(f"{Qnew.shape=}, {Qdot.shape=}") +``` + +```{code-cell} ipython3 +Qnew, Qdot, Unew = opinf.ddt.ctr6(states=Q, dt=dt, inputs=U) +print(f"{Qnew.shape=}, {Qdot.shape=}, {Unew.shape=}") +``` + +### Complete Estimation + ++++ + +The finite difference functions {func}`ord2`, {func}`ord4`, and {func}`ord6` mix forward, central, and backward differences to provide derivative estimates for all provided state snapshots. +These schemes are used by {func}`ddt_uniform`, and {func}`ddt_nonuniform`, which only return the estimated derivatives. + +```{code-cell} ipython3 +Qnew, Qdot = opinf.ddt.ord4(states=Q, dt=dt) +print(f"{Qnew.shape=}, {Qdot.shape=}") +print(f"{(Qnew is Q)=}") +``` + +```{code-cell} ipython3 +Qdot = opinf.ddt.ddt_uniform(states=Q, dt=dt, order=4) +print(f"{Qdot.shape=}") +``` + +```{code-cell} ipython3 +Qdot = opinf.ddt.ddt_nonuniform(states=Q, t=t) +print(f"{Qdot.shape=}") +``` + +### Convenience Classes + ++++ + +The classes {class}`UniformFiniteDifferencer` and {class}`NonuniformFiniteDifferencer` wrap the finite difference methods listed above for use with {mod}`opinf.roms` classes. +They also have a [`verify()`](opinf.ddt.UniformFiniteDifferencer.verify) method for checking the estimation scheme against true derivatives for a limited set of test cases. + +```{code-cell} ipython3 +differ = opinf.ddt.UniformFiniteDifferencer(t, scheme="fwd1") +print(differ) +``` + +```{code-cell} ipython3 +differ.verify(plot=True) +``` + +```{code-cell} ipython3 +Qnew, Qdot, Unew = differ.estimate(states=Q, inputs=U) +print(f"{Qnew.shape=}, {Qdot.shape=}, {Unew.shape=}") +``` + +## Interpolatory Estimators + ++++ + +The {class}`InterpDerivativeEstimator` interpolates the state data using classes from {mod}`scipy.interpolate` and evaluates the derivative of the interpolant. + +```{code-cell} ipython3 +estimator = opinf.ddt.InterpDerivativeEstimator(t, "pchip") +print(estimator) +``` + +```{code-cell} ipython3 +estimator.verify(plot=True) +``` + +## Custom Estimators + ++++ + +New time derivative estimators can be defined by inheriting from {class}`DerivativeEstimatorTemplate`. +Once implemented, the [`verify()`](opinf.ddt.DerivativeEstimatorTemplate.verify) method may be used to compare the results of [`estimate()`](opinf.ddt.DerivativeEstimatorTemplate.estimate) with true derivatives for a limited number of test cases. + +```{code-cell} ipython3 +class MyEstimator(opinf.ddt.DerivativeEstimatorTemplate): + """Inheritance template for custom derivative estimators.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, time_domain, hyperparameters): + """Set any hyperparameters. + If there are no hyperparameters, __init__() may be omitted. + """ + super().__init__(time_domain) + # Process hyperparameters here. + + # Required methods -------------------------------------------------------- + def estimate(self, states, inputs=None): + """Estimate the first time derivatives of the states. + + Parameters + ---------- + states : (r, k) ndarray + State snapshots, either full or (preferably) reduced. + inputs : (m, k) ndarray or None + Inputs corresponding to the state snapshots, if applicable. + + Returns + ------- + _states : (r, k') ndarray + Subset of the state snapshots. + ddts : (r, k') ndarray + First time derivatives corresponding to ``_states``. + _inputs : (m, k') ndarray or None + Inputs corresponding to ``_states``, if applicable. + **Only returned** if ``inputs`` is provided. + """ + raise NotImplementedError +``` + +### Example: Cubic Spline Interpolation + ++++ + +The following class wraps {class}`scipy.interpolate.CubicSpline` and uses its `derivative()` method to compute the derivative. +This is a simplified version of {class}`DerivativeEstimatorTemplate`. + +```{code-cell} ipython3 +class CubicSplineEstimator(opinf.ddt.DerivativeEstimatorTemplate): + """Derivative estimator using cubic spline interpolation.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, time_domain, bc_type="not-a-knot"): + """Save arguments for scipy.interpolate.CubicSpline.""" + super().__init__(time_domain) + self.options = dict(bc_type=bc_type, axis=1, extrapolate=None) + + # Required methods -------------------------------------------------------- + def estimate(self, states, inputs=None): + """Estimate the first time derivatives of the states.""" + spline = spinterpolate.CubicSpline( + x=self.time_domain, + y=states, + **self.options, + ) + ddts = spline(self.time_domain, 1) + if inputs is None: + return states, ddts + return states, ddts, inputs +``` + +```{code-cell} ipython3 +spline_estimator = CubicSplineEstimator(t) +print(spline_estimator) +``` + +```{code-cell} ipython3 +spline_estimator.verify() +``` \ No newline at end of file diff --git a/sphinx-docs/source/api/lift.md b/sphinx-docs/source/api/lift.md new file mode 100644 index 00000000..554f73c0 --- /dev/null +++ b/sphinx-docs/source/api/lift.md @@ -0,0 +1,234 @@ +--- +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.lift` + ++++ + +```{eval-rst} +.. automodule:: opinf.lift + +.. currentmodule:: opinf.lift + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + LifterTemplate + QuadraticLifter + PolynomialLifter +``` + ++++ + +:::{admonition} Overview +:class: note + +- Operator Inference models have polynomial structure. To calibrate a model with data, it can be advantageous to transform and/or augment the state variables to induce a desired structure. +- {class}`LifterTemplate` provides an API for variable transformation / augmentation operations. +::: + +```{code-cell} ipython3 +import numpy as np + +import opinf +``` + +## Lifting Maps + ++++ + +Operator Inference learns models with [polynomial terms](opinf.operators), for example, + +$$ + \ddt\qhat(t) + = \chat + + \Ahat\qhat(t) + + \Hhat[\qhat(t)\otimes\qhat(t)] + + \Bhat\u(t). +$$ + +If training data do not exhibit this kind of polynomial structure, an Operator Inference model is not likely to perform well. +In some systems with nonpolynomial nonlinearities, a change of variables can induce a polynomial structure, which can greatly improve the effectiveness of Operator Inference. +Such variable transformations are often called _lifting maps_, especially if the transformation augments the state by introducing additional variables. + +This module defines a template class for implementing lifting maps that can interface with {mod}`opinf.roms` classes and provides a few examples of lifting maps. + ++++ + +:::{admonition} Example +:class: tip + +This example originates from {cite}`qian2021thesis`. +Consider a nonlinear diffusion-reaction equation with a cubic reaction term: + +$$ +\begin{align*} + \frac{\partial}{\partial t}q + = \Delta q - q^3. +\end{align*} +$$ + +By introducing an auxiliary variable $w = q^{2}$, we have $\frac{\partial}{\partial t}w = 2q\frac{\partial q}{\partial t} = 2q\Delta q - 2q^4$, hence the previous equation can be expressed as the system + +$$ +\begin{align*} + \frac{\partial}{\partial t}q + &= \Delta q - qw, + & + \frac{\partial}{\partial t}w + &= 2q\Delta q - 2w^2. +\end{align*} +$$ + +This system is quadratic in the lifted variables $(q, w)$, motivating a quadratic model structure instead of a cubic one. +The {class}`QuadraticLifter` class implements this transformation. +::: + ++++ + +## Custom Lifting Maps + ++++ + +New transformers can be defined by inheriting from the {class}`LifterTemplate`. +Once implemented, the [`verify()`](LifterTemplate.verify) method may be used to test the consistency of these three methods. + +```{code-cell} ipython3 +class MyLifter(opinf.lift.LifterTemplate): + """Custom lifting map.""" + + # Required methods -------------------------------------------------------- + @staticmethod + def lift(state): + """Lift the native state variables to the learning variables.""" + raise NotImplementedError + + @staticmethod + def unlift(lifted_state): + """Recover the native state variables from the learning variables.""" + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + @staticmethod + def lift_ddts(state, ddts): + """Lift the native state time derivatives to the time derivatives + of the learning variables. + """ + raise NotImplementedError +``` + +A more detailed version of this class is included in the package as {class}`opinf.lift.QuadraticLifter`. + ++++ + +### Example: Specific Volume Variables + ++++ + +This example was used in {cite}`qian2019transform,qian2020liftandlearn,qian2021thesis,guo2022bayesopinf`. +The compressible Euler equations for an ideal gas can be written in conservative form as + +$$ +\begin{align*} + \frac{\partial}{\partial t}\left[\rho\right] + &= -\frac{\partial}{\partial x}\left[\rho u\right], + & + \frac{\partial}{\partial t}\left[\rho u\right] + &= -\frac{\partial}{\partial x}\left[\rho u^2 + p\right], + & + \frac{\partial}{\partial t}\left[\rho e\right] + &= -\frac{\partial}{\partial x}\left[(\rho e + p)u\right]. +\end{align*} +$$ + +These equations are nonpolynomially nonlinear in the conservative variables $\vec{q}_{c} = (\rho, \rho u, \rho e)$. +However, by changing to the specific-volume variables $\vec{q} = (u, p, \zeta)$ and using the ideal gas law + +$$ +\begin{align*} + \rho e = \frac{p}{\gamma - 1} + \frac{\rho u^2}{2}, +\end{align*} +$$ + +we arrive at a _quadratic_ system + +$$ +\begin{align*} + \frac{\partial u}{\partial t} + &= -u \frac{\partial u}{\partial x} - \zeta\frac{\partial p}{\partial x}, + & + \frac{\partial p}{\partial t} + &= -\gamma p \frac{\partial u}{\partial x} - u\frac{\partial p}{\partial x}, + & + \frac{\partial \zeta}{\partial t} + &= -u \frac{\partial\zeta}{\partial x} + \zeta\frac{\partial u}{\partial x}. +\end{align*} +$$ + +Hence, a quadratic reduced-order model of the form + +$$ + \frac{\text{d}}{\text{d}t}\qhat(t) + = \Hhat[\qhat(t)\otimes\qhat(t)] +$$ + +can be learned for this system using data in the variables $\vec{q}$. +See {cite}`qian2020liftandlearn` for details. + +The following class defines this the variable transformation. + +```{code-cell} ipython3 +class EulerLifter(opinf.lift.LifterTemplate): + """Lifting map for the Euler equations transforming conservative + variables to specific volume variables. + """ + + def __init__(self, gamma=1.4): + """Store the heat capacity ratio, gamma.""" + self.gamma = gamma + + def lift(self, state): + """Map the conservative variables to the learning variables, + [rho, rho*u, rho*e] -> [u, p, 1/rho]. + """ + rho, rho_u, rho_e = np.split(state, 3) + + u = rho_u / rho + p = (self.gamma - 1) * (rho_e - 0.5 * rho * u**2) + zeta = 1 / rho + + return np.concatenate((u, p, zeta)) + + def unlift(self, upzeta): + """Map the learning variables to the conservative variables, + [u, p, 1/rho] -> [rho, rho*u, rho*e]. + """ + u, p, zeta = np.split(upzeta, 3) + + rho = 1 / zeta + rho_u = rho * u + rho_e = p / (self.gamma - 1) + 0.5 * rho * u**2 + + return np.concatenate((rho, rho_u, rho_e)) +``` + +```{code-cell} ipython3 +# Get test state data. +n = 100 +Q = np.random.random((3 * n, 200)) + +# Verify the implementation. +EulerLifter().verify(Q) +``` + +:::{admonition} Developer Notes + +- In this example, [`lift()`](LifterTemplate.lift) and [`unlift()`](LifterTemplate.unlift) are *not* static methods because they rely on the `gamma` attribute. +- Since [`lift_ddts()`](LifterTemplate.lift_ddts) is not implemented, [`verify()`](LifterTemplate.verify) only checks the consistency between [`lift()`](LifterTemplate.lift) and [`unlift()`](LifterTemplate.unlift). +::: \ No newline at end of file diff --git a/sphinx-docs/source/api/lstsq.md b/sphinx-docs/source/api/lstsq.md new file mode 100644 index 00000000..03c24dcb --- /dev/null +++ b/sphinx-docs/source/api/lstsq.md @@ -0,0 +1,543 @@ +--- +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.lstsq` + ++++ + +```{eval-rst} +.. automodule:: opinf.lstsq + +.. currentmodule:: opinf.lstsq + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + SolverTemplate + PlainSolver + L2Solver + L2DecoupledSolver + TikhonovSolver + TikhonovDecoupledSolver + TruncatedSVDSolver + TotalLeastSquaresSolver +``` + ++++ + +:::{admonition} Overview +:class: note + +- `opinf.lstsq` classes are solve Operator Inference regression problems. +- `opinf.models` classes handle the construction of the regression matrices from snapshot data, pass these matrices to the solver's [`fit()`](SolverTemplate.fit) method, call the solver's [`solve()`](SolverTemplate.solve) method, and interpret the solution in the context of the model structure. +::: + ++++ + +:::{admonition} Example Data +:class: tip +The examples on this page use data matrices composed of compressed heat flow data. +You can [download the data here](https://github.com/Willcox-Research-Group/rom-operator-inference-Python3/raw/data/lstsq_example.npz) to repeat the demonstration. +::: + +```{code-cell} ipython3 +import numpy as np +import scipy.optimize as opt + +import opinf +``` + +## Operator Inference Regression Problems + ++++ + +Operator Inference uses data to learn the entries of an $r \times d$ *operator matrix* $\Ohat$ by solving a regression problem, stated generally as + +::::{margin} +:::{admonition} What is $\Z$? +For continuous models (systems of ordinary differential equations), $\Z$ consists of the time derivatives of the snapshots; for discrete models (discrete dynamical systems), $\Z$ also contains state snapshots. +::: +:::: + +$$ +\begin{aligned} + \text{find}\quad\Ohat\quad\text{such that}\quad + \Z \approx \Ohat\D\trp + \quad\Longleftrightarrow\quad + \D\Ohat\trp \approx \Z\trp, +\end{aligned} +$$ (eq:lstsq:general) + +where $\D$ is the $k \times d$ *data matrix* formed from state and input snapshots and $\Z$ is the $r \times k$ matrix of left-hand side data. + +This module defines classes for solving different variations of the regression {eq}`eq:lstsq:general` given the data matrices $\D$ and $\Z$. + ++++ {"vscode": {"languageId": "plaintext"}} + +::::{admonition} Example +:class: tip + +Suppose we want to construct a linear time-invariant (LTI) system, + +$$ +\begin{align} + \ddt\qhat(t) + = \Ahat\qhat(t) + \Bhat\u(t), + \qquad + \Ahat\in\RR^{r \times r}, + ~ + \Bhat\in\RR^{r \times m}. +\end{align} +$$ (eq:lstsq:ltiexample) + +The operator matrix is $\Ohat = [~\Ahat~~\Bhat~]\in\RR^{r \times d}$ with column dimension $d = r + m$. +To learn $\Ohat$ with Operator Inference, we need data for $\qhat(t)$, $\u(t)$, and $\ddt\qhat(t)$. +For $j = 0, \ldots, k-1$, let + +- $\qhat_{j}\in\RR^r$ be a measurement of the (reduced) state at time $t_{j}$, +- $\dot{\qhat}_{j} = \ddt\qhat(t)\big|_{t=t_{j}} \in \RR^r$ be the time derivative of the state at time $t_{j}$, and +- $\u_{j} = \u(t_j) \in \RR^m$ be the input at time $t_{j}$. + +In this case, the data matrix $\D$ is given by $\D = [~\Qhat\trp~~\U\trp~]\in\RR^{k \times d}$, where + +$$ +\begin{aligned} + \Qhat = \left[\begin{array}{ccc} + & & \\ + \qhat_0 & \cdots & \qhat_{k-1} + \\ & & + \end{array}\right] + \in \RR^{r\times k}, + \qquad + \U = \left[\begin{array}{ccc} + & & \\ + \u_0 & \cdots & \u_{k-1} + \\ & & + \end{array}\right] + \in \RR^{m \times k}. +\end{aligned} +$$ + +The left-hand side data is $\Z = \dot{\Qhat} = [~\dot{\qhat}_0~~\cdots~~\dot{\qhat}_{k-1}~]\in\RR^{r\times k}$. + +:::{dropdown} Derivation +We seek $\Ahat$ and $\Bhat$ such that + +$$ +\begin{aligned} + \dot{\qhat}_{j} + \approx \Ahat\qhat_j + \Bhat\u_j, + \qquad j = 0, \ldots, k-1. +\end{aligned} +$$ + +Using the snapshot matrices $\Qhat$, $\U$, and $\dot{\Qhat}$ defined above, we want + +$$ +\begin{aligned} + \dot{\Qhat} + \approx \Ahat\Qhat + \Bhat\U + = [~\Ahat~~\Bhat~]\left[\begin{array}{c} \Qhat \\ \U \end{array}\right], + \quad\text{or} + \\ + [~\Qhat\trp~~\U\trp~][~\Ahat~~\Bhat~]\trp \approx \dot{\Qhat}\trp, +\end{aligned} +$$ + +which is $\D\Ohat\trp \approx \Z\trp$. + +More precisely, a regression problem for $\Ohat$ with respect to the data triples $(\qhat_j, \u_j, \dot{\qhat}_j)$ can be written as + +$$ +\begin{aligned} + \argmin_{\Ahat,\Bhat}\sum_{j=0}^{k-1}\left\| + \Ahat\qhat_j + \Bhat\u_j - \dot{\qhat}_j + \right\|_{2}^{2} + &= \argmin_{\Ahat,\Bhat}\left\| + \Ahat\Qhat + \Bhat\U - \dot{\Qhat} + \right\|_{F}^{2} + \\ + &= \argmin_{\Ahat,\Bhat}\left\| + [~\Ahat~~\Bhat~]\left[\begin{array}{c} \Qhat \\ \U \end{array}\right] - \Z + \right\|_{F}^{2} + \\ + &= \argmin_{\Ahat,\Bhat}\left\| + [~\Qhat\trp~~\U\trp~][~\Ahat~~\Bhat~]\trp - \Z\trp + \right\|_{F}^{2}, +\end{aligned} +$$ + +which is $\argmin_{\Ohat}\|\D\Ohat\trp - \Z\trp\|_F^2$. +::: +:::: + ++++ + +## Default Solver + ++++ + +Most often, we pose {eq}`eq:lstsq:general` as a linear least-squares regression, + +$$ +\begin{aligned} + \argmin_{\Ohat} \|\D\Ohat\trp - \Z\trp\|_F^2. +\end{aligned} +$$ (eq:lstsq:plain) + +Note that the matrix least-squares problem {eq}`eq:lstsq:plain` decouples into $r$ independent vector least-squares problems, i.e., + +$$ +\begin{aligned} + \argmin_{\ohat_i} \|\D\ohat_i - \z_i\|_2^2, + \quad i = 1, \ldots, r, +\end{aligned} +$$ + +where $\ohat_i$ and $\z_i$ are the $i$-th rows of $\Ohat$ and $\Z$, respectively. + +The {class}`PlainSolver` class solves {eq}`eq:lstsq:plain` without any additional terms. +This is the default solver used if another solver is not specified in the constructor of an {mod}`opinf.models` class. + +```{code-cell} ipython3 +# Load and extract the example data. +data = np.load("lstsq_example.npz") +D = data["data_matrix"] +Z = data["lhs_matrix"] + +print(f"{D.shape=}, {Z.shape=}") + +# Infer problem dimensions from the data. +k, d = D.shape +r = Z.shape[0] + +print(f"{r=}, {d=}, {k=}") +``` + +```{code-cell} ipython3 +solver = opinf.lstsq.PlainSolver() +print(solver) +``` + +```{code-cell} ipython3 +# Fit the solver to the regression data. +solver.fit(D, Z) +print(solver) +``` + +```{code-cell} ipython3 +# Solve the least-squares problem for the operator matrix. +Ohat = solver.solve() +print(f"{Ohat.shape=}") +``` + +```{code-cell} ipython3 +# Check how well the solution matches the data. +print("\nOptimization residuals:", solver.residual(Ohat), sep="\n") +``` + +## Tikhonov Regularization + ++++ + + +It is often advantageous to add a *regularization term* $\mathcal{R}(\Ohat)$ to penalize the entries of the inferred operators. +This can prevent over-fitting to data and promote stability in the learned reduced-order model {cite}`mcquarrie2021combustion`. +The least-squares regression {eq}`eq:lstsq:plain` then becomes + +$$ +\begin{aligned} + \argmin_{\Ohat}\| + \D\Ohat\trp - \Z\trp + \|_{F}^{2} + \mathcal{R}(\Ohat). +\end{aligned} +$$ + +A [Tikhonov regularization](https://en.wikipedia.org/wiki/Ridge_regression#Tikhonov_regularization) term has the form + +$$ +\begin{aligned} + \mathcal{R}(\Ohat) + = \sum_{i=1}^{r}\|\bfGamma_i\ohat_i\|_2^2, +\end{aligned} +$$ + +where $\ohat_1,\ldots,\ohat_r$ are the rows of $\Ohat$ and each $\bfGamma_1,\ldots,\bfGamma_r$ is a $d \times d$ symmetric positive-definite matrix. +In this case, the decoupled regressions for the rows of $\Ohat$ are given by + +$$ +\begin{aligned} + \argmin_{\ohat_i} \|\D\ohat_i - \z_i\|_2^2 + \|\bfGamma_i\ohat_i\|_2^2, + \quad i = 1, \ldots, r. +\end{aligned} +$$ + +The following classes solve Tikhonov-regularized least-squares Operator Inference regressions for different choices of the regularization term $\mathcal{R}(\Ohat)$. + +| Solver class | Description | Regularization $\mathcal{R}(\Ohat)$ | +| :------------------------------- | :----------------------------------------------- | :--------------------------------------------: | +| {class}`L2Solver` | One scalar regularizer for all $\ohat_i$ | $\lambda^{2}\|\|\Ohat\trp\|\|_F^2$ | +| {class}`L2DecoupledSolver` | Different scalar regularizers for each $\ohat_i$ | $\sum_{i=1}^{r}\lambda_i^2\|\|\ohat_i\|\|_2^2$ | +| {class}`TikhonovSolver` | One matrix regularizer for all $\ohat_i$ | $\|\|\bfGamma\Ohat\trp\|\|_F^2$ | +| {class}`TikhonovDecoupledSolver` | Different matrix regularizers for each $\ohat_i$ | $\sum_{i=1}^{r}\|\|\bfGamma_i\ohat_i\|\|_2^2$ | + +```{code-cell} ipython3 +# Use a single scalar regularizer lambda=1e-6. +l2solver = opinf.lstsq.L2Solver(regularizer=1e-6).fit(D, Z) +print(l2solver) +``` + +```{code-cell} ipython3 +Ohat_L2 = l2solver.solve() +l2solver.residual(Ohat_L2) +``` + +```{code-cell} ipython3 +# Use a different scalar regularizer for each row of the operator matrix. +lambdas = np.logspace(-10, r - 11, r) +l2dsolver = opinf.lstsq.L2DecoupledSolver(regularizer=lambdas) +l2dsolver.fit(D, Z) +print(l2dsolver) +``` + +```{code-cell} ipython3 +Ohat_L2d = l2dsolver.solve() +l2dsolver.residual(Ohat_L2d) +``` + +```{code-cell} ipython3 +# Use a single diagonal matrix regularizer. +gammadiag = np.full(d, 1e-8) +gammadiag[-1] = 1e-4 +tiksolver = opinf.lstsq.TikhonovSolver(regularizer=gammadiag) +tiksolver.fit(D, Z) +print(tiksolver) +``` + +```{code-cell} ipython3 +Ohat_tik = tiksolver.solve() +tiksolver.residual(Ohat_tik) +``` + +```{code-cell} ipython3 +# Use a single non-diagonal matrix regularizer. +offdiag = 0.1 * gammadiag[:-1] +Gamma = np.diag(gammadiag) + np.diag(offdiag, k=1) + np.diag(offdiag, k=-1) +tiksolver.regularizer = Gamma +print(tiksolver) +``` + +```{code-cell} ipython3 +Ohat_tik = tiksolver.solve() +tiksolver.residual(Ohat_tik) +``` + +```{code-cell} ipython3 +# Use a different matrix regularizer for each row of the operator matrix. +diags = np.exp(np.random.random((r, d)) - 10) +tikdsolver = opinf.lstsq.TikhonovDecoupledSolver(regularizer=diags) +tikdsolver.fit(D, Z) +print(tikdsolver) +``` + +## Truncated SVD + ++++ + +The {class}`TruncatedSVDSolver` class approximates the solution to the ordinary least-squares problem {eq}`eq:lstsq:plain` by solving the related problem + +$$ +\begin{aligned} + \argmin_{\Ohat}\|\tilde{\D}\Ohat\trp - \Z\trp\|_{F}^{2} +\end{aligned} +$$ + +where $\tilde{\D}$ is the best rank-$d'$ approximation of $\D$ for some given $d' < \min(k,d)$, i.e., + +$$ +\begin{aligned} + \tilde{D} + = \argmin_{\D' \in \RR^{k \times d}} + \|\D' - \D\|_{F} + \quad\textrm{such that}\quad + \operatorname{rank}(\D') = d'. +\end{aligned} +$$ + +This approach is [related to Tikhonov regularization](https://math.stackexchange.com/questions/1084677/tikhonov-regularization-vs-truncated-svd) and is based on the [truncated singular value decomposition](https://en.wikipedia.org/wiki/Singular_value_decomposition#Truncated_SVD) of the data matrix $\D$. +The optimization residual is guaranteed to be higher than when using the full SVD as in {class}`PlainSolver`, but the condition number of the truncated SVD system is lower than that of the original system. +Truncation can play a similar role to regularization, but the hyperparameter here (the number of columns to use) is an integer, whereas the regularization hyperparameter $\lambda$ for {class}`L2Solver` may be any positive number. + +```{code-cell} ipython3 +tsvdsolver = opinf.lstsq.TruncatedSVDSolver(-2) +tsvdsolver.fit(D, Z) +print(tsvdsolver) +``` + +```{code-cell} ipython3 +Ohat = tsvdsolver.solve() +tsvdsolver.residual(Ohat) +``` + +```{code-cell} ipython3 +# Change the number of columns used without recomputing the SVD. +tsvdsolver.num_svdmodes = 8 +print(tsvdsolver) +``` + +```{code-cell} ipython3 +tsvdsolver.residual(tsvdsolver.solve()) +``` + +## Total Least-Squares + ++++ + +Linear least-squares models for $\D\Ohat\trp \approx \Z\trp$ assume error in $\Z$ only, i.e., + +$$ +\begin{aligned} + \D\Ohat\trp = \Z\trp + \Delta_{\Z\trp} +\end{aligned} +$$ + +for some $\Delta_{\Z\trp} \in \RR^{r\times k}$ +[Total least-squares](https://en.wikipedia.org/wiki/Total_least_squares) is an alternative approach that assumes possible error in the data matrix $\D$ as well as in $\Z$, i.e., + +$$ +\begin{aligned} + (\D + \Delta_{\D})\Ohat\trp = \Z\trp + \Delta_{\Z\trp} +\end{aligned} +$$ + +for some $\Delta_{\D}\in\RR^{k \times d}$ and $\Delta_{\Z\trp}\in\RR^{r \times k}$. + +The {class}`TotalLeastSquaresSolver` class performs a total least-squares solve for $\Ohat$. + +```{code-cell} ipython3 +totalsolver = opinf.lstsq.TotalLeastSquaresSolver() +totalsolver.fit(D, Z) +print(totalsolver) +``` + +```{code-cell} ipython3 +Ohat_total = totalsolver.solve() +totalsolver.residual(Ohat_total) +``` + +## Custom Solvers + ++++ + +New solvers can be defined by inheriting from {class}`SolverTemplate`. +Once implemented, the [`verify()`](SolverTemplate.verify) method may be used to test for consistency between the required methods. + +```{code-cell} ipython3 +class MySolver(opinf.lstsq.SolverTemplate): + """Custom solver for the Operator Inference regression.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, hyperparameters): + """Set any solver hyperparameters. + If there are no hyperparameters, __init__() may be omitted. + """ + super().__init__() + # Process / store hyperparameters here. + + # Required methods -------------------------------------------------------- + def solve(self): + """Solve the regression and return the operator matrix.""" + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + # These may be deleted if not implemented. + def fit(self, data_matrix, lhs_matrix): + """Save data matrices and prepare to solve the regression problem. + This method should do as much work as possible that does not depend on + the hyperparameters in preparation for solving the regression. + If there are no hyperparameters, fit() may be omitted. + """ + super().fit(data_matrix, lhs_matrix) + # Prepare for regression here. + + def save(self, savefile, overwrite=False): + """Save the solver to an HDF5 file.""" + raise NotImplementedError + + @classmethod + def load(cls, loadfile): + """Load a solver from an HDF5 file.""" + raise NotImplementedError + + def copy(self): + """Make a copy of the solver. + If not implemented, copy.deepcopy() is used. + """ + raise NotImplementedError +``` + +See {class}`SolverTemplate` for solver attributes and details on the arguments for each method. + ++++ + +### Example: Wrapping a Least-squares Routine + ++++ + +The following class shows how to wrap an existing numerical least-squares solver routine. +Here, we use `scipy.optimize.nnls()`, which solves the least-squares problem with non-negative constraints, i.e., the entries of $\Ohat$ will all be positive. +**This is just a demonstration** -- the entries of a good $\Ohat$ are rarely all positive. + +```{code-cell} ipython3 +class NNSolver(opinf.lstsq.SolverTemplate): + """Least-squares solver with non-negativity constraints.""" + + def __init__(self, maxiter=None, atol=None): + """Save keyword arguments for scipy.optimize.nnls().""" + super().__init__() + self.options = dict(maxiter=maxiter, atol=atol) + + def solve(self): + """Solve the regression and return the operator matrix.""" + # Allocate space for the operator matrix entries. + Ohat = np.empty((self.r, self.d)) + + # Solve the nonnegative least-squares for each operator matrix row. + for i in range(self.r): + Ohat[i] = opt.nnls( + self.data_matrix, self.lhs_matrix[i], **self.options + )[0] + + return Ohat + + # Alternative implementation: + return np.array( + [ + opt.nnls(self.data_matrix, z_i, **self.options)[0] + for z_i in self.lhs_matrix + ] + ) +``` + +```{code-cell} ipython3 +solver = NNSolver().fit(D, Z) +print(solver) +``` + +```{code-cell} ipython3 +solver.verify() +``` + +```{code-cell} ipython3 +Ohat_nn = solver.solve() +print(f"{Ohat_nn.shape=}") + +# Check that the entries of the operator matrix are nonnegative. +print("Minimal entry of Ohat_nn:", Ohat_nn.min()) +``` \ No newline at end of file diff --git a/sphinx-docs/source/api/main.md b/sphinx-docs/source/api/main.md new file mode 100644 index 00000000..3efec4ff --- /dev/null +++ b/sphinx-docs/source/api/main.md @@ -0,0 +1,21 @@ +# `opinf` + +The `opinf` package contains the following submodules. + +```{eval-rst} + +.. autosummary:: + + opinf.lift + opinf.pre + opinf.basis + opinf.ddt + opinf.operators + opinf.models + opinf.lstsq + opinf.post + opinf.roms + opinf.utils +``` + +The main namespace also contains the [`ROM`](opinf.roms.ROM) and [`ParametricROM`](opinf.roms.ParametricROM) classes from {mod}`opinf.roms`. diff --git a/sphinx-docs/source/api/models.md b/sphinx-docs/source/api/models.md new file mode 100644 index 00000000..dc3d3c4f --- /dev/null +++ b/sphinx-docs/source/api/models.md @@ -0,0 +1,306 @@ +# `opinf.models` + +```{eval-rst} +.. automodule:: opinf.models + +.. currentmodule:: opinf.models + +**Nonparametric Models** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ContinuousModel + DiscreteModel + +**Parametric Models** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ParametricContinuousModel + ParametricDiscreteModel + InterpContinuousModel + InterpDiscreteModel +``` + +:::{admonition} Overview +:class: note +Model classes represent a set equations describing the dynamics of the model state. +The user specifies the structure of the dynamics by providing a list of [operators](opinf.operators) to the constructor. +Model dynamics are calibrated through a least-squares regression of available state and input data. +Solvers for the least-squares problem are specified in the constructor. + +```python +import opinf + +# Specify the model structure through a list of operators. +model = opinf.models.ContinuousModel( + operators=[ + opinf.operators.LinearOperator(), + opinf.operators.InputOperator(), + ], + solver=opinf.lstsq.L2Solver(1e-6), +) + +# Calibrate the model through Operator Inference. +model.fit(state_snapshots, state_time_derivatives, corresponding_inputs) + +# Solve the model. +result = model.predict(initial_condition, time_domain, input_function) +``` + +::: + +## Types of Models + +::::{margin} +:::{note} +Spatially discretizing a partial differential equation results in a continuous-time ODE model; discretizing a continuous-time ODE model in time yields a discrete-time model. +::: +:::: + +The models defined in this module can be classified in a few ways. + +1. **Continuous vs Discrete:** _Continuous-time_ models are for systems of ordinary differential equations (ODEs), and _discrete-time_ models are for discrete dynamical systems. +2. **Parametric vs Nonparametric:** In a _parametric_ model, the dynamics depend on one or more external parameters; a _nonparametric_ model has no external parameter dependence. + + + +## Nonparametric Models + +A _nonparametric_ model is comprised exclusively of [nonparametric operators](sec-operators-nonparametric). + +```{eval-rst} +.. currentmodule:: opinf.models + +.. autosummary:: + :nosignatures: + + ContinuousModel + DiscreteModel +``` + +Nonparametric model classes are initialized with a single argument, `operators`, that must be a list of nonparametric {mod}`opinf.operators` objects. +The right-hand side of the model is defined to be the sum of the action of the operators on the model state and the input (if present). +For example, a {class}`ContinuousModel` represents a system of ODEs + +$$ +\begin{align*} + \ddt\qhat(t) + = \fhat(\qhat,\u) + = \sum_{\ell=1}^{n_\textrm{terms}} + \Ophat_{\ell}(\qhat(t),\u(t)) +\end{align*} +$$ + +where each $\Ophat_{\ell}$ is a nonparametric operator. + +:::{tip} +The `operators` constructor argument for these classes can also be a string that indicates which type of operator to use. + +| Character | {mod}`opinf.operators` class | +| :-------- | :------------------------------- | +| `'c'` | {class}`opinf.operators.ConstantOperator` | +| `'A'` | {class}`opinf.operators.LinearOperator` | +| `'H'` | {class}`opinf.operators.QuadraticOperator` | +| `'G'` | {class}`opinf.operators.CubicOperator` | +| `'B'` | {class}`opinf.operators.InputOperator` | +| `'N'` | {class}`opinf.operators.StateInputOperator` | + +```python +import opinf + +# Initialize the model with a list of operator objects. +model = opinf.models.DiscreteModel( + operators=[ + opinf.operators.QuadraticOperator(), + opinf.operators.InputOperator(), + ] +) + +# Equivalently, initialize the model with a string. +model = opinf.models.DiscreteModel(operators="HB") +``` + +::: + +The individual operators given to the constructor may or may not have their entries set. +A model's `fit()` method uses Operator Inference to calibrate the operators without entries through a regression problem, see [Learning Operators from Data](sec-operators-calibration). +Once the model operators are calibrated, nonparametric models may use the following methods. + +- `rhs()`: Compute the right-hand side of the model, i.e., $\Ophat(\qhat, \u)$. +- `jacobian()`: Construct the state Jacobian of the right-hand side of the model, i.e, $\ddqhat\Ophat(\qhat,\u)$. +- `predict()`: Solve the model with given initial conditions and/or inputs. + +(sec-models-parametric)= + +## Parametric Models + +A _parametric model_ is a model with at least one [parametric operator](sec-operators-parametric). + +Parametric models are similar to nonparametric models: they are initialized with a list of operators, use `fit()` to calibrate operator entries, and `predict()` to solve the model. +In addition, parametric models have an `evaluate()` method that returns a nonparametric model at a fixed parameter value. + +```{eval-rst} +.. currentmodule:: opinf.models + +.. autosummary:: + :nosignatures: + + ParametricContinuousModel + ParametricDiscreteModel +``` + +### Interpolatory Models + +Interpolatory models consist exclusively of [interpolatory operators](sec-operators-interpolated). + +```{eval-rst} +.. currentmodule:: opinf.models + +.. autosummary:: + :nosignatures: + + InterpContinuousModel + InterpDiscreteModel +``` + +:::{tip} +The `operators` constructor argument for these classes can also be a string that indicates which type of operator to use. + +| Character | {mod}`opinf.operators` class | +| :-------- | :------------------------------- | +| `'c'` | {class}`opinf.operators.InterpConstantOperator` | +| `'A'` | {class}`opinf.operators.InterpLinearOperator` | +| `'H'` | {class}`opinf.operators.InterpQuadraticOperator` | +| `'G'` | {class}`opinf.operators.InterpCubicOperator` | +| `'B'` | {class}`opinf.operators.InterpInputOperator` | +| `'N'` | {class}`opinf.operators.InterpStateInputOperator` | + +```python +import opinf + +# Initialize the model with a list of operator objects. +model = opinf.models.InterpContinuousModel( + operators=[ + opinf.operators.InterpCubicOperator(), + opinf.operators.InterpStateInputOperator(), + ] +) + +# Equivalently, initialize the model with a string. +model = opinf.models.InterpContinuousModel(operators="GN") +``` + +::: + + + + + + diff --git a/sphinx-docs/source/api/operators.md b/sphinx-docs/source/api/operators.md new file mode 100644 index 00000000..ada9e0b6 --- /dev/null +++ b/sphinx-docs/source/api/operators.md @@ -0,0 +1,1205 @@ +--- +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.operators` + ++++ + +```{eval-rst} +.. automodule:: opinf.operators + +.. currentmodule:: opinf.operators + +**Nonparametric Operators** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + OperatorTemplate + InputMixin + OpInfOperator + ConstantOperator + LinearOperator + QuadraticOperator + CubicOperator + InputOperator + StateInputOperator + +**Parametric Operators** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ParametricOperatorTemplate + ParametricOpInfOperator + AffineConstantOperator + AffineLinearOperator + AffineQuadraticOperator + AffineCubicOperator + AffineInputOperator + AffineStateInputOperator + InterpConstantOperator + InterpLinearOperator + InterpQuadraticOperator + InterpCubicOperator + InterpInputOperator + InterpStateInputOperator +``` + ++++ + +:::{admonition} Overview +:class: note + +- `opinf.operators` classes represent the individual terms in a [model](opinf.models). [Nonparametric operators](sec-operators-nonparametric) are functions of the state and input, while [parametric operators](sec-operators-parametric) are also dependent on one or more external parameters. +- Operators that can be written as the product of a matrix and a known vector-valued function can be [calibrated](sec-operators-calibration) through solving a regression problem. +- {mod}`opinf.models` objects are constructed with a list of operator objects. A model's [`fit()`](opinf.models.ContinuousModel.fit) method constructs and solves a regression problem to learn the operator matrices. +::: + + + +```{code-cell} ipython3 +import numpy as np +import scipy.linalg as la + +import opinf +``` + +## Operators + ++++ + +Models based on Operator Inference are systems of [ordinary differential equations](opinf.models.ContinuousModel) (or [discrete-time difference equations](opinf.models.DiscreteModel)) that can be written as a sum of terms, + +$$ +\begin{aligned} + \ddt\qhat(t) + = \sum_{\ell=1}^{n_\textrm{terms}}\Ophat_{\ell}(\qhat(t),\u(t)), +\end{aligned} +$$ (eq:operators:model) + +where each $\Ophat_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{r}$ is a vector-valued function of the reduced state $\qhat\in\RR^{r}$ and the input $\u\in\RR^{m}$. +We call these functions *operators* on this page. + +Operator Inference calibrates operators that can be written as the product of a matrix $\Ohat_{\ell}\in\RR^{r \times d_\ell}$ and a known (possibly nonlinear) vector-valued function $\d_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{d_ell}$, + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat,\u) + = \Ohat_{\ell}\d_{\ell}(\qhat,\u). +\end{aligned} +$$ + +Operators with this structure are called *OpInf operators* on this page. +The goal of Operator Inference is to learn the *operator matrix* $\Ohat_\ell$ for each OpInf operator in the model. + +This module defines classes representing various operators. + ++++ + +:::{admonition} Example +:class: tip + +To represent a linear time-invariant (LTI) system + +$$ +\begin{align} + \ddt\qhat(t) + = \Ahat\qhat(t) + \Bhat\u(t), + \qquad + \Ahat\in\RR^{r \times r}, + ~ + \Bhat\in\RR^{r \times m}, +\end{align} +$$ (eq:operators:ltiexample) + +we use the following operator classes. + +| Class | Definition | Operator matrix | data vector | +| :---- | :--------- | :-------------- | :---------- | +| {class}`LinearOperator` | $\Ophat_{1}(\qhat,\u) = \Ahat\qhat$ | $\Ohat_{1} = \Ahat \in \RR^{r\times r}$ | $\d_{1}(\qhat,\u) = \qhat\in\RR^{r}$ | +| {class}`InputOperator` | $\Ophat_{2}(\qhat,\u) = \Bhat\u$ | $\Ohat_{2} = \Bhat \in \RR^{r\times m}$ | $\d_{2}(\qhat,\u) = \u\in\RR^{m}$ | + +An {class}`opinf.models.ContinuousModel` object can be instantiated with a list of operators objects to represent {eq}`eq:operators:ltiexample`: + +```python +LTI_model = opinf.models.ContinuousModel( + operators=[ + opinf.operators.LinearOperator(), + opinf.operators.InputOperator(), + ] +) +``` + +The operator matrices $\Ahat$ and $\Bhat$ are calibrated by calling [`LTI_model.fit()`](opinf.models.ContinuousModel.fit). +::: + ++++ + +(sec-operators-nonparametric)= +## Nonparametric Operators + ++++ + +A _nonparametric_ operator is a function of the state and input only. +For OpInf operators, this means the operator matrix $\Ohat_\ell$ is constant. +See [Parametric Operators](sec-operators-parametric) for operators that also depend on one or more external parameters. + +Available nonparametric OpInf operators are listed below. + ++++ + +```{eval-rst} +.. autosummary:: + :nosignatures: + + ConstantOperator + LinearOperator + QuadraticOperator + CubicOperator + InputOperator + StateInputOperator +``` + ++++ + +Nonparametric OpInf operators can be instantiated without arguments. +If the operator matrix is known, it can be passed into the constructor or set later with [`set_entries()`](OpInfOperator.set_entries). +The operator matrix is stored as the [`entries`](OpInfOperator.entries) attribute and can be accessed with slicing operations `[:]`. + +```{code-cell} ipython3 +N = opinf.operators.StateInputOperator() +print(N) +``` + +```{code-cell} ipython3 +r = 4 +m = 2 +Ohat = np.arange(r * r * m).reshape((r, r * m)) + +N.set_entries(Ohat) +print(N) +``` + +```{code-cell} ipython3 +N.entries # or N[:] +``` + +There are two ways to determine operator matrices in the context of model reduction: + +- [**Non-intrusive Operator Inference**](sec-operators-calibration): Learn operator matrices from data. +- [**Intrusive (Petrov--)Galerkin Projection**](sec-operators-projection): Compress an existing high-dimensional operator. + +Once the `entries` are set, the following methods are used to compute the action +of the operator or its derivatives. + +- [`apply()`](OperatorTemplate.apply): compute the operator action $\Ophat_\ell(\qhat, \u)$. +- [`jacobian()`](OperatorTemplate.jacobian): construct the state Jacobian $\ddqhat\Ophat_\ell(\qhat, \u)$. + +```{code-cell} ipython3 +qhat = np.zeros(r) +u = np.ones(m) + +N.apply(qhat, u) +``` + +```{code-cell} ipython3 +N.jacobian(qhat, u) +``` + +(sec-operators-calibration)= +### Learning Operators from Data + ++++ + +:::{attention} +This section describes the crux of Operator Inference. +::: + ++++ + +Operator Inference requires state, input, and "left-hand side" data $\{(\qhat_j,\u_j,\z_j)\}_{j=0}^{k-1}$ that approximately satisfy the desired model dynamics. +For [time-continuous models](opinf.models.ContinuousModel), $\z_j$ is the [time derivative](opinf.ddt) of the state data; for [fully discrete models](opinf.models.DiscreteModel), $\z_j$ is the ``next state,'' usually $\qhat_{j+1}$. +For {eq}`eq:operators:model`, and assuming each operator is an OpInf operator, the data should approximately satisfy + +$$ +\begin{aligned} + \z_j + \approx \Ophat(\qhat_j, \u_j) + = \sum_{\ell=1}^{n_\textrm{terms}} \Ophat_{\ell}(\qhat_j, \u_j) + = \sum_{\ell=1}^{n_\textrm{terms}} \Ohat_{\ell}\d_{\ell}(\qhat_j, \u_j), + \qquad + j = 0, \ldots, k-1. +\end{aligned} +$$ (eq:operators:approx) + +Operator Inference determines the operator matrices $\Ohat_1,\ldots,\Ohat_{n_\textrm{terms}}$ through a regression problem, written generally as + +$$ +\begin{aligned} + \text{find}\quad\Ohat\quad\text{such that}\quad + \Z \approx \Ohat\D\trp + \quad\Longleftrightarrow\quad + \D\Ohat\trp \approx \Z\trp, +\end{aligned} +$$ + +where $\D$ is the $k \times d$ *data matrix* formed from state and input snapshots and $\Z$ is the $r \times k$ matrix of left-hand side data. +To arrive at this problem, we write + +$$ +\begin{aligned} + \z_j + \approx \sum_{\ell=1}^{n_\textrm{terms}} \Ohat_{\ell}\d_{\ell}(\qhat_j, \u_j) + = [~\Ohat_{1}~~\cdots~~\Ohat_{n_\textrm{terms}}~] + \left[\begin{array}{c} + \d_{1}(\qhat_j, \u_j) + \\ \vdots \\ + \d_{n_\textrm{terms}}(\qhat_j, \u_j) + \end{array}\right]. +\end{aligned} +$$ + +Collecting this matrix-vector product for each $j=0,\ldots,k-1$ results in the matrix-matrix product system + +$$ +\begin{aligned} + \left[\begin{array}{c|c|c} + & & \\ + \z_0 & \cdots & \z_{k-1} + \\ & & + \end{array}\right] + \approx + [~\Ohat_{1}~~\cdots~~\Ohat_{n_\textrm{terms}}~] + \left[\begin{array}{ccc} + \d_{1}(\qhat_0, \u_0) & \cdots & \d_{1}(\qhat_{k-1}, \u_{k-1}) + \\ \vdots & & \vdots \\ + \d_{n_\textrm{terms}}(\qhat_0, \u_0) & \cdots & \d_{n_\textrm{terms}}(\qhat_{k-1}, \u_{k-1}) + \end{array}\right], +\end{aligned} +$$ + +which is $\Z \approx \Ohat\D\trp$ with + +$$ +\begin{aligned} + \Z + &= [~\z_0~~\cdots~~\z_{k-1}~] \in \RR^{r\times k}, + \\ & \\ + \Ohat + &= [~\Ohat_{1}~~\cdots~~\Ohat_{n_\textrm{terms}}~] \in \RR^{r \times d}, + \\ & \\ + \D\trp + &= \left[\begin{array}{ccc} + \d_{1}(\qhat_0, \u_0) & \cdots & \d_{1}(\qhat_{k-1}, \u_{k-1}) + \\ \vdots & & \vdots \\ + \d_{n_\textrm{terms}}(\qhat_0, \u_0) & \cdots & \d_{n_\textrm{terms}}(\qhat_{k-1}, \u_{k-1}) + \end{array}\right] + \in \RR^{d \times k}, +\end{aligned} +$$ + +where $d = \sum_{i=1}^{n_\textrm{terms}}d_\ell$. + ++++ + +Nonparametric OpInf operator classes have two static methods that facilitate constructing the operator regression problem. + +- [`operator_dimension()`](OpInfOperator.operator_dimension): given the state dimension $r$ and the input dimension $r$, return the data dimension $d_\ell$. +- [`datablock()`](OpInfOperator.datablock): given the state-input data pairs $\{(\qhat_j,\u_j)\}_{j=0}^{k-1}$, form the matrix + +$$ +\begin{aligned} + \D_{\ell}\trp = \left[\begin{array}{c|c|c|c} + & & & \\ + \d_{\ell}(\qhat_0,\u_0) & \d_{\ell}(\qhat_1,\u_1) & \cdots & \d_{\ell}(\qhat_{k-1},\u_{k-1}) + \\ & & & + \end{array}\right] + \in \RR^{d_{\ell} \times k}. +\end{aligned} +$$ + +The complete data matrix $\D$ is the concatenation of the data matrices from each operator: + +$$ +\begin{aligned} + \D = \left[\begin{array}{ccc} + & & \\ + \D_1 & \cdots & \D_{n_\textrm{terms}} + \\ & & + \end{array}\right]. +\end{aligned} +$$ + ++++ + +:::{admonition} Model Classes Do the Work +:class: important + +Model classes from {mod}`opinf.models` are instantiated with a list of operators. +The model's `fit()` method calls the [`datablock()`](OpInfOperator.datablock) method of each OpInf operator to assemble the full data matrix $\D$, solves the regression problem for the full data matrix $\Ohat$ (see {mod}`opinf.lstsq`), and extracts from $\Ohat$ the individual operator matrix $\Ohat_{\ell}$ for each $\ell = 1, \ldots, n_{\textrm{terms}}$ using the [`operator_dimension()`](OpInfOperator.operator_dimension). + ++++ + +:::{admonition} Example +:class: tip + +For the LTI system {eq}`eq:operators:ltiexample`, the Operator Inference regression $\Z \approx \Ohat\D\trp$ has the following matrices. + +$$ +\begin{aligned} + \Z + &= [~\z_0~~\cdots~~\z_{k-1}~] \in \RR^{r\times k}, + \\ \\ + \Ohat + &= [~\Ahat~~\Bhat~] \in \RR^{r \times (r + m)}, + \\ \\ + \D\trp + &= \left[\begin{array}{ccc} + \qhat_0 & \cdots & \qhat_{k-1} + \\ + \u_0 & \cdots & \u_{k-1} + \end{array}\right] + \in \RR^{(r + m) \times k}. +\end{aligned} +$$ + +In this setting, $\z_j = \dot{\qhat}_j$, the time derivative of $\qhat_j$. +Collecting the state snapshots in the matrix $\Qhat = [~\qhat_0~~\cdots~~\qhat_{k-1}~]\in\RR^{r\times k}$ and the inputs in the matrix $\U = [~\u_0~~\cdots~~\u_{k-1}~]$, the full data matrix can be abbreviated as $\D = [~\Qhat\trp~~\U\trp~]$. + +If the regression $\Z \approx \Ohat\D\trp$ is treated as an [ordinary least-squares problem](opinf.lstsq.PlainSolver), the optimization problem to solve is given by + +$$ +\begin{aligned} + \min_{\Ohat}\left\| + \D\Ohat\trp - \Z\trp + \right\|_F^2 + = \min_{\Ahat,\Bhat}\sum_{j=0}^{k-1}\left\| + \Ahat\qhat_j + \Bhat\u_j - \z_j + \right\|_2^2. +\end{aligned} +$$ + +That is, the ordinary least-squares Operator Inference regression minimizes a sum of residuals of the model equation {eq}`eq:operators:ltiexample` with respect to available data. +::: + ++++ + +:::{admonition} Operators with Entries are _Not_ Recalibrated +:class: important + +Only operators whose entries are _not initialized_ (set to `None`) when a model is constructed are learned with Operator Inference when [`fit()`](opinf.models.ContinuousModel.fit) is called. +For example, suppose for the LTI system {eq}`eq:operators:ltiexample` an appropriate input matrix $\Bhat$ is known and stored as the variable `B_`. + +```python +LTI_model = opinf.models.ContinuousModel( + operators=[ + opinf.operators.LinearOperator(), # No entries specified. + opinf.operators.InputOperator(B_), # Entries set to B_. + ] +) +``` + +In this case, [`LIT_model.fit()`](opinf.models.ContinuousModel.fit) only determines the entries of the {class}`LinearOperator` object using Operator Inference. +The known information for $\Bhat$ is absorbed into the matrix $\Z$, so the Operator Inference regression $\Z \approx \Ohat\D\trp$ is now defined with the matrices + +$$ +\begin{aligned} + \Z + &= [~(\z_0 - \Bhat\u_0)~~\cdots~~(\z_{k-1} - \Bhat\u_{k-1})~] \in \RR^{r\times k}, + \\ + \Ohat + &= \Ahat \in \RR^{r \times r}, + \qquad + \D\trp + = \Qhat \in \RR^{r \times k}. +\end{aligned} +$$ + +Using ordinary least-squares regression, the optimization problem is given by + +$$ +\begin{aligned} + &\min_{\Ahat}\sum_{j=0}^{k-1}\left\| + \Ahat\qhat_j - (\z_j - \Bhat\u_j) + \right\|_2^2. +\end{aligned} +$$ + +A more likely scenario is that $\Bhat\in\RR^{r \times m}$ is derived from a known $\B\in\RR^{n \times m}$, which is the subject of the next section. +::: + ++++ + +(sec-operators-projection)= +### Learning Operators via Projection + ++++ + +The goal of Operator Inference is to learn operator entries from data because the operators of a high-fidelity / full-order model are unknown or computationally inaccessible. +However, in some scenarios a subset of the full-order model operators are known, in which case the corresponding reduced-order model operators can be determined through *intrusive projection*. +Consider a full-order operator $\Op:\RR^{n}\times\RR^{m}\to\RR^{n}$, written $\Op_{\ell}(\q,\u)$, where + +- $\q\in\RR^n$ is the full-order state, and +- $\u\in\RR^m$ is the input. + +Given a *trial basis* $\Vr\in\RR^{n\times r}$ and a *test basis* $\Wr\in\RR^{n\times r}$, the corresponding intrusive projection of $\Op_{\ell}$ is the operator $\Ophat_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{r}$ defined by + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat, \u) + = (\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}(\Vr\qhat, \u) +\end{aligned} +$$ + +where +- $\qhat\in\RR^{r}$ is the reduced-order state, and +- $\u\in\RR^{m}$ is the input (the same as before). + +This approach uses the low-dimensional state approximation $\q \approx \Vr\qhat$. +If $\Wr = \Vr$, the result is called a *Galerkin projection*. +Note that if $\Vr$ has orthonormal columns, we have in this case the simplification + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat, \u) + = \Vr\trp\Op_{\ell}(\Vr\qhat, \u). +\end{aligned} +$$ + +If $\Wr \neq \Vr$, the result is called a *Petrov--Galerkin projection*. + ++++ + +:::{admonition} Example +:class: tip + +Consider the bilinear operator +$\Op(\q,\u) = \N[\u\otimes\q]$ where $\N\in\RR^{n \times nm}$. +This type of operator can represented as a {class}`StateInputOperator`. +The intrusive Petrov--Galerkin projection of $\Op$ is the bilinear operator + +$$ +\begin{aligned} + \Ophat(\qhat,\u) + = (\Wr\trp\Vr)^{-1}\Wr\trp\N[\u\otimes\Vr\qhat] + = \Nhat[\u\otimes\qhat] +\end{aligned} +$$ + +where $\Nhat = (\Wr\trp\Vr)^{-1}\Wr\trp\N(\I_m\otimes\Vr) \in \RR^{r\times rm}$. +Hence, $\Ophat$ can also be represented as a {class}`StateInputOperator`. +Using Galerkin projection ($\Wr = \Vr$), $\Nhat$ simplifies to $\Nhat = \Vr\trp\N(\I_m\otimes\Vr)$. +::: + +Every operator class has a [`galerkin()`](OperatorTemplate.galerkin) method that performs intrusive projection. + ++++ + +### Custom Nonparametric Operators + ++++ + +New nonparametric operators can be defined by inheriting from {class}`OperatorTemplate` or, for operators that can be calibrated through Operator Inference, {class}`OpInfOperator`. +Once implemented, the [`verify()`](OperatorTemplate.verify) method may be used to test for consistency between [`apply()`](OperatorTemplate.apply) and the other methods outlined below. + +For an arbitrary operator (not calibrated through Operator Inference), use the following inheritance template. + +```{code-cell} ipython3 +:tags: [hide-input] + +class MyOperator(opinf.operators.OperatorTemplate): + """Custom non-OpInf nonparametric operator.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, args_and_kwargs): + """Construct the operator and set the state_dimension.""" + raise NotImplementedError + + # Required properties and methods ----------------------------------------- + @property + def state_dimension(self): + """Dimension of the state that the operator acts on.""" + return NotImplemented + + def apply(self, state, input_=None): + """Apply the operator to the given state / input.""" + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + def jacobian(self, state, input_=None): + """Construct the state Jacobian of the operator.""" + raise NotImplementedError + + def galerkin(self, Vr: np.ndarray, Wr=None): + """Get the (Petrov-)Galerkin projection of this operator.""" + raise NotImplementedError + + def save(self, savefile, overwrite=False): + """Save the operator to an HDF5 file.""" + raise NotImplementedError + + @classmethod + def load(cls, loadfile): + """Load an operator from an HDF5 file.""" + raise NotImplementedError + + def copy(self): + """Make a copy of the operator. + If not implemented, copy.deepcopy() is used. + """ + raise NotImplementedError +``` + +See {class}`OperatorTemplate` for details on the arguments for each method. + +For a new Operator Inference operator, use the following inheritance template. + +```{code-cell} ipython3 +:tags: [hide-input] + +class MyOpInfOperator(opinf.operators.OpInfOperator): + """Custom nonparametric OpInf operator.""" + + # Required methods -------------------------------------------------------- + @opinf.utils.requires("entries") + def apply(self, state=None, input_=None): + """Apply the operator to the given state / input.""" + raise NotImplementedError + + @staticmethod + def datablock(states, inputs=None): + """Return the data matrix block corresponding to the operator.""" + raise NotImplementedError + + @staticmethod + def operator_dimension(r=None, m=None): + """Column dimension of the operator entries matrix.""" + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + @opinf.utils.requires("entries") + def jacobian(self, state, input_=None): + """Construct the state Jacobian of the operator. + NOTE: If this method is omitted it is assumed that the Jacobian is + zero, implying that the operator does not depend on the state. + """ + raise NotImplementedError + + def galerkin(self, Vr, Wr=None): + """Get the (Petrov-)Galerkin projection of this operator.""" + raise NotImplementedError +``` + +See {class}`OpInfOperator` for details on the arguments for each method. + ++++ + +:::{admonition} Developer Notes +:class: note + +- If the operator depends on the input $\u$, the class should also inherit from {class}`InputMixin` and set the [`input_dimension`](InputMixin.input_dimension) attribute. +- The [`jacobian()`](OperatorTemplate.jacobian) method is optional, but {mod}`opinf.models` objects have a `jacobian()` method that calls `jacobian()` for each of its operators. In an {class}`opinf.models.ContinuousModel`, the Jacobian is required for some time integration strategies used in [`predict()`](opinf.models.ContinuousModel.predict). +- The [`galerkin()`](OperatorTemplate.galerkin) method is optional, but {mod}`opinf.models` objects have a `galerkin()` method that calls `galerkin()` for each of its operators. +- The [`save()`](OperatorTemplate.save) and [`load()`](OperatorTemplate.load) methods should be implemented using {func}`opinf.utils.hdf5_savehandle()` and {func}`opinf.utils.hdf5_loadhandle()`, respectively. +::: + ++++ + +#### Example: Hadamard Product with a Fixed Vector + ++++ + +Consider the operator $\Ophat_{\ell}(\qhat, \u) = \qhat \ast \hat{\s}$, where $\hat{\s}\in\RR^{r}$ is a constant vector and $\ast$ denotes the [Hadamard (elementwise) product](https://en.wikipedia.org/wiki/Hadamard_product_(matrices)) (`*` in NumPy). +To implement an operator for this class, we first calculate its state Jacobian and determine the operator produced by (Petrov--)Galerkin projection. + +Let $\qhat = [~\hat{q}_1~~\cdots~~\hat{q}_r~]\trp$ and $\hat{\s} = [~\hat{s}_1~~\cdots~~\hat{s}_r~]\trp$, i.e., the $i$-th entry of $\Ophat_{\ell}(\qhat, \u)$ is $\hat{q}_i\hat{s}_i$. +Then the $(i,j)$-th entry of the Jacobian is + +$$ +\begin{aligned} + \frac{\partial}{\partial \hat{q}_j}\left[\hat{q}_i\hat{s}_i\right] + = \begin{cases} + \hat{s}_i & \textrm{if}~i = j, + \\ + 0 & \textrm{else}. + \end{cases} +\end{aligned} +$$ + +That is, $\ddqhat\Ophat_{\ell}(\qhat, \u) = \operatorname{diag}(\hat{\s}).$ + +Now consider a version of this operator with a large state dimension, $\Op_{\ell}(\q, \u) = \q \ast \s$ for $\q,\s\in\mathbb{R}^{n}$. +For basis matrices $\Vr,\Wr\in\mathbb{R}^{n \times r}$, the Petrov--Galerkin projection of $\Op_{\ell}$ is given by + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat, \u) + = (\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}(\Vr\qhat, \u) + = (\Wr\trp\Vr)^{-1}\Wr\trp((\Vr\qhat)\ast\s). +\end{aligned} +$$ + +It turns out that this product can be written as a matrix-vector product $\Ahat\qhat$ where $\Ahat$ depends on $\Vr$ and $\s$. +Therefore, `galerkin()` should return a {class}`LinearOperator` with entries matrix $\Ahat$. + +The following class inherits from {class}`OperatorTemplate`, stores $\hat{\s}$ and sets the state dimension $r$ in the constructor, and implements the methods outlined the inheritance template. + +```{code-cell} ipython3 +class HadamardOperator(opinf.operators.OperatorTemplate): + """Custom non-OpInf nonparametric operator for the Hadamard product.""" + + # Constructor ------------------------------------------------------------- + def __init__(self, s): + """Construct the operator and set the state_dimension.""" + self.svector = np.array(s) + self._jac = np.diag(self.svector) + + # Required properties and methods ----------------------------------------- + @property + def state_dimension(self): + """Dimension of the state that the operator acts on.""" + return self.svector.shape[0] + + def apply(self, state, input_=None): + """Apply the operator to the given state / input.""" + svec = self.svector + if state.ndim == 2: + svec = svec.reshape((self.state_dimension, 1)) + return state * svec + + # Optional methods -------------------------------------------------------- + def jacobian(self, state, input_=None): + """Construct the state Jacobian of the operator.""" + return self._jac + + def galerkin(self, Vr, Wr=None): + """Get the (Petrov-)Galerkin projection of this operator.""" + if Wr is None: + Wr = Vr + n = self.state_dimension + r = Vr.shape[1] + + M = la.khatri_rao(Vr.T, np.eye(n)).T + Ahat = Wr.T @ (M.reshape((n, r, n)) @ self.svector) + if not np.allclose((WrTVr := Wr.T @ Vr), np.eye(r)): + Ahat = la.solve(WrTVr, Ahat) + return opinf.operators.LinearOperator(Ahat) + + def save(self, savefile, overwrite=False): + """Save the operator to an HDF5 file.""" + with opinf.utils.hdf5_savehandle(savefile, overwrite) as hf: + hf.create_dataset("svector", data=self.svector) + + @classmethod + def load(cls, loadfile): + """Load an operator from an HDF5 file.""" + with opinf.utils.hdf5_loadhandle(loadfile) as hf: + return cls(hf["svector"][:]) + + def copy(self): + """Make a copy of the operator.""" + return self.__class__(self.svector) +``` + +```{code-cell} ipython3 +r = 10 +svec = np.random.standard_normal(r) +hadamard = HadamardOperator(svec) + +print(hadamard) +``` + +```{code-cell} ipython3 +hadamard.verify() +``` + +#### Example: Weighted Hadamard Input Operator + ++++ + +Consider an operator + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat,\u) + = \Ohat_{\ell}(\u\ast\u), +\end{aligned} +$$ + +where $\Ohat_{\ell}\in\RR^{r \times m}$ and $\ast$ is the Hadamard (elementwise) product. +The matrix $\Ohat_{\ell}$ can be calibrated with Operator Inference. +Since $\Ophat_{\ell}$ does not depend on the state $\qhat$, the state Jacobian is zero. + +```{code-cell} ipython3 +class HadamardInputOperator( + opinf.operators.OpInfOperator, + opinf.operators.InputMixin, +): + """Custom nonparametric OpInf operator: Hadamard of inputs.""" + + # Required methods -------------------------------------------------------- + @property + def input_dimension(self): + """Dimension of the input that the operator acts on.""" + return self.entries.shape[1] if self.entries is not None else None + + @opinf.utils.requires("entries") + def apply(self, state=None, input_=None): + """Apply the operator to the given state / input.""" + return self.entries @ (input_**2) + + @staticmethod + def datablock(states, inputs=None): + """Return the data matrix block corresponding to the operator.""" + return inputs**2 + + @staticmethod + def operator_dimension(r=None, m=None): + """Column dimension of the operator entries matrix.""" + return m + + # Optional methods -------------------------------------------------------- + def galerkin(self, Vr, Wr=None): + """Get the (Petrov-)Galerkin projection of this operator.""" + if Wr is None: + Wr = Vr + r = Vr.shape[1] + + Ohat = Wr.T @ self.entries + if not np.allclose((WrTVr := Wr.T @ Vr), np.eye(r)): + Ohat = la.solve(WrTVr, Ohat) + + return self.__class__(Ohat) +``` + +```{code-cell} ipython3 +# Instantiate an operator without entries. +hadamard_input = HadamardInputOperator() + +print(hadamard_input) +``` + +```{code-cell} ipython3 +hadamard_input.verify() +``` + +```{code-cell} ipython3 +m = 4 +Ohat = np.random.random((r, m)) +hadamard_input.set_entries(Ohat) + +print(hadamard_input) +``` + +```{code-cell} ipython3 +hadamard_input.verify() +``` + +(sec-operators-parametric)= +## Parametric Operators + ++++ + +An operator is called _parametric_ if it depends on an independent parameter vector +$\bfmu\in\RR^{p}$, i.e., $\Ophat_{\ell} = \Ophat_{\ell}(\qhat,\u;\bfmu)$ +When the parameter vector is fixed, a parametric operator becomes nonparametric. +In particular, a parametric operator's [`evaluate()`](ParametricOperatorTemplate.evaluate) method accepts a parameter vector $\bfmu$ and returns an instance of a nonparametric operator. + ++++ + +Parametric OpInf operators have the form +$\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ohat_{\ell}(\bfmu)\d_{\ell}(\qhat,\u)$ defined by the matrix-valued function $\Ohat_{\ell}:\RR^{p}\to\RR^{r\times d_\ell}$ and (as in the nonparametric case) the data vector $\d_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{d_\ell}$. +This module provides two options for the parameterization of $\Ohat_{\ell}(\bfmu)$: [affine expansion](sec-operators-affine) and [elementwise interpolation](sec-operators-interpolated). +In each case, Operator Inference begins with $s$ training parameter values $\bfmu_{0},\ldots,\bfmu_{s-1}$ and corresponding state, input, and left-hand side data $\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}$ for each training parameter value $\bfmu_{i}$. +A regression of the form $\Z \approx \Ohat\D\trp$ is formed as in the nonparametric case, with the structure of the matrices $\Ohat$ and $\D$ depending on the choice of parameterization for each $\Ohat_{\ell}(\bfmu)$. + ++++ + +:::{admonition} Example +:class: tip +Let $\bfmu = [~\mu_{0}~~\mu_{1}~]\trp$. +The operator + +$$ +\begin{aligned} + \Ophat_1(\qhat,\u;\bfmu) = (\mu_{0}\Ahat_{0} + \mu_{1}^{2}\Ahat_{1})\qhat +\end{aligned} +$$ + +is a parametric OpInf operator because it can be written as $\Ophat_1(\qhat,\u;\bfmu) = \Ohat_1(\bfmu)\d_1(\qhat,\u)$ with $\Ohat_1(\bfmu) = \mu_{0}\Ahat_{0} + \mu_{1}^{2}\Ahat_{1}$ and $\d_1(\qhat,\u) = \qhat$. + +This operator can be represented with an {class}`AffineLinearOperator`. +For a given parameter value, the [`evaluate()`](AffineLinearOperator.evaluate) method returns a {class}`LinearOperator` instance. +::: + ++++ + +(sec-operators-affine)= +### Affine Operators + ++++ + +Affine parametric OpInf operators $\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ohat_{\ell}(\bfmu)\d_{\ell}(\qhat,\u)$ parameterize the operator matrix $\Ohat_{\ell}(\bfmu)$ as a sum of constant matrices with parameter-dependent scalar coefficients, + +$$ +\begin{aligned} + \Ohat_{\ell}(\bfmu) + &= \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ohat_{\ell}^{(a)}, +\end{aligned} +$$ + +where each $\theta_{\ell}^{(a)}:\RR^{p}\to\RR$ is a scalar-valued function and each $\Ohat_{\ell}^{(a)}\in\RR^{r\times d_\ell}$ is constant. +Affine expansions are grouped such that the coefficient functions $\theta_{\ell}^{(0)},\ldots,\theta_{\ell}^{(A_{\ell}-1)}$ are linearly independent. + ++++ + +Affine parametric operators arise in Operator Inference settings because linear projection preserves affine structure. + +:::{dropdown} Preservation of Affine Structure + +Consider a full-order affine parametric OpInf operator + +$$ +\begin{aligned} + \Op_{\ell}(\q,\u;\bfmu) + = \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Op_{\ell}^{(a)}\!(\q, \u). +\end{aligned} +$$ + +Given a trial basis $\Vr\in\RR^{n\times r}$ and a test basis $\Wr\in\RR^{n\times r}$, the [intrusive projection](sec-operators-projection) of $\Op_{\ell}$ is the operator + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat, \u; \bfmu) + &= (\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}(\Vr\qhat, \u; \bfmu) + \\ + &= (\Wr\trp\Vr)^{-1}\Wr\trp \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Op_{\ell}^{(a)}\!(\V\qhat, \u) + \\ + &= \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,(\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}^{(a)}\!(\V\qhat, \u) + = \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ophat_{\ell}^{(a)}\!(\qhat, \u), +\end{aligned} +$$ + +where $\Ophat_{\ell}^{(a)}\!(\qhat, \u) = (\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}^{(a)}\!(\V\qhat, \u)$ is the intrusive projection of $\Op_{\ell}^{(a)}$. +That is, the intrusive projection of an affine expansion is an affine expansion of intrusive projections, and both expansions feature the same coefficient functions. +::: + ++++ + +Available affine parametric operators are listed below. + +```{eval-rst} +.. currentmodule:: opinf.operators + +.. autosummary:: + :nosignatures: + + AffineConstantOperator + AffineLinearOperator + AffineQuadraticOperator + AffineCubicOperator + AffineInputOperator + AffineStateInputOperator +``` + ++++ + +Affine parametric operators are instantiated with a function $\boldsymbol{\theta}_{\ell}(\mu) = [~\theta_{\ell}^{(0)}(\bfmu)~~\cdots~~\theta_{\ell}^{(A_{\ell}-1)}(\bfmu)~]\trp$ for the affine expansion coefficients, the number of terms $A_{\ell}$ in the expansion, and with or without the operator matrices $\Ohat_{\ell}^{(1)},\ldots,\Ohat_{\ell}^{(A_{\ell})}$. + +```{code-cell} ipython3 +thetas = lambda mu: np.array([mu[0], mu[1] ** 2]) +A = opinf.operators.AffineLinearOperator(thetas, nterms=2) +print(A) +``` + +```{code-cell} ipython3 +# Set the constant operator matrices in the affine expansion. +r = 5 +Ahats = [np.ones((r, r)), np.eye(r)] +A.set_entries(Ahats, fromblock=False) + +print(A) +``` + +```{code-cell} ipython3 +A.entries +``` + +```{code-cell} ipython3 +# Evaluate the parametric operator at a fixed parameter value, +# resulting in a nonparametric operator. +A_nonparametric = A.evaluate([2, 4]) +print(A_nonparametric) +``` + +```{code-cell} ipython3 +A_nonparametric.entries +``` + +```{code-cell} ipython3 +# The parameter dimension p = 2 was recorded when A was evaluated. +print(A) +``` + +:::{dropdown} Operator Inference for Affine Operators + +Consider a model with a single affine operator, + +$$ +\begin{aligned} + \z + = \Ophat_{1}(\qhat,\u;\bfmu) + &= \left(\sum_{a=0}^{A_{1}-1}\theta_{1}^{(a)}\!(\bfmu)\,\Ohat_{1}^{(a)}\right)\d_1(\qhat,\u). +\end{aligned} +$$ + +For the moment we will neglect the $\ell = 1$ subscript. +With data $\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}$ corresponding to $s$ training parameter values $\bfmu_0,\ldots,\bfmu_{s-1}$, we seek the $A$ operator matrices $\Ohat^{(0)},\ldots,\Ohat^{(A-1)}$ such that for each parameter index $i$ and time index $j$, we have + +$$ +\begin{aligned} + \z_{i,j} + \approx \Ophat(\qhat_{i,j},\u_{i,j};\bfmu_{i}) + &= \left(\sum_{a=0}^{A-1}\theta^{(a)}\!(\bfmu_{i})\,\Ohat^{(a)}\right)\d(\qhat_{i,j},\u_{i,j}) + \\ + &= \left[\begin{array}{ccc} + \Ohat^{(0)} & \cdots & \Ohat^{(A-1)} + \end{array}\right] + \underbrace{\left[\begin{array}{c} + \theta^{(0)}\!(\bfmu_{i})\,\d(\qhat_{i,j},\u_{i,j}) + \\ \vdots \\ + \theta^{(A-1)}\!(\bfmu_{i})\,\d(\qhat_{i,j},\u_{i,j}) + \end{array}\right]}_{\d_{i,j}\in\RR^{dA}}, +\end{aligned} +$$ + +where $d$ is the output dimension of $\d$. +Collecting these expressions for each time index $j = 0, \ldots, k_i - 1$ (but keeping the parameter index $i$ fixed for the moment) results in + +$$ +\begin{aligned} + \underbrace{\left[\begin{array}{ccc} + \z_{i,0} & \cdots & \z_{i,k_i-1} + \end{array}\right]}_{\Z_i\in\RR^{r\times k_i}} + \approx \left[\begin{array}{ccc} + \Ohat^{(0)} & \cdots & \Ohat^{(A-1)} + \end{array}\right] + \underbrace{\left[\begin{array}{ccc} + \d_{i,0} & \cdots & \d_{i,k_i-1} + \end{array}\right]}_{\D_i\trp\in\RR^{dA \times k_i}}. +\end{aligned} +$$ + +Finally, we concatenate each of these expressions for each parameter index $i = 0,\ldots, s-1$ to arrive at + +$$ +\begin{aligned} + \underbrace{\left[\begin{array}{ccc} + \Z_{0} & \cdots & \Z_{s-1} + \end{array}\right]}_{\Z\in\RR^{r\times K}} + \approx \left[\begin{array}{ccc} + \Ohat^{(0)} & \cdots & \Ohat^{(A-1)} + \end{array}\right] + \underbrace{\left[\begin{array}{ccc} + \D_{0}\trp & \cdots & \D_{s-1}\trp + \end{array}\right]}_{\D\trp\in\RR^{dA \times K}}, +\end{aligned} +$$ + +where $K = \sum_{i=0}^{s-1}k_i$, the total number of available data instances. +This is the familiar $\Z \approx \Ohat\D\trp$ where $\Ohat = [~\Ohat^{(1)}~~\cdots~~\Ohat^{(A)}~]$, which can be solved for $\Ohat$ using {mod}`opinf.lstsq`. + +The construction of $\D$ is taken care of through the [`datablock()`](AffineLinearOperator.datablock) method of the affine + +For models with multiple affine operators, the operator matrix $\Ohat$ is further concatenated horizontally to accommodate the operator matrices from each affine expansion, and the data matrix $\D\trp$ gains additional block rows. +::: + ++++ + +(sec-operators-interpolated)= +### Interpolatory Operators + ++++ + +Interpolatory parametric OpInf operators define the parametric dependence of the operator matrix on $\bfmu$ through elementwise interpolation. +That is, + +$$ +\begin{aligned} + \Ophat_{\ell}(\qhat,\u;\bfmu) + = \Ohat_{\ell}(\bfmu)\d_{\ell}(\qhat,\u), +\end{aligned} +$$ + +where $\Ohat_{\ell}(\bfmu)$ is determined by interpolating $s$ matrices $\Ohat_{\ell}^{(0)},\ldots,\Ohat_{\ell}^{(s-1)}$. +In the context of Operator Inference, $s$ is the number of training parameter values. + ++++ + +Available interpolatory operators are listed below. + +```{eval-rst} +.. currentmodule:: opinf.operators + +.. autosummary:: + :nosignatures: + + InterpConstantOperator + InterpLinearOperator + InterpQuadraticOperator + InterpCubicOperator + InterpInputOperator + InterpStateInputOperator +``` + ++++ + +Interpolatory operators can be instantiated with no arguments. + +```{code-cell} ipython3 +B = opinf.operators.InterpInputOperator() +print(B) +``` + +```{code-cell} ipython3 +s = 9 # Number of training parameters +p = 1 # Dimension of the training parameters. +r = 4 # Dimension of the states. +m = 2 # Dimension of the inputs. + +training_parameters = np.random.standard_normal((s, p)) +operator_matrices = [np.random.random((r, m)) for _ in range(s)] + +B.set_training_parameters(training_parameters) +print(B) +``` + +```{code-cell} ipython3 +B.set_entries(operator_matrices, fromblock=False) +print(B) +``` + +```{code-cell} ipython3 +B_nonparametric = B.evaluate(np.random.standard_normal(p)) +print(B_nonparametric) +``` + +:::{dropdown} Operator Inference for Interpolatory Operators + +Consider a model with a single affine operator, + +$$ +\begin{aligned} + \z + = \Ophat_{1}(\qhat,\u;\bfmu) + = \Ohat_{1}(\bfmu)\d_{1}(\qhat,\u). +\end{aligned} +$$ + +For the moment we will neglect the $\ell = 1$ subscript. +With data $\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}$ corresponding to $s$ training parameter values $\bfmu_0,\ldots,\bfmu_{s-1}$, we seek $s$ operator matrices $\Ohat^{(0)},\ldots,\Ohat^{(s-1)}\in\RR^{r\times d}$ such that for each parameter index $i$ and time index $j$, we have + +$$ +\begin{aligned} + \z_{i,j} + \approx \Ophat(\qhat_{i,j},\u_{i,j};\bfmu_{i}) + = \Ohat^{(i)}\d(\qhat_{i,j},\u_{i,j}), +\end{aligned} +$$ + +which comes from the interpolation condition $\Ohat^{(i)} = \Ohat_{1}(\bfmu_{i})$ for $i = 0,\ldots,s-1$. +Because only one operator matrix $\Ohat^{(i)}$ defines the operator action at each parameter value for which we have data, we have $s$ independent nonparametric Operator Inference problems: + +$$ +\begin{aligned} + \Z_i + &\approx \Ohat^{(i)}\D_i\trp, + \\ + \Z_i + &= \left[\begin{array}{ccc} + \z_{i,0} & \cdots & \z_{i,k_{i}-1} + \end{array}\right]\in\RR^{r \times k_{i}}, + \\ + \D_i\trp + &= \left[\begin{array}{ccc} + \d(\qhat_{i,0}, \u_{i,0}) & \cdots & \d(\qhat_{i,k_{i}-1}, \u_{i,k_{i}-1}) + \end{array}\right]\in\RR^{d\times k_{i}} +\end{aligned} +$$ + +The InterpolatedModel classes represent models comprised solely of interpolatory operators. +If interpolatory operators are mixed with other operators (nonparametric or affine parametric), the $\Ohat\D\trp$ block of the problem for the interpolatory operator is included as follows: + +$$ +\begin{aligned} + \left[\begin{array}{ccc} + \Ohat^{(0)} & \cdots & \Ohat^{(s-1)} + \end{array}\right] + \left[\begin{array}{cccc} + \D_0\trp & \0 & \cdots & \0 \\ + \0 & \D_1\trp & \cdots & \0 \\ + \vdots & \vdots & \ddots & \vdots \\ + \0 & \0 & \cdots & \D_{s-1}\trp + \end{array}\right] +\end{aligned} +$$ +::: + ++++ + +:::{dropdown} Mixing Nonparametric and Parametric Operators + +Consider a system of ODEs with a mix of parametric and nonparametric operators, + +$$ +\begin{aligned} + \ddt\qhat(t) + = \left(\mu_{0}\Ahat^{(0)} + \cos(\mu_{1})\Ahat^{(1)}\right)\qhat(t) + \Hhat[\qhat(t) \otimes \qhat(t)] + \Bhat(\bfmu)\u(t). +\end{aligned} +$$ + +This model can be written in the general form {eq}`eq:operators:model` with three operators: + +- $\Ophat_1(\qhat,\u;\bfmu) = \left(\theta^{(0)}\!(\bfmu)\,\Ahat^{(0)} + \theta^{(1)}\!(\bfmu)\,\Ahat^{(1)}\right)\qhat(t)$, an affine-parametric linear operator where $\theta^{(0)}\!(\bfmu) = \mu_{0}$ and $\theta^{(1)}\!(\bfmu) = \cos(\mu_{1})$; +- $\Ophat_2(\qhat,\u) = \Hhat[\qhat(t) \otimes \qhat(t)]$, a nonparametric quadratic operator; and +- $\Ophat_3(\qhat,\u;\bfmu) = \Bhat(\bfmu)\u(t)$, a parametric input operator without a specified parametric structure. + +If $\Bhat(\bfmu)$ is parameterized with interpolation, the Operator Inference problem to learn the operator matrices can be written as $\Z \approx \Ohat\D\trp$ in the following way. +Let $\Qhat_i\in\RR^{r\times k_i}$ and $\U_i\in\RR^{m \times k_i}$ collect the state and input data for training parameter value $\bfmu_i$, with corresponding state time derivative data $\Z_i = \dot{\Qhat}_i\in\RR^{r\times k_i}$ for $i = 0,\ldots, s-1$. +We then have + +$$ +\begin{aligned} + \Z + &= \left[\begin{array}{ccc} + \Z_0 & \cdots & \Z_{s-1} + \end{array}\right]\in\RR^{r\times K} + \\ + \Ohat + &= \left[\begin{array}{cc|c|ccc} + \Ahat^{(0)} & \Ahat^{(1)} & \Hhat & \Bhat^{(0)} & \cdots & \Bhat^{(s-1)} + \end{array}\right]\in\RR^{r \times d} + \\ + \D\trp + &= \left[\begin{array}{} + \theta^{(0)}\!(\bfmu_0)\Qhat_{0} & \cdots & \theta^{(0)}\!(\bfmu_s)\Qhat_{s} \\ + \theta^{(1)}\!(\bfmu_0)\Qhat_{0} & \cdots & \theta^{(1)}\!(\bfmu_s)\Qhat_{s} \\ \hline + \Qhat_{0}\odot\Qhat_{0} & \cdots & \Qhat_{s}\odot\Qhat_{s} \\ \hline + \U_{0} & \cdots & \0 \\ + \vdots & \ddots & \0 \\ + \0 & \cdots & \U_{s-1} + \end{array}\right]\in\RR^{d \times K}, +\end{aligned} +$$ + +where $K = \sum_{i=0}^{s-1}k_i$ is the total number of data snapshots and $d = 2r + r(r+1)/2 + sm$ is the total operator dimension. +Note that the operator and data matrices have blocks corresponding to each of the three operators in the model. +::: \ No newline at end of file diff --git a/sphinx-docs/source/api/post.md b/sphinx-docs/source/api/post.md new file mode 100644 index 00000000..c44740ad --- /dev/null +++ b/sphinx-docs/source/api/post.md @@ -0,0 +1,151 @@ +# `opinf.post` + +```{eval-rst} +.. automodule:: opinf.post +``` + +## Absolute and Relative Error + +Given a norm $\|\cdot\|$, "true" data $\Q$, and an approximation $\breve{\Q}$ to $\Q$, the absolute and relative errors of the approximation $\breve{\Q}$ are defined as + +$$ +\begin{align*} + e_{\text{absolute}} + = \|\Q - \breve{\Q}\|, + \qquad + e_{\text{relative}} + = \frac{e_{\text{absolute}}}{\|\Q\|} + = \frac{\|\Q - \breve{\Q}\|}{\|\Q\|}. +\end{align*} +$$ + +In the context of this package, $\Q\in\RR^{n \times k}$ is typically a matrix whose $j$-th column is the true state vector at time $t_{j}$, and the approximation $\breve{\Q}\in\RR^{n\times k}$ is the corresponding matrix of reduced-order model solutions. + +## Projection Error + +The projection error is defined by the low-dimensional representation of the state, not the solution of a reduced-order model *per se*. +For a true state $\q \in \RR^{n}$, consider the low-dimensional (linear) approximation + +$$ +\begin{align*} + \breve{\q} = \Vr\qhat, +\end{align*} +$$ + +where $\Vr\in\RR^{n\times r}$. +The projection error associated with this approximation is + +$$ +\begin{align*} + \|\q - \breve{\q}\| + = \|\q - \Vr\Vr\trp\q\|. +\end{align*} +$$ + +The following function computes this projection error, given a basis matrix $\Vr$. + +```{eval-rst} +.. currentmodule: opinf.post + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + projection_error +``` + +Basis classes such as {class}`opinf.basis.PODBasis` also have a `projection_error()` method. + +## Reduced-order Model Error + +The following functions compute the error between a true state solution $\q(t) \in \RR^{n}$ of the system of interest and an approximation $\breve{\q}(t) \in \RR^{n}$ generated by a reduced-order model. +Each uses a different norm to measure the absolute and relative errors. + +```{eval-rst} +.. currentmodule: opinf.post + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + frobenius_error + lp_error + Lp_error +``` + + + + diff --git a/sphinx-docs/source/api/pre.md b/sphinx-docs/source/api/pre.md new file mode 100644 index 00000000..cd1e2edf --- /dev/null +++ b/sphinx-docs/source/api/pre.md @@ -0,0 +1,654 @@ +--- +kernelspec: + display_name: opinf + language: python + name: python3 +file_format: mystnb +--- + +# `opinf.pre` + ++++ + +```{eval-rst} +.. automodule:: opinf.pre + +.. currentmodule:: opinf.pre + +**Classes** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + TransformerTemplate + NullTransformer + ShiftTransformer + ScaleTransformer + ShiftScaleTransformer + TransformerPipeline + TransformerMulti + +**Functions** + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + shift + scale +``` + ++++ + +:::{admonition} Overview +:class: note + +- Operator Inference performance often improves when the training data are standardized. Multivariable data in particular benefits from preprocessing. +- `opinf.pre` classes define invertible transformations for data standardization. +::: + ++++ + +::::{admonition} Example Data +:class: tip + +The examples on this page use data downsampled from the combustion problem described in {cite}`swischuk2020combustion`. + +:::{dropdown} State Variables + +The data consists of nine variables recorded at 100 points in time. + +- Pressure $p$ +- $x$-velocity $v_{x}$ +- $y$-velocity $v_{y}$ +- Temperature $T$ +- Specific volume (inverse density) $\xi = 1/\rho$ +- Chemical species molar concentrations for CH$_{4}$, O$_{2}$, CO$_{2}$, and H$_{2}$O. + +The dimension of the spatial discretization in the full example in {cite}`swischuk2020combustion` is $n_x = 38{,}523$ for each of the $n_q = 9$ variables, so the total state dimension is $n_q n_x = 9 \times 38{,}523 = 346{,}707$. +For demonstration purposes, we have downsampled the state dimension to $n_x' = 535$, hence $n = n_q n_x' = 9 \times 535 = 4{,}815$ is the total state dimension of the example data. +::: + +You can [download the data here](https://github.com/Willcox-Research-Group/rom-operator-inference-Python3/raw/data/pre_example.npy) to repeat the experiments. +The full dataset is available [here](https://doi.org/10.7302/nj7w-j319). +:::: + +```{code-cell} ipython3 +import numpy as np +import matplotlib.pyplot as plt + +import opinf + +opinf.utils.mpl_config() +``` + +## Preprocessing Data + ++++ + +Raw dynamical systems data often need to be lightly preprocessed before use in Operator Inference. +This module includes tools for centering/shifting and scaling/nondimensionalization of snapshot data after lifting (when applicable) and prior to dimensionality reduction. + ++++ + +:::{admonition} Notation +:class: note + +On this page, +- $\q \in \RR^n$ denotes the unprocessed state variable for which we have $k$ snapshots $\q_0,\ldots,\q_{k-1}\in\RR^n$, +- $\q'\in\RR^n$ denotes state variable after being shifted (centered), and +- $\q''\in\RR^n$ denotes the state variable after being shifted _and_ scaled (non-dimensionalized). + +The tools demonstrated here define a mapping $\mathcal{T}:\RR^n\to\RR^n$ with $\q'' = \mathcal{T}(\q)$. +::: + ++++ + +:::{admonition} Lifting and Preprocessing +:class: note + +A [lifting map](opinf.lift) can be viewed as a type of preprocessing map, $\mathcal{L}:\RR^{n_1}\to\RR^{n_2}$. +However, the preprocessing transformations defined in this module map from a vector space back to itself ($n_1 = n_2$) while lifting maps may augment the state with additional variables ($n_2 \ge n_1$). +::: + ++++ + +::::{admonition} Fit-and-Transform versus Transform +:class: important + +Pre-processing transformation classes are calibrated through user-provided hyperparameters in the constructor and/or training snapshots passed to ``fit()`` or ``fit_transform()``. +The ``transform()`` method applies but *does not alter* the transformation. +Some transformations are designed so that the transformed training data has certain properties, but those properties are not guaranteed to hold for transformed data that was not used for training. + +:::{dropdown} Example + +Consider a set of training snapshots $\{\q_{j}\}_{j=0}^{k-1}\subset\RR^n$. +The {class}`ShiftScaleTransformer` can shift data by the mean training snapshot, meaning it can represent the transformation $\mathcal{T}:\RR^{n}\to\RR^{n}$ given by + +$$ +\begin{aligned} + \mathcal{T}(\q) = \q - \bar{\q}, + \qquad + \bar{\q} = \frac{1}{k}\sum_{j=0}^{k-1}\q_{j}. +\end{aligned} +$$ + +The key property of this transformation is that the transformed training snapshots have zero mean. +That is, + +$$ +\begin{aligned} + \frac{1}{k}\sum_{j=0}^{k-1}\mathcal{T}(\q_j) + = \frac{1}{k}\sum_{j=0}^{k-1}(\q_j - \bar{\q}) + = \frac{1}{k}\sum_{j=0}^{k-1}\q_j - \frac{1}{k}\sum_{j=0}^{k-1}\bar{\q} + = \bar{\q} - \frac{k}{k}\bar{\q} + = \0. +\end{aligned} +$$ + +However, for any other collection $\{\mathbf{x}_j\}_{j=0}^{k'-1}\subset\RR^{n}$ of snapshots, the set of transformed snapshots $\{\mathcal{T}(\mathbf{x}_j)\}_{j=0}^{k'-1}$ is not guaranteed to have zero mean because $\mathcal{T}$ shifts by the mean of the $\q_j$'s, not the mean of the $\mathbf{x}_j$'s. +That is, + +$$ +\begin{aligned} + \frac{1}{k'}\sum_{j=0}^{k'-1}\mathcal{T}(\mathbf{x}_j) + = \frac{1}{k'}\sum_{j=0}^{k'-1}(\mathbf{x}_j - \bar{\q}) + \neq \0. +\end{aligned} +$$ +::: +:::: + ++++ + +## Shifting / Centering + ++++ + +A common first preprocessing step is to shift the training snapshots by some reference snapshot $\bar{\q}\in\RR^n$, i.e., + +$$ + \q' = \q - \bar{\q}. +$$ + +The {class}`ShiftTransformer` receives a reference snapshot $\bar{\q}$ and applies this transformation. +This is useful for scenarios where a specific $\bar{\q}$ can result in desirable properties in the shifted data, such as homogeneous boundary conditions. + +```{code-cell} ipython3 +# Load the example snapshot data. +snapshots = np.load("pre_example.npy") + +snapshots.shape +``` + +```{code-cell} ipython3 +# Extract the pressure variable from the snapshot data. +pressure = np.split(snapshots, 9, axis=0)[0] + +# Initialize a ShiftTransformer for shifting the pressure so that +# each row has a minimum of 0. +pressure_shifter = opinf.pre.ShiftTransformer( + pressure.min(axis=1), + name="pressure", +) +print(pressure_shifter) +``` + +```{code-cell} ipython3 +pressure_shifted = pressure_shifter.fit_transform(pressure) +pressure_shifted.shape +``` + +```{code-cell} ipython3 +print(f"minimum pressure before shift: {pressure.min():.2e}") +print(f"minimum pressure after shift: {pressure_shifted.min():.2e}") +``` + +One strategy that is often effective for Operator Inference is to set the reference snapshot to be the average of the training snapshots: + +$$ + \bar{\q} + := \frac{1}{k}\sum_{j=0}^{k-1}\q_{j}. +$$ + +In this case, the transformed snapshots $\q_j' = \q_j - \bar{\q}$ are centered around $\0$. +This type of transformation can be accomplished using a {class}`ShiftScaleTransformer` with `centering=True`. + +```{code-cell} ipython3 +# Initialize a ShiftScaleTransformer for centering the pressure. +pressure_transformer = opinf.pre.ShiftScaleTransformer( + centering=True, + name="pressure", + verbose=True, +) +print(pressure_transformer) +``` + +```{code-cell} ipython3 +# Shift the pressure snapshots by the average pressure snapshot. +pressure_shifted = pressure_transformer.fit_transform(pressure) +``` + +```{code-cell} ipython3 +# Plot the distribution of the entries of the raw and processed states. +fig, axes = plt.subplots(1, 2, sharey=True) +axes[0].hist(pressure.flatten(), bins=40) +axes[1].hist(pressure_shifted.flatten(), bins=40) + +axes[0].set_ylabel("Frequency") +axes[0].set_xlabel("Pressure") +axes[1].set_xlabel("Shifted pressure") + +fig.tight_layout() +plt.show() +``` + +::::{admonition} Shifting Affects Model Form +:class: important + +Introducing a shift can cause a structural change in the governing dynamics. +When shifting state variables, the structure of a reduced-order model should be determined based on the dynamics of the shifted variable, not the original variable. + +:::{dropdown} Example 1: Linear System + +Consider the linear system + +$$ +\begin{align*} + \ddt\q(t) = \A\q(t). +\end{align*} +$$ + +The dynamics of the shifted variable $\q'(t) = \q(t) - \bar{\q}$ are given by + +$$ +\begin{align*} + \ddt\q'(t) + = \ddt[\q(t) - \bar{\q}] + = \ddt\q(t) + = \A\q(t) + = \A[\bar{\q} + \q'(t)] + = \A\bar{\q} + \A\q'(t), +\end{align*} +$$ + +which has a new constant term $\A\bar{\q}$ in addition to a linear term $\A\q'(t)$. +If the variable $\q$ is used for Operator Inference, the reduced-order model should take on the linear form $\ddt\qhat(t) = \Ahat\qhat(t)$, while if $\q'$ is the state variable, the reduced-order model should be $\ddt\qhat(t) = \chat + \Ahat\qhat(t)$. +::: + +:::{dropdown} Example 2: Quadratic System + +Consider the purely quadratic system + +$$ +\begin{align*} + \ddt\q(t) = \H[\q(t)\otimes\q(t)], +\end{align*} +$$ + +where $\otimes$ denotes the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product). +An appropriate reduced-order model for this system is also quadratic, $\ddt\qhat(t) = \Hhat[\qhat(t)\otimes\qhat(t)]$. +However, the dynamics of the shifted variable $\q'(t) = \q(t) - \bar{\q}$ includes lower-order terms: + +$$ +\begin{align*} + \ddt\q'(t) + &= \ddt[\q(t) - \bar{\q}] + \\ + &= \H[\q(t)\otimes\q(t)] + \\ + &= \H[(\bar{\q} + \q'(t))\otimes(\bar{\q} + \q'(t))] + \\ + &= \H[\bar{\q}\otimes\bar{\q}] + + \H[\bar{\q}\otimes\q'(t)] + \H[\q'(t)\otimes\bar{\q}] + + \H[\q'(t)\otimes\q'(t)]. +\end{align*} +$$ + +The terms $\H[\bar{\q}\otimes\q'(t)] + \H[\q'(t)\otimes\bar{\q}]$ can be interpreted as a linear transformation of $\q'(t)$, hence an appropriate reduced-order model for $\q'(t)$ has the fully quadratic form $\ddt\qhat(t) = \chat + \Ahat\qhat(t) + \Hhat[\qhat(t)\otimes\qhat(t)]$. +::: +:::: + ++++ + +## Scaling / Non-dimensionalization + ++++ + +Many engineering problems feature multiple variables with ranges across different scales. +For such cases, it is often beneficial to scale the variables to similar ranges so that one variable does not overwhelm the other during operator learning. +In other words, training data should be nondimensionalized when possible. + +A scaling operation for a single variable is given by + +$$ + \q'' = \alpha\q', +$$ + +where $\alpha \neq 0$ and $\q'$ is a training snapshot after shifting (when desired). +The {class}`ScaleTransformer` class receives a scaler $\alpha$ and implements this transformation. + +```{code-cell} ipython3 +# Initialize a ScaleTransformer for scaling the pressure to [0, 1]. +pressure_scaler = opinf.pre.ScaleTransformer( + 1 / pressure.max(), name="pressure" +) + +print(pressure_scaler) +``` + +```{code-cell} ipython3 +# Apply the scaling. +pressure_scaled = pressure_scaler.fit_transform(pressure) +pressure_scaled.shape +``` + +```{code-cell} ipython3 +print(f"min pressure before scaling: {pressure.min():.2e}") +print(f"max pressure before scaling: {pressure.max():.2e}") +print(f"min pressure after scaling: {pressure_scaled.min():.2e}") +print(f"max pressure after scaling: {pressure_scaled.max():.2e}") +``` + +The entries of the state can be scaled individually by passing a vector to {class}`ScaleTransformer`. + +```{code-cell} ipython3 +# Scale the pressure so the maximum of each row is 1. +pressure_scaler = opinf.pre.ScaleTransformer( + 1 / pressure.max(axis=1), name="pressure" +) + +print(pressure_scaler) +``` + +```{code-cell} ipython3 +# Apply the scaling. +pressure_scaled2 = pressure_scaler.fit_transform(pressure) +pressure_scaled2.shape +``` + +```{code-cell} ipython3 +print( + "number of rows whose maximum is 1 (whole scaling): " + f"{np.count_nonzero(np.isclose(pressure_scaled.max(axis=1), 1))}" +) +print( + "number of rows whose maximum is 1 (row scaling): " + f"{np.count_nonzero(np.isclose(pressure_scaled2.max(axis=1), 1))}" +) +``` + +The {class}`ShiftScaleTransformer` class implements several types of scalings that are calibrated from data. +For example, setting `scaling="maxabs"` scales the training data by the inverse of its absolute maximum entry so that the resulting data lies in the interval $[-1, 1]$. + +```{code-cell} ipython3 +# Extract the velocity in the x direction. +xvelocity = np.split(snapshots, 9, axis=0)[1] + +# Initialize a ShiftScaleTransformer for scaling the velocity to [-1, 1]. +xvelocity_scaler = opinf.pre.ShiftScaleTransformer( + centering=False, + scaling="maxabs", + name="x velocity", +) + +print(xvelocity_scaler) +``` + +```{code-cell} ipython3 +# Apply the scaling. +xvelocity_scaled = xvelocity_scaler.fit_transform(xvelocity) +xvelocity_scaled.shape +``` + +```{code-cell} ipython3 +print(f"min x-velocity before scaling: {xvelocity.min():.2e}") +print(f"max x-velocity before scaling: {xvelocity.max():.2e}") +print(f"min x-velocity after scaling: {xvelocity_scaled.min():.2e}") +print(f"max x-velocity after scaling: {xvelocity_scaled.max():.2e}") +``` + +The {class}`ShiftScaleTransformer` class can perform a mean-centering shift, followed by a data-driven scaling. +To link a custom shift with a custom scaling, instantiate a {class}`ShiftTransformer` and a {class}`ScaleTransformer` and pass them to a {class}`TransformerPipeline`. + +```{code-cell} ipython3 +# Combine the shift to zero from before with a custom scaling. +pressure_scaler = opinf.pre.ScaleTransformer(1e-6, "pressure") +pressure_transformer2 = opinf.pre.TransformerPipeline( + [pressure_shifter, pressure_scaler], name="pressure" +) + +print(pressure_transformer2) +``` + +```{code-cell} ipython3 +# Apply the scaling. +pressure_transformed = pressure_transformer2.fit_transform(pressure) +pressure_transformed.shape +``` + +```{code-cell} ipython3 +print(f"min pressure before shifting/scaling: {pressure.min():.2e}") +print(f"max pressure before shifting/scaling: {pressure.max():.2e}") +print(f"min pressure after shifting/scaling: {pressure_transformed.min():.2e}") +print(f"max pressure after shifting/scaling: {pressure_transformed.max():.2e}") +``` + +:::{admonition} No Free Lunch +:class: note + +Choosing an advantageous preprocessing strategy is highly problem dependent, and the tools in this module are not the only ways to preprocess snapshot data. +See, for example, {cite}`issan2023shifted` for a compelling application of Operator Inference to solar wind streams in which preprocessing plays a vital role. +::: + ++++ + +## Multivariable Data + ++++ + +For systems where the full state consists of several variables (pressure, velocity, temperature, etc.), it may not be appropriate to apply the same scaling to each variable. +The {class}`TransformerMulti` class joins individual transformers together to handle multi-state data. + ++++ + +Below, we construct the following transformation for the nine state variables. +- Pressure: center, then scale to $[-1, 1]$. +- $x$-velocity: Scale to $[-1, 1]$. +- $y$-velocity: Scale to $[-1, 1]$. +- Temperature: center, then scale to $[-1, 1]$. +- Specific volume: scale to $[0, 1]$. +- Chemical species: scale to $[0, 1]$. + +```{code-cell} ipython3 +combustion_transformer = opinf.pre.TransformerMulti( + transformers=[ + opinf.pre.ShiftScaleTransformer( + name="pressure", centering=True, scaling="maxabs", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="x-velocity", scaling="maxabs", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="y-velocity", scaling="maxabs", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="temperature", centering=True, scaling="maxabs", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="specific volume", scaling="minmax", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="methane", scaling="minmax", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="oxygen", scaling="minmax", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="carbon dioxide", scaling="minmax", verbose=True + ), + opinf.pre.ShiftScaleTransformer( + name="water", scaling="minmax", verbose=True + ), + ] +) + +snapshots_preprocessed = combustion_transformer.fit_transform(snapshots) +``` + +```{code-cell} ipython3 +print(combustion_transformer) +``` + +```{code-cell} ipython3 +# Extract a single variable from the processed snapshots. +oxygen_processed = combustion_transformer.get_var( + "oxygen", + snapshots_preprocessed, +) + +oxygen_processed.shape +``` + +## Custom Transformers + ++++ + +New transformers can be defined by inheriting from the {class}`TransformerTemplate`. +Once implemented, the [`verify()`](TransformerTemplate.verify) method may be used to test for consistency between the required methods. + +```{code-cell} ipython3 +class MyTransformer(opinf.pre.TransformerTemplate): + """Custom pre-processing transformation.""" + + def __init__(self, hyperparameters, name=None): + """Set any transformation hyperparameters. + If there are no hyperparameters, __init__() may be omitted. + """ + super().__init__(name) + # Process/store 'hyperparameters' here. + + # Required methods -------------------------------------------------------- + def fit_transform(self, states, inplace=False): + """Learn and apply the transformation.""" + # Set self.state_dimension in this method, e.g., + self.state_dimension = len(states) + raise NotImplementedError + + def transform(self, states, inplace=False): + """Apply the learned transformation.""" + raise NotImplementedError + + def inverse_transform(self, states_transformed, inplace=False, locs=None): + """Apply the inverse of the learned transformation.""" + raise NotImplementedError + + # Optional methods -------------------------------------------------------- + # These may be deleted if not implemented. + def transform_ddts(self, ddts, inplace=False): + """Apply the learned transformation to snapshot time derivatives.""" + return NotImplemented + + def save(self, savefile, overwrite=False): + """Save the transformer to an HDF5 file.""" + return NotImplemented + + @classmethod + def load(cls, loadfile): + """Load a transformer from an HDF5 file.""" + return NotImplemented +``` + +See the {class}`TransformerTemplate` page for details on the arguments for each method. + ++++ + +### Example: Hadamard Scaling + ++++ + +The following class implements the transformation $\mathcal{T}(\q) = \q \ast \w$ where $\ast$ is the Hadamard (elementwise) product and $\s\in\RR^{n}$ is a given vector with all nonzero entries. +The inverse of this transform is $\mathcal{T}^{-1}(\q) = \q \ast \w'$ where the entries of $\w'\in\RR^{n}$ are the inverse of the entries of $\w$. +This transformation is equivalent to {class}`ScaleTransformer` with `scaler` set to $\w$ and can be interpreted as applying a diagonal weighting matrix to the state snapshots. + +```{code-cell} ipython3 +class HadamardTransformer(opinf.pre.TransformerTemplate): + """Hadamard product transformer (weighting).""" + + def __init__(self, w, name=None): + """Set the product vector.""" + super().__init__(name) + self.w = w + self.winv = 1 / w + + # Required methods -------------------------------------------------------- + def fit_transform(self, states, inplace=False): + """Learn and apply the transformation.""" + self.state_dimension = self.w.size + return self.transform(states, inplace=inplace) + + def transform(self, states, inplace=False): + """Apply the learned transformation.""" + out = states if inplace else np.empty_like(states) + w = self.w + if states.ndim == 2: + w = w.reshape((self.state_dimension, 1)) + out[:] = states * w + return out + + def inverse_transform(self, states_transformed, inplace=False, locs=None): + """Apply the inverse of the learned transformation.""" + winv = self.winv + if locs is not None: + winv = winv[locs] + if states_transformed.ndim == 2: + winv = winv.reshape((-1, 1)) + states = ( + states_transformed + if inplace + else np.empty_like(states_transformed) + ) + states[:] = states_transformed * winv + return states + + def transform_ddts(self, ddts, inplace=False): + """Apply the learned transformation to snapshot time derivatives.""" + return self.transform(ddts, inplace=inplace) + + def save(self, savefile, overwrite=False): + """Save the transformer to an HDF5 file.""" + with opinf.utils.hdf5_savehandle(savefile, overwrite) as hf: + hf.create_dataset("w", data=self.w) + if self.name is not None: + meta = hf.create_dataset("meta", shape=(0,)) + meta.attrs["name"] = self.name + + @classmethod + def load(cls, loadfile): + """Load a transformer from an HDF5 file.""" + name = None + with opinf.utils.hdf5_loadhandle(loadfile) as hf: + w = hf["w"][:] + if "meta" in hf: + name = str(hf["meta"].attrs["name"]) + return cls(w, name=name) +``` + +```{code-cell} ipython3 +w = np.random.uniform(size=pressure.shape[0]) +ht = HadamardTransformer(w, name="Pressure weighter") +pressure_weighted = ht.fit_transform(pressure) +``` + +```{code-cell} ipython3 +ht.verify() +``` + +:::{admonition} Developer Notes +:class: note + +- In this example, the `state_dimension` could be set in the constructor because the `w` argument is a vector of length $n$. However, the `state_dimension` is not required to be set until [`fit_transform()`](TransformerTemplate.fit_transform). +- Because the transformation is dictated by the choice of `w` and not calibrated from data, [`fit_transform()`](TransformerTemplate.fit_transform) simply calls [`transform()`](TransformerTemplate.transform). +- When `locs` is provided in [`inverse_transform()`](TransformerTemplate.inverse_transform), it is assumed that the `states_transformed` are the elements of the state vector at the given locations. That is,`inverse_transform(transform(states)[locs], locs) == states[locs]`. +::: \ No newline at end of file diff --git a/sphinx-docs/source/api/roms.md b/sphinx-docs/source/api/roms.md new file mode 100644 index 00000000..63f69b8c --- /dev/null +++ b/sphinx-docs/source/api/roms.md @@ -0,0 +1,37 @@ +# `opinf.roms` + +```{eval-rst} +.. automodule:: opinf.roms +``` + +## Deterministic Reduced-order Models + +These classes are also available in the main [`opinf`](./main.md) namespace. + +```{eval-rst} +.. currentmodule:: opinf.roms + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + ROM + ParametricROM +``` + +## Probabilistic Reduced-order Models + +The following classes are used to represent probabilistic models. +A probability distribution is constructed for an operator matrix; an individual draw from this distribution defines a new deterministic model. +See {cite}`guo2022bayesopinf`. + +```{eval-rst} +.. currentmodule:: opinf.roms + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + OperatorPosterior + BayesianROM +``` diff --git a/sphinx-docs/source/api/utils.md b/sphinx-docs/source/api/utils.md new file mode 100644 index 00000000..86b424c8 --- /dev/null +++ b/sphinx-docs/source/api/utils.md @@ -0,0 +1,82 @@ +# `opinf.utils` + +```{eval-rst} +.. automodule:: opinf.utils +``` + +## Timing Code + +Model reduction is all about speeding up computational tasks. +The following class defines a context manager for timing blocks of code and logging errors. + +```{eval-rst} +.. currentmodule:: opinf.utils + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + TimedBlock +``` + +## Load/Save HDF5 Utilities + +Many `opinf` classes have `save()` methods that export the object to an HDF5 file and a `load()` class method for importing an object from an HDF5 file. +The following functions facilitate that data transfer. + +```{eval-rst} +.. currentmodule:: opinf.utils + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + hdf5_loadhandle + hdf5_savehandle +``` + +## Helper Routines + +The following functions perform miscellaneous tasks within the rest of the code base. + +```{eval-rst} +.. currentmodule:: opinf.utils + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + requires + requires2 + str2repr +``` + +### Regularization Selection + +These helper routines support automatic regularization selection. + +```{eval-rst} +.. currentmodule:: opinf.utils + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + gridsearch + DiscreteRegTest + ContinuousRegTest +``` + +## Documentation + +The following function initializes the Matplotlib defaults used in the documentation notebooks. + +```{eval-rst} +.. currentmodule:: opinf.utils + +.. autosummary:: + :toctree: _autosummaries + :nosignatures: + + mpl_config +``` diff --git a/sphinx-docs/source/conf.py b/sphinx-docs/source/conf.py new file mode 100644 index 00000000..6bc9326a --- /dev/null +++ b/sphinx-docs/source/conf.py @@ -0,0 +1,197 @@ +# This in the configuration file needed to use Sphinx instead of Jupyter Notebook + +# -- Project Information ----------------------------------------------------- + +project = "Operator Inference" # Added this title here +author = "Willcox Research Group, Oden Institute for Computational Engineering and Sciences" + +# -- General Configuration --------------------------------------------------- + +extensions = [ + # MyST and notebook support (replaces JupyterBook's built-in handling) + "myst_nb", # handles our .md files + + # Bibliography (was bundled in JupyterBook, now explicit) + "sphinxcontrib.bibtex", + + # API documentation (same as before) + "numpydoc", # Parses the Numpy-style docstrings + "sphinx.ext.autodoc", # Pulls the docstrings from Python Source code to create API docs + "sphinx.ext.autosummary", # Generates summary tables and individual pages for modules, classes, and functions + "sphinx.ext.viewcode", # Adds the "view source" to the docs + "sphinx.ext.napoleon", # Understand numpy and google style docstrings + + # Cross-project links + "sphinx.ext.intersphinx", # Allows cross-linking to other packages docs + + # UI components (admonitions, grids, tabs, etc.) + "sphinx_design", # Adds grids, carbs, tabs, and badges to your docs + + # Diagrams + "sphinxcontrib.mermaid", # Allows for the embedding of mermaid diagrams +] + +# Templates for autosummary (same path as before) +templates_path = ["_templates"] + +# Static files (CSS, images, etc.) +html_static_path = ["_static"] + +# Suppress specific warnings (carried over from _config.yml) +suppress_warnings = ["etoc.toctree"] + +# -- MyST Configuration ------------------------------------------------------ +# These replace the parse.myst_enable_extensions block in _config.yml + +myst_enable_extensions = [ + "amsmath", # Lets you use \begin{equation} and others + "colon_fence", # ::: instead of ``` + "dollarmath", # $...$ and $$..$$ for inline block math + "linkify", # bare URLs become links + "substitution", # Lets you define reusable text snippets + "tasklist", # Github Style check boxes +] + +# -- Notebook Execution (myst_nb) -------------------------------------------- +# Replaces execute.execute_notebooks and execute.timeout in _config.yml + +nb_execution_mode = "cache" # auto, force, cache, or "off" +nb_execution_timeout = 120 # seconds before KeyboardInterrupt + +# -- Bibliography ------------------------------------------------------------ +# Replaces bibtex_bibfiles in _config.yml + +bibtex_bibfiles = [ + "references.bib", + "literature.bib", +] +bibtex_reference_style = "label" + +# -- Autosummary / Autodoc --------------------------------------------------- +# Carried over from sphinx.config in _config.yml + +add_function_parentheses = True +add_module_names = False # Shorten function names in API docs + +autosummary_generate = True +autosummary_filename_map = { # Resolves lower/upper case ambiguities + "opinf.post.Lp_error": "bigLp-error", +} + +# -- Numpydoc ---------------------------------------------------------------- + +numpydoc_class_members_toctree = False +numpydoc_show_class_members = False +numpydoc_show_inherited_class_members = False + +# -- Intersphinx ------------------------------------------------------------- +# Cross-links to external package documentation + +intersphinx_mapping = { + "matplotlib": ("https://matplotlib.org/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "python": ("https://docs.python.org/3/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "sklearn": ("https://scikit-learn.org/stable/", None), + "pandas": ("https://pandas.pydata.org/docs/", None), +} + +# -- MathJax ----------------------------------------------------------------- +# Replaces mathjax_path and mathjax3_config in _config.yml +# All your custom LaTeX macros are carried over exactly + +mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" + +mathjax3_config = { + "tex": { + "macros": { + "RR": "\\mathbb{R}", + "NN": "\\mathbb{N}", + "ZZ": "\\mathbb{Z}", + "I": "\\mathbf{I}", + "0": "\\mathbf{0}", + "1": "\\mathbf{1}", + "q": "\\mathbf{q}", + "u": "\\mathbf{u}", + "z": "\\mathbf{z}", + "d": "\\mathbf{d}", + "f": "\\mathbf{f}", + "s": "\\mathbf{s}", + "Q": "\\mathbf{Q}", + "U": "\\mathbf{U}", + "Z": "\\mathbf{Z}", + "Op": "\\mathbf{f}", + "Ophat": "\\hat{\\mathbf{f}}", + "c": "\\mathbf{c}", + "A": "\\mathbf{A}", + "H": "\\mathbf{H}", + "G": "\\mathbf{G}", + "B": "\\mathbf{B}", + "N": "\\mathbf{N}", + "v": "\\mathbf{v}", + "w": "\\mathbf{w}", + "V": "\\mathbf{V}", + "W": "\\mathbf{W}", + "Vr": "\\mathbf{V}_{\\!r}", + "Wr": "\\mathbf{W}_{\\!r}", + "qhat": "\\hat{\\mathbf{q}}", + "zhat": "\\hat{\\mathbf{z}}", + "fhat": "\\hat{\\mathbf{f}}", + "Qhat": "\\hat{\\mathbf{Q}}", + "Zhat": "\\hat{\\mathbf{Z}}", + "chat": "\\hat{\\mathbf{c}}", + "Ahat": "\\hat{\\mathbf{A}}", + "Hhat": "\\hat{\\mathbf{H}}", + "Ghat": "\\hat{\\mathbf{G}}", + "Bhat": "\\hat{\\mathbf{B}}", + "Nhat": "\\hat{\\mathbf{N}}", + "D": "\\mathbf{D}", + "ohat": "\\hat{\\mathbf{o}}", + "Ohat": "\\hat{\\mathbf{O}}", + "bfmu": "\\boldsymbol{\\mu}", + "bfGamma": "\\boldsymbol{\\Gamma}", + "bfPhi": "\\boldsymbol{\\Phi}", + "bfSigma": "\\boldsymbol{\\Sigma}", + "bfPsi": "\\boldsymbol{\\Psi}", + "bfLambda": "\\boldsymbol{\\Lambda}", + "bfxi": "\\boldsymbol{\\xi}", + "trp": "{^{\\mathsf{T}}}", + "ddt": "\\frac{\\textrm{d}}{\\textrm{d}t}", + "ddqhat": "\\frac{\\partial}{\\partial\\qhat}", + "mean": "\\operatorname{mean}", + "std": "\\operatorname{std}", + "argmin": "\\operatorname{argmin}", + } + } +} + +# -- Figure numbering -------------------------------------------------------- +# Eq (1.1) instead of (1) — carried over from numfig_secnum_depth + +numfig = True # Must enable numfig for numfig_secnum_depth to work +numfig_secnum_depth = 1 # JupyterBook set this but didn't require numfig=True explicitly + +# -- HTML Output ------------------------------------------------------------- +# Replaces the html and repository blocks in _config.yml + +html_theme = "sphinx_book_theme" +html_logo = "_static/logo.svg" +html_favicon = "_static/favicon.svg" + +html_css_files = ["properties.css"] # Your custom CSS from _static/ + +html_theme_options = { + "repository_url": "https://github.com/Willcox-Research-Group/rom-operator-inference-Python3", + "repository_branch": "main", + "use_edit_page_button": False, + "use_issues_button": True, + "use_repository_button": True, +} + +html_context = { + # Required by themes that support edit/issue buttons + "github_user": "Willcox-Research-Group", + "github_repo": "rom-operator-inference-Python3", + "github_version": "main", + "doc_path": "docs", +} \ No newline at end of file diff --git a/sphinx-docs/source/contributing/documentation.md b/sphinx-docs/source/contributing/documentation.md new file mode 100644 index 00000000..e5508af2 --- /dev/null +++ b/sphinx-docs/source/contributing/documentation.md @@ -0,0 +1,81 @@ +# Writing Documentation + +Our documentation is powered by [Sphinx](https://www.sphinx-doc.org/en/master/) with [MyST](https://mystmd.org/) and GitHub pages. +Only the maintainers need to worry about GitHub pages, so this guide focuses on Sphinx with MyST and our documentation structure. + +:::{admonition} Summary + +- Documentation is stored as Markdown files or Jupyter notebooks in the `sphinx-docs/` folder. +- Use `tox -e sphinx-docs` to compile the documentation locally. +- Do **not** `git add` files from `sphinx-docs/_build/`. +::: + +## Sphinx + +Sphinx is an open source project for building "intelligent and beautiful documentation with ease". It allows for writing files in [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-primer) or [MyST Markdown](https://www.sphinx-doc.org/en/master/usage/markdown.html#markdown). Those files are then used to create the documentation that we use here. + +## Documentation Structure + +All documentation is grouped in the `sphinx-docs/` folder. +The project `README.md` should be kept short and refer to the official documentation. + +- `sphinx-docs/source/conf.py`: contains the configuration +- `sphinx-docs/references.bib`: Bibtex citations. Cite them in the documentation with `` {cite}`referencekey` ``. +- `sphinx-docs/source/_static`: contains all static images not generated by code in a notebook. SVGs are preferable, PDFs and PNGs are probably fine. +- `sphinx-docs/source/`: contains all of the files with the actual written documentation. The file tree structure should be somewhat obvious from the structure of the page you're looking at right now and from the botom +- `docs/source/tutorials/`: contains markdown files with full examples for specific applications. + +As you can see on the left of this page, `sphinx-docs/source/` is organized into the following chapters. + +- **Operator Inference**: general exposition about the setting and methodology. +- **Tutorials**: notebooks with full examples for specific applications. These should be written in a narrative style: mathematical details are good, but only when accompanied by nontechnical summaries. +- **API Reference**: public function/class signatures and docstrings. Most of these pages are generated automatically, but the index for each submodule is a notebook with demonstration code. +- **Developer Guide**: instructions for developers (such as this page). + +## Building Documentation Locally + +From the root folder of the repository, execute `tox -e sphinx-docs` to build the documentation. +This is a shortcut for invoking `sphinx-build` with the right prerequisites. +If any changes are made to the [Literature](../opinf/literature.md) page through modifications to `docs/bib2md.py`, run `tox -e sphinxliterature` first. + +The documentation is processed and copied to in `sphinx-docs/build`. +Open the file `sphinx-docs/build/html/index.html` in a browser to see a preview (try Google Chrome if the file doesn't render nicely). + +```{attention} +Do not `git add` the build files from `docs/_build`! +Only the `gh-pages` branch should track this folder. +The `.gitignore` should remind you of this if you accidentally try to add them. +``` + +## Sphinx Autodoc + +This project uses Sphinx Autodoc to automattically generate documentation straight from code docstrings. Because of our settings for the automatic codumentation generation, please follow these guidelines. + +- Follow [NumPyDoc style guide](https://numpydoc.readthedocs.io/en/latest/format.html#short-summary) when writing docstrings +- Class docstrings should _not_ have a "Methods" section. +- [Properties](https://docs.python.org/3/library/functions.html#property) show up automatically in the documentation, but attributes created at runtime do not. +- Use `:math:` environments to write actual math. + +Note that docstrings must follow Sphinx syntax, not Jupyter notebook syntax. +For example, use `` :math:`i^2 = -1` `` instead of `$i^2 = -1$`. + +## Example: Contributing a New Tutorial + +:::{admonition} TODO + +- Create a notebook and place it in `sphinx-docs/source/tutorials/`. + - Add it to the book with by editting the `toctree` at the end of the `index.md` file in `sphinx-docs/source` +- Add data files to the `data` branch: + +```shell +git checkout --orphan data +git switch data +git pull upstream data +git add mynewdatafile.npy +git commit -m "add datafile for " +git push origin data +``` + +Then open a pull request to the data branch. + +::: diff --git a/sphinx-docs/source/contributing/how_to_contribute.md b/sphinx-docs/source/contributing/how_to_contribute.md new file mode 100644 index 00000000..dd292862 --- /dev/null +++ b/sphinx-docs/source/contributing/how_to_contribute.md @@ -0,0 +1,137 @@ +# How to Contribute + +Thank you for your interest in contributing to `opinf`! +Before you begin, please review our [Code of Conduct](https://github.com/operator-inference/opinf/blob/main/CODE_OF_CONDUCT.md). + +:::{admonition} Summary + +- Changes to the source code must be accompanied with updates to corresponding [unit tests](./testing.md) and [documentation](./documentation.md). +- Use `tox` to run tests while developing: + - `tox -e style` checks that source code and tests follow the style guide. + - `tox` (without arguments) executes all unit tests. + - `tox -e literature,docs` compiles the documentation. +- When all tests pass, open a pull request to the `main` branch on GitHub. +::: + +## Setup + +:::{attention} +Contributing to this project requires familiarity with GitHub and `git`. +If you are unfamiliar with either, start with the [GitHub tutorial](https://docs.github.com/en/get-started/quickstart/hello-world) or the [git tutorial](https://git-scm.com/docs/gittutorial). +::: + +Now that you are a `git` expert, [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) [the GitHub repository](https://github.com/operator-inference/opinf) and [clone your fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository). +Add the original repository as [an upstream remote](https://docs.github.com/en/get-started/quickstart/fork-a-repo#configuring-git-to-sync-your-fork-with-the-original-repository). + +```bash +git clone git@github.com:/opinf.git OpInf +cd OpInf +git remote add upstream git@github.com:operator-inference/opinf.git +``` + +Like most Python packages, `opinf` has a few [software dependencies](https://github.com/operator-inference/opinf/network/dependencies). +To avoid conflicts with other installed packages, we recommend installing `opinf` within a new [conda environment](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) (recommended) or [virtual Python environment](https://docs.python.org/3/tutorial/venv.html) . + +```shell +# Make a fresh conda environment and install Python 3.12. +conda create -n opinfdev python=3.12 +``` + +Be sure to [activate](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#activating-an-environment) the environment before using `pip` or other installation tools. +Then, install the package with developer dependencies. + +```shell +# Activate the conda environment (updates the PATH). +$ conda activate opinfdev + +# Verify python is now linked to the conda environment. +(opinfdev) $ which python3 +/path/to/your/conda/envs/opinfdev/bin/python3 +(opinfdev) $ python3 --version +Python 3.12.3 + +# Install the package and its dependencies in development mode. +(opinfdev) $ python3 -m pip install -e ".[dev]" +``` + +Style checks, unit tests, and documentation builds are managed with [`tox`](https://tox.wiki/en/latest). +For each of these tasks, `tox` creates a new virtual environment, installs the dependencies (e.g., `pytest` for running unit tests), and executes the task recipe. + +:::{note} +Unit tests are executed for Python 3.10 through 3.14 if they are installed on your system. +The best way to install multiple Python versions varies by platform; for MacOS, [we suggest](https://stackoverflow.com/questions/36968425/how-can-i-install-multiple-versions-of-python-on-latest-os-x-and-use-them-in-par#answer-65094122) using [Homebrew](https://brew.sh/). + +```shell +# After installing Homebrew: +brew install python@3.9 +brew install python@3.10 +brew install python@3.11 +brew install python@3.12 +``` + +::: + +Some of the tutorials and examples in this documentation rely on pre-generated data. This data is stored in the orphan `data` branch of the GitHub respository. For Mac/Unix users, there is a script titled `data_download.sh` that can be run from the root directory that will automatically download all of the data files and put them in their proper places. The script can be run as follows: + +```shell +bash docs/data_download.sh +``` + +Finally, to ensure that new additions follow code standards and conventions, install the [git pre-commit hook](https://pre-commit.com/) with the following command. + +```shell +(opinfdev) $ pre-commit install +``` + +:::{important} +Don't skip this step! +It will help prevent automated tests from failing when a pull request is made. +::: + +## Branches and Workflow + +The source repository has three special branches: + +- `main` is the most up-to-date version of the code. Tags on the `main` branch correspond to [public PyPi releases](https://pypi.org/project/opinf/). +- `gh-pages` contains only the current build files for this documentation. *This branch is updated by maintainers only.* +- `data` contains only data files used in documentation demos. + +To contribute, get synced with the `main` branch, then start a new branch for making active changes. + +```bash +git pull upstream main # Synchronize main with the source repository. +git branch # Create a new branch to make edits from. +git switch # Switch to the new branch to do work. +``` + +You are now ready to make edits on your newly created local branch. +When you're ready, [create a pull request](https://docs.github.com/en/get-started/quickstart/contributing-to-projects#making-a-pull-request) to merge the changes into `operator-inference:main`. + +## Repository Organization + +::::{margin} +:::{note} +Full examples, like the one listed on the left under the **Tutorials** heading, are part of the documentation. +They should be written as Jupyter notebooks and placed in `docs/content/tutorials/`. +::: +:::: + +The GitHub repository is organized as follows. + +- [`src/opinf/`](https://github.com/operator-inference/opinf/tree/main/src/opinf) contains the actual package code. +- [`tests/`](https://github.com/operator-inference/opinf/tree/main/tests) contains tests to be run with [`pytest`](https://docs.pytest.org/en/7.0.x/) (via `tox`). + The file structure of `tests/` should mirror the file structure of `src/opinf/`. See [Testing Source Code](./testing.md). +- [`docs/`](https://github.com/operator-inference/opinf/tree/main/docs) contains documentation (including this page!). See [Documentation](./documentation.md). + +## Acceptance Standards + +Changes are not usually accepted until the following tests pass. + +1. `tox`: write or update tests to validate your additions or changes, preferably with full line coverage. +2. `tox -e style`: write readable code that conforms to our style guide. +3. `tox -e literature,docs`: write or update documentation based on your changes. + +:::{tip} +The `Makefile` has recipes for these commands, run `make` to see options. +See [makefiletutorial.com](https://makefiletutorial.com/) for an overview of `Makefile` syntax and usage. +::: diff --git a/sphinx-docs/source/contributing/notation.md b/sphinx-docs/source/contributing/notation.md new file mode 100644 index 00000000..2a99883f --- /dev/null +++ b/sphinx-docs/source/contributing/notation.md @@ -0,0 +1,52 @@ +# Index of Notation + +In the documentation, we generally denote scalars in lower case, vectors in bold lower case, matrices in upper case, and indicate low-dimensional quantities with a hat. +In the code, low-dimensional quantities ends with an underscore (e.g., `state` is high-dimensional and `state_` is low-dimensional). + +## Dimensions + +| Symbol | Code | Description | +| :----: | :--- | :---------- | +| $n$ | `full_state_dimension` | Dimension of the full-order system (large) | +| $r$ | `reduced_state_dimension` | Dimension of the reduced-order system (small) | +| $m$ | `input_dimension` | Dimension of the input $\u$ | +| $k$ | `k` | Number of state snapshots, i.e., the number of training points | +| $s$ | `s` | Number of parameter samples for parametric training | +| $p$ | `p` | Dimension of the parameter space | +| $d$ | `d` | Number of columns of an operator entries matrix $\Ohat$ | + +## Vectors + +| Symbol | Code | Size | Description | +| :----: | :--- | :--: | :---------- | +| $\q$ | `state` | $n$ | Full-order state vector | +| $\qhat$ | `state_` | $r$ | Reduced-order state vector | +| $\dot{\qhat}$ | `ddt_` | $r$ | Reduced-order state time derivative vector | +| $\q_{\text{ROM}}$ | `q_ROM` | $n$ | Approximation to $\q$ produced by ROM | +| $\chat$ | `c_` | $r$ | Learned constant term | +| $\u$ | `inputs` | $m$ | Input vector | +| $\qhat\otimes\qhat$ | `np.kron(q_,q_)` | $r^2$ | Full quadratic Kronecker product of reduced state | +| $\qhat\,\widehat{\otimes}\,\qhat$ | `utils.kron2c(q_)` | $\frac{r(r+1)}{2}$ | Compact quadratic Kronecker product of reduced state | +| $\qhat\otimes\qhat\otimes\qhat$ | `np.kron(q_,np.kron(q_,q_))` | $r^3$ | Full cubic Kronecker product of reduced state | +| $\qhat\,\widehat{\otimes}\,\qhat\widehat{\otimes}\,\qhat$ | `utils.kron3c(q_)` | $\frac{r(r+1)(r+2)}{6}$ | Compact cubic Kronecker product of reduced state | +| $\v_{j}$ | `vj` | $n$ | $j$th basis vector, i.e., column $j$ of $\Vr$ | + +## Matrices + +| Symbol | Code | Shape | Description | +| :----: | :--- | :---: | :---------- | +| $\Vr$ | `basis` | $n \times r$ | low-rank basis of rank _r_ (usually the POD basis) | +| $\Q$ | `states` | $n \times k$ | Snapshot matrix | +| $\dot{\Q}$ | `ddts` | $n \times k$ | Snapshot time derivative matrix | +| $\U$ | `inputs` | $m \times k$ | Input matrix (inputs corresonding to the snapshots) | +| $\widehat{\Q}$ | `states_` | $r \times k$ | Projected snapshot matrix | +| $\dot{\widehat{\Q}}$ | `ddts_` | $r \times k$ | Projected snapshot time derivative matrix | +| $\D$ | `D` | $k \times d(r,m)$ | Data matrix | +| $\Ohat$ | `Ohat` | $r \times d(r,m)$ | Operator matrix | +| $\mathbf{R}$ | `R` | $r \times k$ | Right-hand side matrix | +| $\boldsymbol{\Gamma}$ | `regularizer` | $d(r,m) \times d(r,m)$ | Tikhonov regularization matrix | +| $\Ahat$ | `A_` | $r \times r$ | Reduced-order linear state matrix | +| $\Hhat$ | `H_` | $r \times \frac{r(r+1)}{2}$ | Compact reduced-order matricized quadratic state tensor | +| $\Ghat$ | `G_` | $r \times \frac{r(r+1)(r+2)}{6}$ | Compact reduced-order matricized quadratic state tensor | +| $\Bhat$ | `B_` | $r \times m$ | Reduced-order input matrix | +| $\Nhat$ | `N_` | $r \times rm$ | Bilinear state-input matrix | diff --git a/sphinx-docs/source/contributing/testing.md b/sphinx-docs/source/contributing/testing.md new file mode 100644 index 00000000..1b6f0a96 --- /dev/null +++ b/sphinx-docs/source/contributing/testing.md @@ -0,0 +1,193 @@ +# Testing Source Code + +This page is an overview of the package testing infrastructure. + +:::{admonition} Summary +:class: note + +- Use `tox -e format` to format the code with `black` conventions. +- Use `tox -e style` to verify that the code follows the `black` and `flake8` style guide. +- Write unit tests in the `tests/` folder that mirror the structure of the source code. +- Use `tox` to run unit tests. + +The `Makefile` also has recipes for these commands, run `make` to see options. +::: + +## Formatting with Black + +Source and test code must conform to [`black`](https://black.readthedocs.io/en/stable/) conventions. + +```shell +# Make sure the development environment is active. +$ conda deactivate +$ conda activate opinfdev + +# Run the formatter. +(opinfdev) $ tox -e format +``` + +If the code was already formatted correctly, a report like the following will be printed. + +```text +format: commands[0]> black src +All done! ✨ 🍰 ✨ +46 files left unchanged. +format: commands[1]> black tests +All done! ✨ 🍰 ✨ +41 files left unchanged. + format: OK (0.33=setup[0.02]+cmd[0.18,0.14] seconds) + congratulations :) (0.52 seconds) +``` + +If instead `black` made any changes ot the code, the report will show which files were changed. + +```text +format: commands[0]> black src +reformatted /../OpInf/src/opinf/.py + +All done! ✨ 🍰 ✨ +1 file reformatted, 45 files left unchanged. +format: commands[1]> black tests +All done! ✨ 🍰 ✨ +41 files left unchanged. + format: OK (0.65=setup[0.03]+cmd[0.46,0.16] seconds) + congratulations :) (0.89 seconds) +``` + +:::{note} +The `pre-commit` hook requires `black` and `flake8` to pass before allowing a `git commit`. + +```shell +(opinfdev) $ pre-commit install +``` + +::: + +:::{tip} +Most IDEs, such as Visual Studio Code, have [plugins](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) for `black` so that code is automatically formatted when a file is saved. +::: + +## Code Style with Black and Flake8 + +Source and test code must be free of syntax error and conform to the standard Python style guide, such as [PEP 8](https://www.python.org/dev/peps/pep-0008/). +Code style is verified by `black` and by [`flake8`](https://flake8.pycqa.org/en/latest/), a common Python [linter](https://en.wikipedia.org/wiki/Lint_(software)). +Unlike `black`, linters do not alter the code; they only checks that code satisfies the style guide and sometimes identifies syntax and other errors. + +```shell +# Make sure the development environment is active. +$ conda deactivate +$ conda activate opinfdev + +# Check code style. +$ tox -e style +``` + +If the code passes, a report like the following will be displayed. + +```text +style: commands[0]> black --check src +All done! ✨ 🍰 ✨ +46 files would be left unchanged. +style: commands[1]> flake8 src +style: commands[2]> black --check tests +All done! ✨ 🍰 ✨ +41 files would be left unchanged. +style: commands[3]> flake8 tests + style: OK (1.44=setup[0.05]+cmd[0.32,0.43,0.20,0.43] seconds) + congratulations :) (1.67 seconds) +``` + +If the code fails, the report will provide specific feedback about what is wrong and where. +For example, suppose we add the following line somewhere in `src/opinf/pre/_base.py`. + +```python +thisvariable = 'is never used' +``` + +This violates `black` conventions by using single quotation marks and `flake8` rules by defining a variable that is not used elsewhere. +Running `tox -e style` now gives a detailed report about the issue. + +```text +style: commands[0]> black --check src +would reformat /../OpInf/src/opinf/pre/_base.py + +Oh no! 💥 💔 💥 +1 file would be reformatted, 45 files would be left unchanged. +style: exit 1 (0.42 seconds) /../OpInf> black --check src pid=XXXXX +style: commands[1]> flake8 src +src/opinf/pre/_base.py:38:9: F841 local variable 'thisvariable' is assigned to but never used +1 F841 local variable 'thisvariable' is assigned to but never used +style: exit 1 (0.50 seconds) /../OpInf> flake8 src pid=XXXXX +style: commands[2]> black --check tests +All done! ✨ 🍰 ✨ +41 files would be left unchanged. +style: commands[3]> flake8 tests + style: FAIL code 1 (1.58=setup[0.04]+cmd[0.42,0.50,0.17,0.46] seconds) + evaluation failed :( (1.79 seconds) +``` + +:::{tip} +The following command gives a more detailed report from `black` about the changes it would like to make. + +```shell +(opinfdev) $ black --check --diff --color . +``` + +```text +--- /../OpInf/src/opinf/pre/_base.py ++++ /../OpInf/src/opinf/pre/_base.py +@@ -33,11 +33,11 @@ + """ + + def __init__(self, name: str = None): + """Initialize attributes.""" + +- thisvariable = 'is never used' ++ thisvariable = "is never used" + + self.__n = None + self.__name = name + + # Properties -------------------------------------------------------------- +would reformat /../OpInf/src/opinf/pre/_base.py + +Oh no! 💥 💔 💥 +1 file would be reformatted, 45 files would be left unchanged. +``` + +::: + +:::{warning} +Code that passes the style check is not guaranteed to _work_ as intended. +Linting does not replace unit tests for gauging code correctness and functionality. +::: + +## Unit Testing with Pytest + +This package uses the [Pytest](https://docs.pytest.org/en/7.0.x/) framework for unit testing. + +- All tests are stored in the `tests/` folder. +- The file structure of `tests/` should mirror the file structure of `src/opinf/`, but test files must start with `test_`. For example, tests for the source file `src/opinf/pre/_basis.py` are grouped in `tests/pre/test_basis.py`. Within that file, the function `test_pod_basis()` runs tests for `pre.pod_basis()`. +- Tests for classes are grouped as classes. For example, the methods of the `TestBaseROM` class in `tests/roms/test_base.py` are unit tests for the methods of the `_BaseROM` class in `src/opinf/roms/_base.py`. + +After making changes to the source code in `src/opinf` and writing corresponding tests in `tests/`, execute `tox` without any arguments in the command line from the root folder of the repository. + +If all tests pass, a line coverage report will be generated. +Open `htmlcov/index.html` in a browser to view the report. + +:::{note} +Running `tox` without any arguments tests the code for Python 3.10 through 3.14 (if they are installed on your system). +To test a single Python version, use `tox -e py314` for Python 3.14, `tox -e py313` for Python3.13, and so on. +::: + +## GitHub Actions + +Pull requests to `develop` are tested through GitHub Actions: +GitHub clones the new version of the repository, runs the linter and unit tests, compiles the documentation, and reports any errors. +**All tests must pass** before changes can be merged. +Before pushing new changes, ensure the following tests pass. + +```shell +(opinfdev) $ tox -e style +(opinfdev) $ tox +``` diff --git a/sphinx-docs/source/index.md b/sphinx-docs/source/index.md new file mode 100644 index 00000000..b9a7f02e --- /dev/null +++ b/sphinx-docs/source/index.md @@ -0,0 +1,79 @@ +# Operator Inference in Python + +[![License](https://img.shields.io/github/license/operator-inference/opinf)](https://github.com/operator-inference/opinf/blob/main/LICENSE) +[![Top language](https://img.shields.io/github/languages/top/operator-inference/opinf)](https://www.python.org) +![Code size](https://img.shields.io/github/languages/code-size/operator-inference/opinf) +[![Issues](https://img.shields.io/github/issues/operator-inference/opinf)](https://github.com/operator-inference/opinf/issues) +[![Latest commit](https://img.shields.io/github/last-commit/operator-inference/opinf)](https://github.com/operator-inference/opinf/commits/main) +[![PyPI](https://img.shields.io/pypi/wheel/opinf)](https://pypi.org/project/opinf/) + +:::{attention} +This documentation is for `opinf` version `0.6.0`. +The `opinf` package is a research code that is still in rapid development. +New versions may introduce substantial new features or API adjustments. +See updates and notes for old versions [here](./opinf/changelog.md). +::: + +This package is a Python implementation of Operator Inference (OpInf), a projection-based model reduction technique for learning polynomial reduced-order models of dynamical systems. +The procedure is data-driven and non-intrusive, making it a viable candidate for model reduction of "glass-box" systems where the structure of the governing equations is known but intrusive code queries are unavailable. + +Get started with [**What is Operator Inference?**](./opinf/intro.md) or head straight to [**Installation**](./opinf/installation.md) and the first tutorial, [**Getting Started**](./tutorials/basics.md). +See [**Literature**](./opinf/literature.md) for a list of scholarly works on operator inference. + +:::{image} ./_static/summary.svg +:align: center +:width: 80 % +::: + +--- + +## Contents + +# Operator Inference + +```{toctree} +:maxdepth: 1 +:caption: Operator Inference + +opinf/intro +opinf/installation +opinf/changelog +opinf/literature +opinf/bibliography +``` + +```{toctree} +:maxdepth: 1 +:caption: Tutorials + +tutorials/basics +tutorials/inputs +tutorials/parametric +``` + +```{toctree} +:maxdepth: 1 +:caption: API Reference + +api/main +api/lift +api/pre +api/basis +api/ddt +api/operators +api/lstsq +api/models +api/roms +api/post +api/utils +``` + +```{toctree} +:maxdepth: 1 +:caption: Developer Guide + +contributing/how_to_contribute +contributing/testing +contributing/documentation +contributing/notation +``` \ No newline at end of file diff --git a/sphinx-docs/source/literature.bib b/sphinx-docs/source/literature.bib new file mode 100644 index 00000000..ca2f0483 --- /dev/null +++ b/sphinx-docs/source/literature.bib @@ -0,0 +1,690 @@ +@article{peherstorfer2016opinf, + title = {Data-driven operator inference for nonintrusive projection-based model reduction}, + author = {Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {306}, + pages = {196--215}, + year = {2016}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2016.03.025}, + category = {origin} +} + +@inproceedings{qian2019transform, + title = {Transform \& {L}earn: {A} data-driven approach to nonlinear model reduction}, + author = {Elizabeth Qian and Boris Kramer and Alexandre N. Marques and Karen E. Willcox}, + booktitle = {AIAA Aviation 2019 Forum}, + pages = {3707}, + year = {2019}, + doi = {10.2514/6.2019-3707}, + category = {nonlinear} +} + +@mastersthesis{swischuk2019thesis, + title = {Physics-based machine learning and data-driven reduced-order modeling}, + author = {Renee Copland Swischuk}, + school = {Massachusetts Institute of Technology}, + year = {2019}, + url = {https://dspace.mit.edu/handle/1721.1/122682}, + category = {thesis} +} + +@article{benner2020deim, + title = {Operator inference for non-intrusive model reduction of systems with non-polynomial nonlinear terms}, + author = {Peter Benner and Pawan Goyal and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {372}, + pages = {113433}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2020.113433}, + category = {nonlinear} +} + +@article{peherstorfer2020reprojection, + title = {Sampling low-dimensional {M}arkovian dynamics for preasymptotically recovering reduced models from data with operator inference}, + author = {Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {42}, + issue = {5}, + pages = {A3489-a3515}, + year = {2020}, + publisher = {SIAM}, + doi = {10.1137/19M1292448}, + category = {reprojection} +} + +@article{qian2020liftandlearn, + title = {Lift \& {L}earn: {P}hysics-informed machine learning for large-scale nonlinear dynamical systems}, + author = {Elizabeth Qian and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {406}, + pages = {132401}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2020.132401}, + category = {nonlinear} +} + +@article{swischuk2020combustion, + title = {Learning physics-based reduced-order models for a single-injector combustion process}, + author = {Renee Swischuk and Boris Kramer and Cheng Huang and Karen Willcox}, + journal = {AIAA Journal}, + volume = {58}, + issue = {6}, + pages = {2658--2672}, + year = {2020}, + publisher = {American Institute of Aeronautics and Astronautics}, + doi = {10.2514/1.J058943}, + category = {application} +} + +@article{ghattas2021acta, + title = {Learning physics-based models from data: {P}erspectives from inverse problems and model reduction}, + author = {Omar Ghattas and Karen Willcox}, + journal = {Acta Numerica}, + volume = {30}, + pages = {445--554}, + year = {2021}, + publisher = {Cambridge University Press}, + doi = {10.1017/S0962492921000064}, + category = {survey} +} + +@inproceedings{jain2021performance, + title = {Performance comparison of data-driven reduced models for a single-injector combustion process}, + author = {Parikshit Jain and Shane McQuarrie and Boris Kramer}, + booktitle = {AIAA Propulsion and Energy 2021 Forum}, + pages = {3633}, + year = {2021}, + doi = {10.2514/6.2021-3633}, + category = {application} +} + +@article{kramer2021quadstability, + title = {Stability domains for quadratic-bilinear reduced-order models}, + author = {Boris Kramer}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {20}, + issue = {2}, + pages = {981--996}, + year = {2021}, + publisher = {SIAM}, + doi = {10.1137/20M1364849}, + category = {nonlinear} +} + +@article{mcquarrie2021combustion, + title = {Data-driven reduced-order models via regularised operator inference for a single-injector combustion process}, + author = {Shane A McQuarrie and Cheng Huang and Karen Willcox}, + journal = {Journal of the Royal Society of New Zealand}, + volume = {51}, + issue = {2}, + pages = {194-211}, + year = {2021}, + publisher = {Taylor \& Francis}, + doi = {10.1080/03036758.2020.1863237}, + category = {scalable} +} + +@phdthesis{qian2021thesis, + title = {A scientific machine learning approach to learning reduced models for nonlinear partial differential equations}, + author = {Elizabeth Yi Qian}, + school = {Massachusetts Institute of Technology}, + year = {2021}, + url = {https://dspace.mit.edu/handle/1721.1/130748}, + category = {thesis} +} + +@mastersthesis{salinger2021thesis, + title = {Toward predictive digital twins for self-aware unmanned aerial vehicles: {N}on-intrusive reduced order models and experimental data analysis}, + author = {Stephanie Joyce Salinger}, + school = {The University of Texas at Austin}, + year = {2021}, + url = {http://dx.doi.org/10.26153/tsw/14557}, + category = {thesis} +} + +@article{uy2021partial, + title = {Operator inference of non-{M}arkovian terms for learning reduced models from partially observed state trajectories}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {Journal of Scientific Computing}, + volume = {88}, + issue = {3}, + pages = {1--31}, + year = {2021}, + publisher = {Springer}, + doi = {10.1007/s10915-021-01580-2}, + category = {statistical} +} + +@article{uy2021error, + title = {Probabilistic error estimation for non-intrusive reduced models learned from data of systems governed by linear parabolic partial differential equations}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {ESAIM: Mathematical Modelling and Numerical Analysis}, + volume = {55}, + issue = {3}, + pages = {735--761}, + year = {2021}, + publisher = {EDP Sciences}, + doi = {10.1051/m2an/2021010}, + category = {reprojection} +} + +@article{yildiz2021shallow, + title = {Learning reduced-order dynamics for parametrized shallow water equations from data}, + author = {S\"{u}leyman Y\i{}ld\i{}z and Pawan Goyal and Peter Benner and B\"{u}lent Karas\"{o}zen}, + journal = {International Journal for Numerical Methods in Fluids}, + volume = {93}, + issue = {8}, + pages = {2803--2821}, + year = {2021}, + publisher = {Wiley Online Library}, + doi = {10.1002/fld.4998}, + category = {parametric} +} + +@article{almeida2022chaotic, + title = {Non-Intrusive reduced models based on operator inference for chaotic systems}, + author = {Jo\~{a}o Lucas de Sousa Almeida and Arthur Cancellieri Pires and Klaus Feine Vaz Cid and Alberto Costa Nogueira Jr}, + journal = {arXiv}, + volume = {2206.01604}, + year = {2022}, + url = {https://arxiv.org/abs/2206.01604}, + category = {application} +} + +@article{benner2022incompressible, + title = {Operator inference and physics-informed learning of low-dimensional models for incompressible flows}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Electronic Transactions on Numerical Analysis}, + volume = {56}, + year = {2022}, + doi = {10.1553/etna_vol56s28}, + category = {structure} +} + +@article{geelen2022localized, + title = {Localized non-intrusive reduced-order modelling in the operator inference framework}, + author = {Rudy Geelen and Karen Willcox}, + journal = {Philosophical Transactions of the Royal Society A}, + volume = {380}, + number = {2229}, + pages = {20210206}, + year = {2022}, + doi = {10.1098/rsta.2021.0206}, + category = {domaindecomp} +} + +@article{guo2022bayesopinf, + title = {Bayesian operator inference for data-driven reduced-order modeling}, + author = {Mengwu Guo and Shane A McQuarrie and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {402}, + pages = {115336}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115336}, + category = {statistical} +} + +@article{khodabakhshi2022diffalg, + title = {Non-intrusive data-driven model reduction for differential algebraic equations derived from lifting transformations}, + author = {Parisa Khodabakhshi and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {389}, + pages = {114296}, + year = {2022}, + doi = {10.1016/j.cma.2021.114296}, + category = {nonlinear} +} + +@article{qian2022pdes, + title = {Reduced operator inference for nonlinear partial differential equations}, + author = {Elizabeth Qian and Ionut-Gabriel Farcas and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {44}, + issue = {4}, + pages = {A1934-a1959}, + year = {2022}, + publisher = {SIAM}, + doi = {10.1137/21M1393972}, + category = {nonlinear} +} + +@inproceedings{rocha2022c02, + title = {Data-driven reduced-order model for atmospheric {CO}2 dispersion}, + author = {Pedro Roberto Barbosa Rocha and Marcos Sebasti\~{a}o de Paula Gomes and Jo\~{a}o Lucas de Sousa Almeida and Allan M Carvalho and Alberto Costa Nogueira Jr}, + booktitle = {AAAI Fall Symposium}, + year = {2022}, + url = {https://s3.us-east-1.amazonaws.com/climate-change-ai/papers/aaaifss2022/2/paper.pdf}, + category = {application} +} + +@article{sharma2022hamiltonian, + title = {Hamiltonian operator inference: {P}hysics-preserving learning of reduced-order models for canonical {H}amiltonian systems}, + author = {Harsh Sharma and Zhu Wang and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {431}, + pages = {133122}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2021.133122}, + category = {structure} +} + +@article{benner2023quaddecoder, + title = {A quadratic decoder approach to nonintrusive reduced-order modeling of nonlinear dynamical systems}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Proceedings in Applied Mathematics and Mechanics}, + volume = {23}, + number = {1}, + pages = {e202200049}, + year = {2023}, + doi = {10.1002/pamm.202200049}, + category = {manifolds} +} + +@inproceedings{farcas2023parametric, + title = {Parametric non-intrusive reduced-order models via operator inference for large-scale rotating detonation engine simulations}, + author = {Ionut-Gabriel Farcas and Rayomand Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0172}, + year = {2023}, + doi = {10.2514/6.2023-0172}, + category = {parametric} +} + +@article{filanova2023mechanical, + title = {An operator inference oriented approach for linear mechanical systems}, + author = {Yevgeniya Filanova and Igor Pontes Duff and Pawan Goyal and Peter Benner}, + journal = {Mechanical Systems and Signal Processing}, + volume = {200}, + pages = {110620}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.ymssp.2023.110620}, + category = {structure} +} + +@article{geelen2023quadmanifold, + title = {Operator inference for non-intrusive model reduction with quadratic manifolds}, + author = {Rudy Geelen and Stephen Wright and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {403}, + pages = {115717}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115717}, + category = {manifolds} +} + +@inproceedings{geelen2023latent, + author = {Rudy Geelen and Laura Balzano and Karen Willcox}, + booktitle = {62nd IEEE Conference on Decision and Control (CDC)}, + title = {Learning latent representations in high-dimensional state spaces using polynomial manifold constructions}, + year = {2023}, + volume = {}, + number = {}, + pages = {4960-4965}, + doi = {10.1109/CDC49753.2023.10384209}, + category = {manifolds} +} + +@article{gruber2023hamiltonian, + title = {Canonical and noncanonical {H}amiltonian operator inference}, + author = {Anthony Gruber and Irina Tezaur}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {416}, + year = {2023}, + doi = {10.1016/j.cma.2023.116334}, + category = {structure} +} + +@article{issan2023shifted, + title = {Predicting solar wind streams from the inner-heliosphere to Earth via shifted operator inference}, + author = {Opal Issan and Boris Kramer}, + journal = {Journal of Computational Physics}, + volume = {473}, + pages = {111689}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.jcp.2022.111689}, + category = {structure} +} + +@article{mcquarrie2023parametric, + title = {Nonintrusive reduced-order models for parametric partial differential equations via data-driven operator inference}, + author = {Shane A McQuarrie and Parisa Khodabakhshi and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1917-A1946}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1452810}, + category = {parametric} +} + +@phdthesis{mcquarrie2023thesis, + title = {Data-driven parametric reduced-order models: {O}perator inference for reactive flow applications}, + author = {Shane Alexander McQuarrie}, + school = {The University of Texas at Austin}, + year = {2023}, + doi = {10.26153/tsw/50172}, + category = {thesis} +} + +@article{rocha2023convection, + title = {Reduced-order modeling of the two-dimensional {R}ayleigh--{B}\'{e}nard convection flow through a non-intrusive operator inference}, + author = {Pedro Roberto Barbosa Rocha and Jo\~{a}o Lucas de Sousa Almeida and Marcos Sebasti\~{a}o de Paula Gomes and Alberto Costa Nogueira Jr}, + journal = {Engineering Applications of Artificial Intelligence}, + volume = {126}, + pages = {106923}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.engappai.2023.106923}, + category = {application} +} + +@article{sawant2023pireg, + title = {Physics-informed regularization and structure preservation for learning stable reduced models from data with operator inference}, + author = {Nihar Sawant and Boris Kramer and Benjamin Peherstorfer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {404}, + pages = {115836}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115836}, + category = {structure} +} + +@phdthesis{sawant2023thesis, + title = {Learning structured and stable reduced models from data with operator inference}, + author = {Nihar Sawant}, + year = {2023}, + school = {New York University}, + category = {thesis} +} + +@article{uy2023active, + title = {Active operator inference for learning low-dimensional dynamical-system models from noisy data}, + author = {Wayne Isaac Tan Uy and Yuepeng Wang and Yuxiao Wen and Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1462-a1490}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1439729}, + category = {statistical} +} + +@article{uy2023rollouts, + title = {Operator inference with roll outs for learning reduced models from scarce and low-quality data}, + author = {Wayne Isaac Tan Uy and Dirk Hartmann and Benjamin Peherstorfer}, + journal = {Computers \& Mathematics with Applications}, + volume = {145}, + pages = {224--239}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.camwa.2023.06.012}, + category = {statistical} +} + +@inproceedings{zastrow2023flutter, + title = {Data-driven model reduction via operator inference for coupled aeroelastic flutter}, + author = {Benjamin G Zastrow and Anirban Chaudhuri and Karen Willcox and Anthony S Ashley and Michael C Henson}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0330}, + year = {2023}, + doi = {10.2514/6.2023-0330}, + category = {application} +} + +@inproceedings{aretz2024enforcing, + title = {Enforcing structure in data-driven reduced modeling through nested {O}perator {I}nference}, + author = {Nicole Aretz and Karen Willcox}, + booktitle = {63rd IEEE Conference on Decision and Control (CDC)}, + year = {2024}, + organization = {IEEE}, + doi = {10.1109/CDC56724.2024.10885857}, + category = {reprojection} +} + +@article{boef2024stablesparse, + title = {Stable sparse operator inference for nonlinear structural dynamics}, + author = {Pascal {den Boef} and Diana Manvelyan and Joseph Maubach and Wil Schilders and Nathan {van de Wouw}}, + journal = {arXiv}, + volume = {2407.21672}, + year = {2024}, + doi = {10.48550/arXiv.2407.21672}, + category = {structure} +} + +@article{bychkov2024quadratization, + title = {Exact and optimal quadratization of nonlinear finite-dimensional non-autonomous dynamical systems}, + author = {Andrey Bychkov and Opal Issan and Gleb Pogudin and Boris Kramer}, + journal = {SIAM Journal of Applied Dynamical Systems}, + volume = {23}, + number = {1}, + pages = {982-1016}, + year = {2024}, + doi = {10.1137/23M1561129}, + category = {nonlinear} +} + +@article{farcas2024domaindecomposition, + title = {Domain decomposition for data-driven reduced modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {AIAA Journal}, + volume = {62}, + number = {11}, + pages = {4071-4086}, + year = {2024}, + doi = {10.2514/1.J063715}, + category = {domaindecomp} +} + +@misc{freitag2024stochastic, + title = {Learning stochastic reduced models from data: a nonintrusive approach}, + author = {Melina A. Freitag and Jan Martin Nicolaus and Martin Redmann}, + journal = {arXiv}, + volume = {2407.05724}, + year = {2024}, + doi = {10.48550/arXiv.2407.05724}, + category = {statistical} +} + +@article{geelen2024nonlinmanifold, + title = {Learning physics-based reduced-order models from data using nonlinear manifolds}, + author = {Rudy Geelen and Laura Balzano and Stephen Wright and Karen Willcox}, + journal = {Chaos: An Interdisciplinary Journal of Nonlinear Science}, + volume = {34}, + number = {3}, + pages = {033122}, + year = {2024}, + doi = {10.1063/5.0170105}, + category = {manifolds} +} + +@article{geng2024gradient, + title = {Gradient preserving {O}perator {I}nference: {D}ata-driven reduced-order models for equations with gradient structure}, + author = {Yuwei Geng and Jasdeep Singh and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {427}, + pages = {117033}, + year = {2024}, + doi = {10.1016/j.cma.2024.117033}, + category = {structure} +} + +@inproceedings{koike2024energy, + title = {Energy-preserving reduced operator inference for efficient design and control}, + author = {Tomoki Koike and Elizabeth Qian}, + booktitle = {AIAA SciTech 2024 Forum}, + pages = {1012}, + year = {2024}, + doi = {10.2514/6.2024-1012}, + category = {structure} +} + +@article{kramer2024survey, + title = {Learning nonlinear reduced models from data with operator inference}, + author = {Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Annual Review of Fluid Mechanics}, + volume = {56}, + pages = {521--548}, + year = {2024}, + publisher = {Annual Reviews}, + doi = {10.1146/annurev-fluid-121021-025220}, + category = {survey} +} + +@article{sharma2024lagrangian, + title = {Lagrangian operator inference enhanced with structure-preserving machine learning for nonintrusive model reduction of mechanical systems}, + author = {Harsh Sharma and David A Najera-Flores and Michael D Todd and Boris Kramer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {423}, + pages = {116865}, + year = {2024}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2024.116865}, + category = {structure} +} + +@article{sharma2024preserving, + title = {Preserving {L}agrangian structure in data-driven reduced-order modeling of large-scale mechanical systems}, + author = {Harsh Sharma and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {462}, + pages = {134128}, + year = {2024}, + doi = {10.1016/j.physd.2024.134128}, + category = {structure} +} + +@article{aretz2025nestedopinf, + title = {Nested operator inference for adaptive data-driven learning of reduced-order models}, + author = {Nicole Aretz and Karen Willcox}, + journal = {arXiv}, + volume = {2508.11542}, + year = {2025}, + doi = {10.48550/arXiv.2508.11542}, + category = {structure} +} + +@inbook{blonigan2025thermal, + title = {Operator inference-based model order reduction of thermal protection system finite element simulations}, + author = {Patrick J. Blonigan and John Tencer and Sean Babiniec and Jonathan Murray}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {2133}, + year = {2025}, + doi = {10.2514/6.2025-2133}, + category = {application} +} + +@inbook{farcas2025parallel, + title = {A parallel implementation of reduced-order modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {1170}, + year = {2025}, + doi = {10.2514/6.2025-1170}, + category = {scalable} +} + +@article{farcas2025distributed, + title = {Distributed computing for physics-based data-driven reduced modeling at scale: {A}pplication to a rotating detonation rocket engine}, + author = {Ionut-Gabriel Farcaş and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {Computer Physics Communications}, + volume = {313}, + pages = {109619}, + year = {2025}, + doi = {10.1016/j.cpc.2025.109619}, + category = {scalable} +} + +@article{geng2025porthamiltonian, + title = {Data-driven reduced-order models for port-{H}amiltonian systems with {O}perator {I}nference}, + author = {Yuwei Geng and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {arXiv}, + volume = {2501.02183}, + year = {2025}, + doi = {10.48550/arXiv.2501.02183}, + category = {structure} +} + +@article{gkimisis2025spatiallylocal, + title = {Non-intrusive reduced-order modeling for dynamical systems with spatially localized features}, + author = {Leonidas Gkimisis and Nicole Aretz and Marco Tezzele and Thomas Richter and Peter Benner and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {444}, + pages = {118115}, + year = {2025}, + doi = {10.1016/j.cma.2025.118115}, + category = {domaindecomp} +} + +@article{gruber2025variational, + author = {Anthony Gruber and Irina Tezaur}, + title = {Variationally consistent {H}amiltonian model reduction}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {24}, + number = {1}, + pages = {376-414}, + year = {2025}, + doi = {10.1137/24M1652490}, + category = {structure} +} + +@article{kang2025semiconductor, + title = {Parametric {O}perator {I}nference to simulate the purging process in semiconductor manufacturing}, + author = {Seunghyon Kang and Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2504.03990}, + year = {2025}, + doi = {10.48550/arXiv.2504.03990}, + category = {application} +} + +@article{kim2025stateconstraints, + title = {Physically consistent predictive reduced-order modeling by enhancing {O}perator {I}nference with state constraints}, + author = {Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2502.03672}, + year = {2025}, + doi = {10.48550/arXiv.2502.03672}, + category = {structure} +} + +@article{mcquarrie2025gpbayesopinf, + title = {Bayesian learning with {G}aussian processes for low-dimensional representations of time-dependent nonlinear systems}, + author = {Shane A. McQuarrie and Anirban Chaudhuri and Karen E. Willcox and Mengwu Guo}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {475}, + pages = {134572}, + year = {2025}, + doi = {10.1016/j.physd.2025.134572}, + category = {statistical} +} + +@article{rosenberger2025exactopinf, + title = {Exact operator inference with minimal data}, + author = {Henrik Rosenberger and Benjamin Sanderse and Giovanni Stabile}, + journal = {arXiv}, + volume = {2506.01244}, + year = {2025}, + doi = {10.48550/arXiv.2506.01244}, + category = {reprojection} +} + +@article{vijaywargiya2025tensoropinf, + title = {Tensor parametric {H}amiltonian operator inference}, + author = {Arjun Vijaywargiya and Shane A. McQuarrie and Anthony Gruber}, + journal = {arXiv}, + volume = {2502.10888}, + year = {2025}, + doi = {10.48550/arXiv.2502.10888}, + category = {parametric} +} diff --git a/sphinx-docs/source/opinf/bibliography.md b/sphinx-docs/source/opinf/bibliography.md new file mode 100644 index 00000000..d977a9bd --- /dev/null +++ b/sphinx-docs/source/opinf/bibliography.md @@ -0,0 +1,4 @@ +# References + +```{bibliography} +``` diff --git a/sphinx-docs/source/opinf/changelog.md b/sphinx-docs/source/opinf/changelog.md new file mode 100644 index 00000000..b00ee45c --- /dev/null +++ b/sphinx-docs/source/opinf/changelog.md @@ -0,0 +1,291 @@ +# What's New + +:::{attention} +`opinf` is a research code that is still in rapid development. +New versions may introduce substantial new features or API adjustments. +::: + +## Version 0.6.0 + +Updates to the documentation to reflect the new address. +No changes to package functionality. + +## ⭐ We've Moved ⭐ + +The `opinf` repository is now hosted at [github.com/operator-inference/opinf](https://github.com/operator-inference/opinf). +The previous address, github.com/Willcox-Research-Group/rom-operator-inference-Python3, now directs to this new address. + +## Version 0.5.17 + +- Dropped support for Python 3.9, added test coverage for Python 3.14. +- Tikhonov / L2 least-squares now take an initial guess +- Expansion of operators to larger or smaller bases (for nested OpInf) +- Parametric and nonparametric polynomial operators of arbitrary order +- Bases now have a `fit_compress()` method +- Update `utils.TimedBlock` to use `time.process_time()` + +## Version 0.5.16 + +Backend improvements to the regularization selection procedure. + +- `ROM.fit()` calls `ROM.model.fit()` instead of `ROM.model._fit_solver()`, which works better for inheritance. +- `BayesianROM.fit_regselect_*()` catches errors and suppresses warnings from the least-squares solver. +- Fixed a bug for interpolatory parametric models where `fit_regselect_*()` would only update the regularizer for the overall `model.solver`, not for each of the individual solvers for the models to be interpolated. +- Fixed a bug related to test cases being processed before the basis was initialized. +- Removed unnecessary abstract methods from `models.mono._base._OpInfModel`. +- Small updates to the literature page. + +## Version 0.5.15 + +- Improvement to `fit_regselect_*()` so that the regularization does not have to be initialized before fitting the model. This fixes a longstanding chicken/egg problem and makes using `fit_regselect_*()` much less cumbersome. +- Ensured error computation correctly aligns training and predicted states in `fit_regselect_continuous()`. Previously, this could have been misaligned when using a time derivative estimator that truncates states. +- Time derivative estimators in `opinf.ddt` now have a `mask()` method that map states to the estimation grid. +- Added regression tests based on the tutorial notebooks. +- Added `pytest` and `pytest-cov` to the dependencies for developers. + +## Version 0.5.14 + +- Catch any errors in `fit_regselect*()` that occur when the model uses `refit()`. +- Tikhonov-type least-squares solvers do not require the regularizer in the constructor but will raise an `AttributeError` in `solve()` (and other methods) if the regularizer is not set. +- `PODBasis.fit(Q)` raises a warning when using the `"method-of-snapshots"`/`"eigh"` strategy if $n < k$ for $\mathbf{Q}\in\mathbb{R}^{n \times k}.$ In this case, calculating the $n \times k$ SVD is likely more efficient than the $k \times k$ eigenvalue problem. +- Added Python 3.13 to list of tests. + +## Version 0.5.13 + +Bayesian operator inference: + +- New `roms.BayesianROM` class. +- New supporting class `roms.OperatorPosterior`. +- Updates to relevant least-squares solvers. + +These changes implement the framework proposed in {cite}`guo2022bayesopinf`. + +## Version 0.5.12 + +- New `operators.QuarticOperator`, plus unit tests. +- Reorganized some unit tests for models and operators to have OOP structure. +- Bugfix: `fit_regselect_continuous()` now returns `self` + +## Version 0.5.11 + +- New scaling option for `pre.ShiftScaleTransformer` so that training snapshots have at maximum norm 1. Contributed by [@nicolearetz](https://github.com/nicolearetz). +- Small clarifications to `pre.ShiftScaleTransformer` and updates to the `pre` documentation. + +## Version 0.5.10 + +New POD basis solver option `basis.PODBasis(solver="method-of-snapshots")` (or `solver="eigh"`), which solves a symmetric eigenvalue problem instead of computing a (weighted) SVD. This method is more efficient than the SVD for snapshot matrices $\mathbf{Q}\in\mathbb{R}^{n\times k}$ where $n \gg k$ and is significantly more efficient than the SVD when a non-diagonal weight matrix is provided. +Contributed by [@nicolearetz](https://github.com/nicolearetz). + +## Version 0.5.9 + +Automatic regularization selection: + +- New methods `fit_regselect_continuous()` and `fit_regselect_discrete()` in the `ROM` and `ParametricROM` classes. +- `utils.gridsearch()` implements grid search followed up by derivative-free log-scale optimization. +- Class method `TikhonovSolver.get_operator_regularizer()` to construct diagonal Tikhonov regularizers where each operator is regularized by a different scalar. + +New transformers for custom shifting / scaling: + +- `pre.ShiftTransformer` +- `pre.ScaleTransformer` +- `pre.NullTransformer` +- `pre.TransformerPipeline` + +Small improvements: + +- Executing individual test files now runs the tests contained within. +- `utils.TimedBlock` has a `rebuffer` attribute that, when set to `True`, prevents printing until the end of the block. +- Improved test coverage, fixed some documentation typos, etc. + +## Version 0.5.8 + +Support for affine-parametric problems: + +- Affine-parametric operator classes `AffineConstantOperator`, `AffineLinearOperator`, etc. +- Parametric model classes `ParametricContinuousModel`, `ParametricDiscreteModel`. +- `ParametricROM` class. +- Updates to operator / model documentation. + +Renamed interpolatory operators / model classes from `Interpolated` to `Interp`. +Old names are deprecated but not yet removed. + +Miscellaneous: + +- Reorganized and expanded tutorials. +- Added and documented `opinf.utils.TimedBlock` context manager for quick timing of code blocks. +- Updated structure for some unit tests. +- Refactored interpolatory operators. +- Standardized string representations, added `[Parametric]ROM.__str__()`. +- Removed some public functions from `operators`, regrouped in `operators._utils`. +- Removed some public functions from `models`, regrouped in `models._utils`. + +## Version 0.5.7 + +Updates to `opinf.lstsq`: + +- New `TruncatedSVDSolver` class. +- `predict()` has been renamed `solve()` for `opinf.lstsq` solver classes to not clash with `predict()` from `opinf.roms` / `opinf.models` classes. +- `solve()` always returns a two-dimensional array, even if $r = 1$. + +Various small improvements to tests and documentation. + +## Version 0.5.6 + +Added public templates to `opinf.operators`: + +- `OperatorTemplate` for general nonparametric operators. +- `OpInfOperator` for nonparametric operators that can be learned through Operator Inference (operator matrix times data vector structure). +- `ParametricOperatorTemplate` for general parametric operators. +- `ParametricOpInfOperator` for parametric operators that can be learned through Operator Inference. + +Also added a new `opinf.ddt.InterpolationDerivativeEstimator` class and made various small changes for compatibility with [NumPy 2.0.0](https://numpy.org/doc/stable/release/2.0.0-notes.html). + +## Version 0.5.5 + +Changes to the `opinf.lstsq` API and improvements to the documentation. + +- `opinf.model` classes now receive solvers **in the constructor, not in fit()**. This change will be useful for future models that require specific solvers. Updated `ROM` class and tutorials accordingly. +- New `SolverTemplate` class and inheritance guide for creating new solvers. +- Renamed attributes to match OpInf terminology. + - `A --> data_matrix`, called $\D$ in the docs. + - `B --> lhs_matrix`, called $\Z$ in the docs. **Warning:** `fit()` receives $\Z$, **not** $\Z\trp$! + - $X$ is replaced with $\Ohat$. **Warning**: `predict()` returns $\Ohat$, **not** $\Ohat\trp$! +- Renamed two Tikhonov solver classes: + - `L2SolverDecoupled --> L2DecoupledSolver` + - `TikhonovSolverDecoupled --> TikhonovDecoupledSolver` +- Tikhonov solvers no longer have a default regularization value of zero. + +Before: + +```python +>>> model = opinf.models.ContinuousModel("A") +>>> solver = opinf.lstsq.L2Solver(regularizer=1e-2) +>>> model.fit(states, ddts, inputs, solver=solver) +``` + +After: + +```python +>>> solver = opinf.lstsq.L2Solver(regularizer=1e-2) +>>> model = opinf.models.ContinuousModel("A", solver=solver) +>>> model.fit(states, ddts, inputs) +``` + +## Version 0.5.4 + +New `opinf.roms` submodule containing an `opinf.roms.ROM` class, also available in the main namespace as `opinf.ROM`. +This class wraps a lifter, transformer, basis, time derivative estimator, model, and least-squares solver together for convenience. +Rewrote the first tutorial to use `opinf.ROM`. + +## Version 0.5.3 + +Expanded the `ddt` submodule (but no API changes to existing functions). + +- New backward, central, and forward difference schemes up to sixth order. +- New `DerivativeEstimatorTemplate` class for implementing custom derivative estimation strategies. +- New `UniformFiniteDifference` and `NonuniformFiniteDifference` convenience classes for working with `ROM` classes in the future. +- Better documentation of the module. + +The documentation was also updated to JupyterBook 1.0.0, a significant improvement to the look and feel. + +Tests are now managed with `tox`, the contributor instructions were updated. + +## Version 0.5.2 + +Significant updates to the `pre` and `basis` submodules. + +Updates to `pre`: + +- New `TransformerTemplate` class for defining custom transformers. +- Renamed `SnapshotTransformer` to `ShiftScaleTransformer`. +- Removed `SnapshotTransformerMulti`. +- New `TransformerMulti` class for joining multiple transformations. +- Renamed some attributes of the transformer classes: `n` -> `full_state_dimension`, `ni` -> `variable_size`, etc. + +Updates to `basis`: + +- New `BasisTemplate` class for defining custom bases. +- Standardized arguments of `fit()` to always be the snapshots. Hyperparameters must come in the constructor. +- `LinearBasis` now accepts an optional weight matrix. +- `LinearBasis` raises a warning if the basis entries are not orthogonal (w.r.t. the weights). +- Updated `PODBasis` dimensionality properties. Use `set_dimension()` to update the reduced state dimension on the fly. +- Removed `PODBasisMulti`. +- New `BasisMulti` multi class for joining multiple bases. +- Renamed some attributes of the basis classes (matching transformer syntax): `n` -> `full_state_dimension`, `r` -> `reduced_state_dimension`, etc. + +Added a new Literature page to the documentation listing Operator Inference publications. + +## Version 0.5.1 + +New `lift` module that defines a template class for implementing lifting transformations. + +## Version 0.5.0 + +- Overhauled the `operators` module so that each operator class is responsible for its portion of the Operator Inference data matrix. + - New `StateInputOperator` for state-input bilinear interactions, $\Nhat[\u\otimes\qhat]$. + - Operator classes now have `state_dimension` and `input_dimension` properties. + - Operator classes must implement `datablock()` and `operator_dimension()` methods to facilitate operator inference. +- Renamed `roms` to `models` and updated class names: + - `ContinuousOpInfROM` to `ContinuousModel` + - `DiscreteOpInfROM` to `DiscreteModel` + - `SteadyOpInfROM` to `SteadyModel` + - Same for interpolated models. +- Model classes now take a list of operators in the constructor. String shortcuts such as `"cAH"` are still valid, similar to the previous `modelform` argument. The `known_operators` argument has been removed from the `fit()` method. +- Model classes no longer have a `basis` attribute. + - The `basis` argument has been removed from the `fit()` method. + - The `compress()` and `decompress()` methods have been removed from model classes. + - The dimensions `n` and `r` have been replaced with `state_dimension`; `m` is now `input_dimension`. +- Moved time derivative estimation tools to the new `ddt` submodule. +- Moved Kronecker product utilities to static methods of nonparametric operators. + - `utils.kron2c()` is now `QuadraticOperator.ckron()` + - `utils.kron2c_indices()` is now `QuadraticOperator.ckron_indices()` + - `utils.compress_quadratic()` is now `QuadraticOperator.compress_entries()` + - `utils.expand_quadratic()` is now `QuadraticOperator.expand_entries()` + - `utils.kron3c()` is now `CubicOperator.ckron()` + - `utils.kron3c_indices()` is now `CubicOperator.ckron_indices()` + - `utils.compress_cubic()` is now `CubicOperator.compress_entries()` + - `utils.expand_cubic()` is now `CubicOperator.expand_entries()` + +## Version 0.4.5 + +- Moved basis classes and dimensionality reduction tools to a new `basis` submodule. +- Moved operator classes from `core.operators` to a new `operators` submodule. +- Renamed the `core` submodule to `roms`. +- Moved time derivative estimation tools to the `utils` module. + +## Version 0.4.4 + +- Fixed a bug in `SnapshotTransformer.load()` that treated the `centered_` attribute incorrectly. +- Removed the `transformer` attribute from basis classes. +- Renamed `encode()` to `compress()` and `decode()` to `decompress()`. + +## Version 0.4.2 + +- In the `fit()` method in ROM classes, replaced the `regularizer` argument with a `solver` keyword argument. The user should pass in an instantiation of a least-squares solver class from the `lstsq` submodule. +- Hyperparameters for least-squares solver classes in the `lstsq` submodule are now passed to the constructor; `predict()` must not take any arguments. +- Renamed the following least-squares solver classes in the `lstsq` submodule: + - `SolverL2` -> `L2Solver` + - `SolverL2Decoupled` -> `L2SolverDecoupled` + - `SolverTikhonov` -> `TikhonovSolver` + - `SolverTikhonovDecoupled` -> `TikhonovSolverDecoupled` + +Before: + +```python +>>> rom.fit(basis, states, ddts, inputs, regularizer=1e-2) +``` + +After: + +```python +>>> solver = opinf.lstsq.L2Solver(regularizer=1e-2) +>>> rom.fit(basis, states, ddts, inputs, solver=solver) + +# The L2 solver is also the default if a float is given: +>>> rom.fit(basis, states, ddts, inputs, solver=1e-2) +``` + +## Versions 0.4.0 and 0.4.1 + +This version is a migration of the old `rom_operator_inference` package, version 1.4.1. +See [this page](https://github.com/operator-inference/opinf/wiki/API-Reference) for the documentation. diff --git a/sphinx-docs/source/opinf/installation.md b/sphinx-docs/source/opinf/installation.md new file mode 100644 index 00000000..d24e9397 --- /dev/null +++ b/sphinx-docs/source/opinf/installation.md @@ -0,0 +1,75 @@ +# Package Installation + +This page describes how to install `opinf` locally. + +:::{important} +Like most Python packages, `opinf` has a few [software dependencies](https://github.com/operator-inference/opinf/network/dependencies). +To avoid conflicts with other installed packages, we recommend installing `opinf` within a new [conda environment](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) (recommended) or [virtual Python environment](https://docs.python.org/3/tutorial/venv.html) . + +```shell +# Make a fresh conda environment and install Python 3.14. +conda create -n opinf python=3.14 +``` + +Be sure to [activate](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#activating-an-environment) the environment before using `pip` or other installation tools. + +```shell +# Activate the conda environment (updates the PATH). +$ conda activate opinf + +# Verify python is now linked to the conda environment. +(opinf) $ which python3 +/path/to/your/conda/envs/opinf/bin/python3 +$ python3 --version +Python 3.14.2 +``` + +::: + +:::{tip} +To check if the package is already installed in the current Python environment, run the following in the command line. + +```shell +(opinf) $ python3 -m pip freeze | grep opinf +9:opinf==0.6.0 +``` + +No output means the package was not found. +::: + +## Latest Release from PyPi (Recommended) + +We recommend installing the package from [the Python Package Index](https://pypi.org/project/opinf/) with [`pip`](https://pypi.org/project/pip/). +This installs the [latest official release](https://github.com/operator-inference/opinf/releases). + +```shell +$ conda activate opinf +(opinf) $ python3 -m pip install opinf +``` + +## Latest Commit to Main Branch + +The following command installs the latest version from the `main` branch, which may or may not be associated with [an official release](https://github.com/operator-inference/opinf/releases). +This requires [`git`](https://git-scm.com/). + +```shell +$ conda activate opinf + +# Install with git + SSH credentials (recommended). +(opinf) $ python3 -m pip install git+ssh://git@github.com/operator-inference/opinf.git + +# Install with git + HTTPS. +(opinf) $ python3 -m pip install git+https://github.com/operator-inference/opinf.git +``` + +## Source Code + +The final option is to download the entire repository (including tests, documentation, etc.) and install the package from source. +This also requires [`git`](https://git-scm.com/) and is the first step for contributing. +See the [Developer Guide](../contributing/how_to_contribute.md) if you are interested in contributing. + +```shell +$ git clone git@github.com:operator-inference/opinf.git OpInf +$ conda activate opinf +(opinf) $ python3 -m pip install OpInf +``` diff --git a/sphinx-docs/source/opinf/intro.md b/sphinx-docs/source/opinf/intro.md new file mode 100644 index 00000000..ef6b7c13 --- /dev/null +++ b/sphinx-docs/source/opinf/intro.md @@ -0,0 +1,541 @@ +# What is Operator Inference? + +The goal of Operator Inference (OpInf) is to construct a low-dimensional, computationally inexpensive system whose solutions are close to those of some high-dimensional system for which we have 1) training data and 2) some knowledge about the system structure. +The main steps are the following. + +1. [**Get training data**](subsec-training-data). Gather and [preprocess](../api/pre.md) high-dimensional data to learn a low-dimensional model from. This package has a few common preprocessing tools, but the user must bring the data to the table. +2. [**Compute a low-dimensional state representation**](subsec-basis-computation). Approximate the high-dimensional data with only a few degrees of freedom. The simplest approach is to take the SVD of the high-dimensional training data, extract the first few left singular vectors, and use these vectors as a new coordinate basis. +3. [**Set up and solve a low-dimensional regression**](subsec-opinf-regression). Use the low-dimensional representation of the training data to determine a reduced-order model (ROM) that best fits the data in a minimum-residual sense. This is the core objective of the package. +4. [**Solve the reduced-order model**](subsec-rom-evaluation). Use the learned model to make computationally efficient predictions. + +:::{figure} ../../_static/summary.svg +--- +width: 80 % +--- +The OpInf workflow. +::: + +This page gives an overview of OpInf by walking through each of these steps. + +## Problem Statement + +::::{margin} +:::{note} +We are most often interested in spatial discretizations of partial differential equations (PDEs), but {eq}`eq:opinf-example-fom` does not necessarily have to be related to a PDE. +::: +:::: + +Consider a system of ordinary differential equations (ODEs) with state $\q(t)\in\RR^{n}$ and inputs $\u(t)\in\RR^{m}$, + +$$ +\begin{aligned} + \ddt\q(t) = \f(t, \q(t), \u(t)), +\end{aligned} +$$ (eq:opinf-example-fom) + +where $\f:\RR\times\RR^n\times\RR^m\to\RR^n$. +We call {eq}`eq:opinf-example-fom` the _full-order model_ (FOM). +Given samples of the state $\q(t)$ and corresponding inputs $\u(t)$, OpInf learns a surrogate system for {eq}`eq:opinf-example-fom` with the much smaller state $\qhat(t) \in \RR^{r}, r \ll n,$ and a polynomial structure, for example: + +::::{margin} +:::{note} +The $\otimes$ operator is called the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product). +::: +:::: + +$$ +\begin{aligned} + \ddt\qhat(t) + = \chat + + \Ahat\qhat(t) + + \Hhat[\qhat(t)\otimes\qhat(t)] + + \Bhat\u(t). +\end{aligned} +$$ (eq:opinf-example-rom) + +We call {eq}`eq:opinf-example-rom` a _reduced-order model_ (ROM) for {eq}`eq:opinf-example-fom`. +Our goal is to infer the _reduced-order operators_ $\chat \in \RR^{r}$, $\Ahat\in\RR^{r\times r}$, $\Hhat\in\RR^{r\times r^{2}}$, and/or $\Bhat\in\RR^{r\times m}$ using data from {eq}`eq:opinf-example-fom`. +The user specifies which terms to include in the model. + +::::{important} +:name: projection-preserves-structure + +The right-hand side of the ROM {eq}`eq:opinf-example-rom` is a polynomial with respect to the state $\qhat(t)$: +$\chat$ are the constant terms, $\Ahat\qhat(t)$ are the linear terms, $\Hhat[\qhat(t)\otimes\qhat(t)]$ are the quadratic terms, with input terms $\Bhat\u(t)$. +The user must choose which terms to include in the ROM, and this choice should be motivated by the structure of the FOM {eq}`eq:opinf-example-fom` and the way that the reduced-order state $\qhat(t)$ approximates the full-order state $\q(t)$. + +For example, suppose the FOM is a linear time-invariant (LTI) system, written as + +$$ +\begin{aligned} + \ddt\q(t) + = \A\q(t) + \B\u(t) +\end{aligned} +$$ + +for some $\A\in\RR^{n\times n}$ and $\B\in\RR^{n\times m}$. +Let $\Vr\in\RR^{n\times r}$ be a matrix with orthonormal columns and suppose we approximate the FOM state with the linear relationship $\q(t) \approx \Vr\qhat(t)$. +Then the ROM should mirror the LTI structure of the FOM: + +$$ +\begin{aligned} + \ddt\qhat(t) + = \Ahat\qhat(t) + \Bhat\u(t), +\end{aligned} +$$ + +with $\Ahat\in\RR^{r\times r}$ and $\Bhat\in\RR^{r\times m}$. + + + +:::{dropdown} Motivation +**Projection preserves polynomial structure** {cite}`benner2015pmorsurvey,peherstorfer2016opinf`. +The classical (Galerkin) projection-based ROM for {eq}`eq:opinf-example-rom` is obtained by substituting $\q(t)$ with its low-dimensional approximation and enforcing an orthogonality condition. + +For example, if $\q(t) \approx \Vr\qhat(t)$, then the Galerkin ROM is + +$$ +\begin{aligned} + \ddt\qhat(t) = \Vr\trp\f(t, \Vr\qhat(t), \u(t)). +\end{aligned} +$$ + +Now suppose that $\f$ can be written in a polynomial form, + +$$ +\begin{aligned} + \f(t,\q(t),\u(t)) + = \c + \A\q(t) + \H[\q(t)\otimes\q(t)] + \B\u(t), +\end{aligned} +$$ + +where $\c\in\RR^{n}$, $\A\in\RR^{n\times n}$, $\H\in\RR^{n\times n^2}$, and $\B\in\RR^{r\times m}$. +We then have + +$$ +\begin{aligned} + \Vr\trp\f(t, \Vr\qhat(t), \u(t)) + = \Vr\trp\left(\c + \A\Vr\qhat(t) + \H[(\Vr\qhat(t))\otimes(\Vr\qhat(t))] + \B\u(t)\right). +\end{aligned} +$$ + +Since $\Vr\trp\Vr$ is the identity matrix and using $(\mathbf{XY})\otimes(\mathbf{ZW}) = (\mathbf{X}\otimes \mathbf{Z})(\mathbf{Y}\otimes\mathbf{W})$, this reduces to + +$$ +\begin{aligned} + \Vr\trp\f(t, \Vr\qhat(t), \u(t)) + = \chat + \Ahat\qhat(t) + \Hhat[\qhat(t)\otimes\qhat(t)] + \Bhat\u(t), +\end{aligned} +$$ + +where $\chat = \Vr\trp\c$, $\Ahat = \Vr\trp\A\Vr$, $\Hhat = \Vr\trp\H\left(\Vr\otimes\Vr\trp\right)$, and $\Bhat = \Vr\trp\B$. +Hence, the Galerkin ROM has the same polynomial form as $\f$. + +OpInf aims to learn appropriate reduced-order operators $\chat$, $\Ahat$, $\Hhat$, and/or $\Bhat$ _from data_ and is therefore useful for situations where $\c$, $\A$, $\H$, and/or $\B$ are not explicitly available for matrix computations. +::: +:::: + +(subsec-training-data)= +## Get Training Data + +OpInf learns ROMs from full-order state/input data. +Start by gathering solution and input data and organizing them columnwise into the _state snapshot matrix_ $\Q$ and _input matrix_ $\U$, + +$$ +\begin{aligned} + \Q + &= \left[\begin{array}{cccc} + & & & \\ + \q_{1} & \q_{2} & \cdots & \q_{k} + \\ & & & + \end{array}\right] + \in \RR^{n \times k}, + & + \U + &= \left[\begin{array}{cccc} + & & & \\ + \u_{1} & \u_{2} & \cdots & \u_{k} + \\ & & & + \end{array}\right] + \in \RR^{m \times k}, +\end{aligned} +$$ + +where $n$ is the dimension of the (discretized) state, $m$ is the dimension of the input, $k$ is the number of available data points, and the columns of $\Q$ and $\U$ are the solution to the FOM at some time $t_j$: + +$$ +\begin{aligned} + \ddt\q(t)\bigg|_{t = t_j} + = \f(t_{j}, \q_{j}, \u_{j}). +\end{aligned} +$$ + +:::{tip} +Raw dynamical systems data often needs to be lightly preprocessed before it can be used in OpInf. +Preprocessing can promote stability in the inference of the reduced-order operators and improve the stability and accuracy of the resulting ROM {eq}`eq:opinf-example-rom`. +Common preprocessing steps include +1. Variable transformations / lifting to induce a polynomial structure. +2. Centering or shifting data to account for boundary conditions. +3. Scaling / nondimensionalizing the variables represented in the state. + +See {mod}`opinf.lift` and {mod}`opinf.pre` for details and examples. +::: + +OpInf uses a regression problem to compute the reduced-order operators, which requires state data ($\Q$), input data ($\U$), as well as data for the corresponding time derivatives: + +$$ +\begin{aligned} + \dot{\Q} + = \left[\begin{array}{cccc} + & & & \\ + \dot{\q}_{1} & + \dot{\q}_{2} & \cdots & + \dot{\q}_{k} + \\ & & & + \end{array}\right] + \in \RR^{n \times k}, + \qquad + \dot{\q}_{j} = \ddt\q(t)\bigg|_{t = t_j} \in \RR^{n}. +\end{aligned} +$$ + +:::{note} +If the time derivatives cannot be computed directly by evaluating $\f(t_{j}, \q_{j}, \u_{j})$, in some cases they may be inferred from the state snapshots. +The simplest approach is to use [finite differences](https://en.wikipedia.org/wiki/Numerical_differentiation) of the state snapshots, see {mod}`opinf.ddt`. +::: + +:::{warning} +If you do any preprocessing on the states, be sure to use the time derivatives of the _processed states_, not of the original states. +::: + + + + +(subsec-basis-computation)= +## Compute a Low-dimensional State Representation + +The purpose of learning a ROM is to achieve a computational speedup. +This is accomplished by introducing an approximate representation of the $n$-dimensional state using only $r \ll n$ degrees of freedom. +The most common approach is to represent the state as a linear combination of $r$ vectors: + +$$ + \q(t) + \approx \Vr \qhat(t) + = \sum_{i=1}^{r}\v_{i}\hat{q}_{i}(t), +$$ (eq-opinf-basis-def) + +where + +$$ + \Vr + = \left[\begin{array}{ccc} + & & \\ + \v_{1} & \cdots & \v_{r} + \\ & & + \end{array}\right] \in \RR^{n \times r}, + \qquad + \qhat + = \left[\begin{array}{c} + \hat{q}_{1}(t) \\ \vdots \\ \hat{q}_{r}(t) + \end{array}\right] \in \RR^{r}. +$$ + +We call $\Vr \in \RR^{n \times r}$ the _basis matrix_ and typically require that it have orthonormal columns. +The basis matrix is the link between the high-dimensional state space of the full-order model {eq}`eq:opinf-example-fom` and the low-dimensional state space of the ROM {eq}`eq:opinf-example-rom`. +See {mod}`opinf.basis` for tools to compute the basis $\Vr\in\RR^{n \times r}$ and select an appropriate dimension $r$. + +(subsec-opinf-regression)= +## Set up and Solve a Low-dimensional Regression + +OpInf determines the operators $\chat$, $\Ahat$, $\Hhat$, and/or $\Bhat$ by solving the following data-driven regression: + +$$ +\begin{aligned} + \min_{\chat,\Ahat,\Hhat,\Bhat}\sum_{j=0}^{k-1}\left\| + \chat + + \Ahat\qhat_{j} + + \Hhat[\qhat_{j} \otimes \qhat_{j}] + + \Bhat\u_{j} + - \dot{\qhat}_{j} + \right\|_{2}^{2} + + \mathcal{R}(\chat,\Ahat,\Hhat,\Bhat), +\end{aligned} +$$ (eq:opinf-lstsq-residual) + +where ++ $\qhat_{j} = \Vr\trp\q(t_{j})$ is the state at time $t_{j}$ represented in the coordinates of the basis, ++ $\dot{\qhat}_{j} = \ddt\Vr\trp\q(t)\big|_{t=t_{j}}$ is the time derivative of the state at time $t_{j}$ in the coordinates of the basis, ++ $\u_{j} = \u(t_j)$ is the input at time $t_{j}$, and ++ $\mathcal{R}$ is a _regularization term_ that penalizes the entries of the learned operators. + +The least-squares minimization {eq}`eq:opinf-lstsq-residual` can be written in the more standard form + +$$ +\min_{\Ohat}\left\| + \D\Ohat\trp - \mathbf{Z}\trp +\right\|_{F}^{2} + \mathcal{R}(\Ohat), +$$ + +where + +::::{margin} +:::{note} +The $\odot$ operator is called the [Khatri-Rao product](https://en.wikipedia.org/wiki/Khatri%E2%80%93Rao_product#Column-wise_Kronecker_product) and indicates taking the Kronecker product column by column. + +::: +:::: + +$$ +\begin{aligned} + \Ohat + &= \left[~\chat~~\Ahat~~\Hhat~~\Bhat~\right]\in\RR^{r\times d(r,m)}, + &\text{(unknown operators)} + \\ + \D + &= \left[~\mathbf{1}_{k}~~\Qhat\trp~~(\Qhat\odot\Qhat)\trp~~\U\trp~\right]\in\RR^{k\times d(r,m)}, + &\text{(known data)} + \\ + \Qhat + &= \left[~\qhat_0~~\qhat_1~~\cdots~~\qhat_{k-1}~\right]\in\RR^{r\times k} + &\text{(snapshots)} + \\ + \mathbf{Z} + &= \left[~\dot{\qhat}_0~~\dot{\qhat}_1~~\cdots~~\dot{\qhat}_{k-1}~\right]\in\RR^{r\times k}, + &\text{(time derivatives)} + \\ + \U + &= \left[~\u_0~\u_1~\cdots~\u_{k-1}~\right]\in\RR^{m\times k}, + &\text{(inputs)} +\end{aligned} +$$ + +in which $d(r,m) = 1 + r + r(r+1)/2 + m$ and $\mathbf{1}_{k}\in\RR^{k}$ is a vector of ones. + +:::{dropdown} Derivation +The Frobenius norm of a matrix is the square root of the sum of the squared entries. +If $\mathbf{Z}$ has entries $z_{ij}$, then + +$$ +\begin{aligned} + \|\mathbf{Z}\|_{F} + = \sqrt{\text{trace}(\mathbf{Z}\trp\mathbf{Z})} + = \sqrt{\sum_{i,j}z_{ij}^{2}}. +\end{aligned} +$$ + +Writing $\mathbf{Z}$ in terms of its columns, + +$$ +\begin{aligned} + \mathbf{Z} + = \left[\begin{array}{c|c|c|c} + &&& \\ + \mathbf{z}_0 & \mathbf{z}_1 & \cdots & \mathbf{z}_{k-1} \\ + &&& + \end{array}\right], +\end{aligned} +$$ + +we have + +$$ +\begin{aligned} + \|\mathbf{Z}\|_F^2 = \sum_{j=0}^{k-1}\|\mathbf{z}_j\|_2^2. +\end{aligned} +$$ + +Furthermore, $\|\mathbf{Z}\|_{F} = \|\mathbf{Z}\trp\|_{F}$. +Using these two properties, we can rewrite the least-squares residual as follows: + +$$ +\begin{aligned} + \sum_{j=0}^{k-1}\left\| + \chat + + \Ahat\qhat_{j} + + \Hhat[\qhat_{j} \otimes \qhat_{j}] + + \Bhat\u_{j} + - \dot{\qhat}_{j} + \right\|_{2}^{2} + &= \left\| + \chat\mathbf{1}\trp + + \Ahat\Qhat + + \Hhat[\Qhat \odot \Qhat] + + \Bhat\U + - \dot{\Qhat} + \right\|_{F}^{2} + \\ + &= \left\| + \left[\begin{array}{cccc} + \chat & \Ahat & \Hhat & \Bhat + \end{array}\right] + \left[\begin{array}{c} + \mathbf{1}\trp + \\ \Qhat + \\ \Qhat \odot \Qhat + \\ \U + \end{array}\right] + - \dot{\Qhat} + \right\|_{F}^{2} + \\ + &= \left\| + \left[\begin{array}{c} + \mathbf{1}\trp + \\ \Qhat + \\ \Qhat \odot \Qhat + \\ \U + \end{array}\right]\trp + \left[\begin{array}{cccc} + \chat & \Ahat & \Hhat & \Bhat + \end{array}\right]\trp + - \dot{\Qhat}\trp + \right\|_{F}^{2} + \\ + &= \left\| + \left[\begin{array}{cccc} + \mathbf{1} + & \Qhat\trp + & [\Qhat \odot \Qhat]\trp + & \U\trp + \end{array}\right] + \left[\begin{array}{c} + \chat\trp + \\ \Ahat\trp + \\ \Hhat\trp + \\ \Bhat\trp + \end{array}\right] + - \dot{\Qhat}\trp + \right\|_{F}^{2}, +\end{aligned} +$$ + +which is the standard form given above. +::: + +:::{important} +For the most common choices of $\mathcal{R}$, the OpInf learning problem {eq}`eq:opinf-lstsq-residual` has a unique solution if and only if the data matrix $\D$ has full column rank. +A necessary condition for this to happen is $k \ge d(r,m)$, that is, the number of training snapshots $k$ should exceed the number of reduced-order operator entries to be learned for each system mode. +If you are experiencing poor performance with OpInf ROMs, try decreasing $r$, increasing $k$, or adding a regularization term to improve the conditioning of the learning problem. +::: + +:::{note} +Let $\ohat_{1},\ldots,\ohat_{r}\in\RR^{d(r,m)}$ be the rows of $\Ohat$. +If the regularization can be written as + +$$ +\begin{align*} + \mathcal{R}(\Ohat) + = \sum_{i=1}^{r}\mathcal{R}_{i}(\ohat_{i}), +\end{align*} +$$ + +then the OpInf regression decouples along the rows of $\Ohat$ into $r$ independent least-squares problems: + +$$ +\begin{align*} + \min_{\Ohat}\left\{\left\| + \D\Ohat\trp - \mathbf{Y}\trp + \right\|_{F}^{2} + \mathcal{R}(\Ohat)\right\} + = + \sum_{i=1}^{r}\min_{\ohat_{i}}\left\{\left\| + \D\ohat_{i} - \mathbf{y}_{i} + \right\|_{2}^{2} + \mathcal{R}_{i}(\ohat_{i})\right\}, +\end{align*} +$$ + +where $\mathbf{y}_{i},\ldots,\mathbf{y}_{r}$ are the rows of $\mathbf{Y}$. +::: + +(subsec-rom-evaluation)= +## Solve the Reduced-order Model + + +Once the reduced-order operators have been determined, the corresponding ROM {eq}`eq:opinf-example-rom` can be solved rapidly to make predictions. +The computational cost of solving the ROM scales with $r$, the number of degrees of freedom in the low-dimensional representation of the state. + +For example, we may use the ROM to obtain approximate solutions of the FOM {eq}`eq:opinf-example-fom` with +- new initial conditions $\q_{0}$, +- a different input function $\u(t)$, +- a longer time horizon than the training data, +- different system parameters. + +:::{important} +The accuracy of any data-driven model depends on how well the training data represents the full-order system. +We should not expect a ROM to perform well under conditions that are wildly different than the training data. +The [**Getting Started**](../tutorials/basics.md) tutorial demonstrates this concept in the context of prediction for new initial conditions. +::: + +## Brief Example + +Suppose we have the following variables. + +| Variable | Symbol | Description | +| :------- | :--------------------- | :---------------------------------------- | +| `Q` | $\Q\in\RR^{n\times k}$ | State snapshot matrix | +| `t` | $t$ | Time domain for snapshot data | +| `u` | $\u:\RR\to\RR^{m}$ | Input function | + +That is, `Q[:, j]` is the state corresponding to time `t[j]` and `u` is a function handle describing the input. +Then the following code learns a ROM of the form + +$$ +\begin{aligned} + \ddt\qhat(t) + = \Ahat\qhat(t) + \Hhat(\qhat(t)\otimes\qhat(t)) + \Bhat\u(t) +\end{aligned} +$$ + +from the training data, uses the ROM to reconstruct the training data, and computes the error of the reconstruction. + +```python +import opinf + +# Set up and calibrate a ROM with a rank-10 basis and quadratic model structure. +rom = opinf.ROM( + basis=opinf.basis.PODBasis(num_vectors=10), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t), + model=opinf.models.ContinuousModel( + operators="AHB", + solver=opinf.lstsq.L2Solver(regularizer=1e-6), + ) +).fit(states=Q, inputs=u(t)) + +# Solve the ROM over a specified time domain. +Q_rom = rom.predict(Q[:, 0], t, input_func=u) + +# Compute the error of the ROM prediction. +absolute_error, relative_error = opinf.post.Lp_error(Q, Q_rom, t) +``` + +The [API pages](../api/main.md) for each submodule provide details on the arguments to the `ROM` class. +See [**Getting Started**](../tutorials/basics.md) for an introductory tutorial. \ No newline at end of file diff --git a/sphinx-docs/source/opinf/literature.md b/sphinx-docs/source/opinf/literature.md new file mode 100644 index 00000000..8d5acb3d --- /dev/null +++ b/sphinx-docs/source/opinf/literature.md @@ -0,0 +1,2092 @@ +# Literature + +This page lists scholarly publications that develop, extend, or apply +Operator Inference, categorized into topics and sorted by publication year, +then by the last name of the first author. Although some could be placed in +multiple categories, each publication is only listed once. + +:::{admonition} Share Your Work! +:class: hint + +Don't see your publication? +[**Click here**](https://forms.gle/BgZK4b4DfuaPsGFd7) +to submit a request to add entries to this page. +::: + +## Original Paper + +* [**Data-driven operator inference for nonintrusive projection-based model reduction**](https://doi.org/10.1016/j.cma.2016.03.025) + [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2016
BibTeX
@article{peherstorfer2016opinf,
+  title = {Data-driven operator inference for nonintrusive projection-based model reduction},
+  author = {Benjamin Peherstorfer and Karen Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {306},
+  pages = {196--215},
+  year = {2016},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2016.03.025},
+  }
+ +## Surveys + +* [**Learning physics-based models from data: Perspectives from inverse problems and model reduction**](https://doi.org/10.1017/S0962492921000064) + [O. Ghattas](https://scholar.google.com/citations?user=A5vhsIYAAAAJ) and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Acta Numerica, 2021
BibTeX
@article{ghattas2021acta,
+  title = {Learning physics-based models from data: {P}erspectives from inverse problems and model reduction},
+  author = {Omar Ghattas and Karen Willcox},
+  journal = {Acta Numerica},
+  volume = {30},
+  pages = {445--554},
+  year = {2021},
+  publisher = {Cambridge University Press},
+  doi = {10.1017/S0962492921000064},
+  }
+

+* [**Learning nonlinear reduced models from data with operator inference**](https://doi.org/10.1146/annurev-fluid-121021-025220) + [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Annual Review of Fluid Mechanics, 2024
BibTeX
@article{kramer2024survey,
+  title = {Learning nonlinear reduced models from data with operator inference},
+  author = {Boris Kramer and Benjamin Peherstorfer and Karen Willcox},
+  journal = {Annual Review of Fluid Mechanics},
+  volume = {56},
+  pages = {521--548},
+  year = {2024},
+  publisher = {Annual Reviews},
+  doi = {10.1146/annurev-fluid-121021-025220},
+  }
+ +## Methodology +### Lifting and Nonlinearity + +Operator Inference learns reduced-order models with +polynomial structure. The methods developed in the following papers focus on +dealing with non-polynomial nonlinearities through variable transformations +(lifting) and/or coupling Operator Inference methods with other approximation +strategies. + +* [**Transform \& Learn: A data-driven approach to nonlinear model reduction**](https://doi.org/10.2514/6.2019-3707) + [E. Qian](https://scholar.google.com/citations?user=jnHI7wQAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), A. N. Marques, and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + AIAA Aviation 2019 Forum, 2019
BibTeX
@inproceedings{qian2019transform,
+  title = {Transform \\& {L}earn: {A} data-driven approach to nonlinear model reduction},
+  author = {Elizabeth Qian and Boris Kramer and Alexandre N. Marques and Karen E. Willcox},
+  booktitle = {AIAA Aviation 2019 Forum},
+  pages = {3707},
+  year = {2019},
+  doi = {10.2514/6.2019-3707},
+  }
+

+* [**Operator inference for non-intrusive model reduction of systems with non-polynomial nonlinear terms**](https://doi.org/10.1016/j.cma.2020.113433) + [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ), [P. Goyal](https://scholar.google.com/citations?user=9rEfaRwAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2020
BibTeX
@article{benner2020deim,
+  title = {Operator inference for non-intrusive model reduction of systems with non-polynomial nonlinear terms},
+  author = {Peter Benner and Pawan Goyal and Boris Kramer and Benjamin Peherstorfer and Karen Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {372},
+  pages = {113433},
+  year = {2020},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2020.113433},
+  }
+

+* [**Lift \& Learn: Physics-informed machine learning for large-scale nonlinear dynamical systems**](https://doi.org/10.1016/j.physd.2020.132401) + [E. Qian](https://scholar.google.com/citations?user=jnHI7wQAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Physica D: Nonlinear Phenomena, 2020
BibTeX
@article{qian2020liftandlearn,
+  title = {Lift \\& {L}earn: {P}hysics-informed machine learning for large-scale nonlinear dynamical systems},
+  author = {Elizabeth Qian and Boris Kramer and Benjamin Peherstorfer and Karen Willcox},
+  journal = {Physica D: Nonlinear Phenomena},
+  volume = {406},
+  pages = {132401},
+  year = {2020},
+  publisher = {Elsevier},
+  doi = {10.1016/j.physd.2020.132401},
+  }
+

+* [**Stability domains for quadratic-bilinear reduced-order models**](https://doi.org/10.1137/20M1364849) + [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + SIAM Journal on Applied Dynamical Systems, 2021
BibTeX
@article{kramer2021quadstability,
+  title = {Stability domains for quadratic-bilinear reduced-order models},
+  author = {Boris Kramer},
+  journal = {SIAM Journal on Applied Dynamical Systems},
+  volume = {20},
+  issue = {2},
+  pages = {981--996},
+  year = {2021},
+  publisher = {SIAM},
+  doi = {10.1137/20M1364849},
+  }
+

+* [**Non-intrusive data-driven model reduction for differential algebraic equations derived from lifting transformations**](https://doi.org/10.1016/j.cma.2021.114296) + [P. Khodabakhshi](https://scholar.google.com/citations?user=lYr_g-MAAAAJ) and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2022
BibTeX
@article{khodabakhshi2022diffalg,
+  title = {Non-intrusive data-driven model reduction for differential algebraic equations derived from lifting transformations},
+  author = {Parisa Khodabakhshi and Karen E. Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {389},
+  pages = {114296},
+  year = {2022},
+  doi = {10.1016/j.cma.2021.114296},
+  }
+

+* [**Reduced operator inference for nonlinear partial differential equations**](https://doi.org/10.1137/21M1393972) + [E. Qian](https://scholar.google.com/citations?user=jnHI7wQAAAAJ), [I. Farcas](https://scholar.google.com/citations?user=Cts5ePIAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + SIAM Journal on Scientific Computing, 2022
BibTeX
@article{qian2022pdes,
+  title = {Reduced operator inference for nonlinear partial differential equations},
+  author = {Elizabeth Qian and Ionut-Gabriel Farcas and Karen Willcox},
+  journal = {SIAM Journal on Scientific Computing},
+  volume = {44},
+  issue = {4},
+  pages = {A1934-a1959},
+  year = {2022},
+  publisher = {SIAM},
+  doi = {10.1137/21M1393972},
+  }
+

+* [**Exact and optimal quadratization of nonlinear finite-dimensional non-autonomous dynamical systems**](https://doi.org/10.1137/23M1561129) + A. Bychkov, [O. Issan](https://scholar.google.com/citations?user=eEIe19oAAAAJ), [G. Pogudin](https://scholar.google.com/citations?user=C5NP1o0AAAAJ), and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + SIAM Journal of Applied Dynamical Systems, 2024
BibTeX
@article{bychkov2024quadratization,
+  title = {Exact and optimal quadratization of nonlinear finite-dimensional non-autonomous dynamical systems},
+  author = {Andrey Bychkov and Opal Issan and Gleb Pogudin and Boris Kramer},
+  journal = {SIAM Journal of Applied Dynamical Systems},
+  volume = {23},
+  number = {1},
+  pages = {982-1016},
+  year = {2024},
+  doi = {10.1137/23M1561129},
+  }
+ +### Re-projection + +In some cases, if the training data are chosen +judiciously, Operator Inference can recover traditional reduced-order models +defined by intrusive projection. The following papers develop and apply this +idea. + +* [**Sampling low-dimensional Markovian dynamics for preasymptotically recovering reduced models from data with operator inference**](https://doi.org/10.1137/19M1292448) + [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + SIAM Journal on Scientific Computing, 2020
BibTeX
@article{peherstorfer2020reprojection,
+  title = {Sampling low-dimensional {M}arkovian dynamics for preasymptotically recovering reduced models from data with operator inference},
+  author = {Benjamin Peherstorfer},
+  journal = {SIAM Journal on Scientific Computing},
+  volume = {42},
+  issue = {5},
+  pages = {A3489-a3515},
+  year = {2020},
+  publisher = {SIAM},
+  doi = {10.1137/19M1292448},
+  }
+

+* [**Probabilistic error estimation for non-intrusive reduced models learned from data of systems governed by linear parabolic partial differential equations**](https://doi.org/10.1051/m2an/2021010) + [W. I. T. Uy](https://scholar.google.com/citations?user=hNN_KRQAAAAJ) and [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + ESAIM: Mathematical Modelling and Numerical Analysis, 2021
BibTeX
@article{uy2021error,
+  title = {Probabilistic error estimation for non-intrusive reduced models learned from data of systems governed by linear parabolic partial differential equations},
+  author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer},
+  journal = {ESAIM: Mathematical Modelling and Numerical Analysis},
+  volume = {55},
+  issue = {3},
+  pages = {735--761},
+  year = {2021},
+  publisher = {EDP Sciences},
+  doi = {10.1051/m2an/2021010},
+  }
+

+* [**Enforcing structure in data-driven reduced modeling through nested Operator Inference**](https://doi.org/10.1109/CDC56724.2024.10885857) + [N. Aretz](https://scholar.google.com/citations?user=Oje7mbAAAAAJ) and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + 63rd IEEE Conference on Decision and Control (CDC), 2024
BibTeX
@inproceedings{aretz2024enforcing,
+  title = {Enforcing structure in data-driven reduced modeling through nested {O}perator {I}nference},
+  author = {Nicole Aretz and Karen Willcox},
+  booktitle = {63rd IEEE Conference on Decision and Control (CDC)},
+  year = {2024},
+  organization = {IEEE},
+  doi = {10.1109/CDC56724.2024.10885857},
+  }
+

+* [**Exact operator inference with minimal data**](https://doi.org/10.48550/arXiv.2506.01244) + H. Rosenberger, B. Sanderse, and G. Stabile + arXiv, 2025
BibTeX
@article{rosenberger2025exactopinf,
+  title = {Exact operator inference with minimal data},
+  author = {Henrik Rosenberger and Benjamin Sanderse and Giovanni Stabile},
+  journal = {arXiv},
+  volume = {2506.01244},
+  year = {2025},
+  doi = {10.48550/arXiv.2506.01244},
+  }
+ +### Structure Preservation + +The methods developed in these works augment Operator +Inference so that the resulting reduced-order models automatically inherit +certain properties from the full-order system, such as block structure, +symmetries, energy conservation, gradient structure, and more. + +* [**Operator inference and physics-informed learning of low-dimensional models for incompressible flows**](https://doi.org/10.1553/etna_vol56s28) + [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ), [P. Goyal](https://scholar.google.com/citations?user=9rEfaRwAAAAJ), [J. Heiland](https://scholar.google.com/citations?user=wkHSeoYAAAAJ), and [I. P. Duff](https://scholar.google.com/citations?user=OAkPFdkAAAAJ) + Electronic Transactions on Numerical Analysis, 2022
BibTeX
@article{benner2022incompressible,
+  title = {Operator inference and physics-informed learning of low-dimensional models for incompressible flows},
+  author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff},
+  journal = {Electronic Transactions on Numerical Analysis},
+  volume = {56},
+  year = {2022},
+  doi = {10.1553/etna_vol56s28},
+  }
+

+* [**Hamiltonian operator inference: Physics-preserving learning of reduced-order models for canonical Hamiltonian systems**](https://doi.org/10.1016/j.physd.2021.133122) + [H. Sharma](https://scholar.google.com/citations?user=Pb-tL5oAAAAJ), [Z. Wang](https://scholar.google.com/citations?user=jkmwEF0AAAAJ), and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + Physica D: Nonlinear Phenomena, 2022
BibTeX
@article{sharma2022hamiltonian,
+  title = {Hamiltonian operator inference: {P}hysics-preserving learning of reduced-order models for canonical {H}amiltonian systems},
+  author = {Harsh Sharma and Zhu Wang and Boris Kramer},
+  journal = {Physica D: Nonlinear Phenomena},
+  volume = {431},
+  pages = {133122},
+  year = {2022},
+  publisher = {Elsevier},
+  doi = {10.1016/j.physd.2021.133122},
+  }
+

+* [**An operator inference oriented approach for linear mechanical systems**](https://doi.org/10.1016/j.ymssp.2023.110620) + Y. Filanova, [I. P. Duff](https://scholar.google.com/citations?user=OAkPFdkAAAAJ), [P. Goyal](https://scholar.google.com/citations?user=9rEfaRwAAAAJ), and [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ) + Mechanical Systems and Signal Processing, 2023
BibTeX
@article{filanova2023mechanical,
+  title = {An operator inference oriented approach for linear mechanical systems},
+  author = {Yevgeniya Filanova and Igor Pontes Duff and Pawan Goyal and Peter Benner},
+  journal = {Mechanical Systems and Signal Processing},
+  volume = {200},
+  pages = {110620},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.ymssp.2023.110620},
+  }
+

+* [**Canonical and noncanonical Hamiltonian operator inference**](https://doi.org/10.1016/j.cma.2023.116334) + [A. Gruber](https://scholar.google.com/citations?user=CJVuqfoAAAAJ) and [I. Tezaur](https://scholar.google.com/citations?user=Q3fx78kAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2023
BibTeX
@article{gruber2023hamiltonian,
+  title = {Canonical and noncanonical {H}amiltonian operator inference},
+  author = {Anthony Gruber and Irina Tezaur},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {416},
+  year = {2023},
+  doi = {10.1016/j.cma.2023.116334},
+  }
+

+* [**Predicting solar wind streams from the inner-heliosphere to Earth via shifted operator inference**](https://doi.org/10.1016/j.jcp.2022.111689) + [O. Issan](https://scholar.google.com/citations?user=eEIe19oAAAAJ) and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + Journal of Computational Physics, 2023
BibTeX
@article{issan2023shifted,
+  title = {Predicting solar wind streams from the inner-heliosphere to Earth via shifted operator inference},
+  author = {Opal Issan and Boris Kramer},
+  journal = {Journal of Computational Physics},
+  volume = {473},
+  pages = {111689},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.jcp.2022.111689},
+  }
+

+* [**Physics-informed regularization and structure preservation for learning stable reduced models from data with operator inference**](https://doi.org/10.1016/j.cma.2022.115836) + N. Sawant, [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), and [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2023
BibTeX
@article{sawant2023pireg,
+  title = {Physics-informed regularization and structure preservation for learning stable reduced models from data with operator inference},
+  author = {Nihar Sawant and Boris Kramer and Benjamin Peherstorfer},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {404},
+  pages = {115836},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2022.115836},
+  }
+

+* [**Stable sparse operator inference for nonlinear structural dynamics**](https://doi.org/10.48550/arXiv.2407.21672) + [P. den Boef](https://scholar.google.com/citations?user=vFlzL7kAAAAJ), [D. Manvelyan](https://scholar.google.com/citations?user=V0k8Xb4AAAAJ), [J. Maubach](https://scholar.google.com/citations?user=nBRKw6cAAAAJ), [W. Schilders](https://scholar.google.com/citations?user=UGKPyqkAAAAJ), and [N. van de Wouw](https://scholar.google.com/citations?user=pcQCbN8AAAAJ) + arXiv, 2024
BibTeX
@article{boef2024stablesparse,
+  title = {Stable sparse operator inference for nonlinear structural dynamics},
+  author = {Pascal {den Boef} and Diana Manvelyan and Joseph Maubach and Wil Schilders and Nathan {van de Wouw}},
+  journal = {arXiv},
+  volume = {2407.21672},
+  year = {2024},
+  doi = {10.48550/arXiv.2407.21672},
+  }
+

+* [**Gradient preserving Operator Inference: Data-driven reduced-order models for equations with gradient structure**](https://doi.org/10.1016/j.cma.2024.117033) + [Y. Geng](https://scholar.google.com/citations?user=lms4MbwAAAAJ), [J. Singh](https://scholar.google.com/citations?user=VcmXMxgAAAAJ), [L. Ju](https://scholar.google.com/citations?user=JkKUWoAAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), and [Z. Wang](https://scholar.google.com/citations?user=jkmwEF0AAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2024
BibTeX
@article{geng2024gradient,
+  title = {Gradient preserving {O}perator {I}nference: {D}ata-driven reduced-order models for equations with gradient structure},
+  author = {Yuwei Geng and Jasdeep Singh and Lili Ju and Boris Kramer and Zhu Wang},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {427},
+  pages = {117033},
+  year = {2024},
+  doi = {10.1016/j.cma.2024.117033},
+  }
+

+* [**Energy-preserving reduced operator inference for efficient design and control**](https://doi.org/10.2514/6.2024-1012) + [T. Koike](https://scholar.google.com/citations?user=HFoIGcMAAAAJ) and [E. Qian](https://scholar.google.com/citations?user=jnHI7wQAAAAJ) + AIAA SciTech 2024 Forum, 2024
BibTeX
@inproceedings{koike2024energy,
+  title = {Energy-preserving reduced operator inference for efficient design and control},
+  author = {Tomoki Koike and Elizabeth Qian},
+  booktitle = {AIAA SciTech 2024 Forum},
+  pages = {1012},
+  year = {2024},
+  doi = {10.2514/6.2024-1012},
+  }
+

+* [**Lagrangian operator inference enhanced with structure-preserving machine learning for nonintrusive model reduction of mechanical systems**](https://doi.org/10.1016/j.cma.2024.116865) + [H. Sharma](https://scholar.google.com/citations?user=Pb-tL5oAAAAJ), [D. A. Najera-Flores](https://scholar.google.com/citations?user=HJ-Dfl8AAAAJ), [M. D. Todd](https://scholar.google.com/citations?user=jzY8TSkAAAAJ), and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2024
BibTeX
@article{sharma2024lagrangian,
+  title = {Lagrangian operator inference enhanced with structure-preserving machine learning for nonintrusive model reduction of mechanical systems},
+  author = {Harsh Sharma and David A Najera-Flores and Michael D Todd and Boris Kramer},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {423},
+  pages = {116865},
+  year = {2024},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2024.116865},
+  }
+

+* [**Preserving Lagrangian structure in data-driven reduced-order modeling of large-scale mechanical systems**](https://doi.org/10.1016/j.physd.2024.134128) + [H. Sharma](https://scholar.google.com/citations?user=Pb-tL5oAAAAJ) and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + Physica D: Nonlinear Phenomena, 2024
BibTeX
@article{sharma2024preserving,
+  title = {Preserving {L}agrangian structure in data-driven reduced-order modeling of large-scale mechanical systems},
+  author = {Harsh Sharma and Boris Kramer},
+  journal = {Physica D: Nonlinear Phenomena},
+  volume = {462},
+  pages = {134128},
+  year = {2024},
+  doi = {10.1016/j.physd.2024.134128},
+  }
+

+* [**Nested operator inference for adaptive data-driven learning of reduced-order models**](https://doi.org/10.48550/arXiv.2508.11542) + [N. Aretz](https://scholar.google.com/citations?user=Oje7mbAAAAAJ) and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + arXiv, 2025
BibTeX
@article{aretz2025nestedopinf,
+  title = {Nested operator inference for adaptive data-driven learning of reduced-order models},
+  author = {Nicole Aretz and Karen Willcox},
+  journal = {arXiv},
+  volume = {2508.11542},
+  year = {2025},
+  doi = {10.48550/arXiv.2508.11542},
+  }
+

+* [**Data-driven reduced-order models for port-Hamiltonian systems with Operator Inference**](https://doi.org/10.48550/arXiv.2501.02183) + [Y. Geng](https://scholar.google.com/citations?user=lms4MbwAAAAJ), [L. Ju](https://scholar.google.com/citations?user=JkKUWoAAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), and [Z. Wang](https://scholar.google.com/citations?user=jkmwEF0AAAAJ) + arXiv, 2025
BibTeX
@article{geng2025porthamiltonian,
+  title = {Data-driven reduced-order models for port-{H}amiltonian systems with {O}perator {I}nference},
+  author = {Yuwei Geng and Lili Ju and Boris Kramer and Zhu Wang},
+  journal = {arXiv},
+  volume = {2501.02183},
+  year = {2025},
+  doi = {10.48550/arXiv.2501.02183},
+  }
+

+* [**Variationally consistent Hamiltonian model reduction**](https://doi.org/10.1137/24M1652490) + [A. Gruber](https://scholar.google.com/citations?user=CJVuqfoAAAAJ) and [I. Tezaur](https://scholar.google.com/citations?user=Q3fx78kAAAAJ) + SIAM Journal on Applied Dynamical Systems, 2025
BibTeX
@article{gruber2025variational,
+  author = {Anthony Gruber and Irina Tezaur},
+  title = {Variationally consistent {H}amiltonian model reduction},
+  journal = {SIAM Journal on Applied Dynamical Systems},
+  volume = {24},
+  number = {1},
+  pages = {376-414},
+  year = {2025},
+  doi = {10.1137/24M1652490},
+  }
+

+* [**Physically consistent predictive reduced-order modeling by enhancing Operator Inference with state constraints**](https://doi.org/10.48550/arXiv.2502.03672) + [H. Kim](https://scholar.google.com/citations?user=sdR-LZ4AAAAJ) and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + arXiv, 2025
BibTeX
@article{kim2025stateconstraints,
+  title = {Physically consistent predictive reduced-order modeling by enhancing {O}perator {I}nference with state constraints},
+  author = {Hyeonghun Kim and Boris Kramer},
+  journal = {arXiv},
+  volume = {2502.03672},
+  year = {2025},
+  doi = {10.48550/arXiv.2502.03672},
+  }
+ +### Parametric Problems + +Many systems depend on independent parameters that +describe material properties or other physical characteristics of the +phenomenon being modeled. The following papers develop Operator Inference +approaches that are specifically designed for parametric problems. + +* [**Learning reduced-order dynamics for parametrized shallow water equations from data**](https://doi.org/10.1002/fld.4998) + [S. Yıldız](https://scholar.google.com/citations?user=UVPD79MAAAAJ), [P. Goyal](https://scholar.google.com/citations?user=9rEfaRwAAAAJ), [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ), and [B. Karasözen](https://scholar.google.com/citations?user=R906kj0AAAAJ) + International Journal for Numerical Methods in Fluids, 2021
BibTeX
@article{yildiz2021shallow,
+  title = {Learning reduced-order dynamics for parametrized shallow water equations from data},
+  author = {S\\\\"{u}leyman Y\\i{}ld\\i{}z and Pawan Goyal and Peter Benner and B\\\\"{u}lent Karas\\\\"{o}zen},
+  journal = {International Journal for Numerical Methods in Fluids},
+  volume = {93},
+  issue = {8},
+  pages = {2803--2821},
+  year = {2021},
+  publisher = {Wiley Online Library},
+  doi = {10.1002/fld.4998},
+  }
+

+* [**Parametric non-intrusive reduced-order models via operator inference for large-scale rotating detonation engine simulations**](https://doi.org/10.2514/6.2023-0172) + [I. Farcas](https://scholar.google.com/citations?user=Cts5ePIAAAAJ), R. Gundevia, R. Munipalli, and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + AIAA SciTech 2023 Forum, 2023
BibTeX
@inproceedings{farcas2023parametric,
+  title = {Parametric non-intrusive reduced-order models via operator inference for large-scale rotating detonation engine simulations},
+  author = {Ionut-Gabriel Farcas and Rayomand Gundevia and Ramakanth Munipalli and Karen E. Willcox},
+  booktitle = {AIAA SciTech 2023 Forum},
+  pages = {0172},
+  year = {2023},
+  doi = {10.2514/6.2023-0172},
+  }
+

+* [**Nonintrusive reduced-order models for parametric partial differential equations via data-driven operator inference**](https://doi.org/10.1137/21M1452810) + [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), [P. Khodabakhshi](https://scholar.google.com/citations?user=lYr_g-MAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + SIAM Journal on Scientific Computing, 2023
BibTeX
@article{mcquarrie2023parametric,
+  title = {Nonintrusive reduced-order models for parametric partial differential equations via data-driven operator inference},
+  author = {Shane A McQuarrie and Parisa Khodabakhshi and Karen Willcox},
+  journal = {SIAM Journal on Scientific Computing},
+  volume = {45},
+  issue = {4},
+  pages = {A1917-A1946},
+  year = {2023},
+  publisher = {SIAM},
+  doi = {10.1137/21M1452810},
+  }
+

+* [**Tensor parametric Hamiltonian operator inference**](https://doi.org/10.48550/arXiv.2502.10888) + [A. Vijaywargiya](https://scholar.google.com/citations?user=_fcSwDYAAAAJ), [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), and [A. Gruber](https://scholar.google.com/citations?user=CJVuqfoAAAAJ) + arXiv, 2025
BibTeX
@article{vijaywargiya2025tensoropinf,
+  title = {Tensor parametric {H}amiltonian operator inference},
+  author = {Arjun Vijaywargiya and Shane A. McQuarrie and Anthony Gruber},
+  journal = {arXiv},
+  volume = {2502.10888},
+  year = {2025},
+  doi = {10.48550/arXiv.2502.10888},
+  }
+ +### Statistical Methods + +These papers focus on problems with noisy or missing +data, stochastic systems, and methods for constructing probabilistic +reduced-order models with Operator Inference. + +* [**Operator inference of non-Markovian terms for learning reduced models from partially observed state trajectories**](https://doi.org/10.1007/s10915-021-01580-2) + [W. I. T. Uy](https://scholar.google.com/citations?user=hNN_KRQAAAAJ) and [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + Journal of Scientific Computing, 2021
BibTeX
@article{uy2021partial,
+  title = {Operator inference of non-{M}arkovian terms for learning reduced models from partially observed state trajectories},
+  author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer},
+  journal = {Journal of Scientific Computing},
+  volume = {88},
+  issue = {3},
+  pages = {1--31},
+  year = {2021},
+  publisher = {Springer},
+  doi = {10.1007/s10915-021-01580-2},
+  }
+

+* [**Bayesian operator inference for data-driven reduced-order modeling**](https://doi.org/10.1016/j.cma.2022.115336) + [M. Guo](https://scholar.google.com/citations?user=eON6MykAAAAJ), [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2022
BibTeX
@article{guo2022bayesopinf,
+  title = {Bayesian operator inference for data-driven reduced-order modeling},
+  author = {Mengwu Guo and Shane A McQuarrie and Karen Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {402},
+  pages = {115336},
+  year = {2022},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2022.115336},
+  }
+

+* [**Active operator inference for learning low-dimensional dynamical-system models from noisy data**](https://doi.org/10.1137/21M1439729) + [W. I. T. Uy](https://scholar.google.com/citations?user=hNN_KRQAAAAJ), Y. Wang, [Y. Wen](https://scholar.google.com/citations?user=uXJoQCAAAAAJ), and [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + SIAM Journal on Scientific Computing, 2023
BibTeX
@article{uy2023active,
+  title = {Active operator inference for learning low-dimensional dynamical-system models from noisy data},
+  author = {Wayne Isaac Tan Uy and Yuepeng Wang and Yuxiao Wen and Benjamin Peherstorfer},
+  journal = {SIAM Journal on Scientific Computing},
+  volume = {45},
+  issue = {4},
+  pages = {A1462-a1490},
+  year = {2023},
+  publisher = {SIAM},
+  doi = {10.1137/21M1439729},
+  }
+

+* [**Operator inference with roll outs for learning reduced models from scarce and low-quality data**](https://doi.org/10.1016/j.camwa.2023.06.012) + [W. I. T. Uy](https://scholar.google.com/citations?user=hNN_KRQAAAAJ), [D. Hartmann](https://scholar.google.com/citations?user=4XvBneEAAAAJ), and [B. Peherstorfer](https://scholar.google.com/citations?user=C81WhlkAAAAJ) + Computers \& Mathematics with Applications, 2023
BibTeX
@article{uy2023rollouts,
+  title = {Operator inference with roll outs for learning reduced models from scarce and low-quality data},
+  author = {Wayne Isaac Tan Uy and Dirk Hartmann and Benjamin Peherstorfer},
+  journal = {Computers \\& Mathematics with Applications},
+  volume = {145},
+  pages = {224--239},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.camwa.2023.06.012},
+  }
+

+* [**Learning stochastic reduced models from data: a nonintrusive approach**](https://doi.org/10.48550/arXiv.2407.05724) + [M. A. Freitag](https://scholar.google.com/citations?user=iE4t4WcAAAAJ), [J. M. Nicolaus](https://scholar.google.com/citations?user=47DRMUwAAAAJ), and M. Redmann + arXiv, 2024
BibTeX
@misc{freitag2024stochastic,
+  title = {Learning stochastic reduced models from data: a nonintrusive approach},
+  author = {Melina A. Freitag and Jan Martin Nicolaus and Martin Redmann},
+  journal = {arXiv},
+  volume = {2407.05724},
+  year = {2024},
+  doi = {10.48550/arXiv.2407.05724},
+  }
+

+* [**Bayesian learning with Gaussian processes for low-dimensional representations of time-dependent nonlinear systems**](https://doi.org/10.1016/j.physd.2025.134572) + [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), [A. Chaudhuri](https://scholar.google.com/citations?user=oGL9YJIAAAAJ), [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ), and [M. Guo](https://scholar.google.com/citations?user=eON6MykAAAAJ) + Physica D: Nonlinear Phenomena, 2025
BibTeX
@article{mcquarrie2025gpbayesopinf,
+  title = {Bayesian learning with {G}aussian processes for low-dimensional representations of time-dependent nonlinear systems},
+  author = {Shane A. McQuarrie and Anirban Chaudhuri and Karen E. Willcox and Mengwu Guo},
+  journal = {Physica D: Nonlinear Phenomena},
+  volume = {475},
+  pages = {134572},
+  year = {2025},
+  doi = {10.1016/j.physd.2025.134572},
+  }
+ +### Domain Decomposition + +The methods in the following papers focus on scalability +and accuracy improvements by decomposition spatial or latent space domains and +learning a coupled system of reduced-order models. + +* [**Localized non-intrusive reduced-order modelling in the operator inference framework**](https://doi.org/10.1098/rsta.2021.0206) + [R. Geelen](https://scholar.google.com/citations?user=vBzKRMsAAAAJ) and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Philosophical Transactions of the Royal Society A, 2022
BibTeX
@article{geelen2022localized,
+  title = {Localized non-intrusive reduced-order modelling in the operator inference framework},
+  author = {Rudy Geelen and Karen Willcox},
+  journal = {Philosophical Transactions of the Royal Society A},
+  volume = {380},
+  number = {2229},
+  pages = {20210206},
+  year = {2022},
+  doi = {10.1098/rsta.2021.0206},
+  }
+

+* [**Domain decomposition for data-driven reduced modeling of large-scale systems**](https://doi.org/10.2514/1.J063715) + [I. Farcas](https://scholar.google.com/citations?user=Cts5ePIAAAAJ), R. P. Gundevia, R. Munipalli, and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + AIAA Journal, 2024
BibTeX
@article{farcas2024domaindecomposition,
+  title = {Domain decomposition for data-driven reduced modeling of large-scale systems},
+  author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox},
+  journal = {AIAA Journal},
+  volume = {62},
+  number = {11},
+  pages = {4071-4086},
+  year = {2024},
+  doi = {10.2514/1.J063715},
+  }
+

+* [**Non-intrusive reduced-order modeling for dynamical systems with spatially localized features**](https://doi.org/10.1016/j.cma.2025.118115) + [L. Gkimisis](https://scholar.google.com/citations?user=0GzUUzMAAAAJ), [N. Aretz](https://scholar.google.com/citations?user=Oje7mbAAAAAJ), [M. Tezzele](https://scholar.google.com/citations?user=UPcyNXIAAAAJ), [T. Richter](https://scholar.google.com/citations?user=C8R6xtMAAAAJ), [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ), and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2025
BibTeX
@article{gkimisis2025spatiallylocal,
+  title = {Non-intrusive reduced-order modeling for dynamical systems with spatially localized features},
+  author = {Leonidas Gkimisis and Nicole Aretz and Marco Tezzele and Thomas Richter and Peter Benner and Karen E. Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {444},
+  pages = {118115},
+  year = {2025},
+  doi = {10.1016/j.cma.2025.118115},
+  }
+ +### Nonlinear Manifolds + +Traditional model reduction methods approximate the +high-dimensional system state with a low-dimensional linear (or affine) +representation. The methods in these papers explore using nonlinear +low-dimensional representations in the context of Operator Inference. + +* [**A quadratic decoder approach to nonintrusive reduced-order modeling of nonlinear dynamical systems**](https://doi.org/10.1002/pamm.202200049) + [P. Benner](https://scholar.google.com/citations?user=6zcRrC4AAAAJ), [P. Goyal](https://scholar.google.com/citations?user=9rEfaRwAAAAJ), [J. Heiland](https://scholar.google.com/citations?user=wkHSeoYAAAAJ), and [I. P. Duff](https://scholar.google.com/citations?user=OAkPFdkAAAAJ) + Proceedings in Applied Mathematics and Mechanics, 2023
BibTeX
@article{benner2023quaddecoder,
+  title = {A quadratic decoder approach to nonintrusive reduced-order modeling of nonlinear dynamical systems},
+  author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff},
+  journal = {Proceedings in Applied Mathematics and Mechanics},
+  volume = {23},
+  number = {1},
+  pages = {e202200049},
+  year = {2023},
+  doi = {10.1002/pamm.202200049},
+  }
+

+* [**Operator inference for non-intrusive model reduction with quadratic manifolds**](https://doi.org/10.1016/j.cma.2022.115717) + [R. Geelen](https://scholar.google.com/citations?user=vBzKRMsAAAAJ), [S. Wright](https://scholar.google.com/citations?user=VFQRIOwAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Methods in Applied Mechanics and Engineering, 2023
BibTeX
@article{geelen2023quadmanifold,
+  title = {Operator inference for non-intrusive model reduction with quadratic manifolds},
+  author = {Rudy Geelen and Stephen Wright and Karen Willcox},
+  journal = {Computer Methods in Applied Mechanics and Engineering},
+  volume = {403},
+  pages = {115717},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.cma.2022.115717},
+  }
+

+* [**Learning latent representations in high-dimensional state spaces using polynomial manifold constructions**](https://doi.org/10.1109/CDC49753.2023.10384209) + [R. Geelen](https://scholar.google.com/citations?user=vBzKRMsAAAAJ), [L. Balzano](https://scholar.google.com/citations?user=X6fRNfUAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + 62nd IEEE Conference on Decision and Control (CDC), 2023
BibTeX
@inproceedings{geelen2023latent,
+  author = {Rudy Geelen and Laura Balzano and Karen Willcox},
+  booktitle = {62nd IEEE Conference on Decision and Control (CDC)},
+  title = {Learning latent representations in high-dimensional state spaces using polynomial manifold constructions},
+  year = {2023},
+  volume = {},
+  number = {},
+  pages = {4960-4965},
+  doi = {10.1109/CDC49753.2023.10384209},
+  }
+

+* [**Learning physics-based reduced-order models from data using nonlinear manifolds**](https://doi.org/10.1063/5.0170105) + [R. Geelen](https://scholar.google.com/citations?user=vBzKRMsAAAAJ), [L. Balzano](https://scholar.google.com/citations?user=X6fRNfUAAAAJ), [S. Wright](https://scholar.google.com/citations?user=VFQRIOwAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Chaos: An Interdisciplinary Journal of Nonlinear Science, 2024
BibTeX
@article{geelen2024nonlinmanifold,
+  title = {Learning physics-based reduced-order models from data using nonlinear manifolds},
+  author = {Rudy Geelen and Laura Balzano and Stephen Wright and Karen Willcox},
+  journal = {Chaos: An Interdisciplinary Journal of Nonlinear Science},
+  volume = {34},
+  number = {3},
+  pages = {033122},
+  year = {2024},
+  doi = {10.1063/5.0170105},
+  }
+ +### Scalability + +These works focus on the computational challenge of applying +Operator Inference to large-scale problems. + +* [**Data-driven reduced-order models via regularised operator inference for a single-injector combustion process**](https://doi.org/10.1080/03036758.2020.1863237) + [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), [C. Huang](https://scholar.google.com/citations?user=lUXijaQAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Journal of the Royal Society of New Zealand, 2021
BibTeX
@article{mcquarrie2021combustion,
+  title = {Data-driven reduced-order models via regularised operator inference for a single-injector combustion process},
+  author = {Shane A McQuarrie and Cheng Huang and Karen Willcox},
+  journal = {Journal of the Royal Society of New Zealand},
+  volume = {51},
+  issue = {2},
+  pages = {194-211},
+  year = {2021},
+  publisher = {Taylor \\& Francis},
+  doi = {10.1080/03036758.2020.1863237},
+  }
+

+* [**A parallel implementation of reduced-order modeling of large-scale systems**](https://doi.org/10.2514/6.2025-1170) + [I. Farcas](https://scholar.google.com/citations?user=Cts5ePIAAAAJ), R. P. Gundevia, R. Munipalli, and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + AIAA SciTech 2025 Forum, 2025
BibTeX
@inbook{farcas2025parallel,
+  title = {A parallel implementation of reduced-order modeling of large-scale systems},
+  author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox},
+  booktitle = {AIAA SciTech 2025 Forum},
+  pages = {1170},
+  year = {2025},
+  doi = {10.2514/6.2025-1170},
+  }
+

+* [**Distributed computing for physics-based data-driven reduced modeling at scale: Application to a rotating detonation rocket engine**](https://doi.org/10.1016/j.cpc.2025.109619) + I. Farcaş, R. P. Gundevia, R. Munipalli, and [K. E. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + Computer Physics Communications, 2025
BibTeX
@article{farcas2025distributed,
+  title = {Distributed computing for physics-based data-driven reduced modeling at scale: {A}pplication to a rotating detonation rocket engine},
+  author = {Ionut-Gabriel Farcaş and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox},
+  journal = {Computer Physics Communications},
+  volume = {313},
+  pages = {109619},
+  year = {2025},
+  doi = {10.1016/j.cpc.2025.109619},
+  }
+ +## Applications + +* [**Learning physics-based reduced-order models for a single-injector combustion process**](https://doi.org/10.2514/1.J058943) + [R. Swischuk](https://scholar.google.com/citations?user=L9D0LBsAAAAJ), [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ), [C. Huang](https://scholar.google.com/citations?user=lUXijaQAAAAJ), and [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ) + AIAA Journal, 2020
BibTeX
@article{swischuk2020combustion,
+  title = {Learning physics-based reduced-order models for a single-injector combustion process},
+  author = {Renee Swischuk and Boris Kramer and Cheng Huang and Karen Willcox},
+  journal = {AIAA Journal},
+  volume = {58},
+  issue = {6},
+  pages = {2658--2672},
+  year = {2020},
+  publisher = {American Institute of Aeronautics and Astronautics},
+  doi = {10.2514/1.J058943},
+  }
+

+* [**Performance comparison of data-driven reduced models for a single-injector combustion process**](https://doi.org/10.2514/6.2021-3633) + P. Jain, [S. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ), and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + AIAA Propulsion and Energy 2021 Forum, 2021
BibTeX
@inproceedings{jain2021performance,
+  title = {Performance comparison of data-driven reduced models for a single-injector combustion process},
+  author = {Parikshit Jain and Shane McQuarrie and Boris Kramer},
+  booktitle = {AIAA Propulsion and Energy 2021 Forum},
+  pages = {3633},
+  year = {2021},
+  doi = {10.2514/6.2021-3633},
+  }
+

+* [**Non-Intrusive reduced models based on operator inference for chaotic systems**](https://arxiv.org/abs/2206.01604) + J. L. d. S. Almeida, [A. C. Pires](https://scholar.google.com/citations?user=qIUw-GEAAAAJ), K. F. V. Cid, and [A. C. Nogueira Jr.](https://scholar.google.com/citations?user=66DEy5wAAAAJ) + arXiv, 2022
BibTeX
@article{almeida2022chaotic,
+  title = {Non-Intrusive reduced models based on operator inference for chaotic systems},
+  author = {Jo\\~{a}o Lucas de Sousa Almeida and Arthur Cancellieri Pires and Klaus Feine Vaz Cid and Alberto Costa Nogueira Jr},
+  journal = {arXiv},
+  volume = {2206.01604},
+  year = {2022},
+  url = {https://arxiv.org/abs/2206.01604},
+  }
+

+* [**Data-driven reduced-order model for atmospheric CO2 dispersion**](https://s3.us-east-1.amazonaws.com/climate-change-ai/papers/aaaifss2022/2/paper.pdf) + P. R. B. Rocha, [M. S. d. P. Gomes](https://scholar.google.com/citations?user=s6mocWAAAAAJ), J. L. d. S. Almeida, A. M. Carvalho, and [A. C. Nogueira Jr.](https://scholar.google.com/citations?user=66DEy5wAAAAJ) + AAAI Fall Symposium, 2022
BibTeX
@inproceedings{rocha2022c02,
+  title = {Data-driven reduced-order model for atmospheric {CO}2 dispersion},
+  author = {Pedro Roberto Barbosa Rocha and Marcos Sebasti\\~{a}o de Paula Gomes and Jo\\~{a}o Lucas de Sousa Almeida and Allan M Carvalho and Alberto Costa Nogueira Jr},
+  booktitle = {AAAI Fall Symposium},
+  year = {2022},
+  url = {https://s3.us-east-1.amazonaws.com/climate-change-ai/papers/aaaifss2022/2/paper.pdf},
+  }
+

+* [**Reduced-order modeling of the two-dimensional Rayleigh--Bénard convection flow through a non-intrusive operator inference**](https://doi.org/10.1016/j.engappai.2023.106923) + P. R. B. Rocha, J. L. d. S. Almeida, [M. S. d. P. Gomes](https://scholar.google.com/citations?user=s6mocWAAAAAJ), and [A. C. Nogueira Jr.](https://scholar.google.com/citations?user=66DEy5wAAAAJ) + Engineering Applications of Artificial Intelligence, 2023
BibTeX
@article{rocha2023convection,
+  title = {Reduced-order modeling of the two-dimensional {R}ayleigh--{B}\\'{e}nard convection flow through a non-intrusive operator inference},
+  author = {Pedro Roberto Barbosa Rocha and Jo\\~{a}o Lucas de Sousa Almeida and Marcos Sebasti\\~{a}o de Paula Gomes and Alberto Costa Nogueira Jr},
+  journal = {Engineering Applications of Artificial Intelligence},
+  volume = {126},
+  pages = {106923},
+  year = {2023},
+  publisher = {Elsevier},
+  doi = {10.1016/j.engappai.2023.106923},
+  }
+

+* [**Data-driven model reduction via operator inference for coupled aeroelastic flutter**](https://doi.org/10.2514/6.2023-0330) + [B. G. Zastrow](https://scholar.google.com/citations?user=ODLjrBAAAAAJ), [A. Chaudhuri](https://scholar.google.com/citations?user=oGL9YJIAAAAJ), [K. Willcox](https://scholar.google.com/citations?user=axvGyXoAAAAJ), [A. S. Ashley](https://scholar.google.com/citations?user=9KFAXLYAAAAJ), and M. C. Henson + AIAA SciTech 2023 Forum, 2023
BibTeX
@inproceedings{zastrow2023flutter,
+  title = {Data-driven model reduction via operator inference for coupled aeroelastic flutter},
+  author = {Benjamin G Zastrow and Anirban Chaudhuri and Karen Willcox and Anthony S Ashley and Michael C Henson},
+  booktitle = {AIAA SciTech 2023 Forum},
+  pages = {0330},
+  year = {2023},
+  doi = {10.2514/6.2023-0330},
+  }
+

+* [**Operator inference-based model order reduction of thermal protection system finite element simulations**](https://doi.org/10.2514/6.2025-2133) + [P. J. Blonigan](https://scholar.google.com/citations?user=lOmH5XcAAAAJ), [J. Tencer](https://scholar.google.com/citations?user=M6AwtC4AAAAJ), [S. Babiniec](https://scholar.google.com/citations?user=xcSVh00AAAAJ), and [J. Murray](https://scholar.google.com/citations?user=NScAg7AAAAAJ) + AIAA SciTech 2025 Forum, 2025
BibTeX
@inbook{blonigan2025thermal,
+  title = {Operator inference-based model order reduction of thermal protection system finite element simulations},
+  author = {Patrick J. Blonigan and John Tencer and Sean Babiniec and Jonathan Murray},
+  booktitle = {AIAA SciTech 2025 Forum},
+  pages = {2133},
+  year = {2025},
+  doi = {10.2514/6.2025-2133},
+  }
+

+* [**Parametric Operator Inference to simulate the purging process in semiconductor manufacturing**](https://doi.org/10.48550/arXiv.2504.03990) + S. Kang, [H. Kim](https://scholar.google.com/citations?user=sdR-LZ4AAAAJ), and [B. Kramer](https://scholar.google.com/citations?user=yfmbPNoAAAAJ) + arXiv, 2025
BibTeX
@article{kang2025semiconductor,
+  title = {Parametric {O}perator {I}nference to simulate the purging process in semiconductor manufacturing},
+  author = {Seunghyon Kang and Hyeonghun Kim and Boris Kramer},
+  journal = {arXiv},
+  volume = {2504.03990},
+  year = {2025},
+  doi = {10.48550/arXiv.2504.03990},
+  }
+ +## Dissertations and Theses + +* [**Physics-based machine learning and data-driven reduced-order modeling**](https://dspace.mit.edu/handle/1721.1/122682) + [R. C. Swischuk](https://scholar.google.com/citations?user=L9D0LBsAAAAJ) + Master's Thesis, Massachusetts Institute of Technology, 2019
BibTeX
@mastersthesis{swischuk2019thesis,
+  title = {Physics-based machine learning and data-driven reduced-order modeling},
+  author = {Renee Copland Swischuk},
+  school = {Massachusetts Institute of Technology},
+  year = {2019},
+  url = {https://dspace.mit.edu/handle/1721.1/122682},
+  }
+

+* [**A scientific machine learning approach to learning reduced models for nonlinear partial differential equations**](https://dspace.mit.edu/handle/1721.1/130748) + [E. Y. Qian](https://scholar.google.com/citations?user=jnHI7wQAAAAJ) + PhD Thesis, Massachusetts Institute of Technology, 2021
BibTeX
@phdthesis{qian2021thesis,
+  title = {A scientific machine learning approach to learning reduced models for nonlinear partial differential equations},
+  author = {Elizabeth Yi Qian},
+  school = {Massachusetts Institute of Technology},
+  year = {2021},
+  url = {https://dspace.mit.edu/handle/1721.1/130748},
+  }
+

+* [**Toward predictive digital twins for self-aware unmanned aerial vehicles: Non-intrusive reduced order models and experimental data analysis**](http://dx.doi.org/10.26153/tsw/14557) + S. J. Salinger + Master's Thesis, The University of Texas at Austin, 2021
BibTeX
@mastersthesis{salinger2021thesis,
+  title = {Toward predictive digital twins for self-aware unmanned aerial vehicles: {N}on-intrusive reduced order models and experimental data analysis},
+  author = {Stephanie Joyce Salinger},
+  school = {The University of Texas at Austin},
+  year = {2021},
+  url = {http://dx.doi.org/10.26153/tsw/14557},
+  }
+

+* [**Data-driven parametric reduced-order models: Operator inference for reactive flow applications**](https://doi.org/10.26153/tsw/50172) + [S. A. McQuarrie](https://scholar.google.com/citations?user=qQ6JDJ4AAAAJ) + PhD Thesis, The University of Texas at Austin, 2023
BibTeX
@phdthesis{mcquarrie2023thesis,
+  title = {Data-driven parametric reduced-order models: {O}perator inference for reactive flow applications},
+  author = {Shane Alexander McQuarrie},
+  school = {The University of Texas at Austin},
+  year = {2023},
+  doi = {10.26153/tsw/50172},
+  }
+

+* **Learning structured and stable reduced models from data with operator inference** + N. Sawant + PhD Thesis, New York University, 2023
BibTeX
@phdthesis{sawant2023thesis,
+  title = {Learning structured and stable reduced models from data with operator inference},
+  author = {Nihar Sawant},
+  year = {2023},
+  school = {New York University},
+  }
+## BibTex File + +:::{dropdown} Sorted alphabetically by author +:color: success + +```bibtex +@article{almeida2022chaotic, + title = {Non-Intrusive reduced models based on operator inference for chaotic systems}, + author = {Jo\~{a}o Lucas de Sousa Almeida and Arthur Cancellieri Pires and Klaus Feine Vaz Cid and Alberto Costa Nogueira Jr}, + journal = {arXiv}, + volume = {2206.01604}, + year = {2022}, +} + +@inproceedings{aretz2024enforcing, + title = {Enforcing structure in data-driven reduced modeling through nested {O}perator {I}nference}, + author = {Nicole Aretz and Karen Willcox}, + booktitle = {63rd IEEE Conference on Decision and Control (CDC)}, + year = {2024}, + organization = {IEEE}, + doi = {10.1109/CDC56724.2024.10885857}, +} + +@article{aretz2025nestedopinf, + title = {Nested operator inference for adaptive data-driven learning of reduced-order models}, + author = {Nicole Aretz and Karen Willcox}, + journal = {arXiv}, + volume = {2508.11542}, + year = {2025}, + doi = {10.48550/arXiv.2508.11542}, +} + +@article{benner2020deim, + title = {Operator inference for non-intrusive model reduction of systems with non-polynomial nonlinear terms}, + author = {Peter Benner and Pawan Goyal and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {372}, + pages = {113433}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2020.113433}, +} + +@article{benner2022incompressible, + title = {Operator inference and physics-informed learning of low-dimensional models for incompressible flows}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Electronic Transactions on Numerical Analysis}, + volume = {56}, + year = {2022}, + doi = {10.1553/etna_vol56s28}, +} + +@article{benner2023quaddecoder, + title = {A quadratic decoder approach to nonintrusive reduced-order modeling of nonlinear dynamical systems}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Proceedings in Applied Mathematics and Mechanics}, + volume = {23}, + number = {1}, + pages = {e202200049}, + year = {2023}, + doi = {10.1002/pamm.202200049}, +} + +@inbook{blonigan2025thermal, + title = {Operator inference-based model order reduction of thermal protection system finite element simulations}, + author = {Patrick J. Blonigan and John Tencer and Sean Babiniec and Jonathan Murray}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {2133}, + year = {2025}, + doi = {10.2514/6.2025-2133}, +} + +@article{boef2024stablesparse, + title = {Stable sparse operator inference for nonlinear structural dynamics}, + author = {Pascal {den Boef} and Diana Manvelyan and Joseph Maubach and Wil Schilders and Nathan {van de Wouw}}, + journal = {arXiv}, + volume = {2407.21672}, + year = {2024}, + doi = {10.48550/arXiv.2407.21672}, +} + +@article{bychkov2024quadratization, + title = {Exact and optimal quadratization of nonlinear finite-dimensional non-autonomous dynamical systems}, + author = {Andrey Bychkov and Opal Issan and Gleb Pogudin and Boris Kramer}, + journal = {SIAM Journal of Applied Dynamical Systems}, + volume = {23}, + number = {1}, + pages = {982-1016}, + year = {2024}, + doi = {10.1137/23M1561129}, +} + +@inproceedings{farcas2023parametric, + title = {Parametric non-intrusive reduced-order models via operator inference for large-scale rotating detonation engine simulations}, + author = {Ionut-Gabriel Farcas and Rayomand Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0172}, + year = {2023}, + doi = {10.2514/6.2023-0172}, +} + +@article{farcas2024domaindecomposition, + title = {Domain decomposition for data-driven reduced modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {AIAA Journal}, + volume = {62}, + number = {11}, + pages = {4071-4086}, + year = {2024}, + doi = {10.2514/1.J063715}, +} + +@article{farcas2025distributed, + title = {Distributed computing for physics-based data-driven reduced modeling at scale: {A}pplication to a rotating detonation rocket engine}, + author = {Ionut-Gabriel Farcaş and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {Computer Physics Communications}, + volume = {313}, + pages = {109619}, + year = {2025}, + doi = {10.1016/j.cpc.2025.109619}, +} + +@inbook{farcas2025parallel, + title = {A parallel implementation of reduced-order modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {1170}, + year = {2025}, + doi = {10.2514/6.2025-1170}, +} + +@article{filanova2023mechanical, + title = {An operator inference oriented approach for linear mechanical systems}, + author = {Yevgeniya Filanova and Igor Pontes Duff and Pawan Goyal and Peter Benner}, + journal = {Mechanical Systems and Signal Processing}, + volume = {200}, + pages = {110620}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.ymssp.2023.110620}, +} + +@misc{freitag2024stochastic, + title = {Learning stochastic reduced models from data: a nonintrusive approach}, + author = {Melina A. Freitag and Jan Martin Nicolaus and Martin Redmann}, + journal = {arXiv}, + volume = {2407.05724}, + year = {2024}, + doi = {10.48550/arXiv.2407.05724}, +} + +@article{geelen2022localized, + title = {Localized non-intrusive reduced-order modelling in the operator inference framework}, + author = {Rudy Geelen and Karen Willcox}, + journal = {Philosophical Transactions of the Royal Society A}, + volume = {380}, + number = {2229}, + pages = {20210206}, + year = {2022}, + doi = {10.1098/rsta.2021.0206}, +} + +@inproceedings{geelen2023latent, + author = {Rudy Geelen and Laura Balzano and Karen Willcox}, + booktitle = {62nd IEEE Conference on Decision and Control (CDC)}, + title = {Learning latent representations in high-dimensional state spaces using polynomial manifold constructions}, + year = {2023}, + volume = {}, + number = {}, + pages = {4960-4965}, + doi = {10.1109/CDC49753.2023.10384209}, +} + +@article{geelen2023quadmanifold, + title = {Operator inference for non-intrusive model reduction with quadratic manifolds}, + author = {Rudy Geelen and Stephen Wright and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {403}, + pages = {115717}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115717}, +} + +@article{geelen2024nonlinmanifold, + title = {Learning physics-based reduced-order models from data using nonlinear manifolds}, + author = {Rudy Geelen and Laura Balzano and Stephen Wright and Karen Willcox}, + journal = {Chaos: An Interdisciplinary Journal of Nonlinear Science}, + volume = {34}, + number = {3}, + pages = {033122}, + year = {2024}, + doi = {10.1063/5.0170105}, +} + +@article{geng2024gradient, + title = {Gradient preserving {O}perator {I}nference: {D}ata-driven reduced-order models for equations with gradient structure}, + author = {Yuwei Geng and Jasdeep Singh and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {427}, + pages = {117033}, + year = {2024}, + doi = {10.1016/j.cma.2024.117033}, +} + +@article{geng2025porthamiltonian, + title = {Data-driven reduced-order models for port-{H}amiltonian systems with {O}perator {I}nference}, + author = {Yuwei Geng and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {arXiv}, + volume = {2501.02183}, + year = {2025}, + doi = {10.48550/arXiv.2501.02183}, +} + +@article{ghattas2021acta, + title = {Learning physics-based models from data: {P}erspectives from inverse problems and model reduction}, + author = {Omar Ghattas and Karen Willcox}, + journal = {Acta Numerica}, + volume = {30}, + pages = {445--554}, + year = {2021}, + publisher = {Cambridge University Press}, + doi = {10.1017/S0962492921000064}, +} + +@article{gkimisis2025spatiallylocal, + title = {Non-intrusive reduced-order modeling for dynamical systems with spatially localized features}, + author = {Leonidas Gkimisis and Nicole Aretz and Marco Tezzele and Thomas Richter and Peter Benner and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {444}, + pages = {118115}, + year = {2025}, + doi = {10.1016/j.cma.2025.118115}, +} + +@article{gruber2023hamiltonian, + title = {Canonical and noncanonical {H}amiltonian operator inference}, + author = {Anthony Gruber and Irina Tezaur}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {416}, + year = {2023}, + doi = {10.1016/j.cma.2023.116334}, +} + +@article{gruber2025variational, + author = {Anthony Gruber and Irina Tezaur}, + title = {Variationally consistent {H}amiltonian model reduction}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {24}, + number = {1}, + pages = {376-414}, + year = {2025}, + doi = {10.1137/24M1652490}, +} + +@article{guo2022bayesopinf, + title = {Bayesian operator inference for data-driven reduced-order modeling}, + author = {Mengwu Guo and Shane A McQuarrie and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {402}, + pages = {115336}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115336}, +} + +@article{issan2023shifted, + title = {Predicting solar wind streams from the inner-heliosphere to Earth via shifted operator inference}, + author = {Opal Issan and Boris Kramer}, + journal = {Journal of Computational Physics}, + volume = {473}, + pages = {111689}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.jcp.2022.111689}, +} + +@inproceedings{jain2021performance, + title = {Performance comparison of data-driven reduced models for a single-injector combustion process}, + author = {Parikshit Jain and Shane McQuarrie and Boris Kramer}, + booktitle = {AIAA Propulsion and Energy 2021 Forum}, + pages = {3633}, + year = {2021}, + doi = {10.2514/6.2021-3633}, +} + +@article{kang2025semiconductor, + title = {Parametric {O}perator {I}nference to simulate the purging process in semiconductor manufacturing}, + author = {Seunghyon Kang and Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2504.03990}, + year = {2025}, + doi = {10.48550/arXiv.2504.03990}, +} + +@article{khodabakhshi2022diffalg, + title = {Non-intrusive data-driven model reduction for differential algebraic equations derived from lifting transformations}, + author = {Parisa Khodabakhshi and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {389}, + pages = {114296}, + year = {2022}, + doi = {10.1016/j.cma.2021.114296}, +} + +@article{kim2025stateconstraints, + title = {Physically consistent predictive reduced-order modeling by enhancing {O}perator {I}nference with state constraints}, + author = {Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2502.03672}, + year = {2025}, + doi = {10.48550/arXiv.2502.03672}, +} + +@inproceedings{koike2024energy, + title = {Energy-preserving reduced operator inference for efficient design and control}, + author = {Tomoki Koike and Elizabeth Qian}, + booktitle = {AIAA SciTech 2024 Forum}, + pages = {1012}, + year = {2024}, + doi = {10.2514/6.2024-1012}, +} + +@article{kramer2021quadstability, + title = {Stability domains for quadratic-bilinear reduced-order models}, + author = {Boris Kramer}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {20}, + issue = {2}, + pages = {981--996}, + year = {2021}, + publisher = {SIAM}, + doi = {10.1137/20M1364849}, +} + +@article{kramer2024survey, + title = {Learning nonlinear reduced models from data with operator inference}, + author = {Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Annual Review of Fluid Mechanics}, + volume = {56}, + pages = {521--548}, + year = {2024}, + publisher = {Annual Reviews}, + doi = {10.1146/annurev-fluid-121021-025220}, +} + +@article{mcquarrie2021combustion, + title = {Data-driven reduced-order models via regularised operator inference for a single-injector combustion process}, + author = {Shane A McQuarrie and Cheng Huang and Karen Willcox}, + journal = {Journal of the Royal Society of New Zealand}, + volume = {51}, + issue = {2}, + pages = {194-211}, + year = {2021}, + publisher = {Taylor \& Francis}, + doi = {10.1080/03036758.2020.1863237}, +} + +@article{mcquarrie2023parametric, + title = {Nonintrusive reduced-order models for parametric partial differential equations via data-driven operator inference}, + author = {Shane A McQuarrie and Parisa Khodabakhshi and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1917-A1946}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1452810}, +} + +@phdthesis{mcquarrie2023thesis, + title = {Data-driven parametric reduced-order models: {O}perator inference for reactive flow applications}, + author = {Shane Alexander McQuarrie}, + school = {The University of Texas at Austin}, + year = {2023}, + doi = {10.26153/tsw/50172}, +} + +@article{mcquarrie2025gpbayesopinf, + title = {Bayesian learning with {G}aussian processes for low-dimensional representations of time-dependent nonlinear systems}, + author = {Shane A. McQuarrie and Anirban Chaudhuri and Karen E. Willcox and Mengwu Guo}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {475}, + pages = {134572}, + year = {2025}, + doi = {10.1016/j.physd.2025.134572}, +} + +@article{peherstorfer2016opinf, + title = {Data-driven operator inference for nonintrusive projection-based model reduction}, + author = {Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {306}, + pages = {196--215}, + year = {2016}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2016.03.025}, +} + +@article{peherstorfer2020reprojection, + title = {Sampling low-dimensional {M}arkovian dynamics for preasymptotically recovering reduced models from data with operator inference}, + author = {Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {42}, + issue = {5}, + pages = {A3489-a3515}, + year = {2020}, + publisher = {SIAM}, + doi = {10.1137/19M1292448}, +} + +@inproceedings{qian2019transform, + title = {Transform \& {L}earn: {A} data-driven approach to nonlinear model reduction}, + author = {Elizabeth Qian and Boris Kramer and Alexandre N. Marques and Karen E. Willcox}, + booktitle = {AIAA Aviation 2019 Forum}, + pages = {3707}, + year = {2019}, + doi = {10.2514/6.2019-3707}, +} + +@article{qian2020liftandlearn, + title = {Lift \& {L}earn: {P}hysics-informed machine learning for large-scale nonlinear dynamical systems}, + author = {Elizabeth Qian and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {406}, + pages = {132401}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2020.132401}, +} + +@phdthesis{qian2021thesis, + title = {A scientific machine learning approach to learning reduced models for nonlinear partial differential equations}, + author = {Elizabeth Yi Qian}, + school = {Massachusetts Institute of Technology}, + year = {2021}, +} + +@article{qian2022pdes, + title = {Reduced operator inference for nonlinear partial differential equations}, + author = {Elizabeth Qian and Ionut-Gabriel Farcas and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {44}, + issue = {4}, + pages = {A1934-a1959}, + year = {2022}, + publisher = {SIAM}, + doi = {10.1137/21M1393972}, +} + +@inproceedings{rocha2022c02, + title = {Data-driven reduced-order model for atmospheric {CO}2 dispersion}, + author = {Pedro Roberto Barbosa Rocha and Marcos Sebasti\~{a}o de Paula Gomes and Jo\~{a}o Lucas de Sousa Almeida and Allan M Carvalho and Alberto Costa Nogueira Jr}, + booktitle = {AAAI Fall Symposium}, + year = {2022}, +} + +@article{rocha2023convection, + title = {Reduced-order modeling of the two-dimensional {R}ayleigh--{B}\'{e}nard convection flow through a non-intrusive operator inference}, + author = {Pedro Roberto Barbosa Rocha and Jo\~{a}o Lucas de Sousa Almeida and Marcos Sebasti\~{a}o de Paula Gomes and Alberto Costa Nogueira Jr}, + journal = {Engineering Applications of Artificial Intelligence}, + volume = {126}, + pages = {106923}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.engappai.2023.106923}, +} + +@article{rosenberger2025exactopinf, + title = {Exact operator inference with minimal data}, + author = {Henrik Rosenberger and Benjamin Sanderse and Giovanni Stabile}, + journal = {arXiv}, + volume = {2506.01244}, + year = {2025}, + doi = {10.48550/arXiv.2506.01244}, +} + +@mastersthesis{salinger2021thesis, + title = {Toward predictive digital twins for self-aware unmanned aerial vehicles: {N}on-intrusive reduced order models and experimental data analysis}, + author = {Stephanie Joyce Salinger}, + school = {The University of Texas at Austin}, + year = {2021}, +} + +@article{sawant2023pireg, + title = {Physics-informed regularization and structure preservation for learning stable reduced models from data with operator inference}, + author = {Nihar Sawant and Boris Kramer and Benjamin Peherstorfer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {404}, + pages = {115836}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115836}, +} + +@phdthesis{sawant2023thesis, + title = {Learning structured and stable reduced models from data with operator inference}, + author = {Nihar Sawant}, + year = {2023}, + school = {New York University}, +} + +@article{sharma2022hamiltonian, + title = {Hamiltonian operator inference: {P}hysics-preserving learning of reduced-order models for canonical {H}amiltonian systems}, + author = {Harsh Sharma and Zhu Wang and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {431}, + pages = {133122}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2021.133122}, +} + +@article{sharma2024lagrangian, + title = {Lagrangian operator inference enhanced with structure-preserving machine learning for nonintrusive model reduction of mechanical systems}, + author = {Harsh Sharma and David A Najera-Flores and Michael D Todd and Boris Kramer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {423}, + pages = {116865}, + year = {2024}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2024.116865}, +} + +@article{sharma2024preserving, + title = {Preserving {L}agrangian structure in data-driven reduced-order modeling of large-scale mechanical systems}, + author = {Harsh Sharma and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {462}, + pages = {134128}, + year = {2024}, + doi = {10.1016/j.physd.2024.134128}, +} + +@mastersthesis{swischuk2019thesis, + title = {Physics-based machine learning and data-driven reduced-order modeling}, + author = {Renee Copland Swischuk}, + school = {Massachusetts Institute of Technology}, + year = {2019}, +} + +@article{swischuk2020combustion, + title = {Learning physics-based reduced-order models for a single-injector combustion process}, + author = {Renee Swischuk and Boris Kramer and Cheng Huang and Karen Willcox}, + journal = {AIAA Journal}, + volume = {58}, + issue = {6}, + pages = {2658--2672}, + year = {2020}, + publisher = {American Institute of Aeronautics and Astronautics}, + doi = {10.2514/1.J058943}, +} + +@article{uy2021error, + title = {Probabilistic error estimation for non-intrusive reduced models learned from data of systems governed by linear parabolic partial differential equations}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {ESAIM: Mathematical Modelling and Numerical Analysis}, + volume = {55}, + issue = {3}, + pages = {735--761}, + year = {2021}, + publisher = {EDP Sciences}, + doi = {10.1051/m2an/2021010}, +} + +@article{uy2021partial, + title = {Operator inference of non-{M}arkovian terms for learning reduced models from partially observed state trajectories}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {Journal of Scientific Computing}, + volume = {88}, + issue = {3}, + pages = {1--31}, + year = {2021}, + publisher = {Springer}, + doi = {10.1007/s10915-021-01580-2}, +} + +@article{uy2023active, + title = {Active operator inference for learning low-dimensional dynamical-system models from noisy data}, + author = {Wayne Isaac Tan Uy and Yuepeng Wang and Yuxiao Wen and Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1462-a1490}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1439729}, +} + +@article{uy2023rollouts, + title = {Operator inference with roll outs for learning reduced models from scarce and low-quality data}, + author = {Wayne Isaac Tan Uy and Dirk Hartmann and Benjamin Peherstorfer}, + journal = {Computers \& Mathematics with Applications}, + volume = {145}, + pages = {224--239}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.camwa.2023.06.012}, +} + +@article{vijaywargiya2025tensoropinf, + title = {Tensor parametric {H}amiltonian operator inference}, + author = {Arjun Vijaywargiya and Shane A. McQuarrie and Anthony Gruber}, + journal = {arXiv}, + volume = {2502.10888}, + year = {2025}, + doi = {10.48550/arXiv.2502.10888}, +} + +@article{yildiz2021shallow, + title = {Learning reduced-order dynamics for parametrized shallow water equations from data}, + author = {S\"{u}leyman Y\i{}ld\i{}z and Pawan Goyal and Peter Benner and B\"{u}lent Karas\"{o}zen}, + journal = {International Journal for Numerical Methods in Fluids}, + volume = {93}, + issue = {8}, + pages = {2803--2821}, + year = {2021}, + publisher = {Wiley Online Library}, + doi = {10.1002/fld.4998}, +} + +@inproceedings{zastrow2023flutter, + title = {Data-driven model reduction via operator inference for coupled aeroelastic flutter}, + author = {Benjamin G Zastrow and Anirban Chaudhuri and Karen Willcox and Anthony S Ashley and Michael C Henson}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0330}, + year = {2023}, + doi = {10.2514/6.2023-0330}, +} + +``` +::: + +:::{dropdown} Sorted by year then alphabetically by author +:color: success + +```bibtex +@article{peherstorfer2016opinf, + title = {Data-driven operator inference for nonintrusive projection-based model reduction}, + author = {Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {306}, + pages = {196--215}, + year = {2016}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2016.03.025}, +} + +@inproceedings{qian2019transform, + title = {Transform \& {L}earn: {A} data-driven approach to nonlinear model reduction}, + author = {Elizabeth Qian and Boris Kramer and Alexandre N. Marques and Karen E. Willcox}, + booktitle = {AIAA Aviation 2019 Forum}, + pages = {3707}, + year = {2019}, + doi = {10.2514/6.2019-3707}, +} + +@mastersthesis{swischuk2019thesis, + title = {Physics-based machine learning and data-driven reduced-order modeling}, + author = {Renee Copland Swischuk}, + school = {Massachusetts Institute of Technology}, + year = {2019}, +} + +@article{benner2020deim, + title = {Operator inference for non-intrusive model reduction of systems with non-polynomial nonlinear terms}, + author = {Peter Benner and Pawan Goyal and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {372}, + pages = {113433}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2020.113433}, +} + +@article{peherstorfer2020reprojection, + title = {Sampling low-dimensional {M}arkovian dynamics for preasymptotically recovering reduced models from data with operator inference}, + author = {Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {42}, + issue = {5}, + pages = {A3489-a3515}, + year = {2020}, + publisher = {SIAM}, + doi = {10.1137/19M1292448}, +} + +@article{qian2020liftandlearn, + title = {Lift \& {L}earn: {P}hysics-informed machine learning for large-scale nonlinear dynamical systems}, + author = {Elizabeth Qian and Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {406}, + pages = {132401}, + year = {2020}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2020.132401}, +} + +@article{swischuk2020combustion, + title = {Learning physics-based reduced-order models for a single-injector combustion process}, + author = {Renee Swischuk and Boris Kramer and Cheng Huang and Karen Willcox}, + journal = {AIAA Journal}, + volume = {58}, + issue = {6}, + pages = {2658--2672}, + year = {2020}, + publisher = {American Institute of Aeronautics and Astronautics}, + doi = {10.2514/1.J058943}, +} + +@article{ghattas2021acta, + title = {Learning physics-based models from data: {P}erspectives from inverse problems and model reduction}, + author = {Omar Ghattas and Karen Willcox}, + journal = {Acta Numerica}, + volume = {30}, + pages = {445--554}, + year = {2021}, + publisher = {Cambridge University Press}, + doi = {10.1017/S0962492921000064}, +} + +@inproceedings{jain2021performance, + title = {Performance comparison of data-driven reduced models for a single-injector combustion process}, + author = {Parikshit Jain and Shane McQuarrie and Boris Kramer}, + booktitle = {AIAA Propulsion and Energy 2021 Forum}, + pages = {3633}, + year = {2021}, + doi = {10.2514/6.2021-3633}, +} + +@article{kramer2021quadstability, + title = {Stability domains for quadratic-bilinear reduced-order models}, + author = {Boris Kramer}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {20}, + issue = {2}, + pages = {981--996}, + year = {2021}, + publisher = {SIAM}, + doi = {10.1137/20M1364849}, +} + +@article{mcquarrie2021combustion, + title = {Data-driven reduced-order models via regularised operator inference for a single-injector combustion process}, + author = {Shane A McQuarrie and Cheng Huang and Karen Willcox}, + journal = {Journal of the Royal Society of New Zealand}, + volume = {51}, + issue = {2}, + pages = {194-211}, + year = {2021}, + publisher = {Taylor \& Francis}, + doi = {10.1080/03036758.2020.1863237}, +} + +@phdthesis{qian2021thesis, + title = {A scientific machine learning approach to learning reduced models for nonlinear partial differential equations}, + author = {Elizabeth Yi Qian}, + school = {Massachusetts Institute of Technology}, + year = {2021}, +} + +@mastersthesis{salinger2021thesis, + title = {Toward predictive digital twins for self-aware unmanned aerial vehicles: {N}on-intrusive reduced order models and experimental data analysis}, + author = {Stephanie Joyce Salinger}, + school = {The University of Texas at Austin}, + year = {2021}, +} + +@article{uy2021partial, + title = {Operator inference of non-{M}arkovian terms for learning reduced models from partially observed state trajectories}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {Journal of Scientific Computing}, + volume = {88}, + issue = {3}, + pages = {1--31}, + year = {2021}, + publisher = {Springer}, + doi = {10.1007/s10915-021-01580-2}, +} + +@article{uy2021error, + title = {Probabilistic error estimation for non-intrusive reduced models learned from data of systems governed by linear parabolic partial differential equations}, + author = {Wayne Isaac Tan Uy and Benjamin Peherstorfer}, + journal = {ESAIM: Mathematical Modelling and Numerical Analysis}, + volume = {55}, + issue = {3}, + pages = {735--761}, + year = {2021}, + publisher = {EDP Sciences}, + doi = {10.1051/m2an/2021010}, +} + +@article{yildiz2021shallow, + title = {Learning reduced-order dynamics for parametrized shallow water equations from data}, + author = {S\"{u}leyman Y\i{}ld\i{}z and Pawan Goyal and Peter Benner and B\"{u}lent Karas\"{o}zen}, + journal = {International Journal for Numerical Methods in Fluids}, + volume = {93}, + issue = {8}, + pages = {2803--2821}, + year = {2021}, + publisher = {Wiley Online Library}, + doi = {10.1002/fld.4998}, +} + +@article{almeida2022chaotic, + title = {Non-Intrusive reduced models based on operator inference for chaotic systems}, + author = {Jo\~{a}o Lucas de Sousa Almeida and Arthur Cancellieri Pires and Klaus Feine Vaz Cid and Alberto Costa Nogueira Jr}, + journal = {arXiv}, + volume = {2206.01604}, + year = {2022}, +} + +@article{benner2022incompressible, + title = {Operator inference and physics-informed learning of low-dimensional models for incompressible flows}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Electronic Transactions on Numerical Analysis}, + volume = {56}, + year = {2022}, + doi = {10.1553/etna_vol56s28}, +} + +@article{geelen2022localized, + title = {Localized non-intrusive reduced-order modelling in the operator inference framework}, + author = {Rudy Geelen and Karen Willcox}, + journal = {Philosophical Transactions of the Royal Society A}, + volume = {380}, + number = {2229}, + pages = {20210206}, + year = {2022}, + doi = {10.1098/rsta.2021.0206}, +} + +@article{guo2022bayesopinf, + title = {Bayesian operator inference for data-driven reduced-order modeling}, + author = {Mengwu Guo and Shane A McQuarrie and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {402}, + pages = {115336}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115336}, +} + +@article{khodabakhshi2022diffalg, + title = {Non-intrusive data-driven model reduction for differential algebraic equations derived from lifting transformations}, + author = {Parisa Khodabakhshi and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {389}, + pages = {114296}, + year = {2022}, + doi = {10.1016/j.cma.2021.114296}, +} + +@article{qian2022pdes, + title = {Reduced operator inference for nonlinear partial differential equations}, + author = {Elizabeth Qian and Ionut-Gabriel Farcas and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {44}, + issue = {4}, + pages = {A1934-a1959}, + year = {2022}, + publisher = {SIAM}, + doi = {10.1137/21M1393972}, +} + +@inproceedings{rocha2022c02, + title = {Data-driven reduced-order model for atmospheric {CO}2 dispersion}, + author = {Pedro Roberto Barbosa Rocha and Marcos Sebasti\~{a}o de Paula Gomes and Jo\~{a}o Lucas de Sousa Almeida and Allan M Carvalho and Alberto Costa Nogueira Jr}, + booktitle = {AAAI Fall Symposium}, + year = {2022}, +} + +@article{sharma2022hamiltonian, + title = {Hamiltonian operator inference: {P}hysics-preserving learning of reduced-order models for canonical {H}amiltonian systems}, + author = {Harsh Sharma and Zhu Wang and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {431}, + pages = {133122}, + year = {2022}, + publisher = {Elsevier}, + doi = {10.1016/j.physd.2021.133122}, +} + +@article{benner2023quaddecoder, + title = {A quadratic decoder approach to nonintrusive reduced-order modeling of nonlinear dynamical systems}, + author = {Peter Benner and Pawan Goyal and Jan Heiland and Igor Pontes Duff}, + journal = {Proceedings in Applied Mathematics and Mechanics}, + volume = {23}, + number = {1}, + pages = {e202200049}, + year = {2023}, + doi = {10.1002/pamm.202200049}, +} + +@inproceedings{farcas2023parametric, + title = {Parametric non-intrusive reduced-order models via operator inference for large-scale rotating detonation engine simulations}, + author = {Ionut-Gabriel Farcas and Rayomand Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0172}, + year = {2023}, + doi = {10.2514/6.2023-0172}, +} + +@article{filanova2023mechanical, + title = {An operator inference oriented approach for linear mechanical systems}, + author = {Yevgeniya Filanova and Igor Pontes Duff and Pawan Goyal and Peter Benner}, + journal = {Mechanical Systems and Signal Processing}, + volume = {200}, + pages = {110620}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.ymssp.2023.110620}, +} + +@article{geelen2023quadmanifold, + title = {Operator inference for non-intrusive model reduction with quadratic manifolds}, + author = {Rudy Geelen and Stephen Wright and Karen Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {403}, + pages = {115717}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115717}, +} + +@inproceedings{geelen2023latent, + author = {Rudy Geelen and Laura Balzano and Karen Willcox}, + booktitle = {62nd IEEE Conference on Decision and Control (CDC)}, + title = {Learning latent representations in high-dimensional state spaces using polynomial manifold constructions}, + year = {2023}, + volume = {}, + number = {}, + pages = {4960-4965}, + doi = {10.1109/CDC49753.2023.10384209}, +} + +@article{gruber2023hamiltonian, + title = {Canonical and noncanonical {H}amiltonian operator inference}, + author = {Anthony Gruber and Irina Tezaur}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {416}, + year = {2023}, + doi = {10.1016/j.cma.2023.116334}, +} + +@article{issan2023shifted, + title = {Predicting solar wind streams from the inner-heliosphere to Earth via shifted operator inference}, + author = {Opal Issan and Boris Kramer}, + journal = {Journal of Computational Physics}, + volume = {473}, + pages = {111689}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.jcp.2022.111689}, +} + +@article{mcquarrie2023parametric, + title = {Nonintrusive reduced-order models for parametric partial differential equations via data-driven operator inference}, + author = {Shane A McQuarrie and Parisa Khodabakhshi and Karen Willcox}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1917-A1946}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1452810}, +} + +@phdthesis{mcquarrie2023thesis, + title = {Data-driven parametric reduced-order models: {O}perator inference for reactive flow applications}, + author = {Shane Alexander McQuarrie}, + school = {The University of Texas at Austin}, + year = {2023}, + doi = {10.26153/tsw/50172}, +} + +@article{rocha2023convection, + title = {Reduced-order modeling of the two-dimensional {R}ayleigh--{B}\'{e}nard convection flow through a non-intrusive operator inference}, + author = {Pedro Roberto Barbosa Rocha and Jo\~{a}o Lucas de Sousa Almeida and Marcos Sebasti\~{a}o de Paula Gomes and Alberto Costa Nogueira Jr}, + journal = {Engineering Applications of Artificial Intelligence}, + volume = {126}, + pages = {106923}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.engappai.2023.106923}, +} + +@article{sawant2023pireg, + title = {Physics-informed regularization and structure preservation for learning stable reduced models from data with operator inference}, + author = {Nihar Sawant and Boris Kramer and Benjamin Peherstorfer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {404}, + pages = {115836}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2022.115836}, +} + +@phdthesis{sawant2023thesis, + title = {Learning structured and stable reduced models from data with operator inference}, + author = {Nihar Sawant}, + year = {2023}, + school = {New York University}, +} + +@article{uy2023active, + title = {Active operator inference for learning low-dimensional dynamical-system models from noisy data}, + author = {Wayne Isaac Tan Uy and Yuepeng Wang and Yuxiao Wen and Benjamin Peherstorfer}, + journal = {SIAM Journal on Scientific Computing}, + volume = {45}, + issue = {4}, + pages = {A1462-a1490}, + year = {2023}, + publisher = {SIAM}, + doi = {10.1137/21M1439729}, +} + +@article{uy2023rollouts, + title = {Operator inference with roll outs for learning reduced models from scarce and low-quality data}, + author = {Wayne Isaac Tan Uy and Dirk Hartmann and Benjamin Peherstorfer}, + journal = {Computers \& Mathematics with Applications}, + volume = {145}, + pages = {224--239}, + year = {2023}, + publisher = {Elsevier}, + doi = {10.1016/j.camwa.2023.06.012}, +} + +@inproceedings{zastrow2023flutter, + title = {Data-driven model reduction via operator inference for coupled aeroelastic flutter}, + author = {Benjamin G Zastrow and Anirban Chaudhuri and Karen Willcox and Anthony S Ashley and Michael C Henson}, + booktitle = {AIAA SciTech 2023 Forum}, + pages = {0330}, + year = {2023}, + doi = {10.2514/6.2023-0330}, +} + +@inproceedings{aretz2024enforcing, + title = {Enforcing structure in data-driven reduced modeling through nested {O}perator {I}nference}, + author = {Nicole Aretz and Karen Willcox}, + booktitle = {63rd IEEE Conference on Decision and Control (CDC)}, + year = {2024}, + organization = {IEEE}, + doi = {10.1109/CDC56724.2024.10885857}, +} + +@article{boef2024stablesparse, + title = {Stable sparse operator inference for nonlinear structural dynamics}, + author = {Pascal {den Boef} and Diana Manvelyan and Joseph Maubach and Wil Schilders and Nathan {van de Wouw}}, + journal = {arXiv}, + volume = {2407.21672}, + year = {2024}, + doi = {10.48550/arXiv.2407.21672}, +} + +@article{bychkov2024quadratization, + title = {Exact and optimal quadratization of nonlinear finite-dimensional non-autonomous dynamical systems}, + author = {Andrey Bychkov and Opal Issan and Gleb Pogudin and Boris Kramer}, + journal = {SIAM Journal of Applied Dynamical Systems}, + volume = {23}, + number = {1}, + pages = {982-1016}, + year = {2024}, + doi = {10.1137/23M1561129}, +} + +@article{farcas2024domaindecomposition, + title = {Domain decomposition for data-driven reduced modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {AIAA Journal}, + volume = {62}, + number = {11}, + pages = {4071-4086}, + year = {2024}, + doi = {10.2514/1.J063715}, +} + +@misc{freitag2024stochastic, + title = {Learning stochastic reduced models from data: a nonintrusive approach}, + author = {Melina A. Freitag and Jan Martin Nicolaus and Martin Redmann}, + journal = {arXiv}, + volume = {2407.05724}, + year = {2024}, + doi = {10.48550/arXiv.2407.05724}, +} + +@article{geelen2024nonlinmanifold, + title = {Learning physics-based reduced-order models from data using nonlinear manifolds}, + author = {Rudy Geelen and Laura Balzano and Stephen Wright and Karen Willcox}, + journal = {Chaos: An Interdisciplinary Journal of Nonlinear Science}, + volume = {34}, + number = {3}, + pages = {033122}, + year = {2024}, + doi = {10.1063/5.0170105}, +} + +@article{geng2024gradient, + title = {Gradient preserving {O}perator {I}nference: {D}ata-driven reduced-order models for equations with gradient structure}, + author = {Yuwei Geng and Jasdeep Singh and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {427}, + pages = {117033}, + year = {2024}, + doi = {10.1016/j.cma.2024.117033}, +} + +@inproceedings{koike2024energy, + title = {Energy-preserving reduced operator inference for efficient design and control}, + author = {Tomoki Koike and Elizabeth Qian}, + booktitle = {AIAA SciTech 2024 Forum}, + pages = {1012}, + year = {2024}, + doi = {10.2514/6.2024-1012}, +} + +@article{kramer2024survey, + title = {Learning nonlinear reduced models from data with operator inference}, + author = {Boris Kramer and Benjamin Peherstorfer and Karen Willcox}, + journal = {Annual Review of Fluid Mechanics}, + volume = {56}, + pages = {521--548}, + year = {2024}, + publisher = {Annual Reviews}, + doi = {10.1146/annurev-fluid-121021-025220}, +} + +@article{sharma2024lagrangian, + title = {Lagrangian operator inference enhanced with structure-preserving machine learning for nonintrusive model reduction of mechanical systems}, + author = {Harsh Sharma and David A Najera-Flores and Michael D Todd and Boris Kramer}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {423}, + pages = {116865}, + year = {2024}, + publisher = {Elsevier}, + doi = {10.1016/j.cma.2024.116865}, +} + +@article{sharma2024preserving, + title = {Preserving {L}agrangian structure in data-driven reduced-order modeling of large-scale mechanical systems}, + author = {Harsh Sharma and Boris Kramer}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {462}, + pages = {134128}, + year = {2024}, + doi = {10.1016/j.physd.2024.134128}, +} + +@article{aretz2025nestedopinf, + title = {Nested operator inference for adaptive data-driven learning of reduced-order models}, + author = {Nicole Aretz and Karen Willcox}, + journal = {arXiv}, + volume = {2508.11542}, + year = {2025}, + doi = {10.48550/arXiv.2508.11542}, +} + +@inbook{blonigan2025thermal, + title = {Operator inference-based model order reduction of thermal protection system finite element simulations}, + author = {Patrick J. Blonigan and John Tencer and Sean Babiniec and Jonathan Murray}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {2133}, + year = {2025}, + doi = {10.2514/6.2025-2133}, +} + +@inbook{farcas2025parallel, + title = {A parallel implementation of reduced-order modeling of large-scale systems}, + author = {Ionut-Gabriel Farcas and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + booktitle = {AIAA SciTech 2025 Forum}, + pages = {1170}, + year = {2025}, + doi = {10.2514/6.2025-1170}, +} + +@article{farcas2025distributed, + title = {Distributed computing for physics-based data-driven reduced modeling at scale: {A}pplication to a rotating detonation rocket engine}, + author = {Ionut-Gabriel Farcaş and Rayomand P. Gundevia and Ramakanth Munipalli and Karen E. Willcox}, + journal = {Computer Physics Communications}, + volume = {313}, + pages = {109619}, + year = {2025}, + doi = {10.1016/j.cpc.2025.109619}, +} + +@article{geng2025porthamiltonian, + title = {Data-driven reduced-order models for port-{H}amiltonian systems with {O}perator {I}nference}, + author = {Yuwei Geng and Lili Ju and Boris Kramer and Zhu Wang}, + journal = {arXiv}, + volume = {2501.02183}, + year = {2025}, + doi = {10.48550/arXiv.2501.02183}, +} + +@article{gkimisis2025spatiallylocal, + title = {Non-intrusive reduced-order modeling for dynamical systems with spatially localized features}, + author = {Leonidas Gkimisis and Nicole Aretz and Marco Tezzele and Thomas Richter and Peter Benner and Karen E. Willcox}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {444}, + pages = {118115}, + year = {2025}, + doi = {10.1016/j.cma.2025.118115}, +} + +@article{gruber2025variational, + author = {Anthony Gruber and Irina Tezaur}, + title = {Variationally consistent {H}amiltonian model reduction}, + journal = {SIAM Journal on Applied Dynamical Systems}, + volume = {24}, + number = {1}, + pages = {376-414}, + year = {2025}, + doi = {10.1137/24M1652490}, +} + +@article{kang2025semiconductor, + title = {Parametric {O}perator {I}nference to simulate the purging process in semiconductor manufacturing}, + author = {Seunghyon Kang and Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2504.03990}, + year = {2025}, + doi = {10.48550/arXiv.2504.03990}, +} + +@article{kim2025stateconstraints, + title = {Physically consistent predictive reduced-order modeling by enhancing {O}perator {I}nference with state constraints}, + author = {Hyeonghun Kim and Boris Kramer}, + journal = {arXiv}, + volume = {2502.03672}, + year = {2025}, + doi = {10.48550/arXiv.2502.03672}, +} + +@article{mcquarrie2025gpbayesopinf, + title = {Bayesian learning with {G}aussian processes for low-dimensional representations of time-dependent nonlinear systems}, + author = {Shane A. McQuarrie and Anirban Chaudhuri and Karen E. Willcox and Mengwu Guo}, + journal = {Physica D: Nonlinear Phenomena}, + volume = {475}, + pages = {134572}, + year = {2025}, + doi = {10.1016/j.physd.2025.134572}, +} + +@article{rosenberger2025exactopinf, + title = {Exact operator inference with minimal data}, + author = {Henrik Rosenberger and Benjamin Sanderse and Giovanni Stabile}, + journal = {arXiv}, + volume = {2506.01244}, + year = {2025}, + doi = {10.48550/arXiv.2506.01244}, +} + +@article{vijaywargiya2025tensoropinf, + title = {Tensor parametric {H}amiltonian operator inference}, + author = {Arjun Vijaywargiya and Shane A. McQuarrie and Anthony Gruber}, + journal = {arXiv}, + volume = {2502.10888}, + year = {2025}, + doi = {10.48550/arXiv.2502.10888}, +} + +``` +::: diff --git a/sphinx-docs/source/references.bib b/sphinx-docs/source/references.bib new file mode 100644 index 00000000..b64398cc --- /dev/null +++ b/sphinx-docs/source/references.bib @@ -0,0 +1,36 @@ +# references.bib: citations NOT already included in literature.bib + +@article{benner2015pmorsurvey, + title = {A survey of projection-based model reduction methods for parametric dynamical systems}, + author = {Peter Benner and Serkan Gugercin and Karen Willcox}, + journal = {SIAM Review}, + volume = {57}, + number = {4}, + pages = {483--531}, + year = {2015}, + publisher = {SIAM}, + doi = {10.1137/130932715} +} + +@article{halko2011rnla, + title = {Finding structure with randomness: {P}robabilistic algorithms for constructing approximate matrix decompositions}, + author = {Nathan Halko and Per-Gunnar Martinsson and Joel A Tropp}, + journal = {SIAM Review}, + volume = {53}, + number = {2}, + pages = {217--288}, + year = {2011}, + publisher = {SIAM}, + doi = {10.1137/090771806} +} + +@article{swischuk2019physicsml, + title = {Projection-based model reduction: {F}ormulations for physics-based machine learning}, + author = {Renee Swischuk and Laura Mainini and Benjamin Peherstorfer and Karen Willcox}, + journal = {Computers \& Fluids}, + volume = {179}, + pages = {704--717}, + year = {2019}, + publisher = {Elsevier}, + doi = {10.1016/j.compfluid.2018.07.021} +} diff --git a/sphinx-docs/source/tutorials/basics.md b/sphinx-docs/source/tutorials/basics.md new file mode 100644 index 00000000..331c9b84 --- /dev/null +++ b/sphinx-docs/source/tutorials/basics.md @@ -0,0 +1,992 @@ +--- +kernelspec: + display_name: .venv + language: python + name: python3 +file_format: mystnb +--- + ++++ {"toc-hr-collapsed": false} + +# Getting Started + ++++ + +$$\def\RR{\mathbb{R}} +\def\NN{\mathbb{N}} +\def\ZZ{\mathbb{Z}} +\def\I{\mathbf{I}} +\def\0{\mathbf{0}} +\def\1{\mathbf{1}} +\def\q{\mathbf{q}} +\def\u{\mathbf{u}} +\def\z{\mathbf{z}} +\def\d{\mathbf{d}} +\def\f{\mathbf{f}} +\def\s{\mathbf{s}} +\def\Q{\mathbf{Q}} +\def\U{\mathbf{U}} +\def\Z{\mathbf{Z}} +\def\Op{\mathbf{f}} +\def\Ophat{\hat{\mathbf{f}}} +\def\c{\mathbf{c}} +\def\A{\mathbf{A}} +\def\H{\mathbf{H}} +\def\G{\mathbf{G}} +\def\B{\mathbf{B}} +\def\N{\mathbf{N}} +\def\v{\mathbf{v}} +\def\w{\mathbf{w}} +\def\V{\mathbf{V}} +\def\W{\mathbf{W}} +\def\Vr{\mathbf{V}_{\!r}} +\def\Wr{\mathbf{W}_{\!r}} +\def\qhat{\hat{\mathbf{q}}} +\def\zhat{\hat{\mathbf{z}}} +\def\fhat{\hat{\mathbf{f}}} +\def\Qhat{\hat{\mathbf{Q}}} +\def\Zhat{\hat{\mathbf{Z}}} +\def\chat{\hat{\mathbf{c}}} +\def\Ahat{\hat{\mathbf{A}}} +\def\Hhat{\hat{\mathbf{H}}} +\def\Ghat{\hat{\mathbf{G}}} +\def\Bhat{\hat{\mathbf{B}}} +\def\Nhat{\hat{\mathbf{N}}} +\def\D{\mathbf{D}} +\def\ohat{\hat{\mathbf{o}}} +\def\Ohat{\hat{\mathbf{O}}} +\def\bfmu{\boldsymbol{\mu}} +\def\bfGamma{\boldsymbol{\Gamma}} +\def\bfPhi{\boldsymbol{\Phi}} +\def\bfSigma{\boldsymbol{\Sigma}} +\def\bfPsi{\boldsymbol{\Psi}} +\def\bfLambda{\boldsymbol{\Lambda}} +\def\bfxi{\boldsymbol{\xi}} +\def\trp{{^{\mathsf{T}}}} +\def\ddt{\frac{\textrm{d}}{\textrm{d}t}} +\def\ddqhat{\frac{\partial}{\partial\qhat}} +\def\mean{\operatorname{mean}} +\def\std{\operatorname{std}} +\def\argmin{\operatorname{argmin}}$$ + ++++ {"toc-hr-collapsed": false} + +The `opinf` package constructs reduced-order models for large dynamical systems. +Such systems often arise from the numerical solution of partial differentials equations. +In this introductory tutorial, we use operator inference (OpInf) to learn a reduced-order model for a simple heat equation. +This is a simplified version of the first numerical example in {cite}`peherstorfer2016opinf`. + ++++ {"toc-hr-collapsed": true, "toc-nb-collapsed": true} + +## Problem Statement + ++++ + +:::{admonition} Governing Equations +:class: note + +For the spatial domain $\Omega = [0,L]\subset \RR$ and the time domain $[t_0,t_f]\subset\RR$, consider the one-dimensional heat equation with homogeneous Dirichlet boundary conditions: + +$$ +\begin{aligned} + &\frac{\partial}{\partial t} q(x,t) = \frac{\partial^2}{\partial x^2}q(x,t) + & x &\in\Omega,\quad t\in(t_0,t_f], + \\ + &q(0,t) = q(L,t) = 0 + & t &\in [t_0,t_f], + \\ + &q(x,t_0) = q_{0}(x) + & x &\in \Omega. +\end{aligned} +$$ + +This is a model for a one-dimensional rod that conducts heat. +The unknown state variable $q(x,t)$ represents the temperature of the rod at location $x$ and time $t$; the temperature at the ends of the rod are fixed at $0$ and heat is allowed to flow out of the rod at the ends. +::: + +:::{admonition} Objective +:class: note + +Construct a low-dimensional system of ordinary differential equations, called the _reduced-order model_ (ROM), which can be solved rapidly to produce approximate solutions $q(x, t)$ to the partial differential equation given above. We will use OpInf to learn the ROM from high-fidelity data for one choice of initial condition $q_0(x)$ and test its performance on new initial conditions. +::: + ++++ + +We will make use of {mod}`numpy`, {mod}`scipy`, and {mod}`matplotlib` from the standard Python scientific stack, which are all automatically installed when `opinf` is [installed](../opinf/installation.md). +The {mod}`pandas` library is also used later to consolidate and report results. We also use [`h5py`](https://docs.h5py.org/en/stable/) (which is also automatically installed with `opinf`) to read the stored data. + +```{code-cell} ipython3 +import numpy as np +import pandas as pd +import scipy.sparse +import scipy.integrate +import scipy.linalg as la +import matplotlib.pyplot as plt +import h5py + +import opinf + +opinf.utils.mpl_config() +``` + +## Training Data + ++++ + +The data used in this tutorial was generated through a typical numerical method. +A spatial discretization of the governing equations with $n$ degrees of freedom via finite differences or the finite element method leads to a linear semi-discrete system of $n$ ordinary differential equations, + +$$ +\begin{aligned} + \ddt\q(t) = \A\q(t), + \qquad + \q(0) = \q_0, +\end{aligned} +$$ (eq_basics_fom) + +where $\q:\RR\to\RR^n$, $\A\in\RR^{n\times n}$, and $\q_0\in\RR^n$. +For this tutorial, we use central finite differences to construct this system. + ++++ + +:::{dropdown} Discretization details + +For a given $n\in\NN$, let $\{x\}_{i=0}^{n+1}$ be an equidistant grid of $n+2$ points on $\Omega$, i.e., + +$$ +\begin{aligned} + 0 &= x_0 < x_1 < \cdots < x_n < x_{n+1} = L + & + &\text{and} + & + x_{i+1} - x_{i} &= \delta x := \frac{L}{n+1},\quad i=1,\ldots,n-1. +\end{aligned} +$$ + +The boundary conditions prescribe $q(x_0,t) = q(x_{n+1},t) = 0$. +Our goal is to compute $q(x, t)$ at the interior spatial points $x_{1}, x_{2}, \ldots, x_{n}$ for various $t \in [t_0,t_f].$ That is, we wish to compute the state vector + +$$ +\begin{aligned} + \q(t) + = \left[\begin{array}{c} + q(x_1,t) \\ \vdots \\ q(x_n,t) + \end{array}\right]\in\RR^n. +\end{aligned} +$$ + +Introducing a central finite difference approximation for the spatial derivative, + +$$ +\begin{aligned} + \frac{\partial^2}{\partial x^2}q(x,t) + &\approx \frac{q(x-\delta x,t) - 2q(x,t) + q(x+\delta x,t)}{(\delta x)^2}, +\end{aligned} +$$ + +yields the semi-discrete linear system + +$$ +\begin{aligned} + \ddt\q(t) = \A\q(t), + \qquad + \q(0) = \q_0, +\end{aligned} +$$ + +where + +$$ +\begin{aligned} + \A &= \frac{1}{(\delta x)^2}\left[\begin{array}{ccccc} + -2 & 1 & & & \\ + 1 & -2 & 1 & & \\ + & \ddots & \ddots & \ddots & \\ + & & 1 & -2 & 1 \\ + & & & 1 & -2 \\ + \end{array}\right] \in\RR^{n\times n}, + & + \q_0 &= \left[\begin{array}{c} + q_{0}(x_{1}) \\ q_{0}(x_{2}) \\ \vdots \\ q_{0}(x_{n-1}) \\ q_{0}(x_{n}) + \end{array}\right] \in\RR^{n}. +\end{aligned} +$$ + +::: + ++++ + +The system {eq}`eq_basics_fom` is called the _full-order model_ (FOM) or the _high-fidelity model_. The computational complexity of solving {eq}`eq_basics_fom` depends on the dimension $n$, which must often be large in order for $\q(t)$ to approximate $q(x,t)$ well over the spatial grid. Our goal is to construct a ROM that approximates the FOM, but whose computational complexity only depends on some smaller dimension $r \ll n$. + ++++ + +:::{admonition} No FOM? No Problem. +:class: important + +One key advantage of OpInf is that, because it learns a ROM from data alone, direct access to a FOM is not required. +We only need the following: +1. Solution data to learn from, and +2. Some knowledge of the structure of the governing equations. +::: + ++++ {"toc-hr-collapsed": true} + +For this demo, we load data from a different file that represents solutions to the system of equations with the initial condition + +$$ +\begin{aligned} + q_{0}(x) = x(1 - x), +\end{aligned} +$$ + +and with data recorded every $\delta t = 0.0025$ time units. +This results in $k = 401$ state snapshots ($400$ time steps after the initial condition), which are organized into the _snapshot matrix_ $\Q\in\RR^{n\times k}$, where the $j$-th column is the solution trajectory at time $t_j$: + +$$ +\begin{aligned} + \Q = \left[\begin{array}{ccc} + && \\ + \q_{0} & \cdots & \q_{k-1} + \\ && + \end{array}\right] \in\RR^{n\times k}, + \qquad + \q_{j} := \q(t_j) \in\RR^{n},\quad j = 0, \ldots, k-1. +\end{aligned} +$$ + +Note that the initial condition $\q_{0}$ is included as a column in the snapshot matrix. + ++++ + +## Load Data + ++++ + +:::{button-link} https://github.com/operator-inference/opinf/raw/refs/heads/data/basics_data.h5 +:color: success +:outline: +To download the data, click here. +::: + ++++ + +Once the data has been downloaded using the above button and placed in the same directory as the notebook for this tutorial, we can import the saved data using the `h5py` module. Note that the variable `t` represents the temporal dimension, the variable `Q` holds the snapshot data ($\mathbf{Q}$), and the variable `x` (derived from `Q`), represents the spatial dimension. For convenience, we also define the initial conditions, $q_0$, as `q0`. + +```{code-cell} ipython3 +filepath = "basics_data.h5" + +with h5py.File(filepath, 'r') as h5file: + t = h5file["t"][:] + Q = h5file["default"][:] + +x_all = np.linspace(0, 1, Q.shape[0]+2) +x = x_all[1:-1] +dt = t[1] - t[0] +q0 = Q[:, 0] +``` + +Next, we visualize the data to see if it looks like we expect. + +```{code-cell} ipython3 +def plot_heat_data(Z, title, ax=None): + """Visualize temperature data in space and time.""" + if ax is None: + _, ax = plt.subplots(1, 1) + + # Plot a few snapshots over the spatial domain. + sample_columns = [0, 2, 5, 10, 20, 40, 80, 160, 320] + color = iter(plt.cm.viridis_r(np.linspace(0.05, 1, len(sample_columns)))) + + leftBC, rightBC = [0], [0] + for j in sample_columns: + q_all = np.concatenate([leftBC, Z[:, j], rightBC]) + ax.plot(x_all, q_all, color=next(color), label=rf"$q(x,t_{{{j}}})$") + + ax.set_xlim(x_all[0], x_all[-1]) + ax.set_xlabel(r"$x$") + ax.set_ylabel(r"$q(x,t)$") + ax.legend(loc=(1.05, 0.05)) + ax.set_title(title) + +plot_heat_data(Q, "snapshot data") +``` + +## Operator Inference + ++++ + +At this point, we have loaded and visualized some training data. +We also have an initial condition and a time domain. + +| Name | Symbol | Code Variable | +| :--- | :----: | :------------ | +| State snapshots | $\Q$ | `Q` | +| Initial state | $\q_0$ | `q0` | +| Time domain | $[t_0,t_f]$ | `t` | + +Our task now is to construct a low-dimensional system whose solutions can be used as approximate solutions to the PDE. +Below we show the overall process, then explain each piece that happens under the hood. + +```{code-cell} ipython3 +import opinf + +# Define the reduced-order model. +rom = opinf.ROM( + basis=opinf.basis.PODBasis(cumulative_energy=0.9999), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t, "ord6"), + model=opinf.models.ContinuousModel( + operators="A", + solver=opinf.lstsq.L2Solver(regularizer=1e-8), + ), +) + +# Calibrate the reduced-order model to data. +rom.fit(Q) + +# Solve the reduced-order model. +with opinf.utils.TimedBlock("Reduced-order solve"): + Q_ROM = rom.predict(q0, t, method="BDF", max_step=dt) + +# Compute the relative error of the ROM solution. +opinf.post.frobenius_error(Q, Q_ROM)[1] +``` + +### Data Compression + ++++ + +Our first task is to construct a low-dimensional representation of the FOM state $\q(t)\in\RR^{n}$, denoted $\qhat(t)\in\RR^r$. +A ROM is a system of equations that acts on the reduced state $\qhat(t)$. +The integer $r$ is the dimension of the ROM: if $r \ll n$, we can expect to be able to solve the ROM much faster than we can solve the FOM. + +The relationship between $\q(t)$ and $\qhat(t)$ helps dictate the structure of the ROM and allows us to compress the state snapshots $\q_0,\ldots,\q_{k-1}\in\RR^{n}$ to low-dimensional representations $\qhat_0,\ldots,\qhat_{k-1}\in\RR^{r}$ that are used to calibrate the ROM. +Tools for defining low-dimensional approximations of high-dimensional states are defined in {mod}`opinf.basis`. + +For this problem, we use a linear approximation for $\q(t)$: + +$$ +\begin{aligned} + \q(t) + \approx \Vr\qhat(t). +\end{aligned} +$$ + +The matrix $\Vr\in\RR^{n\times r}$ is called a _basis matrix_ and its columns are called _basis vectors_. +We typically have $\Vr\trp\Vr = \I$, i.e., the basis vectors form an orthonormal set. +Note that the product $\Vr\qhat(t)$ is a linear combination of the basis vectors, so $\q(t)$ can only be approximated well if it is within or near the span of the basis vectors. + +We choose $\Vr$ using proper orthogonal decomposition (POD), which is based on the singular value decomposition (SVD) of samples of $\q(t)$. +The singular values give some guidance on choosing an appropriate ROM dimension $r$. +Fast singular value decay is a good sign that a ROM may be successful with this kind of data; if the singular values do not decay quickly, then a large $r$ may be required to capture the behavior of the system. +Below, we initialize a {class}`opinf.basis.PODBasis` object with the following criteria for selecting $r$: choose the smallest $r$ such that we capture over $99.9999\%$ of the [cumulative energy](#sec:api-basis-dimselect) of the system. + +```{code-cell} ipython3 +# Initialize a basis. +basis = opinf.basis.PODBasis(cumulative_energy=0.9999) + +# Fit the basis (compute Vr) using the snapshot data. +basis.fit(Q) +print(basis) + +# Visualize the basis vectors. +basis.plot1D(np.linspace(0, 1, Q.shape[0])) +plt.show() +``` + +Solutions of our eventual ROM are restricted to linear combinations of these two basis vectors. + +After the basis is initialized and calibrated, we can use it to compress the state snapshots to an $r$-dimensional representation. +In this case, we have $\qhat_j = \Vr\trp\q_j \in \RR^{r}$. +These $\qhat_j$ are data for the ROM state $\qhat(t)$ at time $t_j$. + +```{code-cell} ipython3 +# Compress the state snapshots to the reduced space defined by the basis. +Q_ = basis.compress(Q) + +print(f"{Q.shape=}, {Q_.shape=}") +``` + +To see how well the state can be represented by a given basis matrix, it is helpful to examine the _projection_ of the state snapshots. +For linear state approximations like POD, the projection of $\q\in\RR^n$ is the vector $\Vr\Vr\trp\q\in\RR^n$. + +```{code-cell} ipython3 +basis.projection_error(Q) +``` + +### Time Derivative Estimation + ++++ + +In addition to the compressed state snapshots $\qhat_0,\ldots,\qhat_{k-1}$, OpInf for time-continuous (ODE) models requires data for the time derivatives of the state snapshots, denoted + +$$ +\begin{aligned} + \dot{\qhat}_j + = \ddt\qhat(t)\big|_{t=t_j} + \in\RR^{r}. +\end{aligned} +$$ + +There are two ways to get such data. + +1. If time derivatives of the original state snapshots are available, they can be compressed to the reduced state space. +2. Otherwise, the time derivatives may be estimated from the compressed states, as demonstrated in this tutorial. + +The {mod}`opinf.ddt` module defines tools for estimating time derivatives from state data. + ++++ + +Since the data $\q_0,\ldots,\q_{k-1}$ are defined on a uniform time grid, we use {class}`opinf.ddt.UniformFiniteDifferencer` to calculate estimate $\dot{\mathbf{\qhat}}$. + +```{code-cell} ipython3 +# Estimate time derivatives using 6th-order finite differences. +ddt_estimator = opinf.ddt.UniformFiniteDifferencer(t, "ord6") +Qdot_ = ddt_estimator.estimate(Q_)[1] + +print(Qdot_.shape) +``` + +### Specifying the Model Operators + ++++ + +We now have low-dimensional state and time derivative data. +To learn a ROM with OpInf, we must specify the structure of the ROM, which should be motivated by the structure of the FOM and the dimensionality reduction strategy. + +The FOM {eq}`eq_basics_fom` is a linear system of ODEs, + +$$ +\begin{aligned} + \ddt\q(t) = \A\q(t), + \qquad + \q(0) = \q_0. +\end{aligned} +$$ + +Substituting in the approximation $\q(t)\approx\Vr\qhat(t)$, we have + +$$ +\begin{aligned} + \ddt\Vr\qhat(t) = \A\Vr\qhat(t), + \qquad + \Vr\qhat(0) = \q_0. +\end{aligned} +$$ + +Next, left multiply by $\Vr\trp$ and use the fact that $\Vr\trp\Vr = \I$ to get the following: + +$$ +\begin{aligned} + \ddt\qhat(t) = \tilde{\A}\qhat(t), + \qquad + \qhat(0) = \Vr\trp\q_0, +\end{aligned} +$$ (eq_basics_intrusiverom) + +where $\tilde{\A} = \Vr\trp\A\Vr \in \RR^{r\times r}$. +The system {eq}`eq_basics_intrusiverom` is called the _intrusive Galerkin ROM_ corresponding to the FOM and the choice of basis matrix $\Vr$. +The intrusive ROM can only be constructed if $\A$ is known; with OpInf, we aim to construct a reduced system with the same linear structure as the intrusive ROM, but without using $\A$ explicitly: + +$$ +\begin{aligned} + \ddt\qhat(t) = \Ahat\qhat(t), + \qquad + \qhat(0) = \Vr\trp\q_0, +\end{aligned} +$$ + +for some $\Ahat\in\RR^{r\times r}$ inferred from the training data. +We specify this linear structure by initializing an {class}`opinf.models.ContinuousModel` with the string `"A"`. + +```{code-cell} ipython3 +model = opinf.models.ContinuousModel("A") +print(model) +``` + +:::{admonition} Model Constructor Shortcut +:class: tip + +The `"A"` argument in the constructor is a shortcut for a slightly longer statement: + +```python +>>> model = opinf.models.ContinuousModel([opinf.operators.LinearOperator()]) +``` + +The {class}`opinf.operators.LinearOperator` class represents the $r \times r$ matrix $\Ahat$, whose entries will be calibrated via regression. +See {mod}`opinf.operators` for the kinds of terms that OpInf ROMs can contain. +::: + ++++ + +### Calibrating Model Operators + ++++ + +Our task now is to learn the entries of $\Ahat$ using the compressed state snapshots $\qhat_0,\ldots,\qhat_{k-1}$ and the corresponding time derivatives $\dot{\qhat}_0,\ldots,\dot{\qhat}_{k-1}$. +OpInf does this through minimizing the residual of the model equation with respect to the data: + +$$ +\begin{aligned} + \min_{\Ahat\in\RR^{r\times r}} + \sum_{j=0}^{k-1}\left\| + \Ahat\qhat_{j} - \dot{\qhat}_{j} + \right\|_{2}^2 + + \mathcal{R}(\Ahat), +\end{aligned} +$$ (eq_basics_opinf) + +where $\mathcal{R}(\Ahat)$ is a regularization term (more on this later). + +The {mod}`opinf.lstsq` module defines tools for solving this problem (or variations on it). +By default, the regression is solved without regularization, i.e., $\mathcal{R}(\Ahat) = 0$. +The following code initializes the model and calibrates the operators. + +```{code-cell} ipython3 +model.fit(states=Q_, ddts=Qdot_) +print(model) +``` + +### Regularization: Stabilizing the Inference Problem + ++++ + + +Ill-conditioning in the data, errors in the estimation of the time derivatives, or overfitting to the data can result in an $\Ahat$ that defines an inaccurate or even unstable ROM. +Introducing a regularization term promotes solutions that respect both the training data and the physics of the problem. +One common option is [Tikhonov regularization](https://en.wikipedia.org/wiki/Tikhonov_regularization), which sets $\mathcal{R}(\Ahat) = \|\lambda\Ahat\|_{F}^{2}$ to penalize the entries of the learned operators. + +```{code-cell} ipython3 +# Define a solver for the Tikhonov-regularized least-squares problem. +model = opinf.models.ContinuousModel( + "A", + solver=opinf.lstsq.L2Solver(regularizer=1e-2), +) + +# Construct the OpInf ROM through regularized least squares. +model.fit(states=Q_, ddts=Qdot_) +A_opinf = model.operators[0].entries +``` + +With inexact time derivatives or regularization, OpInf differs slightly from the intrusive operator $\tilde{\A}$. +However, we will see that in this example the ROM produced by OpInf is highly accurate. +In fact, it is sometimes the case that OpInf outperforms intrusive Galerkin projection. + ++++ + +:::{admonition} Regularization Matters +:class: important + +Regularization is important in all but the simplest OpInf problems. +If OpInf produces an unstable ROM, try different values for the `regularizer`. +See {cite}`mcquarrie2021combustion` for an example of a principled choice of regularization for a combustion problem. +::: + ++++ + +### Solving the Reduced-order Model + ++++ + +Once the model is calibrated, we may solve the ROM with {meth}`opinf.models.ContinuousModel.predict`, which wraps {func}`scipy.integrate.solve_ivp()`. This method takes an initial condition for the model $\qhat_0 = \Vr\trp\q_0$, the time domain over which to record the solution, and any additional arguments for the integrator. + +```{code-cell} ipython3 +q0_ = basis.compress(q0) # Compress the initial conditions. + +model = opinf.models.ContinuousModel( + "A", + solver=opinf.lstsq.L2Solver(regularizer=1e-8), +).fit(Q_, Qdot_) + +Q_ROM_ = model.predict(q0_, t, method="BDF") + +print(f"{Q_ROM_.shape=}") +``` + +The solution is still in the low-dimensional state space; it can be mapped to the original state space by applying $\Vr$. + +```{code-cell} ipython3 +Q_ROM = basis.decompress(Q_ROM_) + +print(f"{Q_ROM.shape=}") +``` + +:::{admonition} Custom ODE Solvers +:class: tip + +{meth}`opinf.models.ContinuousModel.predict` is convenient, but {func}`scipy.integrate.solve_ivp()` implements a limited repertoire of time integration schemes. +However, the ROM can be simulated by any ODE solver scheme by extracting the inferred operator $\Ahat$. +If `timestepper(A, q0)` were a solver for systems of the form $\ddt\qhat = \Ahat\qhat(t),\ \qhat(0) = \qhat_0$, we could simulate the ROM with the following code. + +```python +q0_ = basis.compress(q0) # Compress the initial conditions. +Q_ROM_ = timestepper(model.A_.entries, q0_) # Solve the ROM in the reduced space. +Q_ROM = basis.decompress(Q_ROM_) # Decompress the ROM solutions. +``` + +More generally, the method {meth}`opinf.models.ContinuousModel.rhs` represents the right-hand side of the model, the $\hat{\mathbf{f}}$ of $\ddt\qhat(t) = \hat{\mathbf{f}}(t, \qhat(t))$. +General-purpose integrators can therefore be applied to the function {meth}`opinf.models.ContinuousModel.rhs`. +::: + ++++ + +### The ROM Class + ++++ + +Up to this point, we have done the following steps using several package submodules. + +1. {mod}`opinf.basis`: Data compression. +2. {mod}`opinf.ddt`: Time derivative estimation. +3. {mod}`opinf.models`: Specify and [calibrate](opinf.lstsq) model [operators](opinf.operators), then integrate the reduced system. + +The [`opinf.ROM`](opinf.roms.ROM) class wraps these steps for convenience. +Its constructor takes a initialized basis, time derivative estimator, and model objects. +Then, [`fit()`](opinf.roms.ROM.fit) calibrates the basis, compresses the state data, estimates the time derivatives, and calibrates the model. +Use [`predict()`](opinf.roms.ROM.predict) to compress initial conditions, solve the model, and express the solutions in the original state space. + +```{code-cell} ipython3 +rom = opinf.ROM( + basis=opinf.basis.PODBasis(cumulative_energy=0.9999), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t, "ord6"), + model=opinf.models.ContinuousModel( + operators="A", + solver=opinf.lstsq.L2Solver(regularizer=1e-8), + ), +) + +print(rom) +``` + +```{code-cell} ipython3 +rom.fit(Q) + +print(rom) +``` + +```{code-cell} ipython3 +Q_ROM_2 = rom.predict(q0, t, method="BDF") + +np.all(Q_ROM_2 == Q_ROM) +``` + +### Evaluate ROM Performance + ++++ + +The quality or usefulness of a ROM depends on its accuracy and its computational efficiency. + ++++ + +#### ROM Accuracy + ++++ + +To get a sense of how well the ROM approximates the FOM, we begin by visualizing the simulation output `Q_ROM`. +It should look similar to the plot of the snapshot data `Q`. + +```{code-cell} ipython3 +fig, [ax1, ax2] = plt.subplots(1, 2) +plot_heat_data(Q, "Snapshot data", ax1) +plot_heat_data(Q_ROM, "ROM state output", ax2) +ax1.legend([]) +plt.show() +``` + +For more detail, we evaluate the $\ell^2$ error of the ROM output, comparing it to the snapshot set via {func}`opinf.post.lp_error`. +This calculates the absolute and relative error as a function of time, + +$$ +\begin{aligned} + \text{err}_\text{absolute}(t) + &= \|\q(t) - \q_{\text{ROM}}(t)\|_{2}, + \\ ~ \\ + \text{err}_\text{relative}(t) + &= \frac{\|\q(t) - \q_{\text{ROM}}(t)\|_{2}}{\|\q(t)\|_{2}}. +\end{aligned} +$$ + ++++ + +:::{admonition} Normalized Absolute Error +:class: tip + +In this problem, $\q(t)\to\0$ as $t$ increases, so a relative error may not be appropriate since $\|\q(t)\|_{2}$ appears in the denominator. +In situations like this, consider using the _normalized absolute error_ by replacing the denominator with $\max_{\tau\in[t_0,t_f]}\|\q(t)\|.$ +Set `normalize=True` in {func}`opinf.post.lp_error()` to use this error measure instead of the relative error. +::: + +```{code-cell} ipython3 +abs_l2err, norm_l2err = opinf.post.lp_error(Q, Q_ROM, normalize=True) +fig, ax = plt.subplots(1, 1) +ax.semilogy(t, abs_l2err, "-", label=r"Absolute $\ell^2$ error") +ax.semilogy(t, norm_l2err, "--", label=r"Normalized absolute $\ell^2$ error") +ax.set_xlabel(r"$t$") +ax.set_ylabel("error") +ax.legend(loc="lower left") +plt.show() +``` + +In this simple example, the error decreases with time (as solutions get quickly pushed to zero), but this is not the kind of error behavior that should be expected when modeling more complicated phenomena. + +We can also get a scalar error measurement by calculating the relative Frobenius norm error with {func}`opinf.post.frobenius_error`. + +```{code-cell} ipython3 +abs_froerr, rel_froerr = opinf.post.frobenius_error(Q, Q_ROM) +print(f"Relative Frobenius-norm error: {rel_froerr:%}") +``` + +In other words, the ROM simulation is within about 0.1% of the snapshot data. +Note that this value is very close to the projection error that we calculated earlier. + ++++ + +#### ROM Computational Speedup + ++++ + +When a FOM is available, a ROM is only useful if it can be solved much faster than the FOM. +The solution speed can be quickly checked using {class}`opinf.utils.TimedBlock`. + +```{code-cell} ipython3 +with opinf.utils.TimedBlock("Reduced-order solve"): + rom.predict(q0, t, method="BDF") +``` + +More precise measurements can be take by aliasing the {class}`opinf.utils.TimedBlock` and accessing the `elapsed` attribute. +Below, we solve the model several times to get an average time. + +```{code-cell} ipython3 +n_trials = 10 + +with opinf.utils.TimedBlock(f"{n_trials} ROM solves") as romtime: + for _ in range(n_trials): + rom.predict(q0, t, method="BDF") + +print(f"Average ROM time: {romtime.elapsed / n_trials :.6f} s") +``` + +In this example, the FOM is efficient because it takes advantage of the sparsity of $\A\in\RR^{n\times n}$. +Even so, the ROM achieves a modest speedup due to the smaller size of $\Ahat\in\RR^{r\times r}$. Thus, although in a pratical setting, a FOM could still be used with the present data, many modern applications are far too large to be solved with a FOM, making the ROM speedup not only convenient, but necessary. + ++++ + +## Prediction: New Initial Conditions + ++++ {"jp-MarkdownHeadingCollapsed": true} + +The ROM was trained using only data corresponding to the initial condition $q_0(x) = x(1 - x).$ We'll now test the ROM on the following new initial conditions and compare the results to the corresponding data: + +$$ +\begin{aligned} + q_0(x) &= 10x (1 - x), + & + q_0(x) &= 5x^{2}(1 - x)^{2}, + \\ + q_0(x) &= 50x^{4}(1 - x)^{4}, + & + q_0(x) &= \frac{1}{2}\sqrt{x(1 - x)}, + \\ + q_0(x) &= \frac{1}{4}\sqrt[4]{x(1 - x)}, + & + q_0(x) &= \frac{1}{3}\sin(\pi x) + \tfrac{1}{5}\sin(5\pi x). +\end{aligned} +$$ + +Before we compute the ROM error, we also compute the _projection error_ of the new initial condition, + +$$ +\begin{aligned} + \frac{||\q_{0} - \Vr \Vr\trp\q_{0}||_{2}}{||\q_{0}||_{2}}. +\end{aligned} +$$ + +If this projection error is large, then the new initial condition cannot be represented well within the range of $\Vr$. This will be apparent in the ROM solutions. + ++++ + +### First Attempt + +```{code-cell} ipython3 +def test_new_initial_condition(data, q0, rom, label=None): + """Comparereduced-order model solutions with the generated data + for a given initial condition. + + Parameters + ---------- + data : ndarray + Pre-generated data with different initial conditions to be used. + q0 : (n,) ndarray + Heat equation initial conditions q0(x) to be applied. + rom : opinf.ROM + Trained reduced-order model object. + label : str + Description of the initial condition being tested. + """ + # Calculate the projection error of the new initial condition. + rel_projerr = rom.basis.projection_error(q0, relative=True) + + # Solve the full-order model (FOM) and the reduced-order model (ROM). + Q_ROM = rom.predict(q0, t, method="BDF") + + # Plot the FOM and ROM solutions side by side. + fig, [ax1, ax2] = plt.subplots(1, 2) + plot_heat_data(data, "Snapshot data", ax1) + plot_heat_data(Q_ROM, "Reduced-order model solution", ax2) + ax1.legend([]) + if label: + fig.suptitle(label, y=1) + fig.tight_layout() + + # Calculate the ROM error in the Frobenius norm. + abs_froerr, rel_froerr = opinf.post.frobenius_error(data, Q_ROM) + + # Report results. + plt.show() + print( + f"Relative projection error of initial condition: {rel_projerr:.2%}", + f"Relative Frobenius-norm ROM error: {rel_froerr:.2%}", + sep="\n", + ) + return rel_projerr, rel_froerr +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +# load the data for the other initial conditions +new_initial_conditions_data = [] +with h5py.File(filepath, 'r') as f: + experiments = [f"Experiment {i}" for i in range(1, f.attrs['num_experiments']+1)] + for key in experiments: + new_initial_conditions_data.append((f[key].attrs['title'], f[key][:])) + +q0_new = [ + 10 * x * (1 - x), + 5 * x**2 * (1 - x) ** 2, + 50 * x**4 * (1 - x) ** 4, + 0.5 * np.sqrt(x * (1 - x)), + 0.25 * np.sqrt(np.sqrt(x * (1 - x))), + np.sin(np.pi * x) / 3 + np.sin(5 * np.pi * x) / 5, +] + +results = {} + +for key, q00, (title, data) in zip(experiments, q0_new, new_initial_conditions_data): + results[key] = test_new_initial_condition( + data, q00, rom, f"{key}: {title}" + ) + +labels = [ + "Relative projection error of initial condition", + "Relative Frobenius-norm ROM error", +] +pd.DataFrame(results, index=labels).T +``` + +### Second Attempt: a Better Basis + ++++ + +The ROM performs well for $q_{0}(x) = 10x(1 - x)$, which is unsurprising because this new initial condition is a scalar multiple of the initial condition used to generate the training data. +In other cases, the ROM is less successful because the new initial condition cannot be represented well in the span of the basis vectors. +For example: + +```{code-cell} ipython3 +:tags: [hide-input] + +def plot_initial_condition_projection(base): + """Plot initial conditions 4 and 5 and their projections with respect to + the basis `base`. + + Parameters + ---------- + base : opinf.basis.PODBasis + Trained basis object. + """ + fig, axes = plt.subplots(1, 2) + for j, ax in zip([4, 5], axes): + ax.plot( + x, + q0_new[j], + label=r"True initial condition ($\mathbf{q}_{0}$)", + ) + ax.plot( + x, + base.project(q0_new[j]), + "--", + label=r"Basis approximation of initial condition " + r"($\mathbf{V}_{\!r}\mathbf{V}_{\!r}^{\mathsf{T}}\mathbf{q}_{0}$)", + ) + ax.set_title(f"Experiment {j+1:d}") + + fig.tight_layout(rect=[0, 0.15, 1, 1]) + axes[0].legend( + loc="lower center", + fontsize="large", + bbox_to_anchor=(0.5, -0.05), + bbox_transform=fig.transFigure, + ) + plt.show() +``` + +```{code-cell} ipython3 +plot_initial_condition_projection(basis) +``` + +To improve the ROM performace _without getting new data from the FOM_, we will enrich the basis by + +1. Including the new initial conditions in the basis computation, and +2. Using a few more basis vectors (we currently have $r = 2$, let's use $r = 5$). + +```{code-cell} ipython3 +# Include the new initial conditions in the basis training data. +Q_and_new_q0s = np.column_stack((Q, *q0_new)) +newbasis = opinf.basis.PODBasis(num_vectors=5).fit(Q_and_new_q0s) +print(newbasis) + +# Plot the projection of the initial conditions in the new basis +plot_initial_condition_projection(newbasis) +``` + +```{code-cell} ipython3 +# Initialize a ROM with the new basis. +rom = opinf.ROM( + basis=newbasis, + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t, "ord6"), + model=opinf.models.ContinuousModel( + operators="A", + solver=opinf.lstsq.L2Solver(regularizer=1e-8), + ), +) + +# Use the same training data as before, but do not reset the basis. +_ = rom.fit(Q, fit_basis=False) + +print(rom) +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +# Repeat the experiments. +results_new = {} +for i, [q00, (title, data)] in enumerate(zip(q0_new, new_initial_conditions_data)): + results_new[f"Experiment {i+1:d}"] = test_new_initial_condition( + data, + q00, + rom, + f"Experiment {i+1}: {title}", + ) + +# Display results summary. +pd.DataFrame(results_new, index=labels).T +``` + +With a more expressive basis, the ROM performance improves significantly. + ++++ + +:::{admonition} No Better than the Basis +:class: tip +This example illustrates a fundamental principle of model reduction: the accuracy of the ROM is limited by the accuracy of the underlying low-dimensional approximation, which in this case is $\q(t) \approx \Vr\qhat(t)$. In other words, a good $\Vr$ is critical in order for the ROM to be accurate and predictive. +::: \ No newline at end of file diff --git a/sphinx-docs/source/tutorials/inputs.md b/sphinx-docs/source/tutorials/inputs.md new file mode 100644 index 00000000..ede2e793 --- /dev/null +++ b/sphinx-docs/source/tutorials/inputs.md @@ -0,0 +1,672 @@ +--- +kernelspec: + display_name: .venv + language: python + name: python3 +file_format: mystnb +--- + ++++ {"toc-hr-collapsed": false} + +# External Inputs + ++++ + +$$\def\RR{\mathbb{R}} +\def\NN{\mathbb{N}} +\def\ZZ{\mathbb{Z}} +\def\I{\mathbf{I}} +\def\0{\mathbf{0}} +\def\1{\mathbf{1}} +\def\q{\mathbf{q}} +\def\u{\mathbf{u}} +\def\z{\mathbf{z}} +\def\d{\mathbf{d}} +\def\f{\mathbf{f}} +\def\s{\mathbf{s}} +\def\Q{\mathbf{Q}} +\def\U{\mathbf{U}} +\def\Z{\mathbf{Z}} +\def\Op{\mathbf{f}} +\def\Ophat{\hat{\mathbf{f}}} +\def\c{\mathbf{c}} +\def\A{\mathbf{A}} +\def\H{\mathbf{H}} +\def\G{\mathbf{G}} +\def\B{\mathbf{B}} +\def\N{\mathbf{N}} +\def\v{\mathbf{v}} +\def\w{\mathbf{w}} +\def\V{\mathbf{V}} +\def\W{\mathbf{W}} +\def\Vr{\mathbf{V}_{\!r}} +\def\Wr{\mathbf{W}_{\!r}} +\def\qhat{\hat{\mathbf{q}}} +\def\zhat{\hat{\mathbf{z}}} +\def\fhat{\hat{\mathbf{f}}} +\def\Qhat{\hat{\mathbf{Q}}} +\def\Zhat{\hat{\mathbf{Z}}} +\def\chat{\hat{\mathbf{c}}} +\def\Ahat{\hat{\mathbf{A}}} +\def\Hhat{\hat{\mathbf{H}}} +\def\Ghat{\hat{\mathbf{G}}} +\def\Bhat{\hat{\mathbf{B}}} +\def\Nhat{\hat{\mathbf{N}}} +\def\D{\mathbf{D}} +\def\ohat{\hat{\mathbf{o}}} +\def\Ohat{\hat{\mathbf{O}}} +\def\bfmu{\boldsymbol{\mu}} +\def\bfGamma{\boldsymbol{\Gamma}} +\def\bfPhi{\boldsymbol{\Phi}} +\def\bfSigma{\boldsymbol{\Sigma}} +\def\bfPsi{\boldsymbol{\Psi}} +\def\bfLambda{\boldsymbol{\Lambda}} +\def\bfxi{\boldsymbol{\xi}} +\def\trp{{^{\mathsf{T}}}} +\def\ddt{\frac{\textrm{d}}{\textrm{d}t}} +\def\ddqhat{\frac{\partial}{\partial\qhat}} +\def\mean{\operatorname{mean}} +\def\std{\operatorname{std}} +\def\argmin{\operatorname{argmin}}$$ + ++++ {"toc-hr-collapsed": false} + +The fundamental goal of model reduction is to efficiently make physics-based predictions. Given synthetic or experimental data that was generated or collected under a certain set of conditions, we aim to construct a cost-effective model that produces accurate solutions under new sets of conditions. The first tutorial showed an example of evaluating a reduced-order model (ROM) for various initial conditions. This tutorial focuses on problems with external time-dependent inputs. + ++++ {"toc-nb-collapsed": true} + +## Problem Statement + ++++ + +We consider a problem with external inputs that are parameterized by a scalar-valued function $u:\RR\to\RR.$ + ++++ + +:::{admonition} Governing Equations +:class: info + +Let $\Omega = [0,L]\subset \mathbb{R}$ be the spatial domain indicated by the variable $x$, and let $[0,T]\subset\mathbb{R}$ be the time domain with variable $t$. We consider the one-dimensional heat equation with time-dependent Dirichlet boundary conditions, + +$$ +\begin{aligned} + &\frac{\partial}{\partial t} q(x,t) = \frac{\partial^2}{\partial x^2}q(x,t) + & x &\in\Omega,\quad t\in[0,T], + \\ + &q(0,t) = q(L,t) = u(t) + & t &\in[0,T], + \\ + &q(x,0) = \big(e^{\alpha(x - 1)} + e^{-\alpha x} - e^{-\alpha}\big)u(0) + & x &\in \Omega, +\end{aligned} +$$ + +where $\alpha>0$ is constant and $q(x,t)$ is the unknown state variable. This is a model for a one-dimensional rod conducting heat with a fixed initial heat profile. The temperature at the ends of the rod are governed by the input function $u(t)$, but heat is allowed to diffuse through the rod and flow out at the ends of the domain. +::: + ++++ + +:::{admonition} Objective +:class: info + +Construct a reduced-order model (ROM) which can be solved rapidly to produce approximate solutions $q(x, t)$ to the partial differential equation given above for various choices of the input function $u(t)$. +In addition, we will only observe data over a limited time interval $t \in [0, T']$ with $T' < T$, then use the ROM to predict the solution for the entire time domain $[0, T]$. +Hence, the ROM will be **predictive in time** and **predictive in the inputs**. +::: + +```{code-cell} ipython3 +import h5py + +import numpy as np +import scipy.sparse +import matplotlib.pyplot as plt + +import opinf + +opinf.utils.mpl_config() +``` + +## Single Training Trajectory + ++++ + +In this section a ROM is trained using data collected for a single choice of the input function $u(t).$ + ++++ + +### Full-order Model Definition + ++++ + +As in the last tutorial, we use a centered finite difference approximation for the spatial derivative to arrive at a system of $n$ ordinary differential equations. +This time, due to the nonzero boundary conditions, the system takes the form + +$$ +\begin{aligned} + \ddt\q(t) = \A\q(t) + \B u(t), + \qquad + \q(0) = \q_0, +\end{aligned} +$$ (eq_inputs_fom) + +where $\q:\RR\to\RR^n$, $\A\in\RR^{n\times n}$, and $\B\in\RR^{n}$. +The system {eq}`eq_inputs_fom` is the _full-order model_ (FOM), which informs how we will construct our reduced-order model (ROM). + ++++ + +:::{dropdown} Discretization details + +We take an equidistant grid $\{x_i\}_{i=0}^{n+1} \subset \Omega$, + +$$ +\begin{aligned} + 0 &= x_0 < x_1 < \cdots < x_n < x_{n+1} = L + & + &\text{and} + & + \delta x &= \frac{L}{n+1} = x_{i+1} - x_{i},\quad i=1,\ldots,n-1. +\end{aligned} +$$ + +The boundary conditions prescribe $q(x_0,t) = q(x_{n+1},t) = u(t)$. +Our goal is to compute $q(x,t)$ at the interior spatial points $x_{1},x_{2},\ldots,x_{n}$ for various $t\in[0,T]$, so we consider the state vector $\q(t) = [~q(x_{1}, t)~~\cdots~~q(x_{n}, t)~]\trp\in\RR^n$ and derive a system governing the evolution of $\q(t)$ in time. + +Approximating the spatial derivative with a central finite difference approximation, + +$$ +\begin{aligned} + \frac{\partial^2}{\partial x^2}q(x,t) + \approx \frac{q(x-\delta x,t) - 2q(x,t) + q(x+\delta x,t)}{(\delta x)^2}, +\end{aligned} +$$ + +and using the boundary conditions $q(0,t) = q(L,t) = u(t)$, we arrive at the following matrices for the FOM. + +$$ +\begin{aligned} + \A &= \frac{1}{(\delta x)^2}\left[\begin{array}{ccccc} + -2 & 1 & & & \\ + 1 & -2 & 1 & & \\ + & \ddots & \ddots & \ddots & \\ + & & 1 & -2 & 1 \\ + & & & 1 & -2 \\ + \end{array}\right] \in\RR^{n\times n}, + & + \B &= \frac{1}{(\delta x)^2}\left[\begin{array}{c} + 1 \\ 0 \\ \vdots \\ 0 \\ 1 + \end{array}\right]\in\RR^{n}. +\end{aligned} +$$ +::: + ++++ + +### Training Data + ++++ + +:::{button-link} https://github.com/XanderBys/opinf/raw/refs/heads/data/inputs_data.h5 +:color: success +:outline: +To download the data, click here. +::: + ++++ + +Once the data has been downloaded using the link above and placed in the same directory as this tutorial, we can load the data into the notebook. +The data was generated with $\alpha=100$ and solutions recorded every $\delta t=10^{-3}$ time units. The training input function used was + +$$ +\begin{aligned} + u_\text{train}(t) = 1 + \frac{1}{4}\sin(4\pi t). +\end{aligned} +$$ + +We will assume that we can only observe the first $k = 200$ time steps and use the ROM to predict the remaining $801$ steps. + +Additionally, the trainig dataset contains information about the external inputs. +Define the vector + +$$ +\begin{aligned} + \U = \left[\begin{array}{cccc} + u_\text{train}(t_0) & u_\text{train}(t_1) & \cdots & u_\text{train}(t_{k-1}) + \end{array}\right] + \in\RR^{k}, +\end{aligned} +$$ + +which collects the values of the training input function at the same times as the training snapshots. + +```{code-cell} ipython3 +:tags: [hide-input] + +# load the pre-generated data +filepath = "inputs_data.h5" + +with h5py.File(filepath, 'r') as h5file: + t_all = h5file["t"][:] + Q_all = h5file["Q"][:] + U_all = h5file["U"][:] + +x_all = np.linspace(0, 1, Q_all.shape[0]+2) +x = x_all[1:-1] +dx = x[1] - x[0] +dt = t_all[1] - t_all[0] +q0 = Q_all[:, 0] + +# Retain only the first k snapshots/inputs for training the ROM. +k = 200 +t = t_all[:k] +Q = Q_all[:, :k] +U = U_all[:k] + +print(f"\nSpatial domain:\t\t{x.shape=}") +print(f"Spatial step size:\t{dx=:.10f}") +print(f"\nFull time domain:\t{t_all.shape=}") +print(f"Training time domain:\t{t.shape=}") +print(f"Temporal step size:\t{dt=:f}") +print(f"\nInitial condition:\t{q0.shape=}") +print(f"Training snapshots:\t{Q.shape=}") +print(f"\nFull input data:\t{U_all.shape=}") +print(f"Training input data\t{U.shape=}") +``` + +The following code visualizes the training data by plotting a few snapshots over the spatial domain and the time evolution of the snapshots at a few spatial locations. + +```{code-cell} ipython3 +def training_input(tt): + return np.ones_like(tt) + np.sin(4 * np.pi * tt) / 4 +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +def plot_data_space(Z, u, title, ax=None): + """Plot state data over space at multiple instances in time.""" + if ax is None: + _, ax = plt.subplots(1, 1) + + # Plot a few snapshots over the spatial domain. + sample_columns = [0] + [2**d for d in range(10)] + color = iter(plt.cm.viridis_r(np.linspace(0.05, 1, len(sample_columns)))) + while sample_columns[-1] > Z.shape[1] - 1: + sample_columns = sample_columns[:-1] + for j in sample_columns: + leftBC, rightBC = [u[j]], [u[j]] + q_all = np.concatenate([leftBC, Z[:, j], rightBC]) + c = next(color) + ax.plot(x_all, q_all, lw=1, color=c, label=rf"$q(x,t_{{{j}}})$") + + ax.set_xlim(x_all[0], x_all[-1]) + ax.set_xlabel(r"$x$") + ax.set_ylabel(r"$q(x,t)$") + ax.legend(loc=(1.05, 0.05)) + ax.set_title(title) + + +def plot_data_time(Z, title, ax=None): + """Plot state in time at multiple spatial locations.""" + if ax is None: + _, ax = plt.subplots(1, 1) + + # Plot a few snapshots over the spatial domain. + sample_rows = np.linspace(0, Z.shape[0] - 1, 11) + sample_rows = sample_rows[:-1] + (sample_rows[1] - sample_rows[0]) / 4 + sample_rows = sample_rows.astype(int) + color = iter(plt.cm.inferno(np.linspace(0, 0.8, len(sample_rows)))) + tt = t_all[: Z.shape[1]] + for i in sample_rows: + ax.plot(tt, Z[i], lw=1, color=next(color), label=rf"$q(x_{{{i}}},t)$") + + ax.set_xlim(t_all[0], t_all[-1]) + ax.set_xlabel(r"$t$") + ax.set_ylabel(r"$q(x,t)$") + ax.legend(loc=(1.05, 0.05)) + ax.set_title(title) + + +def plot_two_datasets(Z1, Z2, u, title1="", title2="", cutoff=None): + """Plot two datasets side by side with space and time plots.""" + _, [ax1, ax2] = plt.subplots(1, 2, sharex=True, sharey=True) + plot_data_space(Z1, u, title1, ax1) + plot_data_space(Z2, u, title2, ax2) + ax1.legend([]) + + fig, [ax1, ax2] = plt.subplots(2, 1, sharex=True, sharey=True) + plot_data_time(Z1, title1, ax1) + plot_data_time(Z2, title2, ax2) + ax1.legend([]) + ax1.set_xlabel("") + fig.subplots_adjust(hspace=0.3) + if cutoff is not None: + ax1.axvline(cutoff, color="gray", linewidth=1, linestyle="--") + ax1.text(cutoff - 10 * dt, 0, "training", ha="right", color="gray") + ax1.text(cutoff + 10 * dt, 0, "prediction", ha="left", color="gray") + + plt.show() +``` + +```{code-cell} ipython3 +plot_two_datasets( + Q, + Q_all, + U_all, + "Snapshot data for training", + "Full snapshot data", + cutoff=t[-1], +) +``` + +### ROM Construction + ++++ + +We will use a {class}`opinf.basis.PODBasis` to reduce the dimension of the snapshot training data, which approximates the discretized state vector as $\q(t) \approx \Vr\qhat(t)$ for some $\Vr\in\RR^{n\times r}$ with orthonormal columns and $\qhat(t)\in\RR^{r}$, with and $r\ll n$. +Input training data are *not* typically compressed with dimensionality reduction or subjected to other pre-processing routines. +Because the FOM {eq}`eq_inputs_fom` has the linear-time invariant form $\ddt\q(t) = \A\q(t) + \B u(t)$, we seek a ROM with the same structure, i.e., + +$$ +\begin{aligned} + \ddt\qhat(t) = \Ahat\qhat(t) + \Bhat u(t), + \qquad + \qhat(0) = \Vr\trp\q_0. +\end{aligned} +$$ + +Data for the time derivative $\ddt\qhat(t)$ are estimated in this example with sixth-order finite differences using {class}`opinf.ddt.UniformFiniteDifferencer`. +The underlying least-squares problem to determine $\Ahat$ and $\Bhat$ is given by + +$$ +\begin{aligned} + \min_{\Ahat,\Bhat} + \sum_{j=0}^{k-1}\left\| + \Ahat\qhat_{j} + \Bhat\u_j - \dot{\qhat}_j + \right\|_{2}^{2}, +\end{aligned} +$$ + +where $\qhat_j = \qhat(t_j)\in\RR^{r}$ and $u_j = u(t_j)\in\RR$ are the state snapshots and input data, respectively, and $\dot{\qhat}_j \approx \ddt\qhat(t)|_{t=t_j}\in\RR^{r}$ are the estimated time derivatives. + ++++ + +:::{dropdown} Why Use the Same Structure? + +An OpInf ROM should have the same structure as an intrusive Galerkin ROM. +The Galerkin ROM for {eq}`eq_inputs_fom` is derived by substituting in the approximation $\q(t)\approx\Vr\qhat(t)$, yielding + +$$ +\begin{aligned} + \ddt\Vr\qhat(t) = \A\Vr\qhat(t) + \B u(t), + \qquad + \Vr\qhat(0) = \q_0. +\end{aligned} +$$ + +Next, left multiply by $\Vr\trp$ and use the fact that $\Vr\trp\Vr = \I$ to get the following: + +$$ +\begin{aligned} + \ddt\qhat(t) = \tilde{\A}\qhat(t) + \tilde{\B}u(t), + \qquad + \qhat(0) = \Vr\trp\q_0, +\end{aligned} +$$ + +where $\tilde{\A} = \Vr\trp\A\Vr \in \RR^{r\times r}$ and $\tilde{\B} = \Vr\trp\B\in\RR^{r}$. +Note that this ROM has the same input function $u(t)$ as the FOM. +::: + ++++ + +Training input data are passed to {meth}`opinf.roms.ROM.fit()` as the `inputs` argument. + +```{code-cell} ipython3 +rom = opinf.ROM( + basis=opinf.basis.PODBasis(residual_energy=1e-6), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t, "ord6"), + model=opinf.models.ContinuousModel("AB"), +) + +with opinf.utils.TimedBlock("Fitting OpInf ROM"): + rom.fit(Q, inputs=U) + +with opinf.utils.TimedBlock("Reduced-order solve"): + Q_ROM = rom.predict(q0, t_all, input_func=U_all, method="BDF") +``` + +```{code-cell} ipython3 +plot_two_datasets( + Q_ROM, + Q_all, + U_all, + "Reduced-order model solution", + "State snapshots", + cutoff=t[-1], +) +``` + +For a closer look at the difference between the ROM solutions and the state snapshots, we compute the relative $\ell_2$-norm error of the ROM solution as a function of time using {func}`opinf.post.lp_error()` and the relative Forbenius-norm error using {func}`opinf.post.frobenius_error()`. + +```{code-cell} ipython3 +:tags: [hide-input] + +def plot_errors_over_time( + Ztrue, basis, Z1, label1, Z2=None, label2=None, cutoff=None +): + """Plot normalized absolute projection error and ROM error(s) + as a function of time. + """ + _, ax = plt.subplots(1, 1) + + projection_err = opinf.post.lp_error(Ztrue, basis.project(Ztrue))[1] + ax.semilogy(t_all, projection_err, "C3-", lw=1, label="Projection Error") + + relative_error = opinf.post.lp_error(Ztrue, Z1)[1] + ax.semilogy(t_all, relative_error, "C0--", lw=1, label=label1) + + if Z2 is not None: + relative_error = opinf.post.lp_error(Ztrue, Z2)[1] + ax.semilogy(t_all, relative_error, "C5-.", lw=1, label=label2) + + if cutoff is not None: + ax.axvline(cutoff, color="gray", linewidth=1, linestyle="--") + ymin = projection_err.min() / 4 + ax.text(cutoff - 10 * dt, ymin, "training", ha="right", color="gray") + ax.text(cutoff + 10 * dt, ymin, "prediction", ha="left", color="gray") + ax.set_ylim(bottom=ymin / 2) + + ax.set_xlim(t_all[0], t_all[-1]) + ax.set_xlabel(r"$t$") + ax.set_ylabel("Relative error") + ax.legend(loc="lower right") + plt.show() +``` + +```{code-cell} ipython3 +plot_errors_over_time(Q_all, rom.basis, Q_ROM, "OpInf ROM error", cutoff=t[-1]) +``` + +```{code-cell} ipython3 +error_opinf = opinf.post.frobenius_error(Q_all, Q_ROM)[1] +print(f"OpInf ROM error:\t{error_opinf:.4e}") +``` + +### Generalization to New Inputs + ++++ + +The previous experiment uses a single choice of $u(t)$ for the training and for the prediction in time. +Now, we define a new choice of input function $u(t)$, + +$$ +\begin{aligned} + u_\text{test}(t) + = 1 + t(1 - t), +\end{aligned} +$$ + +and evaluate the ROM for this new input. + +```{code-cell} ipython3 +with h5py.File(filepath, 'r') as h5file: + Q_test = h5file["Q_test"][:] + U_test = h5file["U_test"][:] +``` + +```{code-cell} ipython3 +with opinf.utils.TimedBlock("Reduced-order solve (OpInf)"): + Qtest_ROM = rom.predict(q0, t_all, U_test, method="BDF") +``` + +```{code-cell} ipython3 +plot_two_datasets( + Qtest_ROM, + Q_test, + U_test, + "OpInf Reduced-order model solution", + "State snapshots", +) +``` + +```{code-cell} ipython3 +plot_errors_over_time( + Q_test, + rom.basis, + Qtest_ROM, + "OpInf ROM error", +) +``` + +## Multiple Training Trajectories + ++++ + +If data corresponding to several choices of the input function $u(t)$ are available for training, we collect a list of snapshot matrices and a list of corresponding inputs to pass to `fit()`. + ++++ + +### Training Data Generation + ++++ + +Below, we solve use the ROM to simulate the three input functions for training data: + +$$ +\begin{aligned} + &u_\text{train}^{(1)}(t) = e^{-t}, + &&& + &u_\text{train}^{(2)}(t) = 1 + \frac{1}{2}t^2, + &&& + &u_\text{train}^{(3)}(t) = 1 - \frac{1}{2}\sin(\pi t). +\end{aligned} +$$ + +The following input functions are used for testing: + +$$ +\begin{aligned} + &u_\text{test}^{(1)}(t) = 1 - \frac{1}{2}\sin(3\pi t), + &&& + &u_\text{test}^{(2)}(t) = 1 + 25 (t (t - 1))^3, + &&& + &u_\text{test}^{(3)}(t) = 1 + e^{-2t}\sin(\pi t). +\end{aligned} +$$ + +```{code-cell} ipython3 +# Load the generated training data +Qs_train = [] # State snapshots. +Us_train_all = [] # Corresponding inputs. +Us_train = [] # truncated inputs for training the ROM. +Qs_test = [] +Us_test = [] + +with h5py.File(filepath, 'r') as f: + for idx in range(f["train"].attrs["num_input_functions"]): + Qs_train.append(f["train"][f"Q_{idx}"][:, :k]) + Us_train_all.append(f["train"][f"U_{idx}"][:]) + Us_train.append(f["train"][f"U_{idx}"][:k]) + + for idx in range(f["test"].attrs["num_input_functions"]): + Qs_test.append(f["test"][f"Q_{idx}"][:]) + Us_test.append(f["test"][f"U_{idx}"][:]) +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +# Visualize the input functions. +fig, [ax1, ax2] = plt.subplots(1, 2, sharex=True) +c = 0 +for inputs in Us_train_all: + ax1.plot(t_all, inputs, color=f"C{c}", lw=1) + c += 1 +for inputs in Us_test: + ax2.plot(t_all, inputs, color=f"C{c}", lw=1) + c += 1 + +ax1.set_title("Training inputs") +ax2.set_title("Testing inputs") +# ax1.axvline(t[-1], color="k", lw=1) +ax1.axvline(t[-1], color="gray", linewidth=1, linestyle="--") +ax1.text(t[-1] - 10 * dt, 1.4, "training", ha="right", color="gray") +ax1.text(t[-1] + 10 * dt, 1.4, "prediction", ha="left", color="gray") +for ax in (ax1, ax2): + ax.set_xlim(t_all[0], t_all[-1]) + ax.set_xlabel(r"$t$") + ax.set_ylabel(r"$u(t)$") +plt.show() +``` + +```{code-cell} ipython3 +rom = opinf.ROM( + basis=opinf.basis.PODBasis(residual_energy=1e-6), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t, "ord6"), + model=opinf.models.ContinuousModel("AB"), +) + +with opinf.utils.TimedBlock("Fitting OpInf ROM"): + rom.fit(Qs_train, inputs=Us_train) +``` + +```{code-cell} ipython3 +for i, [Q, U] in enumerate(zip(Qs_test, Us_test)): + print(f"Test input function {i+1:d}") + + with opinf.utils.TimedBlock("Reduced-order solve (OpInf)"): + Q_ROM = rom.predict(q0, t_all, U, method="BDF") + + plot_two_datasets( + Q_ROM, + Q, + U, + "Reduced-order model solution (OpInf)", + "Full-order model solution", + ) + + plot_errors_over_time( + Q, + rom.basis, + Q_ROM, + "OpInf ROM error" + ) +``` + +:::{admonition} Multi-dimensional Inputs +:class: tip + +The examples in this tutorial use a scalar-valued input function $u:\RR\to\RR$. +For models with vector inputs $\u:\RR\to\RR^m$ with $m > 1$, training inputs are collected into a matrix with $m$ rows: + +$$ +\begin{aligned} + \U = \left[\begin{array}{cccc} + \u(t_0) & \u(t_1) & \cdots & \u(t_{k-1}) + \end{array}\right] + \in \RR^{m \times k}. +\end{aligned} +$$ + +This is the matrix used for the `inputs` argument of `fit()`. +::: \ No newline at end of file diff --git a/sphinx-docs/source/tutorials/parametric.md b/sphinx-docs/source/tutorials/parametric.md new file mode 100644 index 00000000..29d7695b --- /dev/null +++ b/sphinx-docs/source/tutorials/parametric.md @@ -0,0 +1,471 @@ +--- +kernelspec: + display_name: opinf + language: python + name: python3 +file_format: mystnb +--- + ++++ {"toc-hr-collapsed": false} + +# Parametric Problems + ++++ + +$$\def\RR{\mathbb{R}} +\def\NN{\mathbb{N}} +\def\ZZ{\mathbb{Z}} +\def\I{\mathbf{I}} +\def\0{\mathbf{0}} +\def\1{\mathbf{1}} +\def\q{\mathbf{q}} +\def\u{\mathbf{u}} +\def\z{\mathbf{z}} +\def\d{\mathbf{d}} +\def\f{\mathbf{f}} +\def\s{\mathbf{s}} +\def\Q{\mathbf{Q}} +\def\U{\mathbf{U}} +\def\Z{\mathbf{Z}} +\def\Op{\mathbf{f}} +\def\Ophat{\hat{\mathbf{f}}} +\def\c{\mathbf{c}} +\def\A{\mathbf{A}} +\def\H{\mathbf{H}} +\def\G{\mathbf{G}} +\def\B{\mathbf{B}} +\def\N{\mathbf{N}} +\def\v{\mathbf{v}} +\def\w{\mathbf{w}} +\def\V{\mathbf{V}} +\def\W{\mathbf{W}} +\def\Vr{\mathbf{V}_{\!r}} +\def\Wr{\mathbf{W}_{\!r}} +\def\qhat{\hat{\mathbf{q}}} +\def\zhat{\hat{\mathbf{z}}} +\def\fhat{\hat{\mathbf{f}}} +\def\Qhat{\hat{\mathbf{Q}}} +\def\Zhat{\hat{\mathbf{Z}}} +\def\chat{\hat{\mathbf{c}}} +\def\Ahat{\hat{\mathbf{A}}} +\def\Hhat{\hat{\mathbf{H}}} +\def\Ghat{\hat{\mathbf{G}}} +\def\Bhat{\hat{\mathbf{B}}} +\def\Nhat{\hat{\mathbf{N}}} +\def\D{\mathbf{D}} +\def\ohat{\hat{\mathbf{o}}} +\def\Ohat{\hat{\mathbf{O}}} +\def\bfmu{\boldsymbol{\mu}} +\def\bfGamma{\boldsymbol{\Gamma}} +\def\bfPhi{\boldsymbol{\Phi}} +\def\bfSigma{\boldsymbol{\Sigma}} +\def\bfPsi{\boldsymbol{\Psi}} +\def\bfLambda{\boldsymbol{\Lambda}} +\def\bfxi{\boldsymbol{\xi}} +\def\trp{{^{\mathsf{T}}}} +\def\ddt{\frac{\textrm{d}}{\textrm{d}t}} +\def\ddqhat{\frac{\partial}{\partial\qhat}} +\def\mean{\operatorname{mean}} +\def\std{\operatorname{std}} +\def\argmin{\operatorname{argmin}}$$ + ++++ + +Many systems depend on independent parameters that describe material properties or other physical characteristics of the phenomenon being modeled. +In such cases, the operators of a reduced-order model (ROM) should be designed to vary with the system parameters. This tutorial demonstrates how to construct and evaluate a parametric ROM through an elementary example. + ++++ + +## Problem Statement + ++++ + +We consider a problem with a single scalar system parameter $\mu > 0$. + ++++ + +:::{admonition} Governing Equations +:class: info + +Let $\Omega = [0,L]\subset \RR$ be the spatial domain indicated by the variable $x$, and let $[0,T]\subset\RR$ be the time domain with variable $t$. We consider the one-dimensional heat equation with constant non-homogeneous Dirichlet boundary conditions, + +$$ +\begin{aligned} + &\frac{\partial}{\partial t} q(x,t;\mu) = \mu\frac{\partial^2}{\partial x^2}q(x,t;\mu) + & x &\in\Omega,\quad t\in[0,T], + \\ + &q(0,t;\mu) = q(L,t;\mu) = 1 + & t &\in[0,T], + \\ + &q(x,0;\mu) = \big(e^{\alpha(x - 1)} + e^{-\alpha x} - e^{-\alpha}\big) + & x &\in \Omega, +\end{aligned} +$$ + +where the constant $\mu > 0$ is a thermal diffusivity parameter, $\alpha>0$ is constant, and $q(x,t;\mu)$ is the unknown state variable. This is a model for a one-dimensional rod conducting heat with a fixed initial heat profile. The temperature at the ends of the rod are fixed, but heat is allowed to diffuse through the rod and flow out at the ends of the domain. +::: + ++++ + +:::{admonition} Objective +:class: info + +Construct a reduced-order model (ROM) which can be solved rapidly to produce approximate solutions $q(x, t; \mu)$ to the partial differential equation given above for various choices of the diffusivity parameter $\mu > 0$. +We will observe data for a few values of $\mu$, then use the ROM to predict the solution for the entire time domain $[0, T]$ and for new values of $\mu$. +Hence, the ROM will be **predictive in the parameter** $\mu$. + + + +::: + +```{code-cell} ipython3 +import numpy as np +import scipy.sparse +import matplotlib.pyplot as plt + +import opinf + +opinf.utils.mpl_config() +``` + +## Full-order Model Definition + ++++ + +We consider the parameter domain $\mathcal{P} = [.1,10]\subset\RR$. +A finite element or finite difference discretization leads to a system of differential equations, + +$$ +\begin{aligned} + \ddt\q(t;\mu) + = \c(\mu) + \A(\mu)\q(t;\mu), + \qquad + \q(0) = \q_0, +\end{aligned} +$$ (eq_parametric_fom) + +where $\q:\RR\times\mathcal{P}\to\RR^n,$ $\c:\mathcal{P}\to\RR^n,$ and $\A:\mathcal{P}\to\RR^{n\times n}.$ +This is the full-order model (FOM). +The constant term $\c(\mu)$ arises due to the nonzero boundary conditions. +In this case, the parametric dependence on $\mu$ is linear: there are $\c^{(0)}\in\RR^{n}$ and $\A^{(0)}\in\RR^{n\times n}$ such that $\c(\mu) = \mu\c^{(0)}$ and $\A(\mu) = \mu\A^{(0)}.$ + ++++ + +:::{dropdown} Discretization details + +We take an equidistant grid $\{x_i\}_{i=0}^{n+1} \subset \Omega$, + +\begin{align*} + 0 &= x_0 < x_1 < \cdots < x_n < x_{n+1} = L + & + &\text{and} + & + \delta x &= \frac{L}{n+1} = x_{i+1} - x_{i},\quad i=1,\ldots,n-1. +\end{align*} + +The boundary conditions prescribe $q(x_0,t;\mu) = q(x_{n+1},t;\mu) = 1$. +Our goal is to compute $q(x,t)$ at the interior spatial points $x_{1},x_{2},\ldots,x_{n}$ for various $t\in[0,T]$, so we consider the state vector $\q(t;\mu) = [~q(x_{1}, t;\mu)~\cdots~q(x_{n}, t;\mu)~]\trp\in\RR^n$ and derive a system governing the evolution of $\q(t;\mu)$ in time. + +Approximating the spatial derivative with a central finite difference approximation, + +$$ + \frac{\partial^2}{\partial x^2}q(x,t) + \approx \frac{q(x-\delta x,t) - 2q(x,t) + q(x+\delta x,t)}{(\delta x)^2}, +$$ + +and using the boundary conditions $q(0,t;\mu) = q(L,t;\mu) = 1$, we arrive at the following matrices for the FOM. + +$$ +\begin{aligned} + \c^{(0)} &= \frac{1}{(\delta x)^2}\left[\begin{array}{c} + 1 \\ 0 \\ \vdots \\ 0 \\ 1 + \end{array}\right]\in\RR^{n}, + & + \A^{(0)} &= \frac{1}{(\delta x)^2}\left[\begin{array}{ccccc} + -2 & 1 & & & \\ + 1 & -2 & 1 & & \\ + & \ddots & \ddots & \ddots & \\ + & & 1 & -2 & 1 \\ + & & & 1 & -2 \\ + \end{array}\right] \in\RR^{n\times n}. +\end{aligned} +$$ +::: + ++++ + +## Training Data Generation + ++++ + +Let $L = 1$, $T = 1$, and set $\alpha = 100$. +For this demo, we use $n = 2^{10} - 1 = 1023$ spatial degrees of freedom and record the FOM solution every $\delta t = 0.0025$ time units. +For each training parameter $\mu_i$, this results in $k = 401$ state snapshots, organized in snapshot matrices + +$$ +\begin{aligned} + \Q_i = \left[\begin{array}{cccc} + \q(t_0;\mu_i) & \q(t_1;\mu_i) & \cdots & \q(t_{k-1};\mu_i) + \end{array}\right] + \in\RR^{n\times k}, + \quad + i = 0,\ldots, s-1. +\end{aligned} +$$ + +```{code-cell} ipython3 +# Get s logarithmically spaced paraneter values in D = [.1, 10]. +s = 10 +training_parameters = np.logspace(-1, 1, s) +print(training_parameters) +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +# Construct the spatial domain. +L = 1 +n = 2**10 - 1 +x_all = np.linspace(0, L, n + 2) +x = x_all[1:-1] +dx = x[1] - x[0] + +# Construct the temporal domain. +T = 1 +K = 401 +t_all = np.linspace(0, T, K) +dt = t_all[1] - t_all[0] + +# Construct the full-order state matrix A. +dx2inv = 1 / dx**2 +diags = np.array([1, -2, 1]) * dx2inv +A0 = scipy.sparse.diags(diags, [-1, 0, 1], (n, n)) + +# Construct the full-order input matrix B. +c0 = np.zeros_like(x) +c0[0], c0[-1] = dx2inv, dx2inv + +# Construct the part of the initial condition not dependent on u(t). +alpha = 100 +q0 = np.exp(alpha * (x - 1)) + np.exp(-alpha * x) - np.exp(-alpha) + + +def full_order_solve(mu, time_domain): + """Solve the full-order model with SciPy. + Here, u is a callable function. + """ + return scipy.integrate.solve_ivp( + fun=lambda t, q: mu * (c0 + A0 @ q), + y0=q0, + t_span=[time_domain[0], time_domain[-1]], + t_eval=time_domain, + method="BDF", + ).y + + +Qs = [] +# Solve the full-order model at the training parameter values. +with opinf.utils.TimedBlock("Full-order solves"): + for mu in training_parameters: + Qs.append(full_order_solve(mu, t_all)) + + +print(f"\nSpatial domain:\t\t{x.shape=}") +print(f"Spatial step size:\t{dx=:.10f}") +print(f"\nFull time domain:\t{t_all.shape=}") +# print(f"Training time domain:\t{t.shape=}") +print(f"Temporal step size:\t{dt=:f}") +print(f"\nFull-order matrix A0:\t{A0.shape=}") +print(f"Full-order vector c0:\t{c0.shape=}") +print(f"\nInitial condition:\t{q0.shape=}") +print(f"Training snapshots:\t{Qs[0].shape=}") +``` + +```{code-cell} ipython3 +:tags: [hide-input] + +def plot_data_space(Z, title, ax=None): + """Plot state data over space at multiple instances in time.""" + if ax is None: + _, ax = plt.subplots(1, 1) + + # Plot a few snapshots over the spatial domain. + sample_columns = [0] + [2**d for d in range(10)] + color = iter(plt.cm.viridis_r(np.linspace(0.05, 1, len(sample_columns)))) + while sample_columns[-1] > Z.shape[1] - 1: + sample_columns = sample_columns[:-1] + for j in sample_columns: + q_all = np.concatenate([[0.5], Z[:, j], [1]]) + c = next(color) + ax.plot(x_all, q_all, lw=1, color=c, label=rf"$q(x,t_{{{j}}})$") + + ax.set_xlim(x_all[0], x_all[-1]) + ax.set_xlabel(r"$x$") + ax.set_ylabel(r"$q(x,t)$") + ax.legend(loc=(1.05, 0.05)) + ax.set_title(title) + + +def plot_two_datasets(Z1, title1, Z2, title2): + """Plot two datasets side by side.""" + _, [ax1, ax2] = plt.subplots(1, 2) + plot_data_space(Z1, title1, ax1) + plot_data_space(Z2, title2, ax2) + ax1.legend([]) +``` + +```{code-cell} ipython3 +for i in [0, s // 2, s - 1]: + plot_data_space(Qs[i], rf"Full-order model solution at $\mu = \mu_{i}$") +``` + +## Reduced-order Model Construction + ++++ + +Now that we have parameter and snapshot data, we instantiate a {class}`opinf.roms.ParametricROM` and pass the training parameter values and the corresponding state snapshots to the `fit()` method. + +We will use a {class}`opinf.basis.PODBasis` to reduce the dimension of the snapshot training data, which approximates the discretized state vector as $\q(t;\mu) \approx \Vr\qhat(t;\mu)$ for some $\Vr\in\RR^{n\times r}$ with orthonormal columns and $\qhat(t)\in\RR^{r}$, with and $r\ll n$. +Based on the FOM {eq}`eq_parametric_fom`, we specify a ROM with the following structure: + +$$ +\begin{aligned} + \ddt\qhat(t;\mu) + &= \chat(\mu) + \Ahat(\mu)\qhat(t;\mu) + = \mu\chat^{(0)} + \mu\Ahat^{(0)}\qhat(t;\mu), +\end{aligned} +$$ + +where $\chat^{(0)}\in\RR^{r}$ and $\Ahat^{(0)}\in\RR^{r\times r}.$ +Data for the time derivative $\ddt\qhat(t)$ are estimated in this example with sixth-order finite differences using {class}`opinf.ddt.UniformFiniteDifferencer`. +The underlying least-squares problem to determine $\chat^{(0)}$ and $\Ahat^{(0)}$ is given by + +$$ +\begin{aligned} + \min_{\Ahat,\Bhat} + \sum_{i=0}^{s-1}\sum_{j=0}^{k-1}\left\| + \mu_{i}\chat^{(0)} + \mu_{i}\Ahat^{(0)}\qhat_{i,j} - \dot{\qhat}_{i,j} + \right\|_{2}^{2}, +\end{aligned} +$$ + +where $\qhat_{i,j} = \qhat(t_j;\mu_i)\in\RR^{r}$ are the state snapshots and $\dot{\qhat}_{i,j} \approx \ddt\qhat(t;\mu_{i})|_{t=t_j}\in\RR^{r}$ are the estimated time derivatives. + ++++ + +:::{dropdown} Preserving Parametric Structure + +An OpInf ROM should have the same structure as an intrusive Galerkin ROM. +The Galerkin ROM for {eq}`eq_parametric_fom` is derived by substituting in the approximation $\q(t;\mu)\approx\Vr\qhat(t;\mu)$, yielding + +$$ +\begin{aligned} + \ddt\Vr\qhat(t;\mu) + = \c(\mu) + \A(\mu)\Vr\qhat(t;\mu) + \qquad + \Vr\qhat(0) = \q_0. +\end{aligned} +$$ + +Next, left multiply by $\Vr\trp$ and use the fact that $\Vr\trp\Vr = \I$ to get the following: + +$$ +\begin{aligned} + \ddt\qhat(t;\mu) + = \tilde{\c} + \tilde{\A}(\mu)\qhat(t;\mu) + \qquad + \qhat(0) = \Vr\trp\q_0, +\end{aligned} +$$ + +where $\tilde{\c}(\mu) = \Vr\trp\c(\mu)\in\RR^{r}$ and $\tilde{\A}(\mu) = \Vr\trp\A(\mu)\Vr \in \RR^{r\times r}.$ +Finally, using the formulae $\c(\mu) = \mu\c^{(0)}$ and $\A(\mu) = \mu\A^{(0)}$, we can further simplify to + +$$ +\begin{aligned} + \tilde{\c}(\mu) + &= \Vr\trp\c(\mu) + = \mu\Vr\trp\c^{(0)} + \\ + \tilde{\A}(\mu) + &= \Vr\trp\A(\mu)\Vr + = \mu\Vr\trp\A^{(0)}\Vr. +\end{aligned} +$$ + +::: + ++++ + +:::{admonition} Interpolatory and Affine Parameterizations +:class: tip + +In this problem, the dependence on $\mu$ in the ROM operators $\chat(\mu)$ and $\Ahat(\mu)$ is known from because the structure from the FOM is preserved by linear projection (see [affine operators](sec-operators-affine)). +If the dependence on $\mu$ is not known a-priori or cannot be written in an affine form, [interpolatory operators](sec-operators-interpolated) sometimes provide a feasible alternative. +::: + +```{code-cell} ipython3 +rom = opinf.ParametricROM( + basis=opinf.basis.PODBasis(projection_error=1e-6), + ddt_estimator=opinf.ddt.UniformFiniteDifferencer(t_all, "ord6"), + model=opinf.models.ParametricContinuousModel( + operators=[ + opinf.operators.AffineConstantOperator(1), + opinf.operators.AffineLinearOperator(1), + ], + solver=opinf.lstsq.L2Solver(1e-6), + ), +).fit(training_parameters, Qs) +``` + +## Reduced-order Model Evaluation + ++++ + +We start by checking comparing the solutions of the ROM at the training parameter values to the training snapshots. + +```{code-cell} ipython3 +for i in [0, s // 2, s - 1]: + with opinf.utils.TimedBlock("Reduced-order solve"): + Q_ROM = rom.predict(training_parameters[i], q0, t_all, method="BDF") + plot_two_datasets(Qs[i], "Snapshot data", Q_ROM, "ROM state output") + plt.show() +``` + +Next, we solve the FOM and ROM at new parameter values not included in the training set. + +```{code-cell} ipython3 +test_parameters = np.sqrt(training_parameters[:-1] * training_parameters[1:]) +print(test_parameters) +``` + +```{code-cell} ipython3 +errors = [] + +for mu in test_parameters: + with opinf.utils.TimedBlock("Full-order solve"): + Q_FOM = full_order_solve(mu, t_all) + + with opinf.utils.TimedBlock("Reduced-order solve"): + Q_ROM = rom.predict(mu, q0, t_all, method="BDF") + + plot_two_datasets( + Q_FOM, + "Full-order model solution", + Q_ROM, + "Reduced-order model solution", + ) + plt.show() + errors.append(opinf.post.frobenius_error(Q_FOM, Q_ROM)[1]) +``` + +```{code-cell} ipython3 +for mu, err in zip(test_parameters, errors): + print(f"Test parameter mu = {mu:.6f}: error = {err:.4%}") +``` + +:::{admonition} Stay Tuned +:class: note + +More examples are forthcoming. +::: \ No newline at end of file diff --git a/tox.ini b/tox.ini index a211866e..26f82de4 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,14 @@ deps = changedir = {toxinidir}/docs commands = python bib2md.py - +[testenv:sphinxliterature] +description = Generate the Literature page of the sphinx documentation +skip_install = true +deps = + bibtexparser>2.0.0b7 +changedir= {toxinidir}/sphinx-docs +commands = + python bib2md.py [testenv:docs] description = Build documentation with Jupyter Book deps = @@ -57,3 +64,20 @@ deps = sphinxcontrib-mermaid commands = jupyter-book build --nitpick docs +[testenv:sphinx-docs] +description = build the documentation using Sphinx and MyST +deps = + sphinx + myst-nb + sphinx-book-theme + numpydoc + pandas + sphinx-design + sphinxcontrib-bibtex + sphinxcontrib-mermaid + jupytext + linkify-it-py +commands = + # Builds the docs, ignores cache, turns warnings into errors, use html builder, defines the source and output directories + sphinx-build -E -b html sphinx-docs/source sphinx-docs/build/html +