|
5 | 5 | with real-time metrics display and requirement verification. |
6 | 6 | """ |
7 | 7 |
|
8 | | -import streamlit as st |
9 | 8 | import numpy as np |
| 9 | +import streamlit as st |
10 | 10 |
|
11 | 11 | st.set_page_config( |
12 | 12 | page_title="Single Case Calculator", |
|
19 | 19 |
|
20 | 20 | # Try to import the package |
21 | 21 | try: |
22 | | - from phased_array_systems.architecture import Architecture, ArrayConfig, RFChainConfig, CostConfig |
23 | | - from phased_array_systems.scenarios import CommsLinkScenario |
| 22 | + from phased_array_systems.architecture import ( |
| 23 | + Architecture, |
| 24 | + ArrayConfig, |
| 25 | + CostConfig, |
| 26 | + RFChainConfig, |
| 27 | + ) |
24 | 28 | from phased_array_systems.evaluate import evaluate_case |
25 | | - from phased_array_systems.requirements import RequirementSet, Requirement |
| 29 | + from phased_array_systems.requirements import Requirement, RequirementSet |
| 30 | + from phased_array_systems.scenarios import CommsLinkScenario |
| 31 | + |
26 | 32 | PACKAGE_AVAILABLE = True |
27 | 33 | except ImportError: |
28 | 34 | PACKAGE_AVAILABLE = False |
|
42 | 48 | "Elements X (nx)", |
43 | 49 | options=[2, 4, 8, 16, 32, 64], |
44 | 50 | value=8, |
45 | | - help="Number of elements in X direction (power of 2)" |
| 51 | + help="Number of elements in X direction (power of 2)", |
46 | 52 | ) |
47 | 53 |
|
48 | 54 | ny = st.sidebar.select_slider( |
49 | 55 | "Elements Y (ny)", |
50 | 56 | options=[2, 4, 8, 16, 32, 64], |
51 | 57 | value=8, |
52 | | - help="Number of elements in Y direction (power of 2)" |
| 58 | + help="Number of elements in Y direction (power of 2)", |
53 | 59 | ) |
54 | 60 |
|
55 | 61 | dx_lambda = st.sidebar.slider( |
|
58 | 64 | max_value=0.7, |
59 | 65 | value=0.5, |
60 | 66 | step=0.05, |
61 | | - help="Element spacing in wavelengths" |
| 67 | + help="Element spacing in wavelengths", |
62 | 68 | ) |
63 | 69 |
|
64 | 70 | dy_lambda = st.sidebar.slider( |
|
67 | 73 | max_value=0.7, |
68 | 74 | value=0.5, |
69 | 75 | step=0.05, |
70 | | - help="Element spacing in wavelengths" |
| 76 | + help="Element spacing in wavelengths", |
71 | 77 | ) |
72 | 78 |
|
73 | 79 | st.sidebar.header("RF Chain") |
|
78 | 84 | max_value=5.0, |
79 | 85 | value=1.0, |
80 | 86 | step=0.1, |
81 | | - help="Transmit power per element in Watts" |
| 87 | + help="Transmit power per element in Watts", |
82 | 88 | ) |
83 | 89 |
|
84 | 90 | pa_efficiency = st.sidebar.slider( |
|
87 | 93 | max_value=0.7, |
88 | 94 | value=0.3, |
89 | 95 | step=0.05, |
90 | | - help="Power amplifier efficiency (0-1)" |
| 96 | + help="Power amplifier efficiency (0-1)", |
91 | 97 | ) |
92 | 98 |
|
93 | 99 | noise_figure_db = st.sidebar.slider( |
|
96 | 102 | max_value=10.0, |
97 | 103 | value=3.0, |
98 | 104 | step=0.5, |
99 | | - help="Receiver noise figure" |
| 105 | + help="Receiver noise figure", |
100 | 106 | ) |
101 | 107 |
|
102 | 108 | st.sidebar.header("Cost") |
|
107 | 113 | max_value=1000, |
108 | 114 | value=100, |
109 | 115 | step=10, |
110 | | - help="Recurring cost per element in USD" |
| 116 | + help="Recurring cost per element in USD", |
111 | 117 | ) |
112 | 118 |
|
113 | 119 | st.sidebar.header("Scenario") |
|
118 | 124 | max_value=40.0, |
119 | 125 | value=10.0, |
120 | 126 | step=0.5, |
121 | | - help="Operating frequency" |
| 127 | + help="Operating frequency", |
122 | 128 | ) |
123 | 129 |
|
124 | 130 | range_km = st.sidebar.slider( |
125 | | - "Range (km)", |
126 | | - min_value=1.0, |
127 | | - max_value=500.0, |
128 | | - value=100.0, |
129 | | - step=5.0, |
130 | | - help="Link distance" |
| 131 | + "Range (km)", min_value=1.0, max_value=500.0, value=100.0, step=5.0, help="Link distance" |
131 | 132 | ) |
132 | 133 |
|
133 | 134 | bandwidth_mhz = st.sidebar.slider( |
134 | | - "Bandwidth (MHz)", |
135 | | - min_value=1.0, |
136 | | - max_value=100.0, |
137 | | - value=10.0, |
138 | | - step=1.0, |
139 | | - help="Signal bandwidth" |
| 135 | + "Bandwidth (MHz)", min_value=1.0, max_value=100.0, value=10.0, step=1.0, help="Signal bandwidth" |
140 | 136 | ) |
141 | 137 |
|
142 | 138 | required_snr_db = st.sidebar.slider( |
|
145 | 141 | max_value=30.0, |
146 | 142 | value=10.0, |
147 | 143 | step=1.0, |
148 | | - help="SNR required for demodulation" |
| 144 | + help="SNR required for demodulation", |
149 | 145 | ) |
150 | 146 |
|
151 | 147 | # Calculate derived values |
|
159 | 155 | # Use the actual package |
160 | 156 | arch = Architecture( |
161 | 157 | array=ArrayConfig( |
162 | | - nx=nx, ny=ny, |
| 158 | + nx=nx, |
| 159 | + ny=ny, |
163 | 160 | dx_lambda=dx_lambda, |
164 | 161 | dy_lambda=dy_lambda, |
165 | 162 | enforce_subarray_constraint=False, |
|
181 | 178 |
|
182 | 179 | # Define requirements |
183 | 180 | requirements = RequirementSet() |
184 | | - requirements.add(Requirement( |
185 | | - id="REQ-SNR", |
186 | | - name="SNR Margin", |
187 | | - metric_key="link_margin_db", |
188 | | - op=">=", |
189 | | - value=0.0, |
190 | | - severity="must", |
191 | | - )) |
192 | | - requirements.add(Requirement( |
193 | | - id="REQ-COST", |
194 | | - name="System Cost", |
195 | | - metric_key="cost_usd", |
196 | | - op="<=", |
197 | | - value=100000.0, |
198 | | - severity="should", |
199 | | - )) |
| 181 | + requirements.add( |
| 182 | + Requirement( |
| 183 | + id="REQ-SNR", |
| 184 | + name="SNR Margin", |
| 185 | + metric_key="link_margin_db", |
| 186 | + op=">=", |
| 187 | + value=0.0, |
| 188 | + severity="must", |
| 189 | + ) |
| 190 | + ) |
| 191 | + requirements.add( |
| 192 | + Requirement( |
| 193 | + id="REQ-COST", |
| 194 | + name="System Cost", |
| 195 | + metric_key="cost_usd", |
| 196 | + op="<=", |
| 197 | + value=100000.0, |
| 198 | + severity="should", |
| 199 | + ) |
| 200 | + ) |
200 | 201 |
|
201 | 202 | # Evaluate |
202 | 203 | metrics = evaluate_case(arch, scenario, requirements) |
|
263 | 264 | col1, col2, col3, col4 = st.columns(4) |
264 | 265 |
|
265 | 266 | with col1: |
266 | | - st.metric( |
267 | | - "Total Elements", |
268 | | - f"{n_elements:,}", |
269 | | - help="nx × ny" |
270 | | - ) |
| 267 | + st.metric("Total Elements", f"{n_elements:,}", help="nx × ny") |
271 | 268 |
|
272 | 269 | with col2: |
273 | | - st.metric( |
274 | | - "Peak Gain", |
275 | | - f"{metrics.get('g_peak_db', 0):.1f} dB", |
276 | | - help="Antenna peak gain" |
277 | | - ) |
| 270 | + st.metric("Peak Gain", f"{metrics.get('g_peak_db', 0):.1f} dB", help="Antenna peak gain") |
278 | 271 |
|
279 | 272 | with col3: |
280 | 273 | st.metric( |
281 | | - "EIRP", |
282 | | - f"{metrics.get('eirp_dbw', 0):.1f} dBW", |
283 | | - help="Effective Isotropic Radiated Power" |
| 274 | + "EIRP", f"{metrics.get('eirp_dbw', 0):.1f} dBW", help="Effective Isotropic Radiated Power" |
284 | 275 | ) |
285 | 276 |
|
286 | | -link_margin = metrics.get('link_margin_db', 0) |
| 277 | +link_margin = metrics.get("link_margin_db", 0) |
287 | 278 | with col4: |
288 | 279 | st.metric( |
289 | 280 | "Link Margin", |
290 | 281 | f"{link_margin:.1f} dB", |
291 | 282 | delta="PASS" if link_margin >= 0 else "FAIL", |
292 | 283 | delta_color="normal" if link_margin >= 0 else "inverse", |
293 | | - help="SNR margin above required" |
| 284 | + help="SNR margin above required", |
294 | 285 | ) |
295 | 286 |
|
296 | 287 | st.divider() |
|
308 | 299 | st.write(f"- Array Size: {nx} × {ny} = {n_elements} elements") |
309 | 300 | st.write(f"- Element Spacing: {dx_lambda}λ × {dy_lambda}λ") |
310 | 301 | st.write(f"- Frequency: {freq_ghz:.1f} GHz") |
311 | | - st.write(f"- Wavelength: {3e8/freq_hz*1000:.1f} mm") |
| 302 | + st.write(f"- Wavelength: {3e8 / freq_hz * 1000:.1f} mm") |
312 | 303 |
|
313 | 304 | with col2: |
314 | 305 | st.markdown("**Performance**") |
|
325 | 316 | with col1: |
326 | 317 | st.markdown("**Transmit**") |
327 | 318 | st.write(f"- TX Power/Element: {tx_power_w:.1f} W") |
328 | | - st.write(f"- Total TX Power: {tx_power_w * n_elements:.1f} W ({10*np.log10(tx_power_w * n_elements):.1f} dBW)") |
| 319 | + st.write( |
| 320 | + f"- Total TX Power: {tx_power_w * n_elements:.1f} W ({10 * np.log10(tx_power_w * n_elements):.1f} dBW)" |
| 321 | + ) |
329 | 322 | st.write(f"- Antenna Gain: {metrics.get('g_peak_db', 0):.1f} dB") |
330 | 323 | st.write(f"- **EIRP: {metrics.get('eirp_dbw', 0):.1f} dBW**") |
331 | 324 |
|
|
341 | 334 | col1, col2, col3 = st.columns(3) |
342 | 335 |
|
343 | 336 | with col1: |
344 | | - snr = metrics.get('snr_rx_db', 0) |
| 337 | + snr = metrics.get("snr_rx_db", 0) |
345 | 338 | st.metric("Received SNR", f"{snr:.1f} dB") |
346 | 339 |
|
347 | 340 | with col2: |
348 | 341 | st.metric("Required SNR", f"{required_snr_db:.1f} dB") |
349 | 342 |
|
350 | 343 | with col3: |
351 | | - margin = metrics.get('link_margin_db', 0) |
| 344 | + margin = metrics.get("link_margin_db", 0) |
352 | 345 | color = "green" if margin >= 0 else "red" |
353 | 346 | status = "✅ PASS" if margin >= 0 else "❌ FAIL" |
354 | 347 | st.metric("Link Margin", f"{margin:.1f} dB", status) |
|
360 | 353 |
|
361 | 354 | with col1: |
362 | 355 | st.markdown("**Power**") |
363 | | - prime_power = metrics.get('prime_power_w', tx_power_w * n_elements / pa_efficiency) |
| 356 | + prime_power = metrics.get("prime_power_w", tx_power_w * n_elements / pa_efficiency) |
364 | 357 | st.write(f"- RF Power: {tx_power_w * n_elements:.1f} W") |
365 | | - st.write(f"- PA Efficiency: {pa_efficiency*100:.0f}%") |
| 358 | + st.write(f"- PA Efficiency: {pa_efficiency * 100:.0f}%") |
366 | 359 | st.write(f"- **Prime Power: {prime_power:.1f} W**") |
367 | 360 |
|
368 | 361 | with col2: |
369 | 362 | st.markdown("**Cost**") |
370 | | - total_cost = metrics.get('cost_usd', cost_per_elem * n_elements) |
| 363 | + total_cost = metrics.get("cost_usd", cost_per_elem * n_elements) |
371 | 364 | st.write(f"- Cost per Element: ${cost_per_elem:,}") |
372 | 365 | st.write(f"- Number of Elements: {n_elements:,}") |
373 | 366 | st.write(f"- **Total Cost: ${total_cost:,.0f}**") |
|
376 | 369 | st.divider() |
377 | 370 | cost_limit = 100000 |
378 | 371 | cost_pct = min(total_cost / cost_limit * 100, 100) |
379 | | - st.progress(cost_pct / 100, f"Cost Budget: ${total_cost:,.0f} / ${cost_limit:,} ({cost_pct:.0f}%)") |
| 372 | + st.progress( |
| 373 | + cost_pct / 100, f"Cost Budget: ${total_cost:,.0f} / ${cost_limit:,} ({cost_pct:.0f}%)" |
| 374 | + ) |
380 | 375 |
|
381 | 376 | # Export configuration |
382 | 377 | st.divider() |
|
408 | 403 | label="Download Configuration", |
409 | 404 | data=yaml_config, |
410 | 405 | file_name="phased_array_config.yaml", |
411 | | - mime="text/yaml" |
| 406 | + mime="text/yaml", |
412 | 407 | ) |
0 commit comments