ArchEGraph's picture
Update README and app.py for energy file handling and visualization details
74d207f
from __future__ import annotations
import csv
import random
import time
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from pathlib import PurePosixPath
import gradio as gr
from huggingface_hub import hf_hub_download
from visualization.building import visualize_graph, visualize_graph_overlap
from visualization.energy import visualize_energy
from visualization.geometry import visualize_geometry
from visualization.weather import visualize_weather
DATASET_REPO_ID = "ArchEGraph/ArchEGraph-demo"
OUTPUT_ROOT = Path("/tmp/archegraph_outputs")
DEFAULT_ENERGY_ZONE_INDEX = 0
DEFAULT_WEATHER_WINDOW_START_HOUR = 1
DEFAULT_WEATHER_WINDOW_HOURS = 24
@dataclass(frozen=True)
class SampleRecord:
sample_id: str
weather_id: str
building_id: str
energy_file: str
n_steps: int
n_spaces: int
@lru_cache(maxsize=1)
def _manifest_index() -> dict[str, SampleRecord]:
manifest_path = hf_hub_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
filename="manifest.csv",
)
out: dict[str, SampleRecord] = {}
with Path(manifest_path).open("r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
sample_id = (row.get("sample_id") or "").strip()
weather_id = (row.get("weather_id") or "").strip()
building_id = (row.get("building_id") or "").strip()
energy_file = (row.get("energy_file") or "").strip()
if not sample_id or not weather_id or not energy_file:
continue
if not building_id:
building_id = sample_id.split("__", 1)[0].lstrip("0") or "0"
n_steps = int(row.get("n_steps") or 0)
n_spaces = int(row.get("n_spaces") or 0)
out[sample_id] = SampleRecord(
sample_id=sample_id,
weather_id=weather_id,
building_id=building_id,
energy_file=energy_file,
n_steps=n_steps,
n_spaces=n_spaces,
)
if not out:
raise RuntimeError("manifest.csv is empty or invalid.")
return out
def _numeric_sort_key(value: str) -> tuple[int, str]:
text = (value or "").strip()
if text.isdigit():
return (0, f"{int(text):09d}")
return (1, text.lower())
@lru_cache(maxsize=1)
def _weather_to_buildings() -> dict[str, list[str]]:
mapping: dict[str, set[str]] = {}
for rec in _manifest_index().values():
mapping.setdefault(rec.weather_id, set()).add(rec.building_id)
return {k: sorted(v, key=_numeric_sort_key) for k, v in mapping.items()}
@lru_cache(maxsize=1)
def _pair_to_sample() -> dict[tuple[str, str], SampleRecord]:
out: dict[tuple[str, str], SampleRecord] = {}
for rec in _manifest_index().values():
out[(rec.weather_id, rec.building_id)] = rec
return out
def _weather_choices() -> list[str]:
return sorted(_weather_to_buildings().keys(), key=lambda x: x.lower())
def _building_choices(weather_id: str) -> list[str]:
return _weather_to_buildings().get((weather_id or "").strip(), [])
def _default_selection() -> tuple[str, str]:
weather_choices = _weather_choices()
if not weather_choices:
raise RuntimeError("No weather options found in manifest")
weather = weather_choices[0]
buildings = _building_choices(weather)
if not buildings:
raise RuntimeError(f"No building options for weather '{weather}'")
return weather, buildings[0]
def _safe_dropdown_defaults() -> tuple[list[str], str | None, list[str], str | None]:
try:
weather_choices = _weather_choices()
if not weather_choices:
return [], None, [], None
weather, building = _default_selection()
building_choices = _building_choices(weather)
return weather_choices, weather, building_choices, building
except Exception:
return [], None, [], None
def _resolve_record(weather_id: str, building_id: str) -> SampleRecord:
weather = (weather_id or "").strip()
building = (building_id or "").strip()
pair_map = _pair_to_sample()
rec = pair_map.get((weather, building))
if rec is not None:
return rec
weather_opts = _weather_choices()
if not weather_opts:
raise ValueError("No weather options available")
if weather not in weather_opts:
raise ValueError(f"Unknown weather city '{weather}'. Example values: {', '.join(weather_opts[:8])}")
building_opts = _building_choices(weather)
raise ValueError(
f"Unknown building id '{building}' for city '{weather}'. "
f"Available building ids: {', '.join(building_opts[:12])}"
)
def _repo_dataset_path(*parts: str) -> str:
clean_parts: list[str] = []
for part in parts:
text = (part or "").replace("\\", "/").strip("/")
if text:
clean_parts.extend(segment for segment in text.split("/") if segment and segment != ".")
return str(PurePosixPath(*clean_parts))
def _download_modalities(record: SampleRecord) -> tuple[Path, Path, Path, Path]:
building_key = record.sample_id.split("__", 1)[0]
energy_relpath = record.energy_file.replace("\\", "/").lstrip("/")
if energy_relpath.startswith("energy/"):
energy_relpath = energy_relpath[len("energy/") :]
geometry_npz = hf_hub_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
filename=_repo_dataset_path("geometry", f"{building_key}.npz"),
)
graph_npz = hf_hub_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
filename=_repo_dataset_path("building", f"{building_key}.npz"),
)
weather_npz = hf_hub_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
filename=_repo_dataset_path("weather", f"{record.weather_id}.npz"),
)
energy_npz = hf_hub_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
filename=_repo_dataset_path("energy", energy_relpath),
)
return Path(geometry_npz), Path(graph_npz), Path(weather_npz), Path(energy_npz)
def _output_paths(sample_id: str) -> tuple[Path, Path, Path, Path, Path]:
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
safe = sample_id.replace("/", "_").replace("\\", "_")
run_dir = OUTPUT_ROOT / f"{safe}_{int(time.time() * 1000)}"
run_dir.mkdir(parents=True, exist_ok=True)
return (
run_dir / "geometry.png",
run_dir / "graph.png",
run_dir / "overlap.png",
run_dir / "weather.png",
run_dir / "energy.png",
)
def update_building_dropdown(weather_id: str) -> gr.update:
buildings = _building_choices(weather_id)
value = buildings[0] if buildings else None
return gr.update(choices=buildings, value=value)
def render_sample(
weather_id: str,
building_id: str,
energy_zone_index: int,
use_custom_window: bool,
window_start_hour: int,
window_hours: int,
) -> tuple[str, str, str, str, str, str]:
record = _resolve_record(weather_id=weather_id, building_id=building_id)
if use_custom_window:
start_hour = max(1, int(window_start_hour))
hours = max(1, int(window_hours))
else:
start_hour = DEFAULT_WEATHER_WINDOW_START_HOUR
hours = DEFAULT_WEATHER_WINDOW_HOURS
zone_idx = max(0, int(energy_zone_index)) if energy_zone_index is not None else DEFAULT_ENERGY_ZONE_INDEX
try:
geometry_npz, graph_npz, weather_npz, energy_npz = _download_modalities(record)
out_geometry, out_graph, out_overlap, out_weather, out_energy = _output_paths(record.sample_id)
visualize_geometry(geometry_npz=geometry_npz, output_png=out_geometry)
visualize_graph(graph_npz=graph_npz, geometry_npz=geometry_npz, output_png=out_graph)
visualize_graph_overlap(graph_npz=graph_npz, geometry_npz=geometry_npz, energy_npz=energy_npz, output_png=out_overlap)
visualize_weather(
weather_npz=weather_npz,
output_png=out_weather,
start_hour=start_hour,
window_hours=hours,
)
visualize_energy(
energy_npz=energy_npz,
output_png=out_energy,
zone_index=zone_idx,
start_hour=start_hour,
window_hours=hours,
)
except Exception as exc:
raise gr.Error(f"Failed to render sample {record.sample_id}: {exc}") from exc
if use_custom_window:
window_text = f"custom window: start={start_hour}, hours={hours}"
else:
window_text = "default window: Jan-1 first 24 hours"
summary = (
f"Rendered **{record.sample_id}** from `{DATASET_REPO_ID}` \n"
f"weather_id: `{record.weather_id}` \n"
f"building_id: `{record.building_id}` \n"
f"n_steps: `{record.n_steps}` \n"
f"n_spaces: `{record.n_spaces}` \n"
f"zone_index: `{zone_idx}` \n"
f"{window_text}"
)
return str(out_geometry), str(out_graph), str(out_overlap), str(out_weather), str(out_energy), summary
def pick_random_sample() -> tuple[str, gr.update]:
rec = random.choice(list(_manifest_index().values()))
choices = _building_choices(rec.weather_id)
return rec.weather_id, gr.update(choices=choices, value=rec.building_id)
def _startup_note() -> str:
try:
total = len(_manifest_index())
weather_count = len(_weather_choices())
return f"Manifest loaded: {total} samples, {weather_count} weather cities from {DATASET_REPO_ID}."
except Exception as exc:
return f"Manifest will be loaded lazily on first run. Reason: {exc}"
default_weathers, default_weather, default_buildings, default_building = _safe_dropdown_defaults()
APP_CSS = """
.gradio-container {
max-width: 1920px !important;
}
#ctrl-row-1, #ctrl-row-2, #ctrl-row-3 {
gap: 8px !important;
}
#ctrl-row-1 .gr-block, #ctrl-row-2 .gr-block, #ctrl-row-3 .gr-block {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
#status-box {
margin-top: 2px !important;
margin-bottom: 4px !important;
}
#viz-row-all {
gap: 8px !important;
}
#viz-row-all img {
object-fit: contain !important;
}
"""
with gr.Blocks(title="ArchEGraph Visualizer", css=APP_CSS) as demo:
gr.Markdown(
"# ArchEGraph Visualizer\n"
"Visualize geometry, graph, weather and energy files from "
"[ArchEGraph/ArchEGraph-demo](https://huggingface.co/datasets/ArchEGraph/ArchEGraph-demo)."
)
gr.Markdown(_startup_note())
with gr.Row(elem_id="ctrl-row-1"):
weather_dropdown = gr.Dropdown(
label="Weather City",
choices=default_weathers,
value=default_weather,
allow_custom_value=False,
scale=2,
)
building_dropdown = gr.Dropdown(
label="Building ID",
choices=default_buildings,
value=default_building,
allow_custom_value=True,
scale=2,
)
energy_zone_index_input = gr.Slider(
label="Energy Zone Index",
minimum=0,
maximum=24,
step=1,
value=DEFAULT_ENERGY_ZONE_INDEX,
scale=1,
)
with gr.Row(elem_id="ctrl-row-2"):
use_custom_window_input = gr.Checkbox(
label="Custom Window",
value=False,
scale=1,
)
window_start_hour_input = gr.Slider(
label="Start Hour",
minimum=1,
maximum=8760,
step=1,
value=DEFAULT_WEATHER_WINDOW_START_HOUR,
scale=2,
)
window_hours_input = gr.Slider(
label="Window Hours",
minimum=1,
maximum=8760,
step=1,
value=DEFAULT_WEATHER_WINDOW_HOURS,
scale=2,
)
with gr.Row(elem_id="ctrl-row-3"):
run_btn = gr.Button("Visualize", variant="primary")
random_btn = gr.Button("Pick Random Sample")
status_md = gr.Markdown(elem_id="status-box")
with gr.Row(elem_id="viz-row-all"):
geometry_img = gr.Image(label="Geometry", type="filepath", height=215)
graph_img = gr.Image(label="Building", type="filepath", height=215)
overlap_img = gr.Image(label="Overlap", type="filepath", height=215)
weather_img = gr.Image(label="Weather (line)", type="filepath", height=215)
energy_img = gr.Image(label="Energy (selected zone)", type="filepath", height=215)
weather_dropdown.change(
fn=update_building_dropdown,
inputs=[weather_dropdown],
outputs=[building_dropdown],
)
run_btn.click(
fn=render_sample,
inputs=[
weather_dropdown,
building_dropdown,
energy_zone_index_input,
use_custom_window_input,
window_start_hour_input,
window_hours_input,
],
outputs=[geometry_img, graph_img, overlap_img, weather_img, energy_img, status_md],
api_name="render_sample",
)
random_btn.click(
fn=pick_random_sample,
inputs=None,
outputs=[weather_dropdown, building_dropdown],
api_name="pick_random_sample",
)
demo.queue()
if __name__ == "__main__":
demo.launch()