mabuseif commited on
Commit
2ca5e8d
·
verified ·
1 Parent(s): 5e95917

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +170 -63
data/climate_data.py CHANGED
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
30
  # Define paths at module level
31
  AU_CCH_DIR = "au_cch" # Relative path to au_cch folder from climate_data.py in data/ (e.g., au_cch/1/RCP2.6/2070/)
32
 
33
- # CSS for consistent formatting (from your original file)
34
  STYLE = """
35
  <style>
36
  .markdown-text {
@@ -59,7 +59,7 @@ STYLE = """
59
  </style>
60
  """
61
 
62
- # Location mapping from provided list (from your original file)
63
  LOCATION_MAPPING = {
64
  "24": {"city": "Canberra", "state": "ACT"},
65
  "11": {"city": "Coffs Harbour", "state": "NSW"},
@@ -148,12 +148,12 @@ class ClimateLocation:
148
  logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
149
 
150
  # Calculate wet-bulb temperature
151
- self.wet_bulb_temperatures = ClimateData.calculate_wet_bulb(dry_bulb, humidity) # Store for potential use
152
 
153
  # Calculate design conditions
154
  self.winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
155
  self.summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
156
- self.summer_design_temp_wb = round(np.nanpercentile(self.wet_bulb_temperatures, 99.6), 1)
157
 
158
  # Calculate degree days
159
  daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
@@ -420,29 +420,15 @@ class ClimateData:
420
 
421
  def load_climate_data(self, country: str, city: str) -> Optional[Dict[str, Any]]:
422
  """Load climate data for a given country and city."""
423
- # This implementation assumes EPW files are stored in a specific directory structure
424
- # e.g., data/au_cch/1/RCP2.6/2070/
425
-
426
- # This is a simplified example, you would need a more robust way to find the EPW file
427
- # based on country and city, potentially from a manifest or by scanning directories.
428
-
429
- # For demonstration, let's assume a direct path if a dummy EPW is available
430
- # In a real scenario, you'd map country/city to a specific EPW file path.
431
-
432
  epw_file_path = None
433
- # Example: If you have a known EPW file for Geelong or other mapped locations
434
  found_id = None
435
  for loc_id, loc_info in LOCATION_MAPPING.items():
436
- if loc_info["city"] == city and (loc_info["state"] == "VIC" if city == "Melbourne RO" else True): # Add specific check for Melbourne RO
437
- # This is a placeholder for the actual EPW file path logic
438
- # You'll need to map LOCATION_MAPPING IDs to actual EPW file names.
439
- # For now, using a generic example.
440
- epw_file_name = f"AUS_VIC_Melbourne.AP.720520_TMY3.epw" # Default example
441
  if city == "Geelong":
442
- epw_file_name = "AUS_VIC_Melbourne.AP.720520_TMY3.epw" # Placeholder for Geelong
443
  elif city == "Sydney RO (Observatory Hill)":
444
- epw_file_name = "AUS_NSW_Sydney.AP.947670_TMY3.epw" # Placeholder for Sydney
445
- # Add more specific mappings as needed
446
 
447
  epw_file_path = os_join("data", AU_CCH_DIR, epw_file_name)
448
  found_id = loc_id
@@ -457,37 +443,40 @@ class ClimateData:
457
  if epw_data_df.empty:
458
  return None
459
 
460
- hourly_data = self._process_epw_data(epw_data_df)
461
-
462
- # Extract metadata from EPW header (simplified for this example)
463
- # In a real scenario, you'd parse the EPW header more thoroughly
464
- location_id = found_id if found_id else f"{country}_{city}".replace(" ", "_").lower()
465
 
466
  # Retrieve typical_extreme_periods and ground_temperatures
467
  typical_extreme_periods = self.get_typical_extreme_periods(epw_file_path)
468
  ground_temperatures = self.get_ground_temperatures(epw_file_path)
469
 
 
 
470
  location = ClimateLocation(
471
- epw_file=epw_file_path,
472
  typical_extreme_periods=typical_extreme_periods,
473
  ground_temperatures=ground_temperatures,
474
  id=location_id,
475
  country=country,
476
- state_province=LOCATION_MAPPING.get(found_id, {}).get("state", "N/A"), # Use mapped state
477
  city=city,
478
- latitude=epw_data_df.loc[0, 'latitude'], # Assuming lat/lon in first row of epw_data_df
479
  longitude=epw_data_df.loc[0, 'longitude'],
480
  elevation=epw_data_df.loc[0, 'elevation'],
481
  time_zone=epw_data_df.loc[0, 'time_zone'],
482
- climate_zone="Unknown", # You'd need a method to determine this from lat/lon or EPW
483
- hourly_data=hourly_data
 
 
 
 
484
  )
485
  self.add_location(location)
486
  return location.to_dict()
487
 
488
  def get_climate_zone_description(self, climate_zone_code: str) -> str:
489
  """Return a description for a given ASHRAE climate zone code."""
490
- # This is a placeholder; you'd have a mapping for climate zone codes to descriptions
491
  zone_descriptions = {
492
  "1A": "Hot-Humid", "1B": "Hot-Dry",
493
  "2A": "Warm-Humid", "2B": "Warm-Dry",
@@ -506,18 +495,14 @@ class ClimateData:
506
  with open(epw_file_path, 'r') as f:
507
  for line in f:
508
  if line.startswith('COMMENT1'):
509
- # Example: COMMENT1 TYPICAL EXTREME PERIODS: Summer Design Day, Winter Design Day
510
  match = re.search(r'TYPICAL EXTREME PERIODS: (.+)', line)
511
  if match:
512
  periods_str = match.group(1)
513
- # This parsing is highly dependent on the exact format in EPW comments
514
- # A more robust parser might be needed for varied EPW files.
515
- # For now, a dummy structure, as in your original file's behavior:
516
  typical_extreme_periods = {
517
  "summer_design_day": {"start_date": "07/21", "end_date": "07/21"},
518
  "winter_design_day": {"start_date": "01/21", "end_date": "01/21"}
519
  }
520
- break # Assume only one COMMENT1 line for this info
521
  except Exception as e:
522
  logger.warning(f"Could not extract typical/extreme periods from EPW comments: {e}")
523
  return typical_extreme_periods
@@ -525,7 +510,7 @@ class ClimateData:
525
  def get_ground_temperatures(self, epw_file_path: str) -> Dict[str, List[float]]:
526
  """Extract ground temperatures from EPW file comments."""
527
  ground_temperatures = {
528
- "depth_0.5m": [15.0] * 12, # Dummy monthly values
529
  "depth_1.0m": [14.0] * 12,
530
  "depth_2.0m": [13.0] * 12,
531
  "depth_4.0m": [12.0] * 12
@@ -534,11 +519,6 @@ class ClimateData:
534
  with open(epw_file_path, 'r') as f:
535
  for line in f:
536
  if line.startswith('COMMENT2'):
537
- # Example: COMMENT2 GROUND TEMPERATURES (C) 0.5m: 15.0,15.1,...
538
- # This parsing is highly dependent on the exact format in EPW comments
539
- # and would need to be robust.
540
- # For now, we'll stick with the dummy values as parsing this from raw EPW
541
- # comments can be fragile without specific format knowledge.
542
  pass
543
  except Exception as e:
544
  logger.warning(f"Could not extract ground temperatures from EPW comments: {e}")
@@ -546,10 +526,7 @@ class ClimateData:
546
 
547
  def save_climate_data(self, climate_data_dict: Dict[str, Any]):
548
  """Save climate data to a JSON file."""
549
- # This is a placeholder for saving logic.
550
- # In a real app, you might save to a user-specific location or database.
551
  logger.info(f"Saving climate data (placeholder): {climate_data_dict['id']}")
552
- # Example: save to a 'saved_data' directory
553
  save_dir = "saved_data"
554
  os.makedirs(save_dir, exist_ok=True)
555
  file_path = os_join(save_dir, f"{climate_data_dict['id']}.json")
@@ -563,8 +540,6 @@ class ClimateData:
563
 
564
  def load_saved_climate_data(self) -> 'ClimateData':
565
  """Load saved climate data from JSON files."""
566
- # This is a placeholder for loading logic.
567
- # It would typically scan a directory or query a database.
568
  load_dir = "saved_data"
569
  if os.path.exists(load_dir):
570
  for filename in os.listdir(load_dir):
@@ -573,16 +548,11 @@ class ClimateData:
573
  try:
574
  with open(file_path, 'r') as f:
575
  loc_dict = json.load(f)
576
- # Reconstruct ClimateLocation object
577
- # Ensure all expected keys are present, handle missing gracefully
578
  hourly_data = loc_dict.get("hourly_data", [])
579
 
580
- # The original ClimateLocation __init__ expects a DataFrame for epw_file.
581
- # When loading from JSON, you'd typically have processed hourly_data.
582
- # We need to adapt this. For now, we'll pass a dummy DataFrame
583
- # if epw_file is not explicitly loaded, and rely on hourly_data.
584
-
585
- # Create a dummy DataFrame if not available, as ClimateLocation expects it
586
  dummy_epw_df = pd.DataFrame({
587
  1: [d['month'] for d in hourly_data],
588
  2: [d['day'] for d in hourly_data],
@@ -595,11 +565,21 @@ class ClimateData:
595
  15: [d['diffuse_horizontal_radiation'] for d in hourly_data],
596
  20: [d['wind_direction'] for d in hourly_data],
597
  21: [d['wind_speed'] for d in hourly_data],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  })
599
- # Add dummy metadata columns if they don't exist, as ClimateLocation expects them
600
- for col in ['latitude', 'longitude', 'elevation', 'time_zone']:
601
- if col not in dummy_epw_df.columns:
602
- dummy_epw_df[col] = loc_dict.get(col, 0.0) # Use saved value or default
603
 
604
  location = ClimateLocation(
605
  epw_file=dummy_epw_df, # Pass dummy DataFrame for __init__
@@ -988,8 +968,135 @@ class ClimateData:
988
 
989
  return fig
990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  # --- Original if __name__ == "__main__": block from your file ---
992
  if __name__ == "__main__":
993
  climate_data = ClimateData()
994
- session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
995
- climate_data.display_climate_input(session_state)
 
30
  # Define paths at module level
31
  AU_CCH_DIR = "au_cch" # Relative path to au_cch folder from climate_data.py in data/ (e.g., au_cch/1/RCP2.6/2070/)
32
 
33
+ # CSS for consistent formatting
34
  STYLE = """
35
  <style>
36
  .markdown-text {
 
59
  </style>
60
  """
61
 
62
+ # Location mapping from provided list
63
  LOCATION_MAPPING = {
64
  "24": {"city": "Canberra", "state": "ACT"},
65
  "11": {"city": "Coffs Harbour", "state": "NSW"},
 
148
  logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
149
 
150
  # Calculate wet-bulb temperature
151
+ wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
152
 
153
  # Calculate design conditions
154
  self.winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
155
  self.summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
156
+ self.summer_design_temp_wb = round(np.nanpercentile(wet_bulb, 99.6), 1)
157
 
158
  # Calculate degree days
159
  daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
 
420
 
421
  def load_climate_data(self, country: str, city: str) -> Optional[Dict[str, Any]]:
422
  """Load climate data for a given country and city."""
 
 
 
 
 
 
 
 
 
423
  epw_file_path = None
 
424
  found_id = None
425
  for loc_id, loc_info in LOCATION_MAPPING.items():
426
+ if loc_info["city"] == city and (loc_info["state"] == "VIC" if city == "Melbourne RO" else True):
427
+ epw_file_name = f"AUS_VIC_Melbourne.AP.720520_TMY3.epw"
 
 
 
428
  if city == "Geelong":
429
+ epw_file_name = "AUS_VIC_Melbourne.AP.720520_TMY3.epw"
430
  elif city == "Sydney RO (Observatory Hill)":
431
+ epw_file_name = "AUS_NSW_Sydney.AP.947670_TMY3.epw"
 
432
 
433
  epw_file_path = os_join("data", AU_CCH_DIR, epw_file_name)
434
  found_id = loc_id
 
443
  if epw_data_df.empty:
444
  return None
445
 
446
+ # Pass the raw epw_data_df to ClimateLocation for its __init__ to process
447
+ # This ensures ClimateLocation's internal calculations (like design temps, degree days)
448
+ # are based on the full EPW data as originally intended.
 
 
449
 
450
  # Retrieve typical_extreme_periods and ground_temperatures
451
  typical_extreme_periods = self.get_typical_extreme_periods(epw_file_path)
452
  ground_temperatures = self.get_ground_temperatures(epw_file_path)
453
 
454
+ location_id = found_id if found_id else f"{country}_{city}".replace(" ", "_").lower()
455
+
456
  location = ClimateLocation(
457
+ epw_file=epw_data_df, # Pass the DataFrame here
458
  typical_extreme_periods=typical_extreme_periods,
459
  ground_temperatures=ground_temperatures,
460
  id=location_id,
461
  country=country,
462
+ state_province=LOCATION_MAPPING.get(found_id, {}).get("state", "N/A"),
463
  city=city,
464
+ latitude=epw_data_df.loc[0, 'latitude'],
465
  longitude=epw_data_df.loc[0, 'longitude'],
466
  elevation=epw_data_df.loc[0, 'elevation'],
467
  time_zone=epw_data_df.loc[0, 'time_zone'],
468
+ climate_zone="Unknown", # This should ideally be determined by a lookup based on location
469
+ # hourly_data is now handled internally by ClimateLocation's __init__
470
+ # based on the epw_file DataFrame passed above.
471
+ # We explicitly pass it as a keyword argument to satisfy the dataclass init if needed,
472
+ # but the __init__ will re-process it.
473
+ hourly_data=[] # Dummy, will be populated by ClimateLocation's __init__
474
  )
475
  self.add_location(location)
476
  return location.to_dict()
477
 
478
  def get_climate_zone_description(self, climate_zone_code: str) -> str:
479
  """Return a description for a given ASHRAE climate zone code."""
 
480
  zone_descriptions = {
481
  "1A": "Hot-Humid", "1B": "Hot-Dry",
482
  "2A": "Warm-Humid", "2B": "Warm-Dry",
 
495
  with open(epw_file_path, 'r') as f:
496
  for line in f:
497
  if line.startswith('COMMENT1'):
 
498
  match = re.search(r'TYPICAL EXTREME PERIODS: (.+)', line)
499
  if match:
500
  periods_str = match.group(1)
 
 
 
501
  typical_extreme_periods = {
502
  "summer_design_day": {"start_date": "07/21", "end_date": "07/21"},
503
  "winter_design_day": {"start_date": "01/21", "end_date": "01/21"}
504
  }
505
+ break
506
  except Exception as e:
507
  logger.warning(f"Could not extract typical/extreme periods from EPW comments: {e}")
508
  return typical_extreme_periods
 
510
  def get_ground_temperatures(self, epw_file_path: str) -> Dict[str, List[float]]:
511
  """Extract ground temperatures from EPW file comments."""
512
  ground_temperatures = {
513
+ "depth_0.5m": [15.0] * 12,
514
  "depth_1.0m": [14.0] * 12,
515
  "depth_2.0m": [13.0] * 12,
516
  "depth_4.0m": [12.0] * 12
 
519
  with open(epw_file_path, 'r') as f:
520
  for line in f:
521
  if line.startswith('COMMENT2'):
 
 
 
 
 
522
  pass
523
  except Exception as e:
524
  logger.warning(f"Could not extract ground temperatures from EPW comments: {e}")
 
526
 
527
  def save_climate_data(self, climate_data_dict: Dict[str, Any]):
528
  """Save climate data to a JSON file."""
 
 
529
  logger.info(f"Saving climate data (placeholder): {climate_data_dict['id']}")
 
530
  save_dir = "saved_data"
531
  os.makedirs(save_dir, exist_ok=True)
532
  file_path = os_join(save_dir, f"{climate_data_dict['id']}.json")
 
540
 
541
  def load_saved_climate_data(self) -> 'ClimateData':
542
  """Load saved climate data from JSON files."""
 
 
543
  load_dir = "saved_data"
544
  if os.path.exists(load_dir):
545
  for filename in os.listdir(load_dir):
 
548
  try:
549
  with open(file_path, 'r') as f:
550
  loc_dict = json.load(f)
 
 
551
  hourly_data = loc_dict.get("hourly_data", [])
552
 
553
+ # Create a dummy DataFrame with required columns for ClimateLocation __init__
554
+ # This ensures the __init__ can run and re-process the hourly_data
555
+ # and calculate design conditions, etc., as it expects a DataFrame.
 
 
 
556
  dummy_epw_df = pd.DataFrame({
557
  1: [d['month'] for d in hourly_data],
558
  2: [d['day'] for d in hourly_data],
 
565
  15: [d['diffuse_horizontal_radiation'] for d in hourly_data],
566
  20: [d['wind_direction'] for d in hourly_data],
567
  21: [d['wind_speed'] for d in hourly_data],
568
+ # Add metadata columns if they are expected by ClimateLocation's __init__
569
+ 'latitude': [loc_dict.get('latitude', 0.0)] * len(hourly_data),
570
+ 'longitude': [loc_dict.get('longitude', 0.0)] * len(hourly_data),
571
+ 'elevation': [loc_dict.get('elevation', 0.0)] * len(hourly_data),
572
+ 'time_zone': [loc_dict.get('time_zone', 0.0)] * len(hourly_data),
573
+ # Add other pvlib expected columns if they are used in ClimateLocation's __init__
574
+ 'temp_dry_bulb': [d['dry_bulb'] for d in hourly_data],
575
+ 'relative_humidity': [d['relative_humidity'] for d in hourly_data],
576
+ 'station_pressure': [d['atmospheric_pressure'] for d in hourly_data],
577
+ 'ghi': [d['global_horizontal_radiation'] for d in hourly_data],
578
+ 'dni': [d['direct_normal_radiation'] for d in hourly_data],
579
+ 'dhi': [d['diffuse_horizontal_radiation'] for d in hourly_data],
580
+ 'wind_direction': [d['wind_direction'] for d in hourly_data],
581
+ 'wind_speed': [d['wind_speed'] for d in hourly_data],
582
  })
 
 
 
 
583
 
584
  location = ClimateLocation(
585
  epw_file=dummy_epw_df, # Pass dummy DataFrame for __init__
 
968
 
969
  return fig
970
 
971
+ def display_climate_input(self, session_state: Dict[str, Any]):
972
+ """Display climate data input and selection in Streamlit."""
973
+ st.markdown(STYLE, unsafe_allow_html=True)
974
+ st.subheader("Climate Data Input")
975
+
976
+ # Country selection
977
+ country_options = sorted(self.countries)
978
+ selected_country = st.selectbox(
979
+ "Select Country",
980
+ country_options,
981
+ index=country_options.index(session_state["building_info"].get("country", "Australia"))
982
+ if session_state["building_info"].get("country", "Australia") in country_options
983
+ else 0,
984
+ key="climate_country_select"
985
+ )
986
+ session_state["building_info"]["country"] = selected_country
987
+
988
+ # State/Province selection
989
+ if selected_country and selected_country in self.country_states:
990
+ state_options = sorted(self.country_states[selected_country].keys())
991
+ selected_state = st.selectbox(
992
+ "Select State/Province",
993
+ state_options,
994
+ index=state_options.index(session_state["building_info"].get("state_province", "Victoria"))
995
+ if session_state["building_info"].get("state_province", "Victoria") in state_options
996
+ else 0,
997
+ key="climate_state_select"
998
+ )
999
+ session_state["building_info"]["state_province"] = selected_state
1000
+ else:
1001
+ selected_state = None
1002
+ st.warning("No states available for the selected country.")
1003
+
1004
+ # City selection
1005
+ if selected_state and selected_country in self.country_states and selected_state in self.country_states[selected_country]:
1006
+ city_options = sorted(self.country_states[selected_country][selected_state])
1007
+ selected_city = st.selectbox(
1008
+ "Select City",
1009
+ city_options,
1010
+ index=city_options.index(session_state["building_info"].get("city", "Geelong"))
1011
+ if session_state["building_info"].get("city", "Geelong") in city_options
1012
+ else 0,
1013
+ key="climate_city_select"
1014
+ )
1015
+ session_state["building_info"]["city"] = selected_city
1016
+ else:
1017
+ selected_city = None
1018
+ st.warning("No cities available for the selected state/province.")
1019
+
1020
+ # Load data button
1021
+ if st.button("Load Climate Data"):
1022
+ if selected_country and selected_city:
1023
+ with st.spinner(f"Loading climate data for {selected_city}, {selected_country}..."):
1024
+ loaded_data = self.load_climate_data(selected_country, selected_city)
1025
+ if loaded_data:
1026
+ if self.validate_climate_data(loaded_data):
1027
+ session_state["climate_data"] = loaded_data
1028
+ st.success(f"Climate data loaded successfully for {selected_city}, {selected_country}!")
1029
+ logger.info(f"Climate data loaded for {selected_city}, {selected_country}.")
1030
+ else:
1031
+ st.error("Loaded climate data failed validation. Please check the data source.")
1032
+ logger.error("Loaded climate data failed validation.")
1033
+ else:
1034
+ st.error(f"Failed to load climate data for {selected_city}, {selected_country}.")
1035
+ logger.error(f"Failed to load climate data for {selected_city}, {selected_country}.")
1036
+ else:
1037
+ st.warning("Please select a country and city to load climate data.")
1038
+
1039
+ # Display loaded data if available
1040
+ if "climate_data" in session_state and session_state["climate_data"]:
1041
+ st.subheader("Loaded Climate Data Summary")
1042
+ climate_info = session_state["climate_data"]
1043
+
1044
+ st.markdown(f"""
1045
+ <div class="markdown-text">
1046
+ <h3>Location Details:</h3>
1047
+ <ul>
1048
+ <li><strong>ID:</strong> {climate_info.get('id', 'N/A')}</li>
1049
+ <li><strong>Country:</strong> {climate_info.get('country', 'N/A')}</li>
1050
+ <li><strong>State/Province:</strong> {climate_info.get('state_province', 'N/A')}</li>
1051
+ <li><strong>City:</strong> {climate_info.get('city', 'N/A')}</li>
1052
+ <li><strong>Latitude:</strong> {climate_info.get('latitude', 'N/A'):.2f}°</li>
1053
+ <li><strong>Longitude:</strong> {climate_info.get('longitude', 'N/A'):.2f}°</li>
1054
+ <li><strong>Elevation:</strong> {climate_info.get('elevation', 'N/A'):.1f} m</li>
1055
+ <li><strong>Time Zone:</strong> UTC{climate_info.get('time_zone', 'N/A'):.1f}</li>
1056
+ <li><strong>Climate Zone:</strong> {climate_info.get('climate_zone', 'N/A')} ({self.get_climate_zone_description(climate_info.get('climate_zone', ''))})</li>
1057
+ </ul>
1058
+
1059
+ <h3>Design Conditions:</h3>
1060
+ <ul>
1061
+ <li><strong>Heating Degree Days (Base 18°C):</strong> {climate_info.get('heating_degree_days', 'N/A')} HDD</li>
1062
+ <li><strong>Cooling Degree Days (Base 18°C):</strong> {climate_info.get('cooling_degree_days', 'N/A')} CDD</li>
1063
+ <li><strong>Winter Design Temp (99.6%):</strong> {climate_info.get('winter_design_temp', 'N/A'):.1f}°C</li>
1064
+ <li><strong>Summer Design Dry-Bulb (0.4%):</strong> {climate_info.get('summer_design_temp_db', 'N/A'):.1f}°C</li>
1065
+ <li><strong>Summer Design Wet-Bulb (0.4%):</strong> {climate_info.get('summer_design_temp_wb', 'N/A'):.1f}°C</li>
1066
+ <li><strong>Summer Daily Range:</strong> {climate_info.get('summer_daily_range', 'N/A'):.1f}°C</li>
1067
+ <li><strong>Mean Wind Speed:</strong> {climate_info.get('wind_speed', 'N/A'):.1f} m/s</li>
1068
+ <li><strong>Mean Atmospheric Pressure:</strong> {climate_info.get('pressure', 'N/A'):.1f} Pa</li>
1069
+ </ul>
1070
+ </div>
1071
+ """, unsafe_allow_html=True)
1072
+
1073
+ # Display Typical/Extreme Periods
1074
+ st.subheader("Typical and Extreme Periods")
1075
+ if climate_info.get("typical_extreme_periods"):
1076
+ for period_name, details in climate_info["typical_extreme_periods"].items():
1077
+ st.write(f"**{period_name.replace('_', ' ').title()}:** Start Date: {details.get('start_date', 'N/A')}, End Date: {details.get('end_date', 'N/A')}")
1078
+ else:
1079
+ st.info("No typical and extreme periods data available for this location.")
1080
+
1081
+ # Display Ground Temperatures
1082
+ st.subheader("Ground Temperatures (Monthly Average)")
1083
+ if climate_info.get("ground_temperatures"):
1084
+ temp_df = pd.DataFrame(climate_info["ground_temperatures"])
1085
+ temp_df.index = [f"Month {i+1}" for i in range(12)]
1086
+ st.dataframe(temp_df)
1087
+ else:
1088
+ st.info("No ground temperature data available for this location.")
1089
+
1090
+ # Option to save data
1091
+ if st.button("Save Climate Data"):
1092
+ self.save_climate_data(climate_info)
1093
+
1094
+ st.subheader("Psychrometric Chart")
1095
+ # Generate and display the psychrometric chart
1096
+ psych_fig = self.plot_psychrometric_chart(climate_info["id"], session_state)
1097
+ st.plotly_chart(psych_fig, use_container_width=True)
1098
+
1099
  # --- Original if __name__ == "__main__": block from your file ---
1100
  if __name__ == "__main__":
1101
  climate_data = ClimateData()
1102
+ session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}