diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..71f5e16 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: + push: + branches: + - main + workflow_dispatch: {} + pull_request: + types: [opened, synchronize] + branches: + - main + +jobs: + # docker-image: + # uses: ./.github/workflows/docker.yml + api-doc: + # needs: [docker-image] + uses: ./.github/workflows/sphinx.yml \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3001815 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,69 @@ +name: docker-image +on: + workflow_call: + +env: + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: LLNL/GPLaSDI/lasdi_env + DOCKERPATH: docker + +jobs: + docker-ci: + runs-on: ubuntu-latest + name: "docker env" + env: + DOCKERPATH: docker + steps: + - name: test command + run: echo "docker-ci command" + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - uses: Ana06/get-changed-files@v2.2.0 + id: files + - name: DockerPATH configuration + run: echo "DOCKERPATH=$DOCKERPATH" + - name: DockerPATH - check if files in docker path changed + if: contains(steps.files.outputs.all,env.DOCKERPATH) || contains(steps.files.outputs.all,'docker.yml') + run: | + echo "CI container needs rebuilding..." + echo "CI_NEEDS_REBUILD=true" >> $GITHUB_ENV + - name: Log into registry ${{ env.REGISTRY }} + if: env.CI_NEEDS_REBUILD + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata (tags, labels) for Docker + id: meta + if: env.CI_NEEDS_REBUILD + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: type=sha + flavor: latest=true + - name: Build Container motd + if: env.CI_NEEDS_REBUILD + run: | + echo "#!/bin/bash" > ${{env.DOCKERPATH}}/motd.sh + echo "echo --------------------------" >> ${{env.DOCKERPATH}}/motd.sh + echo "echo lasdi_env/CI Development Container" >> ${{env.DOCKERPATH}}/motd.sh + echo "echo \"Revision: `echo ${GITHUB_SHA} | cut -c1-8`\"" >> ${{env.DOCKERPATH}}/motd.sh + echo "echo --------------------------" >> ${{env.DOCKERPATH}}/motd.sh + chmod 755 ${{env.DOCKERPATH}}/motd.sh + cat ${{env.DOCKERPATH}}/motd.sh + - name: Docker Image - Build and push + if: env.CI_NEEDS_REBUILD + uses: docker/build-push-action@v5 + with: + push: true + context: ${{ env.DOCKERPATH }} + tags: ${{ steps.meta.outputs.tags }} + # platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml new file mode 100644 index 0000000..c3074a8 --- /dev/null +++ b/.github/workflows/sphinx.yml @@ -0,0 +1,47 @@ +name: "Sphinx: Render docs" + +# on: push +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + # container: + # image: ghcr.io/llnl/gplasdi/lasdi_env:latest + # options: --user 1001 --privileged + # volumes: + # - /mnt:/mnt + permissions: + contents: write + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + - uses: actions/checkout@v4 + - name: Build HTML + uses: ammaraskar/sphinx-action@master + with: + docs-folder: "docs/" + build-command: "sphinx-build -b html source build" + pre-build-command: "pip install --upgrade pip && pip install sphinx-autoapi sphinx_rtd_theme" + # run: | + # sphinx-build --version + # cd ${GITHUB_WORKSPACE}/docs + # mkdir build + # sphinx-build -b html source/ build/ + - name: check resulting files + run: | + ls ${GITHUB_WORKSPACE}/docs/build + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: html-docs + path: docs/build/ + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + # if: github.ref == 'refs/heads/main' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5d540ee..793942b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ examples/results examples/checkpoint *.npy build +docs/build \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..bb49ef6 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,37 @@ +FROM ubuntu:22.04 + +ENV ENVDIR=env + +# install sudo +RUN apt-get -yq update && apt-get -yq install sudo + +WORKDIR /$ENVDIR + +# install packages +RUN sudo apt-get install -yq git +RUN sudo apt-get install --no-install-recommends -yq make gcc gfortran libssl-dev cmake +RUN sudo apt-get install -yq libopenblas-dev libmpich-dev libblas-dev liblapack-dev libscalapack-mpi-dev libhdf5-mpi-dev hdf5-tools +# RUN sudo apt-get install -yq vim +RUN sudo apt-get install -yq git-lfs +# RUN sudo apt-get install -yq valgrind +RUN sudo apt-get install -yq wget +# RUN sudo apt-get install -yq astyle + +# install python +RUN sudo apt-get install -yq python3 +RUN sudo apt-get install -yq python3-dev +RUN sudo apt-get install -yq python3-pip +RUN sudo apt-get install python-is-python3 +RUN sudo python -m pip install --upgrade pip +RUN sudo python -m pip install sphinx sphinx-autoapi sphinx_rtd_theme +#RUN sudo pip3 install numpy scipy argparse tables PyYAML h5py pybind11 pytest mpi4py merlin +# +RUN sudo apt-get clean -q + +# create and switch to a user +ENV USERNAME=test +RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers +RUN useradd --no-log-init -u 1001 --create-home --shell /bin/bash $USERNAME +RUN adduser $USERNAME sudo +USER $USERNAME +WORKDIR /home/$USERNAME diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..29afdaa --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,42 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os, sys +sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../../src')) + +project = 'LaSDI' +copyright = '2023-2024, Lawrence Livermore National Security, LLC and other LaSDI project developers.' +author = 'Christophe Bonneville, Kevin Chung, Youngsoo Choi' +release = '2.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'autoapi.extension', + 'sphinx.ext.napoleon', +] + +autoapi_dirs = ['../../src'] + +napoleon_google_docstring = False +napoleon_use_param = False +napoleon_use_ivar = True + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..69cf38c --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,27 @@ +.. LaSDI documentation master file, created by + sphinx-quickstart on Wed Oct 16 22:11:53 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +LaSDI documentation +=================== + +LaSDI is a light-weight python package for Latent Space Dynamics Identification. +LaSDI maps full-order PDE solutions to a latent space using autoencoders and learns the system of ODEs governing the latent space dynamics. +By interpolating and solving the ODE system in the reduced latent space, fast and accurate ROM predictions can be made by feeding the predicted latent space dynamics into the decoder. +It also supports parametric interpolation of latent dynamics according to uncertainties evaluated via Gaussian process. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +References +=================== + +* Bonneville, Christophe, Xiaolong He, April Tran, Jun Sur Park, William Fries, Daniel A. Messenger, Siu Wun Cheung et al. "A Comprehensive Review of Latent Space Dynamics Identification Algorithms for Intrusive and Non-Intrusive Reduced-Order-Modeling." arXiv preprint arXiv:2403.10748 (2024). +* Fries, William D., Xiaolong He, and Youngsoo Choi. "LaSDI: Parametric latent space dynamics identification." Computer Methods in Applied Mechanics and Engineering 399 (2022): 115436. +* He, Xiaolong, Youngsoo Choi, William D. Fries, Jonathan L. Belof, and Jiun-Shyan Chen. "gLaSDI: Parametric physics-informed greedy latent space dynamics identification." Journal of Computational Physics 489 (2023): 112267. +* Tran, April, Xiaolong He, Daniel A. Messenger, Youngsoo Choi, and David M. Bortz. "Weak-form latent space dynamics identification." Computer Methods in Applied Mechanics and Engineering 427 (2024): 116998. +* Park, Jun Sur Richard, Siu Wun Cheung, Youngsoo Choi, and Yeonjong Shin. "tLaSDI: Thermodynamics-informed latent space dynamics identification." arXiv preprint arXiv:2403.05848 (2024). +* Bonneville, Christophe, Youngsoo Choi, Debojyoti Ghosh, and Jonathan L. Belof. "Gplasdi: Gaussian process-based interpretable latent space dynamics identification through deep autoencoder." Computer Methods in Applied Mechanics and Engineering 418 (2024): 116535. +* He, Xiaolong, April Tran, David M. Bortz, and Youngsoo Choi. "Physics-informed active learning with simultaneous weak-form latent space dynamics identification." arXiv preprint arXiv:2407.00337 (2024). diff --git a/pyproject.toml b/pyproject.toml index f134c31..bf18591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lasdi" -version = "2.0.0-dev" +version = "2.0.0" authors = [ { name="Christophe Bonneville", email="cpb97@cornell.edu" }, { name="Kevin (Seung Whan) Chung", email="chung28@llnl.gov" }, diff --git a/src/lasdi/inputs.py b/src/lasdi/inputs.py index 656102f..0a654b6 100644 --- a/src/lasdi/inputs.py +++ b/src/lasdi/inputs.py @@ -47,7 +47,7 @@ def getDictFromList(list_, inputDict): ''' get a dict with {key: val} from a list of dicts NOTE: it returns only the first item in the list, - even if the list has more than one dict with {key: val}. + even if the list has more than one dict with {key: val}. ''' dict_ = None for item in list_: diff --git a/src/lasdi/timing.py b/src/lasdi/timing.py index da7d7c3..978045a 100644 --- a/src/lasdi/timing.py +++ b/src/lasdi/timing.py @@ -1,14 +1,49 @@ +""" + +.. _NumPy docstring standard: + https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard + +""" + from time import perf_counter class Timer: - def __init__(self): + """A light-weight timer class. + """ + + def __init__(self): + self.names = {} + """:obj:`dict(str:int)`: Dictionary that maps job names to job indices.""" + self.calls = [] + """:obj:`list(int)`: List that stores the number of calls for each job.""" + self.times = [] + """:obj:`list(float)`: List that stores the total time for each job.""" + self.starts = [] + """ + :obj:`list(float)`: List that stores the start time for each job. + If the job is not running, :obj:`None` is stored instead. + """ return def start(self, name): + """Start a job named :obj:`name`. + If the job is not listed, register the job in the job list. + + Args: + name (:obj:`str`): Name of the job to be started. + + Note: + The job must not have started before calling this method. + + Returns: + Does not return a value. + + """ + if name not in self.names: self.names[name] = len(self.names) self.calls += [0] @@ -16,15 +51,30 @@ def start(self, name): self.starts += [None] idx = self.names[name] + # check if the job is already being measured if (self.starts[idx] is not None): raise RuntimeError("Timer.start: %s timer is already ticking!" % name) self.starts[idx] = perf_counter() return def end(self, name): + """End a job named :obj:`name`. + Increase the number of calls and the runtime for the job. + + Args: + name (:obj:`str`): Name of the job to be ended. + + Note: + The job must have started before calling this method. + + Returns: + Does not return a value. + + """ assert(name in self.names) idx = self.names[name] + # check if the job has started. if (self.starts[idx] is None): raise RuntimeError("Timer.end: %s start time is not measured yet!" % name) @@ -34,12 +84,28 @@ def end(self, name): return def print(self): + """Print the list of jobs and their number of calls, total time and time per each call. + + Returns: + Does not return a value. + """ + print("Function name\tCalls\tTotal time\tTime/call\n") for name, idx in self.names.items(): print("%s\t%d\t%.3e\t%.3e\n" % (name, self.calls[idx], self.times[idx], self.times[idx] / self.calls[idx])) return def export(self): + """Export the list of jobs and their number of calls and total time + into a dictionary. + + Note: + All jobs must be ended before calling this method. + + Returns: + :obj:`dict` that contains "names", "calls", and "times" as keys + """ + for start in self.starts: if (start is not None): raise RuntimeError('Timer.export: cannot export while Timer is still ticking!') @@ -51,6 +117,19 @@ def export(self): return param_dict def load(self, dict_): + """Load the list of jobs and their number of calls and total time + from a dictionary. + + Args: + `dict_` (:obj:`dict`): Dictionary that contains the list of jobs and their calls and times. + + Note: + :obj:`dict_['names']`, :obj:`dict_['calls']` and :obj:`dict_['times']` must have the same size. + + Returns: + Does not return a value + """ + self.names = dict_['names'] self.calls = dict_['calls'] self.times = dict_['times']