Skip to content

Commit

Permalink
Merge pull request #69 from EthanJamesLew/enhancement/docstrings
Browse files Browse the repository at this point in the history
WIP: document / reformat tuner, benchmarks, and top-level
  • Loading branch information
EthanJamesLew authored Apr 24, 2024
2 parents 521cd56 + 77195f4 commit 141a853
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 63 deletions.
62 changes: 61 additions & 1 deletion autokoopman/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,64 @@
# TODO: update this
"""
AutoKoopman: Automated Koopman operator methods for data-driven dynamical
systems analysis and control.
The AutoKoopman package provides a high-level system identification tool
that automatically optimizes all hyper-parameters to estimate accurate system
models with globally linearized representations. Implemented as a Python library
under shared class interfaces, AutoKoopman uses a collection of Koopman-based
algorithms centered on conventional dynamic mode decomposition and deep learning.
The package includes functions for generating Koopman operator matrices from
time series data, as well as tools for computing eigenvalues and eigenvectors
of these matrices. AutoKoopman supports both discrete-time and continuous-time
system identification, and includes methods for extended dynamic mode
decomposition (EDMD), deep Koopman, and sparse identification of nonlinear
dynamics (SINDy). The package also provides major types of static observables,
including polynomial observables, neural network observables, and random
Fourier feature observables.
AutoKoopman supports system identification with input and control, including
the Koopman operator with input and control (KIC) method. The package also
includes methods for online (streaming) system identification, including
online dynamic mode decomposition (DMD).
Finally, AutoKoopman includes hyperparameter optimization methods for tuning
model parameters, including random search, grid search, and Bayesian
optimization.
Use Cases
----------
The library is intended for a systems engineer/researcher who wishes to
leverage data-driven dynamical systems techniques. The user may have
measurements of their system with no prior model.
Prediction: AutoKoopman can predict the evolution of a system over long time
horizons by leveraging the globally linearized representation provided by
Koopman operator methods.
Control: AutoKoopman can synthesize control signals that achieve desired
closed-loop behaviors and are optimal with respect to some objective by
using the Koopman operator with input and control (KIC) method.
Verification: AutoKoopman can prove or falsify the safety requirements of
a system by providing a mathematical representation of the system dynamics
that can be analyzed and verified.
For more information on the theory behind Koopman operator methods and
their use in system identification, see the following references:
- Koopman, B. O. (1931). Hamiltonian systems and transformation in Hilbert
space. Proceedings of the National Academy of Sciences, 17(5), 315-318.
- Mezic, I. (2013). Analysis of fluid flows via spectral properties of the
Koopman operator. Annual Review of Fluid Mechanics, 45, 357-378.
- Proctor, J. L., & Brunton, S. L. (2020). Koopman operator-based system
identification: a survey of methods and recent advances. arXiv preprint
arXiv:2009.14544.
For usage examples and additional documentation, see the package
documentation.
"""

__author__ = "Ethan Lew"
__copyright__ = "Copyright 2023"
__credits__ = [
Expand Down
155 changes: 95 additions & 60 deletions autokoopman/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (built by setup.py sdist) and build
Expand Down Expand Up @@ -61,17 +60,18 @@ class NotThisMethod(Exception):

def register_vcs_handler(vcs, method): # decorator
"""Create decorator to mark a method as the handler of a VCS."""

def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f

return decorate


def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
env=None):
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
process = None
Expand All @@ -87,10 +87,14 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
try:
dispcmd = str([command] + args)
# remember shell=False, so use git.cmd on windows, not just git
process = subprocess.Popen([command] + args, cwd=cwd, env=env,
stdout=subprocess.PIPE,
stderr=(subprocess.PIPE if hide_stderr
else None), **popen_kwargs)
process = subprocess.Popen(
[command] + args,
cwd=cwd,
env=env,
stdout=subprocess.PIPE,
stderr=(subprocess.PIPE if hide_stderr else None),
**popen_kwargs,
)
break
except OSError:
e = sys.exc_info()[1]
Expand Down Expand Up @@ -125,15 +129,21 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
for _ in range(3):
dirname = os.path.basename(root)
if dirname.startswith(parentdir_prefix):
return {"version": dirname[len(parentdir_prefix):],
"full-revisionid": None,
"dirty": False, "error": None, "date": None}
return {
"version": dirname[len(parentdir_prefix) :],
"full-revisionid": None,
"dirty": False,
"error": None,
"date": None,
}
rootdirs.append(root)
root = os.path.dirname(root) # up a level

if verbose:
print("Tried directories %s but none started with prefix %s" %
(str(rootdirs), parentdir_prefix))
print(
"Tried directories %s but none started with prefix %s"
% (str(rootdirs), parentdir_prefix)
)
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")


Expand Down Expand Up @@ -192,7 +202,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
TAG = "tag: "
tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)}
if not tags:
# Either we're using git < 1.8.3, or there really are no tags. We use
# a heuristic: assume all version tags have a digit. The old git %d
Expand All @@ -201,32 +211,39 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# between branches and tags. By ignoring refnames without digits, we
# filter out many common branch names like "release" and
# "stabilization", as well as "HEAD" and "master".
tags = {r for r in refs if re.search(r'\d', r)}
tags = {r for r in refs if re.search(r"\d", r)}
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
print("likely tags: %s" % ",".join(sorted(tags)))
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
r = ref[len(tag_prefix) :]
# Filter out refs that exactly match prefix or that don't start
# with a number once the prefix is stripped (mostly a concern
# when prefix is '')
if not re.match(r'\d', r):
if not re.match(r"\d", r):
continue
if verbose:
print("picking %s" % r)
return {"version": r,
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": None,
"date": date}
return {
"version": r,
"full-revisionid": keywords["full"].strip(),
"dirty": False,
"error": None,
"date": date,
}
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
return {"version": "0+unknown",
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": "no suitable tags", "date": None}
return {
"version": "0+unknown",
"full-revisionid": keywords["full"].strip(),
"dirty": False,
"error": "no suitable tags",
"date": None,
}


@register_vcs_handler("git", "pieces_from_vcs")
Expand All @@ -248,19 +265,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
env.pop("GIT_DIR", None)
runner = functools.partial(runner, env=env)

_, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
hide_stderr=not verbose)
_, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
raise NotThisMethod("'git rev-parse --git-dir' returned error")

# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
# if there isn't one, this yields HEX[-dirty] (no NUM)
describe_out, rc = runner(GITS, [
"describe", "--tags", "--dirty", "--always", "--long",
"--match", f"{tag_prefix}[[:digit:]]*"
], cwd=root)
describe_out, rc = runner(
GITS,
[
"describe",
"--tags",
"--dirty",
"--always",
"--long",
"--match",
f"{tag_prefix}[[:digit:]]*",
],
cwd=root,
)
# --long was added in git-1.5.5
if describe_out is None:
raise NotThisMethod("'git describe' failed")
Expand All @@ -275,8 +300,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
pieces["short"] = full_out[:7] # maybe improved later
pieces["error"] = None

branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
cwd=root)
branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
# --abbrev-ref was added in git-1.6.3
if rc != 0 or branch_name is None:
raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
Expand Down Expand Up @@ -316,17 +340,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
git_describe = git_describe[:git_describe.rindex("-dirty")]
git_describe = git_describe[: git_describe.rindex("-dirty")]

# now we have TAG-NUM-gHEX or HEX

if "-" in git_describe:
# TAG-NUM-gHEX
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
if not mo:
# unparsable. Maybe git-describe is misbehaving?
pieces["error"] = ("unable to parse git-describe output: '%s'"
% describe_out)
pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
return pieces

# tag
Expand All @@ -335,10 +358,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
% (full_tag, tag_prefix))
pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (
full_tag,
tag_prefix,
)
return pieces
pieces["closest-tag"] = full_tag[len(tag_prefix):]
pieces["closest-tag"] = full_tag[len(tag_prefix) :]

# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
Expand Down Expand Up @@ -387,8 +412,7 @@ def render_pep440(pieces):
rendered += ".dirty"
else:
# exception #1
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
pieces["short"])
rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
Expand Down Expand Up @@ -417,8 +441,7 @@ def render_pep440_branch(pieces):
rendered = "0"
if pieces["branch"] != "master":
rendered += ".dev0"
rendered += "+untagged.%d.g%s" % (pieces["distance"],
pieces["short"])
rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
Expand Down Expand Up @@ -579,11 +602,13 @@ def render_git_describe_long(pieces):
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
return {"version": "unknown",
"full-revisionid": pieces.get("long"),
"dirty": None,
"error": pieces["error"],
"date": None}
return {
"version": "unknown",
"full-revisionid": pieces.get("long"),
"dirty": None,
"error": pieces["error"],
"date": None,
}

if not style or style == "default":
style = "pep440" # the default
Expand All @@ -607,9 +632,13 @@ def render(pieces, style):
else:
raise ValueError("unknown style '%s'" % style)

return {"version": rendered, "full-revisionid": pieces["long"],
"dirty": pieces["dirty"], "error": None,
"date": pieces.get("date")}
return {
"version": rendered,
"full-revisionid": pieces["long"],
"dirty": pieces["dirty"],
"error": None,
"date": pieces.get("date"),
}


def get_versions():
Expand All @@ -623,8 +652,7 @@ def get_versions():
verbose = cfg.verbose

try:
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
verbose)
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose)
except NotThisMethod:
pass

Expand All @@ -633,13 +661,16 @@ def get_versions():
# versionfile_source is the relative path from the top of the source
# tree (where the .git directory might live) to this file. Invert
# this to find the root from __file__.
for _ in cfg.versionfile_source.split('/'):
for _ in cfg.versionfile_source.split("/"):
root = os.path.dirname(root)
except NameError:
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to find root of source tree",
"date": None}
return {
"version": "0+unknown",
"full-revisionid": None,
"dirty": None,
"error": "unable to find root of source tree",
"date": None,
}

try:
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
Expand All @@ -653,6 +684,10 @@ def get_versions():
except NotThisMethod:
pass

return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to compute version", "date": None}
return {
"version": "0+unknown",
"full-revisionid": None,
"dirty": None,
"error": "unable to compute version",
"date": None,
}
9 changes: 9 additions & 0 deletions autokoopman/benchmark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
AutoKoopman benchmark module.
The `autokoopman.benchmark` module provides classes for generating training and
test trajectories for benchmarking the AutoKoopman algorithms. These
trajectories are generated from various common dynamical systems and
ARCH models using the `autokoopman.core.system` interfaces. Many of the
benchmark systems are symbolic, using `sympy` expressions.
"""
2 changes: 1 addition & 1 deletion autokoopman/benchmark/bio2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class Bio2(asys.SymbolicContinuousSystem):
r"""
Spring pendulum
Biological System 2
A nine-dimensional continuous model which is adapted from a biological system given in [1].
Initial set:
Expand Down
Loading

0 comments on commit 141a853

Please sign in to comment.