Skip to content

Commit

Permalink
dev(init): adding all source code and documentation (#5)
Browse files Browse the repository at this point in the history
* dev(init): adding all source code and documentation
  • Loading branch information
eckelsjd authored Dec 13, 2023
1 parent 24b1019 commit bc91878
Show file tree
Hide file tree
Showing 34 changed files with 5,038 additions and 112 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
python-version: ['3.12']
os: [ ubuntu-latest, macOS-latest, windows-latest ]
steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Custom
todo.txt
amisc_*

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
32 changes: 20 additions & 12 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Pull requests are the best way to propose changes to the codebase (bug fixes, ne

1. Fork the repo and create your branch from `main`.
3. If you are adding a feature or making major changes, first create the [issue](https://github.com/eckelsjd/amisc/issues).
4. If you've added code that should be tested, add a [test](https://github.com/eckelsjd/amisc/tests).
5. If you've made major changes, update the [documentation](https://github.com/eckelsjd/amisc/docs).
4. If you've added code that should be tested, add to `/tests`.
5. If you've made major changes, update the `/docs`.
6. Ensure the test suite passes (`pdm run test`).
7. Make sure your code passes lint checks (coming soon).
8. Follow [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) guidelines when adding a commit message.
Expand All @@ -26,27 +26,35 @@ We strongly recommend using [pdm](https://github.com/pdm-project/pdm) to set up

```shell
pip install --user pdm

# Fork the repo on Github

git clone https://github.com/<your-user-name>/amisc.git
cd amisc
pdm install # Installs all dev and package dependencies (including amisc itself in -e mode) into local .venv
pdm install
git checkout -b <your-branch-name>

# Make local changes

pdm run test # make sure tests pass
git add -A
git commit -m "Adding a bugfix or new feature"
git push -u origin <your-branch-name>

# Go to Github and "Compare & Pull Request" on your fork
# For your PR to be merged:
# squash all your commits on your branch (interactively in an IDE most likely)
# rebase to the top of origin/main to include new changes from others
git fetch
git rebase -i main your-branch # for example
# Resolve any conflicts
# Your history now looks something like this:
# o your-branch
# /
# ---o---o---o main

git fetch
git rebase -i main your-branch # for example

# Resolve any conflicts
# Your history now looks something like this:
# o your-branch
# /
# ---o---o---o main

# You can delete the branch and fork when your PR has been merged
```

Expand All @@ -63,5 +71,5 @@ Start or take part in community [discussions](https://github.com/eckelsjd/amisc/
By contributing, you agree that your contributions will be licensed under its GNU GPLv3 License.

## Releases
The package version is tracked at `amisc.__init__.__version__`. You should not edit this value. Maintainers
will decide when to make a release by bumping this and running `pdm run release`.
The package version is tracked at `amisc.__init__.__version__`. You should not edit this value. The version will be
increased on a case-by-case basis and released depending on the changes being merged.
26 changes: 4 additions & 22 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![Logo](https://raw.githubusercontent.com/eckelsjd/amisc/main/docs/assets/amisc_logo_text.svg)
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm-project.org)
[![PyPI](https://img.shields.io/pypi/v/amisc?logo=python&logoColor=%23cccccc)](https://pypi.org/project/amisc)
[![Python 3.7](https://img.shields.io/badge/python-3.7+-blue.svg?logo=python&logoColor=cccccc)](https://www.python.org/downloads/)
[![Python 3.12](https://img.shields.io/badge/python-3.12+-blue.svg?logo=python&logoColor=cccccc)](https://www.python.org/downloads/)
![Commits](https://img.shields.io/github/commit-activity/m/eckelsjd/amisc?logo=github)
![build](https://img.shields.io/github/actions/workflow/status/eckelsjd/amisc/deploy.yml?logo=github
)
Expand Down Expand Up @@ -39,31 +39,13 @@ pdm sync # reads pdm.lock and sets up an identical venv
```

## Quickstart
```python
from amisc.surrogates import SystemSurrogate
from amisc.utils import UniformRV
import numpy as np

def fun1(x):
return x ** 2

def fun2(y):
return np.sin(y) * np.exp(y)

x, y, z = UniformRV(0, 1, 'x'), UniformRV(0, 1, 'y'), UniformRV(0, 1, 'z')
model1 = {'name': 'model1', 'model': fun1, 'exo_in': ['x'], 'coupling_out': ['y']}
model2 = {'name': 'model2', 'model': fun2, 'coupling_in': ['y'], 'coupling_out': ['z']}

system = SystemSurrogate([model1, model2], [x], [y, z])
system.fit()

xtest = system.sample_inputs(10)
ytest = system.predict(xtest)
```python title="amisc.examples.tutorial.py"
--8<-- "amisc/examples/tutorial.py:simple"
```

## Contributing
See the [contribution](CONTRIBUTING.md) guidelines.

## Reference
## Citations
AMISC paper [[1](https://onlinelibrary.wiley.com/doi/full/10.1002/nme.6958)].

Binary file added docs/assets/fire_sat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/css/extra.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.md-grid {
max-width: 1440px;
}

:root > * {
--md-code-hl-keyword-color: #842e21;
}
1 change: 0 additions & 1 deletion docs/explanation.md

This file was deleted.

66 changes: 65 additions & 1 deletion docs/how-to-guides.md
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
Coming soon
## Specifying model inputs and outputs
Coming soon.

## Defining a component
Coming soon.

## Making a model wrapper function
The examples in the [tutorial](tutorials.md) use the simple function call signatures `ret = func(x)`, where `x` is an `np.ndarray` and
`ret` is a dictionary with the required `y=output` key-value pair. If your model must be executed outside of Python
(such as in a separate `.exe` file), then you can write a Python wrapper function with the same call signature as above
and make any external calls you need inside the function (such as with `os.popen()`). You then pass the wrapper function
to `ComponentSpec` and `SystemSurrogate`.

!!! Note "Requirements for your wrapper function"
- First argument `x` must be an `np.ndarray` of the model inputs whose **last** dimension is the number of inputs, i.e. `x.shape[-1] = x_dim`.
- You can choose to handle as many other dimensions as you want, i.e. `x.shape[:-1]`. The surrogate will handle the same number
of dimensions you give to your wrapper function (so that `model(x)` and `surrogate(x)` are functionally equivalent). We recommend you handle at least
1 extra dimension, i.e. `x.shape = (N, x_dim)`. So your wrapper must handle `N` total sets of inputs at a time. The easiest way is to just
write a for loop over `N` and run your model for a single set of inputs at a time.
- Your wrapper function must expect the `x_dim` inputs in a specific order according to how you defined your system. All
system-level exogenous inputs (i.e. those in `system.exo_vars`) must be first and in the order you specified for
`ComponentSpec(exo_in=[first, second, ...])`. All coupling inputs that come from the outputs of other models are next.
Regardless of what order you chose in `ComponentSpec(coupling_in=[one, two, three,...]`, your wrapper **must** expect them
in _sorted_ order according to `system.coupling_vars`. For example, if `system.coupling_vars = [a, b, c]` and
`comp = ComponentSpec(wrapper, coupling_in=[c, a], exo_in=[d, e], coupling_out=[f])`, then `x_dim = 4` and your `wrapper` function
should expect the inputs in `x` to be ordered as `[d, e, a, c]`.
- If you want to pass in model fidelity indices (see $\alpha$ in [theory](theory.md) for details), they must be in the form of a `tuple`,
and your wrapper function should accept the `alpha=...` keyword argument. Specifying `alpha` allows managing a hierarchy of modeling fidelities, if applicable.
- You can pass any number of additional positional arguments. Specify these with `ComponentSpec(model_args=...)`.
- You can pass any number of keyword arguments. Specify these with `ComponentSpec(model_kwargs=...)`.
- If you want to save and keep track of the full output of your model (i.e. if it writes result files to disk), then
you can specify `ComponentSpec(save_output=True)`. When you do this, you must also specify `SystemSurrogate(..., save_dir='path/to/save/dir')`.
You will then get a folder called `save_dir/amisc_timestamp/components/<your_model_name>`. This folder will be passed to your
wrapper function as the keyword argument `output_dir=<your_model_dir>`. Make sure your `wrapper` accepts this keyword (no need to specify it in `ComponentSpec(model_kwargs=...)`; this is done automatically).
You can then have your model write whatever it wants to this folder. You **must** then pass back the names of the files
you created via `ret=dict(files=[your output files, ...])`. The filenames must be in a list and match the order in
which the samples in `x` were executed by the model.
- To assist the adaptive training procedure, you can also optionally have your model compute and return its computational cost via
`ret=dict(cost=cpu_cost)`. The computational cost should be expressed in units of seconds of CPU time (not walltime!) for _one_ model evaluation.
If your model makes use of `n` CPUs in parallel, then the total CPU time would be `n` times the wall clock time.
- The return dictionary of your wrapper can include anything else you want outside of the three fields `(y, files, cost)` discussed here.
Any extra return values will be ignored by the system.

!!! Example
```python
def wrapper_func(x, alpha, *args, output_dir=None, **kwargs):
print(x.shape) # (..., x_dim)

# Your code here, for example:
output = x ** 2
output_files = ['output_1.json', 'output2.json', ...]
cpu_time = 42 # seconds for one model evaluation

ret = dict(y=output, files=output_files, cost=cpu_time)

return ret
```

!!! Warning
Always specify the model at a _global_ scope, i.e. don't use `lambda` or nested functions. When saving to
file, only a symbolic reference to the function signature will be saved, which must be globally defined
when loading back from that save file.

## Putting it all together
Coming soon.
16 changes: 16 additions & 0 deletions docs/javascripts/mathjax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex"
}
};

document$.subscribe(() => {
MathJax.typesetPromise()
})
1 change: 0 additions & 1 deletion docs/reference.md

This file was deleted.

3 changes: 3 additions & 0 deletions docs/reference/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::: amisc.component
options:
members_order: source
4 changes: 4 additions & 0 deletions docs/reference/interpolators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: amisc.interpolator
options:
filters: [""]
members_order: source
91 changes: 91 additions & 0 deletions docs/reference/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
The `amisc` package takes an object-oriented approach to building a surrogate of a multidisciplinary system. From the
bottom up, you have:

- **variables** that serve as inputs and outputs for the models,
- **interpolators** that define a specific input &rarr; output mathematical relationship to interpolate a function,
- **components** that wrap a model for a single discipline, and a
- **system** that defines the connections between components in a multidisciplinary system.

The variables, interpolators, and components all have abstract base classes, so that the **system** is ultimately
independent of the specific models, interpolation methods, or underlying variables. As such, the primary top-level object
that users of the `amisc` package will interact with is the `SystemSurrogate`.

!!! Note
There are already pretty good implementations of the other abstractions that most users will not need to worry about,
but they are provided in this API reference for completeness. The abstractions allow new interpolation
(i.e. function approximation) methods to be implemented if desired, such as neural networks, kriging, etc.

Here is a class diagram summary of this workflow:

``` mermaid
classDiagram
namespace Core {
class SystemSurrogate {
+list[BaseRV] exo_vars
+list[BaseRV] coupling_vars
+int refine_level
+fit()
+predict(x)
+sample_inputs(size)
+insert_component(comp)
}
class ComponentSurrogate {
<<abstract>>
+IndexSet index_set
+IndexSet candidate_set
+list[BaseRV] x_vars
+dict[str: BaseInterpolator] surrogates
+dict[str: float] misc_coeff
+predict(x)
+activate_index(alpha, beta)
+add_surrogate(alpha, beta)
+update_misc_coeff()
}
class BaseInterpolator {
<<abstract>>
+tuple beta
+list[BaseRV] x_vars
+np.ndarray xi
+np.ndarray yi
+set_yi()
+refine()
+__call__(x)
}
}
class SparseGridSurrogate {
+np.ndarray x_grids
+dict xi_map
+dict yi_map
+get_tensor_grid(alpha, beta)
}
class LagrangeInterpolator {
+np.ndarray x_grids
+np.ndarray weights
+get_grid_sizes()
+leja_1d()
}
class BaseRV {
<<abstract>>
+tuple bounds
+str units
+float nominal
+pdf(x)
+sample(size)
}
class UniformRV {
+str type
+get_uniform_bounds(nominal)
}
SystemSurrogate o-- "1..n" ComponentSurrogate
ComponentSurrogate o-- "1..n" BaseInterpolator
direction LR
ComponentSurrogate <|-- SparseGridSurrogate
BaseInterpolator <|-- LagrangeInterpolator
SparseGridSurrogate ..> LagrangeInterpolator
BaseRV <|-- UniformRV
```
Note how the `SystemSurrogate` aggregates the `ComponentSurrogate`, which aggregates the `BaseInterpolator`. In other words,
interpolators can act independently of components, and components can act independently of systems. All three make use
of the random variables (these connections and some RVs are not shown for visual clarity). Currently, the only underlying surrogate
method that is implemented here is Lagrange polynomial interpolation (i.e. the `LagrangeInterpolator`). If one wanted
to use neural networks instead, the only change required is a new implementation of `BaseInterpolator`.
1 change: 1 addition & 0 deletions docs/reference/system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: amisc.system
3 changes: 3 additions & 0 deletions docs/reference/utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Package utilities

::: amisc.utils
1 change: 1 addition & 0 deletions docs/reference/variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: amisc.rv
1 change: 1 addition & 0 deletions docs/theory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Coming soon.
27 changes: 26 additions & 1 deletion docs/tutorials.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
Coming soon
## Single component example
Here is an example of interpolating a simple quadratic function.
```python title="amisc.examples.tutorial.py"
--8<-- "amisc/examples/tutorial.py:single"
```

## Two component system
Here is a simple example of a two-component multidisciplinary system.
```python title="amisc.examples.tutorial.py"
--8<-- "amisc/examples/tutorial.py:simple"
```
The first component computes $y=x\sin(\pi x)$. The second component takes the output of the first and computes
$z=1 / (1 + 25y^2)$. The system-level input is $x$ and the system-level outputs are $y$ and $z$.

!!! Note
Each component always locally returns a dictionary with the output saved as `y=value`. This is not to be confused with the
_system-level_ `y` variable in this example.

## Fire detection satellite
Here is an example of a three-component fire detection satellite system from [Chauduri (2018)](https://dspace.mit.edu/handle/1721.1/117036).
```python title="amisc.examples.tutorial.py"
--8<-- "amisc/examples/tutorial.py:fire_sat"
```
We first generate a test set using the ground truth model predictions (and filter any bad values out). Then we train the
surrogate in 10 iterations, and finally plot some results. Here is the output of `plot_slice()`:
![Fire satellite system results](assets/fire_sat.png)
Loading

0 comments on commit bc91878

Please sign in to comment.