Skip to content

Commit 93ee27f

Browse files
authored
Merge pull request #11 from LIBRA-project/sensitivity_analysis
Merge all changes implemented during sensitivity analysis: graph view of the input, fix source normalization and T leakage due to dispersion, several sensitivity analysis and UQ scripts along with data processing notebooks
2 parents 84ac0e5 + 7483c37 commit 93ee27f

33 files changed

Lines changed: 4522 additions & 777 deletions

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
on: [pull_request, push]
3+
4+
jobs:
5+
run-tests:
6+
name: run-tests
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- name: Checkout code
11+
uses: actions/checkout@v5
12+
13+
- name: Set up Conda
14+
uses: conda-incubator/setup-miniconda@v3
15+
with:
16+
activate-environment: libra_sparging
17+
environment-file: environment.yml
18+
miniforge-version: latest
19+
use-mamba: false
20+
channels: conda-forge
21+
22+
- name: Install package
23+
shell: bash -l {0}
24+
run: |
25+
python -m pip install -e .[dev]
26+
27+
- name: Run tests
28+
shell: bash -l {0}
29+
run: |
30+
python -m pytest test --cov src/sparging --cov-report xml
31+
32+
- name: Upload to codecov
33+
uses: codecov/codecov-action@v4
34+
with:
35+
token: ${{ secrets.CODECOV_TOKEN }}
36+
files: ./coverage.xml

.gitignore

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ cover/
5656
*.pot
5757

5858
# Django stuff:
59-
*.log
6059
local_settings.py
6160
db.sqlite3
6261
db.sqlite3-journal
@@ -207,6 +206,16 @@ marimo/_lsp/
207206
__marimo__/
208207

209208
.vscode/
209+
misc_scripts/
210+
datasets/
211+
figures/
210212
*.yaml
211213
*.csv
212-
mwe.py
214+
*.json
215+
!standard_input.json
216+
*.ipynb
217+
!sensitivity.ipynb
218+
!LL_sensitivity.ipynb
219+
*.code-workspace
220+
*.pkl
221+
*.html

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,24 @@ To run the model:
1010
> [!NOTE]
1111
> This uses `dolfinx` which doesn't run on Linux. For windows users, consider using Windows Subsystem for Linux (WSL)
1212
13+
To simply use the library, use the libra_sparging environment:
1314
```
1415
conda env create -f environment.yml
1516
conda activate libra_sparging
1617
```
1718

19+
To also train a surrogate model (for sensitivity analysis, for example), you need autoemulate, use the autoemulate environment:
20+
```
21+
conda env create -f autoemulate_env.yml
22+
conda activate autoemulate_env
23+
```
24+
25+
```
26+
python -m pip install -e .[dev]
27+
```
28+
29+
## How to run tests
1830

1931
```
20-
python model.py
32+
python -m pytest test
2133
```

autoemulate_env.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: autoemulate_env
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- python=3.12
6+
- fenics-dolfinx
7+
- matplotlib
8+
- pyvista
9+
- pyyaml
10+
- numpy
11+
- scipy
12+
- pandas
13+
- pint
14+
- networkx
15+
- pip:
16+
- autoemulate
17+
- pyvis

data_processing/LL_sensitivity.ipynb

Lines changed: 989 additions & 0 deletions
Large diffs are not rendered by default.

data_processing/sensitivity.ipynb

Lines changed: 468 additions & 0 deletions
Large diffs are not rendered by default.

environment.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ name: libra_sparging
22
channels:
33
- conda-forge
44
dependencies:
5+
- python>=3.12
56
- fenics-dolfinx
67
- matplotlib
78
- pyvista
89
- pyyaml
910
- numpy
1011
- scipy
11-
- pandas
12+
- pandas
13+
- pint
14+
- networkx

examples/LL_sensitivity.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from datetime import datetime
2+
from pathlib import Path
3+
from sparging import (
4+
LIBRA_PI_GEOM,
5+
LIBRA_PI_MAT,
6+
LIBRA_PI_OPERATING_PARAMS,
7+
LIBRA_PI_SPARGING_PARAMS,
8+
SimulationInput,
9+
ureg,
10+
all_correlations,
11+
CorrelationType,
12+
)
13+
import logging
14+
from autoemulate.simulations.base import Simulator
15+
from autoemulate import AutoEmulate
16+
from autoemulate.core.sensitivity_analysis import SensitivityAnalysis
17+
import torch
18+
import pandas as pd
19+
import json
20+
import networkx as nx
21+
22+
COMPUTE_SOBOL = False
23+
24+
logger = logging.getLogger(__name__)
25+
logging.basicConfig(level=logging.WARNING)
26+
27+
FOLDER = Path("datasets") / datetime.now().strftime("%Y%m%d_%H%M%S")
28+
FOLDER.mkdir(exist_ok=True, parents=True)
29+
FOLDER_SAMPLES = FOLDER / "samples"
30+
FOLDER_SAMPLES.mkdir(exist_ok=True, parents=True)
31+
FOLDER_PP = FOLDER / "postprocessing"
32+
FOLDER_PP.mkdir(exist_ok=True)
33+
34+
outputs = []
35+
h_l_corrs = all_correlations.get_list(CorrelationType.MASS_TRANSFER_COEFF)
36+
D_l_corrs = all_correlations.get_list(CorrelationType.DIFFUSIVITY)
37+
38+
39+
class SpargingProblem(Simulator):
40+
def __init__(self, parameters_range, output_names):
41+
self.counter = 0
42+
super().__init__(parameters_range, output_names)
43+
44+
def _forward(self, x: torch.Tensor) -> torch.Tensor:
45+
# construct simulation input
46+
LIBRA_PI_OPERATING_PARAMS.temperature = x[0, 0].item() * ureg.celsius
47+
LIBRA_PI_OPERATING_PARAMS.P_top = x[0, 1].item() * ureg.bar
48+
LIBRA_PI_OPERATING_PARAMS.flow_g_mol = x[0, 2].item() * ureg.sccm
49+
LIBRA_PI_GEOM.nozzle_diameter = x[0, 3].item() * ureg.m
50+
# LIBRA_PI_SPARGING_PARAMS.h_l = h_l_corrs[int(x[0, 4].item())]
51+
LIBRA_PI_MAT.D_l = D_l_corrs[int(x[0, 4].item())]
52+
# breakpoint()
53+
54+
graph = nx.Graph()
55+
sim_input = SimulationInput.from_parameters(
56+
LIBRA_PI_GEOM,
57+
LIBRA_PI_MAT,
58+
LIBRA_PI_OPERATING_PARAMS,
59+
LIBRA_PI_SPARGING_PARAMS,
60+
graph=graph,
61+
)
62+
tau = sim_input.get_tau()
63+
h_l = sim_input.h_l
64+
a = sim_input.a
65+
eps_g = sim_input.eps_g
66+
sim_input.to_json(
67+
FOLDER_SAMPLES / f"sample_{self.counter}.json"
68+
) # for debugging and postprocessing
69+
self.counter += 1
70+
71+
# for post processing
72+
Pi = sim_input.get_Pi_number().to("dimensionless").magnitude
73+
PP_numbers.append(
74+
[
75+
Pi,
76+
tau.to("s").magnitude,
77+
h_l.to("m/s").magnitude,
78+
a.to("1/m").magnitude,
79+
eps_g.to("dimensionless").magnitude,
80+
graph.nodes["d_b"]["value"].to("m").magnitude,
81+
graph.nodes["u_g0"]["value"].to("m/s").magnitude,
82+
graph.nodes["h_l"]["origin"],
83+
graph.nodes["D_l"]["origin"],
84+
]
85+
)
86+
y = torch.tensor(
87+
[[tau.to("s").magnitude]],
88+
dtype=torch.float64,
89+
)
90+
return y
91+
92+
93+
simulator = SpargingProblem(
94+
parameters_range={ # realistic (wide) parameters range for LIBRA
95+
"temperature": (500, 600), # celsius
96+
"P_top": (1, 2), # bar
97+
"flow_g_mol": (100, 500), # sccm
98+
"nozzle_diameter": (1e-3, 5e-3), # m
99+
"D_l_corr": (0, 0), # index for selecting diffusivity correlation
100+
},
101+
output_names=["tau"],
102+
)
103+
104+
n_samples = 5000
105+
106+
X = simulator.sample_inputs(n_samples)
107+
108+
PP_numbers = []
109+
Y, _ = simulator.forward_batch(X, allow_failures=False)
110+
111+
# save training data
112+
pd.DataFrame(Y, columns=simulator.output_names).to_csv(
113+
FOLDER / "simulator_outputs.csv", index=False
114+
)
115+
pd.DataFrame(X, columns=simulator.param_names).to_csv(
116+
FOLDER / "simulator_inputs.csv", index=False
117+
)
118+
pd.DataFrame(
119+
PP_numbers,
120+
columns=["Pi", "tau", "h_l", "a", "eps_g", "d_b", "u_g0", "h_l_corr", "D_l_corr"],
121+
).to_csv(FOLDER / "PP_data.csv", index=False)
122+
123+
# sensitivity analysis problem
124+
problem = {
125+
"num_vars": simulator.in_dim,
126+
"names": simulator.param_names,
127+
"bounds": simulator.param_bounds,
128+
"output_names": simulator.output_names,
129+
}
130+
131+
with open(FOLDER / "problem.json", "w") as f:
132+
json.dump(problem, f, indent=4)
133+
134+
if COMPUTE_SOBOL:
135+
# Run AutoEmulate with default settings
136+
ae = AutoEmulate(X, Y, log_level="WARNING", models=["GaussianProcessRBF"])
137+
ae.summarise()
138+
139+
# pick best model
140+
emulator = ae.best_result()
141+
print(f"Selected model: {emulator.model_name} with id: {emulator.id}")
142+
143+
# The use_timestamp paramater ensures a new result is saved each time the save method is called
144+
best_result_filepath = ae.save(emulator, FOLDER, use_timestamp=False)
145+
print("Model and metadata saved to: ", best_result_filepath)
146+
147+
ae.plot_preds(
148+
emulator,
149+
output_names=simulator.output_names,
150+
fname=FOLDER_PP / "predictions.png",
151+
)
152+
153+
# === Sensitivity analysis ===
154+
sa = SensitivityAnalysis(emulator.model, problem=problem)
155+
sobol_df = sa.run("sobol")
156+
sa.plot_sobol(sobol_df, index="ST", fname=FOLDER_PP / "sobolTot.png")

examples/Sparging_UQ.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from datetime import datetime
2+
from pathlib import Path
3+
4+
from sparging import (
5+
LIBRA_PI_GEOM,
6+
LIBRA_PI_MAT,
7+
LIBRA_PI_OPERATING_PARAMS,
8+
LIBRA_PI_SPARGING_PARAMS,
9+
SimulationInput,
10+
ureg,
11+
all_correlations,
12+
CorrelationType,
13+
)
14+
15+
import logging
16+
import pandas as pd
17+
import json
18+
import networkx as nx
19+
import numpy as np
20+
21+
logger = logging.getLogger(__name__)
22+
logging.basicConfig(level=logging.WARNING)
23+
24+
FOLDER = Path("datasets") / datetime.now().strftime("%Y%m%d_%H%M%S")
25+
FOLDER.mkdir(exist_ok=True, parents=True)
26+
FOLDER_SAMPLES = FOLDER / "samples"
27+
FOLDER_SAMPLES.mkdir(exist_ok=True, parents=True)
28+
FOLDER_PP = FOLDER / "postprocessing"
29+
FOLDER_PP.mkdir(exist_ok=True)
30+
31+
outputs = []
32+
h_l_corrs = all_correlations.get_list(CorrelationType.MASS_TRANSFER_COEFF)
33+
D_l_corrs = all_correlations.get_list(CorrelationType.DIFFUSIVITY)
34+
35+
36+
def forward(count, x: dict) -> list:
37+
"""return list of values for post processing"""
38+
# construct simulation input
39+
LIBRA_PI_OPERATING_PARAMS.temperature = x["temperature"] * ureg.celsius
40+
LIBRA_PI_OPERATING_PARAMS.P_top = x["P_top"] * ureg.bar
41+
LIBRA_PI_OPERATING_PARAMS.flow_g_mol = x["flow_g_mol"] * ureg.sccm
42+
LIBRA_PI_GEOM.nozzle_diameter = x["nozzle_diameter"] * ureg.m
43+
# LIBRA_PI_SPARGING_PARAMS.h_l = x["h_l_corr"]
44+
LIBRA_PI_MAT.D_l = D_l_corrs[x["D_l_corr_number"]]
45+
# breakpoint()
46+
47+
graph = nx.Graph()
48+
sim_input = SimulationInput.from_parameters(
49+
LIBRA_PI_GEOM,
50+
LIBRA_PI_MAT,
51+
LIBRA_PI_OPERATING_PARAMS,
52+
LIBRA_PI_SPARGING_PARAMS,
53+
graph=graph,
54+
)
55+
tau = sim_input.get_tau()
56+
h_l = sim_input.h_l
57+
a = sim_input.a
58+
eps_g = sim_input.eps_g
59+
# sim_input.to_json(
60+
# FOLDER_SAMPLES / f"sample_{count}.json"
61+
# ) # for debugging and postprocessing
62+
63+
# for post processing
64+
Pi = sim_input.get_Pi_number().to("dimensionless").magnitude
65+
return [
66+
Pi,
67+
tau.to("s").magnitude,
68+
h_l.to("m/s").magnitude,
69+
a.to("1/m").magnitude,
70+
eps_g.to("dimensionless").magnitude,
71+
graph.nodes["d_b"]["value"].to("m").magnitude,
72+
graph.nodes["u_g0"]["value"].to("m/s").magnitude,
73+
graph.nodes["h_l"]["origin"],
74+
graph.nodes["D_l"]["origin"],
75+
]
76+
77+
78+
n_samples = 1000
79+
inputs = []
80+
postprocess = []
81+
82+
for i in range(n_samples):
83+
input = {
84+
"temperature": np.random.normal(550, 25), # celsius
85+
"P_top": 1.2, # bar
86+
"flow_g_mol": 400, # sccm
87+
"nozzle_diameter": 1.5e-3, # m
88+
"D_l_corr_number": np.random.randint(
89+
0, 2
90+
), # index for selecting diffusivity correlation
91+
}
92+
inputs.append(input)
93+
print(f"Running simulation {i + 1}/{n_samples}")
94+
postprocess.append(forward(i, input))
95+
96+
# save training data
97+
PP_frame = pd.DataFrame(
98+
postprocess,
99+
columns=["Pi", "tau", "h_l", "a", "eps_g", "d_b", "u_g0", "h_l_corr", "D_l_corr"],
100+
)
101+
PP_frame.to_csv(FOLDER / "PP_data.csv", index=False)
102+
103+
# for compatibility with postprocessing code
104+
pd.DataFrame(PP_frame, columns=["tau"]).to_csv(
105+
FOLDER / "simulator_outputs.csv", index=False
106+
)
107+
input_frame = pd.DataFrame(inputs)
108+
input_frame.to_csv(FOLDER / "simulator_inputs.csv", index=False)
109+
110+
problem = {
111+
"names": [x for x in inputs[0].keys()],
112+
"bounds": ["550 +- 25", "1.2", "400", "1.5e-3", "oishi or calderoni"],
113+
}
114+
115+
with open(FOLDER / "problem.json", "w") as f:
116+
json.dump(problem, f, indent=4)

0 commit comments

Comments
 (0)