Update src/streamlit_app.py
Browse files- src/streamlit_app.py +1805 -34
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,1811 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
indices = np.linspace(0, 1, num_points)
|
| 20 |
-
theta = 2 * np.pi * num_turns * indices
|
| 21 |
-
radius = indices
|
| 22 |
-
|
| 23 |
-
x = radius * np.cos(theta)
|
| 24 |
-
y = radius * np.sin(theta)
|
| 25 |
-
|
| 26 |
-
df = pd.DataFrame({
|
| 27 |
-
"x": x,
|
| 28 |
-
"y": y,
|
| 29 |
-
"idx": indices,
|
| 30 |
-
"rand": np.random.randn(num_points),
|
| 31 |
-
})
|
| 32 |
-
|
| 33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
| 34 |
-
.mark_point(filled=True)
|
| 35 |
-
.encode(
|
| 36 |
-
x=alt.X("x", axis=None),
|
| 37 |
-
y=alt.Y("y", axis=None),
|
| 38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
| 39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
| 40 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from scipy.io import loadmat
|
| 5 |
+
from scipy.interpolate import PchipInterpolator, interp1d
|
| 6 |
+
import io
|
| 7 |
+
import time
|
| 8 |
+
import tempfile
|
| 9 |
+
import os
|
| 10 |
|
| 11 |
+
###########################################################################
|
| 12 |
+
# Configure Streamlit page
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="Bubble Dynamics Analysis",
|
| 15 |
+
page_icon="๐ซง",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state="expanded"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Try importing TensorFlow
|
| 21 |
+
try:
|
| 22 |
+
import tensorflow as tf
|
| 23 |
+
from tensorflow import keras
|
| 24 |
+
from tensorflow.keras import layers
|
| 25 |
+
|
| 26 |
+
TENSORFLOW_AVAILABLE = True
|
| 27 |
+
except ImportError:
|
| 28 |
+
TENSORFLOW_AVAILABLE = False
|
| 29 |
+
|
| 30 |
+
# ULTRA-AGGRESSIVE CSS FIX - Complete stability with UT Austin background and Logo
|
| 31 |
+
st.markdown("""
|
| 32 |
+
<style>
|
| 33 |
+
/* BACKGROUND IMAGE STYLING */
|
| 34 |
+
.stApp {
|
| 35 |
+
background-image: url('data:image/jpeg;base64,BACKGROUND_IMAGE_BASE64_HERE');
|
| 36 |
+
background-size: cover;
|
| 37 |
+
background-position: center;
|
| 38 |
+
background-repeat: no-repeat;
|
| 39 |
+
background-attachment: fixed;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* ALTERNATIVE: If you have the image in assets folder, use this instead:
|
| 43 |
+
.stApp {
|
| 44 |
+
background-image: url('./src/ut_yang_background.jpg');
|
| 45 |
+
background-size: cover;
|
| 46 |
+
background-position: center;
|
| 47 |
+
background-repeat: no-repeat;
|
| 48 |
+
background-attachment: fixed;
|
| 49 |
+
}
|
| 50 |
+
*/
|
| 51 |
+
|
| 52 |
+
/* OVERLAY FOR BETTER TEXT READABILITY */
|
| 53 |
+
.stApp::before {
|
| 54 |
+
content: '';
|
| 55 |
+
position: fixed;
|
| 56 |
+
top: 0;
|
| 57 |
+
left: 0;
|
| 58 |
+
width: 100%;
|
| 59 |
+
height: 100%;
|
| 60 |
+
background: rgba(255, 255, 255, 0.85);
|
| 61 |
+
z-index: -1;
|
| 62 |
+
pointer-events: none;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* FORCE HEADER TO BE COMPLETELY FIXED AND STABLE WITH LOGO */
|
| 66 |
+
.main-header {
|
| 67 |
+
position: fixed !important;
|
| 68 |
+
top: 0 !important;
|
| 69 |
+
left: 0 !important;
|
| 70 |
+
right: 0 !important;
|
| 71 |
+
width: 100% !important;
|
| 72 |
+
background: rgba(255, 255, 255, 0.95) !important;
|
| 73 |
+
backdrop-filter: blur(10px) !important;
|
| 74 |
+
z-index: 99999 !important;
|
| 75 |
+
padding: 1rem 2rem !important;
|
| 76 |
+
border-bottom: 3px solid #bf5700 !important;
|
| 77 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
|
| 78 |
+
transform: translateZ(0) !important;
|
| 79 |
+
will-change: auto !important;
|
| 80 |
+
display: flex !important;
|
| 81 |
+
align-items: center !important;
|
| 82 |
+
justify-content: center !important;
|
| 83 |
+
flex-direction: row !important;
|
| 84 |
+
gap: 20px !important;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* LOGO STYLING */
|
| 88 |
+
.header-logo {
|
| 89 |
+
max-height: 70px !important;
|
| 90 |
+
max-width: 280px !important;
|
| 91 |
+
height: auto !important;
|
| 92 |
+
width: auto !important;
|
| 93 |
+
object-fit: contain !important;
|
| 94 |
+
border-radius: 8px !important;
|
| 95 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
|
| 96 |
+
flex-shrink: 0 !important;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* HEADER TEXT STYLING */
|
| 100 |
+
.header-text {
|
| 101 |
+
font-size: 2.2rem !important;
|
| 102 |
+
color: #bf5700 !important; /* UT Austin burnt orange */
|
| 103 |
+
text-align: left !important;
|
| 104 |
+
margin: 0 !important;
|
| 105 |
+
font-weight: bold !important;
|
| 106 |
+
text-shadow: 1px 1px 2px rgba(0,0,0,0.1) !important;
|
| 107 |
+
white-space: nowrap !important;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* ADD TOP MARGIN TO MAIN CONTENT TO AVOID OVERLAP - ADJUSTED FOR HORIZONTAL LOGO */
|
| 111 |
+
.main > .block-container {
|
| 112 |
+
margin-top: 120px !important; /* Reduced back to 120px for horizontal layout */
|
| 113 |
+
padding-top: 20px !important;
|
| 114 |
+
background: rgba(255, 255, 255, 0.9) !important;
|
| 115 |
+
border-radius: 10px !important;
|
| 116 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
|
| 117 |
+
backdrop-filter: blur(5px) !important;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* RESPONSIVE DESIGN FOR MOBILE */
|
| 121 |
+
@media (max-width: 768px) {
|
| 122 |
+
.header-logo {
|
| 123 |
+
max-height: 50px !important;
|
| 124 |
+
max-width: 200px !important;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.header-text {
|
| 128 |
+
font-size: 1.2rem !important;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.main-header {
|
| 132 |
+
padding: 0.8rem 1rem !important;
|
| 133 |
+
gap: 15px !important;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.main > .block-container {
|
| 137 |
+
margin-top: 100px !important;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* EXTRA SMALL MOBILE */
|
| 142 |
+
@media (max-width: 480px) {
|
| 143 |
+
.main-header {
|
| 144 |
+
flex-direction: column !important;
|
| 145 |
+
gap: 8px !important;
|
| 146 |
+
padding: 0.8rem 0.5rem !important;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header-logo {
|
| 150 |
+
max-height: 40px !important;
|
| 151 |
+
max-width: 180px !important;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.header-text {
|
| 155 |
+
font-size: 1.1rem !important;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.main > .block-container {
|
| 159 |
+
margin-top: 110px !important;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* UT AUSTIN THEMING */
|
| 164 |
+
.section-header {
|
| 165 |
+
font-size: 1.5rem;
|
| 166 |
+
color: #bf5700; /* UT Austin burnt orange */
|
| 167 |
+
margin-top: 2rem;
|
| 168 |
+
margin-bottom: 1rem;
|
| 169 |
+
font-weight: bold;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* SIDEBAR STYLING WITH UT THEME */
|
| 173 |
+
.css-1d391kg {
|
| 174 |
+
background: rgba(255, 255, 255, 0.95) !important;
|
| 175 |
+
backdrop-filter: blur(10px) !important;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* ENHANCED CONTAINERS WITH UT THEMING */
|
| 179 |
+
.metric-card {
|
| 180 |
+
background: linear-gradient(135deg, #bf5700, #d67700) !important;
|
| 181 |
+
color: white !important;
|
| 182 |
+
padding: 1rem;
|
| 183 |
+
border-radius: 0.5rem;
|
| 184 |
+
margin: 0.5rem 0;
|
| 185 |
+
}
|
| 186 |
+
.success-box {
|
| 187 |
+
background: linear-gradient(135deg, #28a745, #20c997) !important;
|
| 188 |
+
color: white !important;
|
| 189 |
+
border-left: 5px solid #155724;
|
| 190 |
+
padding: 1rem;
|
| 191 |
+
margin: 1rem 0;
|
| 192 |
+
border-radius: 8px;
|
| 193 |
+
}
|
| 194 |
+
.warning-box {
|
| 195 |
+
background: linear-gradient(135deg, #ffc107, #fd7e14) !important;
|
| 196 |
+
color: #212529 !important;
|
| 197 |
+
border-left: 5px solid #856404;
|
| 198 |
+
padding: 1rem;
|
| 199 |
+
margin: 1rem 0;
|
| 200 |
+
border-radius: 8px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/* NUCLEAR OPTION: DISABLE ALL ANIMATIONS AND TRANSITIONS EVERYWHERE */
|
| 204 |
+
*, *::before, *::after {
|
| 205 |
+
transition: none !important;
|
| 206 |
+
animation: none !important;
|
| 207 |
+
animation-duration: 0s !important;
|
| 208 |
+
animation-delay: 0s !important;
|
| 209 |
+
transform: none !important;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* FORCE STABILITY ON ALL STREAMLIT ELEMENTS */
|
| 213 |
+
.stProgress, .stProgress > div, .stProgress * {
|
| 214 |
+
transition: none !important;
|
| 215 |
+
animation: none !important;
|
| 216 |
+
transform: none !important;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.stSpinner, .stSpinner > div, .stSpinner * {
|
| 220 |
+
transition: none !important;
|
| 221 |
+
animation: none !important;
|
| 222 |
+
transform: none !important;
|
| 223 |
+
position: relative !important;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/* STABILIZE CONTAINERS */
|
| 227 |
+
.element-container, .stMarkdown, .stButton {
|
| 228 |
+
transition: none !important;
|
| 229 |
+
animation: none !important;
|
| 230 |
+
transform: none !important;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* PREVENT LAYOUT SHIFTS */
|
| 234 |
+
.main .block-container .element-container {
|
| 235 |
+
transition: none !important;
|
| 236 |
+
animation: none !important;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* FORCE GPU ACCELERATION FOR STABILITY */
|
| 240 |
+
.main-header {
|
| 241 |
+
transform: translate3d(0,0,0) !important;
|
| 242 |
+
backface-visibility: hidden !important;
|
| 243 |
+
perspective: 1000px !important;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* HIDE STREAMLIT'S RERUN INDICATOR */
|
| 247 |
+
.stAppViewMain > .main > .block-container > div:first-child {
|
| 248 |
+
visibility: hidden !important;
|
| 249 |
+
height: 0 !important;
|
| 250 |
+
margin: 0 !important;
|
| 251 |
+
padding: 0 !important;
|
| 252 |
+
}
|
| 253 |
+
</style>
|
| 254 |
+
""", unsafe_allow_html=True)
|
| 255 |
+
|
| 256 |
+
# Initialize session state
|
| 257 |
+
if 'data_loaded' not in st.session_state:
|
| 258 |
+
st.session_state.data_loaded = False
|
| 259 |
+
if 'processed_data' not in st.session_state:
|
| 260 |
+
st.session_state.processed_data = False
|
| 261 |
+
if 'model_loaded' not in st.session_state:
|
| 262 |
+
st.session_state.model_loaded = False
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# ULTRA-OPTIMIZED BubbleSimulation class - ZERO UI UPDATES during simulation
|
| 266 |
+
class OptimizedBubbleSimulation:
|
| 267 |
+
"""
|
| 268 |
+
ULTRA-OPTIMIZED version - ZERO UI updates during simulation to prevent any trembling
|
| 269 |
+
"""
|
| 270 |
+
|
| 271 |
+
def __init__(self):
|
| 272 |
+
self.setup_parameters()
|
| 273 |
+
|
| 274 |
+
def setup_parameters(self):
|
| 275 |
+
# Fixed parameters (same as original MATLAB)
|
| 276 |
+
self.R0 = 35e-6
|
| 277 |
+
self.P_inf = 101325
|
| 278 |
+
self.T_inf = 298.15
|
| 279 |
+
self.cav_type = 'LIC'
|
| 280 |
+
|
| 281 |
+
# Material parameters (same as original)
|
| 282 |
+
self.c_long = 1700
|
| 283 |
+
self.alpha = 0.0
|
| 284 |
+
self.rho = 1000
|
| 285 |
+
self.gamma = 0.0725
|
| 286 |
+
|
| 287 |
+
# Parameters for bubble contents (same as original)
|
| 288 |
+
self.D0 = 24.2e-6
|
| 289 |
+
self.kappa = 1.4
|
| 290 |
+
self.Ru = 8.3144598
|
| 291 |
+
self.Rv = self.Ru / (18.01528e-3)
|
| 292 |
+
self.Ra = self.Ru / (28.966e-3)
|
| 293 |
+
self.A = 5.28e-5
|
| 294 |
+
self.B = 1.17e-2
|
| 295 |
+
self.P_ref = 1.17e11
|
| 296 |
+
self.T_ref = 5200
|
| 297 |
+
|
| 298 |
+
# OPTIMIZED numerical parameters for speed
|
| 299 |
+
self.NT = 100 # Reduced from 500 to 100 (5x faster, still accurate)
|
| 300 |
+
self.RelTol = 1e-5 # Relaxed from 1e-7 to 1e-5 (faster convergence)
|
| 301 |
+
|
| 302 |
+
def run_optimized_simulation(self, G, mu, lambda_max_mean=None):
|
| 303 |
+
"""ULTRA-OPTIMIZED simulation - ZERO UI updates to prevent trembling"""
|
| 304 |
+
from scipy.integrate import solve_ivp
|
| 305 |
+
|
| 306 |
+
# Set lambdamax from loaded data or use default
|
| 307 |
+
if lambda_max_mean is not None:
|
| 308 |
+
self.lambdamax = lambda_max_mean
|
| 309 |
+
print(f"Using lambda_max_mean = {self.lambdamax} from .mat file")
|
| 310 |
+
else:
|
| 311 |
+
self.lambdamax = 5.99 # Fallback default
|
| 312 |
+
print(f"Warning: Using default lambda_max = {self.lambdamax}")
|
| 313 |
+
|
| 314 |
+
print(f"Running ULTRA-OPTIMIZED simulation with predicted G={G:.2e} Pa, ฮผ={mu:.4f} Paยทs")
|
| 315 |
+
|
| 316 |
+
# Use predicted values
|
| 317 |
+
self.G = G
|
| 318 |
+
self.mu = mu
|
| 319 |
+
|
| 320 |
+
# Setup (same as original)
|
| 321 |
+
if self.cav_type == 'LIC':
|
| 322 |
+
self.Rmax = self.lambdamax * self.R0
|
| 323 |
+
self.PA = 0
|
| 324 |
+
self.omega = 0
|
| 325 |
+
self.delta = 0
|
| 326 |
+
self.n = 0
|
| 327 |
+
|
| 328 |
+
if self.cav_type == 'LIC':
|
| 329 |
+
self.Rc = self.Rmax
|
| 330 |
+
self.Uc = np.sqrt(self.P_inf / self.rho)
|
| 331 |
+
self.tc = self.Rmax / self.Uc
|
| 332 |
+
self.tspan = 3 * self.tc # Reduced for speed
|
| 333 |
+
|
| 334 |
+
# Calculate parameters (same as original)
|
| 335 |
+
self.Pv = self.P_ref * np.exp(-self.T_ref / self.T_inf)
|
| 336 |
+
self.K_inf = self.A * self.T_inf + self.B
|
| 337 |
+
|
| 338 |
+
# Non-dimensional variables (same as original)
|
| 339 |
+
self.C_star = self.c_long / self.Uc
|
| 340 |
+
self.We = self.P_inf * self.Rc / (2 * self.gamma)
|
| 341 |
+
self.Ca = self.P_inf / self.G
|
| 342 |
+
self.Re = self.P_inf * self.Rc / (self.mu * self.Uc)
|
| 343 |
+
self.fom = self.D0 / (self.Uc * self.Rc)
|
| 344 |
+
self.chi = self.T_inf * self.K_inf / (self.P_inf * self.Rc * self.Uc)
|
| 345 |
+
self.A_star = self.A * self.T_inf / self.K_inf
|
| 346 |
+
self.B_star = self.B / self.K_inf
|
| 347 |
+
self.Pv_star = self.Pv / self.P_inf
|
| 348 |
+
|
| 349 |
+
self.tspan_star = self.tspan / self.tc
|
| 350 |
+
self.Req = self.R0 / self.Rmax
|
| 351 |
+
|
| 352 |
+
self.PA_star = self.PA / self.P_inf
|
| 353 |
+
self.omega_star = self.omega * self.tc
|
| 354 |
+
self.delta_star = self.delta / self.tc
|
| 355 |
+
|
| 356 |
+
# Parameters vector (same as original)
|
| 357 |
+
self.params = [self.NT, self.C_star, self.We, self.Ca, self.alpha, self.Re,
|
| 358 |
+
self.Rv, self.Ra, self.kappa, self.fom, self.chi, self.A_star,
|
| 359 |
+
self.B_star, self.Pv_star, self.Req, self.PA_star,
|
| 360 |
+
self.omega_star, self.delta_star, self.n]
|
| 361 |
+
|
| 362 |
+
# Initial conditions (same as original)
|
| 363 |
+
R0_star = 1
|
| 364 |
+
U0_star = 0
|
| 365 |
+
Theta0 = np.zeros(self.NT)
|
| 366 |
+
|
| 367 |
+
if self.cav_type == 'LIC':
|
| 368 |
+
P0 = (self.Pv + (self.P_inf + 2 * self.gamma / self.R0 - self.Pv) *
|
| 369 |
+
((self.R0 / self.Rmax) ** 3))
|
| 370 |
+
P0_star = P0 / self.P_inf
|
| 371 |
+
S0 = ((3 * self.alpha - 1) * (5 - 4 * self.Req - self.Req ** 4) / (2 * self.Ca) +
|
| 372 |
+
2 * self.alpha * (27 / 40 + self.Req ** 8 / 8 + self.Req ** 5 / 5 +
|
| 373 |
+
self.Req ** 2 - 2 / self.Req) / self.Ca)
|
| 374 |
+
k0 = ((1 + (self.Rv / self.Ra) * (P0_star / self.Pv_star - 1)) ** (-1)) * np.ones(self.NT)
|
| 375 |
+
|
| 376 |
+
X0 = np.concatenate([[R0_star, U0_star, P0_star, S0], Theta0, k0])
|
| 377 |
+
|
| 378 |
+
print(f"State vector size: {len(X0)} (4 + {self.NT} + {self.NT})")
|
| 379 |
+
print(f"Time span: 0 to {self.tspan_star:.4f}")
|
| 380 |
+
|
| 381 |
+
# CRITICAL FIX: NO UI UPDATES AT ALL - store them for later
|
| 382 |
+
self.simulation_status = "Starting simulation..."
|
| 383 |
+
|
| 384 |
+
# ULTRA-OPTIMIZED ODE solving - NO UI updates during solving
|
| 385 |
+
try:
|
| 386 |
+
sol = solve_ivp(
|
| 387 |
+
self.bubble_optimized,
|
| 388 |
+
[0, self.tspan_star],
|
| 389 |
+
X0,
|
| 390 |
+
method='BDF',
|
| 391 |
+
rtol=self.RelTol,
|
| 392 |
+
atol=1e-8,
|
| 393 |
+
max_step=self.tspan_star / 200,
|
| 394 |
+
dense_output=False
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
self.simulation_status = "Processing results..."
|
| 398 |
+
|
| 399 |
+
except Exception as e:
|
| 400 |
+
print(f"BDF failed: {str(e)}, trying LSODA...")
|
| 401 |
+
self.simulation_status = "Trying backup solver..."
|
| 402 |
+
try:
|
| 403 |
+
sol = solve_ivp(
|
| 404 |
+
self.bubble_optimized,
|
| 405 |
+
[0, self.tspan_star],
|
| 406 |
+
X0,
|
| 407 |
+
method='LSODA',
|
| 408 |
+
rtol=1e-4,
|
| 409 |
+
atol=1e-7,
|
| 410 |
+
max_step=self.tspan_star / 100,
|
| 411 |
+
)
|
| 412 |
+
except Exception as e2:
|
| 413 |
+
print(f"All solvers failed: {str(e2)}")
|
| 414 |
+
return self.fast_fallback()
|
| 415 |
+
|
| 416 |
+
if not sol.success:
|
| 417 |
+
print(f"Solver failed: {sol.message}")
|
| 418 |
+
return self.fast_fallback()
|
| 419 |
+
|
| 420 |
+
# Extract solution
|
| 421 |
+
t_nondim = sol.t
|
| 422 |
+
X_nondim = sol.y.T
|
| 423 |
+
R_nondim = X_nondim[:, 0]
|
| 424 |
+
|
| 425 |
+
# Filter valid solutions
|
| 426 |
+
valid_mask = (R_nondim > 0.01) & (R_nondim < 20) & np.isfinite(R_nondim)
|
| 427 |
+
t_nondim = t_nondim[valid_mask]
|
| 428 |
+
R_nondim = R_nondim[valid_mask]
|
| 429 |
+
|
| 430 |
+
if len(t_nondim) < 10:
|
| 431 |
+
print("Too few valid points, using fast fallback")
|
| 432 |
+
return self.fast_fallback()
|
| 433 |
+
|
| 434 |
+
# Back to physical units
|
| 435 |
+
t = t_nondim * self.tc
|
| 436 |
+
R = R_nondim * self.Rc
|
| 437 |
+
|
| 438 |
+
# Change units
|
| 439 |
+
scale = 1e4
|
| 440 |
+
t_newunit = t * scale
|
| 441 |
+
R_newunit = R * scale
|
| 442 |
+
|
| 443 |
+
self.simulation_status = "Simulation complete!"
|
| 444 |
+
|
| 445 |
+
print(f"ULTRA-OPTIMIZED simulation completed in {len(t_newunit)} points!")
|
| 446 |
+
print(f"Time range: {t_newunit[0]:.3f} to {t_newunit[-1]:.3f} (0.1 ms)")
|
| 447 |
+
print(f"Radius range: {np.min(R_newunit):.3f} to {np.max(R_newunit):.3f} (0.1 mm)")
|
| 448 |
+
|
| 449 |
+
return t_newunit, R_newunit
|
| 450 |
+
|
| 451 |
+
def bubble_optimized(self, t, x):
|
| 452 |
+
"""
|
| 453 |
+
OPTIMIZED bubble physics function - same physics, no UI updates
|
| 454 |
+
"""
|
| 455 |
+
# Extract parameters (same as original)
|
| 456 |
+
NT = int(self.params[0])
|
| 457 |
+
C_star = self.params[1]
|
| 458 |
+
We = self.params[2]
|
| 459 |
+
Ca = self.params[3]
|
| 460 |
+
alpha = self.params[4]
|
| 461 |
+
Re = self.params[5]
|
| 462 |
+
Rv = self.params[6]
|
| 463 |
+
Ra = self.params[7]
|
| 464 |
+
kappa = self.params[8]
|
| 465 |
+
fom = self.params[9]
|
| 466 |
+
chi = self.params[10]
|
| 467 |
+
A_star = self.params[11]
|
| 468 |
+
B_star = self.params[12]
|
| 469 |
+
Pv_star = self.params[13]
|
| 470 |
+
Req = self.params[14]
|
| 471 |
+
|
| 472 |
+
# Extract state variables (same as original)
|
| 473 |
+
R = x[0]
|
| 474 |
+
U = x[1]
|
| 475 |
+
P = x[2]
|
| 476 |
+
S = x[3]
|
| 477 |
+
Theta = x[4:4 + NT]
|
| 478 |
+
k = x[4 + NT:4 + 2 * NT]
|
| 479 |
+
|
| 480 |
+
# Grid setup (same physics, fewer points)
|
| 481 |
+
deltaY = 1 / (NT - 1)
|
| 482 |
+
ii = np.arange(1, NT + 1)
|
| 483 |
+
yk = (ii - 1) * deltaY
|
| 484 |
+
|
| 485 |
+
# Boundary condition (same as original)
|
| 486 |
+
k = k.copy()
|
| 487 |
+
k[-1] = (1 + (Rv / Ra) * (P / Pv_star - 1)) ** (-1)
|
| 488 |
+
|
| 489 |
+
# Calculate mixture fields (same physics)
|
| 490 |
+
T = (A_star - 1 + np.sqrt(1 + 2 * A_star * Theta)) / A_star
|
| 491 |
+
K_star = A_star * T + B_star
|
| 492 |
+
Rmix = k * Rv + (1 - k) * Ra
|
| 493 |
+
|
| 494 |
+
# OPTIMIZATION: Vectorized spatial derivatives for speed
|
| 495 |
+
DTheta = np.zeros(NT)
|
| 496 |
+
DDTheta = np.zeros(NT)
|
| 497 |
+
Dk = np.zeros(NT)
|
| 498 |
+
DDk = np.zeros(NT)
|
| 499 |
+
|
| 500 |
+
# Neumann BC at origin
|
| 501 |
+
DTheta[0] = 0
|
| 502 |
+
Dk[0] = 0
|
| 503 |
+
|
| 504 |
+
# Vectorized central differences (faster than loops)
|
| 505 |
+
if NT >= 3:
|
| 506 |
+
DTheta[1:-1] = (Theta[2:] - Theta[:-2]) / (2 * deltaY)
|
| 507 |
+
Dk[1:-1] = (k[2:] - k[:-2]) / (2 * deltaY)
|
| 508 |
+
|
| 509 |
+
# Backward difference at wall
|
| 510 |
+
DTheta[-1] = (3 * Theta[-1] - 4 * Theta[-2] + Theta[-3]) / (2 * deltaY)
|
| 511 |
+
Dk[-1] = (3 * k[-1] - 4 * k[-2] + k[-3]) / (2 * deltaY)
|
| 512 |
+
|
| 513 |
+
# Laplacians (vectorized where possible)
|
| 514 |
+
DDTheta[0] = 6 * (Theta[1] - Theta[0]) / deltaY ** 2
|
| 515 |
+
DDk[0] = 6 * (k[1] - k[0]) / deltaY ** 2
|
| 516 |
+
|
| 517 |
+
if NT >= 3:
|
| 518 |
+
# Vectorized Laplacian calculation
|
| 519 |
+
for i in range(1, NT - 1):
|
| 520 |
+
DDTheta[i] = ((Theta[i + 1] - 2 * Theta[i] + Theta[i - 1]) / deltaY ** 2 +
|
| 521 |
+
(2 / yk[i]) * DTheta[i] if yk[i] > 1e-12 else
|
| 522 |
+
(Theta[i + 1] - 2 * Theta[i] + Theta[i - 1]) / deltaY ** 2)
|
| 523 |
+
DDk[i] = ((k[i + 1] - 2 * k[i] + k[i - 1]) / deltaY ** 2 +
|
| 524 |
+
(2 / yk[i]) * Dk[i] if yk[i] > 1e-12 else
|
| 525 |
+
(k[i + 1] - 2 * k[i] + k[i - 1]) / deltaY ** 2)
|
| 526 |
+
|
| 527 |
+
if NT >= 4:
|
| 528 |
+
DDTheta[-1] = ((2 * Theta[-1] - 5 * Theta[-2] + 4 * Theta[-3] - Theta[-4]) / deltaY ** 2 +
|
| 529 |
+
(2 / yk[-1]) * DTheta[-1] if yk[-1] > 1e-12 else
|
| 530 |
+
(2 * Theta[-1] - 5 * Theta[-2] + 4 * Theta[-3] - Theta[-4]) / deltaY ** 2)
|
| 531 |
+
DDk[-1] = ((2 * k[-1] - 5 * k[-2] + 4 * k[-3] - k[-4]) / deltaY ** 2 +
|
| 532 |
+
(2 / yk[-1]) * Dk[-1] if yk[-1] > 1e-12 else
|
| 533 |
+
(2 * k[-1] - 5 * k[-2] + 4 * k[-3] - k[-4]) / deltaY ** 2)
|
| 534 |
+
|
| 535 |
+
# Internal pressure evolution (same physics)
|
| 536 |
+
if Rmix[-1] > 1e-12 and (1 - k[-1]) > 1e-12 and R > 1e-12:
|
| 537 |
+
pdot = (3 / R * (-kappa * P * U + (kappa - 1) * chi * DTheta[-1] / R +
|
| 538 |
+
kappa * P * fom * Rv * Dk[-1] / (R * Rmix[-1] * (1 - k[-1]))))
|
| 539 |
+
else:
|
| 540 |
+
pdot = -3 * kappa * P * U / R if R > 1e-12 else 0
|
| 541 |
+
|
| 542 |
+
# OPTIMIZATION: Vectorized mixture velocity calculation
|
| 543 |
+
Umix = np.zeros(NT)
|
| 544 |
+
valid_indices = (Rmix > 1e-12) & (kappa * P > 1e-12)
|
| 545 |
+
|
| 546 |
+
if np.any(valid_indices):
|
| 547 |
+
idx = np.where(valid_indices)[0]
|
| 548 |
+
Umix[idx] = (((kappa - 1) * chi / R * DTheta[idx] - R * yk[idx] * pdot / 3) / (kappa * P) +
|
| 549 |
+
fom / R * (Rv - Ra) / Rmix[idx] * Dk[idx])
|
| 550 |
+
|
| 551 |
+
# Temperature evolution (vectorized where possible)
|
| 552 |
+
Theta_prime = np.zeros(NT)
|
| 553 |
+
valid_P = P > 1e-12
|
| 554 |
+
if valid_P:
|
| 555 |
+
for i in range(NT - 1): # Exclude wall point
|
| 556 |
+
if Rmix[i] > 1e-12:
|
| 557 |
+
Theta_prime[i] = (
|
| 558 |
+
(pdot + DDTheta[i] * chi / R ** 2) * (K_star[i] * T[i] / P * (kappa - 1) / kappa) -
|
| 559 |
+
DTheta[i] * (Umix[i] - yk[i] * U) / R +
|
| 560 |
+
fom / R ** 2 * (Rv - Ra) / Rmix[i] * Dk[i] * DTheta[i])
|
| 561 |
+
Theta_prime[-1] = 0 # Dirichlet BC
|
| 562 |
+
|
| 563 |
+
# Vapor concentration evolution (vectorized where possible)
|
| 564 |
+
k_prime = np.zeros(NT)
|
| 565 |
+
for i in range(NT - 1): # Exclude wall point
|
| 566 |
+
if Rmix[i] > 1e-12 and T[i] > 1e-12:
|
| 567 |
+
term1 = fom / R ** 2 * (DDk[i] + Dk[i] * (-((Rv - Ra) / Rmix[i]) * Dk[i] -
|
| 568 |
+
DTheta[i] / np.sqrt(1 + 2 * A_star * Theta[i]) / T[i]))
|
| 569 |
+
term2 = -(Umix[i] - U * yk[i]) / R * Dk[i]
|
| 570 |
+
k_prime[i] = term1 + term2
|
| 571 |
+
k_prime[-1] = 0 # Dirichlet BC
|
| 572 |
+
|
| 573 |
+
# Elastic stress evolution (same physics)
|
| 574 |
+
if self.cav_type == 'LIC':
|
| 575 |
+
if Req > 1e-12:
|
| 576 |
+
Rst = R / Req
|
| 577 |
+
if Rst > 1e-12:
|
| 578 |
+
Sdot = (2 * U / R * (3 * alpha - 1) * (1 / Rst + 1 / Rst ** 4) / Ca -
|
| 579 |
+
2 * alpha * U / R * (1 / Rst ** 8 + 1 / Rst ** 5 + 2 / Rst ** 2 + 2 * Rst) / Ca)
|
| 580 |
+
else:
|
| 581 |
+
Sdot = 0
|
| 582 |
+
else:
|
| 583 |
+
Sdot = 0
|
| 584 |
+
|
| 585 |
+
# Keller-Miksis equations (same physics)
|
| 586 |
+
rdot = U
|
| 587 |
+
|
| 588 |
+
if R > 1e-12:
|
| 589 |
+
numerator = ((1 + U / C_star) * (P - 1 / (We * R) + S - 4 * U / (Re * R) - 1) +
|
| 590 |
+
R / C_star * (pdot + U / (We * R ** 2) + Sdot + 4 * U ** 2 / (Re * R ** 2)) -
|
| 591 |
+
(3 / 2) * (1 - U / (3 * C_star)) * U ** 2)
|
| 592 |
+
denominator = (1 - U / C_star) * R + 4 / (C_star * Re)
|
| 593 |
+
|
| 594 |
+
if abs(denominator) > 1e-12:
|
| 595 |
+
udot = numerator / denominator
|
| 596 |
+
else:
|
| 597 |
+
udot = 0
|
| 598 |
+
else:
|
| 599 |
+
udot = 0
|
| 600 |
+
|
| 601 |
+
# Return derivatives
|
| 602 |
+
dxdt = np.concatenate([[rdot, udot, pdot, Sdot], Theta_prime, k_prime])
|
| 603 |
+
|
| 604 |
+
return dxdt
|
| 605 |
+
|
| 606 |
+
def fast_fallback(self):
|
| 607 |
+
"""Faster fallback for validation"""
|
| 608 |
+
print("Using fast analytical approximation for validation")
|
| 609 |
+
|
| 610 |
+
# Quick analytical approximation based on Rayleigh-Plesset equation
|
| 611 |
+
t_nondim = np.linspace(0, 3, 200) # Fewer points for speed
|
| 612 |
+
|
| 613 |
+
# Simple damped oscillation model using actual parameters
|
| 614 |
+
if hasattr(self, 'P_inf') and hasattr(self, 'rho') and hasattr(self, 'lambdamax'):
|
| 615 |
+
Rmax = self.lambdamax * self.R0 # Use dynamic lambda value
|
| 616 |
+
omega_natural = np.sqrt(3 * self.P_inf / (self.rho * Rmax ** 2))
|
| 617 |
+
damping = self.mu / (self.rho * Rmax ** 2) if hasattr(self, 'mu') else 0.1
|
| 618 |
+
else:
|
| 619 |
+
omega_natural = 1000
|
| 620 |
+
damping = 0.1
|
| 621 |
+
|
| 622 |
+
# Analytical solution approximation
|
| 623 |
+
omega_d = omega_natural * np.sqrt(1 - damping ** 2) if damping < 1 else omega_natural
|
| 624 |
+
decay = np.exp(-damping * omega_natural * t_nondim * self.tc)
|
| 625 |
+
|
| 626 |
+
R_nondim = self.Req + (1 - self.Req) * decay * np.cos(omega_d * t_nondim * self.tc)
|
| 627 |
+
R_nondim = np.maximum(R_nondim, 0.05) # Prevent negative values
|
| 628 |
+
|
| 629 |
+
# Convert to physical units
|
| 630 |
+
t = t_nondim * self.tc
|
| 631 |
+
R = R_nondim * self.Rc
|
| 632 |
+
scale = 1e4
|
| 633 |
+
|
| 634 |
+
return t * scale, R * scale
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
# Main Streamlit App
|
| 638 |
+
def main():
|
| 639 |
+
# Header - ULTRA-STABLE with fixed positioning and UT Austin branding + Logo
|
| 640 |
+
st.markdown("""
|
| 641 |
+
<div class="main-header">
|
| 642 |
+
<img src="https://huggingface.co/spaces/lehuaaa/Bubble/resolve/main/src/group_logo.JPG"
|
| 643 |
+
alt="YANG Research Group Logo"
|
| 644 |
+
class="header-logo">
|
| 645 |
+
<h1 class="header-text">Bubble Dynamics Analysis</h1>
|
| 646 |
+
</div>
|
| 647 |
+
""", unsafe_allow_html=True)
|
| 648 |
+
|
| 649 |
+
# Initialize current page in session state
|
| 650 |
+
if 'current_page' not in st.session_state:
|
| 651 |
+
st.session_state.current_page = "๐ Home"
|
| 652 |
+
|
| 653 |
+
# Sidebar for navigation with clickable menu
|
| 654 |
+
st.sidebar.title("๐ Navigation")
|
| 655 |
+
|
| 656 |
+
# Create clickable menu buttons
|
| 657 |
+
menu_items = [
|
| 658 |
+
"๐ Home",
|
| 659 |
+
"๐ Data Loading",
|
| 660 |
+
"โ๏ธ Data Processing",
|
| 661 |
+
"๐ค ML Prediction",
|
| 662 |
+
"โ
Validation",
|
| 663 |
+
"๐ Results & Export"
|
| 664 |
+
]
|
| 665 |
+
|
| 666 |
+
# Display menu buttons
|
| 667 |
+
for item in menu_items:
|
| 668 |
+
# Check if this is the current page to highlight it
|
| 669 |
+
if st.session_state.current_page == item:
|
| 670 |
+
# Use different styling for active page
|
| 671 |
+
if st.sidebar.button(f"โถ๏ธ {item}", key=f"nav_{item}", use_container_width=True):
|
| 672 |
+
st.session_state.current_page = item
|
| 673 |
+
st.rerun()
|
| 674 |
+
else:
|
| 675 |
+
if st.sidebar.button(f" {item}", key=f"nav_{item}", use_container_width=True):
|
| 676 |
+
st.session_state.current_page = item
|
| 677 |
+
st.rerun()
|
| 678 |
+
|
| 679 |
+
# Add some spacing
|
| 680 |
+
st.sidebar.markdown("---")
|
| 681 |
+
|
| 682 |
+
# Show current status in sidebar
|
| 683 |
+
st.sidebar.markdown("### ๐ Status")
|
| 684 |
+
if st.session_state.data_loaded:
|
| 685 |
+
st.sidebar.success("โ
Data loaded")
|
| 686 |
+
else:
|
| 687 |
+
st.sidebar.info("๐ No data loaded")
|
| 688 |
+
|
| 689 |
+
if st.session_state.processed_data:
|
| 690 |
+
st.sidebar.success("โ
Data processed")
|
| 691 |
+
else:
|
| 692 |
+
st.sidebar.info("โ๏ธ Data not processed")
|
| 693 |
+
|
| 694 |
+
if st.session_state.model_loaded:
|
| 695 |
+
st.sidebar.success("โ
Model loaded")
|
| 696 |
+
else:
|
| 697 |
+
st.sidebar.info("๐ค No model loaded")
|
| 698 |
+
|
| 699 |
+
# Display the selected page
|
| 700 |
+
page = st.session_state.current_page
|
| 701 |
+
|
| 702 |
+
if page == "๐ Home":
|
| 703 |
+
show_home()
|
| 704 |
+
elif page == "๐ Data Loading":
|
| 705 |
+
show_data_loading()
|
| 706 |
+
elif page == "โ๏ธ Data Processing":
|
| 707 |
+
show_data_processing()
|
| 708 |
+
elif page == "๐ค ML Prediction":
|
| 709 |
+
show_ml_prediction()
|
| 710 |
+
elif page == "โ
Validation":
|
| 711 |
+
show_validation()
|
| 712 |
+
elif page == "๐ Results & Export":
|
| 713 |
+
show_results()
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def show_home():
|
| 717 |
+
"""Home page with overview"""
|
| 718 |
+
col1, col2 = st.columns([2, 1])
|
| 719 |
+
|
| 720 |
+
with col1:
|
| 721 |
+
st.markdown("""
|
| 722 |
+
### Welcome to the YANG Research Group Bubble Dynamics Analysis Platform
|
| 723 |
+
|
| 724 |
+
**The University of Texas at Austin - Aerospace Engineering and Engineering Mechanics**
|
| 725 |
+
**Cockrell School of Engineering**
|
| 726 |
+
|
| 727 |
+
This advanced web application provides comprehensive tools for analyzing bubble dynamics data:
|
| 728 |
+
|
| 729 |
+
**Features:**
|
| 730 |
+
- ๐ **Data Loading**: Upload and analyze .mat files containing bubble dynamics data
|
| 731 |
+
- โ๏ธ **Data Processing**: Interpolate and process experimental data
|
| 732 |
+
- ๐ค **ML Prediction**: Use machine learning to predict material properties (G & ฮผ)
|
| 733 |
+
- โ
**Validation**: Compare experimental vs simulated bubble behavior
|
| 734 |
+
- ๐ **Export**: Download processed results and visualizations
|
| 735 |
+
|
| 736 |
+
**Getting Started:**
|
| 737 |
+
1. Navigate to "Data Loading" to upload your .mat file
|
| 738 |
+
2. Process your data in "Data Processing"
|
| 739 |
+
3. Use ML models in "ML Prediction"
|
| 740 |
+
4. Validate results in "Validation"
|
| 741 |
+
5. Export your findings in "Results & Export"
|
| 742 |
+
""")
|
| 743 |
+
|
| 744 |
+
with col2:
|
| 745 |
+
if TENSORFLOW_AVAILABLE:
|
| 746 |
+
st.success("""
|
| 747 |
+
**โ
System Status: Full Features Available**
|
| 748 |
+
|
| 749 |
+
โ
Core Features: Ready
|
| 750 |
+
โ
Data Processing: Ready
|
| 751 |
+
โ
Visualization: Ready
|
| 752 |
+
โ
ML Models: Ready
|
| 753 |
+
โ
Simulations: Ready
|
| 754 |
+
""")
|
| 755 |
+
else:
|
| 756 |
+
st.warning("""
|
| 757 |
+
**โ ๏ธ System Status: Limited Features**
|
| 758 |
+
|
| 759 |
+
โ
Core Features: Ready
|
| 760 |
+
โ
Data Processing: Ready
|
| 761 |
+
โ
Visualization: Ready
|
| 762 |
+
โ ML Models: TensorFlow not installed
|
| 763 |
+
โ
Simulations: Ready
|
| 764 |
+
|
| 765 |
+
๐ก Install TensorFlow to enable ML predictions:
|
| 766 |
+
`pip install tensorflow-cpu`
|
| 767 |
+
""")
|
| 768 |
+
|
| 769 |
+
# Show current session state
|
| 770 |
+
if st.session_state.data_loaded:
|
| 771 |
+
st.success("๐ Data loaded successfully")
|
| 772 |
+
if st.session_state.processed_data:
|
| 773 |
+
st.success("โ๏ธ Data processed")
|
| 774 |
+
if st.session_state.model_loaded:
|
| 775 |
+
st.success("๐ค ML model loaded")
|
| 776 |
+
|
| 777 |
+
|
| 778 |
+
def show_data_loading():
|
| 779 |
+
"""Data loading interface"""
|
| 780 |
+
st.markdown('<h2 class="section-header">๐ Data Loading</h2>', unsafe_allow_html=True)
|
| 781 |
+
|
| 782 |
+
uploaded_file = st.file_uploader(
|
| 783 |
+
"Upload your .mat file",
|
| 784 |
+
type=['mat'],
|
| 785 |
+
help="Upload a MATLAB .mat file containing 'R_nondim_All', 't_nondim_All', and 'lambda_max_mean'"
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
if uploaded_file is not None:
|
| 789 |
+
try:
|
| 790 |
+
# Save uploaded file temporarily
|
| 791 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mat') as tmp_file:
|
| 792 |
+
tmp_file.write(uploaded_file.getvalue())
|
| 793 |
+
tmp_file_path = tmp_file.name
|
| 794 |
+
|
| 795 |
+
# Load the .mat file
|
| 796 |
+
data = loadmat(tmp_file_path)
|
| 797 |
+
|
| 798 |
+
# Check required variables
|
| 799 |
+
required_vars = ['R_nondim_All', 't_nondim_All']
|
| 800 |
+
missing_vars = [var for var in required_vars if var not in data]
|
| 801 |
+
|
| 802 |
+
if missing_vars:
|
| 803 |
+
st.error(f"Missing required variables: {missing_vars}")
|
| 804 |
+
return
|
| 805 |
+
|
| 806 |
+
# Extract data
|
| 807 |
+
R_nondim_all = data['R_nondim_All']
|
| 808 |
+
t_nondim_all = data['t_nondim_All']
|
| 809 |
+
num_datasets = R_nondim_all.shape[1]
|
| 810 |
+
|
| 811 |
+
# Extract lambda_max_mean
|
| 812 |
+
if 'lambda_max_mean' in data:
|
| 813 |
+
lambda_max_mean = float(data['lambda_max_mean'])
|
| 814 |
+
else:
|
| 815 |
+
st.warning("lambda_max_mean not found in file. Using default value 5.99")
|
| 816 |
+
lambda_max_mean = 5.99
|
| 817 |
+
|
| 818 |
+
# Store in session state
|
| 819 |
+
st.session_state.data = data
|
| 820 |
+
st.session_state.R_nondim_all = R_nondim_all
|
| 821 |
+
st.session_state.t_nondim_all = t_nondim_all
|
| 822 |
+
st.session_state.lambda_max_mean = lambda_max_mean
|
| 823 |
+
st.session_state.num_datasets = num_datasets
|
| 824 |
+
st.session_state.data_loaded = True
|
| 825 |
+
|
| 826 |
+
# Calculate physical parameters
|
| 827 |
+
R0_sim = 35e-6
|
| 828 |
+
P_inf_exp = 101325
|
| 829 |
+
rho_exp = 1000
|
| 830 |
+
|
| 831 |
+
Rmax_exp = lambda_max_mean * R0_sim
|
| 832 |
+
Rc_exp = Rmax_exp
|
| 833 |
+
Uc_exp = np.sqrt(P_inf_exp / rho_exp)
|
| 834 |
+
tc_exp = Rc_exp / Uc_exp
|
| 835 |
+
|
| 836 |
+
st.session_state.physical_params = {
|
| 837 |
+
'Rmax_exp': Rmax_exp,
|
| 838 |
+
'Rc_exp': Rc_exp,
|
| 839 |
+
'Uc_exp': Uc_exp,
|
| 840 |
+
'tc_exp': tc_exp
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
# Display success message
|
| 844 |
+
st.markdown(f"""
|
| 845 |
+
<div class="success-box">
|
| 846 |
+
<strong>โ
Data loaded successfully!</strong><br>
|
| 847 |
+
๐ Datasets found: {num_datasets}<br>
|
| 848 |
+
๐ฏ Lambda max: {lambda_max_mean:.3f}<br>
|
| 849 |
+
๐ Physical parameters calculated
|
| 850 |
+
</div>
|
| 851 |
+
""", unsafe_allow_html=True)
|
| 852 |
+
|
| 853 |
+
# Show data preview
|
| 854 |
+
col1, col2 = st.columns(2)
|
| 855 |
+
|
| 856 |
+
with col1:
|
| 857 |
+
st.subheader("๐ Data Overview")
|
| 858 |
+
st.write(f"**Number of datasets:** {num_datasets}")
|
| 859 |
+
st.write(f"**Lambda max mean:** {lambda_max_mean:.3f}")
|
| 860 |
+
st.write(f"**Data shape:** {R_nondim_all.shape}")
|
| 861 |
+
|
| 862 |
+
with col2:
|
| 863 |
+
st.subheader("๐ง Physical Parameters")
|
| 864 |
+
st.write(f"**R_max:** {Rmax_exp * 1e6:.1f} ฮผm")
|
| 865 |
+
st.write(f"**Time scale:** {tc_exp * 1e6:.1f} ฮผs")
|
| 866 |
+
st.write(f"**Velocity scale:** {Uc_exp:.1f} m/s")
|
| 867 |
+
|
| 868 |
+
# Clean up temporary file
|
| 869 |
+
os.unlink(tmp_file_path)
|
| 870 |
+
|
| 871 |
+
except Exception as e:
|
| 872 |
+
st.error(f"Error loading file: {str(e)}")
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def show_data_processing():
|
| 876 |
+
"""Data processing interface"""
|
| 877 |
+
st.markdown('<h2 class="section-header">โ๏ธ Data Processing</h2>', unsafe_allow_html=True)
|
| 878 |
+
|
| 879 |
+
if not st.session_state.data_loaded:
|
| 880 |
+
st.warning("Please load data first in the 'Data Loading' section.")
|
| 881 |
+
return
|
| 882 |
+
|
| 883 |
+
# Dataset selection
|
| 884 |
+
dataset_idx = st.selectbox(
|
| 885 |
+
"Select dataset to process:",
|
| 886 |
+
range(st.session_state.num_datasets),
|
| 887 |
+
format_func=lambda x: f"Dataset {x + 1}"
|
| 888 |
+
)
|
| 889 |
+
|
| 890 |
+
# Processing parameters
|
| 891 |
+
col1, col2 = st.columns(2)
|
| 892 |
+
with col1:
|
| 893 |
+
interp_range = st.number_input(
|
| 894 |
+
"Interpolation Range",
|
| 895 |
+
min_value=0.1,
|
| 896 |
+
max_value=2.0,
|
| 897 |
+
value=0.8,
|
| 898 |
+
step=0.1,
|
| 899 |
+
help="Time range for interpolation"
|
| 900 |
+
)
|
| 901 |
+
|
| 902 |
+
with col2:
|
| 903 |
+
time_step = st.number_input(
|
| 904 |
+
"Time Step",
|
| 905 |
+
min_value=0.001,
|
| 906 |
+
max_value=0.1,
|
| 907 |
+
value=0.008,
|
| 908 |
+
step=0.001,
|
| 909 |
+
format="%.3f",
|
| 910 |
+
help="Time step for interpolation"
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
if st.button("๐ Process Data", type="primary"):
|
| 914 |
+
with st.spinner("Processing data..."):
|
| 915 |
+
try:
|
| 916 |
+
# Extract data for selected dataset
|
| 917 |
+
R_nondim_exp = np.array(st.session_state.R_nondim_all[0, dataset_idx]).flatten()
|
| 918 |
+
t_nondim_exp = np.array(st.session_state.t_nondim_all[0, dataset_idx]).flatten()
|
| 919 |
+
|
| 920 |
+
# Find zero index
|
| 921 |
+
zero_candidates = np.where(np.abs(t_nondim_exp) < 1e-10)[0]
|
| 922 |
+
if len(zero_candidates) > 0:
|
| 923 |
+
zero_idx = zero_candidates[0]
|
| 924 |
+
else:
|
| 925 |
+
zero_idx = np.argmin(np.abs(t_nondim_exp))
|
| 926 |
+
|
| 927 |
+
# Convert to physical units
|
| 928 |
+
tc_exp = st.session_state.physical_params['tc_exp']
|
| 929 |
+
Rc_exp = st.session_state.physical_params['Rc_exp']
|
| 930 |
+
|
| 931 |
+
t_exp = t_nondim_exp * tc_exp
|
| 932 |
+
R_exp = R_nondim_exp * Rc_exp
|
| 933 |
+
|
| 934 |
+
# Process from zero point
|
| 935 |
+
t_fromzero = t_exp[zero_idx:].flatten()
|
| 936 |
+
R_frommax = R_exp[zero_idx:].flatten()
|
| 937 |
+
|
| 938 |
+
# Scale to new units
|
| 939 |
+
scale_exp = 1e4
|
| 940 |
+
t_newunit_exp = t_fromzero * scale_exp
|
| 941 |
+
R_newunit_exp = R_frommax * scale_exp
|
| 942 |
+
|
| 943 |
+
# Sort data
|
| 944 |
+
sort_indices = np.argsort(t_newunit_exp)
|
| 945 |
+
t_newunit_exp = t_newunit_exp[sort_indices]
|
| 946 |
+
R_newunit_exp = R_newunit_exp[sort_indices]
|
| 947 |
|
| 948 |
+
# Interpolate
|
| 949 |
+
t_interp_newunit = np.arange(0, interp_range + time_step, time_step)
|
| 950 |
+
pchip_interpolator = PchipInterpolator(t_newunit_exp, R_newunit_exp)
|
| 951 |
+
R_interp_newunit = pchip_interpolator(t_interp_newunit)
|
| 952 |
|
| 953 |
+
# Store results
|
| 954 |
+
st.session_state.t_interp_newunit = t_interp_newunit
|
| 955 |
+
st.session_state.R_interp_newunit = R_interp_newunit
|
| 956 |
+
st.session_state.t_original = t_newunit_exp
|
| 957 |
+
st.session_state.R_original = R_newunit_exp
|
| 958 |
+
st.session_state.processed_data = True
|
| 959 |
+
st.session_state.selected_dataset = dataset_idx
|
| 960 |
+
|
| 961 |
+
st.success(f"โ
Data processed successfully! {len(R_interp_newunit)} interpolated points created.")
|
| 962 |
+
|
| 963 |
+
except Exception as e:
|
| 964 |
+
st.error(f"Processing failed: {str(e)}")
|
| 965 |
+
|
| 966 |
+
# Show results if data is processed
|
| 967 |
+
if st.session_state.processed_data:
|
| 968 |
+
st.subheader("๐ Processing Results")
|
| 969 |
+
|
| 970 |
+
# Create plot
|
| 971 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
| 972 |
+
|
| 973 |
+
# Original vs interpolated
|
| 974 |
+
ax1.plot(st.session_state.t_original, st.session_state.R_original, 'b-', linewidth=2, label='Original Data')
|
| 975 |
+
ax1.plot(st.session_state.t_interp_newunit, st.session_state.R_interp_newunit, 'ro', markersize=3,
|
| 976 |
+
label='Interpolated Points')
|
| 977 |
+
ax1.set_xlabel('Time (0.1 ms)')
|
| 978 |
+
ax1.set_ylabel('Radius (0.1 mm)')
|
| 979 |
+
ax1.set_title('Original vs Interpolated Data')
|
| 980 |
+
ax1.grid(True, alpha=0.3)
|
| 981 |
+
ax1.legend()
|
| 982 |
+
|
| 983 |
+
# Interpolated data only
|
| 984 |
+
ax2.plot(st.session_state.t_interp_newunit, st.session_state.R_interp_newunit, 'ro', markersize=4)
|
| 985 |
+
ax2.set_xlabel('Time (0.1 ms)')
|
| 986 |
+
ax2.set_ylabel('Radius (0.1 mm)')
|
| 987 |
+
ax2.set_title('Interpolated R-t Curve')
|
| 988 |
+
ax2.grid(True, alpha=0.3)
|
| 989 |
+
|
| 990 |
+
plt.tight_layout()
|
| 991 |
+
st.pyplot(fig)
|
| 992 |
+
|
| 993 |
+
# Download processed data
|
| 994 |
+
if st.button("๐พ Download Processed Data"):
|
| 995 |
+
# Create download data
|
| 996 |
+
data_str = ' '.join([f'{val:.6f}' for val in st.session_state.R_interp_newunit[:-1]])
|
| 997 |
+
st.download_button(
|
| 998 |
+
label="๐ฅ Download as TXT",
|
| 999 |
+
data=data_str,
|
| 1000 |
+
file_name=f"interpolated_data_dataset_{dataset_idx + 1}.txt",
|
| 1001 |
+
mime="text/plain"
|
| 1002 |
+
)
|
| 1003 |
+
|
| 1004 |
+
|
| 1005 |
+
def show_ml_prediction():
|
| 1006 |
+
"""ML prediction interface - MODIFIED FOR FILE UPLOAD"""
|
| 1007 |
+
st.markdown('<h2 class="section-header">๐ค ML Prediction</h2>', unsafe_allow_html=True)
|
| 1008 |
+
|
| 1009 |
+
if not TENSORFLOW_AVAILABLE:
|
| 1010 |
+
st.error("โ **TensorFlow not available.** ML prediction features are disabled.")
|
| 1011 |
+
|
| 1012 |
+
with st.expander("๐ง How to Enable ML Features", expanded=True):
|
| 1013 |
+
st.markdown("""
|
| 1014 |
+
**To enable ML predictions:**
|
| 1015 |
+
|
| 1016 |
+
1. **Install TensorFlow:**
|
| 1017 |
+
```bash
|
| 1018 |
+
pip install tensorflow-cpu # Recommended (smaller)
|
| 1019 |
+
# or
|
| 1020 |
+
pip install tensorflow # Full version
|
| 1021 |
+
```
|
| 1022 |
+
|
| 1023 |
+
2. **Restart the web app:**
|
| 1024 |
+
- Stop the app (Ctrl+C in terminal)
|
| 1025 |
+
- Run: `streamlit run streamlit_bubble_app.py`
|
| 1026 |
+
- Refresh your browser
|
| 1027 |
+
""")
|
| 1028 |
+
return
|
| 1029 |
+
|
| 1030 |
+
if not st.session_state.processed_data:
|
| 1031 |
+
st.warning("Please process data first in the 'Data Processing' section.")
|
| 1032 |
+
return
|
| 1033 |
+
|
| 1034 |
+
# Model file upload section (MODIFIED)
|
| 1035 |
+
st.subheader("๐ Upload ML Model")
|
| 1036 |
+
|
| 1037 |
+
col1, col2 = st.columns([2, 1])
|
| 1038 |
+
|
| 1039 |
+
with col1:
|
| 1040 |
+
st.markdown("**Upload your trained model files:**")
|
| 1041 |
+
|
| 1042 |
+
# Primary model file upload
|
| 1043 |
+
uploaded_model = st.file_uploader(
|
| 1044 |
+
"Upload model file (.h5, .keras, or .zip for SavedModel)",
|
| 1045 |
+
type=['h5', 'keras', 'zip'],
|
| 1046 |
+
help="Upload your trained model in H5, Keras, or ZIP format (for SavedModel)",
|
| 1047 |
+
key="model_file_upload"
|
| 1048 |
+
)
|
| 1049 |
+
|
| 1050 |
+
# Optional config file upload
|
| 1051 |
+
uploaded_config = st.file_uploader(
|
| 1052 |
+
"Upload model config (optional)",
|
| 1053 |
+
type=['npy'],
|
| 1054 |
+
help="Upload model_config.npy if available",
|
| 1055 |
+
key="config_file_upload"
|
| 1056 |
+
)
|
| 1057 |
+
|
| 1058 |
+
with col2:
|
| 1059 |
+
if st.button("๐ Model Format Help"):
|
| 1060 |
+
st.info("""
|
| 1061 |
+
**Supported model formats:**
|
| 1062 |
+
|
| 1063 |
+
**๐ฏ H5 Format (.h5)** - Recommended
|
| 1064 |
+
- Single file containing model architecture and weights
|
| 1065 |
+
- Most compatible format
|
| 1066 |
+
|
| 1067 |
+
**โก Keras Format (.keras)**
|
| 1068 |
+
- Native Keras 3.0 format
|
| 1069 |
+
- Single file format
|
| 1070 |
+
|
| 1071 |
+
**๐ฆ SavedModel (.zip)**
|
| 1072 |
+
- Zip the entire SavedModel folder
|
| 1073 |
+
- Should contain: saved_model.pb, variables/, assets/
|
| 1074 |
+
|
| 1075 |
+
**โ๏ธ Config File (.npy)** - Optional
|
| 1076 |
+
- Contains model configuration metadata
|
| 1077 |
+
- Helps with model information display
|
| 1078 |
+
""")
|
| 1079 |
+
|
| 1080 |
+
# Model loading with uploaded files (MODIFIED)
|
| 1081 |
+
if uploaded_model is not None and st.button("๐ Load Uploaded Model", type="primary"):
|
| 1082 |
+
with st.spinner("Loading uploaded ML model..."):
|
| 1083 |
+
try:
|
| 1084 |
+
# Create temporary directory for uploaded files
|
| 1085 |
+
temp_dir = tempfile.mkdtemp()
|
| 1086 |
+
model_loaded = False
|
| 1087 |
+
loading_method = "Unknown"
|
| 1088 |
+
model = None
|
| 1089 |
+
|
| 1090 |
+
# Get file extension
|
| 1091 |
+
file_extension = uploaded_model.name.split('.')[-1].lower()
|
| 1092 |
+
|
| 1093 |
+
# Define custom layers exactly matching your training code
|
| 1094 |
+
class CustomMultiHeadAttention(layers.Layer):
|
| 1095 |
+
def __init__(self, embed_dim, num_heads=8, **kwargs):
|
| 1096 |
+
super(CustomMultiHeadAttention, self).__init__(**kwargs)
|
| 1097 |
+
self.embed_dim = embed_dim
|
| 1098 |
+
self.num_heads = num_heads
|
| 1099 |
+
self.projection_dim = embed_dim // num_heads
|
| 1100 |
+
self.query_dense = layers.Dense(embed_dim)
|
| 1101 |
+
self.key_dense = layers.Dense(embed_dim)
|
| 1102 |
+
self.value_dense = layers.Dense(embed_dim)
|
| 1103 |
+
self.combine_heads = layers.Dense(embed_dim)
|
| 1104 |
+
|
| 1105 |
+
def attention(self, query, key, value):
|
| 1106 |
+
score = tf.matmul(query, key, transpose_b=True)
|
| 1107 |
+
dim_key = tf.cast(tf.shape(key)[-1], tf.float32)
|
| 1108 |
+
scaled_score = score / tf.math.sqrt(dim_key)
|
| 1109 |
+
weights = tf.nn.softmax(scaled_score, axis=-1)
|
| 1110 |
+
output = tf.matmul(weights, value)
|
| 1111 |
+
return output, weights
|
| 1112 |
+
|
| 1113 |
+
def separate_heads(self, x, batch_size):
|
| 1114 |
+
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.projection_dim))
|
| 1115 |
+
return tf.transpose(x, perm=[0, 2, 1, 3])
|
| 1116 |
+
|
| 1117 |
+
def call(self, inputs):
|
| 1118 |
+
batch_size = tf.shape(inputs)[0]
|
| 1119 |
+
query = self.query_dense(inputs)
|
| 1120 |
+
key = self.key_dense(inputs)
|
| 1121 |
+
value = self.value_dense(inputs)
|
| 1122 |
+
query = self.separate_heads(query, batch_size)
|
| 1123 |
+
key = self.separate_heads(key, batch_size)
|
| 1124 |
+
value = self.separate_heads(value, batch_size)
|
| 1125 |
+
attention, weights = self.attention(query, key, value)
|
| 1126 |
+
attention = tf.transpose(attention, perm=[0, 2, 1, 3])
|
| 1127 |
+
concat_attention = tf.reshape(attention, (batch_size, -1, self.embed_dim))
|
| 1128 |
+
output = self.combine_heads(concat_attention)
|
| 1129 |
+
return output
|
| 1130 |
+
|
| 1131 |
+
def get_config(self):
|
| 1132 |
+
config = super(CustomMultiHeadAttention, self).get_config()
|
| 1133 |
+
config.update({
|
| 1134 |
+
'embed_dim': self.embed_dim,
|
| 1135 |
+
'num_heads': self.num_heads,
|
| 1136 |
+
})
|
| 1137 |
+
return config
|
| 1138 |
+
|
| 1139 |
+
@classmethod
|
| 1140 |
+
def from_config(cls, config):
|
| 1141 |
+
return cls(**config)
|
| 1142 |
+
|
| 1143 |
+
class CustomTransformerEncoderLayer(layers.Layer):
|
| 1144 |
+
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1, **kwargs):
|
| 1145 |
+
super(CustomTransformerEncoderLayer, self).__init__(**kwargs)
|
| 1146 |
+
self.embed_dim = embed_dim
|
| 1147 |
+
self.num_heads = num_heads
|
| 1148 |
+
self.ff_dim = ff_dim
|
| 1149 |
+
self.rate = rate
|
| 1150 |
+
self.att = CustomMultiHeadAttention(embed_dim, num_heads)
|
| 1151 |
+
self.ffn = keras.Sequential(
|
| 1152 |
+
[layers.Dense(ff_dim, activation="softplus"), layers.Dense(embed_dim)])
|
| 1153 |
+
self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
|
| 1154 |
+
self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
|
| 1155 |
+
self.dropout1 = layers.Dropout(rate)
|
| 1156 |
+
self.dropout2 = layers.Dropout(rate)
|
| 1157 |
+
|
| 1158 |
+
def call(self, inputs, training):
|
| 1159 |
+
attn_output = self.att(inputs)
|
| 1160 |
+
attn_output = self.dropout1(attn_output, training=training)
|
| 1161 |
+
out1 = self.layernorm1(inputs + attn_output)
|
| 1162 |
+
ffn_output = self.ffn(out1)
|
| 1163 |
+
ffn_output = self.dropout2(ffn_output, training=training)
|
| 1164 |
+
return self.layernorm2(out1 + ffn_output)
|
| 1165 |
+
|
| 1166 |
+
def get_config(self):
|
| 1167 |
+
config = super(CustomTransformerEncoderLayer, self).get_config()
|
| 1168 |
+
config.update({
|
| 1169 |
+
'embed_dim': self.embed_dim,
|
| 1170 |
+
'num_heads': self.num_heads,
|
| 1171 |
+
'ff_dim': self.ff_dim,
|
| 1172 |
+
'rate': self.rate,
|
| 1173 |
+
})
|
| 1174 |
+
return config
|
| 1175 |
+
|
| 1176 |
+
@classmethod
|
| 1177 |
+
def from_config(cls, config):
|
| 1178 |
+
return cls(**config)
|
| 1179 |
+
|
| 1180 |
+
# Custom objects for model loading
|
| 1181 |
+
custom_objects = {
|
| 1182 |
+
'CustomMultiHeadAttention': CustomMultiHeadAttention,
|
| 1183 |
+
'CustomTransformerEncoderLayer': CustomTransformerEncoderLayer
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
# Handle different file formats
|
| 1187 |
+
if file_extension == 'h5':
|
| 1188 |
+
# Handle H5 format
|
| 1189 |
+
model_path = os.path.join(temp_dir, uploaded_model.name)
|
| 1190 |
+
with open(model_path, 'wb') as f:
|
| 1191 |
+
f.write(uploaded_model.getvalue())
|
| 1192 |
+
|
| 1193 |
+
try:
|
| 1194 |
+
model = keras.models.load_model(model_path, custom_objects=custom_objects)
|
| 1195 |
+
loading_method = "H5 format (uploaded)"
|
| 1196 |
+
model_loaded = True
|
| 1197 |
+
st.success("โ
Loaded H5 model successfully")
|
| 1198 |
+
except Exception as e:
|
| 1199 |
+
st.error(f"H5 loading failed: {str(e)}")
|
| 1200 |
+
|
| 1201 |
+
elif file_extension == 'keras':
|
| 1202 |
+
# Handle Keras format
|
| 1203 |
+
model_path = os.path.join(temp_dir, uploaded_model.name)
|
| 1204 |
+
with open(model_path, 'wb') as f:
|
| 1205 |
+
f.write(uploaded_model.getvalue())
|
| 1206 |
+
|
| 1207 |
+
try:
|
| 1208 |
+
model = keras.models.load_model(model_path, custom_objects=custom_objects)
|
| 1209 |
+
loading_method = "Keras format (uploaded)"
|
| 1210 |
+
model_loaded = True
|
| 1211 |
+
st.success("โ
Loaded Keras model successfully")
|
| 1212 |
+
except Exception as e:
|
| 1213 |
+
st.error(f"Keras loading failed: {str(e)}")
|
| 1214 |
+
|
| 1215 |
+
elif file_extension == 'zip':
|
| 1216 |
+
# Handle SavedModel ZIP format
|
| 1217 |
+
import zipfile
|
| 1218 |
+
|
| 1219 |
+
zip_path = os.path.join(temp_dir, uploaded_model.name)
|
| 1220 |
+
with open(zip_path, 'wb') as f:
|
| 1221 |
+
f.write(uploaded_model.getvalue())
|
| 1222 |
+
|
| 1223 |
+
# Extract ZIP file
|
| 1224 |
+
extract_dir = os.path.join(temp_dir, 'extracted_model')
|
| 1225 |
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
| 1226 |
+
zip_ref.extractall(extract_dir)
|
| 1227 |
+
|
| 1228 |
+
# Look for SavedModel directory
|
| 1229 |
+
savedmodel_dirs = []
|
| 1230 |
+
for root, dirs, files in os.walk(extract_dir):
|
| 1231 |
+
if 'saved_model.pb' in files:
|
| 1232 |
+
savedmodel_dirs.append(root)
|
| 1233 |
+
|
| 1234 |
+
if savedmodel_dirs:
|
| 1235 |
+
try:
|
| 1236 |
+
model = keras.models.load_model(savedmodel_dirs[0], custom_objects=custom_objects)
|
| 1237 |
+
loading_method = "SavedModel format (uploaded ZIP)"
|
| 1238 |
+
model_loaded = True
|
| 1239 |
+
st.success("โ
Loaded SavedModel successfully")
|
| 1240 |
+
except Exception as e:
|
| 1241 |
+
# Try TFSMLayer as fallback
|
| 1242 |
+
try:
|
| 1243 |
+
model = layers.TFSMLayer(savedmodel_dirs[0], call_endpoint='serving_default')
|
| 1244 |
+
loading_method = "TFSMLayer (uploaded ZIP fallback)"
|
| 1245 |
+
model_loaded = True
|
| 1246 |
+
st.success("โ
Loaded using TFSMLayer")
|
| 1247 |
+
except Exception as e2:
|
| 1248 |
+
st.error(f"SavedModel loading failed: {str(e)}, TFSMLayer failed: {str(e2)}")
|
| 1249 |
+
else:
|
| 1250 |
+
st.error("โ No SavedModel found in ZIP file. Ensure ZIP contains saved_model.pb")
|
| 1251 |
+
|
| 1252 |
+
if not model_loaded:
|
| 1253 |
+
st.error("โ Failed to load uploaded model")
|
| 1254 |
+
|
| 1255 |
+
# Show debug information
|
| 1256 |
+
st.subheader("๐ Debug Information")
|
| 1257 |
+
st.write(f"**File name:** {uploaded_model.name}")
|
| 1258 |
+
st.write(f"**File size:** {len(uploaded_model.getvalue()):,} bytes")
|
| 1259 |
+
st.write(f"**File extension:** {file_extension}")
|
| 1260 |
+
|
| 1261 |
+
st.info("""
|
| 1262 |
+
**Troubleshooting:**
|
| 1263 |
+
- Ensure your model is saved in a compatible format
|
| 1264 |
+
- For H5: Use `model.save('model.h5', save_format='h5')`
|
| 1265 |
+
- For Keras: Use `model.save('model.keras')`
|
| 1266 |
+
- For SavedModel: ZIP the entire SavedModel folder
|
| 1267 |
+
- Verify custom layers are properly saved
|
| 1268 |
+
""")
|
| 1269 |
+
return
|
| 1270 |
+
|
| 1271 |
+
# Store in session state
|
| 1272 |
+
st.session_state.loaded_model = model
|
| 1273 |
+
st.session_state.model_name = uploaded_model.name
|
| 1274 |
+
st.session_state.model_loaded = True
|
| 1275 |
+
st.session_state.loading_method = loading_method
|
| 1276 |
+
st.session_state.temp_model_dir = temp_dir
|
| 1277 |
+
|
| 1278 |
+
# Load config file if provided
|
| 1279 |
+
model_config = None
|
| 1280 |
+
if uploaded_config is not None:
|
| 1281 |
+
try:
|
| 1282 |
+
config_path = os.path.join(temp_dir, uploaded_config.name)
|
| 1283 |
+
with open(config_path, 'wb') as f:
|
| 1284 |
+
f.write(uploaded_config.getvalue())
|
| 1285 |
+
model_config = np.load(config_path, allow_pickle=True).item()
|
| 1286 |
+
st.session_state.model_config = model_config
|
| 1287 |
+
st.success("โ
Config file loaded successfully")
|
| 1288 |
+
except Exception as e:
|
| 1289 |
+
st.warning(f"Config file loading failed: {str(e)}")
|
| 1290 |
+
|
| 1291 |
+
# Display model info
|
| 1292 |
+
st.success(f"โ
Model uploaded and loaded successfully!")
|
| 1293 |
+
|
| 1294 |
+
with st.expander("๐ Model Information", expanded=False):
|
| 1295 |
+
col1, col2 = st.columns(2)
|
| 1296 |
+
with col1:
|
| 1297 |
+
st.write(f"**Model file:** {uploaded_model.name}")
|
| 1298 |
+
st.write(f"**Loading method:** {loading_method}")
|
| 1299 |
+
st.write(f"**Model type:** {type(model).__name__}")
|
| 1300 |
+
st.write(f"**File size:** {len(uploaded_model.getvalue()):,} bytes")
|
| 1301 |
+
with col2:
|
| 1302 |
+
try:
|
| 1303 |
+
if hasattr(model, 'count_params'):
|
| 1304 |
+
total_params = model.count_params()
|
| 1305 |
+
st.write(f"**Total parameters:** {total_params:,}")
|
| 1306 |
+
if hasattr(model, 'input_shape'):
|
| 1307 |
+
st.write(f"**Input shape:** {model.input_shape}")
|
| 1308 |
+
elif hasattr(model, 'input_spec'):
|
| 1309 |
+
st.write(f"**Input spec:** Available")
|
| 1310 |
+
|
| 1311 |
+
# Show config info if available
|
| 1312 |
+
if model_config:
|
| 1313 |
+
st.write(f"**Sequence length:** {model_config.get('sequence_length', 'Unknown')}")
|
| 1314 |
+
st.write(f"**Model type:** {model_config.get('model_type', 'Unknown')}")
|
| 1315 |
+
|
| 1316 |
+
except Exception as config_error:
|
| 1317 |
+
st.write("**Configuration:** Unable to read")
|
| 1318 |
+
|
| 1319 |
+
except Exception as e:
|
| 1320 |
+
st.error(f"โ Failed to load uploaded model: {str(e)}")
|
| 1321 |
+
|
| 1322 |
+
with st.expander("๐ Error Details"):
|
| 1323 |
+
st.write(f"**Error type:** {type(e).__name__}")
|
| 1324 |
+
st.write(f"**Error message:** {str(e)}")
|
| 1325 |
+
st.write(f"**File name:** {uploaded_model.name if uploaded_model else 'None'}")
|
| 1326 |
+
|
| 1327 |
+
# Show current model status
|
| 1328 |
+
if st.session_state.model_loaded:
|
| 1329 |
+
method = st.session_state.get('loading_method', 'Unknown method')
|
| 1330 |
+
model_name = st.session_state.get('model_name', 'Unknown model')
|
| 1331 |
+
st.success(f"๐ค **Model Ready:** `{model_name}` ({method})")
|
| 1332 |
+
|
| 1333 |
+
# Input file selection and prediction (UNCHANGED)
|
| 1334 |
+
if st.session_state.model_loaded:
|
| 1335 |
+
st.subheader("๐ฅ Input Data")
|
| 1336 |
+
|
| 1337 |
+
col1, col2 = st.columns([2, 1])
|
| 1338 |
+
|
| 1339 |
+
# File uploader for input data
|
| 1340 |
+
uploaded_input = st.file_uploader(
|
| 1341 |
+
"Upload input data file",
|
| 1342 |
+
type=['txt'],
|
| 1343 |
+
help="Upload a text file with R-t curve data"
|
| 1344 |
+
)
|
| 1345 |
+
|
| 1346 |
+
# Use current data button
|
| 1347 |
+
if st.button("๐ Use Current Processed Data"):
|
| 1348 |
+
if st.session_state.processed_data and 'R_interp_newunit' in st.session_state:
|
| 1349 |
+
temp_data = ' '.join([f'{val:.6f}' for val in st.session_state.R_interp_newunit[:-1]])
|
| 1350 |
+
st.session_state.temp_input_data = temp_data
|
| 1351 |
+
st.session_state.using_current_data = True
|
| 1352 |
+
st.success("โ
Current interpolated data ready for prediction")
|
| 1353 |
+
else:
|
| 1354 |
+
st.error("No processed data available. Please process data first.")
|
| 1355 |
+
|
| 1356 |
+
# Prediction interface
|
| 1357 |
+
st.subheader("๐ฏ Make Predictions")
|
| 1358 |
+
|
| 1359 |
+
if st.button("๐ Predict G & ฮผ", type="primary"):
|
| 1360 |
+
# Determine input data source
|
| 1361 |
+
input_data = None
|
| 1362 |
+
|
| 1363 |
+
if st.session_state.get('using_current_data', False) and 'temp_input_data' in st.session_state:
|
| 1364 |
+
try:
|
| 1365 |
+
input_values = [float(x) for x in st.session_state.temp_input_data.split()]
|
| 1366 |
+
input_data = np.array(input_values)
|
| 1367 |
+
st.info("Using current processed data for prediction")
|
| 1368 |
+
except Exception as e:
|
| 1369 |
+
st.error(f"Error processing current data: {e}")
|
| 1370 |
+
return
|
| 1371 |
+
|
| 1372 |
+
elif uploaded_input is not None:
|
| 1373 |
+
try:
|
| 1374 |
+
input_data = np.loadtxt(io.StringIO(uploaded_input.getvalue().decode()))
|
| 1375 |
+
st.info("Using uploaded file for prediction")
|
| 1376 |
+
except Exception as e:
|
| 1377 |
+
st.error(f"Error reading uploaded file: {e}")
|
| 1378 |
+
return
|
| 1379 |
+
else:
|
| 1380 |
+
st.error("Please select input data: use current processed data or upload a file")
|
| 1381 |
+
return
|
| 1382 |
+
|
| 1383 |
+
# Run prediction (exactly matching your training code format) - UNCHANGED
|
| 1384 |
+
with st.spinner("Running ML prediction..."):
|
| 1385 |
+
try:
|
| 1386 |
+
# Process input data exactly as in training
|
| 1387 |
+
test_input_curves = input_data
|
| 1388 |
+
|
| 1389 |
+
if test_input_curves.ndim == 1:
|
| 1390 |
+
test_input_curves = test_input_curves.reshape(1, -1)
|
| 1391 |
+
|
| 1392 |
+
# Ensure input size is 100 (matching your training)
|
| 1393 |
+
if test_input_curves.shape[1] != 100:
|
| 1394 |
+
if test_input_curves.shape[1] > 100:
|
| 1395 |
+
test_input_curves = test_input_curves[:, :100]
|
| 1396 |
+
else:
|
| 1397 |
+
padding = np.zeros((test_input_curves.shape[0], 100 - test_input_curves.shape[1]))
|
| 1398 |
+
test_input_curves = np.concatenate([test_input_curves, padding], axis=1)
|
| 1399 |
+
|
| 1400 |
+
# Reshape for model input (matching training: sequence_length, 1)
|
| 1401 |
+
test_input_curves = test_input_curves.reshape(-1, 100, 1)
|
| 1402 |
+
|
| 1403 |
+
# Position inputs for transformer (exactly from training)
|
| 1404 |
+
position_inputs = np.arange(100)
|
| 1405 |
+
test_position_inputs = np.tile(position_inputs, (test_input_curves.shape[0], 1))
|
| 1406 |
+
|
| 1407 |
+
# Make prediction
|
| 1408 |
+
start_time = time.time()
|
| 1409 |
+
|
| 1410 |
+
# Handle different model types
|
| 1411 |
+
if st.session_state.loading_method == "TFSMLayer (uploaded ZIP fallback)":
|
| 1412 |
+
# For TFSMLayer, call directly
|
| 1413 |
+
predictions = st.session_state.loaded_model([test_input_curves, test_position_inputs])
|
| 1414 |
+
if isinstance(predictions, dict):
|
| 1415 |
+
# Extract from TFSMLayer output dictionary
|
| 1416 |
+
predictions_g = predictions.get('g_output',
|
| 1417 |
+
predictions.get('output_1', list(predictions.values())[0]))
|
| 1418 |
+
predictions_mu = predictions.get('mu_output',
|
| 1419 |
+
predictions.get('output_2', list(predictions.values())[1]))
|
| 1420 |
+
else:
|
| 1421 |
+
predictions_g, predictions_mu = predictions
|
| 1422 |
+
else:
|
| 1423 |
+
# Standard model prediction (matching training)
|
| 1424 |
+
predictions_g, predictions_mu = st.session_state.loaded_model.predict(
|
| 1425 |
+
[test_input_curves, test_position_inputs])
|
| 1426 |
+
|
| 1427 |
+
prediction_time = time.time() - start_time
|
| 1428 |
+
|
| 1429 |
+
# Process predictions (exactly from desktop GUI)
|
| 1430 |
+
num_samples = 1
|
| 1431 |
+
pred_G = predictions_g[:num_samples]
|
| 1432 |
+
pred_mu = predictions_mu[:num_samples]
|
| 1433 |
+
|
| 1434 |
+
# Apply scaling (exactly matching training scaling)
|
| 1435 |
+
pred_G_scaled = 10 ** (pred_G * (6 - 3) + 6 - 3)
|
| 1436 |
+
pred_mu_scaled = 10 ** (pred_mu * (0 + 3) - 3)
|
| 1437 |
+
|
| 1438 |
+
# Store results
|
| 1439 |
+
st.session_state.pred_G = pred_G_scaled
|
| 1440 |
+
st.session_state.pred_mu = pred_mu_scaled
|
| 1441 |
+
|
| 1442 |
+
# Extract values for display
|
| 1443 |
+
G_value = st.session_state.pred_G[0][0] if st.session_state.pred_G.ndim > 1 else \
|
| 1444 |
+
st.session_state.pred_G[0]
|
| 1445 |
+
mu_value = st.session_state.pred_mu[0][0] if st.session_state.pred_mu.ndim > 1 else \
|
| 1446 |
+
st.session_state.pred_mu[0]
|
| 1447 |
+
|
| 1448 |
+
# Display results
|
| 1449 |
+
col1, col2, col3 = st.columns(3)
|
| 1450 |
+
|
| 1451 |
+
with col1:
|
| 1452 |
+
st.metric(
|
| 1453 |
+
label="Shear Modulus (G)",
|
| 1454 |
+
value=f"{G_value:.2e} Pa",
|
| 1455 |
+
help="Predicted shear modulus of the material"
|
| 1456 |
+
)
|
| 1457 |
+
|
| 1458 |
+
with col2:
|
| 1459 |
+
st.metric(
|
| 1460 |
+
label="Viscosity (ฮผ)",
|
| 1461 |
+
value=f"{mu_value:.4f} Paยทs",
|
| 1462 |
+
help="Predicted viscosity of the material"
|
| 1463 |
+
)
|
| 1464 |
+
|
| 1465 |
+
with col3:
|
| 1466 |
+
st.metric(
|
| 1467 |
+
label="Prediction Time",
|
| 1468 |
+
value=f"{prediction_time:.3f} s",
|
| 1469 |
+
help="Time taken for ML inference"
|
| 1470 |
+
)
|
| 1471 |
+
|
| 1472 |
+
# Show detailed results
|
| 1473 |
+
result_text = f"G: {G_value:.2e} Pa, ฮผ: {mu_value:.4f}"
|
| 1474 |
+
st.success(f"๐ **Prediction Results:** {result_text}")
|
| 1475 |
+
|
| 1476 |
+
detailed_results = f"""**Prediction completed successfully!**
|
| 1477 |
+
|
| 1478 |
+
**Shear Modulus (G):** {G_value:.2e} Pa
|
| 1479 |
+
**Viscosity (ฮผ):** {mu_value:.4f} Paยทs
|
| 1480 |
+
**Prediction Time:** {prediction_time:.4f} seconds ({prediction_time * 1000:.2f} ms)
|
| 1481 |
+
|
| 1482 |
+
Ready for validation simulation!"""
|
| 1483 |
+
|
| 1484 |
+
st.info(detailed_results)
|
| 1485 |
+
|
| 1486 |
+
# Enable validation
|
| 1487 |
+
st.session_state.validation_ready = True
|
| 1488 |
+
|
| 1489 |
+
except Exception as e:
|
| 1490 |
+
st.error(f"โ Prediction failed: {str(e)}")
|
| 1491 |
+
|
| 1492 |
+
with st.expander("๐ Debug Information"):
|
| 1493 |
+
st.write(f"**Error:** {str(e)}")
|
| 1494 |
+
st.write(f"**Model type:** {type(st.session_state.loaded_model).__name__}")
|
| 1495 |
+
st.write(f"**Loading method:** {st.session_state.get('loading_method', 'Unknown')}")
|
| 1496 |
+
if 'test_input_curves' in locals():
|
| 1497 |
+
st.write(f"**Input shape:** {test_input_curves.shape}")
|
| 1498 |
+
if 'test_position_inputs' in locals():
|
| 1499 |
+
st.write(f"**Position shape:** {test_position_inputs.shape}")
|
| 1500 |
+
|
| 1501 |
+
# Reset using_current_data flag when file is uploaded
|
| 1502 |
+
if uploaded_input is not None:
|
| 1503 |
+
st.session_state.using_current_data = False
|
| 1504 |
+
|
| 1505 |
+
# Cleanup temporary files when session ends
|
| 1506 |
+
if hasattr(st.session_state, 'temp_model_dir') and st.session_state.temp_model_dir:
|
| 1507 |
+
# Note: In a real deployment, you might want to implement proper cleanup
|
| 1508 |
+
# For now, the temporary directory will be cleaned up when the container restarts
|
| 1509 |
+
pass
|
| 1510 |
+
|
| 1511 |
+
|
| 1512 |
+
def show_validation():
|
| 1513 |
+
"""ULTRA-STABLE Validation interface - ZERO trembling using session state control"""
|
| 1514 |
+
st.markdown('<h2 class="section-header">โ
Validation</h2>', unsafe_allow_html=True)
|
| 1515 |
+
|
| 1516 |
+
if not st.session_state.processed_data:
|
| 1517 |
+
st.warning("Please process data first.")
|
| 1518 |
+
return
|
| 1519 |
+
|
| 1520 |
+
if not st.session_state.get('validation_ready', False) or 'pred_G' not in st.session_state or 'pred_mu' not in st.session_state:
|
| 1521 |
+
st.warning("Please run ML prediction first to get material properties for validation.")
|
| 1522 |
+
st.info("""
|
| 1523 |
+
**Validation Process:**
|
| 1524 |
+
1. ๐ Load experimental data
|
| 1525 |
+
2. โ๏ธ Process and interpolate data
|
| 1526 |
+
3. ๐ค Use ML model to predict G & ฮผ
|
| 1527 |
+
4. โ
**Run validation simulation** (you are here)
|
| 1528 |
+
5. ๐ Compare experimental vs simulated results
|
| 1529 |
+
""")
|
| 1530 |
+
return
|
| 1531 |
+
|
| 1532 |
+
st.subheader("๐ฌ IMR Simulation using predicted G and ฮผ vs Experimental R-t curve")
|
| 1533 |
+
|
| 1534 |
+
# REVOLUTIONARY FIX: Cache all static values in session state to prevent re-computation
|
| 1535 |
+
if 'validation_G_cached' not in st.session_state:
|
| 1536 |
+
st.session_state.validation_G_cached = st.session_state.pred_G[0][0] if st.session_state.pred_G.ndim > 1 else st.session_state.pred_G[0]
|
| 1537 |
+
st.session_state.validation_mu_cached = st.session_state.pred_mu[0][0] if st.session_state.pred_mu.ndim > 1 else st.session_state.pred_mu[0]
|
| 1538 |
+
st.session_state.validation_lambda_cached = st.session_state.lambda_max_mean
|
| 1539 |
+
|
| 1540 |
+
# ULTRA-STABLE LAYOUT: Single column layout to prevent trembling
|
| 1541 |
+
# Removed predicted values display to eliminate trembling
|
| 1542 |
+
|
| 1543 |
+
# ULTRA-CRITICAL FIX: Use session state to control button behavior
|
| 1544 |
+
if 'validation_button_clicked' not in st.session_state:
|
| 1545 |
+
st.session_state.validation_button_clicked = False
|
| 1546 |
+
if 'validation_running' not in st.session_state:
|
| 1547 |
+
st.session_state.validation_running = False
|
| 1548 |
+
if 'validation_complete' not in st.session_state:
|
| 1549 |
+
st.session_state.validation_complete = False
|
| 1550 |
+
|
| 1551 |
+
# STABLE BUTTON: Only show if not running
|
| 1552 |
+
if not st.session_state.validation_running:
|
| 1553 |
+
if st.button("๐ Run Validation Simulation", type="primary", key="validation_btn_stable"):
|
| 1554 |
+
st.session_state.validation_button_clicked = True
|
| 1555 |
+
st.session_state.validation_running = True
|
| 1556 |
+
st.session_state.validation_complete = False
|
| 1557 |
+
st.rerun()
|
| 1558 |
+
|
| 1559 |
+
# HANDLE SIMULATION EXECUTION
|
| 1560 |
+
if st.session_state.validation_running and not st.session_state.validation_complete:
|
| 1561 |
+
# Check required data
|
| 1562 |
+
if st.session_state.lambda_max_mean is None:
|
| 1563 |
+
st.error("No lambda_max_mean loaded. Please load data first.")
|
| 1564 |
+
st.session_state.validation_running = False
|
| 1565 |
+
return
|
| 1566 |
+
|
| 1567 |
+
# Show status in fixed container
|
| 1568 |
+
status_placeholder = st.empty()
|
| 1569 |
+
status_placeholder.info("๐ Running simulation... Please wait")
|
| 1570 |
+
|
| 1571 |
+
try:
|
| 1572 |
+
# Use cached values - absolutely no re-computation
|
| 1573 |
+
G_value = st.session_state.validation_G_cached
|
| 1574 |
+
mu_value = st.session_state.validation_mu_cached
|
| 1575 |
+
|
| 1576 |
+
# Initialize and run simulation
|
| 1577 |
+
bubble_sim = OptimizedBubbleSimulation()
|
| 1578 |
+
|
| 1579 |
+
start_time = time.time()
|
| 1580 |
+
t_sim, R_sim = bubble_sim.run_optimized_simulation(
|
| 1581 |
+
G_value, mu_value, st.session_state.validation_lambda_cached
|
| 1582 |
+
)
|
| 1583 |
+
simulation_time = time.time() - start_time
|
| 1584 |
+
|
| 1585 |
+
# Store ALL results in session state
|
| 1586 |
+
st.session_state.validation_t_sim = t_sim
|
| 1587 |
+
st.session_state.validation_R_sim = R_sim
|
| 1588 |
+
st.session_state.validation_simulation_time = simulation_time
|
| 1589 |
+
|
| 1590 |
+
# Calculate and store error metrics
|
| 1591 |
+
rmse = mae = max_error = 0
|
| 1592 |
+
if len(t_sim) > 0 and len(st.session_state.t_interp_newunit) > 0:
|
| 1593 |
+
t_min = max(st.session_state.t_interp_newunit[0], t_sim[0])
|
| 1594 |
+
t_max = min(st.session_state.t_interp_newunit[-1], t_sim[-1])
|
| 1595 |
+
|
| 1596 |
+
if t_max > t_min:
|
| 1597 |
+
f_sim = interp1d(t_sim, R_sim, kind='linear', bounds_error=False, fill_value='extrapolate')
|
| 1598 |
+
mask = (st.session_state.t_interp_newunit >= t_min) & (st.session_state.t_interp_newunit <= t_max)
|
| 1599 |
+
t_common = st.session_state.t_interp_newunit[mask]
|
| 1600 |
+
R_exp_common = st.session_state.R_interp_newunit[mask]
|
| 1601 |
+
R_sim_common = f_sim(t_common)
|
| 1602 |
+
|
| 1603 |
+
if len(R_exp_common) > 0:
|
| 1604 |
+
rmse = np.sqrt(np.mean((R_exp_common - R_sim_common) ** 2))
|
| 1605 |
+
mae = np.mean(np.abs(R_exp_common - R_sim_common))
|
| 1606 |
+
max_error = np.max(np.abs(R_exp_common - R_sim_common))
|
| 1607 |
+
|
| 1608 |
+
# Store metrics in session state
|
| 1609 |
+
st.session_state.validation_rmse = rmse
|
| 1610 |
+
st.session_state.validation_mae = mae
|
| 1611 |
+
st.session_state.validation_max_error = max_error
|
| 1612 |
+
|
| 1613 |
+
# Clear status and mark complete
|
| 1614 |
+
status_placeholder.empty()
|
| 1615 |
+
st.session_state.validation_running = False
|
| 1616 |
+
st.session_state.validation_complete = True
|
| 1617 |
+
|
| 1618 |
+
# Rerun to show results
|
| 1619 |
+
st.rerun()
|
| 1620 |
+
|
| 1621 |
+
except Exception as e:
|
| 1622 |
+
st.session_state.validation_running = False
|
| 1623 |
+
st.session_state.validation_error = str(e)
|
| 1624 |
+
st.rerun()
|
| 1625 |
+
|
| 1626 |
+
# DISPLAY RESULTS (only if complete)
|
| 1627 |
+
if st.session_state.validation_complete and 'validation_t_sim' in st.session_state:
|
| 1628 |
+
# Create comparison plot
|
| 1629 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 1630 |
+
|
| 1631 |
+
ax.plot(st.session_state.t_interp_newunit, st.session_state.R_interp_newunit,
|
| 1632 |
+
'ro', markersize=4, label='Interpolated (Experimental)', alpha=0.7)
|
| 1633 |
+
ax.plot(st.session_state.validation_t_sim, st.session_state.validation_R_sim,
|
| 1634 |
+
'b-', linewidth=2, label='Simulated (Predicted G & ฮผ)')
|
| 1635 |
+
|
| 1636 |
+
ax.set_xlabel('Time (0.1 ms)')
|
| 1637 |
+
ax.set_ylabel('Radius (0.1 mm)')
|
| 1638 |
+
ax.set_title('Validation: Experimental vs Simulated R-t Curves')
|
| 1639 |
+
ax.grid(True, alpha=0.3)
|
| 1640 |
+
ax.legend()
|
| 1641 |
+
|
| 1642 |
+
# Removed error metrics from plot to prevent trembling
|
| 1643 |
+
|
| 1644 |
+
plt.tight_layout()
|
| 1645 |
+
st.pyplot(fig)
|
| 1646 |
+
|
| 1647 |
+
# Removed metrics display to eliminate trembling
|
| 1648 |
+
|
| 1649 |
+
# Results summary
|
| 1650 |
+
st.success("โ
Validation simulation completed!")
|
| 1651 |
+
|
| 1652 |
+
st.info(f"""**Validation Results:**
|
| 1653 |
+
|
| 1654 |
+
**Predicted Values:**
|
| 1655 |
+
- Shear Modulus (G): {st.session_state.validation_G_cached:.2e} Pa
|
| 1656 |
+
- Viscosity (ฮผ): {st.session_state.validation_mu_cached:.4f} Paยทs
|
| 1657 |
+
- Lambda Max: {st.session_state.validation_lambda_cached:.3f}
|
| 1658 |
+
|
| 1659 |
+
**Performance:**
|
| 1660 |
+
- Simulation Time: {st.session_state.validation_simulation_time:.2f} seconds
|
| 1661 |
+
- Simulated Points: {len(st.session_state.validation_R_sim)}
|
| 1662 |
+
- Time Range: {st.session_state.validation_t_sim[0]:.3f} to {st.session_state.validation_t_sim[-1]:.3f} (0.1 ms)
|
| 1663 |
+
""")
|
| 1664 |
+
|
| 1665 |
+
# Reset button
|
| 1666 |
+
if st.button("๐ Run New Simulation", key="reset_validation"):
|
| 1667 |
+
# Clear all validation session state
|
| 1668 |
+
for key in list(st.session_state.keys()):
|
| 1669 |
+
if key.startswith('validation_'):
|
| 1670 |
+
del st.session_state[key]
|
| 1671 |
+
st.rerun()
|
| 1672 |
+
|
| 1673 |
+
# Handle simulation error
|
| 1674 |
+
if hasattr(st.session_state, 'validation_error'):
|
| 1675 |
+
st.error(f"Simulation failed: {st.session_state.validation_error}")
|
| 1676 |
+
if st.button("๐ Try Again", key="retry_validation"):
|
| 1677 |
+
del st.session_state.validation_error
|
| 1678 |
+
st.session_state.validation_running = False
|
| 1679 |
+
st.rerun()
|
| 1680 |
+
|
| 1681 |
+
|
| 1682 |
+
# Additional CSS to ensure zero movement
|
| 1683 |
+
st.markdown("""
|
| 1684 |
+
<style>
|
| 1685 |
+
/* ULTIMATE ANTI-TREMBLING CSS */
|
| 1686 |
+
.stButton > button {
|
| 1687 |
+
transition: none !important;
|
| 1688 |
+
animation: none !important;
|
| 1689 |
+
transform: none !important;
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
.element-container {
|
| 1693 |
+
transition: none !important;
|
| 1694 |
+
animation: none !important;
|
| 1695 |
+
transform: none !important;
|
| 1696 |
+
position: relative !important;
|
| 1697 |
+
}
|
| 1698 |
+
|
| 1699 |
+
/* Force container stability */
|
| 1700 |
+
[data-testid="column"] {
|
| 1701 |
+
transition: none !important;
|
| 1702 |
+
animation: none !important;
|
| 1703 |
+
transform: none !important;
|
| 1704 |
+
}
|
| 1705 |
+
|
| 1706 |
+
/* Prevent any layout shifts */
|
| 1707 |
+
.main .block-container .element-container {
|
| 1708 |
+
will-change: auto !important;
|
| 1709 |
+
transform: translateZ(0) !important;
|
| 1710 |
+
backface-visibility: hidden !important;
|
| 1711 |
+
}
|
| 1712 |
+
</style>
|
| 1713 |
+
""", unsafe_allow_html=True)
|
| 1714 |
+
|
| 1715 |
+
|
| 1716 |
+
def show_results():
|
| 1717 |
+
"""Results and export interface"""
|
| 1718 |
+
st.markdown('<h2 class="section-header">๐ Results & Export</h2>', unsafe_allow_html=True)
|
| 1719 |
+
|
| 1720 |
+
if not st.session_state.processed_data:
|
| 1721 |
+
st.warning("No processed data available.")
|
| 1722 |
+
return
|
| 1723 |
+
|
| 1724 |
+
# Summary of all results
|
| 1725 |
+
st.subheader("๐ Analysis Summary")
|
| 1726 |
+
|
| 1727 |
+
col1, col2 = st.columns(2)
|
| 1728 |
+
|
| 1729 |
+
with col1:
|
| 1730 |
+
st.markdown("**Data Information:**")
|
| 1731 |
+
if st.session_state.data_loaded:
|
| 1732 |
+
st.write(f"โ
Datasets loaded: {st.session_state.num_datasets}")
|
| 1733 |
+
st.write(f"โ
Lambda max: {st.session_state.lambda_max_mean:.3f}")
|
| 1734 |
+
st.write(f"โ
Selected dataset: {st.session_state.get('selected_dataset', 'N/A') + 1}")
|
| 1735 |
+
|
| 1736 |
+
if st.session_state.processed_data:
|
| 1737 |
+
st.write(f"โ
Interpolated points: {len(st.session_state.R_interp_newunit)}")
|
| 1738 |
+
|
| 1739 |
+
with col2:
|
| 1740 |
+
st.markdown("**ML Predictions:**")
|
| 1741 |
+
if 'pred_G' in st.session_state:
|
| 1742 |
+
G_value = st.session_state.pred_G[0][0] if st.session_state.pred_G.ndim > 1 else st.session_state.pred_G[0]
|
| 1743 |
+
mu_value = st.session_state.pred_mu[0][0] if st.session_state.pred_mu.ndim > 1 else \
|
| 1744 |
+
st.session_state.pred_mu[0]
|
| 1745 |
+
st.write(f"๐ฏ Shear Modulus (G): {G_value:.2e} Pa")
|
| 1746 |
+
st.write(f"๐ฏ Viscosity (ฮผ): {mu_value:.4f} Paยทs")
|
| 1747 |
+
else:
|
| 1748 |
+
st.write("โ No predictions available")
|
| 1749 |
+
|
| 1750 |
+
# Export options
|
| 1751 |
+
st.subheader("๐พ Export Options")
|
| 1752 |
+
|
| 1753 |
+
export_col1, export_col2, export_col3 = st.columns(3)
|
| 1754 |
+
|
| 1755 |
+
with export_col1:
|
| 1756 |
+
if st.session_state.processed_data:
|
| 1757 |
+
# Export interpolated data
|
| 1758 |
+
data_str = ' '.join([f'{val:.6f}' for val in st.session_state.R_interp_newunit[:-1]])
|
| 1759 |
+
st.download_button(
|
| 1760 |
+
label="๐ฅ Download Interpolated Data",
|
| 1761 |
+
data=data_str,
|
| 1762 |
+
file_name="interpolated_bubble_data.txt",
|
| 1763 |
+
mime="text/plain"
|
| 1764 |
+
)
|
| 1765 |
+
|
| 1766 |
+
with export_col2:
|
| 1767 |
+
if 'pred_G' in st.session_state:
|
| 1768 |
+
# Export predictions
|
| 1769 |
+
G_value = st.session_state.pred_G[0][0] if st.session_state.pred_G.ndim > 1 else st.session_state.pred_G[0]
|
| 1770 |
+
mu_value = st.session_state.pred_mu[0][0] if st.session_state.pred_mu.ndim > 1 else \
|
| 1771 |
+
st.session_state.pred_mu[0]
|
| 1772 |
+
pred_summary = f"""Bubble Dynamics Analysis Results
|
| 1773 |
+
|
| 1774 |
+
Dataset: {st.session_state.get('selected_dataset', 'N/A') + 1}
|
| 1775 |
+
Lambda Max: {st.session_state.lambda_max_mean:.3f}
|
| 1776 |
+
|
| 1777 |
+
ML Predictions:
|
| 1778 |
+
Shear Modulus (G): {G_value:.2e} Pa
|
| 1779 |
+
Viscosity (ฮผ): {mu_value:.4f} Paยทs
|
| 1780 |
+
|
| 1781 |
+
Analysis completed on: {time.strftime('%Y-%m-%d %H:%M:%S')}
|
| 1782 |
"""
|
| 1783 |
+
st.download_button(
|
| 1784 |
+
label="๐ Download Results Summary",
|
| 1785 |
+
data=pred_summary,
|
| 1786 |
+
file_name="bubble_analysis_results.txt",
|
| 1787 |
+
mime="text/plain"
|
| 1788 |
+
)
|
| 1789 |
+
|
| 1790 |
+
with export_col3:
|
| 1791 |
+
if 'validation_t_sim' in st.session_state:
|
| 1792 |
+
# Export simulation data
|
| 1793 |
+
sim_data = np.column_stack([st.session_state.validation_t_sim, st.session_state.validation_R_sim])
|
| 1794 |
+
sim_str = '\n'.join([f'{t:.6f}\t{r:.6f}' for t, r in sim_data])
|
| 1795 |
+
st.download_button(
|
| 1796 |
+
label="๐ฌ Download Simulation Data",
|
| 1797 |
+
data=sim_str,
|
| 1798 |
+
file_name="simulation_results.txt",
|
| 1799 |
+
mime="text/plain"
|
| 1800 |
+
)
|
| 1801 |
+
|
| 1802 |
+
# Session reset
|
| 1803 |
+
st.subheader("๐ Reset Session")
|
| 1804 |
+
if st.button("๐๏ธ Clear All Data", type="secondary"):
|
| 1805 |
+
for key in list(st.session_state.keys()):
|
| 1806 |
+
del st.session_state[key]
|
| 1807 |
+
st.success("โ
Session cleared! Refresh the page to start over.")
|
| 1808 |
+
|
| 1809 |
|
| 1810 |
+
if __name__ == "__main__":
|
| 1811 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|