mabuseif commited on
Commit
f7ea784
·
verified ·
1 Parent(s): 22ffd6a

Upload 31 files

Browse files
Files changed (2) hide show
  1. app/main.py +90 -298
  2. utils/psychrometric_visualization.py +163 -364
app/main.py CHANGED
@@ -116,6 +116,10 @@ class HVACCalculator:
116
  self.data_persistence = DataPersistence()
117
  self.data_export = DataExport()
118
 
 
 
 
 
119
  # Set up the application layout
120
  self.setup_layout()
121
 
@@ -142,6 +146,10 @@ class HVACCalculator:
142
  if selected_page != st.session_state.page:
143
  st.session_state.page = selected_page
144
 
 
 
 
 
145
  # Display the selected page
146
  self.display_page(st.session_state.page)
147
 
@@ -272,323 +280,48 @@ class HVACCalculator:
272
  )
273
 
274
  st.plotly_chart(fig, use_container_width=True)
275
-
276
- # Navigation buttons
277
- col1, col2 = st.columns(2)
278
- with col1:
279
- st.button("Back to Building Information", on_click=self.navigate_to, args=["Building Information"])
280
- with col2:
281
- st.button("Continue to Building Components", on_click=self.navigate_to, args=["Building Components"])
282
 
283
  def display_internal_loads(self):
284
  """Display the internal loads page."""
285
  st.title("Internal Loads")
286
 
287
- # Check if building components are available
288
- if not any(st.session_state.components.values()):
289
- st.warning("Please define building components first.")
290
- st.button("Go to Building Components", on_click=self.navigate_to, args=["Building Components"])
291
  return
292
 
293
- # Tabs for different internal load types
294
- tabs = st.tabs(["People", "Lighting", "Equipment"])
295
 
296
- # People tab
297
- with tabs[0]:
298
  self.display_people_loads()
299
 
300
- # Lighting tab
301
- with tabs[1]:
302
  self.display_lighting_loads()
303
 
304
- # Equipment tab
305
- with tabs[2]:
306
  self.display_equipment_loads()
307
 
308
  # Display summary of internal loads
309
- self.display_internal_loads_summary()
310
-
311
- # Navigation buttons
312
- col1, col2 = st.columns(2)
313
- with col1:
314
- st.button("Back to Building Components", on_click=self.navigate_to, args=["Building Components"])
315
- with col2:
316
- # Validate internal loads before proceeding
317
- if self.data_validation.validate_internal_loads(st.session_state.get("internal_loads", {})):
318
- st.button("Continue to Calculation Results", on_click=self.navigate_to, args=["Calculation Results"])
319
- else:
320
- st.button("Continue to Calculation Results", disabled=True)
321
-
322
- def display_people_loads(self):
323
- """Display the people loads section."""
324
- st.subheader("People")
325
-
326
- # Form for adding people loads
327
- with st.form("people_load_form"):
328
- col1, col2 = st.columns(2)
329
-
330
- with col1:
331
- name = st.text_input("Name", "Occupants")
332
- num_people = st.number_input("Number of People", min_value=1, value=10)
333
-
334
- with col2:
335
- activity_level = st.selectbox(
336
- "Activity Level",
337
- options=["Seated, resting", "Seated, light work", "Office work", "Standing, light work", "Walking", "Heavy work"]
338
- )
339
- zone_type = st.selectbox(
340
- "Zone Type",
341
- options=["Office", "Residential", "Retail", "Educational", "Healthcare"]
342
- )
343
-
344
- hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8)
345
-
346
- submitted = st.form_submit_button("Add People Load")
347
-
348
- if submitted:
349
- # Create people load
350
- people_load = {
351
- "id": f"people_{len(st.session_state.internal_loads['people'])}",
352
- "name": name,
353
- "num_people": num_people,
354
- "activity_level": activity_level,
355
- "zone_type": zone_type,
356
- "hours_in_operation": hours_in_operation
357
- }
358
-
359
- # Add to session state
360
- st.session_state.internal_loads['people'].append(people_load)
361
- st.success(f"Added {name} with {num_people} people")
362
-
363
- # Display existing people loads
364
- if st.session_state.internal_loads['people']:
365
- st.subheader("Existing People Loads")
366
-
367
- people_data = []
368
- for load in st.session_state.internal_loads['people']:
369
- people_data.append({
370
- "Name": load['name'],
371
- "Number of People": load['num_people'],
372
- "Activity Level": load['activity_level'],
373
- "Zone Type": load['zone_type'],
374
- "Hours in Operation": load['hours_in_operation'],
375
- "Actions": load['id']
376
- })
377
-
378
- df = pd.DataFrame(people_data)
379
-
380
- # Display table with edit and delete buttons
381
- for i, row in df.iterrows():
382
- col1, col2, col3, col4, col5, col6, col7 = st.columns([2, 1, 2, 2, 1, 1, 1])
383
-
384
- with col1:
385
- st.write(row["Name"])
386
- with col2:
387
- st.write(row["Number of People"])
388
- with col3:
389
- st.write(row["Activity Level"])
390
- with col4:
391
- st.write(row["Zone Type"])
392
- with col5:
393
- st.write(row["Hours in Operation"])
394
- with col6:
395
- if st.button("Edit", key=f"edit_{row['Actions']}"):
396
- # Set session state for editing
397
- st.session_state.editing_people = row['Actions']
398
- with col7:
399
- if st.button("Delete", key=f"delete_{row['Actions']}"):
400
- # Remove from session state
401
- st.session_state.internal_loads['people'] = [
402
- load for load in st.session_state.internal_loads['people']
403
- if load['id'] != row['Actions']
404
- ]
405
- st.experimental_rerun()
406
-
407
- def display_lighting_loads(self):
408
- """Display the lighting loads section."""
409
- st.subheader("Lighting")
410
-
411
- # Form for adding lighting loads
412
- with st.form("lighting_load_form"):
413
- col1, col2 = st.columns(2)
414
-
415
- with col1:
416
- name = st.text_input("Name", "General Lighting")
417
- power = st.number_input("Power (W)", min_value=0.0, value=1000.0)
418
-
419
- with col2:
420
- usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1)
421
- zone_type = st.selectbox(
422
- "Zone Type",
423
- options=["Office", "Residential", "Retail", "Educational", "Healthcare"]
424
- )
425
-
426
- hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8)
427
-
428
- submitted = st.form_submit_button("Add Lighting Load")
429
-
430
- if submitted:
431
- # Create lighting load
432
- lighting_load = {
433
- "id": f"lighting_{len(st.session_state.internal_loads['lighting'])}",
434
- "name": name,
435
- "power": power,
436
- "usage_factor": usage_factor,
437
- "zone_type": zone_type,
438
- "hours_in_operation": hours_in_operation
439
- }
440
-
441
- # Add to session state
442
- st.session_state.internal_loads['lighting'].append(lighting_load)
443
- st.success(f"Added {name} with {power} W")
444
-
445
- # Display existing lighting loads
446
- if st.session_state.internal_loads['lighting']:
447
- st.subheader("Existing Lighting Loads")
448
-
449
- lighting_data = []
450
- for load in st.session_state.internal_loads['lighting']:
451
- lighting_data.append({
452
- "Name": load['name'],
453
- "Power (W)": load['power'],
454
- "Usage Factor": load['usage_factor'],
455
- "Zone Type": load['zone_type'],
456
- "Hours in Operation": load['hours_in_operation'],
457
- "Actions": load['id']
458
- })
459
-
460
- df = pd.DataFrame(lighting_data)
461
-
462
- # Display table with edit and delete buttons
463
- for i, row in df.iterrows():
464
- col1, col2, col3, col4, col5, col6, col7 = st.columns([2, 1, 1, 2, 1, 1, 1])
465
-
466
- with col1:
467
- st.write(row["Name"])
468
- with col2:
469
- st.write(f"{row['Power (W)']:.1f}")
470
- with col3:
471
- st.write(f"{row['Usage Factor']:.1f}")
472
- with col4:
473
- st.write(row["Zone Type"])
474
- with col5:
475
- st.write(row["Hours in Operation"])
476
- with col6:
477
- if st.button("Edit", key=f"edit_{row['Actions']}"):
478
- # Set session state for editing
479
- st.session_state.editing_lighting = row['Actions']
480
- with col7:
481
- if st.button("Delete", key=f"delete_{row['Actions']}"):
482
- # Remove from session state
483
- st.session_state.internal_loads['lighting'] = [
484
- load for load in st.session_state.internal_loads['lighting']
485
- if load['id'] != row['Actions']
486
- ]
487
- st.experimental_rerun()
488
-
489
- def display_equipment_loads(self):
490
- """Display the equipment loads section."""
491
- st.subheader("Equipment")
492
-
493
- # Form for adding equipment loads
494
- with st.form("equipment_load_form"):
495
- col1, col2 = st.columns(2)
496
-
497
- with col1:
498
- name = st.text_input("Name", "Office Equipment")
499
- power = st.number_input("Power (W)", min_value=0.0, value=500.0)
500
-
501
- with col2:
502
- usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=0.7, step=0.1)
503
- radiation_fraction = st.slider("Radiation Fraction", min_value=0.0, max_value=1.0, value=0.3, step=0.1)
504
-
505
- zone_type = st.selectbox(
506
- "Zone Type",
507
- options=["Office", "Residential", "Retail", "Educational", "Healthcare"]
508
- )
509
-
510
- hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8)
511
-
512
- submitted = st.form_submit_button("Add Equipment Load")
513
-
514
- if submitted:
515
- # Create equipment load
516
- equipment_load = {
517
- "id": f"equipment_{len(st.session_state.internal_loads['equipment'])}",
518
- "name": name,
519
- "power": power,
520
- "usage_factor": usage_factor,
521
- "radiation_fraction": radiation_fraction,
522
- "zone_type": zone_type,
523
- "hours_in_operation": hours_in_operation
524
- }
525
-
526
- # Add to session state
527
- st.session_state.internal_loads['equipment'].append(equipment_load)
528
- st.success(f"Added {name} with {power} W")
529
-
530
- # Display existing equipment loads
531
- if st.session_state.internal_loads['equipment']:
532
- st.subheader("Existing Equipment Loads")
533
-
534
- equipment_data = []
535
- for load in st.session_state.internal_loads['equipment']:
536
- equipment_data.append({
537
- "Name": load['name'],
538
- "Power (W)": load['power'],
539
- "Usage Factor": load['usage_factor'],
540
- "Radiation Fraction": load['radiation_fraction'],
541
- "Zone Type": load['zone_type'],
542
- "Hours in Operation": load['hours_in_operation'],
543
- "Actions": load['id']
544
- })
545
-
546
- df = pd.DataFrame(equipment_data)
547
-
548
- # Display table with edit and delete buttons
549
- for i, row in df.iterrows():
550
- col1, col2, col3, col4, col5, col6, col7, col8 = st.columns([2, 1, 1, 1, 2, 1, 1, 1])
551
-
552
- with col1:
553
- st.write(row["Name"])
554
- with col2:
555
- st.write(f"{row['Power (W)']:.1f}")
556
- with col3:
557
- st.write(f"{row['Usage Factor']:.1f}")
558
- with col4:
559
- st.write(f"{row['Radiation Fraction']:.1f}")
560
- with col5:
561
- st.write(row["Zone Type"])
562
- with col6:
563
- st.write(row["Hours in Operation"])
564
- with col7:
565
- if st.button("Edit", key=f"edit_{row['Actions']}"):
566
- # Set session state for editing
567
- st.session_state.editing_equipment = row['Actions']
568
- with col8:
569
- if st.button("Delete", key=f"delete_{row['Actions']}"):
570
- # Remove from session state
571
- st.session_state.internal_loads['equipment'] = [
572
- load for load in st.session_state.internal_loads['equipment']
573
- if load['id'] != row['Actions']
574
- ]
575
- st.experimental_rerun()
576
-
577
- def display_internal_loads_summary(self):
578
- """Display a summary of all internal loads."""
579
  st.subheader("Internal Loads Summary")
580
 
581
- # Check if any internal loads exist
582
- if not any(st.session_state.internal_loads.values()):
583
- st.info("No internal loads defined yet.")
584
- return
 
 
 
 
 
585
 
586
- # Calculate total loads
587
- total_people = sum(load['num_people'] for load in st.session_state.internal_loads['people'])
588
- total_lighting_power = sum(load['power'] * load['usage_factor'] for load in st.session_state.internal_loads['lighting'])
589
- total_equipment_power = sum(load['power'] * load['usage_factor'] for load in st.session_state.internal_loads['equipment'])
590
 
591
- # Display summary
592
  col1, col2, col3 = st.columns(3)
593
 
594
  with col1:
@@ -623,6 +356,65 @@ class HVACCalculator:
623
  """
624
  st.session_state.page = page
625
  st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
 
628
  if __name__ == "__main__":
 
116
  self.data_persistence = DataPersistence()
117
  self.data_export = DataExport()
118
 
119
+ # Initialize calculation modules
120
+ self.cooling_load = CoolingLoad()
121
+ self.heating_load = HeatingLoad()
122
+
123
  # Set up the application layout
124
  self.setup_layout()
125
 
 
146
  if selected_page != st.session_state.page:
147
  st.session_state.page = selected_page
148
 
149
+ # Add calculate button in sidebar
150
+ if st.sidebar.button("Run Calculations"):
151
+ self.run_calculations()
152
+
153
  # Display the selected page
154
  self.display_page(st.session_state.page)
155
 
 
280
  )
281
 
282
  st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
283
 
284
  def display_internal_loads(self):
285
  """Display the internal loads page."""
286
  st.title("Internal Loads")
287
 
288
+ # Check if building information is available
289
+ if not st.session_state.building_info:
290
+ st.warning("Please enter building information first.")
291
+ st.button("Go to Building Information", on_click=self.navigate_to, args=["Building Information"])
292
  return
293
 
294
+ # Create tabs for different internal load types
295
+ tab1, tab2, tab3 = st.tabs(["People", "Lighting", "Equipment"])
296
 
297
+ with tab1:
 
298
  self.display_people_loads()
299
 
300
+ with tab2:
 
301
  self.display_lighting_loads()
302
 
303
+ with tab3:
 
304
  self.display_equipment_loads()
305
 
306
  # Display summary of internal loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  st.subheader("Internal Loads Summary")
308
 
309
+ # Calculate total people
310
+ total_people = 0
311
+ for people_load in st.session_state.internal_loads.get('people', []):
312
+ total_people += people_load.get('count', 0)
313
+
314
+ # Calculate total lighting power
315
+ total_lighting_power = 0
316
+ for lighting_load in st.session_state.internal_loads.get('lighting', []):
317
+ total_lighting_power += lighting_load.get('power', 0) * lighting_load.get('usage_factor', 1.0)
318
 
319
+ # Calculate total equipment power
320
+ total_equipment_power = 0
321
+ for equipment_load in st.session_state.internal_loads.get('equipment', []):
322
+ total_equipment_power += equipment_load.get('power', 0) * equipment_load.get('usage_factor', 1.0)
323
 
324
+ # Display metrics
325
  col1, col2, col3 = st.columns(3)
326
 
327
  with col1:
 
356
  """
357
  st.session_state.page = page
358
  st.experimental_rerun()
359
+
360
+ def run_calculations(self):
361
+ """Run HVAC load calculations and update session state with results."""
362
+ # Check if required data is available
363
+ if not self.data_validation.validate_calculation_inputs(st.session_state):
364
+ st.error("Cannot run calculations. Please complete all required inputs first.")
365
+ return
366
+
367
+ # Get building components and design conditions
368
+ building_components = st.session_state.components
369
+ building_info = st.session_state.building_info
370
+ internal_loads = st.session_state.internal_loads
371
+
372
+ # Extract design conditions
373
+ design_conditions = building_info.get('design_conditions', {})
374
+
375
+ # Run cooling load calculations
376
+ cooling_results = self.cooling_load.calculate_total_cooling_load(
377
+ building_components=building_components,
378
+ outdoor_temp=design_conditions.get('summer_outdoor_db', 35),
379
+ indoor_temp=design_conditions.get('summer_indoor_db', 24),
380
+ outdoor_humidity=design_conditions.get('summer_outdoor_wb', 25), # Using wet-bulb for outdoor humidity
381
+ indoor_humidity=design_conditions.get('summer_indoor_rh', 50),
382
+ ground_temp=design_conditions.get('summer_ground_temp', 20),
383
+ daily_range=design_conditions.get('summer_daily_range', 8),
384
+ month=design_conditions.get('summer_design_month', 7),
385
+ hour=design_conditions.get('summer_design_hour', 15),
386
+ latitude=building_info.get('latitude', 40),
387
+ internal_loads=internal_loads
388
+ )
389
+
390
+ # Run heating load calculations
391
+ heating_results = self.heating_load.calculate_design_heating_load(
392
+ building_components=building_components,
393
+ outdoor_temp=design_conditions.get('winter_outdoor_db', 0),
394
+ indoor_temp=design_conditions.get('winter_indoor_db', 22),
395
+ outdoor_humidity=design_conditions.get('winter_outdoor_rh', 80),
396
+ indoor_humidity=design_conditions.get('winter_indoor_rh', 40),
397
+ ground_temp=design_conditions.get('winter_ground_temp', 10),
398
+ safety_factor=design_conditions.get('heating_safety_factor', 15)
399
+ )
400
+
401
+ # Calculate load per area
402
+ floor_area = building_info.get('floor_area', 100) # Default to 100 m² if not specified
403
+
404
+ cooling_results['load_per_area'] = cooling_results['total_load'] * 1000 / floor_area # Convert kW to W/m²
405
+ heating_results['load_per_area'] = heating_results['total_load'] * 1000 / floor_area # Convert kW to W/m²
406
+
407
+ # Update session state with calculation results
408
+ st.session_state.calculation_results = {
409
+ 'cooling': cooling_results,
410
+ 'heating': heating_results
411
+ }
412
+
413
+ # Show success message
414
+ st.success("Calculations completed successfully!")
415
+
416
+ # Navigate to results page
417
+ self.navigate_to("Calculation Results")
418
 
419
 
420
  if __name__ == "__main__":
utils/psychrometric_visualization.py CHANGED
@@ -225,13 +225,7 @@ class PsychrometricVisualization:
225
  marker=dict(size=10, color=color),
226
  text=[name],
227
  textposition="top center",
228
- name=name,
229
- hovertemplate=(
230
- f"<b>{name}</b><br>" +
231
- "Temperature: %{x:.1f}°C<br>" +
232
- "Humidity Ratio: %{y:.5f} kg/kg<br>" +
233
- f"Relative Humidity: {rh:.1f}%<br>"
234
- )
235
  ))
236
 
237
  # Add processes if specified
@@ -256,380 +250,185 @@ class PsychrometricVisualization:
256
  y=[start_w, end_w],
257
  mode="lines+markers",
258
  line=dict(color=color, width=2, dash="solid"),
259
- marker=dict(size=8, color=color),
260
  name=name
261
  ))
262
-
263
- # Add arrow to indicate direction
264
- fig.add_annotation(
265
- x=end_temp,
266
- y=end_w,
267
- ax=start_temp,
268
- ay=start_w,
269
- xref="x",
270
- yref="y",
271
- axref="x",
272
- ayref="y",
273
- showarrow=True,
274
- arrowhead=2,
275
- arrowsize=1,
276
- arrowwidth=2,
277
- arrowcolor=color
278
- )
279
 
280
  # Update layout
281
  fig.update_layout(
282
  title="Psychrometric Chart",
283
  xaxis_title="Dry-Bulb Temperature (°C)",
284
  yaxis_title="Humidity Ratio (kg/kg)",
285
- xaxis=dict(
286
- range=[self.temp_min, self.temp_max],
287
- gridcolor="rgba(0, 0, 0, 0.1)",
288
- showgrid=True
289
- ),
290
- yaxis=dict(
291
- range=[self.w_min, self.w_max],
292
- gridcolor="rgba(0, 0, 0, 0.1)",
293
- showgrid=True
294
- ),
295
  height=700,
296
- margin=dict(l=50, r=50, b=50, t=50),
297
- legend=dict(
298
- orientation="h",
299
- yanchor="bottom",
300
- y=1.02,
301
- xanchor="right",
302
- x=1
303
- ),
304
- hovermode="closest"
305
  )
306
 
 
 
 
 
307
  return fig
308
-
309
- def create_process_visualization(self, process: Dict[str, Any]) -> go.Figure:
310
  """
311
- Create a visualization of a psychrometric process.
312
 
313
  Args:
314
- process: Dictionary with process parameters
315
-
316
- Returns:
317
- Plotly figure with process visualization
318
  """
319
- # Extract process parameters
320
- start_point = process.get("start", {})
321
- end_point = process.get("end", {})
322
-
323
- start_temp = start_point.get("temp", 0)
324
- start_rh = start_point.get("rh", 0)
325
-
326
- end_temp = end_point.get("temp", 0)
327
- end_rh = end_point.get("rh", 0)
328
-
329
- # Calculate psychrometric properties
330
- start_props = self.psychrometrics.moist_air_properties(start_temp, start_rh, self.pressure)
331
- end_props = self.psychrometrics.moist_air_properties(end_temp, end_rh, self.pressure)
332
-
333
- # Calculate process changes
334
- delta_t = end_temp - start_temp
335
- delta_w = end_props["humidity_ratio"] - start_props["humidity_ratio"]
336
- delta_h = end_props["enthalpy"] - start_props["enthalpy"]
337
-
338
- # Determine process type
339
- process_type = "Unknown"
340
- if abs(delta_w) < 0.0001: # Sensible heating/cooling
341
- if delta_t > 0:
342
- process_type = "Sensible Heating"
343
- else:
344
- process_type = "Sensible Cooling"
345
- elif abs(delta_t) < 0.1: # Humidification/Dehumidification
346
- if delta_w > 0:
347
- process_type = "Humidification"
348
- else:
349
- process_type = "Dehumidification"
350
- elif delta_t > 0 and delta_w > 0:
351
- process_type = "Heating and Humidification"
352
- elif delta_t < 0 and delta_w < 0:
353
- process_type = "Cooling and Dehumidification"
354
- elif delta_t > 0 and delta_w < 0:
355
- process_type = "Heating and Dehumidification"
356
- elif delta_t < 0 and delta_w > 0:
357
- process_type = "Cooling and Humidification"
358
-
359
- # Create figure
360
- fig = go.Figure()
361
-
362
- # Add process to psychrometric chart
363
- chart_fig = self.create_psychrometric_chart(
364
- points=[
365
- {"temp": start_temp, "rh": start_rh, "name": "Start", "color": "blue"},
366
- {"temp": end_temp, "rh": end_rh, "name": "End", "color": "red"}
367
- ],
368
- processes=[
369
- {"start": {"temp": start_temp, "rh": start_rh},
370
- "end": {"temp": end_temp, "rh": end_rh},
371
- "name": process_type,
372
- "color": "green"}
373
- ]
374
- )
375
-
376
- # Create process diagram
377
- # Create data for process parameters
378
- params = [
379
- "Dry-Bulb Temperature (°C)",
380
- "Relative Humidity (%)",
381
- "Humidity Ratio (g/kg)",
382
- "Enthalpy (kJ/kg)",
383
- "Wet-Bulb Temperature (°C)",
384
- "Dew Point Temperature (°C)",
385
- "Specific Volume (m³/kg)"
386
- ]
387
-
388
- start_values = [
389
- start_props["dry_bulb_temperature"],
390
- start_props["relative_humidity"],
391
- start_props["humidity_ratio"] * 1000, # Convert to g/kg
392
- start_props["enthalpy"] / 1000, # Convert to kJ/kg
393
- start_props["wet_bulb_temperature"],
394
- start_props["dew_point_temperature"],
395
- start_props["specific_volume"]
396
- ]
397
-
398
- end_values = [
399
- end_props["dry_bulb_temperature"],
400
- end_props["relative_humidity"],
401
- end_props["humidity_ratio"] * 1000, # Convert to g/kg
402
- end_props["enthalpy"] / 1000, # Convert to kJ/kg
403
- end_props["wet_bulb_temperature"],
404
- end_props["dew_point_temperature"],
405
- end_props["specific_volume"]
406
  ]
407
 
408
- delta_values = [end - start for start, end in zip(start_values, end_values)]
409
-
410
- # Create table
411
- table_fig = go.Figure(data=[go.Table(
412
- header=dict(
413
- values=["Parameter", "Start", "End", "Change"],
414
- fill_color="paleturquoise",
415
- align="left",
416
- font=dict(size=12)
417
- ),
418
- cells=dict(
419
- values=[
420
- params,
421
- [f"{val:.2f}" for val in start_values],
422
- [f"{val:.2f}" for val in end_values],
423
- [f"{val:.2f}" for val in delta_values]
424
- ],
425
- fill_color="lavender",
426
- align="left",
427
- font=dict(size=11)
428
- )
429
- )])
430
-
431
- table_fig.update_layout(
432
- title=f"Process Parameters: {process_type}",
433
- height=300,
434
- margin=dict(l=0, r=0, b=0, t=30)
435
- )
436
-
437
- return chart_fig, table_fig
438
-
439
- def display_psychrometric_visualization(self) -> None:
440
- """
441
- Display psychrometric visualization in Streamlit.
442
- """
443
- st.header("Psychrometric Visualization")
444
-
445
- # Create tabs for different visualizations
446
- tab1, tab2, tab3 = st.tabs([
447
- "Interactive Psychrometric Chart",
448
- "Process Visualization",
449
- "Comfort Zone Analysis"
450
- ])
451
-
452
- with tab1:
453
- st.subheader("Interactive Psychrometric Chart")
454
-
455
- # Add controls for points
456
- st.write("Add points to the chart:")
457
-
458
- col1, col2, col3 = st.columns(3)
459
-
460
- with col1:
461
- point1_temp = st.number_input("Point 1 Temperature (°C)", -10.0, 50.0, 20.0, key="point1_temp")
462
- point1_rh = st.number_input("Point 1 RH (%)", 0.0, 100.0, 50.0, key="point1_rh")
463
-
464
- with col2:
465
- point2_temp = st.number_input("Point 2 Temperature (°C)", -10.0, 50.0, 30.0, key="point2_temp")
466
- point2_rh = st.number_input("Point 2 RH (%)", 0.0, 100.0, 40.0, key="point2_rh")
467
-
468
- with col3:
469
- show_process = st.checkbox("Show Process Line", True, key="show_process")
470
- process_name = st.text_input("Process Name", "Cooling Process", key="process_name")
471
-
472
- # Create points
473
- points = [
474
- {"temp": point1_temp, "rh": point1_rh, "name": "Point 1", "color": "blue"},
475
- {"temp": point2_temp, "rh": point2_rh, "name": "Point 2", "color": "red"}
476
- ]
477
-
478
- # Create process if enabled
479
- processes = []
480
- if show_process:
481
- processes.append({
482
- "start": {"temp": point1_temp, "rh": point1_rh},
483
- "end": {"temp": point2_temp, "rh": point2_rh},
484
- "name": process_name,
485
- "color": "green"
486
- })
487
-
488
- # Create and display chart
489
- fig = self.create_psychrometric_chart(points=points, processes=processes)
490
- st.plotly_chart(fig, use_container_width=True)
491
-
492
- # Display point properties
493
- col1, col2 = st.columns(2)
494
-
495
- with col1:
496
- st.subheader("Point 1 Properties")
497
- props1 = self.psychrometrics.moist_air_properties(point1_temp, point1_rh, self.pressure)
498
- st.write(f"Dry-Bulb Temperature: {props1['dry_bulb_temperature']:.2f} °C")
499
- st.write(f"Relative Humidity: {props1['relative_humidity']:.2f} %")
500
- st.write(f"Humidity Ratio: {props1['humidity_ratio']*1000:.2f} g/kg")
501
- st.write(f"Enthalpy: {props1['enthalpy']/1000:.2f} kJ/kg")
502
- st.write(f"Wet-Bulb Temperature: {props1['wet_bulb_temperature']:.2f} °C")
503
- st.write(f"Dew Point Temperature: {props1['dew_point_temperature']:.2f} °C")
504
-
505
- with col2:
506
- st.subheader("Point 2 Properties")
507
- props2 = self.psychrometrics.moist_air_properties(point2_temp, point2_rh, self.pressure)
508
- st.write(f"Dry-Bulb Temperature: {props2['dry_bulb_temperature']:.2f} °C")
509
- st.write(f"Relative Humidity: {props2['relative_humidity']:.2f} %")
510
- st.write(f"Humidity Ratio: {props2['humidity_ratio']*1000:.2f} g/kg")
511
- st.write(f"Enthalpy: {props2['enthalpy']/1000:.2f} kJ/kg")
512
- st.write(f"Wet-Bulb Temperature: {props2['wet_bulb_temperature']:.2f} °C")
513
- st.write(f"Dew Point Temperature: {props2['dew_point_temperature']:.2f} °C")
514
-
515
- with tab2:
516
- st.subheader("Process Visualization")
517
-
518
- # Add controls for process
519
- st.write("Define a psychrometric process:")
520
-
521
- col1, col2 = st.columns(2)
522
-
523
- with col1:
524
- st.write("Starting Point")
525
- start_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 24.0, key="start_temp")
526
- start_rh = st.number_input("RH (%)", 0.0, 100.0, 50.0, key="start_rh")
527
-
528
- with col2:
529
- st.write("Ending Point")
530
- end_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 14.0, key="end_temp")
531
- end_rh = st.number_input("RH (%)", 0.0, 100.0, 90.0, key="end_rh")
532
-
533
- # Create process
534
- process = {
535
- "start": {"temp": start_temp, "rh": start_rh},
536
- "end": {"temp": end_temp, "rh": end_rh}
537
  }
538
-
539
- # Create and display process visualization
540
- chart_fig, table_fig = self.create_process_visualization(process)
541
-
542
- st.plotly_chart(chart_fig, use_container_width=True)
543
- st.plotly_chart(table_fig, use_container_width=True)
544
-
545
- # Calculate process energy requirements
546
- start_props = self.psychrometrics.moist_air_properties(start_temp, start_rh, self.pressure)
547
- end_props = self.psychrometrics.moist_air_properties(end_temp, end_rh, self.pressure)
548
-
549
- delta_h = end_props["enthalpy"] - start_props["enthalpy"] # J/kg
550
-
551
- st.subheader("Energy Calculations")
552
-
553
- air_flow = st.number_input("Air Flow Rate (m³/s)", 0.1, 100.0, 1.0, key="air_flow")
554
-
555
- # Calculate mass flow rate
556
- density = start_props["density"] # kg/m³
557
- mass_flow = air_flow * density # kg/s
558
-
559
- # Calculate energy rate
560
- energy_rate = mass_flow * delta_h # W
561
-
562
- st.write(f"Air Density: {density:.2f} kg/m³")
563
- st.write(f"Mass Flow Rate: {mass_flow:.2f} kg/s")
564
- st.write(f"Enthalpy Change: {delta_h/1000:.2f} kJ/kg")
565
- st.write(f"Energy Rate: {energy_rate/1000:.2f} kW")
566
 
567
- with tab3:
568
- st.subheader("Comfort Zone Analysis")
569
-
570
- # Add controls for comfort zone
571
- st.write("Define comfort zone parameters:")
572
-
573
- col1, col2 = st.columns(2)
574
-
575
- with col1:
576
- temp_min = st.number_input("Minimum Temperature (°C)", 10.0, 30.0, 20.0, key="temp_min")
577
- temp_max = st.number_input("Maximum Temperature (°C)", 10.0, 30.0, 26.0, key="temp_max")
578
-
579
- with col2:
580
- rh_min = st.number_input("Minimum RH (%)", 0.0, 100.0, 30.0, key="rh_min")
581
- rh_max = st.number_input("Maximum RH (%)", 0.0, 100.0, 60.0, key="rh_max")
582
-
583
- # Create comfort zone
584
- comfort_zone = {
585
- "temp_min": temp_min,
586
- "temp_max": temp_max,
587
- "rh_min": rh_min,
588
- "rh_max": rh_max
589
- }
590
-
591
- # Add point to check if it's in comfort zone
592
- st.write("Check if a point is within the comfort zone:")
593
-
594
- col1, col2 = st.columns(2)
595
-
596
- with col1:
597
- check_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 22.0, key="check_temp")
598
- check_rh = st.number_input("RH (%)", 0.0, 100.0, 45.0, key="check_rh")
599
-
600
- # Check if point is in comfort zone
601
- in_comfort_zone = (
602
- temp_min <= check_temp <= temp_max and
603
- rh_min <= check_rh <= rh_max
604
- )
605
-
606
- with col2:
607
- if in_comfort_zone:
608
- st.success("✅ Point is within the comfort zone")
609
- else:
610
- st.error("❌ Point is outside the comfort zone")
611
-
612
- # Calculate properties
613
- check_props = self.psychrometrics.moist_air_properties(check_temp, check_rh, self.pressure)
614
- st.write(f"Humidity Ratio: {check_props['humidity_ratio']*1000:.2f} g/kg")
615
- st.write(f"Enthalpy: {check_props['enthalpy']/1000:.2f} kJ/kg")
616
- st.write(f"Wet-Bulb Temperature: {check_props['wet_bulb_temperature']:.2f} °C")
617
-
618
- # Create and display chart with comfort zone
619
- fig = self.create_psychrometric_chart(
620
- points=[{"temp": check_temp, "rh": check_rh, "name": "Test Point", "color": "purple"}],
621
- comfort_zone=comfort_zone
622
- )
623
-
624
- st.plotly_chart(fig, use_container_width=True)
625
-
626
-
627
- # Create a singleton instance
628
- psychrometric_visualization = PsychrometricVisualization()
629
-
630
- # Example usage
631
- if __name__ == "__main__":
632
- import streamlit as st
633
-
634
- # Display psychrometric visualization
635
- psychrometric_visualization.display_psychrometric_visualization()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  marker=dict(size=10, color=color),
226
  text=[name],
227
  textposition="top center",
228
+ name=name
 
 
 
 
 
 
229
  ))
230
 
231
  # Add processes if specified
 
250
  y=[start_w, end_w],
251
  mode="lines+markers",
252
  line=dict(color=color, width=2, dash="solid"),
253
+ marker=dict(size=8),
254
  name=name
255
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  # Update layout
258
  fig.update_layout(
259
  title="Psychrometric Chart",
260
  xaxis_title="Dry-Bulb Temperature (°C)",
261
  yaxis_title="Humidity Ratio (kg/kg)",
262
+ legend_title="Legend",
 
 
 
 
 
 
 
 
 
263
  height=700,
264
+ margin=dict(l=50, r=50, t=50, b=50),
265
+ plot_bgcolor="white",
266
+ paper_bgcolor="white",
267
+ font=dict(size=12)
 
 
 
 
 
268
  )
269
 
270
+ # Set axis ranges
271
+ fig.update_xaxes(range=[self.temp_min, self.temp_max], gridcolor="lightgray")
272
+ fig.update_yaxes(range=[self.w_min, self.w_max], gridcolor="lightgray")
273
+
274
  return fig
275
+
276
+ def display_psychrometric_chart(self, calculation_results: Dict[str, Any], design_conditions: Dict[str, Any]) -> None:
277
  """
278
+ Display psychrometric chart with calculation results.
279
 
280
  Args:
281
+ calculation_results: Dictionary containing calculation results
282
+ design_conditions: Dictionary containing design conditions
 
 
283
  """
284
+ # Extract design conditions
285
+ summer_outdoor_db = design_conditions.get("summer_outdoor_db", 35)
286
+ summer_outdoor_wb = design_conditions.get("summer_outdoor_wb", 25)
287
+ summer_indoor_db = design_conditions.get("summer_indoor_db", 24)
288
+ summer_indoor_rh = design_conditions.get("summer_indoor_rh", 50)
289
+
290
+ winter_outdoor_db = design_conditions.get("winter_outdoor_db", 0)
291
+ winter_outdoor_rh = design_conditions.get("winter_outdoor_rh", 80)
292
+ winter_indoor_db = design_conditions.get("winter_indoor_db", 22)
293
+ winter_indoor_rh = design_conditions.get("winter_indoor_rh", 40)
294
+
295
+ # Calculate humidity ratios
296
+ summer_outdoor_w = self.psychrometrics.humidity_ratio_from_wb(summer_outdoor_db, summer_outdoor_wb, self.pressure)
297
+ summer_indoor_w = self.psychrometrics.humidity_ratio(summer_indoor_db, summer_indoor_rh, self.pressure)
298
+ winter_outdoor_w = self.psychrometrics.humidity_ratio(winter_outdoor_db, winter_outdoor_rh, self.pressure)
299
+ winter_indoor_w = self.psychrometrics.humidity_ratio(winter_indoor_db, winter_indoor_rh, self.pressure)
300
+
301
+ # Create points for psychrometric chart
302
+ points = [
303
+ {
304
+ "temp": summer_outdoor_db,
305
+ "w": summer_outdoor_w,
306
+ "name": "Summer Outdoor",
307
+ "color": "red"
308
+ },
309
+ {
310
+ "temp": summer_indoor_db,
311
+ "w": summer_indoor_w,
312
+ "name": "Summer Indoor",
313
+ "color": "blue"
314
+ },
315
+ {
316
+ "temp": winter_outdoor_db,
317
+ "w": winter_outdoor_w,
318
+ "name": "Winter Outdoor",
319
+ "color": "purple"
320
+ },
321
+ {
322
+ "temp": winter_indoor_db,
323
+ "w": winter_indoor_w,
324
+ "name": "Winter Indoor",
325
+ "color": "green"
326
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  ]
328
 
329
+ # Create processes for psychrometric chart
330
+ processes = [
331
+ {
332
+ "start": {"temp": summer_outdoor_db, "w": summer_outdoor_w},
333
+ "end": {"temp": summer_indoor_db, "w": summer_indoor_w},
334
+ "name": "Cooling Process",
335
+ "color": "blue"
336
+ },
337
+ {
338
+ "start": {"temp": winter_outdoor_db, "w": winter_outdoor_w},
339
+ "end": {"temp": winter_indoor_db, "w": winter_indoor_w},
340
+ "name": "Heating Process",
341
+ "color": "red"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ # Create comfort zone
346
+ comfort_zone = {
347
+ "temp_min": 20,
348
+ "temp_max": 26,
349
+ "rh_min": 30,
350
+ "rh_max": 60
351
+ }
352
+
353
+ # Create psychrometric chart
354
+ fig = self.create_psychrometric_chart(points, processes, comfort_zone)
355
+
356
+ # Display chart in Streamlit
357
+ st.plotly_chart(fig, use_container_width=True)
358
+
359
+ # Display psychrometric properties
360
+ st.subheader("Psychrometric Properties")
361
+
362
+ # Create dataframe for properties
363
+ properties = []
364
+
365
+ # Summer outdoor properties
366
+ summer_outdoor_rh = self.psychrometrics.relative_humidity_from_wb(summer_outdoor_db, summer_outdoor_wb, self.pressure)
367
+ summer_outdoor_dp = self.psychrometrics.dew_point(summer_outdoor_db, summer_outdoor_rh, self.pressure)
368
+ summer_outdoor_h = self.psychrometrics.enthalpy(summer_outdoor_db, summer_outdoor_w)
369
+ summer_outdoor_v = self.psychrometrics.specific_volume(summer_outdoor_db, summer_outdoor_w, self.pressure)
370
+
371
+ properties.append({
372
+ "Point": "Summer Outdoor",
373
+ "Dry-Bulb (°C)": f"{summer_outdoor_db:.1f}",
374
+ "Wet-Bulb (°C)": f"{summer_outdoor_wb:.1f}",
375
+ "Relative Humidity (%)": f"{summer_outdoor_rh:.1f}",
376
+ "Humidity Ratio (g/kg)": f"{summer_outdoor_w*1000:.1f}",
377
+ "Dew Point (°C)": f"{summer_outdoor_dp:.1f}",
378
+ "Enthalpy (kJ/kg)": f"{summer_outdoor_h/1000:.1f}",
379
+ "Specific Volume (m³/kg)": f"{summer_outdoor_v:.3f}"
380
+ })
381
+
382
+ # Summer indoor properties
383
+ summer_indoor_wb = self.psychrometrics.wet_bulb_temperature(summer_indoor_db, summer_indoor_rh, self.pressure)
384
+ summer_indoor_dp = self.psychrometrics.dew_point(summer_indoor_db, summer_indoor_rh, self.pressure)
385
+ summer_indoor_h = self.psychrometrics.enthalpy(summer_indoor_db, summer_indoor_w)
386
+ summer_indoor_v = self.psychrometrics.specific_volume(summer_indoor_db, summer_indoor_w, self.pressure)
387
+
388
+ properties.append({
389
+ "Point": "Summer Indoor",
390
+ "Dry-Bulb (°C)": f"{summer_indoor_db:.1f}",
391
+ "Wet-Bulb (°C)": f"{summer_indoor_wb:.1f}",
392
+ "Relative Humidity (%)": f"{summer_indoor_rh:.1f}",
393
+ "Humidity Ratio (g/kg)": f"{summer_indoor_w*1000:.1f}",
394
+ "Dew Point (°C)": f"{summer_indoor_dp:.1f}",
395
+ "Enthalpy (kJ/kg)": f"{summer_indoor_h/1000:.1f}",
396
+ "Specific Volume (m³/kg)": f"{summer_indoor_v:.3f}"
397
+ })
398
+
399
+ # Winter outdoor properties
400
+ winter_outdoor_wb = self.psychrometrics.wet_bulb_temperature(winter_outdoor_db, winter_outdoor_rh, self.pressure)
401
+ winter_outdoor_dp = self.psychrometrics.dew_point(winter_outdoor_db, winter_outdoor_rh, self.pressure)
402
+ winter_outdoor_h = self.psychrometrics.enthalpy(winter_outdoor_db, winter_outdoor_w)
403
+ winter_outdoor_v = self.psychrometrics.specific_volume(winter_outdoor_db, winter_outdoor_w, self.pressure)
404
+
405
+ properties.append({
406
+ "Point": "Winter Outdoor",
407
+ "Dry-Bulb (°C)": f"{winter_outdoor_db:.1f}",
408
+ "Wet-Bulb (°C)": f"{winter_outdoor_wb:.1f}",
409
+ "Relative Humidity (%)": f"{winter_outdoor_rh:.1f}",
410
+ "Humidity Ratio (g/kg)": f"{winter_outdoor_w*1000:.1f}",
411
+ "Dew Point (°C)": f"{winter_outdoor_dp:.1f}",
412
+ "Enthalpy (kJ/kg)": f"{winter_outdoor_h/1000:.1f}",
413
+ "Specific Volume (m³/kg)": f"{winter_outdoor_v:.3f}"
414
+ })
415
+
416
+ # Winter indoor properties
417
+ winter_indoor_wb = self.psychrometrics.wet_bulb_temperature(winter_indoor_db, winter_indoor_rh, self.pressure)
418
+ winter_indoor_dp = self.psychrometrics.dew_point(winter_indoor_db, winter_indoor_rh, self.pressure)
419
+ winter_indoor_h = self.psychrometrics.enthalpy(winter_indoor_db, winter_indoor_w)
420
+ winter_indoor_v = self.psychrometrics.specific_volume(winter_indoor_db, winter_indoor_w, self.pressure)
421
+
422
+ properties.append({
423
+ "Point": "Winter Indoor",
424
+ "Dry-Bulb (°C)": f"{winter_indoor_db:.1f}",
425
+ "Wet-Bulb (°C)": f"{winter_indoor_wb:.1f}",
426
+ "Relative Humidity (%)": f"{winter_indoor_rh:.1f}",
427
+ "Humidity Ratio (g/kg)": f"{winter_indoor_w*1000:.1f}",
428
+ "Dew Point (°C)": f"{winter_indoor_dp:.1f}",
429
+ "Enthalpy (kJ/kg)": f"{winter_indoor_h/1000:.1f}",
430
+ "Specific Volume (m³/kg)": f"{winter_indoor_v:.3f}"
431
+ })
432
+
433
+ # Display properties table
434
+ st.dataframe(pd.DataFrame(properties), use_container_width=True)