Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import numpy as np
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
|
5 |
+
# --------------------------------------
|
6 |
+
# Utility functions
|
7 |
+
# --------------------------------------
|
8 |
+
|
9 |
+
def generate_random_pile(num_points=50, max_height=10.0, noise=0.5):
|
10 |
+
"""
|
11 |
+
Generates a random 'pile shape' with a certain maximum height.
|
12 |
+
We'll shape it as a bell-curve-like mound plus random noise.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
num_points (int): Number of discrete x-points to model the silo cross-section.
|
16 |
+
max_height (float): Approximate max possible height.
|
17 |
+
noise (float): Random fluctuations around the main shape.
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
x_array (np.array): X-coordinates from 0..1 (normalized).
|
21 |
+
height_array (np.array): Random mound shape values at each x.
|
22 |
+
"""
|
23 |
+
x_array = np.linspace(0, 1, num_points)
|
24 |
+
center = 0.5
|
25 |
+
sigma = 0.15 # controls how wide the mound is
|
26 |
+
base_curve = max_height * np.exp(-0.5 * ((x_array - center) / sigma)**2)
|
27 |
+
|
28 |
+
random_variation = noise * np.random.randn(num_points)
|
29 |
+
height_array = np.clip(base_curve + random_variation, 0, None)
|
30 |
+
return x_array, height_array
|
31 |
+
|
32 |
+
def simulate_sensor_readings(x_array, height_array, sensor_type="LiDAR", num_sensors=5):
|
33 |
+
"""
|
34 |
+
Simulates sensor readings from the height array.
|
35 |
+
We randomly pick a few positions along x_array,
|
36 |
+
then record the height + some noise depending on sensor_type.
|
37 |
+
|
38 |
+
Args:
|
39 |
+
x_array (np.array): X-coordinates for the silo cross-section
|
40 |
+
height_array (np.array): True pile height at each x
|
41 |
+
sensor_type (str): "LiDAR" or "Ultrasonic" - determines noise level
|
42 |
+
num_sensors (int): number of sensors/points to read
|
43 |
+
|
44 |
+
Returns:
|
45 |
+
sensor_positions (np.array): Chosen positions
|
46 |
+
sensor_heights (np.array): Measured heights (with noise)
|
47 |
+
"""
|
48 |
+
sensor_positions = np.random.choice(x_array, size=num_sensors, replace=False)
|
49 |
+
sensor_positions = np.sort(sensor_positions)
|
50 |
+
|
51 |
+
sensor_heights = []
|
52 |
+
for pos in sensor_positions:
|
53 |
+
idx = np.argmin(np.abs(x_array - pos))
|
54 |
+
true_height = height_array[idx]
|
55 |
+
|
56 |
+
if sensor_type.lower() == "lidar":
|
57 |
+
noise_std = 0.1
|
58 |
+
else:
|
59 |
+
noise_std = 0.2
|
60 |
+
|
61 |
+
measured = true_height + np.random.randn() * noise_std
|
62 |
+
sensor_heights.append(max(measured, 0.0))
|
63 |
+
|
64 |
+
return sensor_positions, np.array(sensor_heights)
|
65 |
+
|
66 |
+
def estimate_peak_height(sensor_positions, sensor_heights):
|
67 |
+
"""
|
68 |
+
Simple approach: take the max sensor reading as approximate peak.
|
69 |
+
"""
|
70 |
+
return np.max(sensor_heights)
|
71 |
+
|
72 |
+
def estimate_volume(x_array, height_array, silo_width=5.0, silo_depth=5.0):
|
73 |
+
"""
|
74 |
+
Approximates volume of the pile using trapezoidal integration
|
75 |
+
across the 2D cross-section, then multiplying by depth.
|
76 |
+
|
77 |
+
Args:
|
78 |
+
x_array: normalized x from 0..1
|
79 |
+
height_array: height at each x
|
80 |
+
silo_width (float): actual width of silo in meters
|
81 |
+
silo_depth (float): depth into the page in meters
|
82 |
+
|
83 |
+
Returns:
|
84 |
+
volume (float): approximate volume in cubic meters
|
85 |
+
"""
|
86 |
+
x_meters = x_array * silo_width
|
87 |
+
area_2d = np.trapz(height_array, x_meters) # m^2
|
88 |
+
volume = area_2d * silo_depth # m^3
|
89 |
+
return volume
|
90 |
+
|
91 |
+
def plot_pile_shape(x_array, height_array, sensor_positions=None, sensor_heights=None):
|
92 |
+
"""
|
93 |
+
Creates and returns a matplotlib figure showing
|
94 |
+
the random pile shape and optionally sensor readings.
|
95 |
+
"""
|
96 |
+
fig, ax = plt.subplots(figsize=(5,3))
|
97 |
+
ax.plot(x_array, height_array, label='Pile Shape', linewidth=2, color='blue')
|
98 |
+
|
99 |
+
if sensor_positions is not None and sensor_heights is not None:
|
100 |
+
ax.scatter(sensor_positions, sensor_heights, color='red',
|
101 |
+
label='Sensor Readings', s=50, zorder=5)
|
102 |
+
|
103 |
+
ax.set_title("Silo Cross-Section (2D) - Wood Chip Pile")
|
104 |
+
ax.set_xlabel("Normalized Silo Width (0..1)")
|
105 |
+
ax.set_ylabel("Height (m)")
|
106 |
+
ax.legend()
|
107 |
+
fig.tight_layout()
|
108 |
+
return fig
|
109 |
+
|
110 |
+
def plot_sensor_bar(sensor_positions, sensor_heights):
|
111 |
+
"""
|
112 |
+
Creates and returns a bar chart showing sensor heights
|
113 |
+
vs. sensor ID or position.
|
114 |
+
"""
|
115 |
+
fig, ax = plt.subplots(figsize=(4,3))
|
116 |
+
# Let's use sensor index as x-ticks
|
117 |
+
indices = np.arange(len(sensor_positions))
|
118 |
+
ax.bar(indices, sensor_heights, color='green')
|
119 |
+
ax.set_title("Sensor Readings Bar Chart")
|
120 |
+
ax.set_xlabel("Sensor Index")
|
121 |
+
ax.set_ylabel("Measured Height (m)")
|
122 |
+
ax.set_xticks(indices)
|
123 |
+
ax.set_xticklabels([f"{pos:.2f}" for pos in sensor_positions], rotation=45)
|
124 |
+
fig.tight_layout()
|
125 |
+
return fig
|
126 |
+
|
127 |
+
# --------------------------------------
|
128 |
+
# Streamlit App
|
129 |
+
# --------------------------------------
|
130 |
+
|
131 |
+
st.set_page_config(page_title="ITC PSPD - Wood Chip Silo Demo", layout="centered")
|
132 |
+
|
133 |
+
st.title("Wood Chip Silo Monitoring - ITC PSPD Demo")
|
134 |
+
|
135 |
+
st.markdown("""
|
136 |
+
**Welcome to a simple demonstration of ** *Wood-chip silo* height profile Detection and Volume Estimation*.
|
137 |
+
This is an **Industry 4.0** inspired solution to **digitally** track the *height* and *volume*
|
138 |
+
of wood chips in a silo, supporting **paperboard and specialty paper** operations at **ITC PSPD**.
|
139 |
+
|
140 |
+
---
|
141 |
+
""")
|
142 |
+
|
143 |
+
# Sidebar controls
|
144 |
+
st.sidebar.header("Simulation Controls")
|
145 |
+
|
146 |
+
sensor_type = st.sidebar.selectbox("Select Sensor Type:", ["LiDAR", "Ultrasonic"])
|
147 |
+
max_height = st.sidebar.slider("Max Potential Height (m):", 5.0, 30.0, 10.0, 1.0)
|
148 |
+
noise_level = st.sidebar.slider("Random Noise Level:", 0.0, 3.0, 0.5, 0.1)
|
149 |
+
num_points = st.sidebar.slider("Pile Resolution (Points):", 20, 200, 50, 10)
|
150 |
+
num_sensors = st.sidebar.slider("Number of Sensors:", 1, 12, 5)
|
151 |
+
silo_width = st.sidebar.slider("Silo Width (m):", 1.0, 20.0, 5.0, 1.0)
|
152 |
+
silo_depth = st.sidebar.slider("Silo Depth (m):", 1.0, 20.0, 5.0, 1.0)
|
153 |
+
|
154 |
+
# Button to generate new random pile
|
155 |
+
if "x_array" not in st.session_state or "height_array" not in st.session_state:
|
156 |
+
st.session_state["x_array"] = None
|
157 |
+
st.session_state["height_array"] = None
|
158 |
+
|
159 |
+
if st.sidebar.button("Generate New Random Pile"):
|
160 |
+
x_array, height_array = generate_random_pile(
|
161 |
+
num_points=num_points,
|
162 |
+
max_height=max_height,
|
163 |
+
noise=noise_level
|
164 |
+
)
|
165 |
+
st.session_state["x_array"] = x_array
|
166 |
+
st.session_state["height_array"] = height_array
|
167 |
+
|
168 |
+
# If we have data, proceed
|
169 |
+
if st.session_state["x_array"] is not None and st.session_state["height_array"] is not None:
|
170 |
+
x_array = st.session_state["x_array"]
|
171 |
+
height_array = st.session_state["height_array"]
|
172 |
+
|
173 |
+
# Simulate sensor readings
|
174 |
+
sensor_positions, sensor_heights = simulate_sensor_readings(
|
175 |
+
x_array, height_array, sensor_type, num_sensors
|
176 |
+
)
|
177 |
+
|
178 |
+
# Estimate peak height from sensor
|
179 |
+
approx_peak = estimate_peak_height(sensor_positions, sensor_heights)
|
180 |
+
# Estimate volume
|
181 |
+
volume_est = estimate_volume(x_array, height_array, silo_width, silo_depth)
|
182 |
+
|
183 |
+
st.subheader("1) Silo Cross-Section Visualization")
|
184 |
+
fig_pile = plot_pile_shape(x_array, height_array, sensor_positions, sensor_heights)
|
185 |
+
st.pyplot(fig_pile)
|
186 |
+
|
187 |
+
# Show measured peak height
|
188 |
+
col1, col2 = st.columns(2)
|
189 |
+
with col1:
|
190 |
+
st.markdown(f"**Approx Peak Height (from sensors):** `{approx_peak:.2f} m`")
|
191 |
+
st.progress(min(approx_peak/max_height, 1.0))
|
192 |
+
with col2:
|
193 |
+
st.markdown(f"**Estimated Volume:** `{volume_est:.2f} m³`")
|
194 |
+
st.progress(min(volume_est/((max_height*silo_width*silo_depth)), 1.0))
|
195 |
+
|
196 |
+
st.subheader("2) Sensor Data Overview")
|
197 |
+
|
198 |
+
col3, col4 = st.columns(2)
|
199 |
+
with col3:
|
200 |
+
# Show bar chart of sensor readings
|
201 |
+
fig_bars = plot_sensor_bar(sensor_positions, sensor_heights)
|
202 |
+
st.pyplot(fig_bars)
|
203 |
+
with col4:
|
204 |
+
st.write("### Sensor Data Table")
|
205 |
+
table_data = {
|
206 |
+
"Position (norm)": [f"{pos:.2f}" for pos in sensor_positions],
|
207 |
+
"Height (m)": [f"{h:.2f}" for h in sensor_heights]
|
208 |
+
}
|
209 |
+
st.table(table_data)
|
210 |
+
|
211 |
+
st.markdown("---")
|
212 |
+
|
213 |
+
st.markdown("""
|
214 |
+
### Interpretation & Industry 4.0 Connection
|
215 |
+
|
216 |
+
- **Digital Twins**: This simulation mimics how a *digital twin* of the silo tracks
|
217 |
+
wood-chip heights. The real system could have *LiDAR* sensors scanning
|
218 |
+
or an array of *ultrasonic* sensors measuring the pile at intervals.
|
219 |
+
- **Data-Driven Insights**: Automated volume estimation helps production planning,
|
220 |
+
ensuring continuous supply for **paperboard** manufacturing while minimizing
|
221 |
+
waste or silo overflow.
|
222 |
+
- **ITC PSPD**: As a leading division in paper & packaging, adopting *Industry 4.0*
|
223 |
+
solutions supports sustainability and operational efficiency—aligned with ITC’s
|
224 |
+
triple bottom line.
|
225 |
+
- **Big Data & AI**: If multiple silos or sites feed data into a cloud platform,
|
226 |
+
advanced analytics/predictive models can optimize inventory or detect anomalies.
|
227 |
+
|
228 |
+
---
|
229 |
+
""")
|
230 |
+
else:
|
231 |
+
st.info("Use the **sidebar** to generate a new random pile and view the results.")
|
232 |
+
|
233 |
+
st.markdown("""
|
234 |
+
## Made By:
|
235 |
+
- Name: Kaustubh Raykar
|
236 |
+
- PRN: 21070126048
|
237 |
+
- Btech AIML 2021-25
|
238 |
+
- Contact: +91 7020524609
|
239 |
+
- Symbiosis Institute Of Technology, Pune
|
240 | |
241 | |
242 |
+
**Thank you** for exploring this Wood Chip Silo Demo for **ITC PSPD**.
|
243 |
+
""")
|